Compare commits

...

9 Commits

24 changed files with 268 additions and 82 deletions

View File

@ -10,6 +10,10 @@ insert_final_newline = true
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[**.ts]
indent_style = space
indent_size = 2
[**.json] [**.json]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
@ -50,4 +54,4 @@ trim_trailing_whitespace = ignore
charset = ignore charset = ignore
[{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**] [{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**]
insert_final_newline = false insert_final_newline = false

View File

@ -5,6 +5,10 @@ on:
branches-ignore: branches-ignore:
- master - master
- develop - develop
pull_request:
branches-ignore:
- master
- develop
jobs: jobs:
build_on_linux: build_on_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -5,6 +5,10 @@ on:
branches: branches:
- master - master
- develop - develop
pull_request:
branches:
- master
jobs: jobs:
build_on_linux: build_on_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -16,7 +20,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
@ -25,12 +29,12 @@ jobs:
path: dist/ path: dist/
build_on_mac: build_on_mac:
runs-on: macOS-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
@ -44,7 +48,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 16 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ ts-dist/**
ts-dist ts-dist
themes themes
!src/themes !src/themes
.sass-cache

131
README.md
View File

@ -1,42 +1,80 @@
# Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/> # Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/>
![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg) ![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg) [![Discord logo](./docs/images/discord.png)](https://discord.gg/yhNwf4v4He)
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine. The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
![tidal-hifi preview](./docs/preview.png) ![tidal-hifi preview](./docs/images/preview.png)
## Table of Contents ## Table of Contents
<!-- toc --> <!-- toc -->
- [Installation](#installation) - [Tidal-hifi](#tidal-hifi)
- [Dependencies](#dependencies) - [Table of Contents](#table-of-contents)
- [Using releases](#using-releases) - [Features](#features)
- [Snap](#snap) - [Contributions](#contributions)
- [Arch Linux](#arch-linux) - [Why did I create tidal-hifi?](#why-did-i-create-tidal-hifi)
- [Flatpak](#flatpak) - [Why not extend existing projects?](#why-not-extend-existing-projects)
- [Nix](#nix) - [Installation](#installation)
- [Using source](#using-source) - [Dependencies](#dependencies)
- [Features](#features) - [Using releases](#using-releases)
- [Integrations](#integrations) - [Snap](#snap)
- [Known bugs](#known-bugs) - [Arch Linux](#arch-linux)
- [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) - [Flatpak](#flatpak)
- [Why](#why) - [Nix](#nix)
- [Why not extend existing projects?](#why-not-extend-existing-projects) - [Using source](#using-source)
- [Special thanks to](#special-thanks-to) - [Integrations](#integrations)
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont) - [Known bugs](#known-bugs)
- [Images](#images) - [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)
- [Settings window](#settings-window) - [Special thanks to](#special-thanks-to)
- [User setups](#user-setups) - [Donations](#donations)
- [Images](#images)
- [Settings window](#settings-window)
- [User setups](#user-setups)
<!-- tocstop --> <!-- tocstop -->
## Features
- HiFi playback
- Notifications
- Custom [theming](./docs/theming.md)
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- 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)
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
## Contributions
To contribute you can use the standard GitHub features (issues, prs, etc) or join the discord server to talk with like-minded individuals.
- ![Discord logo](./docs/images/discord.png) [Join the Discord server](https://discord.gg/yhNwf4v4He)
## Why did I create tidal-hifi?
I moved from Spotify over to Tidal and found Linux support to be lacking.
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it.
### Why not extend existing projects?
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
- 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
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :)
## Installation ## Installation
### Dependencies ### Dependencies
Note that you **need** a notification library such as [libnotify](https://github.com/GNOME/libnotify) or [dunst](https://github.com/dunst-project/dunst) in order for the software to work properly. Note that you **need** a notification library such as [libnotify](https://github.com/GNOME/libnotify) or [dunst](https://github.com/dunst-project/dunst) for the software to work properly.
### Using releases ### Using releases
@ -48,15 +86,15 @@ To install with `snap` you need to download the pre-packaged snap-package from t
1. Download 1. Download
```sh ```sh
wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap
``` ```
2. Install 2. Install
```sh ```sh
snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap
``` ```
### Arch Linux ### Arch Linux
@ -91,23 +129,12 @@ To install and work with the code on this project follow these steps:
- npm install - npm install
- npm start - npm start
## Features
- HiFi playback
- Notifications
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- 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)
- [Settings feature](./docs/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
## Integrations ## Integrations
Tidal-hifi comes with several integrations out of the box. Tidal-hifi comes with several integrations out of the box.
You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab. You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab.
![integrations menu, showing a list of integrations](./docs/integrations.png) ![integrations menu, showing a list of integrations](./docs/images/integrations.png)
It currently includes: It currently includes:
@ -126,38 +153,20 @@ 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.
## Why
I moved from Spotify over to Tidal and found Linux support to be lacking.
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it.
## Why not extend existing projects?
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
- Lack of a 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
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :)
## Special thanks to ## Special thanks to
- [Castlabs](https://castlabs.com/) - [Castlabs](https://castlabs.com/)
For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID) For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID)
## Buy me a coffee? Please don't ## Donations
Instead spend some money on a charity I care for: [kwf.nl](https://www.kwf.nl/donatie/donation). You can find my Github sponsorship page at: [https://github.com/sponsors/Mastermindzh](https://github.com/sponsors/Mastermindzh)
Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429)
## Images ## Images
### Settings window ### Settings window
![settings window](./docs/settings-preview.png) ![settings window](./docs/images/settings-preview.png)
### User setups ### User setups

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/images/customcss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/images/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 800 KiB

After

Width:  |  Height:  |  Size: 800 KiB

View File

Before

Width:  |  Height:  |  Size: 726 KiB

After

Width:  |  Height:  |  Size: 726 KiB

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

BIN
docs/images/theming.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

38
docs/theming.md Normal file
View File

@ -0,0 +1,38 @@
# Theming tidal-hifi
## Table of contents
<!-- toc -->
- [Theming tidal-hifi](#theming-tidal-hifi)
- [Table of contents](#table-of-contents)
- [Custom CSS](#custom-css)
- [config](#config)
- [Warning! Themes might break](#warning-themes-might-break)
<!-- tocstop -->
By default tidal-hifi comes with a few themes.
You can select these in the settings window under the theming tab as shown below.
![Settings window with the theming tab opened](./images/theming.png)
## Custom CSS
The custom CSS will be added to the HTML document last.
This means that it will overwrite any existing CSS, even that of themes, unless the original has an access modifier such as `$important`.
![settings window on the theming tab with a custom CSS override](./images/customcss.png)
## config
The theme selector and customCSS are stored in the config file.
The custom CSS is stored as a list of lines.
![settings window on the theming tab next to the config file](./images/customcss-config.png)
## Warning! Themes might break
Themes might break at any point. Tidal changes their webpage structure a ton (they probably generate classNames and don't provide roles/ids/attributes.)
If one breaks you can create an Issue on GitHub or ask for assistance in the [Discord channel](https://discord.gg/yhNwf4v4He).

View File

@ -1,4 +1,4 @@
export const flags: { [key: string]: { flag: string; value?: any }[] } = { export const flags: { [key: string]: { flag: string; value?: string }[] } = {
gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }], gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }],
disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }], disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }],
}; };

View File

@ -1,9 +1,10 @@
import remote from "@electron/remote"; import remote, { 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";
import { settings } from "../../constants/settings"; import { settings } from "../../constants/settings";
import { settingsStore } from "./../../scripts/settings"; import { settingsStore } from "./../../scripts/settings";
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
let adBlock: HTMLInputElement, let adBlock: HTMLInputElement,
api: HTMLInputElement, api: HTMLInputElement,
@ -25,23 +26,25 @@ let adBlock: HTMLInputElement,
theme: HTMLSelectElement, theme: HTMLSelectElement,
trayIcon: HTMLInputElement, trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement; updateFrequency: HTMLInputElement;
function getThemeFiles() { function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement; const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const fileNames = fs const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
.readdirSync(process.resourcesPath) const userThemes = getThemeListFromDirectory(`${app.getPath("userData")}/themes`);
.filter((file) => file.endsWith(".css"))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
const options = fileNames.map((name) => { let allThemes = [
return new Option(name.replace(".css", ""), name); getOptionsHeader("Built-in Themes"),
}); new Option("Tidal - Default", "none"),
].concat(getOptions(builtInThemes));
if (userThemes.length >= 1) {
allThemes = allThemes.concat([getOptionsHeader("User Themes")]).concat(getOptions(userThemes));
}
// empty old options // empty old options
const oldOptions = document.querySelectorAll("#themesList option"); const oldOptions = document.querySelectorAll("#themesList option");
oldOptions.forEach((o) => o.remove()); oldOptions.forEach((o) => o.remove());
[new Option("Tidal - Default", "none")].concat(options).forEach((option) => { allThemes.forEach((option) => {
selectElement.add(option, null); selectElement.add(option, null);
}); });
} }
@ -52,7 +55,7 @@ function handleFileUploads() {
document.getElementById("theme-files").addEventListener("change", function (e: any) { document.getElementById("theme-files").addEventListener("change", function (e: any) {
Array.from(e.target.files).forEach((file: File) => { Array.from(e.target.files).forEach((file: File) => {
const destination = `${process.resourcesPath}/${file.name}`; const destination = `${app.getPath("userData")}/themes/${file.name}`;
fs.copyFileSync(file.path, destination, null); fs.copyFileSync(file.path, destination, null);
}); });
fileMessage.innerText = `${e.target.files.length} files successfully uploaded`; fileMessage.innerText = `${e.target.files.length} files successfully uploaded`;

View File

@ -434,5 +434,12 @@ html {
option { option {
background-color: $tidal-grey-darkest; background-color: $tidal-grey-darkest;
&:disabled {
font-size: 1.2em;
line-height: 1.5em;
text-align: center;
color: $white;
}
} }
} }

View File

@ -0,0 +1,55 @@
import fs from "fs";
const cssFilter = (file: string) => file.endsWith(".css");
const sort = (a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase());
/**
* Create an "options header" (disabled option) based on a bit of text
* @param text of the header
* @returns
*/
export const getOptionsHeader = (text: string): HTMLOptionElement => {
const opt = new Option(text, undefined, false, false);
opt.disabled = true;
return opt;
};
/**
* Maps a list of filenames to a list of HTMLOptionElements
* Will strip ".css" from the name but keeps it in the value
* @param array array of filenames
* @returns
*/
export const getOptions = (array: string[]) => {
return array.map((name) => {
return new Option(name.replace(".css", ""), name);
});
};
/**
* Read .css files from a directory and return them in a sorted array.
* @param directory to read from. Will be created if it doesn't exist
* @returns
*/
export const getThemeListFromDirectory = (directory: string) => {
try {
makeUserThemesDirectory(directory);
return fs.readdirSync(directory).filter(cssFilter).sort(sort);
} catch (err) {
console.error(err);
}
};
/**
* Create the directory to store user themes in
* @param directory directory to create
*/
export const makeUserThemesDirectory = (directory: string) => {
try {
fs.mkdir(directory, { recursive: true }, (err) => {
if (err) throw err;
});
} catch (err) {
console.error(err);
}
};

View File

@ -14,7 +14,7 @@ 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 = "";
let player: any; let player: Player;
let currentPlayStatus = statuses.paused; let currentPlayStatus = statuses.paused;
const elements = { const elements = {

View File

@ -18,7 +18,7 @@ export const startExpress = (mainWindow: BrowserWindow) => {
* @param {*} res * @param {*} res
* @param {*} action * @param {*} action
*/ */
function handleGlobalEvent(res: Response, action: any) { function handleGlobalEvent(res: Response, action: string) {
mainWindow.webContents.send("globalEvent", action); mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200); res.sendStatus(200);
} }

View File

@ -55,7 +55,7 @@ export const createSettingsWindow = function () {
settingsWindow = new BrowserWindow({ settingsWindow = new BrowserWindow({
width: 700, width: 700,
height: 600, height: 600,
resizable: false, resizable: true,
show: false, show: false,
transparent: true, transparent: true,
frame: false, frame: false,
@ -67,7 +67,7 @@ export const createSettingsWindow = function () {
}, },
}); });
settingsWindow.on("close", (event: any) => { settingsWindow.on("close", (event: Event) => {
if (settingsWindow != null) { if (settingsWindow != null) {
event.preventDefault(); event.preventDefault();
settingsWindow.hide(); settingsWindow.hide();

60
src/types/mpris-service.d.ts vendored Normal file
View File

@ -0,0 +1,60 @@
declare class InitOptions {
name: string;
identity: string;
supportedUriSchemes: string[];
supportedMimeTypes: string[];
supportedInterfaces: string[];
desktopEntry: string;
}
declare class Player {
metadata: {
"xesam:title": string;
"xesam:artist": string[];
"xesam:album": string;
"mpris:artUrl": string;
"mpris:length": number;
"mpris:trackid": string;
// other options
[key: string]: string | number | string[] | object;
};
playbackStatus: string;
identity: string;
fullscreen: boolean;
supportedUriSchemes: string[];
supportedMimeTypes: string[];
canQuit: boolean;
canRaise: boolean;
canSetFullscreen: boolean;
hasTrackList: boolean;
desktopEntry: string;
loopStatus: string;
shuffle: boolean;
volume: number;
canControl: boolean;
canPause: boolean;
canPlay: boolean;
canSeek: boolean;
canGoNext: boolean;
canGoPrevious: boolean;
rate: number;
minimumRate: number;
maximumRate: number;
playlists: string[];
activePlaylist: string;
constructor(opts: { name: string; supportedInterfaces?: string[] });
constructor(opts: InitOptions);
getPosition(): number;
seeked(): void;
getTrackIndex(trackId: number): number;
getTrack(trackId: number): string;
addTrack(track: object): void;
removeTrack(trackId: number): number;
getPlaylistIndex(playlistId: number): number;
setPlaylists(playlists: object): void;
setActivePlaylist(playlistId: number): void;
on(event: string | symbol, listener: (...args: object[]) => void): this;
}

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"typeRoots": ["src/types"],
"module": "commonjs", "module": "commonjs",
"target": "ES6", "target": "ES6",
"noImplicitAny": true, "noImplicitAny": true,