Compare commits
9 Commits
1761c8dd40
...
8036cbb919
Author | SHA1 | Date | |
---|---|---|---|
8036cbb919 | |||
90cf231c76 | |||
42a70534f2 | |||
b07865d98b | |||
cc26bfa080 | |||
822bdf401e | |||
a169c57a52 | |||
60eb1bbef9 | |||
62244f432a |
@ -10,6 +10,10 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[**.ts]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[**.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
4
.github/workflows/build.yml
vendored
@ -5,6 +5,10 @@ on:
|
||||
branches-ignore:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- master
|
||||
- develop
|
||||
jobs:
|
||||
build_on_linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
12
.github/workflows/release.yml
vendored
@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_on_linux:
|
||||
runs-on: ubuntu-latest
|
||||
@ -16,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 19
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@master
|
||||
@ -25,12 +29,12 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
build_on_mac:
|
||||
runs-on: macOS-latest
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 19
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@master
|
||||
@ -44,7 +48,7 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 19
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@master
|
||||
|
1
.gitignore
vendored
@ -16,3 +16,4 @@ ts-dist/**
|
||||
ts-dist
|
||||
themes
|
||||
!src/themes
|
||||
.sass-cache
|
||||
|
131
README.md
@ -1,42 +1,80 @@
|
||||
# Tidal-hifi<img src = "./build/icon.png" height="40" align="right"/>
|
||||
|
||||

|
||||
 [](https://discord.gg/yhNwf4v4He)
|
||||
|
||||
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
|
||||
|
||||

|
||||

|
||||
|
||||
## Table of Contents
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Using releases](#using-releases)
|
||||
- [Snap](#snap)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Flatpak](#flatpak)
|
||||
- [Nix](#nix)
|
||||
- [Using source](#using-source)
|
||||
- [Features](#features)
|
||||
- [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)
|
||||
- [Why](#why)
|
||||
- [Why not extend existing projects?](#why-not-extend-existing-projects)
|
||||
- [Special thanks to](#special-thanks-to)
|
||||
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont)
|
||||
- [Images](#images)
|
||||
- [Settings window](#settings-window)
|
||||
- [User setups](#user-setups)
|
||||
- [Tidal-hifi](#tidal-hifi)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Contributions](#contributions)
|
||||
- [Why did I create tidal-hifi?](#why-did-i-create-tidal-hifi)
|
||||
- [Why not extend existing projects?](#why-not-extend-existing-projects)
|
||||
- [Installation](#installation)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Using releases](#using-releases)
|
||||
- [Snap](#snap)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Flatpak](#flatpak)
|
||||
- [Nix](#nix)
|
||||
- [Using source](#using-source)
|
||||
- [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)
|
||||
- [Special thanks to](#special-thanks-to)
|
||||
- [Donations](#donations)
|
||||
- [Images](#images)
|
||||
- [Settings window](#settings-window)
|
||||
- [User setups](#user-setups)
|
||||
|
||||
<!-- 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.
|
||||
|
||||
-  [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
|
||||
|
||||
### 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
|
||||
|
||||
@ -48,15 +86,15 @@ To install with `snap` you need to download the pre-packaged snap-package from t
|
||||
|
||||
1. Download
|
||||
|
||||
```sh
|
||||
wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap
|
||||
```
|
||||
```sh
|
||||
wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap
|
||||
```
|
||||
|
||||
2. Install
|
||||
|
||||
```sh
|
||||
snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap
|
||||
```
|
||||
```sh
|
||||
snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
@ -91,23 +129,12 @@ To install and work with the code on this project follow these steps:
|
||||
- npm install
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
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).
|
||||
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
|
||||
|
||||
- [Castlabs](https://castlabs.com/)
|
||||
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).
|
||||
Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429)
|
||||
You can find my Github sponsorship page at: [https://github.com/sponsors/Mastermindzh](https://github.com/sponsors/Mastermindzh)
|
||||
|
||||
## Images
|
||||
|
||||
### Settings window
|
||||
|
||||

|
||||

|
||||
|
||||
### User setups
|
||||
|
||||
|
BIN
docs/images/customcss-config.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
docs/images/customcss.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/images/discord.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 800 KiB After Width: | Height: | Size: 800 KiB |
Before Width: | Height: | Size: 726 KiB After Width: | Height: | Size: 726 KiB |
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 317 KiB |
BIN
docs/images/theming.png
Normal file
After Width: | Height: | Size: 49 KiB |
38
docs/theming.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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`.
|
||||
|
||||

|
||||
|
||||
## config
|
||||
|
||||
The theme selector and customCSS are stored in the config file.
|
||||
The custom CSS is stored as a list of lines.
|
||||
|
||||

|
||||
|
||||
## 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).
|
@ -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 }],
|
||||
disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }],
|
||||
};
|
||||
|
@ -1,9 +1,10 @@
|
||||
import remote from "@electron/remote";
|
||||
import remote, { app } from "@electron/remote";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
import fs from "fs";
|
||||
import { globalEvents } from "../../constants/globalEvents";
|
||||
import { settings } from "../../constants/settings";
|
||||
import { settingsStore } from "./../../scripts/settings";
|
||||
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
|
||||
|
||||
let adBlock: HTMLInputElement,
|
||||
api: HTMLInputElement,
|
||||
@ -25,23 +26,25 @@ let adBlock: HTMLInputElement,
|
||||
theme: HTMLSelectElement,
|
||||
trayIcon: HTMLInputElement,
|
||||
updateFrequency: HTMLInputElement;
|
||||
|
||||
function getThemeFiles() {
|
||||
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
|
||||
const fileNames = fs
|
||||
.readdirSync(process.resourcesPath)
|
||||
.filter((file) => file.endsWith(".css"))
|
||||
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
|
||||
const userThemes = getThemeListFromDirectory(`${app.getPath("userData")}/themes`);
|
||||
|
||||
const options = fileNames.map((name) => {
|
||||
return new Option(name.replace(".css", ""), name);
|
||||
});
|
||||
let allThemes = [
|
||||
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
|
||||
const oldOptions = document.querySelectorAll("#themesList option");
|
||||
oldOptions.forEach((o) => o.remove());
|
||||
|
||||
[new Option("Tidal - Default", "none")].concat(options).forEach((option) => {
|
||||
allThemes.forEach((option) => {
|
||||
selectElement.add(option, null);
|
||||
});
|
||||
}
|
||||
@ -52,7 +55,7 @@ function handleFileUploads() {
|
||||
|
||||
document.getElementById("theme-files").addEventListener("change", function (e: any) {
|
||||
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);
|
||||
});
|
||||
fileMessage.innerText = `${e.target.files.length} files successfully uploaded`;
|
||||
|
@ -434,5 +434,12 @@ html {
|
||||
|
||||
option {
|
||||
background-color: $tidal-grey-darkest;
|
||||
|
||||
&:disabled {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
55
src/pages/settings/theming.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -14,7 +14,7 @@ import { setTitle } from "./scripts/window-functions";
|
||||
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
|
||||
const appName = "Tidal Hifi";
|
||||
let currentSong = "";
|
||||
let player: any;
|
||||
let player: Player;
|
||||
let currentPlayStatus = statuses.paused;
|
||||
|
||||
const elements = {
|
||||
|
@ -18,7 +18,7 @@ export const startExpress = (mainWindow: BrowserWindow) => {
|
||||
* @param {*} res
|
||||
* @param {*} action
|
||||
*/
|
||||
function handleGlobalEvent(res: Response, action: any) {
|
||||
function handleGlobalEvent(res: Response, action: string) {
|
||||
mainWindow.webContents.send("globalEvent", action);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export const createSettingsWindow = function () {
|
||||
settingsWindow = new BrowserWindow({
|
||||
width: 700,
|
||||
height: 600,
|
||||
resizable: false,
|
||||
resizable: true,
|
||||
show: false,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
@ -67,7 +67,7 @@ export const createSettingsWindow = function () {
|
||||
},
|
||||
});
|
||||
|
||||
settingsWindow.on("close", (event: any) => {
|
||||
settingsWindow.on("close", (event: Event) => {
|
||||
if (settingsWindow != null) {
|
||||
event.preventDefault();
|
||||
settingsWindow.hide();
|
||||
|
60
src/types/mpris-service.d.ts
vendored
Normal 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;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"typeRoots": ["src/types"],
|
||||
"module": "commonjs",
|
||||
"target": "ES6",
|
||||
"noImplicitAny": true,
|
||||
|