Compare commits

...

59 Commits

Author SHA1 Message Date
dc87b20ab8 Merge pull request #265 from Mastermindzh/feature/5.6.0
Feature/5.6.0
2023-08-12 15:42:27 +02:00
c7b3921514 Added app suspension inhibitors when music is playing. fixes #257 2023-08-12 15:02:23 +02:00
89f1ff4228 Create SECURITY.md 2023-08-12 14:30:10 +02:00
a0c73596e4 feat: added wayland support. fixes #262 #157 2023-08-07 20:48:29 +02:00
aa17d80450 added new quality names to readme + added neptune mention. fixes #261 2023-08-07 20:28:14 +02:00
5ea3972053 fixed errors with user theme files loading 2023-08-07 20:04:06 +02:00
4b81378423 fixed feature flag parsing & setting 2023-08-07 19:48:29 +02:00
c7931cf913 simplified logger 2023-08-07 19:45:44 +02:00
c6dff0b0e5 Merge pull request #260 from Mastermindzh/5.5.0
5.5.0
2023-07-31 21:13:39 +02:00
644beea2a6 fixed listenbrainz link 2023-07-31 21:13:24 +02:00
df1c45982b 5.5.0 docs, versions, etc 2023-07-31 15:49:29 +02:00
ec82aa8401 Merge pull request #258 from Mar0xy/master2
Add ListenBrainz implementation
2023-07-31 15:06:39 +02:00
586f7b595b various code improvements and some boyscout rule fixes :) 2023-07-31 13:43:32 +02:00
Mar0xy
de8a5a1b07 Fix bug where it does not run if condition 2023-07-31 12:14:06 +02:00
Mar0xy
38c1f05c35 Allow listenbrainz to be triggered on every play 2023-07-31 12:06:31 +02:00
Mar0xy
ed6f04b6d4 Fix bug where it does not unhide 2023-07-30 21:25:35 +02:00
Mar0xy
ffe8278c8c Fix complainy by sonarcloud 2023-07-30 21:22:38 +02:00
Mar0xy
e9434cc5ea Hide/Show ListenBrainz settings 2023-07-30 21:18:25 +02:00
Marie
d81912db0c Fix music_service domain 2023-07-30 11:46:27 +02:00
Mar0xy
c0110632e6 Seperate old ListenBrainz data from config 2023-07-30 10:42:32 +02:00
Mar0xy
3571289d28 Add ListenBrainz implementation 2023-07-30 02:38:38 +02:00
11cc209025 Merge pull request #255 from Mastermindzh/feature/5.4.0
Feature/5.4.0
2023-07-24 21:59:57 +02:00
f5ccbda7d9 set versions to 5.4.0 2023-07-24 12:04:08 +02:00
e8cf1783e8 chore(docs): updated README spelling 2023-07-23 23:59:45 +02:00
8037a73e57 Merge branch 'feature/5.4.0' of github.com:Mastermindzh/tidal-hifi into feature/5.4.0 2023-07-23 23:51:24 +02:00
45e191dae0 updated dependencies 2023-07-23 23:51:20 +02:00
f147536b12 updated dependencies 2023-07-23 23:43:28 +02:00
d03bb58afa removed windows builds from publishes 2023-07-23 23:20:01 +02:00
a39fef8d49 fix(hotkeys): Fixed bug with several hotkeys not working due to Tidal's HTML/css changes. fixes #250 2023-07-23 23:13:37 +02:00
41ca1d5a43 added songwhip 2023-07-23 23:12:18 +02:00
6969de8270 added several dev improvements 2023-07-23 23:07:19 +02:00
ad05b767d8 Merge pull request #245 from Mastermindzh/dependabot/npm_and_yarn/stylelint-15.10.1
chore(deps-dev): bump stylelint from 15.6.0 to 15.10.1
2023-07-09 00:38:31 +02:00
dependabot[bot]
6d873ce287 chore(deps-dev): bump stylelint from 15.6.0 to 15.10.1
Bumps [stylelint](https://github.com/stylelint/stylelint) from 15.6.0 to 15.10.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/15.6.0...15.10.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 22:30:49 +00:00
63d123f96a Merge pull request #241 from Mastermindzh/release/5.3.0
Release/5.3.0
2023-06-24 13:05:52 +02:00
f038412c50 release 5.3.0 2023-06-24 12:41:41 +02:00
ff02287df7 Merge pull request #240 from SPKChaosPhoenix/patch-1
Update Tokyo Night.scss
2023-06-23 15:19:31 +02:00
Marces
f221ded108 Update Tokyo Night.scss
Updatet Tokyo Night to work with the newest version of Tidal.
2023-06-22 16:12:41 +02:00
1440f70100 Merge pull request #237 from Mastermindzh/release/5.2.0
Release/5.2.0
2023-06-18 21:39:30 +02:00
439333e15a Merge pull request #236 from Mastermindzh/feature/drone
added drone builds
2023-06-18 20:25:55 +02:00
b9854e0595 Merge pull request #200 from drom98/fix-album-not-updating
Fix album not updating on playlists
2023-06-18 19:45:45 +02:00
8b56c28d75 Merge branch 'release/5.2.0' of github.com:Mastermindzh/tidal-hifi into fix-album-not-updating 2023-06-18 15:45:01 +02:00
700a14fe88 Merge pull request #227 from Mastermindzh/feature/theming
Feature/theming
2023-06-18 15:42:59 +02:00
3c835077d5 added drone builds 2023-06-18 15:39:14 +02:00
194de286c8 fix: customCSS default value was still a string, causing new users to have settings issues 2023-05-18 17:56:45 +02:00
a7dee5c2c9 fix: settings window was unresponsive on first start because of fs.mkdir that wasn't awaited 2023-05-16 23:41:00 +02:00
8036cbb919 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-14 23:49:25 +02:00
90cf231c76 fix: user uploaded themes are now stored in the config directory. Missing directories will be created and added docs for theming 2023-05-14 23:48:13 +02:00
42a70534f2 --amend 2023-05-14 14:42:05 +02:00
b07865d98b removed sass-cache 2023-05-14 14:41:30 +02:00
cc26bfa080 Merge pull request #225 from Mastermindzh/feature/typescript
Feature/typescript
2023-05-14 14:40:20 +02:00
822bdf401e Merge branch 'feature/typescript' of github.com:Mastermindzh/tidal-hifi into feature/theming 2023-05-13 22:55:10 +02:00
a169c57a52 don't run double builds on PR, just release builds (for pre-releases + testing) 2023-05-13 22:47:36 +02:00
60eb1bbef9 chore: removed last 'any' types + added declaration for mpris-service's Player class 2023-05-13 22:45:15 +02:00
1761c8dd40 feat: theme files are now loaded & applied on startup 2023-05-10 22:07:11 +02:00
62244f432a ci: release now also runs on feature branches (for test builds) 2023-05-10 08:48:13 +02:00
a408a6a8cc theme: added Tokyo Night by https://github.com/wojciech-zurek 2023-05-10 00:02:19 +02:00
6e5a2c626c feat: theme selection is now stored in the config file 2023-05-09 23:57:16 +02:00
4350ab9bd9 added styling on theme selector 2023-05-09 23:28:45 +02:00
Diogo Oliveira
0120391418 Fix album not updating on playlists 2023-01-30 17:49:04 +00:00
52 changed files with 2472 additions and 997 deletions

16
.drone.yml Normal file
View File

@@ -0,0 +1,16 @@
kind: pipeline
type: docker
name: default
steps:
- name: install
image: node:19.4.0
commands:
- npm install
- name: build_with_linux
image: node:19.4.0
commands:
- apt-get update && apt-get upgrade -y
- apt-get install -y libarchive-tools rpm
- npm run build-unpacked

View File

@@ -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
@@ -50,4 +54,4 @@ trim_trailing_whitespace = ignore
charset = ignore
[{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**]
insert_final_newline = false
insert_final_newline = false

View File

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

View File

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

View File

@@ -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

3
.gitignore vendored
View File

@@ -9,10 +9,11 @@ build/linux/arch/*
!build/linux/arch/install.sh
*.css
*.css.map
!src/themes/**/**.css
# JetBrains IDE configuration
.idea
ts-dist/**
ts-dist
themes
!src/themes
.sass-cache

View File

@@ -1,10 +1,17 @@
{
"cSpell.words": [
"Brainz",
"Castlabs",
"flac",
"Flatpak",
"geqnfr",
"hifi",
"listenbrainz",
"playpause",
"rescrobbler",
"scrobble",
"scrobbling",
"Songwhip",
"trackid",
"tracklist",
"widevine",

View File

@@ -4,12 +4,44 @@ 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.6.0]
- Added support for Wayland (on by default) fixes [#262](https://github.com/Mastermindzh/tidal-hifi/issues/262) and [#157](https://github.com/Mastermindzh/tidal-hifi/issues/157)
- Made it clear in the readme that this tidal-hifi client supports High & Max audio settings. fixes [#261](https://github.com/Mastermindzh/tidal-hifi/issues/261)
- Added app suspension inhibitors when music is playing. fixes [#257](https://github.com/Mastermindzh/tidal-hifi/issues/257)
- Fixed bug with theme files from user directory trying to load: "an error occurred reading the theme file"
- Fixed: config flags not being set correctly
- [DEV]:
- Logger is now static and will automatically call either ipcRenderer or ipcMain
## 5.5.0
- ListenBrainz integration added (thanks @Mar0xy)
## 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:
![tidal with the tokyo night theme applied](./docs/images/tokyo-night.png)
## 5.2.0
- moved from Javascript to Typescript for all files
- use `npm run watch` to watch for changes & recompile typescript and sass files
- Added support for theming the application
- Added drone build file use `drone exec` or drone.ci to build it
## 5.1.0

151
README.md
View File

@@ -1,42 +1,85 @@
# 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) [![github builds](https://github.com/mastermindzh/tidal-hifi/actions/workflows/build.yml/badge.svg)](https://github.com/Mastermindzh/tidal-hifi/actions) [![Build Status](https://ci.mastermindzh.tech/api/badges/Mastermindzh/tidal-hifi/status.svg)](https://ci.mastermindzh.tech/Mastermindzh/tidal-hifi) [![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 (High & Max) support thanks to widevine.
![tidal-hifi preview](./docs/preview.png)
![tidal-hifi preview](./docs/images/preview.png)
## 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)
- [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)
- [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)
- [DRM not working on Windows](#drm-not-working-on-windows)
- [Special thanks to](#special-thanks-to)
- [Donations](#donations)
- [Images](#images)
- [Settings window](#settings-window)
- [User setups](#user-setups)
<!-- tocstop -->
## Features
- HiFi playback (High & Max settings)
- Notifications
- Custom [theming](./docs/theming.md)
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- 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))
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
- Custom [integrations](#integrations)
- [ListenBrainz](https://listenbrainz.org/?redirect=false) integration
- Songwhip.com integration (hotkey `ctrl + w`)
- Discord RPC integration (showing "now listening", "Browsing", etc)
- MPRIS integration
## 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.
I made this app to support the highest quality audio available on the Linux platform. It used to be "hifi" but now is ["High & Max"](https://tidal.com/sound-quality).
### 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 after that 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 +91,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,73 +134,45 @@ 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.
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.
![integrations menu, showing a list of integrations](./docs/integrations.png)
![integrations menu, showing a list of integrations](./docs/images/integrations.png)
It currently includes:
- MPRIS - MPRIS media player controls/status
- Discord - Shows what you're listening to on Discord.
Not included:
Integrations with other projects that are not included natively:
- [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit)
- [neptune](https://github.com/uwu/neptune) third party plugins & theming
### Known bugs
## Known bugs
#### last.fm doesn't work out of the box. Use rescrobbler as a workaround
### last.fm doesn't work out of the box. Use rescrobbler as a workaround
The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4).
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
### DRM not working on Windows
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 :)
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/)
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
![settings window](./docs/settings-preview.png)
![settings window](./docs/images/settings-preview.png)
### User setups

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
Only the very latest 😄.
## Reporting a Vulnerability
If you find a vulnerability just add it as an issue.
If there's an especially bad vulnerability that you don't want to make public just send me a private message (email, discord, wherever).

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

BIN
docs/images/tokyo-night.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

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

@@ -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;
})();

1990
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,16 @@
{
"name": "tidal-hifi",
"version": "5.2.0",
"version": "5.6.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",
"sass-and-copy": "npm run sass && npm run copy-files",
"copy-themes-dev": "copyfiles -u 1 \"./themes/*\" node_modules/electron/dist/resources",
"sass-and-copy": "npm run sass && npm run copy-files && npm run copy-themes-dev",
"build": "npm run builder -- -c ./build/electron-builder.yml",
"build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml",
"build-unpacked": "npm run builder -- -c ./build/electron-builder.unpacked.yml",
@@ -28,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.6.0",
"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"
}
}

View File

@@ -1,4 +1,9 @@
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" }],
enableWaylandSupport: [
{ flag: "enable-features", value: "UseOzonePlatform" },
{ flag: "ozone-platform-hint", value: "auto" },
{ flag: "enable-features", value: "WaylandWindowDecorations" },
],
};

View File

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

View File

@@ -20,10 +20,17 @@ export const settings = {
disableHardwareMediaKeys: "disableHardwareMediaKeys",
enableCustomHotkeys: "enableCustomHotkeys",
enableDiscord: "enableDiscord",
ListenBrainz: {
root: "ListenBrainz",
enabled: "ListenBrainz.enabled",
api: "ListenBrainz.api",
token: "ListenBrainz.token",
},
flags: {
root: "flags",
disableHardwareMediaKeys: "flags.disableHardwareMediaKeys",
gpuRasterization: "flags.gpuRasterization",
enableWaylandSupport: "flags.enableWaylandSupport",
},
menuBar: "menuBar",
minimizeOnClose: "minimizeOnClose",
@@ -33,6 +40,7 @@ export const settings = {
singleInstance: "singleInstance",
skipArtists: "skipArtists",
skippedArtists: "skippedArtists",
theme: "theme",
trayIcon: "trayIcon",
updateFrequency: "updateFrequency",
windowBounds: {

View File

@@ -1,4 +0,0 @@
export const statuses = {
playing: "playing",
paused: "paused",
};

View File

@@ -0,0 +1,41 @@
import { App } from "electron";
import { flags } from "../../constants/flags";
import { settings } from "../../constants/settings";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
/**
* Set default Electron flags
*/
export function setDefaultFlags(app: App) {
setFlag(app, "disable-seccomp-filter-sandbox");
}
/**
* Set Tidal's managed flags from the user settings
* @param app
*/
export function setManagedFlagsFromSettings(app: App) {
const flagsFromSettings = settingsStore.get(settings.flags.root);
if (flagsFromSettings) {
for (const [key, value] of Object.entries(flagsFromSettings)) {
if (value) {
flags[key].forEach((flag) => {
Logger.log(`enabling command line option ${flag.flag} with value ${flag.value}`);
setFlag(app, flag.flag, flag.value);
});
}
}
}
}
/**
* Set a single flag for Electron
* @param app app to set it on
* @param flag flag name
* @param value value to be set for the flag
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setFlag(app: App, flag: string, value?: any) {
app.commandLine.appendSwitch(flag, value);
}

View File

@@ -0,0 +1,65 @@
import { PowerSaveBlocker, powerSaveBlocker } from "electron";
import { Logger } from "../logger";
/**
* Start blocking idle/screen timeouts
* @param blocker optional instance of the powerSaveBlocker to use
* @returns id of current block
*/
export const acquireInhibitor = (blocker?: PowerSaveBlocker): number => {
const currentBlocker = blocker ?? powerSaveBlocker;
const blockId = currentBlocker.start("prevent-app-suspension");
Logger.log(`Started preventing app suspension with id: ${blockId}`);
return blockId;
};
/**
* Check whether there is a blocker active for the current id, if not start it.
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const acquireInhibitorIfInactive = (id: number, blocker?: PowerSaveBlocker): number => {
const currentBlocker = blocker ?? powerSaveBlocker;
if (!isInhibitorActive(id, currentBlocker)) {
return acquireInhibitor();
}
return id;
};
/**
* stop blocking idle/screen timeouts
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const releaseInhibitor = (id: number, blocker?: PowerSaveBlocker) => {
try {
const currentBlocker = blocker ?? powerSaveBlocker;
currentBlocker.stop(id);
Logger.log(`Released inhibitor with id: ${id}`);
} catch (error) {
Logger.log("Releasing inhibitor failed");
}
};
/**
* stop blocking idle/screen timeouts if a inhibitor is active
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const releaseInhibitorIfActive = (id: number, blocker?: PowerSaveBlocker) => {
const currentBlocker = blocker ?? powerSaveBlocker;
if (isInhibitorActive(id, currentBlocker)) {
releaseInhibitor(id, currentBlocker);
}
};
/**
* check whether the inhibitor is active
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const isInhibitorActive = (id: number, blocker?: PowerSaveBlocker) => {
const currentBlocker = blocker ?? powerSaveBlocker;
return currentBlocker.isStarted(id);
};

View File

@@ -0,0 +1,132 @@
import axios from "axios";
import Store from "electron-store";
import { settings } from "../../constants/settings";
import { MediaStatus } from "../../models/mediaStatus";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
import { StoreData } from "./models/storeData";
const ListenBrainzStore = new Store({ name: "listenbrainz" });
export const ListenBrainzConstants = {
oldData: "oldData",
};
export class ListenBrainz {
/**
* Create the object to store old information in the Store :)
* @param title
* @param artists
* @param duration
* @returns data passed along in an object + a "listenedAt" key with the current time
*/
private static constructStoreData(title: string, artists: string, duration: number): StoreData {
return {
listenedAt: Math.floor(new Date().getTime() / 1000),
title,
artists,
duration,
};
}
/**
* Call the ListenBrainz API and create playing now payload and scrobble old song
* @param title
* @param artists
* @param status
* @param duration
*/
public static async scrobble(
title: string,
artists: string,
status: string,
duration: number
): Promise<void> {
try {
if (status === MediaStatus.paused) {
return;
} else {
// Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
const playing_data = {
listen_type: "playing_now",
payload: [
{
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "tidal.com",
duration: duration,
},
artist_name: artists,
track_name: title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
playing_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
if (!oldData) {
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
} else {
if (oldData.title !== title) {
// This constructs the data required to scrobble the data after the song finishes
const scrobble_data = {
listen_type: "single",
payload: [
{
listened_at: oldData.listenedAt,
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "listen.tidal.com",
duration: oldData.duration,
},
artist_name: oldData.artists,
track_name: oldData.title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
scrobble_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
}
}
}
} catch (error) {
Logger.log(JSON.stringify(error));
}
}
}
export { ListenBrainzStore };

View File

@@ -0,0 +1,9 @@
/**
* Data saved for ListenBrainz
*/
export interface StoreData {
listenedAt: number;
title: string;
artists: string;
duration: number;
}

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

@@ -0,0 +1,52 @@
import { IpcMain, ipcMain, IpcMainEvent, ipcRenderer } from "electron";
import { globalEvents } from "../constants/globalEvents";
export class Logger {
/**
* 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: IpcMainEvent | { content: string; message: string }, message) => {
const { content, object } = message ?? event;
this.logToSTDOut(content, object);
}
);
}
/**
* Log content to STDOut
* @param content
* @param object js(on) object that will be prettyPrinted
*/
public static log(content: string, object: object = {}) {
if (ipcRenderer) {
ipcRenderer.send(globalEvents.log, { content, object });
} else {
ipcMain.emit(globalEvents.log, { content, object });
}
}
/**
* Log content to STDOut and use the provided alert function to alert
* @param content
* @param object js(on) object that will be prettyPrinted
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static alert(content: string, object: any = {}, alert?: (msg: string) => void) {
Logger.log(content, object);
if (alert) {
alert(`${content} \n\nwith details: \n${JSON.stringify(object, null, 2)}`);
}
}
/**
* Log to STDOut
* @param content
* @param object
*/
private static logToSTDOut(content: string, object = {}) {
console.log(content, Object.keys(object).length > 0 ? 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

@@ -1,7 +1,7 @@
import { enable, initialize } from "@electron/remote/main";
import {
BrowserWindow,
app,
BrowserWindow,
components,
globalShortcut,
ipcMain,
@@ -9,9 +9,18 @@ import {
session,
} from "electron";
import path from "path";
import { flags } from "./constants/flags";
import { globalEvents } from "./constants/globalEvents";
import { mediaKeys } from "./constants/mediaKeys";
import { settings } from "./constants/settings";
import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags";
import {
acquireInhibitorIfInactive,
releaseInhibitorIfActive,
} from "./features/idleInhibitor/idleInhibitor";
import { Logger } from "./features/logger";
import { Songwhip } from "./features/songwhip/songwhip";
import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus";
import { initRPC, rpc, unRPC } from "./scripts/discord";
import { startExpress } from "./scripts/express";
import { updateMediaInfo } from "./scripts/mediaInfo";
@@ -20,13 +29,12 @@ import {
closeSettingsWindow,
createSettingsWindow,
hideSettingsWindow,
showSettingsWindow,
settingsStore,
showSettingsWindow,
} from "./scripts/settings";
import { settings } from "./constants/settings";
import { addTray, refreshTray } from "./scripts/tray";
import { MediaInfo } from "./models/mediaInfo";
const tidalUrl = "https://listen.tidal.com";
let mainInhibitorId = -1;
initialize();
@@ -34,29 +42,11 @@ let mainWindow: BrowserWindow;
const icon = path.join(__dirname, "../assets/icon.png");
const PROTOCOL_PREFIX = "tidal";
setFlags();
function setFlags() {
const flagsFromSettings = settingsStore.get(settings.flags.root);
if (flagsFromSettings) {
for (const [key, value] of Object.entries(flags)) {
if (value) {
flags[key].forEach((flag) => {
console.log(`enabling command line switch ${flag.flag} with value ${flag.value}`);
app.commandLine.appendSwitch(flag.flag, flag.value);
});
}
}
}
/**
* Fix Display Compositor issue.
*/
app.commandLine.appendSwitch("disable-seccomp-filter-sandbox");
}
setDefaultFlags(app);
setManagedFlagsFromSettings(app);
/**
* Update the menuBarVisbility according to the store value
* Update the menuBarVisibility according to the store value
*
*/
function syncMenuBarWithStore() {
@@ -88,8 +78,8 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
mainWindow = new BrowserWindow({
x: options.x,
y: options.y,
width: settingsStore && settingsStore.get(settings.windowBounds.width),
height: settingsStore && settingsStore.get(settings.windowBounds.height),
width: settingsStore?.get(settings.windowBounds.width),
height: settingsStore?.get(settings.windowBounds.height),
icon,
backgroundColor: options.backgroundColor,
autoHideMenuBar: true,
@@ -122,6 +112,7 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
});
// Emitted when the window is closed.
mainWindow.on("closed", function () {
releaseInhibitorIfActive(mainInhibitorId);
closeSettingsWindow();
app.quit();
});
@@ -194,6 +185,12 @@ app.on("browser-window-created", (_, window) => {
// IPC
ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => {
updateMediaInfo(arg);
if (arg.status === MediaStatus.playing) {
mainInhibitorId = acquireInhibitorIfInactive(mainInhibitorId);
} else {
releaseInhibitorIfActive(mainInhibitorId);
mainInhibitorId = -1;
}
});
ipcMain.on(globalEvents.hideSettings, () => {
@@ -220,3 +217,9 @@ ipcMain.on(globalEvents.storeChanged, () => {
ipcMain.on(globalEvents.error, (event) => {
console.log(event);
});
ipcMain.handle(globalEvents.whip, async (event, url) => {
return Songwhip.whip(url);
});
Logger.watch(ipcMain);

View File

@@ -1,9 +1,11 @@
import remote from "@electron/remote";
import { app } from "@electron/remote";
import { ipcRenderer, shell } from "electron";
import fs from "fs";
import { globalEvents } from "../../constants/globalEvents";
import { settings } from "../../constants/settings";
import { Logger } from "../../features/logger";
import { settingsStore } from "./../../scripts/settings";
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
let adBlock: HTMLInputElement,
api: HTMLInputElement,
@@ -22,21 +24,33 @@ let adBlock: HTMLInputElement,
singleInstance: HTMLInputElement,
skipArtists: HTMLInputElement,
skippedArtists: HTMLInputElement,
theme: HTMLSelectElement,
trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement;
updateFrequency: HTMLInputElement,
enableListenBrainz: HTMLInputElement,
ListenBrainzAPI: HTMLInputElement,
ListenBrainzToken: HTMLInputElement,
enableWaylandSupport: HTMLInputElement;
function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const fileNames = fs.readdirSync(process.resourcesPath).filter((file) => file.endsWith(".css"));
const options = fileNames.map((name) => {
return new Option(name, name);
});
const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
const userThemes = getThemeListFromDirectory(`${app.getPath("userData")}/themes`);
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);
});
}
@@ -47,7 +61,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`;
@@ -59,25 +73,34 @@ function handleFileUploads() {
* Sync the UI forms with the current settings
*/
function refreshSettings() {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
customCSS.value = settingsStore.get(settings.customCSS);
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
enableDiscord.checked = settingsStore.get(settings.enableDiscord);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris);
notifications.checked = settingsStore.get(settings.notifications);
playBackControl.checked = settingsStore.get(settings.playBackControl);
port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
try {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
enableDiscord.checked = settingsStore.get(settings.enableDiscord);
enableWaylandSupport.checked = settingsStore.get(settings.flags.enableWaylandSupport);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris);
notifications.checked = settingsStore.get(settings.notifications);
playBackControl.checked = settingsStore.get(settings.playBackControl);
port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists);
theme.value = settingsStore.get(settings.theme);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api);
ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token);
} catch (error) {
Logger.log("Refreshing settings failed.", error);
}
}
/**
@@ -98,16 +121,16 @@ function hide() {
* Restart tidal-hifi after changes
*/
function restart() {
remote.app.relaunch();
remote.app.exit();
app.relaunch();
app.exit();
}
/**
* Bind UI components to functions after DOMContentLoaded
*/
window.addEventListener("DOMContentLoaded", () => {
function get(id: string): HTMLInputElement {
return document.getElementById(id) as HTMLInputElement;
function get<T = HTMLInputElement>(id: string): T {
return document.getElementById(id) as T;
}
getThemeFiles();
@@ -128,6 +151,12 @@ window.addEventListener("DOMContentLoaded", () => {
} else {
settingsStore.set(key, source.value);
}
// Live update the view for ListenBrainz input, hide if disabled/show if enabled
if (source.value === "on" && source.id === "enableListenBrainz") {
source.checked
? document.getElementById("listenbrainz__options").removeAttribute("hidden")
: document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
@@ -139,6 +168,13 @@ window.addEventListener("DOMContentLoaded", () => {
});
}
function addSelectListener(source: HTMLSelectElement, key: string) {
source.addEventListener("change", () => {
settingsStore.set(key, source.value);
ipcRenderer.send(globalEvents.storeChanged);
});
}
ipcRenderer.on("refreshData", () => {
refreshSettings();
});
@@ -154,6 +190,7 @@ window.addEventListener("DOMContentLoaded", () => {
disableHardwareMediaKeys = get("disableHardwareMediaKeys");
enableCustomHotkeys = get("enableCustomHotkeys");
enableDiscord = get("enableDiscord");
enableWaylandSupport = get("enableWaylandSupport");
gpuRasterization = get("gpuRasterization");
menuBar = get("menuBar");
minimizeOnClose = get("minimizeOnClose");
@@ -161,13 +198,20 @@ window.addEventListener("DOMContentLoaded", () => {
notifications = get("notifications");
playBackControl = get("playBackControl");
port = get("port");
theme = get<HTMLSelectElement>("themesList");
trayIcon = get("trayIcon");
skipArtists = get("skipArtists");
skippedArtists = get("skippedArtists");
singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency");
enableListenBrainz = get("enableListenBrainz");
ListenBrainzAPI = get("ListenBrainzAPI");
ListenBrainzToken = get("ListenBrainzToken");
refreshSettings();
enableListenBrainz.checked
? document.getElementById("listenbrainz__options").removeAttribute("hidden")
: document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
addInputListener(adBlock, settings.adBlock);
addInputListener(api, settings.api);
@@ -176,6 +220,7 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys);
addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys);
addInputListener(enableDiscord, settings.enableDiscord);
addInputListener(enableWaylandSupport, settings.flags.enableWaylandSupport);
addInputListener(gpuRasterization, settings.flags.gpuRasterization);
addInputListener(menuBar, settings.menuBar);
addInputListener(minimizeOnClose, settings.minimizeOnClose);
@@ -186,6 +231,10 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(skipArtists, settings.skipArtists);
addTextAreaListener(skippedArtists, settings.skippedArtists);
addInputListener(singleInstance, settings.singleInstance);
addSelectListener(theme, settings.theme);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(updateFrequency, settings.updateFrequency);
addInputListener(enableListenBrainz, settings.ListenBrainz.enabled);
addTextAreaListener(ListenBrainzAPI, settings.ListenBrainz.api);
addTextAreaListener(ListenBrainzToken, settings.ListenBrainz.token);
});

View File

@@ -212,6 +212,35 @@
</label>
</div>
</div>
<div class="group">
<p class="group__title">ListenBrainz</p>
<div class="group__option">
<div class="group__description">
<h4>Enable ListenBrainz</h4>
<p>Scrobble your listens directly to ListenBrainz.</p>
</div>
<label class="switch">
<input id="enableListenBrainz" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div id="listenbrainz__options" hidden="true">
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz API Url</h4>
<p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p>
</div>
</div>
<textarea id="ListenBrainzAPI" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz User Token</h4>
<p>Provide the user token you can get from the settings page.</p>
</div>
</div>
<textarea id="ListenBrainzToken" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
</div>
</div>
</section>
<section id="advanced-section" class="tabs__section">
@@ -229,44 +258,57 @@
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
</div>
</div>
<div class="group">
<p class="group__title">Flags</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome MPRIS
client separately from the custom MPRIS client.
</p>
</div>
<label class="switch">
<input id="disableHardwareMediaKeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Enable GPU rasterization</h4>
<p>Move a part of the rendering to the GPU for increased performance.</p>
</div>
<label class="switch">
<input id="gpuRasterization" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Disable Background Throttling</h4>
<p>
Makes app more responsive while in the background, at the cost of performance.
</p>
</div>
<label class="switch">
<input id="disableBackgroundThrottle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group">
<p class="group__title">Flags</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome MPRIS
client separately from the custom MPRIS client.
</p>
</div>
<label class="switch">
<input id="disableHardwareMediaKeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Enable GPU rasterization</h4>
<p>Move a part of the rendering to the GPU for increased performance.</p>
</div>
<label class="switch">
<input id="gpuRasterization" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Disable Background Throttling</h4>
<p>
Makes app more responsive while in the background, at the cost of performance.
</p>
</div>
<label class="switch">
<input id="disableBackgroundThrottle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Wayland support</h4>
<p>
Adds a couple of Electron flags to help Tidal-hifi run smoothly on the Wayland window system.
</p>
</div>
<label class="switch">
<input id="enableWaylandSupport" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
</section>
<section id="theming-section" class="tabs__section">
@@ -291,7 +333,7 @@
<p>
Select a theme below or "Tidal - Default" to return to the original Tidal look.
</p>
<select id="themesList" name="themesList">
<select class="select-input" id="themesList" name="themesList">
</select>
</div>

View File

@@ -230,8 +230,6 @@ html {
border-color: $tidal-blue;
color: $white;
}
// --- Switch slider component ---
}
}
}
@@ -415,3 +413,33 @@ html {
outline: none;
}
}
.select-input {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 5px 0;
transition: 0.2s;
border: 0;
border-bottom: solid 1px $grey-333;
outline: none;
background: transparent;
color: $tidal-grey;
font-size: 14px;
&:focus {
border-color: $tidal-blue;
color: $white;
}
option {
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";
import { Logger } from "../../features/logger";
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): string[] => {
try {
makeUserThemesDirectory(directory);
return fs.readdirSync(directory).filter(cssFilter).sort(sort);
} catch (err) {
Logger.log(`Failed to get files from ${directory}`, err);
return [];
}
};
/**
* Create the directory to store user themes in
* @param directory directory to create
*/
export const makeUserThemesDirectory = (directory: string) => {
try {
fs.mkdirSync(directory, { recursive: true });
} catch (err) {
Logger.log(`Failed to make user theme directory: ${directory}`, err);
}
};

View File

@@ -1,20 +1,29 @@
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 {
ListenBrainz,
ListenBrainzConstants,
ListenBrainzStore,
} from "./features/listenbrainz/listenbrainz";
import { StoreData } from "./features/listenbrainz/models/storeData";
import { Logger } from "./features/logger";
import { Songwhip } from "./features/songwhip/songwhip";
import { MediaStatus } from "./models/mediaStatus";
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 = "";
let player: any;
let currentPlayStatus = statuses.paused;
let player: Player;
let currentPlayStatus = MediaStatus.paused;
const elements = {
play: '*[data-test="play"]',
@@ -24,13 +33,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",
@@ -38,9 +46,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-geqnfr',
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"]',
/**
@@ -103,8 +112,10 @@ const elements = {
window.location.href.includes("/playlist/") ||
window.location.href.includes("/mix/")
) {
if (currentPlayStatus === statuses.playing) {
const row = window.document.querySelector(this.playing_title).closest(this.tracklist_row);
if (currentPlayStatus === 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.
// 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;
}
@@ -147,8 +158,26 @@ const elements = {
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const selectedTheme = settingsStore.get<string, string>(settings.theme);
if (selectedTheme !== "none") {
const userThemePath = `${app.getPath("userData")}/themes/${selectedTheme}`;
const resourcesThemePath = `${process.resourcesPath}/${selectedTheme}`;
const themeFile = fs.existsSync(userThemePath) ? userThemePath : resourcesThemePath;
fs.readFile(themeFile, "utf-8", (err, data) => {
if (err) {
Logger.alert("An error ocurred reading the theme file.", err, alert);
return;
}
const themeStyle = document.createElement("style");
themeStyle.innerHTML = data;
document.head.appendChild(themeStyle);
});
}
// read customCSS (it will override the theme)
const style = document.createElement("style");
style.innerHTML = settingsStore.get(settings.customCSS);
style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
document.head.appendChild(style);
});
}
@@ -158,7 +187,7 @@ function addCustomCss() {
* make sure it returns a number, if not use the default
*/
function getUpdateFrequency() {
const storeValue = settingsStore.get(settings.updateFrequency) as number;
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
const defaultValue = 500;
if (!isNaN(storeValue)) {
@@ -181,6 +210,11 @@ function playPause() {
}
}
/**
* Clears the old listenbrainz data on launch
*/
ListenBrainzStore.clear();
/**
* Add hotkeys for when tidal is focused
* Reflects the desktop hotkeys found on:
@@ -189,7 +223,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();
@@ -215,6 +252,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
@@ -242,7 +288,7 @@ function handleLogout() {
defaultId: 2,
})
.then((result: { response: number }) => {
if (logoutOptions.indexOf("Yes, please") == result.response) {
if (logoutOptions.indexOf("Yes, please") === result.response) {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key.startsWith("_TIDAL_activeSession")) {
@@ -284,6 +330,8 @@ function addIPCEventListeners() {
case globalEvents.pause:
elements.click("pause");
break;
default:
break;
}
});
});
@@ -298,9 +346,9 @@ function getCurrentlyPlayingStatus() {
// if pause button is visible tidal is playing
if (pause) {
status = statuses.playing;
status = MediaStatus.playing;
} else {
status = statuses.paused;
status = MediaStatus.paused;
}
return status;
}
@@ -325,19 +373,41 @@ function updateMediaInfo(options: Options, notify: boolean) {
if (settingsStore.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
}
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
};
player.playbackStatus = options.status == statuses.paused ? "Paused" : "Playing";
updateMpris(options);
updateListenBrainz(options);
}
}
function updateMpris(options: Options) {
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
};
player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing";
}
}
function updateListenBrainz(options: Options) {
if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
if (
(!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== options.title)
) {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
);
}
}
}

View File

@@ -1,11 +1,11 @@
import { BrowserWindow, dialog } from "electron";
import express, { Response } from "express";
import fs from "fs";
import { settings } from "../constants/settings";
import { MediaStatus } from "../models/mediaStatus";
import { globalEvents } from "./../constants/globalEvents";
import { statuses } from "./../constants/statuses";
import { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings";
import { settings } from "../constants/settings";
/**
* Function to enable tidal-hifi's express api
@@ -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);
}
@@ -44,7 +44,7 @@ export const startExpress = (mainWindow: BrowserWindow) => {
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status == statuses.playing) {
if (mediaInfo.status === MediaStatus.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);

View File

@@ -1,12 +1,12 @@
import { MediaInfo } from "../models/mediaInfo";
import { statuses } from "./../constants/statuses";
import { MediaStatus } from "../models/mediaStatus";
export const mediaInfo = {
title: "",
artists: "",
album: "",
icon: "",
status: statuses.paused,
status: MediaStatus.paused as string,
url: "",
current: "",
duration: "",

View File

@@ -13,14 +13,20 @@ export const settingsStore = new Store({
apiSettings: {
port: 47836,
},
customCSS: "",
customCSS: [],
disableBackgroundThrottle: true,
disableHardwareMediaKeys: false,
enableCustomHotkeys: false,
enableDiscord: false,
ListenBrainz: {
enabled: false,
api: "https://api.listenbrainz.org",
token: "",
},
flags: {
gpuRasterization: true,
disableHardwareMediaKeys: false,
enableWaylandSupport: true,
gpuRasterization: true,
},
menuBar: true,
minimizeOnClose: false,
@@ -30,6 +36,7 @@ export const settingsStore = new Store({
singleInstance: true,
skipArtists: false,
skippedArtists: [""],
theme: "none",
trayIcon: true,
updateFrequency: 500,
windowBounds: { width: 800, height: 600 },
@@ -54,7 +61,7 @@ export const createSettingsWindow = function () {
settingsWindow = new BrowserWindow({
width: 700,
height: 600,
resizable: false,
resizable: true,
show: false,
transparent: true,
frame: false,
@@ -66,7 +73,7 @@ export const createSettingsWindow = function () {
},
});
settingsWindow.on("close", (event: any) => {
settingsWindow.on("close", (event: Event) => {
if (settingsWindow != null) {
event.preventDefault();
settingsWindow.hide();

10
src/themes/Blood.scss Normal file
View File

@@ -0,0 +1,10 @@
$foreground: red;
$background: black;
span {
color: $foreground;
}
.sidebar--WvRg_ {
background-color: $background;
}

View File

@@ -0,0 +1,85 @@
:root {
--footer-player-background: #1a1b26;
--sidebar-background: #1a1b26;
--sidebar-hover-background: #414868;
--sidebar-menu-top-text: #565f89;
--sidebar-menu-playlist-text: #565f89;
--search-background: #1a1b26;
--main-background: #16161e;
--main-navigation-control-background: #1a1b26;
--main-feed-button-background: #1a1b26;
--player-control-background: #24283b;
--player-control-active-button: #ff9e64;
--player-progress-bar: #ff9e64;
--indicator-hifi-background: #9ece6a;
--indicator-hifi-span: #1a1b26;
--player-control-favorite: #f7768e;
--search-dialog-background: #24283b;
--right-queue-background: #24283b;
}
.player--gAOQG.notFullscreen--xbpBL {
background-color: var(--footer-player-background);
}
.sidebar--jVJai {
background-color: var(--sidebar-background);
contain: strict;
flex-grow: 1;
overflow-y: auto;
}
.item--buEQw:hover {
background-color: var(--sidebar-hover-background);
}
.main--jxfcQ {
background-color: var(--main-background);
}
button.button--yO9Cd {
background-color: var(--main-navigation-control-background);
}
.player--gAOQG.lossLess--ON3FI button.withBackground[aria-checked="true"] path {
fill: var(--player-control-active-button);
}
.player--gAOQG.lossLess--ON3FI button.withBackground[aria-checked="true"] {
background-color: var(--player-control-background);
}
.activeItem--kFIk0 .activeItem--kFIk0 .playlistItem--mQrxp .section--PSIay.playingItem--eWkYS {
color: #565f89;
}
.progressBarWrapper--IBBI9 {
color: var(--player-progress-bar);
}
.playbackControls--FhKVf button .tidal-ui__icon {
transform: scale(1);
}
.css-11m9iw3 {
background-color: var(--indicator-hifi-background);
}
.css-11m9iw3 span {
color: var(--indicator-hifi-span);
}
.activeItem--kFIk0 {
color: var(--sidebar-menu-top-text);
}
.activeItem--kFIk0 .playlistItem--mQrxp {
color: var(--sidebar-menu-playlist-text);
}
button.feedBell--kvAbD {
background-color: var(--main-feed-button-background);
}
.baseContainer--jxCbW {
background-color: var(--search-dialog-background);
}
.favoriteButton--Qladw.is-favorite path {
fill: var(--player-control-favorite);
}
.container--PFTHk {
background-color: var(--right-queue-background);
}
.container--cl4MJ{
background-color: var(--search-background);
}
.searchFieldHighlighted--Fitvs {
color: var(--snow-white);
}
.searchField--EGBSq {
background-color: var(--search-background);
}

View File

@@ -1,3 +0,0 @@
h2 {
color: black;
}

View File

@@ -1,7 +0,0 @@
h1 {
color: black;
.title {
color: blue;
}
}

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,7 +1,9 @@
{
"compilerOptions": {
"typeRoots": ["src/types", "node_modules/@types"],
"module": "commonjs",
"target": "ES6",
"lib": ["ES2020", "DOM"],
"noImplicitAny": true,
"sourceMap": true,
"allowJs": true,