mirror of
https://github.com/Mastermindzh/tidal-hifi.git
synced 2024-11-25 14:53:41 +01:00
commit
11cc209025
@ -13,4 +13,4 @@ steps:
|
||||
commands:
|
||||
- apt-get update && apt-get upgrade -y
|
||||
- apt-get install -y libarchive-tools rpm
|
||||
- npm run build
|
||||
- npm run build-unpacked
|
||||
|
@ -1,5 +1,9 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -5,6 +5,7 @@
|
||||
"hifi",
|
||||
"playpause",
|
||||
"rescrobbler",
|
||||
"Songwhip",
|
||||
"trackid",
|
||||
"tracklist",
|
||||
"widevine",
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -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/),
|
||||
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
|
||||
|
||||
- SPKChaosPhoenix updated the beautiful Tokyo Night theme:
|
||||
|
13
README.md
13
README.md
@ -27,6 +27,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
|
||||
- [Integrations](#integrations)
|
||||
- [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)
|
||||
- [DRM not working on Windows](#drm-not-working-on-windows)
|
||||
- [Special thanks to](#special-thanks-to)
|
||||
- [Donations](#donations)
|
||||
- [Images](#images)
|
||||
@ -41,6 +42,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
|
||||
- Notifications
|
||||
- Custom [theming](./docs/theming.md)
|
||||
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
|
||||
- Songwhip.com integration (hotkey `ctrl + w`)
|
||||
- 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))
|
||||
- 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)
|
||||
- Most are simple web wrappers, not my cup of tea.
|
||||
- Some are DE oriented. I want this to work on WM's too.
|
||||
- None have widevine working at the moment
|
||||
- Some are DE-oriented. I want this to work on WM's too.
|
||||
- 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
|
||||
|
||||
@ -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).
|
||||
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
|
||||
|
||||
- [Castlabs](https://castlabs.com/)
|
||||
|
43
listen.tidal.com-parsing-scripts/verifyElements.js
Normal file
43
listen.tidal.com-parsing-scripts/verifyElements.js
Normal 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
1487
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "tidal-hifi",
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.0",
|
||||
"description": "Tidal on Electron with widevine(hifi) support",
|
||||
"main": "ts-dist/main.js",
|
||||
"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",
|
||||
"watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
|
||||
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
|
||||
@ -29,41 +30,46 @@
|
||||
"electron",
|
||||
"hifi",
|
||||
"widevine",
|
||||
"linux"
|
||||
"linux",
|
||||
"drm",
|
||||
"castlabs"
|
||||
],
|
||||
"author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)",
|
||||
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.9",
|
||||
"@electron/remote": "^2.0.10",
|
||||
"axios": "^1.4.0",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"express": "^4.18.2",
|
||||
"hotkeys-js": "^3.10.2",
|
||||
"hotkeys-js": "^3.11.2",
|
||||
"mpris-service": "^2.1.2",
|
||||
"request": "^2.88.2",
|
||||
"sass": "^1.62.0"
|
||||
"sass": "^1.64.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mastermindzh/prettier-config": "^1.0.0",
|
||||
"@types/discord-rpc": "^4.0.4",
|
||||
"@types/discord-rpc": "^4.0.5",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.4.4",
|
||||
"@types/request": "^2.48.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus",
|
||||
"electron-builder": "^24.2.1",
|
||||
"eslint": "^8.39.0",
|
||||
"electron-builder": "^24.4.0",
|
||||
"eslint": "^8.45.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"stylelint-config-standard-scss": "^9.0.0",
|
||||
"stylelint-prettier": "^3.0.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.0.0",
|
||||
"stylelint": "^15.10.2",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-config-standard-scss": "^10.0.0",
|
||||
"stylelint-prettier": "^4.0.0",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"prettier": "@mastermindzh/prettier-config"
|
||||
}
|
||||
|
@ -10,4 +10,6 @@ export const globalEvents = {
|
||||
showSettings: "showSettings",
|
||||
storeChanged: "storeChanged",
|
||||
error: "error",
|
||||
whip: "whip",
|
||||
log: "log",
|
||||
};
|
||||
|
33
src/features/logger.ts
Normal file
33
src/features/logger.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
21
src/features/songwhip/models/Artist.ts
Normal file
21
src/features/songwhip/models/Artist.ts
Normal 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;
|
||||
}
|
4
src/features/songwhip/models/ServiceLinks.ts
Normal file
4
src/features/songwhip/models/ServiceLinks.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface ServiceLinks {
|
||||
link: string;
|
||||
countries: string[];
|
||||
}
|
27
src/features/songwhip/models/whip.ts
Normal file
27
src/features/songwhip/models/whip.ts
Normal 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[];
|
||||
};
|
||||
};
|
||||
}
|
32
src/features/songwhip/songwhip.ts
Normal file
32
src/features/songwhip/songwhip.ts
Normal 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}`;
|
||||
}
|
||||
}
|
@ -26,6 +26,8 @@ import {
|
||||
import { settings } from "./constants/settings";
|
||||
import { addTray, refreshTray } from "./scripts/tray";
|
||||
import { MediaInfo } from "./models/mediaInfo";
|
||||
import { Songwhip } from "./features/songwhip/songwhip";
|
||||
import { Logger } from "./features/logger";
|
||||
const tidalUrl = "https://listen.tidal.com";
|
||||
|
||||
initialize();
|
||||
@ -220,3 +222,9 @@ ipcMain.on(globalEvents.storeChanged, () => {
|
||||
ipcMain.on(globalEvents.error, (event) => {
|
||||
console.log(event);
|
||||
});
|
||||
|
||||
ipcMain.handle(globalEvents.whip, async (event, url) => {
|
||||
return await Songwhip.whip(url);
|
||||
});
|
||||
|
||||
Logger.watch(ipcMain);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import remote, { app } from "@electron/remote";
|
||||
import { app } from "@electron/remote";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
import fs from "fs";
|
||||
import { globalEvents } from "../../constants/globalEvents";
|
||||
@ -107,8 +107,8 @@ function hide() {
|
||||
* Restart tidal-hifi after changes
|
||||
*/
|
||||
function restart() {
|
||||
remote.app.relaunch();
|
||||
remote.app.exit();
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { Notification, app, dialog } from "@electron/remote";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { app, dialog, Notification } from "@electron/remote";
|
||||
import { clipboard, ipcRenderer } from "electron";
|
||||
import fs from "fs";
|
||||
import Player from "mpris-service";
|
||||
import { globalEvents } from "./constants/globalEvents";
|
||||
import { settings } from "./constants/settings";
|
||||
import { statuses } from "./constants/statuses";
|
||||
import { Songwhip } from "./features/songwhip/songwhip";
|
||||
import { Options } from "./models/options";
|
||||
import { downloadFile } from "./scripts/download";
|
||||
import { addHotkey } from "./scripts/hotkeys";
|
||||
|
||||
import { settingsStore } from "./scripts/settings";
|
||||
import { setTitle } from "./scripts/window-functions";
|
||||
|
||||
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
|
||||
const appName = "Tidal Hifi";
|
||||
let currentSong = "";
|
||||
@ -25,13 +26,12 @@ const elements = {
|
||||
title: '*[data-test^="footer-track-title"]',
|
||||
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
|
||||
home: '*[data-test="menu--home"]',
|
||||
back: '[class^="backwardButton"]',
|
||||
forward: '[class^="forwardButton"]',
|
||||
back: '[title^="Back"]',
|
||||
forward: '[title^="Next"]',
|
||||
search: '[class^="searchField"]',
|
||||
shuffle: '*[data-test="shuffle"]',
|
||||
repeat: '*[data-test="repeat"]',
|
||||
block: '[class="blockButton"]',
|
||||
account: '*[data-test^="profile-image-button"]',
|
||||
account: '*[class^="profileOptions"]',
|
||||
settings: '*[data-test^="open-settings"]',
|
||||
media: '*[data-test="current-media-imagery"]',
|
||||
image: "img",
|
||||
@ -39,9 +39,10 @@ const elements = {
|
||||
duration: '*[data-test="duration"]',
|
||||
bar: '*[data-test="progress-bar"]',
|
||||
footer: "#footerPlayer",
|
||||
mediaItem: "[data-type='mediaItem']",
|
||||
album_header_title: '.header-details [data-test="title"]',
|
||||
playing_title: 'span[data-test="table-cell-title"].css-1vjc1xk',
|
||||
album_name_cell: '[data-test="table-cell-album"]',
|
||||
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
|
||||
album_name_cell: '[class^="album"]',
|
||||
tracklist_row: '[data-test="tracklist-row"]',
|
||||
volume: '*[data-test="volume"]',
|
||||
/**
|
||||
@ -105,7 +106,9 @@ const elements = {
|
||||
window.location.href.includes("/mix/")
|
||||
) {
|
||||
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) {
|
||||
return row.querySelector(this.album_name_cell).textContent;
|
||||
}
|
||||
@ -206,7 +209,10 @@ function playPause() {
|
||||
function addHotKeys() {
|
||||
if (settingsStore.get(settings.enableCustomHotkeys)) {
|
||||
addHotkey("Control+p", function () {
|
||||
elements.click("account").click("settings");
|
||||
elements.click("account");
|
||||
setTimeout(() => {
|
||||
elements.click("settings");
|
||||
}, 100);
|
||||
});
|
||||
addHotkey("Control+l", function () {
|
||||
handleLogout();
|
||||
@ -232,6 +238,15 @@ function addHotKeys() {
|
||||
addHotkey("control+r", function () {
|
||||
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
|
||||
|
@ -1,8 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"typeRoots": ["src/types"],
|
||||
"typeRoots": ["src/types", "node_modules/@types"],
|
||||
"module": "commonjs",
|
||||
"target": "ES6",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
|
Loading…
Reference in New Issue
Block a user