Compare commits

...

38 Commits

Author SHA1 Message Date
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
45 changed files with 1933 additions and 875 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

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

View File

@@ -4,12 +4,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 5.4.0
- Removed Windows builds (from publishes) as they don't work anymore.
- Added [Songwhip](https://songwhip.com/) integration
- Fixed bug with several hotkeys not working due to Tidal's HTML/css changes
- [DEV]:
- added a logger to log into STDout
- added "watchStart" which will automatically restart electron when it detects a source code change
- added "listen.tidal.com-parsing-scripts" folder with a script to verify whether all elements (in the main preload.ts) are present on the page
## 5.3.0
- SPKChaosPhoenix updated the beautiful Tokyo Night theme:
![](./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

134
README.md
View File

@@ -1,42 +1,82 @@
# 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.
![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)
- [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)
- [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
- Notifications
- Custom [theming](./docs/theming.md)
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- Songwhip.com integration (hotkey `ctrl + w`)
- API for status and playback
- Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495))
- Custom [integrations](#integrations)
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
## Contributions
To contribute you can use the standard GitHub features (issues, prs, etc) or join the discord server to talk with like-minded individuals.
- ![Discord logo](./docs/images/discord.png) [Join the Discord server](https://discord.gg/yhNwf4v4He)
## Why did I create tidal-hifi?
I moved from Spotify over to Tidal and found Linux support to be lacking.
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it.
### Why not extend existing projects?
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
- Lack of maintainers/developers. (no hotfixes, no issues being handled etc)
- Most are simple web wrappers, not my cup of tea.
- Some are DE-oriented. I want this to work on WM's too.
- None have Widevine working at the moment
Sometimes it's just easier to start over, cover my own needs and 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 +88,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 +131,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.
![integrations menu, showing a list of integrations](./docs/integrations.png)
![integrations menu, showing a list of integrations](./docs/images/integrations.png)
It currently includes:
@@ -126,38 +155,25 @@ 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
#### 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

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

1988
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.4.0",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js",
"scripts": {
"start": "electron .",
"start": "electron --inspect=0.0.0.0:5858 .",
"watchStart": "nodemon dist -x \"npm run start\"",
"compile": "tsc && npm run sass-and-copy",
"watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
"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,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" }],
};

View File

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

View File

@@ -33,6 +33,7 @@ export const settings = {
singleInstance: "singleInstance",
skipArtists: "skipArtists",
skippedArtists: "skippedArtists",
theme: "theme",
trayIcon: "trayIcon",
updateFrequency: "updateFrequency",
windowBounds: {

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
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 { settingsStore } from "./../../scripts/settings";
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
let adBlock: HTMLInputElement,
api: HTMLInputElement,
@@ -22,21 +23,28 @@ let adBlock: HTMLInputElement,
singleInstance: HTMLInputElement,
skipArtists: HTMLInputElement,
skippedArtists: 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"));
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 +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`;
@@ -61,7 +69,7 @@ function handleFileUploads() {
function refreshSettings() {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
customCSS.value = settingsStore.get(settings.customCSS);
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);
@@ -75,6 +83,7 @@ function refreshSettings() {
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);
@@ -98,16 +107,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();
@@ -139,6 +148,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();
});
@@ -161,6 +177,7 @@ 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");
@@ -186,6 +203,7 @@ 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);
});

View File

@@ -291,7 +291,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,54 @@
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): string[] => {
try {
makeUserThemesDirectory(directory);
return fs.readdirSync(directory).filter(cssFilter).sort(sort);
} catch (err) {
console.error(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) {
console.error(err);
}
};

View File

@@ -1,19 +1,21 @@
import { Notification, app, dialog } from "@electron/remote";
import { ipcRenderer } from "electron";
import { app, dialog, Notification } from "@electron/remote";
import { clipboard, ipcRenderer } from "electron";
import fs from "fs";
import Player from "mpris-service";
import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings";
import { statuses } from "./constants/statuses";
import { Songwhip } from "./features/songwhip/songwhip";
import { Options } from "./models/options";
import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys";
import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions";
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "Tidal Hifi";
let currentSong = "";
let player: any;
let player: Player;
let currentPlayStatus = statuses.paused;
const elements = {
@@ -24,13 +26,12 @@ const elements = {
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[class^="backwardButton"]',
forward: '[class^="forwardButton"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
block: '[class="blockButton"]',
account: '*[data-test^="profile-image-button"]',
account: '*[class^="profileOptions"]',
settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
@@ -38,9 +39,10 @@ const elements = {
duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
playing_title: 'span[data-test="table-cell-title"].css-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"]',
/**
@@ -104,7 +106,9 @@ const elements = {
window.location.href.includes("/mix/")
) {
if (currentPlayStatus === statuses.playing) {
const row = window.document.querySelector(this.playing_title).closest(this.tracklist_row);
// find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell.
// document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent
const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem);
if (row) {
return row.querySelector(this.album_name_cell).textContent;
}
@@ -147,8 +151,24 @@ const elements = {
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const selectedTheme = settingsStore.get(settings.theme);
if (selectedTheme !== "none") {
const themeFile = `${process.resourcesPath}/${selectedTheme}`;
fs.readFile(themeFile, "utf-8", (err, data) => {
if (err) {
alert("An error ocurred reading the theme file.");
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);
});
}
@@ -189,7 +209,10 @@ function playPause() {
function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () {
elements.click("account").click("settings");
elements.click("account");
setTimeout(() => {
elements.click("settings");
}, 100);
});
addHotkey("Control+l", function () {
handleLogout();
@@ -215,6 +238,15 @@ function addHotKeys() {
addHotkey("control+r", function () {
elements.click("repeat");
});
addHotkey("control+w", async function () {
const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL());
const url = Songwhip.getWhipUrl(result);
clipboard.writeText(url);
new Notification({
title: `Successfully whipped: `,
body: `URL copied to clipboard: ${url}`,
}).show();
});
}
// always add the hotkey for the settings window

View File

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

View File

@@ -13,7 +13,7 @@ export const settingsStore = new Store({
apiSettings: {
port: 47836,
},
customCSS: "",
customCSS: [],
disableBackgroundThrottle: true,
disableHardwareMediaKeys: false,
enableCustomHotkeys: false,
@@ -30,6 +30,7 @@ export const settingsStore = new Store({
singleInstance: true,
skipArtists: false,
skippedArtists: [""],
theme: "none",
trayIcon: true,
updateFrequency: 500,
windowBounds: { width: 800, height: 600 },
@@ -54,7 +55,7 @@ export const createSettingsWindow = function () {
settingsWindow = new BrowserWindow({
width: 700,
height: 600,
resizable: false,
resizable: true,
show: false,
transparent: true,
frame: false,
@@ -66,7 +67,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,