mirror of
https://github.com/Mastermindzh/tidal-hifi.git
synced 2025-04-19 19:22:29 +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": {
|
"node_modules/@discordjs/rest": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz",
|
||||||
"integrity": "sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==",
|
"integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/collection": "^2.1.1",
|
"@discordjs/collection": "^2.1.1",
|
||||||
@ -424,10 +424,10 @@
|
|||||||
"@sapphire/async-queue": "^1.5.3",
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
"@sapphire/snowflake": "^3.5.3",
|
"@sapphire/snowflake": "^3.5.3",
|
||||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
"@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",
|
"magic-bytes.js": "^1.10.0",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"undici": "6.19.8"
|
"undici": "6.21.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@ -436,12 +436,6 @@
|
|||||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
"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": {
|
"node_modules/@discordjs/util": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
|
||||||
@ -3196,10 +3190,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
"shebang-command": "^2.0.0",
|
"shebang-command": "^2.0.0",
|
||||||
@ -3490,9 +3485,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/discord-api-types": {
|
"node_modules/discord-api-types": {
|
||||||
"version": "0.37.103",
|
"version": "0.37.119",
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.103.tgz",
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
|
||||||
"integrity": "sha512-r+qitxXKe2l6KFw5odPdZSSqdEou+7eNC7BfbZ7mny5Me/K06wCTeKUMVeH/YsI9+4QQudskeQ307kr/7ppQ1A==",
|
"integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dmg-builder": {
|
"node_modules/dmg-builder": {
|
||||||
@ -6223,9 +6218,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -6233,6 +6228,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@ -8825,9 +8821,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.19.8",
|
"version": "6.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||||
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
|
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"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 { convertDurationToSeconds } from "../../features/time/parse";
|
||||||
import { RepeatState } from "../models/repeatState";
|
import { MediaInfo } from "../../models/mediaInfo";
|
||||||
import { TidalController } from "./TidalController";
|
import { MediaStatus } from "../../models/mediaStatus";
|
||||||
|
import { RepeatState } from "../../models/repeatState";
|
||||||
|
import { TidalController } from "../TidalController";
|
||||||
|
import { DomControllerOptions } from "./DomControllerOptions";
|
||||||
|
|
||||||
export class DomTidalController implements TidalController {
|
export class DomTidalController implements TidalController<DomControllerOptions> {
|
||||||
private currentPlayStatus = MediaStatus.paused;
|
private updateSubscriber: (state: Partial<MediaInfo>) => void;
|
||||||
|
private currentlyPlaying = MediaStatus.paused;
|
||||||
/**
|
private currentRepeatState: RepeatState = RepeatState.off;
|
||||||
* Convert the duration from MM:SS to seconds
|
private currentShuffleState = false;
|
||||||
* @param {*} duration
|
|
||||||
*/
|
|
||||||
private convertDuration(duration: string) {
|
|
||||||
const parts = duration.split(":");
|
|
||||||
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly elements = {
|
private readonly elements = {
|
||||||
play: '*[data-test="play"]',
|
play: '*[data-test="play"]',
|
||||||
@ -106,7 +103,7 @@ export class DomTidalController implements TidalController {
|
|||||||
globalThis.location.href.includes("/playlist/") ||
|
globalThis.location.href.includes("/playlist/") ||
|
||||||
globalThis.location.href.includes("/mix/")
|
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.
|
// 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);
|
||||||
@ -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 => {
|
playPause = (): void => {
|
||||||
const play = this.elements.get("play");
|
const play = this.elements.get("play");
|
||||||
|
|
||||||
@ -246,7 +302,7 @@ export class DomTidalController implements TidalController {
|
|||||||
return this.elements.getText("current");
|
return this.elements.getText("current");
|
||||||
}
|
}
|
||||||
getCurrentPositionInSeconds(): number {
|
getCurrentPositionInSeconds(): number {
|
||||||
return this.convertDuration(this.getCurrentPosition()) * 1000 * 1000;
|
return convertDurationToSeconds(this.getCurrentPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
getTrackId(): string {
|
getTrackId(): string {
|
||||||
@ -289,7 +345,4 @@ export class DomTidalController implements TidalController {
|
|||||||
getSongIcon(): string {
|
getSongIcon(): string {
|
||||||
return this.elements.getSongIcon();
|
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 { MediaStatus } from "../models/mediaStatus";
|
||||||
import { RepeatState } from "../models/repeatState";
|
import { RepeatState } from "../models/repeatState";
|
||||||
import { DomTidalController } from "./DomTidalController";
|
import { DomTidalController } from "./DomController/DomTidalController";
|
||||||
import { TidalController } from "./TidalController";
|
import { TidalController } from "./TidalController";
|
||||||
|
|
||||||
export class MediaSessionTidalController implements TidalController {
|
export class MediaSessionTidalController implements TidalController {
|
||||||
@ -9,15 +10,20 @@ export class MediaSessionTidalController implements TidalController {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.domMediaController = new DomTidalController();
|
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
|
// example of using the original domMediaController as a fallback
|
||||||
goToHome(): void {
|
goToHome(): void {
|
||||||
this.domMediaController.goToHome();
|
this.domMediaController.goToHome();
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlayStatus(status: MediaStatus): void {
|
|
||||||
globalThis.alert("Method not implemented: " + status);
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
getDuration(): string {
|
getDuration(): string {
|
||||||
globalThis.alert("Method not implemented");
|
globalThis.alert("Method not implemented");
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { MediaInfo } from "../models/mediaInfo";
|
||||||
import { MediaStatus } from "../models/mediaStatus";
|
import { MediaStatus } from "../models/mediaStatus";
|
||||||
import { RepeatState } from "../models/repeatState";
|
import { RepeatState } from "../models/repeatState";
|
||||||
|
|
||||||
export interface TidalController {
|
export interface TidalController<TBootstrapOptions = object> {
|
||||||
goToHome(): void;
|
goToHome(): void;
|
||||||
openSettings(): void;
|
openSettings(): void;
|
||||||
|
|
||||||
@ -20,6 +21,17 @@ export interface TidalController {
|
|||||||
previous(): void;
|
previous(): void;
|
||||||
toggleShuffle(): 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)
|
* Update the current status of tidal (e.g playing or paused)
|
||||||
*/
|
*/
|
||||||
@ -37,11 +49,5 @@ export interface TidalController {
|
|||||||
getArtistsString(): string;
|
getArtistsString(): string;
|
||||||
getPlayingFrom(): string;
|
getPlayingFrom(): string;
|
||||||
getSongIcon(): string;
|
getSongIcon(): string;
|
||||||
|
|
||||||
isFavorite(): boolean;
|
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 { MediaPlayerInfo } from "./mediaPlayerInfo";
|
||||||
import { MediaStatus } from "./mediaStatus";
|
import { MediaStatus } from "./mediaStatus";
|
||||||
|
import { RepeatState } from "./repeatState";
|
||||||
|
|
||||||
export interface MediaInfo {
|
export interface MediaInfo {
|
||||||
title: string;
|
title: string;
|
||||||
artists: string;
|
artists: string;
|
||||||
|
artistsArray?: string[];
|
||||||
album: string;
|
album: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
status: MediaStatus;
|
status: MediaStatus;
|
||||||
@ -17,3 +19,30 @@ export interface MediaInfo {
|
|||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
player?: MediaPlayerInfo;
|
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 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 { downloadIcon } from "./features/icon/downloadIcon";
|
||||||
import {
|
import {
|
||||||
ListenBrainz,
|
ListenBrainz,
|
||||||
ListenBrainzConstants,
|
ListenBrainzConstants,
|
||||||
@ -10,39 +11,42 @@ import {
|
|||||||
} from "./features/listenbrainz/listenbrainz";
|
} from "./features/listenbrainz/listenbrainz";
|
||||||
import { StoreData } from "./features/listenbrainz/models/storeData";
|
import { StoreData } from "./features/listenbrainz/models/storeData";
|
||||||
import { Logger } from "./features/logger";
|
import { Logger } from "./features/logger";
|
||||||
import { SharingService } from "./features/sharingService/sharingService";
|
|
||||||
import { addCustomCss } from "./features/theming/theming";
|
import { addCustomCss } from "./features/theming/theming";
|
||||||
|
import { getTrackURL, getUniversalLink } from "./features/tidal/url";
|
||||||
import { convertDurationToSeconds } from "./features/time/parse";
|
import { convertDurationToSeconds } from "./features/time/parse";
|
||||||
import { MediaInfo } from "./models/mediaInfo";
|
import { getEmptyMediaInfo, MediaInfo } from "./models/mediaInfo";
|
||||||
import { MediaStatus } from "./models/mediaStatus";
|
import { MediaStatus } from "./models/mediaStatus";
|
||||||
import { RepeatState } from "./models/repeatState";
|
|
||||||
import { downloadFile } from "./scripts/download";
|
|
||||||
import { addHotkey } from "./scripts/hotkeys";
|
import { addHotkey } from "./scripts/hotkeys";
|
||||||
import { ObjectToDotNotation } from "./scripts/objectUtilities";
|
import { ObjectToDotNotation } from "./scripts/objectUtilities";
|
||||||
import { settingsStore } from "./scripts/settings";
|
import { settingsStore } from "./scripts/settings";
|
||||||
import { setTitle } from "./scripts/window-functions";
|
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 { MediaSessionTidalController } from "./TidalControllers/MediaSessionTidalController";
|
||||||
import { TidalController } from "./TidalControllers/TidalController";
|
import { TidalController } from "./TidalControllers/TidalController";
|
||||||
|
|
||||||
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
|
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
|
||||||
|
const staticTitle = "TIDAL Hi-Fi";
|
||||||
|
|
||||||
let currentSong = "";
|
let currentSong = "";
|
||||||
let player: Player;
|
let player: Player;
|
||||||
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
|
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
|
||||||
let scrobbleWaitingForDelay = false;
|
let scrobbleWaitingForDelay = false;
|
||||||
|
|
||||||
let currentlyPlaying = MediaStatus.paused;
|
|
||||||
let currentRepeatState: RepeatState = RepeatState.off;
|
|
||||||
let currentShuffleState = false;
|
|
||||||
let currentMediaInfo: MediaInfo;
|
|
||||||
let currentNotification: Electron.Notification;
|
let currentNotification: Electron.Notification;
|
||||||
|
|
||||||
let tidalController: TidalController;
|
let tidalController: TidalController;
|
||||||
|
let controllerOptions = {};
|
||||||
|
let currentMediaInfo = getEmptyMediaInfo();
|
||||||
|
|
||||||
// TODO: replace with setting
|
// TODO: replace with setting
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
if (true) {
|
if (true) {
|
||||||
tidalController = new DomTidalController();
|
tidalController = new DomTidalController();
|
||||||
|
const domControllerOptions: DomControllerOptions = {
|
||||||
|
refreshInterval: getDomUpdateFrequency(),
|
||||||
|
};
|
||||||
|
controllerOptions = domControllerOptions;
|
||||||
} else {
|
} else {
|
||||||
tidalController = new MediaSessionTidalController();
|
tidalController = new MediaSessionTidalController();
|
||||||
}
|
}
|
||||||
@ -51,7 +55,7 @@ if (true) {
|
|||||||
* Get the update frequency from the store
|
* Get the update frequency from the store
|
||||||
* 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 getDomUpdateFrequency() {
|
||||||
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
|
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
|
||||||
const defaultValue = 500;
|
const defaultValue = 500;
|
||||||
|
|
||||||
@ -105,7 +109,7 @@ function addHotKeys() {
|
|||||||
tidalController.repeat();
|
tidalController.repeat();
|
||||||
});
|
});
|
||||||
addHotkey("control+w", async function () {
|
addHotkey("control+w", async function () {
|
||||||
const url = SharingService.getUniversalLink(getTrackURL());
|
const url = getUniversalLink(getTrackURL(tidalController.getTrackId()));
|
||||||
clipboard.writeText(url);
|
clipboard.writeText(url);
|
||||||
new Notification({
|
new Notification({
|
||||||
title: `Universal link generated: `,
|
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
|
* Update Tidal-hifi's media info
|
||||||
*
|
*
|
||||||
@ -209,7 +204,6 @@ function convertDuration(duration: string) {
|
|||||||
*/
|
*/
|
||||||
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) {
|
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) {
|
||||||
if (mediaInfo) {
|
if (mediaInfo) {
|
||||||
currentMediaInfo = mediaInfo;
|
|
||||||
ipcRenderer.send(globalEvents.updateInfo, mediaInfo);
|
ipcRenderer.send(globalEvents.updateInfo, mediaInfo);
|
||||||
updateMpris(mediaInfo);
|
updateMpris(mediaInfo);
|
||||||
updateListenBrainz(mediaInfo);
|
updateListenBrainz(mediaInfo);
|
||||||
@ -320,7 +314,7 @@ function updateMpris(mediaInfo: MediaInfo) {
|
|||||||
"xesam:artist": [mediaInfo.artists],
|
"xesam:artist": [mediaInfo.artists],
|
||||||
"xesam:album": mediaInfo.album,
|
"xesam:album": mediaInfo.album,
|
||||||
"mpris:artUrl": mediaInfo.image,
|
"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(),
|
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + tidalController.getTrackId(),
|
||||||
},
|
},
|
||||||
...ObjectToDotNotation(mediaInfo, "custom:"),
|
...ObjectToDotNotation(mediaInfo, "custom:"),
|
||||||
@ -348,7 +342,7 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
|
|||||||
mediaInfo.title,
|
mediaInfo.title,
|
||||||
mediaInfo.artists,
|
mediaInfo.artists,
|
||||||
mediaInfo.status,
|
mediaInfo.status,
|
||||||
convertDuration(mediaInfo.duration)
|
convertDurationToSeconds(mediaInfo.duration)
|
||||||
);
|
);
|
||||||
scrobbleWaitingForDelay = false;
|
scrobbleWaitingForDelay = false;
|
||||||
},
|
},
|
||||||
@ -359,103 +353,36 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
tidalController.bootstrap(controllerOptions);
|
||||||
* Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
|
tidalController.onMediaInfoUpdate(async (newState) => {
|
||||||
* If it's a song it returns the track URL, if not it will return undefined
|
currentMediaInfo = { ...currentMediaInfo, ...newState };
|
||||||
*/
|
|
||||||
function getTrackURL() {
|
|
||||||
const id = tidalController.getTrackId();
|
|
||||||
return `https://tidal.com/browse/track/${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const songDashArtistTitle = `${currentMediaInfo.title} - ${currentMediaInfo.artists}`;
|
||||||
* Watch for song changes and update title + notify
|
const isNewSong = currentSong !== songDashArtistTitle;
|
||||||
*/
|
|
||||||
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 playStateChanged = currentStatus != currentlyPlaying;
|
if (isNewSong) {
|
||||||
const shuffleStateChanged = shuffleState != currentShuffleState;
|
// check whether one of the artists is in the "skip artist" array, if so, skip...
|
||||||
const repeatStateChanged = repeatState != currentRepeatState;
|
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
|
// update the window title with the new info
|
||||||
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)
|
settingsStore.get(settings.staticWindowTitle)
|
||||||
? setTitle(staticTitle)
|
? setTitle(staticTitle)
|
||||||
: setTitle(songDashArtistTitle);
|
: setTitle(`${currentMediaInfo.title} - ${currentMediaInfo.artists}`);
|
||||||
getTrackURL();
|
|
||||||
currentSong = songDashArtistTitle;
|
|
||||||
tidalController.setPlayStatus(currentStatus);
|
|
||||||
|
|
||||||
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) => {
|
updateMediaInfo(currentMediaInfo, true);
|
||||||
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 {
|
} else {
|
||||||
// just update the time
|
// if titleOrArtists didn't change then only minor mediaInfo (like timings) changed, so don't bother the user with notifications
|
||||||
updateMediaInfo(
|
updateMediaInfo(currentMediaInfo, false);
|
||||||
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -475,8 +402,7 @@ setInterval(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, getUpdateFrequency());
|
});
|
||||||
|
|
||||||
addMPRIS();
|
addMPRIS();
|
||||||
addCustomCss(app);
|
addCustomCss(app);
|
||||||
addHotKeys();
|
addHotKeys();
|
||||||
|
@ -6,8 +6,8 @@ import request from "request";
|
|||||||
* @param {string} fileUrl url to download
|
* @param {string} fileUrl url to download
|
||||||
* @param {string} targetPath path to save it at
|
* @param {string} targetPath path to save it at
|
||||||
*/
|
*/
|
||||||
export const downloadFile = function (fileUrl: string, targetPath: string) {
|
export const downloadFile = function (fileUrl: string, targetPath: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const req = request({
|
const req = request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
uri: fileUrl,
|
uri: fileUrl,
|
||||||
@ -16,7 +16,9 @@ export const downloadFile = function (fileUrl: string, targetPath: string) {
|
|||||||
const out = fs.createWriteStream(targetPath);
|
const out = fs.createWriteStream(targetPath);
|
||||||
req.pipe(out);
|
req.pipe(out);
|
||||||
|
|
||||||
req.on("end", resolve);
|
req.on("end", () => {
|
||||||
|
resolve(targetPath);
|
||||||
|
});
|
||||||
|
|
||||||
req.on("error", reject);
|
req.on("error", reject);
|
||||||
});
|
});
|
||||||
|
@ -1,28 +1,6 @@
|
|||||||
import { MediaInfo } from "../models/mediaInfo";
|
import { getEmptyMediaInfo, MediaInfo } from "../models/mediaInfo";
|
||||||
import { MediaStatus } from "../models/mediaStatus";
|
|
||||||
import { RepeatState } from "../models/repeatState";
|
|
||||||
|
|
||||||
const defaultInfo: MediaInfo = {
|
const defaultInfo: MediaInfo = getEmptyMediaInfo();
|
||||||
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 };
|
export let mediaInfo: MediaInfo = { ...defaultInfo };
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user