Compare commits

..

No commits in common. "f553fe98e7bd0b7ccc4c3b8a0ee6b5fe0edde450" and "d3e126d86b3e4883ba9ea8fa0e69f05e402bc13a" have entirely different histories.

12 changed files with 213 additions and 251 deletions

80
package-lock.json generated
View File

@ -12,11 +12,11 @@
"@electron/remote": "^2.1.2",
"@types/swagger-jsdoc": "^6.0.4",
"@xhayper/discord-rpc": "^1.2.0",
"axios": "^1.7.9",
"axios": "^1.7.7",
"cors": "^2.8.5",
"electron-store": "^8.2.0",
"express": "^4.21.2",
"hotkeys-js": "^3.13.9",
"express": "^4.21.1",
"hotkeys-js": "^3.13.7",
"mpris-service": "^2.1.2",
"request": "^2.88.2",
"sass": "^1.79.4",
@ -414,9 +414,9 @@
}
},
"node_modules/@discordjs/rest": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz",
"integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.0.tgz",
"integrity": "sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
@ -424,10 +424,10 @@
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.3",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.37.119",
"discord-api-types": "0.37.97",
"magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3",
"undici": "6.21.1"
"undici": "6.19.8"
},
"engines": {
"node": ">=18"
@ -436,6 +436,12 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/discord-api-types": {
"version": "0.37.97",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz",
"integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==",
"license": "MIT"
},
"node_modules/@discordjs/util": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
@ -2358,9 +2364,9 @@
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg=="
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@ -3190,11 +3196,10 @@
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -3485,9 +3490,9 @@
}
},
"node_modules/discord-api-types": {
"version": "0.37.119",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
"integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==",
"version": "0.37.103",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.103.tgz",
"integrity": "sha512-r+qitxXKe2l6KFw5odPdZSSqdEou+7eNC7BfbZ7mny5Me/K06wCTeKUMVeH/YsI9+4QQudskeQ307kr/7ppQ1A==",
"license": "MIT"
},
"node_modules/dmg-builder": {
@ -4162,9 +4167,9 @@
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@ -4186,7 +4191,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -4201,10 +4206,6 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@ -5090,10 +5091,9 @@
}
},
"node_modules/hotkeys-js": {
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz",
"integrity": "sha512-3TRCj9u9KUH6cKo25w4KIdBfdBfNRjfUwrljCLDC2XhmPDG0SjAZFcFZekpUZFmXzfYoGhFDcdx2gX/vUVtztQ==",
"license": "MIT",
"version": "3.13.7",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.7.tgz",
"integrity": "sha512-ygFIdTqqwG4fFP7kkiYlvayZppeIQX2aPpirsngkv1xM1lP0piDY5QEh68nQnIKvz64hfocxhBaD/uK3sSK1yQ==",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
@ -6218,9 +6218,9 @@
"optional": true
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
@ -6228,7 +6228,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -6686,10 +6685,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/path-type": {
"version": "4.0.0",
@ -8821,9 +8819,9 @@
"dev": true
},
"node_modules/undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
"license": "MIT",
"engines": {
"node": ">=18.17"

View File

@ -44,11 +44,11 @@
"@electron/remote": "^2.1.2",
"@types/swagger-jsdoc": "^6.0.4",
"@xhayper/discord-rpc": "^1.2.0",
"axios": "^1.7.9",
"axios": "^1.7.7",
"cors": "^2.8.5",
"electron-store": "^8.2.0",
"express": "^4.21.2",
"hotkeys-js": "^3.13.9",
"express": "^4.21.1",
"hotkeys-js": "^3.13.7",
"mpris-service": "^2.1.2",
"request": "^2.88.2",
"sass": "^1.79.4",

View File

@ -1,6 +0,0 @@
export type DomControllerOptions = {
/**
* Interval that tidal-hifi scrapes the dom for information
*/
refreshInterval: number;
};

View File

@ -1,15 +1,18 @@
import { convertDurationToSeconds } from "../../features/time/parse";
import { MediaInfo } from "../../models/mediaInfo";
import { MediaStatus } from "../../models/mediaStatus";
import { RepeatState } from "../../models/repeatState";
import { TidalController } from "../TidalController";
import { DomControllerOptions } from "./DomControllerOptions";
import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
import { TidalController } from "./TidalController";
export class DomTidalController implements TidalController<DomControllerOptions> {
private updateSubscriber: (state: Partial<MediaInfo>) => void;
private currentlyPlaying = MediaStatus.paused;
private currentRepeatState: RepeatState = RepeatState.off;
private currentShuffleState = false;
export class DomTidalController implements TidalController {
private currentPlayStatus = MediaStatus.paused;
/**
* Convert the duration from MM:SS to seconds
* @param {*} duration
*/
private convertDuration(duration: string) {
const parts = duration.split(":");
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
}
private readonly elements = {
play: '*[data-test="play"]',
@ -103,7 +106,7 @@ export class DomTidalController implements TidalController<DomControllerOptions>
globalThis.location.href.includes("/playlist/") ||
globalThis.location.href.includes("/mix/")
) {
if (this.getCurrentlyPlayingStatus() === MediaStatus.playing) {
if (this.currentPlayStatus === MediaStatus.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.
// 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);
@ -157,65 +160,6 @@ export class DomTidalController implements TidalController<DomControllerOptions>
},
};
onMediaInfoUpdate(callback: (state: Partial<MediaInfo>) => void): void {
this.updateSubscriber = callback;
}
bootstrap(options: DomControllerOptions): void {
/**
* Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
* If it's a song it returns the track URL, if not it will return undefined
*/
const getTrackURL = () => {
const id = this.getTrackId();
return `https://tidal.com/browse/track/${id}`;
};
setInterval(async () => {
const title = this.getTitle();
const artistsArray = this.getArtists();
const artistsString = this.getArtistsString();
const current = this.getCurrentTime();
const currentStatus = this.getCurrentlyPlayingStatus();
const shuffleState = this.getCurrentShuffleState();
const repeatState = this.getCurrentRepeatState();
const playStateChanged = currentStatus != this.currentlyPlaying;
const shuffleStateChanged = shuffleState != this.currentShuffleState;
const repeatStateChanged = repeatState != this.currentRepeatState;
if (playStateChanged) this.currentlyPlaying = currentStatus;
if (shuffleStateChanged) this.currentShuffleState = shuffleState;
if (repeatStateChanged) this.currentRepeatState = repeatState;
const album = this.getAlbumName();
const duration = this.getDuration();
const updatedInfo = {
title,
artists: artistsString,
artistsArray,
album: album,
playingFrom: this.getPlayingFrom(),
status: currentStatus,
url: getTrackURL(),
current,
currentInSeconds: convertDurationToSeconds(current),
duration,
durationInSeconds: convertDurationToSeconds(duration),
image: this.getSongIcon(),
favorite: this.isFavorite(),
player: {
status: currentStatus,
shuffle: shuffleState,
repeat: repeatState,
},
};
this.updateSubscriber(updatedInfo);
}, options.refreshInterval);
}
playPause = (): void => {
const play = this.elements.get("play");
@ -302,7 +246,7 @@ export class DomTidalController implements TidalController<DomControllerOptions>
return this.elements.getText("current");
}
getCurrentPositionInSeconds(): number {
return convertDurationToSeconds(this.getCurrentPosition());
return this.convertDuration(this.getCurrentPosition()) * 1000 * 1000;
}
getTrackId(): string {
@ -345,4 +289,7 @@ export class DomTidalController implements TidalController<DomControllerOptions>
getSongIcon(): string {
return this.elements.getSongIcon();
}
setPlayStatus(status: MediaStatus): void {
this.currentPlayStatus = status;
}
}

View File

@ -1,7 +1,6 @@
import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
import { DomTidalController } from "./DomController/DomTidalController";
import { DomTidalController } from "./DomTidalController";
import { TidalController } from "./TidalController";
export class MediaSessionTidalController implements TidalController {
@ -10,20 +9,15 @@ export class MediaSessionTidalController implements TidalController {
constructor() {
this.domMediaController = new DomTidalController();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onMediaInfoUpdate(callback: (state: Partial<MediaInfo>) => void): void {
globalThis.alert("method not implemented");
throw new Error("Method not implemented.");
}
bootstrap(): void {
globalThis.alert("Method not implemented: ");
throw new Error("Method not implemented.");
}
// example of using the original domMediaController as a fallback
goToHome(): void {
this.domMediaController.goToHome();
}
setPlayStatus(status: MediaStatus): void {
globalThis.alert("Method not implemented: " + status);
throw new Error("Method not implemented.");
}
getDuration(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");

View File

@ -1,8 +1,7 @@
import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
export interface TidalController<TBootstrapOptions = object> {
export interface TidalController {
goToHome(): void;
openSettings(): void;
@ -21,17 +20,6 @@ export interface TidalController<TBootstrapOptions = object> {
previous(): void;
toggleShuffle(): void;
/**
* Optional setup/startup/bootstrap for this controller
*/
bootstrap(options: TBootstrapOptions): void;
/**
* Method that triggers every time the MediaInfo updates
* @param callback function that receives the updated media info
*/
onMediaInfoUpdate(callback: (state: Partial<MediaInfo>) => void): void;
/**
* Update the current status of tidal (e.g playing or paused)
*/
@ -49,5 +37,11 @@ export interface TidalController<TBootstrapOptions = object> {
getArtistsString(): string;
getPlayingFrom(): string;
getSongIcon(): string;
isFavorite(): boolean;
// add an observable to react on instead of a hookup function
// onMediaChange(): any;
// this can probably be removed after ^
setPlayStatus(status: MediaStatus): void;
}

View File

@ -1,15 +0,0 @@
import { downloadFile } from "../../scripts/download";
import { Logger } from "../logger";
export const downloadIcon = async (imagePath: string, destination: string): Promise<string> => {
if (imagePath.startsWith("http")) {
try {
return await downloadFile(imagePath, destination);
} catch (error) {
Logger.log("Downloading file failed", { error });
return "";
}
}
return "";
};

View File

@ -1,15 +0,0 @@
/**
* Build a track url given the id
*/
export const getTrackURL = (trackId: string) => {
return `https://tidal.com/browse/track/${trackId}`;
};
/**
* Retrieve the universal link given a regular track link
* @param trackLink
* @returns
*/
export const getUniversalLink = (trackLink: string) => {
return `${trackLink}?u`;
};

View File

@ -1,11 +1,9 @@
import { MediaPlayerInfo } from "./mediaPlayerInfo";
import { MediaStatus } from "./mediaStatus";
import { RepeatState } from "./repeatState";
export interface MediaInfo {
title: string;
artists: string;
artistsArray?: string[];
album: string;
icon: string;
status: MediaStatus;
@ -19,30 +17,3 @@ export interface MediaInfo {
favorite: boolean;
player?: MediaPlayerInfo;
}
export const getEmptyMediaInfo = () => {
const emptyState: MediaInfo = {
title: "",
artists: "",
artistsArray: [],
album: "",
playingFrom: "",
status: MediaStatus.playing,
url: "",
current: "",
currentInSeconds: 100,
duration: "",
durationInSeconds: 100,
image: "",
icon: "",
favorite: true,
player: {
status: MediaStatus.playing,
shuffle: true,
repeat: RepeatState.all,
},
};
return emptyState;
};

View File

@ -3,7 +3,6 @@ import { clipboard, ipcRenderer } from "electron";
import Player from "mpris-service";
import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings";
import { downloadIcon } from "./features/icon/downloadIcon";
import {
ListenBrainz,
ListenBrainzConstants,
@ -11,42 +10,39 @@ import {
} from "./features/listenbrainz/listenbrainz";
import { StoreData } from "./features/listenbrainz/models/storeData";
import { Logger } from "./features/logger";
import { SharingService } from "./features/sharingService/sharingService";
import { addCustomCss } from "./features/theming/theming";
import { getTrackURL, getUniversalLink } from "./features/tidal/url";
import { convertDurationToSeconds } from "./features/time/parse";
import { getEmptyMediaInfo, MediaInfo } from "./models/mediaInfo";
import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus";
import { RepeatState } from "./models/repeatState";
import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys";
import { ObjectToDotNotation } from "./scripts/objectUtilities";
import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions";
import { DomControllerOptions } from "./TidalControllers/DomController/DomControllerOptions";
import { DomTidalController } from "./TidalControllers/DomController/DomTidalController";
import { DomTidalController } from "./TidalControllers/DomTidalController";
import { MediaSessionTidalController } from "./TidalControllers/MediaSessionTidalController";
import { TidalController } from "./TidalControllers/TidalController";
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const staticTitle = "TIDAL Hi-Fi";
let currentSong = "";
let player: Player;
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
let scrobbleWaitingForDelay = false;
let currentlyPlaying = MediaStatus.paused;
let currentRepeatState: RepeatState = RepeatState.off;
let currentShuffleState = false;
let currentMediaInfo: MediaInfo;
let currentNotification: Electron.Notification;
let tidalController: TidalController;
let controllerOptions = {};
let currentMediaInfo = getEmptyMediaInfo();
// TODO: replace with setting
// eslint-disable-next-line no-constant-condition
if (true) {
tidalController = new DomTidalController();
const domControllerOptions: DomControllerOptions = {
refreshInterval: getDomUpdateFrequency(),
};
controllerOptions = domControllerOptions;
} else {
tidalController = new MediaSessionTidalController();
}
@ -55,7 +51,7 @@ if (true) {
* Get the update frequency from the store
* make sure it returns a number, if not use the default
*/
function getDomUpdateFrequency() {
function getUpdateFrequency() {
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
const defaultValue = 500;
@ -109,7 +105,7 @@ function addHotKeys() {
tidalController.repeat();
});
addHotkey("control+w", async function () {
const url = getUniversalLink(getTrackURL(tidalController.getTrackId()));
const url = SharingService.getUniversalLink(getTrackURL());
clipboard.writeText(url);
new Notification({
title: `Universal link generated: `,
@ -197,6 +193,15 @@ function addIPCEventListeners() {
});
}
/**
* Convert the duration from MM:SS to seconds
* @param {*} duration
*/
function convertDuration(duration: string) {
const parts = duration.split(":");
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
}
/**
* Update Tidal-hifi's media info
*
@ -204,6 +209,7 @@ function addIPCEventListeners() {
*/
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) {
if (mediaInfo) {
currentMediaInfo = mediaInfo;
ipcRenderer.send(globalEvents.updateInfo, mediaInfo);
updateMpris(mediaInfo);
updateListenBrainz(mediaInfo);
@ -314,7 +320,7 @@ function updateMpris(mediaInfo: MediaInfo) {
"xesam:artist": [mediaInfo.artists],
"xesam:album": mediaInfo.album,
"mpris:artUrl": mediaInfo.image,
"mpris:length": convertDurationToSeconds(mediaInfo.duration) * 1000 * 1000,
"mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + tidalController.getTrackId(),
},
...ObjectToDotNotation(mediaInfo, "custom:"),
@ -342,7 +348,7 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
mediaInfo.title,
mediaInfo.artists,
mediaInfo.status,
convertDurationToSeconds(mediaInfo.duration)
convertDuration(mediaInfo.duration)
);
scrobbleWaitingForDelay = false;
},
@ -353,36 +359,103 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
}
}
tidalController.bootstrap(controllerOptions);
tidalController.onMediaInfoUpdate(async (newState) => {
currentMediaInfo = { ...currentMediaInfo, ...newState };
/**
* Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
* If it's a song it returns the track URL, if not it will return undefined
*/
function getTrackURL() {
const id = tidalController.getTrackId();
return `https://tidal.com/browse/track/${id}`;
}
const songDashArtistTitle = `${currentMediaInfo.title} - ${currentMediaInfo.artists}`;
const isNewSong = currentSong !== songDashArtistTitle;
/**
* Watch for song changes and update title + notify
*/
setInterval(function () {
const title = tidalController.getTitle();
const artistsArray = tidalController.getArtists();
const artistsString = tidalController.getArtistsString();
const songDashArtistTitle = `${title} - ${artistsString}`;
const staticTitle = "TIDAL Hi-Fi";
const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
const current = tidalController.getCurrentTime();
const currentStatus = tidalController.getCurrentlyPlayingStatus();
const shuffleState = tidalController.getCurrentShuffleState();
const repeatState = tidalController.getCurrentRepeatState();
if (isNewSong) {
// check whether one of the artists is in the "skip artist" array, if so, skip...
skipArtistsIfFoundInSkippedArtistsList(currentMediaInfo.artistsArray ?? []);
const playStateChanged = currentStatus != currentlyPlaying;
const shuffleStateChanged = shuffleState != currentShuffleState;
const repeatStateChanged = repeatState != currentRepeatState;
// update the currently playing song
currentSong = songDashArtistTitle;
const hasStateChanged = playStateChanged || shuffleStateChanged || repeatStateChanged;
// update the window title with the new info
// update info if song changed or was just paused/resumed
if (titleOrArtistsChanged || hasStateChanged) {
if (playStateChanged) currentlyPlaying = currentStatus;
if (shuffleStateChanged) currentShuffleState = shuffleState;
if (repeatStateChanged) currentRepeatState = repeatState;
skipArtistsIfFoundInSkippedArtistsList(artistsArray);
const album = tidalController.getAlbumName();
const duration = tidalController.getDuration();
const options: MediaInfo = {
title,
artists: artistsString,
album: album,
playingFrom: tidalController.getPlayingFrom(),
status: currentStatus,
url: getTrackURL(),
current,
currentInSeconds: convertDurationToSeconds(current),
duration,
durationInSeconds: convertDurationToSeconds(duration),
image: "",
icon: "",
favorite: tidalController.isFavorite(),
player: {
status: currentStatus,
shuffle: shuffleState,
repeat: repeatState,
},
};
// update title, url and play info with new info
settingsStore.get(settings.staticWindowTitle)
? setTitle(staticTitle)
: setTitle(`${currentMediaInfo.title} - ${currentMediaInfo.artists}`);
: setTitle(songDashArtistTitle);
getTrackURL();
currentSong = songDashArtistTitle;
tidalController.setPlayStatus(currentStatus);
// download a new icon and use it for the media info
if (!newState.icon && newState.image) {
currentMediaInfo.icon = await downloadIcon(currentMediaInfo.image, notificationPath);
} else {
currentMediaInfo.icon = "";
}
const image = tidalController.getSongIcon();
updateMediaInfo(currentMediaInfo, true);
new Promise<void>((resolve) => {
if (image.startsWith("http")) {
options.image = image;
downloadFile(image, notificationPath).then(
() => {
options.icon = notificationPath;
resolve();
},
() => {
// if the image can't be downloaded then continue without it
resolve();
}
);
} else {
// if the image can't be found on the page continue without it
resolve();
}
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
});
} else {
// if titleOrArtists didn't change then only minor mediaInfo (like timings) changed, so don't bother the user with notifications
updateMediaInfo(currentMediaInfo, false);
// just update the time
updateMediaInfo(
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
false
);
}
/**
@ -402,7 +475,8 @@ tidalController.onMediaInfoUpdate(async (newState) => {
}
}
}
});
}, getUpdateFrequency());
addMPRIS();
addCustomCss(app);
addHotKeys();

View File

@ -6,8 +6,8 @@ import request from "request";
* @param {string} fileUrl url to download
* @param {string} targetPath path to save it at
*/
export const downloadFile = function (fileUrl: string, targetPath: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
export const downloadFile = function (fileUrl: string, targetPath: string) {
return new Promise((resolve, reject) => {
const req = request({
method: "GET",
uri: fileUrl,
@ -16,9 +16,7 @@ export const downloadFile = function (fileUrl: string, targetPath: string): Prom
const out = fs.createWriteStream(targetPath);
req.pipe(out);
req.on("end", () => {
resolve(targetPath);
});
req.on("end", resolve);
req.on("error", reject);
});

View File

@ -1,6 +1,28 @@
import { getEmptyMediaInfo, MediaInfo } from "../models/mediaInfo";
import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
const defaultInfo: MediaInfo = getEmptyMediaInfo();
const defaultInfo: MediaInfo = {
title: "",
artists: "",
album: "",
icon: "",
playingFrom: "",
status: MediaStatus.paused,
url: "",
current: "",
currentInSeconds: 0,
duration: "",
durationInSeconds: 0,
image: "tidal-hifi-icon",
favorite: false,
player: {
status: MediaStatus.paused,
shuffle: false,
repeat: RepeatState.off,
},
};
export let mediaInfo: MediaInfo = { ...defaultInfo };