mirror of
https://github.com/Mastermindzh/tidal-hifi.git
synced 2025-04-17 18:23:51 +02:00
finished interface for TidalController and moved most domLogic to the domController
This commit is contained in:
parent
aed40ec4ef
commit
f553fe98e7
42
package-lock.json
generated
42
package-lock.json
generated
@ -414,9 +414,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"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==",
|
||||
"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==",
|
||||
"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.97",
|
||||
"discord-api-types": "^0.37.119",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.19.8"
|
||||
"undici": "6.21.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@ -436,12 +436,6 @@
|
||||
"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",
|
||||
@ -3196,10 +3190,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@ -3490,9 +3485,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"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==",
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dmg-builder": {
|
||||
@ -6223,9 +6218,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -6233,6 +6228,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@ -8825,9 +8821,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
|
||||
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
@ -0,0 +1,6 @@
|
||||
export type DomControllerOptions = {
|
||||
/**
|
||||
* Interval that tidal-hifi scrapes the dom for information
|
||||
*/
|
||||
refreshInterval: number;
|
||||
};
|
@ -1,18 +1,15 @@
|
||||
import { MediaStatus } from "../models/mediaStatus";
|
||||
import { RepeatState } from "../models/repeatState";
|
||||
import { TidalController } from "./TidalController";
|
||||
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";
|
||||
|
||||
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]);
|
||||
}
|
||||
export class DomTidalController implements TidalController<DomControllerOptions> {
|
||||
private updateSubscriber: (state: Partial<MediaInfo>) => void;
|
||||
private currentlyPlaying = MediaStatus.paused;
|
||||
private currentRepeatState: RepeatState = RepeatState.off;
|
||||
private currentShuffleState = false;
|
||||
|
||||
private readonly elements = {
|
||||
play: '*[data-test="play"]',
|
||||
@ -106,7 +103,7 @@ export class DomTidalController implements TidalController {
|
||||
globalThis.location.href.includes("/playlist/") ||
|
||||
globalThis.location.href.includes("/mix/")
|
||||
) {
|
||||
if (this.currentPlayStatus === MediaStatus.playing) {
|
||||
if (this.getCurrentlyPlayingStatus() === 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);
|
||||
@ -160,6 +157,65 @@ export class DomTidalController implements TidalController {
|
||||
},
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
@ -246,7 +302,7 @@ export class DomTidalController implements TidalController {
|
||||
return this.elements.getText("current");
|
||||
}
|
||||
getCurrentPositionInSeconds(): number {
|
||||
return this.convertDuration(this.getCurrentPosition()) * 1000 * 1000;
|
||||
return convertDurationToSeconds(this.getCurrentPosition());
|
||||
}
|
||||
|
||||
getTrackId(): string {
|
||||
@ -289,7 +345,4 @@ export class DomTidalController implements TidalController {
|
||||
getSongIcon(): string {
|
||||
return this.elements.getSongIcon();
|
||||
}
|
||||
setPlayStatus(status: MediaStatus): void {
|
||||
this.currentPlayStatus = status;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { MediaInfo } from "../models/mediaInfo";
|
||||
import { MediaStatus } from "../models/mediaStatus";
|
||||
import { RepeatState } from "../models/repeatState";
|
||||
import { DomTidalController } from "./DomTidalController";
|
||||
import { DomTidalController } from "./DomController/DomTidalController";
|
||||
import { TidalController } from "./TidalController";
|
||||
|
||||
export class MediaSessionTidalController implements TidalController {
|
||||
@ -9,15 +10,20 @@ 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.");
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { MediaInfo } from "../models/mediaInfo";
|
||||
import { MediaStatus } from "../models/mediaStatus";
|
||||
import { RepeatState } from "../models/repeatState";
|
||||
|
||||
export interface TidalController {
|
||||
export interface TidalController<TBootstrapOptions = object> {
|
||||
goToHome(): void;
|
||||
openSettings(): void;
|
||||
|
||||
@ -20,6 +21,17 @@ export interface TidalController {
|
||||
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)
|
||||
*/
|
||||
@ -37,11 +49,5 @@ export interface TidalController {
|
||||
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;
|
||||
}
|
||||
|
15
src/features/icon/downloadIcon.ts
Normal file
15
src/features/icon/downloadIcon.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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 "";
|
||||
};
|
15
src/features/tidal/url.ts
Normal file
15
src/features/tidal/url.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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`;
|
||||
};
|
@ -1,9 +1,11 @@
|
||||
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;
|
||||
@ -17,3 +19,30 @@ 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;
|
||||
};
|
||||
|
152
src/preload.ts
152
src/preload.ts
@ -3,6 +3,7 @@ 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,
|
||||
@ -10,39 +11,42 @@ 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 { MediaInfo } from "./models/mediaInfo";
|
||||
import { getEmptyMediaInfo, 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 { DomTidalController } from "./TidalControllers/DomTidalController";
|
||||
import { DomControllerOptions } from "./TidalControllers/DomController/DomControllerOptions";
|
||||
import { DomTidalController } from "./TidalControllers/DomController/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();
|
||||
}
|
||||
@ -51,7 +55,7 @@ if (true) {
|
||||
* Get the update frequency from the store
|
||||
* make sure it returns a number, if not use the default
|
||||
*/
|
||||
function getUpdateFrequency() {
|
||||
function getDomUpdateFrequency() {
|
||||
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
|
||||
const defaultValue = 500;
|
||||
|
||||
@ -105,7 +109,7 @@ function addHotKeys() {
|
||||
tidalController.repeat();
|
||||
});
|
||||
addHotkey("control+w", async function () {
|
||||
const url = SharingService.getUniversalLink(getTrackURL());
|
||||
const url = getUniversalLink(getTrackURL(tidalController.getTrackId()));
|
||||
clipboard.writeText(url);
|
||||
new Notification({
|
||||
title: `Universal link generated: `,
|
||||
@ -193,15 +197,6 @@ 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
|
||||
*
|
||||
@ -209,7 +204,6 @@ function convertDuration(duration: string) {
|
||||
*/
|
||||
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) {
|
||||
if (mediaInfo) {
|
||||
currentMediaInfo = mediaInfo;
|
||||
ipcRenderer.send(globalEvents.updateInfo, mediaInfo);
|
||||
updateMpris(mediaInfo);
|
||||
updateListenBrainz(mediaInfo);
|
||||
@ -320,7 +314,7 @@ function updateMpris(mediaInfo: MediaInfo) {
|
||||
"xesam:artist": [mediaInfo.artists],
|
||||
"xesam:album": mediaInfo.album,
|
||||
"mpris:artUrl": mediaInfo.image,
|
||||
"mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000,
|
||||
"mpris:length": convertDurationToSeconds(mediaInfo.duration) * 1000 * 1000,
|
||||
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + tidalController.getTrackId(),
|
||||
},
|
||||
...ObjectToDotNotation(mediaInfo, "custom:"),
|
||||
@ -348,7 +342,7 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
|
||||
mediaInfo.title,
|
||||
mediaInfo.artists,
|
||||
mediaInfo.status,
|
||||
convertDuration(mediaInfo.duration)
|
||||
convertDurationToSeconds(mediaInfo.duration)
|
||||
);
|
||||
scrobbleWaitingForDelay = false;
|
||||
},
|
||||
@ -359,103 +353,36 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
tidalController.bootstrap(controllerOptions);
|
||||
tidalController.onMediaInfoUpdate(async (newState) => {
|
||||
currentMediaInfo = { ...currentMediaInfo, ...newState };
|
||||
|
||||
/**
|
||||
* 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();
|
||||
const songDashArtistTitle = `${currentMediaInfo.title} - ${currentMediaInfo.artists}`;
|
||||
const isNewSong = currentSong !== songDashArtistTitle;
|
||||
|
||||
const playStateChanged = currentStatus != currentlyPlaying;
|
||||
const shuffleStateChanged = shuffleState != currentShuffleState;
|
||||
const repeatStateChanged = repeatState != currentRepeatState;
|
||||
if (isNewSong) {
|
||||
// check whether one of the artists is in the "skip artist" array, if so, skip...
|
||||
skipArtistsIfFoundInSkippedArtistsList(currentMediaInfo.artistsArray ?? []);
|
||||
|
||||
const hasStateChanged = playStateChanged || shuffleStateChanged || repeatStateChanged;
|
||||
// update the currently playing song
|
||||
currentSong = songDashArtistTitle;
|
||||
|
||||
// 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
|
||||
// update the window title with the new info
|
||||
settingsStore.get(settings.staticWindowTitle)
|
||||
? setTitle(staticTitle)
|
||||
: setTitle(songDashArtistTitle);
|
||||
getTrackURL();
|
||||
currentSong = songDashArtistTitle;
|
||||
tidalController.setPlayStatus(currentStatus);
|
||||
: setTitle(`${currentMediaInfo.title} - ${currentMediaInfo.artists}`);
|
||||
|
||||
const image = tidalController.getSongIcon();
|
||||
// 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 = "";
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
updateMediaInfo(currentMediaInfo, true);
|
||||
} else {
|
||||
// just update the time
|
||||
updateMediaInfo(
|
||||
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
|
||||
false
|
||||
);
|
||||
// if titleOrArtists didn't change then only minor mediaInfo (like timings) changed, so don't bother the user with notifications
|
||||
updateMediaInfo(currentMediaInfo, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -475,8 +402,7 @@ setInterval(function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, getUpdateFrequency());
|
||||
|
||||
});
|
||||
addMPRIS();
|
||||
addCustomCss(app);
|
||||
addHotKeys();
|
||||
|
@ -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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
export const downloadFile = function (fileUrl: string, targetPath: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const req = request({
|
||||
method: "GET",
|
||||
uri: fileUrl,
|
||||
@ -16,7 +16,9 @@ export const downloadFile = function (fileUrl: string, targetPath: string) {
|
||||
const out = fs.createWriteStream(targetPath);
|
||||
req.pipe(out);
|
||||
|
||||
req.on("end", resolve);
|
||||
req.on("end", () => {
|
||||
resolve(targetPath);
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
});
|
||||
|
@ -1,28 +1,6 @@
|
||||
import { MediaInfo } from "../models/mediaInfo";
|
||||
import { MediaStatus } from "../models/mediaStatus";
|
||||
import { RepeatState } from "../models/repeatState";
|
||||
import { getEmptyMediaInfo, MediaInfo } from "../models/mediaInfo";
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
const defaultInfo: MediaInfo = getEmptyMediaInfo();
|
||||
|
||||
export let mediaInfo: MediaInfo = { ...defaultInfo };
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user