Compare commits

..

2 Commits

Author SHA1 Message Date
mjokfox
3d5ea58bda Merge 1b4c1e1cd8 into 3740ce5a12 2024-05-15 07:19:17 +02:00
mjokfox
1b4c1e1cd8 Add the cors module and use it with express api 2024-05-14 12:53:39 +02:00
42 changed files with 1539 additions and 3429 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,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
@@ -31,7 +30,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
@@ -41,6 +40,6 @@ 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

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
@@ -35,7 +34,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
@@ -49,7 +48,7 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 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

View File

@@ -1,7 +1,13 @@
{ {
"plugins": ["stylelint-prettier"], "plugins": [
"extends": ["stylelint-config-standard-scss"], "stylelint-prettier"
"ignoreFiles": ["src/themes/**.scss"], ],
"extends": [
"stylelint-config-standard-scss"
],
"ignoreFiles": [
"src/themes/**.scss"
],
"rules": { "rules": {
"prettier/prettier": true, "prettier/prettier": true,
"scss/at-extend-no-missing-placeholder": null, "scss/at-extend-no-missing-placeholder": null,

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,63 +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.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] ## [5.12.0]
- Added Shuffle and Repeat state to API response - By [ThatGravyBoat](https://github.com/ThatGravyBoat) - Added Shuffle and Repeat state to API response - By [ThatGravyBoat](https://github.com/ThatGravyBoat)
@@ -113,12 +56,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.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))
- Updated dependencies to latest - Updated dependencies to latest
- added theme files to stylelint ignore - added theme files to stylelint ignore
- fixed other stylelint errors - fixed other stylelint errors
- Added functionality to favorite a song (fixes [#323](https://github.com/Mastermindzh/tidal-hifi/issues/323)) - Added functionality to favorite a song (fixes [#323](https://github.com/Mastermindzh/tidal-hifi/issues/323))
- Added a hotkey to favorite ("Add to collection") songs: Control+a - Added a hotkey to favorite ("Add to collection") songs: Control+a
- Added the "favorite" field in the `mediaInfo` and the API `/current` endpoint - Added the "favorite" field in the `mediaInfo` and the API `/current` endpoint
- Added an endpoint to toggle favoriting a song: `http://localhost:47836/favorite/toggle` - Added an endpoint to toggle favoriting a song: `http://localhost:47836/favorite/toggle`

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,7 +130,7 @@ 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-hifi`
- `npm install` - `npm install`
- `npm run watch` to watch for auto-reload of Typescript/SCSS changes. - `npm run watch` to watch for auto-reload of Typescript/SCSS changes.
@@ -152,13 +152,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

@@ -8,3 +8,4 @@ Only the very latest 😄.
If you find a vulnerability just add it as an issue. If you find a vulnerability just add it as an issue.
If there's an especially bad vulnerability that you don't want to make public just send me a private message (email, discord, wherever). If there's an especially bad vulnerability that you don't want to make public just send me a private message (email, discord, wherever).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

2318
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "tidal-hifi", "name": "tidal-hifi",
"version": "5.17.0", "version": "5.12.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": {
@@ -8,10 +8,9 @@
"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",
@@ -23,7 +22,6 @@
"build-mac": "npm run builder -- -c ./build/electron-builder.yml -m", "build-mac": "npm run builder -- -c ./build/electron-builder.yml -m",
"build-base": "npm run builder -- -c ./build/electron-builder.base.yml", "build-base": "npm run builder -- -c ./build/electron-builder.base.yml",
"prebuilder": "npm run compile", "prebuilder": "npm run compile",
"prettier": "prettier . --write",
"builder": "electron-builder --publish=never", "builder": "electron-builder --publish=never",
"sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css && sass --no-source-map src/themes:themes", "sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css && sass --no-source-map src/themes:themes",
"style-lint": "npx stylelint **/*.scss", "style-lint": "npx stylelint **/*.scss",
@@ -42,43 +40,39 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/remote": "^2.1.2", "@electron/remote": "^2.1.2",
"@types/swagger-jsdoc": "^6.0.4", "axios": "^1.6.8",
"@xhayper/discord-rpc": "^1.2.0",
"axios": "^1.7.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"discord-rpc": "^4.0.1",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"express": "^4.21.1", "express": "^4.19.2",
"hotkeys-js": "^3.13.7", "hotkeys-js": "^3.13.7",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"request": "^2.88.2", "request": "^2.88.2",
"sass": "^1.79.4", "sass": "^1.75.0"
"swagger-ui-express": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0", "@mastermindzh/prettier-config": "^1.0.0",
"@types/cors": "^2.8.17", "@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": "git+https://github.com/castlabs/electron-releases#v31.1.0+wvcus", "electron": "git+https://github.com/castlabs/electron-releases#v28.1.1+wvcus",
"electron-builder": "~24.9.4", "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,9 +16,8 @@
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"]',
@@ -26,8 +25,7 @@
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"]',
playingFrom: '*[class^="playingFrom"] 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,7 +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", toggleShuffle: "toggleShuffle",

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",
@@ -30,7 +25,6 @@ export const settings = {
buttonText: "discord.buttonText", buttonText: "discord.buttonText",
includeTimestamps: "discord.includeTimestamps", includeTimestamps: "discord.includeTimestamps",
showSong: "discord.showSong", showSong: "discord.showSong",
showIdle: "discord.showIdle",
idleText: "discord.idleText", idleText: "discord.idleText",
usingText: "discord.usingText", usingText: "discord.usingText",
}, },
@@ -55,7 +49,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

@@ -3,110 +3,7 @@ import fs from "fs";
import { mediaInfo } from "../../../scripts/mediaInfo"; import { mediaInfo } from "../../../scripts/mediaInfo";
export const addCurrentInfo = (expressApp: Router) => { 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 })); 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); expressApp.get("/current/image", getCurrentImage);
}; };

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { BrowserWindow } from "electron"; import { BrowserWindow } from "electron";
import { Router } from "express"; import { Router } from "express";
import { globalEvents } from "../../../constants/globalEvents"; import { globalEvents } from "../../../constants/globalEvents";
@@ -11,148 +12,19 @@ export const addPlaybackControl = (expressApp: Router, mainWindow: BrowserWindow
const windowEvent = handleWindowEvent(mainWindow); const windowEvent = handleWindowEvent(mainWindow);
const createRoute = (route: string) => `/player${route}`; 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) => { const createPlayerAction = (route: string, action: string) => {
expressApp.post(createRoute(route), (req, res) => windowEvent(res, action)); expressApp.post(createRoute(route), (req, res) => windowEvent(res, action));
}; };
if (settingsStore.get(settings.playBackControl)) { 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); 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); 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); 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); 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); 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); 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); 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) => { expressApp.post(createRoute("/playpause"), (req, res) => {
if (mediaInfo.status === MediaStatus.playing) { if (mediaInfo.status === MediaStatus.playing) {
windowEvent(res, globalEvents.pause); windowEvent(res, globalEvents.pause);

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,36 +1,27 @@
import cors from "cors";
import { BrowserWindow, dialog } from "electron"; import { BrowserWindow, dialog } from "electron";
import express from "express"; import express from "express";
import swaggerUi from "swagger-ui-express"; import { settings } from "../../constants/settings";
import { settingsStore } from "../../scripts/settings"; import { settingsStore } from "../../scripts/settings";
import { settings } from "./../../constants/settings";
import { addCurrentInfo } from "./features/current"; import { addCurrentInfo } from "./features/current";
import { addPlaybackControl } from "./features/player"; import { addPlaybackControl } from "./features/player";
import { addSettingsAPI } from "./features/settings/settings";
import { addLegacyApi } from "./legacy"; import { addLegacyApi } from "./legacy";
import swaggerSpec from "./swagger.json"; import cors from "cors";
/** /**
* Function to enable TIDAL Hi-Fi's express api * Function to enable TIDAL Hi-Fi's express api
*/ */
export const startApi = (mainWindow: BrowserWindow) => { 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(); const expressApp = express();
expressApp.use(cors()); 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("/", (req, res) => res.send("Hello World!"));
expressApp.get("/swagger.json", (req, res) => res.json(swaggerSpec));
// add features // add features
addLegacyApi(expressApp, mainWindow); addLegacyApi(expressApp, mainWindow);
addPlaybackControl(expressApp, mainWindow); addPlaybackControl(expressApp, mainWindow);
addCurrentInfo(expressApp); addCurrentInfo(expressApp);
addSettingsAPI(expressApp, mainWindow);
const expressInstance = expressApp.listen(port, hostname); 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 }) { expressInstance.on("error", function (e: { code: string }) {
let message = e.code; let message = e.code;
if (e.code === "EADDRINUSE") { if (e.code === "EADDRINUSE") {

View File

@@ -13,132 +13,19 @@ import { getCurrentImage } from "./features/current";
* @param mainWindow * @param mainWindow
*/ */
export const addLegacyApi = (expressApp: Router, mainWindow: BrowserWindow) => { 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); expressApp.get("/image", getCurrentImage);
if (settingsStore.get(settings.playBackControl)) { if (settingsStore.get(settings.playBackControl)) {
addLegacyControls(); addLegacyControls();
} }
function 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)); 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) => expressApp.post("/favorite/toggle", (req, res) =>
handleGlobalEvent(res, globalEvents.toggleFavorite) 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)); 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)); 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)); 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) => { expressApp.get("/playpause", (req, res) => {
if (mediaInfo.status === MediaStatus.playing) { if (mediaInfo.status === MediaStatus.playing) {
handleGlobalEvent(res, globalEvents.pause); handleGlobalEvent(res, globalEvents.pause);

View File

@@ -1,535 +0,0 @@
{
"openapi": "3.1.0",
"info": {
"title": "TIDAL Hi-Fi API",
"version": "5.17.0",
"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

@@ -10,7 +10,7 @@ import {
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";
@@ -24,6 +24,7 @@ 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(); initialize();
@@ -39,9 +40,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
* *
@@ -250,8 +248,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,5 +1,5 @@
import { MediaPlayerInfo } from "./mediaPlayerInfo";
import { MediaStatus } from "./mediaStatus"; import { MediaStatus } from "./mediaStatus";
import { MediaPlayerInfo } from "./mediaPlayerInfo";
export interface MediaInfo { export interface MediaInfo {
title: string; title: string;
@@ -8,12 +8,11 @@ export interface MediaInfo {
icon: string; icon: string;
status: MediaStatus; status: MediaStatus;
url: string; url: string;
playingFrom: string;
current: string; current: string;
currentInSeconds?: number; currentInSeconds?: number;
duration: string; duration: string;
durationInSeconds?: number; durationInSeconds?: number;
image: string; image: string;
favorite: boolean; favorite: boolean;
player?: MediaPlayerInfo; player: MediaPlayerInfo;
} }

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

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

View File

@@ -24,19 +24,17 @@ const switchesWithSettings = {
switch: "discord_show_song", switch: "discord_show_song",
classToHide: "discord_show_song_options", classToHide: "discord_show_song_options",
settingsKey: settings.discord.showSong, 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 +44,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,
@@ -59,7 +56,6 @@ let adBlock: HTMLInputElement,
discord_include_timestamps: HTMLInputElement, discord_include_timestamps: HTMLInputElement,
discord_button_text: HTMLInputElement, discord_button_text: HTMLInputElement,
discord_show_song: HTMLInputElement, discord_show_song: HTMLInputElement,
discord_show_idle: HTMLInputElement,
discord_idle_text: HTMLInputElement, discord_idle_text: HTMLInputElement,
discord_using_text: HTMLInputElement; discord_using_text: HTMLInputElement;
@@ -124,7 +120,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 +127,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 +135,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);
@@ -154,7 +147,6 @@ function refreshSettings() {
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_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_idle_text.value = settingsStore.get(settings.discord.idleText);
discord_using_text.value = settingsStore.get(settings.discord.usingText); discord_using_text.value = settingsStore.get(settings.discord.usingText);
@@ -244,7 +236,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 +243,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 +253,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");
@@ -274,14 +263,12 @@ window.addEventListener("DOMContentLoaded", () => {
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_song = get("discord_show_song");
discord_show_idle = get("discord_show_idle");
discord_using_text = get("discord_using_text"); discord_using_text = get("discord_using_text");
discord_idle_text = get("discord_idle_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 +276,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 +284,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 +299,7 @@ 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( addInputListener(discord_show_song, settings.discord.showSong, switchesWithSettings.discord_show_song);
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_idle_text, settings.discord.idleText);
addInputListener(discord_using_text, settings.discord.usingText); addInputListener(discord_using_text, settings.discord.usingText);
}); });

View File

@@ -1,34 +1,25 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Tidal Hi-Fi settings</title> <title>Tidal Hi-Fi settings</title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="./settings.css" /> <link rel="stylesheet" href="./settings.css" />
<link <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
</head> </head>
<body class="settings-window"> <body class="settings-window">
<div class="settings-window__wrapper"> <div class="settings-window__wrapper">
<div class="settings-window__drag-area"></div> <div class="settings-window__drag-area"></div>
<a id="close" class="settings-window__close-button" title="Close settings"> <a id="close" class="settings-window__close-button" title="Close settings">
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 348.333 348.334" class="settings-window__svg-icon">
xmlns="http://www.w3.org/2000/svg" <path fill="white" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85
viewBox="0 0 348.333 348.334"
class="settings-window__svg-icon"
>
<path
fill="white"
d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85
c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563 c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563
c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85 c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85
l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554 l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554
L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" />
/>
</svg> </svg>
</a> </a>
@@ -58,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" />
@@ -75,13 +66,7 @@
<span class="switch__slider"></span> <span class="switch__slider"></span>
</label> </label>
</div> </div>
<textarea <textarea id="skippedArtists" class="textarea" cols="40" rows="5" spellcheck="false"></textarea>
id="skippedArtists"
class="textarea"
cols="40"
rows="5"
spellcheck="false"
></textarea>
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Block ads</h4> <h4>Block ads</h4>
@@ -121,19 +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>
@@ -149,9 +121,7 @@
<h4>Hotkeys</h4> <h4>Hotkeys</h4>
<p> <p>
Enable extra hotkeys to achieve feature parity with the Enable extra hotkeys to achieve feature parity with the
<a class="external-link" data-url="https://defkey.com/tidal-desktop-shortcuts" <a class="external-link" data-url="https://defkey.com/tidal-desktop-shortcuts">desktop apps</a>.
>desktop apps</a
>.
</p> </p>
</div> </div>
<label class="switch"> <label class="switch">
@@ -177,8 +147,8 @@
<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 TIDAL Hi-Fi has a built-in web API to allow users to get current song information.
information. You can optionally enable playback control as well. You can optionally enable playback control as well.
</p> </p>
</div> </div>
<div class="group__option"> <div class="group__option">
@@ -197,17 +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>
@@ -257,49 +216,27 @@
</label> </label>
</div> </div>
<div id="discord_options"> <div id="discord_options">
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Show Idle Text</h4>
<p>Should the idle text be shown when idle?</p>
</div>
<label class="switch">
<input id="discord_show_idle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</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>Idle Text</h4>
<p>The text displayed on Discord's rich presence while idling in the app.</p> <p>The text displayed on Discord's rich presence while idling in the app.</p>
<input <input id="discord_idle_text" type="text" class="text-input" name="discord_idle_text" />
id="discord_idle_text"
type="text"
class="text-input"
name="discord_idle_text"
/>
</div> </div>
</div> </div>
<div class="group__option" class="hidden"> <div class="group__option" class="hidden">
<div class="group__description"> <div class="group__description">
<h4>Using Tidal Text</h4> <h4>Using Tidal Text</h4>
<p> <p>The text displayed on Discord's rich presence while "showSong" is turned off</p>
The text displayed on Discord's rich presence while "showSong" is turned off <input id="discord_using_text" type="text" class="text-input" name="discord_using_text" />
</p>
<input
id="discord_using_text"
type="text"
class="text-input"
name="discord_using_text"
/>
</div> </div>
</div> </div>
<div class="group__option" class="hidden"> <div class="group__option" class="hidden">
<div class="group__description"> <div class="group__description">
<h4>Show media</h4> <h4>Show song</h4>
<p>Show the current media in the Discord client</p> <p>Show the current song in the Discord client</p>
</div> </div>
<label class="switch"> <label class="switch">
<input id="discord_show_song" type="checkbox" /> <input id="discord_show_song" type="checkbox" />
@@ -308,6 +245,7 @@
</div> </div>
<div id="discord_show_song_options" class="hidden"> <div id="discord_show_song_options" class="hidden">
<div class="group__option" class="hidden"> <div class="group__option" class="hidden">
<div class="group__description"> <div class="group__description">
<h4>Include timestamps</h4> <h4>Include timestamps</h4>
@@ -323,28 +261,19 @@
<div class="group__description"> <div class="group__description">
<h4>Details prefix</h4> <h4>Details prefix</h4>
<p>Prefix for the "details" field of Discord's rich presence.</p> <p>Prefix for the "details" field of Discord's rich presence.</p>
<input <input id="discord_details_prefix" type="text" class="text-input" name="discord_details_prefix" />
id="discord_details_prefix"
type="text"
class="text-input"
name="discord_details_prefix"
/>
</div> </div>
</div> </div>
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Button text</h4> <h4>Button text</h4>
<p>Text to display on the button below the media information.</p> <p>Text to display on the button below the song information.</p>
<input <input id="discord_button_text" type="text" class="text-input" name="discord_button_text" />
id="discord_button_text"
type="text"
class="text-input"
name="discord_button_text"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="group"> <div class="group">
@@ -363,43 +292,23 @@
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>ListenBrainz API Url</h4> <h4>ListenBrainz API Url</h4>
<p> <p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p>
There are multiple instances for ListenBrainz you can set the corresponding <input id="ListenBrainzAPI" type="text" class="text-input" name="ListenBrainzAPI" />
API url below.
</p>
<input
id="ListenBrainzAPI"
type="text"
class="text-input"
name="ListenBrainzAPI"
/>
</div> </div>
</div> </div>
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>ListenBrainz User Token</h4> <h4>ListenBrainz User Token</h4>
<p>Provide the user token you can get from the settings page.</p> <p>Provide the user token you can get from the settings page.</p>
<input <input id="ListenBrainzToken" type="text" class="text-input" name="ListenBrainzToken" />
id="ListenBrainzToken"
type="text"
class="text-input"
name="ListenBrainzToken"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="group__description"> <div class="group__description">
<h4>ScrobbleDelay</h4> <h4>ScrobbleDelay</h4>
<p> <p>The delay (in ms) to send a song to ListenBrainz. Prevents spamming the API when you fast forward
The delay (in ms) to send a listen to ListenBrainz. Prevents spamming the API when immediately</p>
you fast forward immediately <input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" />
</p>
<input
id="listenbrainz_delay"
type="number"
class="text-input"
name="listenbrainz_delay"
/>
</div> </div>
</div> </div>
</section> </section>
@@ -411,32 +320,12 @@
<div class="group__description"> <div class="group__description">
<h4>Update frequency</h4> <h4>Update frequency</h4>
<p> <p>
The amount of time, in milliseconds, that TIDAL Hi-Fi will refresh its playback The amount of time, in milliseconds, that TIDAL Hi-Fi will refresh its playback info by scraping the
info by scraping the website. The default of 500 seems to work in more cases but website.
if you are fine with a bit more resource usage you can decrease it as well. The default of 500 seems to work in more cases but if you are fine with a bit more resource usage you
can decrease it as well.
</p> </p>
<input <input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
id="updateFrequency"
type="number"
class="text-input"
name="updateFrequency"
/>
</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> </div>
@@ -481,8 +370,7 @@
<div class="group__description"> <div class="group__description">
<h4>Wayland support</h4> <h4>Wayland support</h4>
<p> <p>
Adds a couple of Electron flags to help TIDAL Hi-Fi run smoothly on the Wayland Adds a couple of Electron flags to help TIDAL Hi-Fi run smoothly on the Wayland window system.
window system.
</p> </p>
</div> </div>
<label class="switch"> <label class="switch">
@@ -500,19 +388,12 @@
<div class="group__description"> <div class="group__description">
<h4>Custom CSS</h4> <h4>Custom CSS</h4>
<p> <p>
The css that you put in here will be injected into a style tag in the head of The css that you put in here will be injected into a style tag in the head of the document.
the document.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<textarea <textarea id="customCSS" class="textarea" cols="40" rows="8" spellcheck="false"></textarea>
id="customCSS"
class="textarea"
cols="40"
rows="8"
spellcheck="false"
></textarea>
<div class="group"> <div class="group">
<p class="group__title">Theme files</p> <p class="group__title">Theme files</p>
@@ -522,7 +403,9 @@
<p> <p>
Select a theme below or "Tidal - Default" to return to the original Tidal look. Select a theme below or "Tidal - Default" to return to the original Tidal look.
</p> </p>
<select class="select-input" id="themesList" name="themesList"></select> <select class="select-input" id="themesList" name="themesList">
</select>
</div> </div>
</div> </div>
@@ -530,20 +413,14 @@
<div class="group__description"> <div class="group__description">
<h4>Upload new themes</h4> <h4>Upload new themes</h4>
<p> <p>
Click the button and select the css files to import. They will be added to the Click the button and select the css files to import. They will be added to the theme list
theme list automatically. automatically.
</p> </p>
<div class="file-drop-area"> <div class="file-drop-area">
<div> <div>
<span class="file-btn">Choose files</span> <span class="file-btn">Choose files</span>
<span id="file-message" class="file-msg">or drag and drop files here</span> <span id="file-message" class="file-msg">or drag and drop files here</span>
<input <input id="theme-files" class="file-input" type="file" accept=".css" multiple>
id="theme-files"
class="file-input"
type="file"
accept=".css"
multiple
/>
</div> </div>
</div> </div>
</div> </div>
@@ -555,35 +432,17 @@
<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 <a target="_blank" rel="noopener"
target="_blank" href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.12.0">5.12.0</a>
rel="noopener"
href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.17.0"
>5.17.0</a
>
</div> </div>
<div class="about-section__links"> <div class="about-section__links">
<a <a target="_blank" rel="noopener" href="https://github.com/mastermindzh/tidal-hifi/"
target="_blank" class="about-section__button">Github
rel="noopener" <i class="fa fa-external-link"></i></a>
href="https://github.com/mastermindzh/tidal-hifi/" <a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/issues"
class="about-section__button" class="about-section__button">Report an issue <i class="fa fa-external-link"></i></a>
>Github <i class="fa fa-external-link"></i <a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors"
></a> class="about-section__button">Contributors <i class="fa fa-external-link"></i></a>
<a
target="_blank"
rel="noopener"
href="https://github.com/Mastermindzh/tidal-hifi/issues"
class="about-section__button"
>Report an issue <i class="fa fa-external-link"></i
></a>
<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>
</div> </div>
</section> </section>
@@ -596,4 +455,5 @@
</main> </main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -10,19 +10,19 @@ 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 { 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";
import { RepeatState } from "./models/repeatState";
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;
@@ -32,7 +32,7 @@ let scrobbleWaitingForDelay = false;
let currentlyPlaying = MediaStatus.paused; let currentlyPlaying = MediaStatus.paused;
let currentRepeatState: RepeatState = RepeatState.off; let currentRepeatState: RepeatState = RepeatState.off;
let currentShuffleState = false; let currentShuffleState = false;
let currentMediaInfo: MediaInfo; let currentMediaInfo: Options;
let currentNotification: Electron.Notification; let currentNotification: Electron.Notification;
const elements = { const elements = {
@@ -48,9 +48,8 @@ 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"]',
@@ -58,9 +57,7 @@ const elements = {
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 +72,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 +88,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 +133,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 +187,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 +212,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 +246,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();
}); });
@@ -388,37 +380,24 @@ 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; currentMediaInfo = options;
ipcRenderer.send(globalEvents.updateInfo, mediaInfo); ipcRenderer.send(globalEvents.updateInfo, options);
updateMpris(mediaInfo); if (settingsStore.get(settings.notifications) && notify) {
updateListenBrainz(mediaInfo); if (currentNotification) currentNotification.close();
if (notify) {
sendNotification(mediaInfo);
}
}
}
/**
* 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({ currentNotification = new Notification({
title: mediaInfo.title, title: options.title,
body: mediaInfo.artists, body: options.artists,
icon: mediaInfo.icon, icon: options.icon,
}); });
currentNotification.show(); currentNotification.show();
} }
updateMpris(options);
updateListenBrainz(options);
}
} }
function addMPRIS() { function addMPRIS() {
@@ -475,33 +454,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,
"mpris:artUrl": mediaInfo.image, "mpris:artUrl": options.image,
"mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000, "mpris:length": convertDuration(options.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;
@@ -509,10 +487,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;
}, },
@@ -550,7 +528,6 @@ setInterval(function () {
const artistsArray = elements.getArtistsArray(); const artistsArray = elements.getArtistsArray();
const artistsString = elements.getArtistsString(artistsArray); const artistsString = elements.getArtistsString(artistsArray);
const songDashArtistTitle = `${title} - ${artistsString}`; const songDashArtistTitle = `${title} - ${artistsString}`;
const staticTitle = "TIDAL Hi-Fi";
const titleOrArtistsChanged = currentSong !== songDashArtistTitle; const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
const current = elements.getText("current"); const current = elements.getText("current");
const currentStatus = getCurrentlyPlayingStatus(); const currentStatus = getCurrentlyPlayingStatus();
@@ -570,19 +547,20 @@ setInterval(function () {
if (repeatStateChanged) currentRepeatState = repeatState; if (repeatStateChanged) currentRepeatState = repeatState;
skipArtistsIfFoundInSkippedArtistsList(artistsArray); skipArtistsIfFoundInSkippedArtistsList(artistsArray);
const album = elements.getAlbumName(); const album = elements.getAlbumName();
const duration = elements.getText("duration"); const duration = elements.getText("duration");
const options: MediaInfo = { const options = {
title, title,
artists: artistsString, artists: artistsString,
album: album, album: album,
playingFrom: elements.getText("playing_from"),
status: currentStatus, status: currentStatus,
url: getTrackURL(), url: getTrackURL(),
current, current,
currentInSeconds: convertDurationToSeconds(current), currentInSeconds: convertDurationToSeconds(current),
duration, duration,
durationInSeconds: convertDurationToSeconds(duration), durationInSeconds: convertDurationToSeconds(duration),
"app-name": appName,
image: "", image: "",
icon: "", icon: "",
favorite: elements.isFavorite(), favorite: elements.isFavorite(),
@@ -595,9 +573,7 @@ setInterval(function () {
}; };
// update title, url and play info with new info // update title, url and play info with new info
settingsStore.get(settings.staticWindowTitle) setTitle(songDashArtistTitle);
? setTitle(staticTitle)
: setTitle(songDashArtistTitle);
getTrackURL(); getTrackURL();
currentSong = songDashArtistTitle; currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus; currentPlayStatus = currentStatus;

View File

@@ -1,4 +1,4 @@
import { Client, SetActivity } from "@xhayper/discord-rpc"; import { Client, Presence } 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";
@@ -12,13 +12,9 @@ const clientId = "833617820704440341";
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 (rpc) {
updateActivity(); rpc.setActivity(getActivity());
} }
}; };
@@ -26,20 +22,10 @@ const defaultPresence = {
largeImageKey: "tidal-hifi-icon", largeImageKey: "tidal-hifi-icon",
largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`, largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`,
instance: false, instance: false,
type: ACTIVITY_LISTENING,
}; };
const updateActivity = () => { const getActivity = (): Presence => {
const showIdle = settingsStore.get<string, boolean>(settings.discord.showIdle) ?? true; const presence: Presence = { ...defaultPresence };
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) { if (mediaInfo.status === MediaStatus.paused) {
presence.details = presence.details =
@@ -55,7 +41,6 @@ const getActivity = (): SetActivity => {
settingsStore.get<string, string>(settings.discord.usingText) ?? "Playing media on TIDAL"; settingsStore.get<string, string>(settings.discord.usingText) ?? "Playing media on TIDAL";
} }
} }
return presence; return presence;
function getFromStore() { function getFromStore() {
@@ -69,33 +54,18 @@ const getActivity = (): SetActivity => {
return { includeTimestamps, detailsPrefix, buttonText }; return { includeTimestamps, detailsPrefix, buttonText };
} }
/**
* Pad a string using spaces to at least 2 characters
* @param input string to pad with 2 characters
* @returns
*/
function pad(input: string): string {
return input.padEnd(2, " ");
}
function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) { 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}`; presence.details = `${detailsPrefix}${mediaInfo.title}`;
presence.state = artists ? artists : "unknown artist(s)"; presence.state = mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)";
presence.largeImageKey = mediaInfo.image; presence.largeImageKey = mediaInfo.image;
if (album) { if (mediaInfo.album) {
presence.largeImageText = album; presence.largeImageText = mediaInfo.album;
} }
presence.buttons = [{ label: buttonText, url: mediaInfo.url }]; presence.buttons = [{ label: buttonText, url: mediaInfo.url }];
} else { } else {
presence.details = `Watching ${title}`; presence.details = `Watching ${mediaInfo.title}`;
presence.state = artists; presence.state = mediaInfo.artists;
} }
} }
@@ -104,31 +74,10 @@ const getActivity = (): SetActivity => {
const currentSeconds = convertDurationToSeconds(mediaInfo.current); const currentSeconds = convertDurationToSeconds(mediaInfo.current);
const durationSeconds = convertDurationToSeconds(mediaInfo.duration); const durationSeconds = convertDurationToSeconds(mediaInfo.duration);
const date = new Date(); const date = new Date();
const now = Math.floor(date.getTime() / 1000); const now = (date.getTime() / 1000) | 0;
presence.startTimestamp = now - currentSeconds; const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
presence.endTimestamp = presence.startTimestamp + durationSeconds; presence.startTimestamp = now;
} presence.endTimestamp = remaining;
}
};
/**
* Try to login to RPC and retry if it errors
* @param retryCount Max retry count
*/
const connectWithRetry = async (retryCount = 0) => {
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");
} }
} }
}; };
@@ -137,8 +86,18 @@ const connectWithRetry = async (retryCount = 0) => {
* 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(getActivity());
});
ipcMain.on(globalEvents.updateInfo, observer);
},
() => {
Logger.log("Can't connect to Discord, is it running?");
}
);
}; };
/** /**
@@ -146,7 +105,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);

View File

@@ -2,13 +2,12 @@ import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus"; import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState"; 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, currentInSeconds: 0,
@@ -18,19 +17,40 @@ const defaultInfo: MediaInfo = {
favorite: false, favorite: false,
player: { player: {
status: MediaStatus.paused, status: MediaStatus.paused as string,
shuffle: false, shuffle: false,
repeat: RepeatState.off, repeat: RepeatState.off as string,
}, }
}; };
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 = toUniversalUrl(propOrDefault(arg.url));
mediaInfo.status = propOrDefault(arg.status);
mediaInfo.current = propOrDefault(arg.current);
mediaInfo.currentInSeconds = arg.currentInSeconds ?? 0;
mediaInfo.duration = propOrDefault(arg.duration);
mediaInfo.durationInSeconds = arg.durationInSeconds ?? 0;
mediaInfo.image = propOrDefault(arg.image);
mediaInfo.favorite = arg.favorite;
mediaInfo.player.status = propOrDefault(arg.player?.status);
mediaInfo.player.shuffle = arg.player.shuffle;
mediaInfo.player.repeat = propOrDefault(arg.player?.repeat);
}; };
/**
* Return the property or a default value
* @param {*} prop property to check
* @param {*} defaultValue defaults to ""
*/
function propOrDefault(prop: string, defaultValue = "") {
return prop || defaultValue;
}
/** /**
* Append the universal link syntax (?u) to any url * Append the universal link syntax (?u) to any url
* @param url url to append the universal link syntax to * @param url url to append the universal link syntax to

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

@@ -28,13 +28,9 @@ const buildMigration = (
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,
@@ -43,7 +39,6 @@ export const settingsStore = new Store({
enableDiscord: false, enableDiscord: false,
discord: { discord: {
showSong: true, showSong: true,
showIdle: true,
idleText: "Browsing Tidal", idleText: "Browsing Tidal",
usingText: "Playing media on TIDAL", usingText: "Playing media on TIDAL",
includeTimestamps: true, includeTimestamps: true,
@@ -69,7 +64,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,
@@ -107,19 +101,6 @@ export const settingsStore = new Store({
}, },
]); ]);
}, },
"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" }]);
},
}, },
}); });
@@ -178,25 +159,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": {