Compare commits

..

No commits in common. "master" and "5.8.0" have entirely different histories.

60 changed files with 1839 additions and 6310 deletions

View File

@ -9,7 +9,6 @@ on:
branches-ignore: branches-ignore:
- master - master
- develop - develop
workflow_dispatch:
jobs: jobs:
build_on_linux: build_on_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -21,30 +20,26 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 22.4 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
# - uses: actions/upload-artifact@master
# with:
# name: linux-builds
# path: dist/
# build_on_mac: build_on_mac:
# runs-on: macos-latest runs-on: macos-latest
# steps: steps:
# - uses: actions/checkout@master - uses: actions/checkout@master
# - uses: actions/setup-node@master - uses: actions/setup-node@master
# with: with:
# node-version: 22.4 node-version: 19
# - run: npm install - run: npm install
# - run: npm run build - run: npm run build
# build_on_win: build_on_win:
# runs-on: windows-latest runs-on: windows-latest
# steps: steps:
# - uses: actions/checkout@master - uses: actions/checkout@master
# - uses: actions/setup-node@master - uses: actions/setup-node@master
# with: with:
# node-version: 22.4 node-version: 19
# - run: npm install - run: npm install
# - run: npm run build - run: npm run build

View File

@ -8,7 +8,6 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
workflow_dispatch:
jobs: jobs:
build_on_linux: build_on_linux:
@ -21,7 +20,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 22.4 node-version: 19
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
@ -29,30 +28,30 @@ jobs:
name: linux-builds name: linux-builds
path: dist/ path: dist/
# build_on_mac: build_on_mac:
# runs-on: macos-latest runs-on: macos-latest
# steps: steps:
# - uses: actions/checkout@master - uses: actions/checkout@master
# - uses: actions/setup-node@master - uses: actions/setup-node@master
# with: with:
# node-version: 22.4 node-version: 19
# - run: npm install - run: npm install
# - run: npm run build - run: npm run build
# - uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
# with: with:
# name: mac-builds name: mac-builds
# path: ./dist/ path: ./dist/
# build_on_win: build_on_win:
# runs-on: windows-latest runs-on: windows-latest
# steps: steps:
# - uses: actions/checkout@master - uses: actions/checkout@master
# - uses: actions/setup-node@master - uses: actions/setup-node@master
# with: with:
# node-version: 22.4 node-version: 19
# - run: npm install - run: npm install
# - run: npm run build - run: npm run build
# - uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
# with: with:
# name: windows-builds name: windows-builds
# path: dist/ path: dist/

View File

@ -1,5 +0,0 @@
POST /settings/skipped-artists HTTP/1.1
Host: localhost:47836
Content-Type: application/json
["abc", "def"]

View File

@ -1,2 +0,0 @@
POST /settings/skipped-artists/current HTTP/1.1
Host: localhost:47836

View File

@ -1,5 +0,0 @@
POST /settings/skipped-artists/delete HTTP/1.1
Host: localhost:47836
Content-Type: application/json
["abc", "def"]

View File

@ -1,2 +0,0 @@
DELETE /settings/skipped-artists/current HTTP/1.1
Host: localhost:47836

View File

@ -2,7 +2,6 @@
"cSpell.words": [ "cSpell.words": [
"Brainz", "Brainz",
"Castlabs", "Castlabs",
"Fi's",
"flac", "flac",
"Flatpak", "Flatpak",
"geqnfr", "geqnfr",
@ -14,6 +13,7 @@
"rescrobbler", "rescrobbler",
"scrobble", "scrobble",
"scrobbling", "scrobbling",
"Songwhip",
"trackid", "trackid",
"tracklist", "tracklist",
"widevine", "widevine",

View File

@ -4,129 +4,6 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.18.2]
- Reverted to sass 1.79.4 to fix `Nix` builds
- Changed electron-builder.base.yml to now generate the correct .desktop entries again
- Should fix flatpak build
## [5.18.1]
- Fixed the login bug
- Upgraded electron to 35.1.1
- Added Widevine/CDM info to startup
- delayed remote electron initializer
## [5.18.0]
- [Dianoga](https://github.com/Dianoga) fixed the duration selector, restoring mpris & partial API data.
- PR: #554
- Added `xesam:url` property to mpris metadata fixes [#506](https://github.com/Mastermindzh/tidal-hifi/issues/506)
## [5.17.0]
- Added an option to disable the dynamic title and set it to a static one, [#491](https://github.com/Mastermindzh/tidal-hifi/pull/491)
- Discord integration now says "Listening to" instead of "playing" [#488](https://github.com/Mastermindzh/tidal-hifi/pull/488) && [#454](https://github.com/Mastermindzh/tidal-hifi/pull/454)
- Fixed several element names in the dom scraper
- Removed the Songwhip (they shut down) integration and replaced it with TIDAL's universal link system
## [5.16.0]
- Fix issue #449 Discord RPC stuck on "Browsing Tidal".
- Fix issue #448 Add option to disable the discord rpc idle text
- Notifications are now send at the end of the update process, allowing other events to happen sooner.
## [5.15.0]
- Added all missing swagger/openApi info with the help of [Times-Z](https://github.com/Times-Z)
- Updated most dependency versions
- This includes Electron 31!
- Added a channel selector so we can now use Tidal's staging environment directly from the app
- implements [#437](https://github.com/Mastermindzh/tidal-hifi/issues/437)
## [5.14.1]
- Fixed `getAlbumName` not finding album name whilst on queue page
- Added all mediaInfo to mpris interface using the `custom:` prefix
## [5.14]
- Simplified `MediaInfo` & `Options` types
- Added `playingFrom` information to the info API
- also changed the way we update Album info since Playing From now shows the correct Album.
- API now allows you to set the `hostname` so you can control who can interact with the API.
- Reworked swagger generation hotfix to properly generate `swagger.json` during the compile step
- Might switch to tsoa in the future, idk yet.
- Added [Tidal Magazine](https://tidal.com/magazine/) integration (in the menubar or use `Ctrl + M`)
## [5.13.1]
- removed Swagger generation step in favor of pre-generated file.
- This also fixes the API issue [#409](https://github.com/Mastermindzh/tidal-hifi/issues/409)
- This also stops TIDAL-hifi from scanning your entire home directory... the glob was very broad apparently.
## [5.13.0]
- Fixed [#403](https://github.com/Mastermindzh/tidal-hifi/issues/403) "cannot read shuffle of undefined" error
- Added an API to add & delete entries from the skippedArtists list in the settings. fixes [#405](https://github.com/Mastermindzh/tidal-hifi/issues/405)
- `GET /settings/skipped-artists` -> get list of skipped artists
- `POST /settings/skipped-artists` -> add to the list of skipped artists
- `POST /settings/skipped-artists/delete` -> delete from the list of skipped artists
- `POST /settings/skipped-artists/current` -> skip the current artist
- `DELETE /settings/skipped-artists/current` -> delete the current artist from the skip list
- Added Swagger documentation to the new endpoints:
![picture of swagger documentation](./docs/images/swagger.png)
- CORS support added by [Mjokfox](https://github.com/Mjokfox)
## [5.12.0]
- Added Shuffle and Repeat state to API response - By [ThatGravyBoat](https://github.com/ThatGravyBoat)
## [5.11.0]
- Re-implemented the API, added support for duration/current in seconds & shuffle+repeat
- made the original API "legacy" (still works the same)
- Now using the correct HTTP verb for all new endpoints
- Implemented TIDAL's universal links. All links are now universal.
- Custom `tidal://` protocol fixed - By [TheRockYT](https://github.com/TheRockYT)
- Global media shortcuts removed since TIDAL includes them by default - By [TheRockYT](https://github.com/TheRockYT)
- Fixes
- [#390](https://github.com/Mastermindzh/tidal-hifi/issues/390)
- [#376](https://github.com/Mastermindzh/tidal-hifi/issues/376)
- [#383](https://github.com/Mastermindzh/tidal-hifi/issues/383)
- [#393](https://github.com/Mastermindzh/tidal-hifi/issues/393)
## [5.10.0]
- TIDAL will now close the previous notification if a new one is sent whilst the old is still visible. [#364](https://github.com/Mastermindzh/tidal-hifi/pull/364)
- Updated developer documentation to get started in README [#365](https://github.com/Mastermindzh/tidal-hifi/pull/365)
- Links in the about window now open in the user's default browser. fixes [#360](https://github.com/Mastermindzh/tidal-hifi/issues/360)
- Refactored "nowPlaying" code to always display the current state, even when the built-in UI is updated.
- fixes [#351](https://github.com/Mastermindzh/tidal-hifi/issues/351)
- fixes [#356](https://github.com/Mastermindzh/tidal-hifi/issues/356)
- fixes [#370](https://github.com/Mastermindzh/tidal-hifi/issues/370)
- Reverted to using old icon syntax with icons in the build directory. fixes [#350](https://github.com/Mastermindzh/tidal-hifi/issues/350)
- Enabled wayland platform flags by default when launching through .desktop file
- fixes [#273](https://github.com/Mastermindzh/tidal-hifi/issues/273)
- fixes [#347](https://github.com/Mastermindzh/tidal-hifi/issues/347)
## [5.9.0]
- More Discord options:
- Added the ability to hide the current song from the discord activity and display a custom text instead
- Added the ability to customize the text that is shown when no song is playing
- Discord now reacts to pausing/unpausing events
- Refactored media info updates so it only updates the required info, fixes #342, #306
- Added 5.9.0 logs/versions/migrations
### Fixed
- Fixed chromium mediaSession instance showing up. fixes #338 #198
- Set a new icon, should fix #302
- Made sure settingsWindow exists before operating on it. fixes #344
## [5.8.0] ## [5.8.0]
- Updated Electron to 28.1.1 (fixes [325](https://github.com/Mastermindzh/tidal-hifi/issues/325)) - Updated Electron to 28.1.1 (fixes [325](https://github.com/Mastermindzh/tidal-hifi/issues/325))

View File

@ -26,7 +26,7 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- [Using source](#using-source) - [Using source](#using-source)
- [Integrations](#integrations) - [Integrations](#integrations)
- [Known bugs](#known-bugs) - [Known bugs](#known-bugs)
- [DRM not working on Windows (error S6007)](#drm-not-working-on-windows-error-s6007) - [DRM not working on Windows](#drm-not-working-on-windows)
- [Special thanks to](#special-thanks-to) - [Special thanks to](#special-thanks-to)
- [Donations](#donations) - [Donations](#donations)
- [Images](#images) - [Images](#images)
@ -43,13 +43,13 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts)) - Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- Better icons thanks to [Papirus-icon-theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/) - Better icons thanks to [Papirus-icon-theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/)
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`) - [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- API for status, playback and settings (see the [/docs](http://localhost:47836/docs/) route) - 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)) - 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)) - AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
- Custom [integrations](#integrations) - Custom [integrations](#integrations)
- [ListenBrainz](https://listenbrainz.org/?redirect=false) integration - [ListenBrainz](https://listenbrainz.org/?redirect=false) integration
- Songwhip.com integration (hotkey `ctrl + w`)
- Discord RPC integration (showing "now listening", "Browsing", etc) - Discord RPC integration (showing "now listening", "Browsing", etc)
- Flatpak version only works if both Discord and Tidal-HiFi are flatpaks
- MPRIS integration - MPRIS integration
- UI + Json config (`~/.config/tidal-hifi/`, or `~/.var/app/com.mastermindzh.tidal-hifi/` for Flatpak) - UI + Json config (`~/.config/tidal-hifi/`, or `~/.var/app/com.mastermindzh.tidal-hifi/` for Flatpak)
@ -130,13 +130,10 @@ nix-env -iA nixpkgs.tidal-hifi
To install and work with the code on this project follow these steps: To install and work with the code on this project follow these steps:
- `git clone https://github.com/Mastermindzh/tidal-hifi.git` - git clone [https://github.com/Mastermindzh/tidal-hifi.git](https://github.com/Mastermindzh/tidal-hifi.git)
- `cd tidal-hifi` - cd TIDAL Hi-Fi
- `npm install` - npm install
- `npm run watch` to watch for auto-reload of Typescript/SCSS changes. - npm start
- `npm run compile` can be used to trigger it once
- `npm watchStart` to auto watch for any updates files and reload Tidal Hi-Fi
- `npm start` can be used to run Tidal Hi-Fi manually once
## Integrations ## Integrations
@ -152,13 +149,11 @@ Integrations with other projects that are not included natively:
## Known bugs ## Known bugs
### DRM not working on Windows (error S6007) ### DRM not working on Windows
Most Windows users run into DRM issues when trying to use TIDAL Hi-Fi. Most Windows users run into DRM issues when trying to use TIDAL Hi-Fi.
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. 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.
Until then you'll have to use the official app unfortunately.
## Special thanks to ## Special thanks to
- [Castlabs](https://castlabs.com/) - [Castlabs](https://castlabs.com/)

View File

@ -1,7 +1,7 @@
appId: com.rickvanlieshout.tidal-hifi appId: com.rickvanlieshout.tidal-hifi
electronVersion: 35.1.1 electronVersion: 28.1.1
electronDownload: electronDownload:
version: 35.1.1+wvcus version: 28.1.1+wvcus
mirror: https://github.com/castlabs/electron-releases/releases/download/v mirror: https://github.com/castlabs/electron-releases/releases/download/v
snap: snap:
plugs: plugs:
@ -11,30 +11,24 @@ extraResources:
- "themes/**" - "themes/**"
linux: linux:
category: AudioVideo category: AudioVideo
icon: build/icons icon: assets/icons
target: target:
- dir - dir
executableName: tidal-hifi executableName: tidal-hifi
executableArgs:
[
"--enable-features=UseOzonePlatform",
"--ozone-platform-hint=auto",
"--enable-features=WaylandWindowDecorations",
]
desktop: desktop:
entry: Encoding: UTF-8
Encoding: "UTF-8" Name: TIDAL Hi-Fi
Name: "TIDAL Hi-Fi" GenericName: TIDAL Hi-Fi
GenericName: "TIDAL Hi-Fi" Comment: The web version of listen.tidal.com running in electron with hifi support thanks to widevine.
Comment: "The web version of listen.tidal.com running in electron with hifi support thanks to widevine." Icon: tidal-hifi
Icon: "tidal-hifi" StartupNotify: true
StartupNotify: "true" Terminal: false
Terminal: "false" Type: Application
Type: "Application" Categories: Network;Application;AudioVideo;Audio;Video
Categories: "Network;Application;AudioVideo;Audio;Video" StartupWMClass: tidal-hifi
StartupWMClass: "tidal-hifi" X-PulseAudio-Properties: media.role=music
X-PulseAudio-Properties: "media.role=music" MimeType: x-scheme-handler/tidal;
MimeType: "x-scheme-handler/tidal;"
mac: mac:
category: public.app-category.entertainment category: public.app-category.entertainment
win: win:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

5490
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,16 @@
{ {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "5.18.2", "version": "5.8.0",
"description": "Tidal on Electron with widevine(hifi) support", "description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js", "main": "ts-dist/main.js",
"scripts": { "scripts": {
"start": "electron --inspect=0.0.0.0:5858 --remote-debugging-port=8315 --remote-allow-origins=* .", "start": "electron --inspect=0.0.0.0:5858 .",
"watchStart": "nodemon dist -x \"npm run start\"", "watchStart": "nodemon dist -x \"npm run start\"",
"compile": "tsc && npm run sass-and-copy", "compile": "tsc && npm run sass-and-copy",
"deps": "npm run watch", "deps": "npm run watch",
"watch": "tsc-watch --onSuccess \"npm run compile-all\"", "watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist", "copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
"copy-themes-dev": "copyfiles -u 1 \"./themes/*\" node_modules/electron/dist/resources", "copy-themes-dev": "copyfiles -u 1 \"./themes/*\" node_modules/electron/dist/resources",
"compile-all": "npm run sass-and-copy && ts-node scripts/generate-swagger.ts",
"sass-and-copy": "npm run sass && npm run copy-files && npm run copy-themes-dev", "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": "npm run builder -- -c ./build/electron-builder.yml",
"build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml", "build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml",
@ -40,44 +39,38 @@
"homepage": "https://github.com/Mastermindzh/tidal-hifi", "homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/remote": "^2.1.2", "@electron/remote": "^2.1.1",
"@types/swagger-jsdoc": "^6.0.4", "axios": "^1.6.5",
"@xhayper/discord-rpc": "^1.2.1", "discord-rpc": "^4.0.1",
"axios": "^1.8.4", "electron-store": "^8.1.0",
"electron-store": "^8.2.0", "express": "^4.18.2",
"express": "^4.21.2", "hotkeys-js": "^3.13.3",
"hotkeys-js": "^3.13.9",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"sass": "1.86.0", "request": "^2.88.2",
"swagger-ui-express": "^5.0.1", "sass": "^1.69.7"
"cors": "^2.8.5",
"request": "^2.88.2"
}, },
"devDependencies": { "devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0", "@mastermindzh/prettier-config": "^1.0.0",
"@types/cors": "^2.8.17", "@types/discord-rpc": "^4.0.8",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^20.14.10", "@types/node": "^20.10.6",
"@types/request": "^2.48.12", "@types/request": "^2.48.12",
"@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^6.18.0",
"@typescript-eslint/parser": "^7.15.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"electron": "github:castlabs/electron-releases#v35.1.1+wvcus", "electron": "git+https://github.com/castlabs/electron-releases#v28.1.1+wvcus",
"electron-builder": "~26.0.12", "electron-builder": "^24.9.1",
"eslint": "^8.57.0", "eslint": "^8.56.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",
"nodemon": "^3.1.4", "nodemon": "^3.0.2",
"prettier": "^3.3.2", "prettier": "^3.1.1",
"stylelint": "^16.6.1", "stylelint": "^16.1.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^36.0.0",
"stylelint-config-standard-scss": "^13.1.0", "stylelint-config-standard-scss": "^13.0.0",
"stylelint-prettier": "^5.0.0", "stylelint-prettier": "^5.0.0",
"swagger-jsdoc": "^6.2.8", "tsc-watch": "^6.0.4",
"ts-node": "^10.9.2", "typescript": "^5.3.3"
"tsc-watch": "^6.2.0",
"typescript": "^5.5.3"
}, },
"prettier": "@mastermindzh/prettier-config" "prettier": "@mastermindzh/prettier-config"
} }

View File

@ -1,30 +0,0 @@
import fs from "fs";
import swaggerjsdoc from "swagger-jsdoc";
import packagejson from "./../package.json";
const specs = swaggerjsdoc({
definition: {
openapi: "3.1.0",
info: {
title: "TIDAL Hi-Fi API",
version: packagejson.version,
description: "",
license: {
name: packagejson.license,
url: "https://github.com/Mastermindzh/tidal-hifi/blob/master/LICENSE",
},
contact: {
name: "Rick <mastermindzh> van Lieshout",
url: "https://www.rickvanlieshout.com",
},
},
externalDocs: {
description: "swagger.json",
url: "swagger.json",
},
},
apis: ["**/*.ts"],
});
fs.writeFileSync("src/features/api/swagger.json", JSON.stringify(specs, null, 2), "utf8");
console.log("Written swagger.json");

View File

@ -16,19 +16,16 @@
search: '[class^="searchField"]', search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]', shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]', repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]', account: '*[class^="profileOptions"]',
settings: '*[data-test^="sidebar-menu-button"]', settings: '*[data-test^="open-settings"]',
openSettings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]', media: '*[data-test="current-media-imagery"]',
image: "img", image: "img",
current: '*[data-test="current-time"]', current: '*[data-test="current-time"]',
duration: '*[class^=_playbackControlsContainer] *[data-test="duration"]', duration: '*[class^=playbackControlsContainer] *[data-test="duration"]',
bar: '*[data-test="progress-bar"]', bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer", footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']", mediaItem: "[data-type='mediaItem']",
album_header_title: '*[class^="_playingFrom"] span:nth-child(2)', album_header_title: '.header-details [data-test="title"]',
playing_from: '*[class^="_playingFrom"] span:nth-child(2)',
queue_album: "*[class^=playQueueItemsContainer] *[class^=groupTitle] span:nth-child(2)",
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]', album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]', tracklist_row: '[data-test="tracklist-row"]',

View File

@ -10,9 +10,7 @@ export const globalEvents = {
showSettings: "showSettings", showSettings: "showSettings",
storeChanged: "storeChanged", storeChanged: "storeChanged",
error: "error", error: "error",
getUniversalLink: "getUniversalLink", whip: "whip",
log: "log", log: "log",
toggleFavorite: "toggleFavorite", toggleFavorite: "toggleFavorite",
toggleShuffle: "toggleShuffle",
toggleRepeat: "toggleRepeat",
}; };

View File

@ -10,15 +10,10 @@
*/ */
export const settings = { export const settings = {
adBlock: "adBlock", adBlock: "adBlock",
advanced: {
root: "advanced",
tidalUrl: "advanced.tidalUrl",
},
api: "api", api: "api",
apiSettings: { apiSettings: {
root: "apiSettings", root: "apiSettings",
port: "apiSettings.port", port: "apiSettings.port",
hostname: "apiSettings.hostname",
}, },
customCSS: "customCSS", customCSS: "customCSS",
disableBackgroundThrottle: "disableBackgroundThrottle", disableBackgroundThrottle: "disableBackgroundThrottle",
@ -29,10 +24,6 @@ export const settings = {
detailsPrefix: "discord.detailsPrefix", detailsPrefix: "discord.detailsPrefix",
buttonText: "discord.buttonText", buttonText: "discord.buttonText",
includeTimestamps: "discord.includeTimestamps", includeTimestamps: "discord.includeTimestamps",
showSong: "discord.showSong",
showIdle: "discord.showIdle",
idleText: "discord.idleText",
usingText: "discord.usingText",
}, },
ListenBrainz: { ListenBrainz: {
root: "ListenBrainz", root: "ListenBrainz",
@ -55,7 +46,6 @@ export const settings = {
singleInstance: "singleInstance", singleInstance: "singleInstance",
skipArtists: "skipArtists", skipArtists: "skipArtists",
skippedArtists: "skippedArtists", skippedArtists: "skippedArtists",
staticWindowTitle: "staticWindowTitle",
theme: "theme", theme: "theme",
trayIcon: "trayIcon", trayIcon: "trayIcon",
updateFrequency: "updateFrequency", updateFrequency: "updateFrequency",

View File

@ -1,123 +0,0 @@
import { Request, Response, Router } from "express";
import fs from "fs";
import { mediaInfo } from "../../../scripts/mediaInfo";
export const addCurrentInfo = (expressApp: Router) => {
/**
* @swagger
* tags:
* name: current
* description: The current media info API
* components:
* schemas:
* MediaInfo:
* type: object
* properties:
* title:
* type: string
* artists:
* type: string
* album:
* type: string
* icon:
* type: string
* format: uri
* playingFrom:
* type: string
* status:
* type: string
* url:
* type: string
* format: uri
* current:
* type: string
* currentInSeconds:
* type: integer
* duration:
* type: string
* durationInSeconds:
* type: integer
* image:
* type: string
* format: uri
* favorite:
* type: boolean
* player:
* type: object
* properties:
* status:
* type: string
* shuffle:
* type: boolean
* repeat:
* type: string
* artist:
* type: string
* example:
* title: "Sample Title"
* artists: "Sample Artist"
* album: "Sample Album"
* icon: "/path/to/sample/icon.jpg"
* playingFrom: "Sample Playlist"
* status: "playing"
* url: "https://tidal.com/browse/track/sample"
* current: "1:23"
* currentInSeconds: 83
* duration: "3:45"
* durationInSeconds: 225
* image: "https://example.com/sample-image.jpg"
* favorite: true
* player:
* status: "playing"
* shuffle: true
* repeat: "one"
* artist: "Sample Artist"
*/
/**
* @swagger
* /current:
* get:
* summary: Get current media info
* tags: [current]
* responses:
* 200:
* description: Current media info
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/MediaInfo'
*/
expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists }));
/**
* @swagger
* /current/image:
* get:
* summary: Get current media image
* tags: [current]
* responses:
* 200:
* description: Current media image
* content:
* image/png:
* schema:
* type: string
* format: binary
* 404:
* description: Not found
*/
expressApp.get("/current/image", getCurrentImage);
};
export const getCurrentImage = (req: Request, res: Response) => {
const stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function () {
res.set("Content-Type", "image/png");
stream.pipe(res);
});
stream.on("error", function () {
res.set("Content-Type", "text/plain");
res.status(404).end("Not found");
});
};

View File

@ -1,164 +0,0 @@
import { BrowserWindow } from "electron";
import { Router } from "express";
import { globalEvents } from "../../../constants/globalEvents";
import { settings } from "../../../constants/settings";
import { MediaStatus } from "../../../models/mediaStatus";
import { mediaInfo } from "../../../scripts/mediaInfo";
import { settingsStore } from "../../../scripts/settings";
import { handleWindowEvent } from "../helpers/handleWindowEvent";
export const addPlaybackControl = (expressApp: Router, mainWindow: BrowserWindow) => {
const windowEvent = handleWindowEvent(mainWindow);
const createRoute = (route: string) => `/player${route}`;
/**
* @swagger
* tags:
* name: player
* description: The player control API
* components:
* schemas:
* OkResponse:
* type: string
* example: "OK"
*/
const createPlayerAction = (route: string, action: string) => {
expressApp.post(createRoute(route), (req, res) => windowEvent(res, action));
};
if (settingsStore.get(settings.playBackControl)) {
/**
* @swagger
* /player/play:
* post:
* summary: Play the current media
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/play", globalEvents.play);
/**
* @swagger
* /player/favorite/toggle:
* post:
* summary: Add the current media to your favorites, or remove it if its already added to your favorites
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/favorite/toggle", globalEvents.toggleFavorite);
/**
* @swagger
* /player/pause:
* post:
* summary: Pause the current media
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/pause", globalEvents.pause);
/**
* @swagger
* /player/next:
* post:
* summary: Play the next song
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/next", globalEvents.next);
/**
* @swagger
* /player/previous:
* post:
* summary: Play the previous song
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/previous", globalEvents.previous);
/**
* @swagger
* /player/shuffle/toggle:
* post:
* summary: Play the previous song
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/shuffle/toggle", globalEvents.toggleShuffle);
/**
* @swagger
* /player/repeat/toggle:
* post:
* summary: Toggle the repeat status, toggles between "off" , "single" and "all"
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
createPlayerAction("/repeat/toggle", globalEvents.toggleRepeat);
/**
* @swagger
* /player/playpause:
* post:
* summary: Start playing the media if paused, or pause the media if playing
* tags: [player]
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.post(createRoute("/playpause"), (req, res) => {
if (mediaInfo.status === MediaStatus.playing) {
windowEvent(res, globalEvents.pause);
} else {
windowEvent(res, globalEvents.play);
}
});
}
};

View File

@ -1,121 +0,0 @@
import { Request, Router } from "express";
import { settings } from "../../../../constants/settings";
import { mediaInfo } from "../../../../scripts/mediaInfo";
import {
addSkippedArtists,
removeSkippedArtists,
settingsStore,
} from "../../../../scripts/settings";
import { BrowserWindow } from "electron";
import { globalEvents } from "../../../../constants/globalEvents";
/**
* @swagger
* tags:
* name: settings
* description: The settings management API
* components:
* schemas:
* StringArray:
* type: array
* items:
* type: string
* example: ["Artist1", "Artist2"]
*
* @param expressApp
* @param mainWindow
*/
export const addSettingsAPI = (expressApp: Router, mainWindow: BrowserWindow) => {
/**
* @swagger
* /settings/skipped-artists:
* get:
* summary: get a list of artists that TIDAL Hi-Fi will skip if skipping is enabled
* tags: [settings]
* responses:
* 200:
* description: The list book.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/StringArray'
*/
expressApp.get("/settings/skipped-artists", (req, res) => {
res.json(settingsStore.get<string, string[]>(settings.skippedArtists));
});
/**
* @swagger
* /settings/skipped-artists:
* post:
* summary: Add new artists to the list of skipped artists
* tags: [settings]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/StringArray'
* responses:
* 200:
* description: Ok
*/
expressApp.post("/settings/skipped-artists", (req: Request<object, object, string[]>, res) => {
addSkippedArtists(req.body);
res.sendStatus(200);
});
/**
* @swagger
* /settings/skipped-artists/delete:
* post:
* summary: Remove artists from the list of skipped artists
* tags: [settings]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/StringArray'
* responses:
* 200:
* description: Ok
*/
expressApp.post(
"/settings/skipped-artists/delete",
(req: Request<object, object, string[]>, res) => {
removeSkippedArtists(req.body);
res.sendStatus(200);
}
);
/**
* @swagger
* /settings/skipped-artists/current:
* post:
* summary: Add the current artist to the list of skipped artists
* tags: [settings]
* responses:
* 200:
* description: Ok
*/
expressApp.post("/settings/skipped-artists/current", (req, res) => {
addSkippedArtists([mediaInfo.artists]);
mainWindow.webContents.send("globalEvent", globalEvents.next);
res.sendStatus(200);
});
/**
* @swagger
* /settings/skipped-artists/current:
* delete:
* summary: Remove the current artist from the list of skipped artists
* tags: [settings]
* responses:
* 200:
* description: Ok
*/
expressApp.delete("/settings/skipped-artists/current", (req, res) => {
removeSkippedArtists([mediaInfo.artists]);
res.sendStatus(200);
});
};

View File

@ -1,12 +0,0 @@
import { BrowserWindow } from "electron";
import { Response } from "express";
/**
* Shorthand to handle a fire and forget global event
* @param {*} res
* @param {*} action
*/
export const handleWindowEvent = (mainWindow: BrowserWindow) => (res: Response, action: string) => {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
};

View File

@ -1,42 +0,0 @@
import cors from "cors";
import { BrowserWindow, dialog } from "electron";
import express from "express";
import swaggerUi from "swagger-ui-express";
import { settingsStore } from "../../scripts/settings";
import { settings } from "./../../constants/settings";
import { addCurrentInfo } from "./features/current";
import { addPlaybackControl } from "./features/player";
import { addSettingsAPI } from "./features/settings/settings";
import { addLegacyApi } from "./legacy";
import swaggerSpec from "./swagger.json";
/**
* Function to enable TIDAL Hi-Fi's express api
*/
export const startApi = (mainWindow: BrowserWindow) => {
const port = settingsStore.get<string, number>(settings.apiSettings.port);
const hostname = settingsStore.get<string, string>(settings.apiSettings.hostname) ?? "127.0.0.1";
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.json());
expressApp.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/swagger.json", (req, res) => res.json(swaggerSpec));
// add features
addLegacyApi(expressApp, mainWindow);
addPlaybackControl(expressApp, mainWindow);
addCurrentInfo(expressApp);
addSettingsAPI(expressApp, mainWindow);
const expressInstance = expressApp.listen(port, hostname);
expressInstance.on("error", function (e: { code: string }) {
let message = e.code;
if (e.code === "EADDRINUSE") {
message = `Port ${port} in use.`;
}
dialog.showErrorBox("Api failed to start.", message);
});
};

View File

@ -1,160 +0,0 @@
import { BrowserWindow } from "electron";
import { Response, Router } from "express";
import { globalEvents } from "../../constants/globalEvents";
import { settings } from "../../constants/settings";
import { MediaStatus } from "../../models/mediaStatus";
import { mediaInfo } from "../../scripts/mediaInfo";
import { settingsStore } from "../../scripts/settings";
import { getCurrentImage } from "./features/current";
/**
* The legacy API, this will not be maintained and probably has duplicate code :)
* @param expressApp
* @param mainWindow
*/
export const addLegacyApi = (expressApp: Router, mainWindow: BrowserWindow) => {
/**
* @swagger
* /image:
* get:
* summary: Get current image
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Current image
* content:
* image/png:
* schema:
* type: string
* format: binary
* 404:
* description: Not found
*/
expressApp.get("/image", getCurrentImage);
if (settingsStore.get(settings.playBackControl)) {
addLegacyControls();
}
function addLegacyControls() {
/**
* @swagger
* /play:
* get:
* summary: Play the current media
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Action performed
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.get("/play", ({ res }) => handleGlobalEvent(res, globalEvents.play));
/**
* @swagger
* /favorite/toggle:
* get:
* summary: Add the current media to your favorites, or remove it if its already added to your favorites
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.post("/favorite/toggle", (req, res) =>
handleGlobalEvent(res, globalEvents.toggleFavorite)
);
/**
* @swagger
* /pause:
* get:
* summary: Pause the current media
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause));
/**
* @swagger
* /next:
* get:
* summary: Play the next song
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next));
/**
* @swagger
* /previous:
* get:
* summary: Play the previous song
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous));
/**
* @swagger
* /playpause:
* get:
* summary: Toggle play/pause
* tags: [legacy]
* deprecated: true
* responses:
* 200:
* description: Ok
* content:
* text/plain:
* schema:
* $ref: '#/components/schemas/OkResponse'
*/
expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status === MediaStatus.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);
}
});
}
/**
* Shorthand to handle a fire and forget global event
* @param {*} res
* @param {*} action
*/
function handleGlobalEvent(res: Response, action: string) {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
}
};

View File

@ -1,535 +0,0 @@
{
"openapi": "3.1.0",
"info": {
"title": "TIDAL Hi-Fi API",
"version": "5.18.2",
"description": "",
"license": {
"name": "MIT",
"url": "https://github.com/Mastermindzh/tidal-hifi/blob/master/LICENSE"
},
"contact": {
"name": "Rick <mastermindzh> van Lieshout",
"url": "https://www.rickvanlieshout.com"
}
},
"externalDocs": {
"description": "swagger.json",
"url": "swagger.json"
},
"paths": {
"/current": {
"get": {
"summary": "Get current media info",
"tags": ["current"],
"responses": {
"200": {
"description": "Current media info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MediaInfo"
}
}
}
}
}
}
},
"/current/image": {
"get": {
"summary": "Get current media image",
"tags": ["current"],
"responses": {
"200": {
"description": "Current media image",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
},
"/player/play": {
"post": {
"summary": "Play the current media",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/favorite/toggle": {
"post": {
"summary": "Add the current media to your favorites, or remove it if its already added to your favorites",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/pause": {
"post": {
"summary": "Pause the current media",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/next": {
"post": {
"summary": "Play the next song",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/previous": {
"post": {
"summary": "Play the previous song",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/shuffle/toggle": {
"post": {
"summary": "Play the previous song",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/repeat/toggle": {
"post": {
"summary": "Toggle the repeat status, toggles between \"off\" , \"single\" and \"all\"",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/player/playpause": {
"post": {
"summary": "Start playing the media if paused, or pause the media if playing",
"tags": ["player"],
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/settings/skipped-artists": {
"get": {
"summary": "get a list of artists that TIDAL Hi-Fi will skip if skipping is enabled",
"tags": ["settings"],
"responses": {
"200": {
"description": "The list book.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StringArray"
}
}
}
}
}
},
"post": {
"summary": "Add new artists to the list of skipped artists",
"tags": ["settings"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StringArray"
}
}
}
},
"responses": {
"200": {
"description": "Ok"
}
}
}
},
"/settings/skipped-artists/delete": {
"post": {
"summary": "Remove artists from the list of skipped artists",
"tags": ["settings"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StringArray"
}
}
}
},
"responses": {
"200": {
"description": "Ok"
}
}
}
},
"/settings/skipped-artists/current": {
"post": {
"summary": "Add the current artist to the list of skipped artists",
"tags": ["settings"],
"responses": {
"200": {
"description": "Ok"
}
}
},
"delete": {
"summary": "Remove the current artist from the list of skipped artists",
"tags": ["settings"],
"responses": {
"200": {
"description": "Ok"
}
}
}
},
"/image": {
"get": {
"summary": "Get current image",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Current image",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
},
"/play": {
"get": {
"summary": "Play the current media",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Action performed",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/favorite/toggle": {
"get": {
"summary": "Add the current media to your favorites, or remove it if its already added to your favorites",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/pause": {
"get": {
"summary": "Pause the current media",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/next": {
"get": {
"summary": "Play the next song",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/previous": {
"get": {
"summary": "Play the previous song",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
},
"/playpause": {
"get": {
"summary": "Toggle play/pause",
"tags": ["legacy"],
"deprecated": true,
"responses": {
"200": {
"description": "Ok",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"MediaInfo": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"artists": {
"type": "string"
},
"album": {
"type": "string"
},
"icon": {
"type": "string",
"format": "uri"
},
"playingFrom": {
"type": "string"
},
"status": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"current": {
"type": "string"
},
"currentInSeconds": {
"type": "integer"
},
"duration": {
"type": "string"
},
"durationInSeconds": {
"type": "integer"
},
"image": {
"type": "string",
"format": "uri"
},
"favorite": {
"type": "boolean"
},
"player": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"shuffle": {
"type": "boolean"
},
"repeat": {
"type": "string"
}
}
},
"artist": {
"type": "string"
}
},
"example": {
"title": "Sample Title",
"artists": "Sample Artist",
"album": "Sample Album",
"icon": "/path/to/sample/icon.jpg",
"playingFrom": "Sample Playlist",
"status": "playing",
"url": "https://tidal.com/browse/track/sample",
"current": "1:23",
"currentInSeconds": 83,
"duration": "3:45",
"durationInSeconds": 225,
"image": "https://example.com/sample-image.jpg",
"favorite": true,
"player": {
"status": "playing",
"shuffle": true,
"repeat": "one"
},
"artist": "Sample Artist"
}
},
"OkResponse": {
"type": "string",
"example": "OK"
},
"StringArray": {
"type": "array",
"items": {
"type": "string"
},
"example": ["Artist1", "Artist2"]
}
}
},
"tags": [
{
"name": "current",
"description": "The current media info API"
},
{
"name": "player",
"description": "The player control API"
},
{
"name": "settings",
"description": "The settings management API"
}
]
}

View File

@ -48,9 +48,6 @@ export class ListenBrainz {
} else { } else {
// Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area // 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 oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
const tidalUrl =
settingsStore.get<string, string>(settings.advanced.tidalUrl) ||
"https://listen.tidal.com";
const playing_data = { const playing_data = {
listen_type: "playing_now", listen_type: "playing_now",
payload: [ payload: [
@ -98,7 +95,7 @@ export class ListenBrainz {
additional_info: { additional_info: {
media_player: "Tidal Hi-Fi", media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi", submission_client: "Tidal Hi-Fi",
music_service: tidalUrl, music_service: "listen.tidal.com",
duration: oldData.duration, duration: oldData.duration,
}, },
artist_name: oldData.artists, artist_name: oldData.artists,

View File

@ -1,10 +0,0 @@
export class SharingService {
/**
* Retrieve the universal link given a regular track link
* @param currentUrl
* @returns
*/
public static getUniversalLink(currentUrl: string): string {
return `${currentUrl}?u`;
}
}

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,14 +0,0 @@
/**
* Convert a HH:MM:SS string (or variants such as MM:SS or SS) to plain seconds
* @param duration in HH:MM:SS format
* @returns number of seconds in duration
*/
export const convertDurationToSeconds = (duration: string) => {
return duration
.split(":")
.reverse()
.map((val) => Number(val))
.reduce((previous, current, index) => {
return index === 0 ? current : previous + current * Math.pow(60, index);
}, 0);
};

View File

@ -1,19 +1,28 @@
import { enable, initialize } from "@electron/remote/main"; import { enable, initialize } from "@electron/remote/main";
import { BrowserWindow, app, components, ipcMain, session } from "electron"; import {
app,
BrowserWindow,
components,
globalShortcut,
ipcMain,
protocol,
session,
} from "electron";
import path from "path"; import path from "path";
import { globalEvents } from "./constants/globalEvents"; import { globalEvents } from "./constants/globalEvents";
import { mediaKeys } from "./constants/mediaKeys";
import { settings } from "./constants/settings"; import { settings } from "./constants/settings";
import { startApi } from "./features/api";
import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags"; import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags";
import { import {
acquireInhibitorIfInactive, acquireInhibitorIfInactive,
releaseInhibitorIfActive, releaseInhibitorIfActive,
} from "./features/idleInhibitor/idleInhibitor"; } from "./features/idleInhibitor/idleInhibitor";
import { Logger } from "./features/logger"; import { Logger } from "./features/logger";
import { SharingService } from "./features/sharingService/sharingService"; import { Songwhip } from "./features/songwhip/songwhip";
import { MediaInfo } from "./models/mediaInfo"; import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus"; import { MediaStatus } from "./models/mediaStatus";
import { initRPC, rpc, unRPC } from "./scripts/discord"; import { initRPC, rpc, unRPC } from "./scripts/discord";
import { startExpress } from "./scripts/express";
import { updateMediaInfo } from "./scripts/mediaInfo"; import { updateMediaInfo } from "./scripts/mediaInfo";
import { addMenu } from "./scripts/menu"; import { addMenu } from "./scripts/menu";
import { import {
@ -24,8 +33,10 @@ import {
showSettingsWindow, showSettingsWindow,
} from "./scripts/settings"; } from "./scripts/settings";
import { addTray, refreshTray } from "./scripts/tray"; import { addTray, refreshTray } from "./scripts/tray";
const tidalUrl = "https://listen.tidal.com";
let mainInhibitorId = -1; let mainInhibitorId = -1;
initialize();
let mainWindow: BrowserWindow; let mainWindow: BrowserWindow;
const icon = path.join(__dirname, "../assets/icon.png"); const icon = path.join(__dirname, "../assets/icon.png");
const PROTOCOL_PREFIX = "tidal"; const PROTOCOL_PREFIX = "tidal";
@ -38,9 +49,6 @@ const windowPreferences = {
setDefaultFlags(app); setDefaultFlags(app);
setManagedFlagsFromSettings(app); setManagedFlagsFromSettings(app);
const tidalUrl =
settingsStore.get<string, string>(settings.advanced.tidalUrl) || "https://listen.tidal.com";
/** /**
* Update the menuBarVisibility according to the store value * Update the menuBarVisibility according to the store value
* *
@ -53,31 +61,20 @@ function syncMenuBarWithStore() {
} }
/** /**
* @returns true/false based on whether the current window is the main window * Determine whether the current window is the main window
* if singleInstance is requested.
* If singleInstance isn't requested simply return true
* @returns true if singInstance is not requested, otherwise true/false based on whether the current window is the main window
*/ */
function isMainInstance() { function isMainInstanceOrMultipleInstancesAllowed() {
return app.requestSingleInstanceLock(); if (settingsStore.get(settings.singleInstance)) {
} const gotTheLock = app.requestSingleInstanceLock();
/** if (!gotTheLock) {
* @returns true/false based on whether multiple instances are allowed return false;
*/ }
function isMultipleInstancesAllowed() {
return !settingsStore.get(settings.singleInstance);
}
/**
* @param args the arguments passed to the app
* @returns the custom protocol url if it exists, otherwise null
*/
function getCustomProtocolUrl(args: string[]) {
const customProtocolArg = args.find((arg) => arg.startsWith(PROTOCOL_PREFIX));
if (!customProtocolArg) {
return null;
} }
return true;
return tidalUrl + "/" + customProtocolArg.substring(PROTOCOL_PREFIX.length + 3);
} }
function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
@ -97,21 +94,12 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
}, },
}, },
}); });
enable(mainWindow.webContents); enable(mainWindow.webContents);
registerHttpProtocols(); registerHttpProtocols();
syncMenuBarWithStore(); syncMenuBarWithStore();
// find the custom protocol argument // load the Tidal website
const customProtocolUrl = getCustomProtocolUrl(process.argv); mainWindow.loadURL(tidalUrl);
if (customProtocolUrl) {
// load the url received from the custom protocol
mainWindow.loadURL(customProtocolUrl);
} else {
// load the Tidal website
mainWindow.loadURL(tidalUrl);
}
if (settingsStore.get(settings.disableBackgroundThrottle)) { if (settingsStore.get(settings.disableBackgroundThrottle)) {
// prevent setInterval lag // prevent setInterval lag
@ -126,7 +114,6 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
} }
return false; return false;
}); });
// Emitted when the window is closed. // Emitted when the window is closed.
mainWindow.on("closed", function () { mainWindow.on("closed", function () {
releaseInhibitorIfActive(mainInhibitorId); releaseInhibitorIfActive(mainInhibitorId);
@ -152,34 +139,28 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
} }
function registerHttpProtocols() { function registerHttpProtocols() {
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request) => {
mainWindow.loadURL(`${tidalUrl}/${request.url.substring(PROTOCOL_PREFIX.length + 3)}`);
});
if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) { if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) {
app.setAsDefaultProtocolClient(PROTOCOL_PREFIX); app.setAsDefaultProtocolClient(PROTOCOL_PREFIX);
} }
} }
function addGlobalShortcuts() {
Object.keys(mediaKeys).forEach((key) => {
globalShortcut.register(`${key}`, () => {
mainWindow.webContents.send("globalEvent", `${(mediaKeys as any)[key]}`);
});
});
}
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.on("ready", async () => { app.on("ready", async () => {
// check if the app is the main instance and multiple instances are not allowed if (isMainInstanceOrMultipleInstancesAllowed()) {
if (isMainInstance() && !isMultipleInstancesAllowed()) {
app.on("second-instance", (_, commandLine) => {
const customProtocolUrl = getCustomProtocolUrl(commandLine);
if (customProtocolUrl) {
mainWindow.loadURL(customProtocolUrl);
}
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
if (isMainInstance() || isMultipleInstancesAllowed()) {
await components.whenReady(); await components.whenReady();
initialize();
// Adblock // Adblock
if (settingsStore.get(settings.adBlock)) { if (settingsStore.get(settings.adBlock)) {
@ -190,16 +171,15 @@ app.on("ready", async () => {
}); });
} }
Logger.log("components ready:", components.status());
createWindow(); createWindow();
addMenu(mainWindow); addMenu(mainWindow);
createSettingsWindow(); createSettingsWindow();
addGlobalShortcuts();
if (settingsStore.get(settings.trayIcon)) { if (settingsStore.get(settings.trayIcon)) {
addTray(mainWindow, { icon }); addTray(mainWindow, { icon });
refreshTray(mainWindow); refreshTray(mainWindow);
} }
settingsStore.get(settings.api) && startApi(mainWindow); settingsStore.get(settings.api) && startExpress(mainWindow);
settingsStore.get(settings.enableDiscord) && initRPC(); settingsStore.get(settings.enableDiscord) && initRPC();
} else { } else {
app.quit(); app.quit();
@ -254,8 +234,8 @@ ipcMain.on(globalEvents.error, (event) => {
console.log(event); console.log(event);
}); });
ipcMain.handle(globalEvents.getUniversalLink, async (event, url) => { ipcMain.handle(globalEvents.whip, async (event, url) => {
return SharingService.getUniversalLink(url); return Songwhip.whip(url);
}); });
Logger.watch(ipcMain); Logger.watch(ipcMain);

View File

@ -1,4 +1,3 @@
import { MediaPlayerInfo } from "./mediaPlayerInfo";
import { MediaStatus } from "./mediaStatus"; import { MediaStatus } from "./mediaStatus";
export interface MediaInfo { export interface MediaInfo {
@ -8,12 +7,8 @@ export interface MediaInfo {
icon: string; icon: string;
status: MediaStatus; status: MediaStatus;
url: string; url: string;
playingFrom: string;
current: string; current: string;
currentInSeconds?: number;
duration: string; duration: string;
durationInSeconds?: number;
image: string; image: string;
favorite: boolean; favorite: boolean;
player?: MediaPlayerInfo;
} }

View File

@ -1,8 +0,0 @@
import { RepeatState } from "./repeatState";
import { MediaStatus } from "./mediaStatus";
export interface MediaPlayerInfo {
status: MediaStatus;
shuffle: boolean;
repeat: RepeatState;
}

13
src/models/options.ts Normal file
View File

@ -0,0 +1,13 @@
export interface Options {
title: string;
artists: string;
album: string;
status: string;
url: string;
current: string;
duration: string;
"app-name": string;
image: string;
icon: string;
favorite: boolean;
}

View File

@ -1,5 +0,0 @@
export enum RepeatState {
off = "off",
all = "all",
single = "single",
}

View File

@ -20,23 +20,16 @@ const switchesWithSettings = {
classToHide: "discord_options", classToHide: "discord_options",
settingsKey: settings.enableDiscord, settingsKey: settings.enableDiscord,
}, },
discord_show_song: {
switch: "discord_show_song",
classToHide: "discord_show_song_options",
settingsKey: settings.discord.showSong,
},
}; };
let adBlock: HTMLInputElement, let adBlock: HTMLInputElement,
api: HTMLInputElement, api: HTMLInputElement,
channel: HTMLSelectElement,
customCSS: HTMLInputElement, customCSS: HTMLInputElement,
disableBackgroundThrottle: HTMLInputElement, disableBackgroundThrottle: HTMLInputElement,
disableHardwareMediaKeys: HTMLInputElement, disableHardwareMediaKeys: HTMLInputElement,
enableCustomHotkeys: HTMLInputElement, enableCustomHotkeys: HTMLInputElement,
enableDiscord: HTMLInputElement, enableDiscord: HTMLInputElement,
gpuRasterization: HTMLInputElement, gpuRasterization: HTMLInputElement,
hostname: HTMLInputElement,
menuBar: HTMLInputElement, menuBar: HTMLInputElement,
minimizeOnClose: HTMLInputElement, minimizeOnClose: HTMLInputElement,
mpris: HTMLInputElement, mpris: HTMLInputElement,
@ -46,7 +39,6 @@ let adBlock: HTMLInputElement,
singleInstance: HTMLInputElement, singleInstance: HTMLInputElement,
skipArtists: HTMLInputElement, skipArtists: HTMLInputElement,
skippedArtists: HTMLInputElement, skippedArtists: HTMLInputElement,
staticWindowTitle: HTMLInputElement,
theme: HTMLSelectElement, theme: HTMLSelectElement,
trayIcon: HTMLInputElement, trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement, updateFrequency: HTMLInputElement,
@ -57,11 +49,7 @@ let adBlock: HTMLInputElement,
enableWaylandSupport: HTMLInputElement, enableWaylandSupport: HTMLInputElement,
discord_details_prefix: HTMLInputElement, discord_details_prefix: HTMLInputElement,
discord_include_timestamps: HTMLInputElement, discord_include_timestamps: HTMLInputElement,
discord_button_text: HTMLInputElement, discord_button_text: HTMLInputElement;
discord_show_song: HTMLInputElement,
discord_show_idle: HTMLInputElement,
discord_idle_text: HTMLInputElement,
discord_using_text: HTMLInputElement;
addCustomCss(app); addCustomCss(app);
@ -124,7 +112,6 @@ function refreshSettings() {
try { try {
adBlock.checked = settingsStore.get(settings.adBlock); adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api); api.checked = settingsStore.get(settings.api);
channel.value = settingsStore.get(settings.advanced.tidalUrl);
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n"); customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle); disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys); disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
@ -132,7 +119,6 @@ function refreshSettings() {
enableDiscord.checked = settingsStore.get(settings.enableDiscord); enableDiscord.checked = settingsStore.get(settings.enableDiscord);
enableWaylandSupport.checked = settingsStore.get(settings.flags.enableWaylandSupport); enableWaylandSupport.checked = settingsStore.get(settings.flags.enableWaylandSupport);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization); gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
hostname.value = settingsStore.get(settings.apiSettings.hostname);
menuBar.checked = settingsStore.get(settings.menuBar); menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose); minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris); mpris.checked = settingsStore.get(settings.mpris);
@ -141,9 +127,8 @@ function refreshSettings() {
port.value = settingsStore.get(settings.apiSettings.port); port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance); singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists); skipArtists.checked = settingsStore.get(settings.skipArtists);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
staticWindowTitle.checked = settingsStore.get(settings.staticWindowTitle);
theme.value = settingsStore.get(settings.theme); theme.value = settingsStore.get(settings.theme);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon); trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency); updateFrequency.value = settingsStore.get(settings.updateFrequency);
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled); enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
@ -153,10 +138,6 @@ function refreshSettings() {
discord_details_prefix.value = settingsStore.get(settings.discord.detailsPrefix); discord_details_prefix.value = settingsStore.get(settings.discord.detailsPrefix);
discord_include_timestamps.checked = settingsStore.get(settings.discord.includeTimestamps); discord_include_timestamps.checked = settingsStore.get(settings.discord.includeTimestamps);
discord_button_text.value = settingsStore.get(settings.discord.buttonText); discord_button_text.value = settingsStore.get(settings.discord.buttonText);
discord_show_song.checked = settingsStore.get(settings.discord.showSong);
discord_show_idle.checked = settingsStore.get(settings.discord.showIdle);
discord_idle_text.value = settingsStore.get(settings.discord.idleText);
discord_using_text.value = settingsStore.get(settings.discord.usingText);
// set state of all switches with additional settings // set state of all switches with additional settings
Object.values(switchesWithSettings).forEach((settingSwitch) => { Object.values(switchesWithSettings).forEach((settingSwitch) => {
@ -244,7 +225,6 @@ window.addEventListener("DOMContentLoaded", () => {
adBlock = get("adBlock"); adBlock = get("adBlock");
api = get("apiCheckbox"); api = get("apiCheckbox");
channel = get<HTMLSelectElement>("channel");
customCSS = get("customCSS"); customCSS = get("customCSS");
disableBackgroundThrottle = get("disableBackgroundThrottle"); disableBackgroundThrottle = get("disableBackgroundThrottle");
disableHardwareMediaKeys = get("disableHardwareMediaKeys"); disableHardwareMediaKeys = get("disableHardwareMediaKeys");
@ -252,7 +232,6 @@ window.addEventListener("DOMContentLoaded", () => {
enableDiscord = get("enableDiscord"); enableDiscord = get("enableDiscord");
enableWaylandSupport = get("enableWaylandSupport"); enableWaylandSupport = get("enableWaylandSupport");
gpuRasterization = get("gpuRasterization"); gpuRasterization = get("gpuRasterization");
hostname = get("hostname");
menuBar = get("menuBar"); menuBar = get("menuBar");
minimizeOnClose = get("minimizeOnClose"); minimizeOnClose = get("minimizeOnClose");
mpris = get("mprisCheckbox"); mpris = get("mprisCheckbox");
@ -263,7 +242,6 @@ window.addEventListener("DOMContentLoaded", () => {
trayIcon = get("trayIcon"); trayIcon = get("trayIcon");
skipArtists = get("skipArtists"); skipArtists = get("skipArtists");
skippedArtists = get("skippedArtists"); skippedArtists = get("skippedArtists");
staticWindowTitle = get("staticWindowTitle");
singleInstance = get("singleInstance"); singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency"); updateFrequency = get("updateFrequency");
enableListenBrainz = get("enableListenBrainz"); enableListenBrainz = get("enableListenBrainz");
@ -273,15 +251,10 @@ window.addEventListener("DOMContentLoaded", () => {
discord_include_timestamps = get("discord_include_timestamps"); discord_include_timestamps = get("discord_include_timestamps");
listenbrainz_delay = get("listenbrainz_delay"); listenbrainz_delay = get("listenbrainz_delay");
discord_button_text = get("discord_button_text"); discord_button_text = get("discord_button_text");
discord_show_song = get("discord_show_song");
discord_show_idle = get("discord_show_idle");
discord_using_text = get("discord_using_text");
discord_idle_text = get("discord_idle_text");
refreshSettings(); refreshSettings();
addInputListener(adBlock, settings.adBlock); addInputListener(adBlock, settings.adBlock);
addInputListener(api, settings.api); addInputListener(api, settings.api);
addSelectListener(channel, settings.advanced.tidalUrl);
addTextAreaListener(customCSS, settings.customCSS); addTextAreaListener(customCSS, settings.customCSS);
addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle); addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle);
addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys); addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys);
@ -289,7 +262,6 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(enableDiscord, settings.enableDiscord, switchesWithSettings.discord); addInputListener(enableDiscord, settings.enableDiscord, switchesWithSettings.discord);
addInputListener(enableWaylandSupport, settings.flags.enableWaylandSupport); addInputListener(enableWaylandSupport, settings.flags.enableWaylandSupport);
addInputListener(gpuRasterization, settings.flags.gpuRasterization); addInputListener(gpuRasterization, settings.flags.gpuRasterization);
addInputListener(hostname, settings.apiSettings.hostname);
addInputListener(menuBar, settings.menuBar); addInputListener(menuBar, settings.menuBar);
addInputListener(minimizeOnClose, settings.minimizeOnClose); addInputListener(minimizeOnClose, settings.minimizeOnClose);
addInputListener(mpris, settings.mpris); addInputListener(mpris, settings.mpris);
@ -298,7 +270,6 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(port, settings.apiSettings.port); addInputListener(port, settings.apiSettings.port);
addInputListener(skipArtists, settings.skipArtists); addInputListener(skipArtists, settings.skipArtists);
addTextAreaListener(skippedArtists, settings.skippedArtists); addTextAreaListener(skippedArtists, settings.skippedArtists);
addInputListener(staticWindowTitle, settings.staticWindowTitle);
addInputListener(singleInstance, settings.singleInstance); addInputListener(singleInstance, settings.singleInstance);
addSelectListener(theme, settings.theme); addSelectListener(theme, settings.theme);
addInputListener(trayIcon, settings.trayIcon); addInputListener(trayIcon, settings.trayIcon);
@ -314,12 +285,4 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(discord_details_prefix, settings.discord.detailsPrefix); addInputListener(discord_details_prefix, settings.discord.detailsPrefix);
addInputListener(discord_include_timestamps, settings.discord.includeTimestamps); addInputListener(discord_include_timestamps, settings.discord.includeTimestamps);
addInputListener(discord_button_text, settings.discord.buttonText); addInputListener(discord_button_text, settings.discord.buttonText);
addInputListener(
discord_show_song,
settings.discord.showSong,
switchesWithSettings.discord_show_song
);
addInputListener(discord_show_idle, settings.discord.showIdle);
addInputListener(discord_idle_text, settings.discord.idleText);
addInputListener(discord_using_text, settings.discord.usingText);
}); });

View File

@ -49,7 +49,7 @@
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Notifications</h4> <h4>Notifications</h4>
<p>Show a notification when new media starts.</p> <p>Show a notification when a new song starts.</p>
</div> </div>
<label class="switch"> <label class="switch">
<input id="notifications" type="checkbox" /> <input id="notifications" type="checkbox" />
@ -106,16 +106,6 @@
<span class="switch__slider"></span> <span class="switch__slider"></span>
</label> </label>
</div> </div>
<div class="group__option">
<div class="group__description">
<h4>Static Window Title</h4>
<p>Makes the window title "TIDAL Hi-Fi" instead of changing to the currently playing song.</p>
</div>
<label class="switch">
<input id="staticWindowTitle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Minimize on Close</h4> <h4>Minimize on Close</h4>
@ -157,7 +147,7 @@
<p class="group__title">API</p> <p class="group__title">API</p>
<div class="group__description"> <div class="group__description">
<p> <p>
TIDAL Hi-Fi has a built-in web API to allow users to get current media information. TIDAL Hi-Fi has a built-in web API to allow users to get current song information.
You can optionally enable playback control as well. You can optionally enable playback control as well.
</p> </p>
</div> </div>
@ -177,16 +167,6 @@
<input id="port" type="number" class="text-input" name="port" /> <input id="port" type="number" class="text-input" name="port" />
</div> </div>
</div> </div>
<div class="group__option">
<div class="group__description">
<h4>API hostname</h4>
<p>By default (127.0.0.1) only local apps can interface with the API. <br />
Change to 0.0.0.0 to allow <strong>anyone</strong> to interact with it. <br />
Other options are available
</p>
<input id="hostname" type="text" class="text-input" name="hostname" />
</div>
</div>
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Playback control</h4> <h4>Playback control</h4>
@ -236,75 +216,32 @@
</label> </label>
</div> </div>
<div id="discord_options"> <div id="discord_options">
<div class="group__option" class="hidden"> <div class="group__option" class="hidden">
<div class="group__description"> <div class="group__description">
<h4>Show Idle Text</h4> <h4>Include timestamps</h4>
<p>Should the idle text be shown when idle?</p> <p>Show current/end playtime in the Discord client</p>
</div> </div>
<label class="switch"> <label class="switch">
<input id="discord_show_idle" type="checkbox" /> <input id="discord_include_timestamps" type="checkbox" />
<span class="switch__slider"></span> <span class="switch__slider"></span>
</label> </label>
</div> </div>
<div class="group__option" class="hidden"> <div class="group__option" class="hidden">
<div class="group__description"> <div class="group__description">
<h4>Idle Text</h4> <h4>Details prefix</h4>
<p>The text displayed on Discord's rich presence while idling in the app.</p> <p>Prefix for the "details" field of Discord's rich presence.</p>
<input id="discord_idle_text" type="text" class="text-input" name="discord_idle_text" /> <input id="discord_details_prefix" type="text" class="text-input" name="discord_details_prefix" />
</div> </div>
</div> </div>
<div class="group__option" class="hidden"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Using Tidal Text</h4> <h4>Button text</h4>
<p>The text displayed on Discord's rich presence while "showSong" is turned off</p> <p>Text to display on the button below the song information.</p>
<input id="discord_using_text" type="text" class="text-input" name="discord_using_text" /> <input id="discord_button_text" type="text" class="text-input" name="discord_button_text" />
</div> </div>
</div> </div>
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Show media</h4>
<p>Show the current media in the Discord client</p>
</div>
<label class="switch">
<input id="discord_show_song" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div id="discord_show_song_options" class="hidden">
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Include timestamps</h4>
<p>Show current/end playtime in the Discord client</p>
</div>
<label class="switch">
<input id="discord_include_timestamps" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Details prefix</h4>
<p>Prefix for the "details" field of Discord's rich presence.</p>
<input id="discord_details_prefix" type="text" class="text-input" name="discord_details_prefix" />
</div>
</div>
<div class="group__option">
<div class="group__description">
<h4>Button text</h4>
<p>Text to display on the button below the media information.</p>
<input id="discord_button_text" type="text" class="text-input" name="discord_button_text" />
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="group"> <div class="group">
@ -337,7 +274,7 @@
</div> </div>
<div class="group__description"> <div class="group__description">
<h4>ScrobbleDelay</h4> <h4>ScrobbleDelay</h4>
<p>The delay (in ms) to send a listen to ListenBrainz. Prevents spamming the API when you fast forward <p>The delay (in ms) to send a song to ListenBrainz. Prevents spamming the API when you fast forward
immediately</p> immediately</p>
<input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" /> <input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" />
</div> </div>
@ -359,20 +296,6 @@
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" /> <input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
</div> </div>
</div> </div>
<div class="group__option">
<div class="group__description">
<h4>Tidal channel / URL</h4>
<p>
Which URL Tidal Hi-Fi should use.
<strong>note! Beta might break at any time</strong>
</p>
<select class="select-input" id="channel" name="channel">
<option value="https://listen.tidal.com">Stable (listen.tidal.com)</option>
<option value="https://listen.stage.tidal.com">Staging (listen.stage.tidal.com)</option>
</select>
</div>
</div>
</div> </div>
<div class="group"> <div class="group">
<p class="group__title">Flags</p> <p class="group__title">Flags</p>
@ -477,16 +400,14 @@
<img alt="tidal icon" class="about-section__icon" src="./icon.png" /> <img alt="tidal icon" class="about-section__icon" src="./icon.png" />
<h4>TIDAL Hi-Fi</h4> <h4>TIDAL Hi-Fi</h4>
<div class="about-section__version"> <div class="about-section__version">
<a target="_blank" rel="noopener" <a href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.8.0">5.8.0</a>
href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.18.2">5.18.2</a>
</div> </div>
<div class="about-section__links"> <div class="about-section__links">
<a target="_blank" rel="noopener" href="https://github.com/mastermindzh/tidal-hifi/" <a href="https://github.com/mastermindzh/tidal-hifi/" class="about-section__button">Github <i
class="about-section__button">Github class="fa fa-external-link"></i></a>
<i class="fa fa-external-link"></i></a> <a href="https://github.com/Mastermindzh/tidal-hifi/issues" class="about-section__button">Report an issue <i
<a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/issues" class="fa fa-external-link"></i></a>
class="about-section__button">Report an issue <i class="fa fa-external-link"></i></a> <a href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors"
<a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors"
class="about-section__button">Contributors <i class="fa fa-external-link"></i></a> class="about-section__button">Contributors <i class="fa fa-external-link"></i></a>
</div> </div>
</section> </section>

View File

@ -415,6 +415,7 @@ html {
} }
// file upload // file upload
.file-drop-area { .file-drop-area {
position: relative; position: relative;
display: flex; display: flex;

View File

@ -10,31 +10,23 @@ import {
} from "./features/listenbrainz/listenbrainz"; } from "./features/listenbrainz/listenbrainz";
import { StoreData } from "./features/listenbrainz/models/storeData"; import { StoreData } from "./features/listenbrainz/models/storeData";
import { Logger } from "./features/logger"; import { Logger } from "./features/logger";
import { SharingService } from "./features/sharingService/sharingService"; import { Songwhip } from "./features/songwhip/songwhip";
import { addCustomCss } from "./features/theming/theming"; import { addCustomCss } from "./features/theming/theming";
import { convertDurationToSeconds } from "./features/time/parse";
import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus"; import { MediaStatus } from "./models/mediaStatus";
import { RepeatState } from "./models/repeatState"; import { Options } from "./models/options";
import { downloadFile } from "./scripts/download"; import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys"; import { addHotkey } from "./scripts/hotkeys";
import { ObjectToDotNotation } from "./scripts/objectUtilities";
import { settingsStore } from "./scripts/settings"; import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions"; import { setTitle } from "./scripts/window-functions";
const notificationPath = `${app.getPath("userData")}/notification.jpg`; const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "TIDAL Hi-Fi";
let currentSong = ""; let currentSong = "";
let player: Player; let player: Player;
let currentPlayStatus = MediaStatus.paused; let currentPlayStatus = MediaStatus.paused;
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>; let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
let scrobbleWaitingForDelay = false; let scrobbleWaitingForDelay = false;
let currentlyPlaying = MediaStatus.paused;
let currentRepeatState: RepeatState = RepeatState.off;
let currentShuffleState = false;
let currentMediaInfo: MediaInfo;
let currentNotification: Electron.Notification;
const elements = { const elements = {
play: '*[data-test="play"]', play: '*[data-test="play"]',
pause: '*[data-test="pause"]', pause: '*[data-test="pause"]',
@ -48,19 +40,16 @@ const elements = {
search: '[class^="searchField"]', search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]', shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]', repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]', account: '*[class^="profileOptions"]',
settings: '*[data-test^="sidebar-menu-button"]', settings: '*[data-test^="open-settings"]',
openSettings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]', media: '*[data-test="current-media-imagery"]',
image: "img", image: "img",
current: '*[data-test="current-time"]', current: '*[data-test="current-time"]',
duration: '*[class^=_playbackControlsContainer] *[data-test="duration"]', duration: '*[class^=playbackControlsContainer] *[data-test="duration"]',
bar: '*[data-test="progress-bar"]', bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer", footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']", mediaItem: "[data-type='mediaItem']",
album_header_title: '*[class^="_playingFrom"] span:nth-child(2)', album_header_title: '.header-details [data-test="title"]',
playing_from: '*[class^="_playingFrom"] span:nth-child(2)',
queue_album: "*[class^=playQueueItemsContainer] *[class^=groupTitle] span:nth-child(2)",
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']", currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]', album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]', tracklist_row: '[data-test="tracklist-row"]',
@ -75,7 +64,7 @@ const elements = {
}, },
/** /**
* Get the icon of the current media * Get the icon of the current song
*/ */
getSongIcon: function () { getSongIcon: function () {
const figure = this.get("media"); const figure = this.get("media");
@ -91,7 +80,7 @@ const elements = {
}, },
/** /**
* returns an array of all artists in the current media * returns an array of all artists in the current song
* @returns {Array} artists * @returns {Array} artists
*/ */
getArtistsArray: function () { getArtistsArray: function () {
@ -136,12 +125,6 @@ const elements = {
} }
} }
// see whether we're on the queue page and get it from there
const queueAlbumName = elements.getText("queue_album");
if (queueAlbumName) {
return queueAlbumName;
}
return ""; return "";
}, },
@ -196,7 +179,7 @@ function getUpdateFrequency() {
} }
/** /**
* Play or pause the current media * Play or pause the current song
*/ */
function playPause() { function playPause() {
const play = elements.get("play"); const play = elements.get("play");
@ -221,9 +204,9 @@ ListenBrainzStore.clear();
function addHotKeys() { function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) { if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () { addHotkey("Control+p", function () {
elements.click("settings"); elements.click("account");
setTimeout(() => { setTimeout(() => {
elements.click("openSettings"); elements.click("settings");
}, 100); }, 100);
}); });
addHotkey("Control+l", function () { addHotkey("Control+l", function () {
@ -255,10 +238,11 @@ function addHotKeys() {
elements.click("repeat"); elements.click("repeat");
}); });
addHotkey("control+w", async function () { addHotkey("control+w", async function () {
const url = SharingService.getUniversalLink(getTrackURL()); const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL());
const url = Songwhip.getWhipUrl(result);
clipboard.writeText(url); clipboard.writeText(url);
new Notification({ new Notification({
title: `Universal link generated: `, title: `Successfully whipped: `,
body: `URL copied to clipboard: ${url}`, body: `URL copied to clipboard: ${url}`,
}).show(); }).show();
}); });
@ -317,8 +301,6 @@ function addIPCEventListeners() {
ipcRenderer.on("globalEvent", (_event, args) => { ipcRenderer.on("globalEvent", (_event, args) => {
switch (args) { switch (args) {
case globalEvents.playPause: case globalEvents.playPause:
case globalEvents.play:
case globalEvents.pause:
playPause(); playPause();
break; break;
case globalEvents.next: case globalEvents.next:
@ -327,15 +309,15 @@ function addIPCEventListeners() {
case globalEvents.previous: case globalEvents.previous:
elements.click("previous"); elements.click("previous");
break; break;
case globalEvents.play:
elements.click("play");
break;
case globalEvents.pause:
elements.click("pause");
break;
case globalEvents.toggleFavorite: case globalEvents.toggleFavorite:
elements.click("favorite"); elements.click("favorite");
break; break;
case globalEvents.toggleShuffle:
elements.click("shuffle");
break;
case globalEvents.toggleRepeat:
elements.click("repeat");
break;
default: default:
break; break;
} }
@ -359,23 +341,6 @@ function getCurrentlyPlayingStatus() {
return status; return status;
} }
function getCurrentShuffleState() {
const shuffle = elements.get("shuffle");
return shuffle?.getAttribute("aria-checked") === "true";
}
function getCurrentRepeatState() {
const repeat = elements.get("repeat");
switch (repeat?.getAttribute("data-type")) {
case "button__repeatAll":
return RepeatState.all;
case "button__repeatSingle":
return RepeatState.single;
default:
return RepeatState.off;
}
}
/** /**
* Convert the duration from MM:SS to seconds * Convert the duration from MM:SS to seconds
* @param {*} duration * @param {*} duration
@ -388,36 +353,16 @@ function convertDuration(duration: string) {
/** /**
* Update Tidal-hifi's media info * Update Tidal-hifi's media info
* *
* @param {*} mediaInfo * @param {*} options
*/ */
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) { function updateMediaInfo(options: Options, notify: boolean) {
if (mediaInfo) { if (options) {
currentMediaInfo = mediaInfo; ipcRenderer.send(globalEvents.updateInfo, options);
ipcRenderer.send(globalEvents.updateInfo, mediaInfo); if (settingsStore.get(settings.notifications) && notify) {
updateMpris(mediaInfo); new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
updateListenBrainz(mediaInfo);
if (notify) {
sendNotification(mediaInfo);
} }
} updateMpris(options);
} updateListenBrainz(options);
/**
* send a desktop notification if enabled in settings
* @param mediaInfo
* @param notify Whether to notify
*/
async function sendNotification(mediaInfo: MediaInfo) {
if (settingsStore.get(settings.notifications)) {
if (currentNotification) {
currentNotification.close();
}
currentNotification = new Notification({
title: mediaInfo.title,
body: mediaInfo.artists,
icon: mediaInfo.icon,
});
currentNotification.show();
} }
} }
@ -475,34 +420,32 @@ function addMPRIS() {
} }
} }
function updateMpris(mediaInfo: MediaInfo) { function updateMpris(options: Options) {
if (player) { if (player) {
player.metadata = { player.metadata = {
...player.metadata, ...player.metadata,
...{ ...{
"xesam:title": mediaInfo.title, "xesam:title": options.title,
"xesam:artist": [mediaInfo.artists], "xesam:artist": [options.artists],
"xesam:album": mediaInfo.album, "xesam:album": options.album,
"xesam:url": mediaInfo.url, "mpris:artUrl": options.image,
"mpris:artUrl": mediaInfo.image, "mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(), "mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
}, },
...ObjectToDotNotation(mediaInfo, "custom:"),
}; };
player.playbackStatus = mediaInfo.status === MediaStatus.paused ? "Paused" : "Playing"; player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing";
} }
} }
/** /**
* Update the listenbrainz service with new data based on a few conditions * Update the listenbrainz service with new data based on a few conditions
*/ */
function updateListenBrainz(mediaInfo: MediaInfo) { function updateListenBrainz(options: Options) {
if (settingsStore.get(settings.ListenBrainz.enabled)) { if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData; const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
if ( if (
(!oldData && mediaInfo.status === MediaStatus.playing) || (!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== mediaInfo.title) (oldData && oldData.title !== options.title)
) { ) {
if (!scrobbleWaitingForDelay) { if (!scrobbleWaitingForDelay) {
scrobbleWaitingForDelay = true; scrobbleWaitingForDelay = true;
@ -510,10 +453,10 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
currentListenBrainzDelayId = setTimeout( currentListenBrainzDelayId = setTimeout(
() => { () => {
ListenBrainz.scrobble( ListenBrainz.scrobble(
mediaInfo.title, options.title,
mediaInfo.artists, options.artists,
mediaInfo.status, options.status,
convertDuration(mediaInfo.duration) convertDuration(options.duration)
); );
scrobbleWaitingForDelay = false; scrobbleWaitingForDelay = false;
}, },
@ -543,6 +486,23 @@ function getTrackID() {
return window.location; return window.location;
} }
function updateMediaSession(options: Options) {
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: options.title,
artist: options.artists,
album: options.album,
artwork: [
{
src: options.icon,
sizes: "640x640",
type: "image/png",
},
],
});
}
}
/** /**
* Watch for song changes and update title + notify * Watch for song changes and update title + notify
*/ */
@ -550,88 +510,60 @@ setInterval(function () {
const title = elements.getText("title"); const title = elements.getText("title");
const artistsArray = elements.getArtistsArray(); const artistsArray = elements.getArtistsArray();
const artistsString = elements.getArtistsString(artistsArray); const artistsString = elements.getArtistsString(artistsArray);
const songDashArtistTitle = `${title} - ${artistsString}`; skipArtistsIfFoundInSkippedArtistsList(artistsArray);
const staticTitle = "TIDAL Hi-Fi";
const titleOrArtistsChanged = currentSong !== songDashArtistTitle; const album = elements.getAlbumName();
const current = elements.getText("current"); const current = elements.getText("current");
const duration = elements.getText("duration");
const songDashArtistTitle = `${title} - ${artistsString}`;
const currentStatus = getCurrentlyPlayingStatus(); const currentStatus = getCurrentlyPlayingStatus();
const shuffleState = getCurrentShuffleState(); const options = {
const repeatState = getCurrentRepeatState(); title,
artists: artistsString,
album: album,
status: currentStatus,
url: getTrackURL(),
current,
duration,
"app-name": appName,
image: "",
icon: "",
favorite: elements.isFavorite(),
};
const playStateChanged = currentStatus != currentlyPlaying; const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
const shuffleStateChanged = shuffleState != currentShuffleState;
const repeatStateChanged = repeatState != currentRepeatState;
const hasStateChanged = playStateChanged || shuffleStateChanged || repeatStateChanged; // update title, url and play info with new info
setTitle(songDashArtistTitle);
getTrackURL();
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
// update info if song changed or was just paused/resumed const image = elements.getSongIcon();
if (titleOrArtistsChanged || hasStateChanged) {
if (playStateChanged) currentlyPlaying = currentStatus;
if (shuffleStateChanged) currentShuffleState = shuffleState;
if (repeatStateChanged) currentRepeatState = repeatState;
skipArtistsIfFoundInSkippedArtistsList(artistsArray); new Promise<void>((resolve) => {
const album = elements.getAlbumName(); if (image.startsWith("http")) {
const duration = elements.getText("duration"); options.image = image;
const options: MediaInfo = { downloadFile(image, notificationPath).then(
title, () => {
artists: artistsString, options.icon = notificationPath;
album: album, resolve();
playingFrom: elements.getText("playing_from"), },
status: currentStatus, () => {
url: getTrackURL(), // if the image can't be downloaded then continue without it
current, resolve();
currentInSeconds: convertDurationToSeconds(current), }
duration, );
durationInSeconds: convertDurationToSeconds(duration), } else {
image: "", // if the image can't be found on the page continue without it
icon: "", resolve();
favorite: elements.isFavorite(), }
}).then(() => {
player: { updateMediaInfo(options, titleOrArtistsChanged);
status: currentStatus, if (titleOrArtistsChanged) {
shuffle: shuffleState, updateMediaSession(options);
repeat: repeatState, }
}, });
};
// update title, url and play info with new info
settingsStore.get(settings.staticWindowTitle)
? setTitle(staticTitle)
: setTitle(songDashArtistTitle);
getTrackURL();
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
const image = elements.getSongIcon();
new Promise<void>((resolve) => {
if (image.startsWith("http")) {
options.image = image;
downloadFile(image, notificationPath).then(
() => {
options.icon = notificationPath;
resolve();
},
() => {
// if the image can't be downloaded then continue without it
resolve();
}
);
} else {
// if the image can't be found on the page continue without it
resolve();
}
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
});
} else {
// just update the time
updateMediaInfo(
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
false
);
}
/** /**
* automatically skip a song if the artists are found in the list of artists to skip * automatically skip a song if the artists are found in the list of artists to skip

View File

@ -1,141 +1,93 @@
import { Client, SetActivity } from "@xhayper/discord-rpc"; import { Client } from "discord-rpc";
import { app, ipcMain } from "electron"; import { app, ipcMain } from "electron";
import { globalEvents } from "../constants/globalEvents"; import { globalEvents } from "../constants/globalEvents";
import { settings } from "../constants/settings"; import { settings } from "../constants/settings";
import { Logger } from "../features/logger"; import { Logger } from "../features/logger";
import { convertDurationToSeconds } from "../features/time/parse";
import { MediaStatus } from "../models/mediaStatus"; import { MediaStatus } from "../models/mediaStatus";
import { mediaInfo } from "./mediaInfo"; import { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings"; import { settingsStore } from "./settings";
const clientId = "833617820704440341"; const clientId = "833617820704440341";
function timeToSeconds(timeArray: string[]) {
const minutes = parseInt(timeArray[0]) * 1;
const seconds = minutes * 60 + parseInt(timeArray[1]) * 1;
return seconds;
}
export let rpc: Client; export let rpc: Client;
const ACTIVITY_LISTENING = 2;
const MAX_RETRIES = 5;
const RETRY_DELAY = 10000;
const observer = () => { const observer = () => {
if (rpc) { if (mediaInfo.status === MediaStatus.paused && rpc) {
updateActivity(); rpc.setActivity(idleStatus);
} } else if (rpc) {
}; const currentSeconds = timeToSeconds(mediaInfo.current.split(":"));
const durationSeconds = timeToSeconds(mediaInfo.duration.split(":"));
const defaultPresence = { const date = new Date();
largeImageKey: "tidal-hifi-icon", const now = (date.getTime() / 1000) | 0;
largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`, const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
instance: false,
type: ACTIVITY_LISTENING
};
const updateActivity = () => {
const showIdle = settingsStore.get<string, boolean>(settings.discord.showIdle) ?? true;
if (mediaInfo.status === MediaStatus.paused && !showIdle) {
rpc.user?.clearActivity();
} else {
rpc.user?.setActivity(getActivity());
}
};
const getActivity = (): SetActivity => {
const presence: SetActivity = { ...defaultPresence };
if (mediaInfo.status === MediaStatus.paused) {
presence.details =
settingsStore.get<string, string>(settings.discord.idleText) ?? "Browsing Tidal";
} else {
const showSong = settingsStore.get<string, boolean>(settings.discord.showSong) ?? false;
if (showSong) {
const { includeTimestamps, detailsPrefix, buttonText } = getFromStore();
includeTimeStamps(includeTimestamps);
setPresenceFromMediaInfo(detailsPrefix, buttonText);
} else {
presence.details =
settingsStore.get<string, string>(settings.discord.usingText) ?? "Playing media on TIDAL";
}
}
return presence;
function getFromStore() {
const includeTimestamps =
settingsStore.get<string, boolean>(settings.discord.includeTimestamps) ?? true;
const detailsPrefix = const detailsPrefix =
settingsStore.get<string, string>(settings.discord.detailsPrefix) ?? "Listening to "; settingsStore.get<string, string>(settings.discord.detailsPrefix) ?? "Listening to ";
const buttonText = const buttonText =
settingsStore.get<string, string>(settings.discord.buttonText) ?? "Play on TIDAL"; settingsStore.get<string, string>(settings.discord.buttonText) ?? "Play on TIDAL";
const includeTimestamps =
settingsStore.get<string, boolean>(settings.discord.includeTimestamps) ?? true;
return { includeTimestamps, detailsPrefix, buttonText }; let activity = {
} ...idleStatus,
...{
/** startTimestamp: includeTimestamps ? now : undefined,
* Pad a string using spaces to at least 2 characters endTimestamp: includeTimestamps ? remaining : undefined,
* @param input string to pad with 2 characters },
* @returns };
*/
function pad(input: string): string {
return input.padEnd(2, " ");
}
function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) {
// discord requires a minimum of 2 characters
const title = pad(mediaInfo.title);
const album = pad(mediaInfo.album);
const artists = pad(mediaInfo.artists);
if (mediaInfo.url) { if (mediaInfo.url) {
presence.details = `${detailsPrefix}${title}`; activity = {
presence.state = artists ? artists : "unknown artist(s)"; ...activity,
presence.largeImageKey = mediaInfo.image; ...{
if (album) { details: `${detailsPrefix}${mediaInfo.title}`,
presence.largeImageText = album; state: mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)",
} largeImageKey: mediaInfo.image,
largeImageText: mediaInfo.album ? mediaInfo.album : `${idleStatus.largeImageText}`,
presence.buttons = [{ label: buttonText, url: mediaInfo.url }]; buttons: [{ label: buttonText, url: mediaInfo.url }],
},
};
} else { } else {
presence.details = `Watching ${title}`; activity = {
presence.state = artists; ...activity,
...{
details: `Watching ${mediaInfo.title}`,
state: mediaInfo.artists,
},
};
} }
}
function includeTimeStamps(includeTimestamps: boolean) { rpc.setActivity(activity);
if (includeTimestamps) {
const currentSeconds = convertDurationToSeconds(mediaInfo.current);
const durationSeconds = convertDurationToSeconds(mediaInfo.duration);
const now = Math.trunc((Date.now() + 500) / 1000);
presence.startTimestamp = now - currentSeconds;
presence.endTimestamp = presence.startTimestamp + durationSeconds;
}
} }
}; };
/** const idleStatus = {
* Try to login to RPC and retry if it errors details: `Browsing Tidal`,
* @param retryCount Max retry count largeImageKey: "tidal-hifi-icon",
*/ largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`,
const connectWithRetry = async (retryCount = 0) => { instance: false,
try {
await rpc.login();
Logger.log('Connected to Discord');
rpc.on("ready", updateActivity);
Object.values(globalEvents).forEach(event => ipcMain.on(event, observer));
} catch (error) {
if (retryCount < MAX_RETRIES) {
Logger.log(`Failed to connect to Discord, retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
setTimeout(() => connectWithRetry(retryCount + 1), RETRY_DELAY);
} else {
Logger.log('Failed to connect to Discord after maximum retry attempts');
}
}
}; };
/** /**
* Set up the discord rpc and listen on globalEvents.updateInfo * Set up the discord rpc and listen on globalEvents.updateInfo
*/ */
export const initRPC = () => { export const initRPC = () => {
rpc = new Client({ transport: {type: "ipc"}, clientId }); rpc = new Client({ transport: "ipc" });
connectWithRetry(); rpc.login({ clientId }).then(
() => {
rpc.on("ready", () => {
rpc.setActivity(idleStatus);
});
ipcMain.on(globalEvents.updateInfo, observer);
},
() => {
Logger.log("Can't connect to Discord, is it running?");
}
);
}; };
/** /**
@ -143,7 +95,7 @@ export const initRPC = () => {
*/ */
export const unRPC = () => { export const unRPC = () => {
if (rpc) { if (rpc) {
rpc.user?.clearActivity(); rpc.clearActivity();
rpc.destroy(); rpc.destroy();
rpc = null; rpc = null;
ipcMain.removeListener(globalEvents.updateInfo, observer); ipcMain.removeListener(globalEvents.updateInfo, observer);

69
src/scripts/express.ts Normal file
View File

@ -0,0 +1,69 @@
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 { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings";
/**
* Function to enable TIDAL Hi-Fi's express api
*/
// expressModule.run = function (mainWindow)
export const startExpress = (mainWindow: BrowserWindow) => {
/**
* Shorthand to handle a fire and forget global event
* @param {*} res
* @param {*} action
*/
function handleGlobalEvent(res: Response, action: string) {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
}
const expressApp = express();
expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists }));
expressApp.get("/image", (req, res) => {
const stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function () {
res.set("Content-Type", "image/png");
stream.pipe(res);
});
stream.on("error", function () {
res.set("Content-Type", "text/plain");
res.status(404).end("Not found");
});
});
if (settingsStore.get(settings.playBackControl)) {
expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play));
expressApp.post("/favorite/toggle", (req, res) =>
handleGlobalEvent(res, globalEvents.toggleFavorite)
);
expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause));
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 === MediaStatus.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);
}
});
}
const port = settingsStore.get<string, number>(settings.apiSettings.port);
const expressInstance = expressApp.listen(port, "127.0.0.1");
expressInstance.on("error", function (e: { code: string }) {
let message = e.code;
if (e.code === "EADDRINUSE") {
message = `Port ${port} in use.`;
}
dialog.showErrorBox("Api failed to start.", message);
});
};

View File

@ -1,45 +1,37 @@
import { MediaInfo } from "../models/mediaInfo"; import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus"; import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
const defaultInfo: MediaInfo = { export const mediaInfo = {
title: "", title: "",
artists: "", artists: "",
album: "", album: "",
icon: "", icon: "",
playingFrom: "", status: MediaStatus.paused as string,
status: MediaStatus.paused,
url: "", url: "",
current: "", current: "",
currentInSeconds: 0,
duration: "", duration: "",
durationInSeconds: 0,
image: "tidal-hifi-icon", image: "tidal-hifi-icon",
favorite: false, favorite: false,
player: {
status: MediaStatus.paused,
shuffle: false,
repeat: RepeatState.off,
},
}; };
export let mediaInfo: MediaInfo = { ...defaultInfo };
export const updateMediaInfo = (arg: MediaInfo) => { export const updateMediaInfo = (arg: MediaInfo) => {
mediaInfo = { ...defaultInfo, ...arg }; mediaInfo.title = propOrDefault(arg.title);
mediaInfo.url = toUniversalUrl(mediaInfo.url); mediaInfo.artists = propOrDefault(arg.artists);
mediaInfo.album = propOrDefault(arg.album);
mediaInfo.icon = propOrDefault(arg.icon);
mediaInfo.url = propOrDefault(arg.url);
mediaInfo.status = propOrDefault(arg.status);
mediaInfo.current = propOrDefault(arg.current);
mediaInfo.duration = propOrDefault(arg.duration);
mediaInfo.image = propOrDefault(arg.image);
mediaInfo.favorite = arg.favorite;
}; };
/** /**
* Append the universal link syntax (?u) to any url * Return the property or a default value
* @param url url to append the universal link syntax to * @param {*} prop property to check
* @returns url with `?u` appended, or the original value of url if falsy * @param {*} defaultValue defaults to ""
*/ */
function toUniversalUrl(url: string) { function propOrDefault(prop: string, defaultValue = "") {
if (url) { return prop ? prop : defaultValue;
const queryParamsSet = url.indexOf("?");
return queryParamsSet > -1 ? `${url}&u` : `${url}?u`;
}
return url;
} }

View File

@ -11,23 +11,6 @@ const settingsMenuEntry = {
accelerator: "Control+=", accelerator: "Control+=",
}; };
const tidalMagazineEntry = {
label: "Magazine",
click() {
const magazineWindow = new BrowserWindow({
autoHideMenuBar: true,
webPreferences: {
sandbox: false,
plugins: true,
devTools: true, // I like tinkering, others might too
},
});
magazineWindow.loadURL("https://tidal.com/magazine/");
magazineWindow.show();
},
accelerator: "Control+M",
};
const quitMenuEntry = { const quitMenuEntry = {
label: "Quit", label: "Quit",
click() { click() {
@ -58,7 +41,6 @@ export const getMenu = function (mainWindow: BrowserWindow) {
{ role: "hideothers" }, { role: "hideothers" },
{ role: "unhide" }, { role: "unhide" },
{ type: "separator" }, { type: "separator" },
tidalMagazineEntry,
quitMenuEntry, quitMenuEntry,
], ],
}, },
@ -66,7 +48,7 @@ export const getMenu = function (mainWindow: BrowserWindow) {
: []), : []),
{ {
label: "File", label: "File",
submenu: [settingsMenuEntry, tidalMagazineEntry, isMac ? { role: "close" } : quitMenuEntry], submenu: [settingsMenuEntry, isMac ? { role: "close" } : quitMenuEntry],
}, },
{ {
label: "Edit", label: "Edit",
@ -119,7 +101,6 @@ export const getMenu = function (mainWindow: BrowserWindow) {
}, },
settingsMenuEntry, settingsMenuEntry,
toggleWindow, toggleWindow,
tidalMagazineEntry,
quitMenuEntry, quitMenuEntry,
]; ];

View File

@ -1,13 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const ObjectToDotNotation = (obj: any, prefix: string = "", target: any = {}) => {
Object.keys(obj).forEach((key: string) => {
if (typeof obj[key] === "object" && obj[key] !== null) {
ObjectToDotNotation(obj[key], prefix + key + ".", target);
} else {
const dotLocation = prefix + key;
target[dotLocation] = obj[key];
return target;
}
});
return target;
};

View File

@ -1,40 +1,17 @@
import Store from "electron-store"; import Store from "electron-store";
import { BrowserWindow, shell } from "electron"; import { BrowserWindow } from "electron";
import path from "path"; import path from "path";
import { settings } from "../constants/settings"; import { settings } from "../constants/settings";
let settingsWindow: BrowserWindow; let settingsWindow: BrowserWindow;
/**
* Build a migration step for several settings.
* All settings will be checked and set to the default if non-existent.
* @param version
* @param migrationStore
* @param options
*/
const buildMigration = (
version: string,
migrationStore: { get: (str: string) => string; set: (str: string, val: unknown) => void },
options: Array<{ key: string; value: unknown }>
) => {
console.log(`running migrations for ${version}`);
options.forEach(({ key, value }) => {
const valueToSet = migrationStore.get(key) ?? value;
console.log(` - setting ${key} to ${value}`);
migrationStore.set(key, valueToSet);
});
};
export const settingsStore = new Store({ export const settingsStore = new Store({
defaults: { defaults: {
adBlock: false, adBlock: false,
advanced: {
tidalUrl: "https://listen.tidal.com",
},
api: true, api: true,
apiSettings: { apiSettings: {
port: 47836, port: 47836,
hostname: "127.0.0.1",
}, },
customCSS: [], customCSS: [],
disableBackgroundThrottle: true, disableBackgroundThrottle: true,
@ -42,13 +19,9 @@ export const settingsStore = new Store({
enableCustomHotkeys: false, enableCustomHotkeys: false,
enableDiscord: false, enableDiscord: false,
discord: { discord: {
showSong: true,
showIdle: true,
idleText: "Browsing Tidal",
usingText: "Playing media on TIDAL",
includeTimestamps: true,
detailsPrefix: "Listening to ", detailsPrefix: "Listening to ",
buttonText: "Play on Tidal", buttonText: "Play on Tidal",
includeTimestamps: true,
}, },
ListenBrainz: { ListenBrainz: {
enabled: false, enabled: false,
@ -69,7 +42,6 @@ export const settingsStore = new Store({
singleInstance: true, singleInstance: true,
skipArtists: false, skipArtists: false,
skippedArtists: [""], skippedArtists: [""],
staticWindowTitle: false,
theme: "none", theme: "none",
trayIcon: true, trayIcon: true,
updateFrequency: 500, updateFrequency: 500,
@ -97,29 +69,6 @@ export const settingsStore = new Store({
migrationStore.get(settings.discord.includeTimestamps) ?? true migrationStore.get(settings.discord.includeTimestamps) ?? true
); );
}, },
"5.9.0": (migrationStore) => {
buildMigration("5.9.0", migrationStore, [
{ key: settings.discord.showSong, value: "true" },
{ key: settings.discord.idleText, value: "Browsing Tidal" },
{
key: settings.discord.usingText,
value: "Playing media on TIDAL",
},
]);
},
"5.14.0": (migrationStore) => {
buildMigration("5.14.0", migrationStore, [
{ key: settings.apiSettings.hostname, value: "127.0.0.1" },
]);
},
"5.15.0": (migrationStore) => {
buildMigration("5.15.0", migrationStore, [
{ key: settings.advanced.tidalUrl, value: "https://listen.tidal.com" },
]);
},
"5.16.0": (migrationStore) => {
buildMigration("5.16.0", migrationStore, [{ key: settings.discord.showIdle, value: "true" }]);
},
}, },
}); });
@ -153,18 +102,10 @@ export const createSettingsWindow = function () {
settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`); settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`);
settingsWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: "deny" };
});
settingsModule.settingsWindow = settingsWindow; settingsModule.settingsWindow = settingsWindow;
}; };
export const showSettingsWindow = function (tab = "general") { export const showSettingsWindow = function (tab = "general") {
if (!settingsWindow) {
console.log("Settings window is not initialized. Attempting to create it.");
createSettingsWindow();
}
settingsWindow.webContents.send("goToTab", tab); settingsWindow.webContents.send("goToTab", tab);
// refresh data just before showing the window // refresh data just before showing the window
@ -178,25 +119,3 @@ export const hideSettingsWindow = function () {
export const closeSettingsWindow = function () { export const closeSettingsWindow = function () {
settingsWindow = null; settingsWindow = null;
}; };
/**
* add artists to the list of skipped artists
* @param artists list of artists to append
*/
export const addSkippedArtists = (artists: string[]) => {
const { skippedArtists } = settings;
const previousStoreValue = settingsStore.get<string, string[]>(skippedArtists);
settingsStore.set(skippedArtists, Array.from(new Set([...previousStoreValue, ...artists])));
};
/**
* Remove artists from the list of skipped artists
* @param artists list of artists to remove
*/
export const removeSkippedArtists = (artists: string[]) => {
const { skippedArtists } = settings;
const previousStoreValue = settingsStore.get<string, string[]>(skippedArtists);
const filteredArtists = previousStoreValue.filter((value) => ![...artists].includes(value));
settingsStore.set(skippedArtists, filteredArtists);
};

View File

@ -8,7 +8,6 @@
"sourceMap": true, "sourceMap": true,
"allowJs": true, "allowJs": true,
"outDir": "ts-dist", "outDir": "ts-dist",
"resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {