Compare commits

..

1 Commits

Author SHA1 Message Date
Marie
ce894868e2 Merge ed6f04b6d4 into 11cc209025 2023-07-30 21:25:40 +02:00
14 changed files with 101 additions and 201 deletions

View File

@@ -1,16 +1,10 @@
{ {
"cSpell.words": [ "cSpell.words": [
"Brainz",
"Castlabs",
"flac", "flac",
"Flatpak",
"geqnfr", "geqnfr",
"hifi", "hifi",
"listenbrainz",
"playpause", "playpause",
"rescrobbler", "rescrobbler",
"scrobble",
"scrobbling",
"Songwhip", "Songwhip",
"trackid", "trackid",
"tracklist", "tracklist",

View File

@@ -4,10 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 5.5.0
- ListenBrainz integration added (thanks @Mar0xy)
## 5.4.0 ## 5.4.0
- Removed Windows builds (from publishes) as they don't work anymore. - Removed Windows builds (from publishes) as they don't work anymore.

View File

@@ -48,7 +48,6 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- Custom [integrations](#integrations) - Custom [integrations](#integrations)
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`) - [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847)) - AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
- [ListenBrainz](https://listenbrainz.org/?redirect=false) integration
## Contributions ## Contributions
@@ -134,7 +133,7 @@ To install and work with the code on this project follow these steps:
## Integrations ## Integrations
tidal-hifi comes with several integrations out of the box. Tidal-hifi comes with several integrations out of the box.
You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab. You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab.
![integrations menu, showing a list of integrations](./docs/images/integrations.png) ![integrations menu, showing a list of integrations](./docs/images/integrations.png)
@@ -154,11 +153,11 @@ Not included:
The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4). The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4).
However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled). However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled).
For now, that will be the default workaround. For now that will be the default workaround.
#### DRM not working on Windows #### DRM not working on Windows
Most Windows users run into DRM issues when trying to use tidal-hifi. Most Windows users run into DRM issues when trying to use Tidal-hifi.
Nothing I can do about that I'm afraid... Tidal is working on removing/changing DRM so when they finish with that we can give it another shot. Nothing I can do about that I'm afraid... Tidal is working on removing/changing DRM so when they finish with that we can give it another shot.
## Special thanks to ## Special thanks to

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "5.5.0", "version": "5.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "5.5.0", "version": "5.4.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.10", "@electron/remote": "^2.0.10",
@@ -9340,4 +9340,4 @@
} }
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "5.5.0", "version": "5.4.0",
"description": "Tidal on Electron with widevine(hifi) support", "description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js", "main": "ts-dist/main.js",
"scripts": { "scripts": {
@@ -72,4 +72,4 @@
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },
"prettier": "@mastermindzh/prettier-config" "prettier": "@mastermindzh/prettier-config"
} }

View File

@@ -0,0 +1,4 @@
export const statuses = {
playing: "playing",
paused: "paused",
};

View File

@@ -1,35 +1,12 @@
import axios from "axios"; import axios from "axios";
import { ipcRenderer } from "electron"; import { settingsStore } from "../../scripts/settings";
import Store from "electron-store";
import { settings } from "../../constants/settings"; import { settings } from "../../constants/settings";
import { MediaStatus } from "../../models/mediaStatus"; import { MediaStatus } from "../../models/mediaStatus";
import { settingsStore } from "../../scripts/settings"; import Store from "electron-store";
import { Logger } from "../logger";
import { StoreData } from "./models/storeData";
const ListenBrainzStore = new Store({ name: "listenbrainz" }); const ListenBrainzStore = new Store({name: "listenbrainz"});
export const ListenBrainzConstants = {
oldData: "oldData",
};
export class ListenBrainz { export class ListenBrainz {
/**
* Create the object to store old information in the Store :)
* @param title
* @param artists
* @param duration
* @returns data passed along in an object + a "listenedAt" key with the current time
*/
private static constructStoreData(title: string, artists: string, duration: number): StoreData {
return {
listenedAt: Math.floor(new Date().getTime() / 1000),
title,
artists,
duration,
};
}
/** /**
* Call the ListenBrainz API and create playing now payload and scrobble old song * Call the ListenBrainz API and create playing now payload and scrobble old song
* @param title * @param title
@@ -37,98 +14,70 @@ export class ListenBrainz {
* @param status * @param status
* @param duration * @param duration
*/ */
public static async scrobble( public static async scrobble(title: string, artists: string, status: string, duration: number): Promise<any> {
title: string,
artists: string,
status: string,
duration: number
): Promise<void> {
try { try {
if (status === MediaStatus.paused) { if (status == MediaStatus.paused) {
return; return false;
} else { } else {
// Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area // Fetches the OldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData; const OldData = ListenBrainzStore.get("OldData") as string[];
const playing_data = { const playing_data = {
listen_type: "playing_now", listen_type: "playing_now",
payload: [ payload: [
{ {
track_metadata: { track_metadata: {
additional_info: { additional_info: {
media_player: "Tidal Hi-Fi", media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi", submission_client: "Tidal Hi-Fi",
music_service: "tidal.com", music_service: "tidal.com",
duration: duration, duration: duration,
}, },
artist_name: artists, artist_name: artists,
track_name: title, track_name: title,
}, }
}, }
], ]
}; };
await axios.post( await axios.post(`${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, playing_data, {
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`, headers:{
playing_data, "Content-Type": "application/json",
{ "Authorization": `Token ${settingsStore.get(settings.ListenBrainz.token)}`
headers: { }
"Content-Type": "application/json", });
Authorization: `Token ${settingsStore.get<string, string>( if (!OldData) {
settings.ListenBrainz.token ListenBrainzStore.set("OldData", [Math.floor(new Date().getTime() / 1000), title, artists, duration]);
)}`, } else if (OldData[1] != title) {
},
}
);
if (!oldData) {
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
} else {
if (oldData.title !== title) {
// This constructs the data required to scrobble the data after the song finishes // This constructs the data required to scrobble the data after the song finishes
const scrobble_data = { const scrobble_data = {
listen_type: "single", listen_type: "single",
payload: [ payload: [
{ {
listened_at: oldData.listenedAt, listened_at: OldData[0],
track_metadata: { track_metadata: {
additional_info: { additional_info: {
media_player: "Tidal Hi-Fi", media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi", submission_client: "Tidal Hi-Fi",
music_service: "listen.tidal.com", music_service: "listen.tidal.com",
duration: oldData.duration, duration: OldData[3],
}, },
artist_name: oldData.artists, artist_name: OldData[2],
track_name: oldData.title, track_name: OldData[1],
}, }
}, }
], ]
}; };
await axios.post( await axios.post(`${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, scrobble_data, {
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`, headers:{
scrobble_data, "Content-Type": "application/json",
{ "Authorization": `Token ${settingsStore.get(settings.ListenBrainz.token)}`
headers: { }
"Content-Type": "application/json", });
Authorization: `Token ${settingsStore.get<string, string>( ListenBrainzStore.set("OldData", [Math.floor(new Date().getTime() / 1000), title, artists, duration]);
settings.ListenBrainz.token
)}`,
},
}
);
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
}
} }
} }
} catch (error) { } catch (error) {
const logger = new Logger(ipcRenderer); console.log(JSON.stringify(error));
logger.log(JSON.stringify(error));
} }
} }
} }
export { ListenBrainzStore };

View File

@@ -1,9 +0,0 @@
/**
* Data saved for ListenBrainz
*/
export interface StoreData {
listenedAt: number;
title: string;
artists: string;
duration: number;
}

View File

@@ -26,7 +26,8 @@ export class Logger {
public log(content: string, object: object = {}) { public log(content: string, object: object = {}) {
if (this.ipcRenderer) { if (this.ipcRenderer) {
this.ipcRenderer.send(globalEvents.log, { content, object }); this.ipcRenderer.send(globalEvents.log, { content, object });
} else {
console.log(`${content} \n ${JSON.stringify(object, null, 2)}`);
} }
console.log(`${content} \n ${JSON.stringify(object, null, 2)}`);
} }
} }

View File

@@ -58,7 +58,7 @@ function setFlags() {
} }
/** /**
* Update the menuBarVisibility according to the store value * Update the menuBarVisbility according to the store value
* *
*/ */
function syncMenuBarWithStore() { function syncMenuBarWithStore() {

View File

@@ -211,12 +211,9 @@
<span class="switch__slider"></span> <span class="switch__slider"></span>
</label> </label>
</div> </div>
</div>
<div class="group">
<p class="group__title">ListenBrainz</p>
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Enable ListenBrainz</h4> <h4>ListenBrainz</h4>
<p>Scrobble your listens directly to ListenBrainz.</p> <p>Scrobble your listens directly to ListenBrainz.</p>
</div> </div>
<label class="switch"> <label class="switch">

View File

@@ -4,25 +4,20 @@ import fs from "fs";
import Player from "mpris-service"; import Player from "mpris-service";
import { globalEvents } from "./constants/globalEvents"; import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings"; import { settings } from "./constants/settings";
import { statuses } from "./constants/statuses";
import { Songwhip } from "./features/songwhip/songwhip"; import { Songwhip } from "./features/songwhip/songwhip";
import { import { ListenBrainz } from "./features/listenbrainz/listenbrainz";
ListenBrainz,
ListenBrainzConstants,
ListenBrainzStore,
} from "./features/listenbrainz/listenbrainz";
import { Options } from "./models/options"; import { Options } from "./models/options";
import { downloadFile } from "./scripts/download"; import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys"; import { addHotkey } from "./scripts/hotkeys";
import { settingsStore } from "./scripts/settings"; import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions"; import { setTitle } from "./scripts/window-functions";
import { StoreData } from "./features/listenbrainz/models/storeData";
import { MediaStatus } from "./models/mediaStatus";
const notificationPath = `${app.getPath("userData")}/notification.jpg`; const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "Tidal Hifi"; const appName = "Tidal Hifi";
let currentSong = ""; let currentSong = "";
let player: Player; let player: Player;
let currentPlayStatus = MediaStatus.paused; let currentPlayStatus = statuses.paused;
const elements = { const elements = {
play: '*[data-test="play"]', play: '*[data-test="play"]',
@@ -111,7 +106,7 @@ const elements = {
window.location.href.includes("/playlist/") || window.location.href.includes("/playlist/") ||
window.location.href.includes("/mix/") window.location.href.includes("/mix/")
) { ) {
if (currentPlayStatus === MediaStatus.playing) { if (currentPlayStatus === statuses.playing) {
// find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell. // find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell.
// document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent // document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent
const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem); const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem);
@@ -184,7 +179,7 @@ function addCustomCss() {
* make sure it returns a number, if not use the default * make sure it returns a number, if not use the default
*/ */
function getUpdateFrequency() { function getUpdateFrequency() {
const storeValue = settingsStore.get<string, number>(settings.updateFrequency); const storeValue = settingsStore.get(settings.updateFrequency) as number;
const defaultValue = 500; const defaultValue = 500;
if (!isNaN(storeValue)) { if (!isNaN(storeValue)) {
@@ -207,11 +202,6 @@ function playPause() {
} }
} }
/**
* Clears the old listenbrainz data on launch
*/
ListenBrainzStore.clear();
/** /**
* Add hotkeys for when tidal is focused * Add hotkeys for when tidal is focused
* Reflects the desktop hotkeys found on: * Reflects the desktop hotkeys found on:
@@ -285,7 +275,7 @@ function handleLogout() {
defaultId: 2, defaultId: 2,
}) })
.then((result: { response: number }) => { .then((result: { response: number }) => {
if (logoutOptions.indexOf("Yes, please") === result.response) { if (logoutOptions.indexOf("Yes, please") == result.response) {
for (let i = 0; i < window.localStorage.length; i++) { for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i); const key = window.localStorage.key(i);
if (key.startsWith("_TIDAL_activeSession")) { if (key.startsWith("_TIDAL_activeSession")) {
@@ -327,8 +317,6 @@ function addIPCEventListeners() {
case globalEvents.pause: case globalEvents.pause:
elements.click("pause"); elements.click("pause");
break; break;
default:
break;
} }
}); });
}); });
@@ -343,9 +331,9 @@ function getCurrentlyPlayingStatus() {
// if pause button is visible tidal is playing // if pause button is visible tidal is playing
if (pause) { if (pause) {
status = MediaStatus.playing; status = statuses.playing;
} else { } else {
status = MediaStatus.paused; status = statuses.paused;
} }
return status; return status;
} }
@@ -370,41 +358,19 @@ function updateMediaInfo(options: Options, notify: boolean) {
if (settingsStore.get(settings.notifications) && notify) { if (settingsStore.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show(); new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
} }
updateMpris(options); if (player) {
updateListenBrainz(options); player.metadata = {
} ...player.metadata,
} ...{
"xesam:title": options.title,
function updateMpris(options: Options) { "xesam:artist": [options.artists],
if (player) { "xesam:album": options.album,
player.metadata = { "mpris:artUrl": options.image,
...player.metadata, "mpris:length": convertDuration(options.duration) * 1000 * 1000,
...{ "mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
"xesam:title": options.title, },
"xesam:artist": [options.artists], };
"xesam:album": options.album, player.playbackStatus = options.status == statuses.paused ? "Paused" : "Playing";
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
};
player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing";
}
}
function updateListenBrainz(options: Options) {
if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
if (
(!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== options.title)
) {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
);
} }
} }
} }
@@ -503,6 +469,9 @@ setInterval(function () {
updateMediaInfo(options, titleOrArtistsChanged); updateMediaInfo(options, titleOrArtistsChanged);
if (titleOrArtistsChanged) { if (titleOrArtistsChanged) {
updateMediaSession(options); updateMediaSession(options);
if (settingsStore.get(settings.ListenBrainz.enabled)) {
ListenBrainz.scrobble(options.title, options.artists, options.status, convertDuration(options.duration));
}
} }
}); });

View File

@@ -1,11 +1,11 @@
import { BrowserWindow, dialog } from "electron"; import { BrowserWindow, dialog } from "electron";
import express, { Response } from "express"; import express, { Response } from "express";
import fs from "fs"; import fs from "fs";
import { settings } from "../constants/settings";
import { MediaStatus } from "../models/mediaStatus";
import { globalEvents } from "./../constants/globalEvents"; import { globalEvents } from "./../constants/globalEvents";
import { statuses } from "./../constants/statuses";
import { mediaInfo } from "./mediaInfo"; import { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings"; import { settingsStore } from "./settings";
import { settings } from "../constants/settings";
/** /**
* Function to enable tidal-hifi's express api * Function to enable tidal-hifi's express api
@@ -44,7 +44,7 @@ export const startExpress = (mainWindow: BrowserWindow) => {
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next)); expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous)); expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
expressApp.get("/playpause", (req, res) => { expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status === MediaStatus.playing) { if (mediaInfo.status == statuses.playing) {
handleGlobalEvent(res, globalEvents.pause); handleGlobalEvent(res, globalEvents.pause);
} else { } else {
handleGlobalEvent(res, globalEvents.play); handleGlobalEvent(res, globalEvents.play);

View File

@@ -1,12 +1,12 @@
import { MediaInfo } from "../models/mediaInfo"; import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus"; import { statuses } from "./../constants/statuses";
export const mediaInfo = { export const mediaInfo = {
title: "", title: "",
artists: "", artists: "",
album: "", album: "",
icon: "", icon: "",
status: MediaStatus.paused as string, status: statuses.paused,
url: "", url: "",
current: "", current: "",
duration: "", duration: "",