Merge pull request #255 from Mastermindzh/feature/5.4.0

Feature/5.4.0
This commit is contained in:
Rick van Lieshout 2023-07-24 21:59:57 +02:00 committed by GitHub
commit 11cc209025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1230 additions and 543 deletions

View File

@ -13,4 +13,4 @@ steps:
commands: commands:
- apt-get update && apt-get upgrade -y - apt-get update && apt-get upgrade -y
- apt-get install -y libarchive-tools rpm - apt-get install -y libarchive-tools rpm
- npm run build - npm run build-unpacked

View File

@ -1,5 +1,9 @@
{ {
"root": true, "root": true,
"env": {
"node": true,
"browser": true
},
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": [ "plugins": [
"@typescript-eslint" "@typescript-eslint"

View File

@ -5,6 +5,7 @@
"hifi", "hifi",
"playpause", "playpause",
"rescrobbler", "rescrobbler",
"Songwhip",
"trackid", "trackid",
"tracklist", "tracklist",
"widevine", "widevine",

View File

@ -4,6 +4,16 @@ 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.4.0
- Removed Windows builds (from publishes) as they don't work anymore.
- Added [Songwhip](https://songwhip.com/) integration
- Fixed bug with several hotkeys not working due to Tidal's HTML/css changes
- [DEV]:
- added a logger to log into STDout
- added "watchStart" which will automatically restart electron when it detects a source code change
- added "listen.tidal.com-parsing-scripts" folder with a script to verify whether all elements (in the main preload.ts) are present on the page
## 5.3.0 ## 5.3.0
- SPKChaosPhoenix updated the beautiful Tokyo Night theme: - SPKChaosPhoenix updated the beautiful Tokyo Night theme:

View File

@ -27,6 +27,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- [Integrations](#integrations) - [Integrations](#integrations)
- [Known bugs](#known-bugs) - [Known bugs](#known-bugs)
- [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround) - [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround)
- [DRM not working on Windows](#drm-not-working-on-windows)
- [Special thanks to](#special-thanks-to) - [Special thanks to](#special-thanks-to)
- [Donations](#donations) - [Donations](#donations)
- [Images](#images) - [Images](#images)
@ -41,6 +42,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- Notifications - Notifications
- Custom [theming](./docs/theming.md) - Custom [theming](./docs/theming.md)
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- Songwhip.com integration (hotkey `ctrl + w`)
- API for status and playback - API for status and playback
- Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495)) - Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495))
- Custom [integrations](#integrations) - Custom [integrations](#integrations)
@ -65,10 +67,10 @@ Whilst there are a handful of projects attempting to run Tidal on Electron they
- Lack of maintainers/developers. (no hotfixes, no issues being handled etc) - Lack of maintainers/developers. (no hotfixes, no issues being handled etc)
- Most are simple web wrappers, not my cup of tea. - Most are simple web wrappers, not my cup of tea.
- Some are DE oriented. I want this to work on WM's too. - Some are DE-oriented. I want this to work on WM's too.
- None have widevine working at the moment - None have Widevine working at the moment
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :) Sometimes it's just easier to start over, cover my own needs and after that making it available to the public :)
## Installation ## Installation
@ -153,6 +155,11 @@ The last.fm login doesn't work, as is evident from the following issue: [Last.fm
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
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.
## Special thanks to ## Special thanks to
- [Castlabs](https://castlabs.com/) - [Castlabs](https://castlabs.com/)

View File

@ -0,0 +1,43 @@
// for some dumb reason `listen.tidal.com` has disabled console.log
// we can simply return an array with values though...
// run this on a playlist or mix page and observe the result
// NOTE: play & pause can't live together so one or the other will throw an error
(() => {
let elements = {
play: '*[data-test="play"]',
pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
};
let results = [];
Object.entries(elements).forEach(([key, value]) => {
const returnValue = document.querySelector(`${value}`);
if (!returnValue) {
results.push(`element ${key} not found`);
}
});
return results;
})();

1487
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{ {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "5.3.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": {
"start": "electron .", "start": "electron --inspect=0.0.0.0:5858 .",
"watchStart": "nodemon dist -x \"npm run start\"",
"compile": "tsc && npm run sass-and-copy", "compile": "tsc && npm run sass-and-copy",
"watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"", "watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist", "copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
@ -29,41 +30,46 @@
"electron", "electron",
"hifi", "hifi",
"widevine", "widevine",
"linux" "linux",
"drm",
"castlabs"
], ],
"author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)", "author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)",
"homepage": "https://github.com/Mastermindzh/tidal-hifi", "homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.9", "@electron/remote": "^2.0.10",
"axios": "^1.4.0",
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"express": "^4.18.2", "express": "^4.18.2",
"hotkeys-js": "^3.10.2", "hotkeys-js": "^3.11.2",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"request": "^2.88.2", "request": "^2.88.2",
"sass": "^1.62.0" "sass": "^1.64.1"
}, },
"devDependencies": { "devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0", "@mastermindzh/prettier-config": "^1.0.0",
"@types/discord-rpc": "^4.0.4", "@types/discord-rpc": "^4.0.5",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/node": "^20.4.4",
"@types/request": "^2.48.8", "@types/request": "^2.48.8",
"@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^5.59.1", "@typescript-eslint/parser": "^6.1.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus", "electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus",
"electron-builder": "^24.2.1", "electron-builder": "^24.4.0",
"eslint": "^8.39.0", "eslint": "^8.45.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",
"prettier": "^2.8.8", "nodemon": "^3.0.1",
"stylelint": "^15.10.1", "prettier": "^3.0.0",
"stylelint-config-standard": "^33.0.0", "stylelint": "^15.10.2",
"stylelint-config-standard-scss": "^9.0.0", "stylelint-config-standard": "^34.0.0",
"stylelint-prettier": "^3.0.0", "stylelint-config-standard-scss": "^10.0.0",
"stylelint-prettier": "^4.0.0",
"tsc-watch": "^6.0.4", "tsc-watch": "^6.0.4",
"typescript": "^5.0.4" "typescript": "^5.1.6"
}, },
"prettier": "@mastermindzh/prettier-config" "prettier": "@mastermindzh/prettier-config"
} }

View File

@ -10,4 +10,6 @@ export const globalEvents = {
showSettings: "showSettings", showSettings: "showSettings",
storeChanged: "storeChanged", storeChanged: "storeChanged",
error: "error", error: "error",
whip: "whip",
log: "log",
}; };

33
src/features/logger.ts Normal file
View File

@ -0,0 +1,33 @@
import { IpcMain, IpcRenderer } from "electron";
import { globalEvents } from "../constants/globalEvents";
export class Logger {
/**
*
* @param ipcRenderer renderer IPC client so we can send messages to the main thread
*/
constructor(private ipcRenderer: IpcRenderer) {}
/**
* Subscribe to watch for logs from the IPC client
* @param ipcMain main thread IPC client so we can subscribe to events
*/
public static watch(ipcMain: IpcMain) {
ipcMain.on(globalEvents.log, (event, content, object) => {
console.log(content, JSON.stringify(object, null, 2));
});
}
/**
* Log content to STDOut
* @param content
* @param object js(on) object that will be prettyPrinted
*/
public log(content: string, object: object = {}) {
if (this.ipcRenderer) {
this.ipcRenderer.send(globalEvents.log, { content, object });
} else {
console.log(`${content} \n ${JSON.stringify(object, null, 2)}`);
}
}
}

View File

@ -0,0 +1,21 @@
import { ServiceLinks } from "./ServiceLinks";
export interface Artist {
type: string;
id: number;
path: string;
name: string;
sourceUrl: string;
sourceCountry: string;
url: string;
image: string;
createdAt: string;
updatedAt: string;
refreshedAt: string;
serviceIds: { [key: string]: string };
orchardId: string;
spotifyId: string;
links: { [key: string]: ServiceLinks[] };
linksCountries: string[];
description: string;
}

View File

@ -0,0 +1,4 @@
export interface ServiceLinks {
link: string;
countries: string[];
}

View File

@ -0,0 +1,27 @@
import { Artist } from "./Artist";
import { ServiceLinks } from "./ServiceLinks";
export interface WhippedResult {
status: string;
data: {
item: {
type: string;
id: number;
path: string;
name: string;
url: string;
sourceUrl: string;
sourceCountry: string;
releaseDate: string;
createdAt: string;
updatedAt: string;
refreshedAt: string;
image: string;
isrc: string;
isExplicit: boolean;
links: { [key: string]: ServiceLinks[] };
linksCountries: string[];
artists: Artist[];
};
};
}

View File

@ -0,0 +1,32 @@
import { WhippedResult } from "./models/whip";
import axios from "axios";
export class Songwhip {
/**
* Call the songwhip API and create a shareable songwhip page
* @param currentUrl
* @returns
*/
public static async whip(currentUrl: string): Promise<WhippedResult> {
try {
const response = await axios.post("https://songwhip.com/api/songwhip/create", {
url: currentUrl,
// doesn't actually matter.. returns everything the same way anyway
country: "NL",
});
return response.data;
} catch (error) {
console.log(JSON.stringify(error));
}
}
/**
* Transform a songwhip response into a shareable url
* @param response
* @returns
*/
public static getWhipUrl(response: WhippedResult) {
return `https://songwhip.com${response.data.item.url}`;
}
}

View File

@ -26,6 +26,8 @@ import {
import { settings } from "./constants/settings"; import { settings } from "./constants/settings";
import { addTray, refreshTray } from "./scripts/tray"; import { addTray, refreshTray } from "./scripts/tray";
import { MediaInfo } from "./models/mediaInfo"; import { MediaInfo } from "./models/mediaInfo";
import { Songwhip } from "./features/songwhip/songwhip";
import { Logger } from "./features/logger";
const tidalUrl = "https://listen.tidal.com"; const tidalUrl = "https://listen.tidal.com";
initialize(); initialize();
@ -220,3 +222,9 @@ ipcMain.on(globalEvents.storeChanged, () => {
ipcMain.on(globalEvents.error, (event) => { ipcMain.on(globalEvents.error, (event) => {
console.log(event); console.log(event);
}); });
ipcMain.handle(globalEvents.whip, async (event, url) => {
return await Songwhip.whip(url);
});
Logger.watch(ipcMain);

View File

@ -1,4 +1,4 @@
import remote, { app } from "@electron/remote"; import { app } from "@electron/remote";
import { ipcRenderer, shell } from "electron"; import { ipcRenderer, shell } from "electron";
import fs from "fs"; import fs from "fs";
import { globalEvents } from "../../constants/globalEvents"; import { globalEvents } from "../../constants/globalEvents";
@ -107,8 +107,8 @@ function hide() {
* Restart tidal-hifi after changes * Restart tidal-hifi after changes
*/ */
function restart() { function restart() {
remote.app.relaunch(); app.relaunch();
remote.app.exit(); app.exit();
} }
/** /**

View File

@ -1,16 +1,17 @@
import { Notification, app, dialog } from "@electron/remote"; import { app, dialog, Notification } from "@electron/remote";
import { ipcRenderer } from "electron"; import { clipboard, ipcRenderer } from "electron";
import fs from "fs"; 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 { statuses } from "./constants/statuses";
import { Songwhip } from "./features/songwhip/songwhip";
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";
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 = "";
@ -25,13 +26,12 @@ const elements = {
title: '*[data-test^="footer-track-title"]', title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]', artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]', home: '*[data-test="menu--home"]',
back: '[class^="backwardButton"]', back: '[title^="Back"]',
forward: '[class^="forwardButton"]', forward: '[title^="Next"]',
search: '[class^="searchField"]', search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]', shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]', repeat: '*[data-test="repeat"]',
block: '[class="blockButton"]', account: '*[class^="profileOptions"]',
account: '*[data-test^="profile-image-button"]',
settings: '*[data-test^="open-settings"]', settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]', media: '*[data-test="current-media-imagery"]',
image: "img", image: "img",
@ -39,9 +39,10 @@ const elements = {
duration: '*[data-test="duration"]', duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]', bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer", footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]', album_header_title: '.header-details [data-test="title"]',
playing_title: 'span[data-test="table-cell-title"].css-1vjc1xk', currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[data-test="table-cell-album"]', album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]', tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]', volume: '*[data-test="volume"]',
/** /**
@ -105,7 +106,9 @@ const elements = {
window.location.href.includes("/mix/") window.location.href.includes("/mix/")
) { ) {
if (currentPlayStatus === statuses.playing) { if (currentPlayStatus === statuses.playing) {
const row = window.document.querySelector(this.playing_title).closest(this.tracklist_row); // 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);
if (row) { if (row) {
return row.querySelector(this.album_name_cell).textContent; return row.querySelector(this.album_name_cell).textContent;
} }
@ -206,7 +209,10 @@ function playPause() {
function addHotKeys() { function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) { if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () { addHotkey("Control+p", function () {
elements.click("account").click("settings"); elements.click("account");
setTimeout(() => {
elements.click("settings");
}, 100);
}); });
addHotkey("Control+l", function () { addHotkey("Control+l", function () {
handleLogout(); handleLogout();
@ -232,6 +238,15 @@ function addHotKeys() {
addHotkey("control+r", function () { addHotkey("control+r", function () {
elements.click("repeat"); elements.click("repeat");
}); });
addHotkey("control+w", async function () {
const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL());
const url = Songwhip.getWhipUrl(result);
clipboard.writeText(url);
new Notification({
title: `Successfully whipped: `,
body: `URL copied to clipboard: ${url}`,
}).show();
});
} }
// always add the hotkey for the settings window // always add the hotkey for the settings window

View File

@ -1,8 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"typeRoots": ["src/types"], "typeRoots": ["src/types", "node_modules/@types"],
"module": "commonjs", "module": "commonjs",
"target": "ES6", "target": "ES6",
"lib": ["ES2020", "DOM"],
"noImplicitAny": true, "noImplicitAny": true,
"sourceMap": true, "sourceMap": true,
"allowJs": true, "allowJs": true,