Compare commits

..

2 Commits

Author SHA1 Message Date
4a1152175c Merge pull request #678 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.42.0
2025-09-03 09:30:25 +02:00
Renovate Bot
44c8c01b8b chore(deps): update typescript-eslint monorepo to 8.42.0 2025-09-02 20:46:38 +02:00
24 changed files with 653 additions and 957 deletions

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,

36
.vscode/launch.json vendored
View File

@@ -1,36 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
// we can use this to debug the main process
"name": "Electron: Main",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/electron",
"args": [
"${workspaceFolder}/ts-dist/main.js",
"--no-sandbox",
"--disable-gpu",
"--disable-software-rasterizer",
"--in-process-gpu",
"--inspect=0.0.0.0:5858",
"--remote-debugging-port=8315"
],
"protocol": "inspector",
"env": {
"ELECTRON_DISABLE_SECURITY_WARNINGS": "false"
},
"outputCapture": "std"
},
// first run, then connect this to make sure we debug the UI
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 8315,
"webRoot": "${workspaceFolder}",
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"]
}
]
}

View File

@@ -57,7 +57,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added all missing swagger/openApi info with the help of [Times-Z](https://github.com/Times-Z) - Added all missing swagger/openApi info with the help of [Times-Z](https://github.com/Times-Z)
- Updated most dependency versions - Updated most dependency versions
- This includes Electron 31! - This includes Electron 31!
- Added a channel selector so we can now use Tidal's staging environment directly from the app - Added a channel selector so we can now use Tidal's staging environment directly from the app
@@ -149,12 +148,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`
@@ -175,10 +172,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added settings to customize the Discord rich presence information - Added settings to customize the Discord rich presence information
- Discord settings are now also collapsible like the ListenBrainz ones are - Discord settings are now also collapsible like the ListenBrainz ones are
- Restyled settings menu to include version number and useful links on the about page - Restyled settings menu to include version number and useful links on the about page
![The new about page](./docs/images/new-about.png) ![The new about page](./docs/images/new-about.png)
- The ListenBrainz integration has been extended with a configurable (5 seconds by default) delay in song reporting so that it doesn't spam the API when you are cycling through songs. - The ListenBrainz integration has been extended with a configurable (5 seconds by default) delay in song reporting so that it doesn't spam the API when you are cycling through songs.
- Custom CSS now also applies to settings window - Custom CSS now also applies to settings window
![Tokyo Night theme on settings window](./docs/images/customcss-menu.png) ![Tokyo Night theme on settings window](./docs/images/customcss-menu.png)
## [5.6.0] ## [5.6.0]

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).

271
package-lock.json generated
View File

@@ -1674,6 +1674,13 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/@parcel/watcher/node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1928,6 +1935,18 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/plist": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
"integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*",
"xmlbuilder": ">=11.0.1"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.18", "version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -2011,6 +2030,14 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/verror": {
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
"integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@types/yauzl": { "node_modules/@types/yauzl": {
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -2022,17 +2049,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
"integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", "integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/type-utils": "8.42.0",
"@typescript-eslint/utils": "8.41.0", "@typescript-eslint/utils": "8.42.0",
"@typescript-eslint/visitor-keys": "8.41.0", "@typescript-eslint/visitor-keys": "8.42.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -2046,7 +2073,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.41.0", "@typescript-eslint/parser": "^8.42.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
@@ -2062,16 +2089,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
"integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.41.0", "@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/visitor-keys": "8.41.0", "@typescript-eslint/visitor-keys": "8.42.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2087,14 +2114,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
"integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.41.0", "@typescript-eslint/tsconfig-utils": "^8.42.0",
"@typescript-eslint/types": "^8.41.0", "@typescript-eslint/types": "^8.42.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2109,14 +2136,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
"integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.41.0", "@typescript-eslint/types": "8.42.0",
"@typescript-eslint/visitor-keys": "8.41.0" "@typescript-eslint/visitor-keys": "8.42.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2127,9 +2154,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
"integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2144,15 +2171,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
"integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", "integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.41.0", "@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.41.0", "@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/utils": "8.41.0", "@typescript-eslint/utils": "8.42.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -2169,9 +2196,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
"integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2183,16 +2210,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
"integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.41.0", "@typescript-eslint/project-service": "8.42.0",
"@typescript-eslint/tsconfig-utils": "8.41.0", "@typescript-eslint/tsconfig-utils": "8.42.0",
"@typescript-eslint/types": "8.41.0", "@typescript-eslint/types": "8.42.0",
"@typescript-eslint/visitor-keys": "8.41.0", "@typescript-eslint/visitor-keys": "8.42.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -2212,16 +2239,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
"integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", "integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.41.0", "@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.41.0" "@typescript-eslint/typescript-estree": "8.42.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2236,13 +2263,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.41.0", "version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
"integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.41.0", "@typescript-eslint/types": "8.42.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -3428,6 +3455,24 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/cli-truncate": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -3787,6 +3832,17 @@
} }
} }
}, },
"node_modules/crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.1.0"
}
},
"node_modules/create-require": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -4186,6 +4242,33 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/dmg-license": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
"integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"@types/plist": "^3.0.1",
"@types/verror": "^1.10.3",
"ajv": "^6.10.0",
"crc": "^3.8.0",
"iconv-corefoundation": "^1.1.7",
"plist": "^3.0.4",
"smart-buffer": "^4.0.2",
"verror": "^1.10.0"
},
"bin": {
"dmg-license": "bin/dmg-license.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -5446,6 +5529,21 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6055,6 +6153,24 @@
"ms": "^2.0.0" "ms": "^2.0.0"
} }
}, },
"node_modules/iconv-corefoundation": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
"integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"cli-truncate": "^2.1.0",
"node-addon-api": "^1.6.3"
},
"engines": {
"node": "^8.11.2 || >=10"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -7420,9 +7536,10 @@
} }
}, },
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "7.1.1", "version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@@ -9221,6 +9338,22 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/smart-buffer": { "node_modules/smart-buffer": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -10616,6 +10749,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/verror/node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/wcwidth": { "node_modules/wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -23,7 +23,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",

View File

@@ -1,6 +0,0 @@
export type DomControllerOptions = {
/**
* Interval that tidal-hifi scrapes the dom for information
*/
refreshInterval: number;
};

View File

@@ -1,354 +0,0 @@
import { convertDurationToSeconds } from "../../features/time/parse";
import { MediaInfo } from "../../models/mediaInfo";
import { MediaStatus } from "../../models/mediaStatus";
import { RepeatState } from "../../models/repeatState";
import { TidalController } from "../TidalController";
import { DomControllerOptions } from "./DomControllerOptions";
export class DomTidalController implements TidalController<DomControllerOptions> {
private updateSubscriber: (state: Partial<MediaInfo>) => void;
private currentlyPlaying = MediaStatus.paused;
private currentRepeatState: RepeatState = RepeatState.off;
private currentShuffleState = false;
private readonly elements = {
play: '*[data-test="play"]',
pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]',
settings: '*[data-test^="sidebar-menu-button"]',
openSettings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[class^=_playbackControlsContainer] *[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '*[class^="playingFrom"] span:nth-child(2)',
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']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
favorite: '*[data-test="footer-favorite-button"]',
/**
* Get an element from the dom
* @param {*} key key in elements object to fetch
*/
get: function (key: string) {
return globalThis.document.querySelector(this[key.toLowerCase()]) ?? "";
},
/**
* Get the icon of the current media
*/
getSongIcon: function () {
const figure = this.get("media");
if (figure) {
const mediaElement = figure.querySelector(this["image"]);
if (mediaElement) {
return mediaElement.src.replace("80x80", "640x640");
}
}
return "";
},
/**
* returns an array of all artists in the current media
* @returns {Array} artists
*/
getArtistsArray: function () {
const footer = this.get("footer");
if (footer) {
const artists = footer.querySelectorAll(this.artists);
if (artists)
return Array.from(artists).map((artist) => (artist as HTMLElement).textContent);
}
return [];
},
/**
* unify the artists array into a string separated by commas
* @param {Array} artistsArray
* @returns {String} artists
*/
getArtistsString: function (artistsArray: string[]) {
if (artistsArray.length > 0) return artistsArray.join(", ");
return "unknown artist(s)";
},
getAlbumName: function () {
try {
//If listening to an album, get its name from the header title
if (globalThis.location.href.includes("/album/")) {
const albumName = globalThis.document.querySelector(this.album_header_title);
if (albumName) {
return albumName.textContent;
}
//If listening to a playlist or a mix, get album name from the list
} else if (
globalThis.location.href.includes("/playlist/") ||
globalThis.location.href.includes("/mix/")
) {
if (this.currentlyPlaying === MediaStatus.playing) {
// find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell.
// document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent
const row = window.document
.querySelector(this.currentlyPlaying)
.closest(this.mediaItem);
if (row) {
return row.querySelector(this.album_name_cell).textContent;
}
}
}
// see whether we're on the queue page and get it from there
const queueAlbumName = this.getText("queue_album");
if (queueAlbumName) {
return queueAlbumName;
}
return "";
} catch {
return "";
}
},
isMuted: function () {
return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false
},
isFavorite: function () {
return this.get("favorite").getAttribute("aria-checked") === "true";
},
/**
* Shorthand function to get the text of a dom element
* @param {*} key key in elements object to fetch
*/
getText: function (key: string) {
const element = this.get(key);
return element ? element.textContent : "";
},
/**
* Shorthand function to click a dom element
* @param {*} key key in elements object to fetch
*/
click: function (key: string) {
this.get(key).click();
return this;
},
/**
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function (key: string) {
return this.get(key).focus();
},
};
onMediaInfoUpdate(callback: (state: Partial<MediaInfo>) => void): void {
this.updateSubscriber = callback;
}
bootstrap(options: DomControllerOptions): void {
/**
* Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
* If it's a song it returns the track URL, if not it will return undefined
*/
const getTrackURL = () => {
const id = this.getTrackId();
return `https://tidal.com/browse/track/${id}`;
};
setInterval(async () => {
const title = this.getTitle();
const artistsArray = this.getArtists();
const artistsString = this.getArtistsString();
const current = this.getCurrentTime();
const currentStatus = this.getCurrentlyPlayingStatus();
const shuffleState = this.getCurrentShuffleState();
const repeatState = this.getCurrentRepeatState();
const playStateChanged = currentStatus != this.currentlyPlaying;
const shuffleStateChanged = shuffleState != this.currentShuffleState;
const repeatStateChanged = repeatState != this.currentRepeatState;
if (playStateChanged) this.currentlyPlaying = currentStatus;
if (shuffleStateChanged) this.currentShuffleState = shuffleState;
if (repeatStateChanged) this.currentRepeatState = repeatState;
const album = this.getAlbumName();
const duration = this.getDuration();
const updatedInfo = {
title,
artists: artistsString,
artistsArray,
album: album,
playingFrom: this.getPlayingFrom(),
status: currentStatus,
url: getTrackURL(),
current,
currentInSeconds: convertDurationToSeconds(current),
duration,
durationInSeconds: convertDurationToSeconds(duration),
image: this.getSongIcon(),
favorite: this.isFavorite(),
player: {
status: currentStatus,
shuffle: shuffleState,
repeat: repeatState,
},
};
this.updateSubscriber(updatedInfo);
}, options.refreshInterval);
}
playPause = (): void => {
const play = this.elements.get("play");
if (play) {
this.elements.click("play");
} else {
this.elements.click("pause");
}
};
goToHome(): void {
this.elements.click("home");
}
openSettings(): void {
this.elements.click("settings");
setTimeout(() => {
this.elements.click("openSettings");
}, 100);
}
toggleFavorite(): void {
this.elements.click("favorite");
}
back(): void {
this.elements.click("back");
}
forward(): void {
this.elements.click("forward");
}
repeat(): void {
this.elements.click("repeat");
}
next(): void {
this.elements.click("next");
}
previous(): void {
this.elements.click("previous");
}
toggleShuffle(): void {
this.elements.click("shuffle");
}
getCurrentlyPlayingStatus() {
const pause = this.elements.get("pause");
// if pause button is visible tidal is playing
if (pause) {
return MediaStatus.playing;
} else {
return MediaStatus.paused;
}
}
getCurrentShuffleState() {
const shuffle = this.elements.get("shuffle");
return shuffle?.getAttribute("aria-checked") === "true";
}
getCurrentRepeatState() {
const repeat = this.elements.get("repeat");
switch (repeat?.getAttribute("data-type")) {
case "button__repeatAll":
return RepeatState.all;
case "button__repeatSingle":
return RepeatState.single;
default:
return RepeatState.off;
}
}
play(): void {
this.playPause();
}
pause(): void {
this.playPause();
}
stop(): void {
this.playPause();
}
getCurrentPosition() {
return this.elements.getText("current");
}
getCurrentPositionInSeconds(): number {
return convertDurationToSeconds(this.getCurrentPosition());
}
getTrackId(): string {
const URLelement = this.elements.get("title").querySelector("a");
if (URLelement !== null) {
const id = URLelement.href.replace(/\D/g, "");
return id;
}
return window.location.toString();
}
getCurrentTime(): string {
return this.elements.getText("current");
}
getDuration(): string {
return this.elements.getText("duration");
}
getAlbumName(): string {
return this.elements.getAlbumName();
}
getTitle(): string {
return this.elements.getText("title");
}
getArtists(): string[] {
return this.elements.getArtistsArray();
}
getArtistsString(): string {
return this.elements.getArtistsString(this.getArtists());
}
getPlayingFrom(): string {
return this.elements.getText("playing_from");
}
isFavorite(): boolean {
return this.elements.isFavorite();
}
getSongIcon(): string {
return this.elements.getSongIcon();
}
}

View File

@@ -1,53 +0,0 @@
import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
export interface TidalController<TBootstrapOptions = object> {
goToHome(): void;
openSettings(): void;
/**
* Play or pause the current media
*/
playPause(): void;
play(): void;
pause(): void;
stop(): void;
toggleFavorite(): void;
back(): void;
forward(): void;
repeat(): void;
next(): void;
previous(): void;
toggleShuffle(): void;
/**
* Optional setup/startup/bootstrap for this controller
*/
bootstrap(options: TBootstrapOptions): void;
/**
* Method that triggers every time the MediaInfo updates
* @param callback function that receives the updated media info
*/
onMediaInfoUpdate(callback: (state: Partial<MediaInfo>) => void): void;
/**
* Update the current status of tidal (e.g playing or paused)
*/
getCurrentlyPlayingStatus(): MediaStatus;
getCurrentShuffleState(): boolean;
getCurrentRepeatState(): RepeatState;
getCurrentPosition(): string;
getCurrentPositionInSeconds(): number;
getTrackId(): string;
getCurrentTime(): string;
getDuration(): string;
getAlbumName(): string;
getTitle(): string;
getArtists(): string[];
getArtistsString(): string;
getPlayingFrom(): string;
getSongIcon(): string;
isFavorite(): boolean;
}

View File

@@ -1,137 +0,0 @@
import { Logger } from "../../features/logger";
import { MediaInfo } from "../../models/mediaInfo";
import { MediaStatus } from "../../models/mediaStatus";
import { RepeatState } from "../../models/repeatState";
import { DomTidalController } from "../DomController/DomTidalController";
import { TidalController } from "../TidalController";
export class TidalApiController implements TidalController {
public domMediaController: TidalController;
constructor() {
this.domMediaController = new DomTidalController();
Logger.log("[TidalApiController] - Initialized domController as a backup controller");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onMediaInfoUpdate(callback: (state: Partial<MediaInfo>) => void): void {
globalThis.alert("method not implemented");
throw new Error("Method not implemented.");
}
bootstrap(): void {
globalThis.alert("Method not implemented: ");
throw new Error("Method not implemented.");
}
// example of using the original domMediaController as a fallback
goToHome(): void {
this.domMediaController.goToHome();
}
getDuration(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getAlbumName(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getTitle(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getArtists(): string[] {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getArtistsString(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getPlayingFrom(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
isFavorite(): boolean {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getSongIcon(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getCurrentTime(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getCurrentPosition(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getCurrentPositionInSeconds(): number {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getTrackId(): string {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
play(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
pause(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
stop(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getCurrentShuffleState(): boolean {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getCurrentRepeatState(): RepeatState {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
getCurrentlyPlayingStatus(): MediaStatus {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
back(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
forward(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
repeat(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
next(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
previous(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
toggleShuffle(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
openSettings(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
toggleFavorite(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
playPause(): void {
globalThis.alert("Method not implemented");
throw new Error("Method not implemented.");
}
}

View File

@@ -1,4 +0,0 @@
export const tidalControllers = {
domController: "domController",
tidalApiController: "tidalApiController",
};

View File

@@ -13,7 +13,6 @@ export const settings = {
advanced: { advanced: {
root: "advanced", root: "advanced",
tidalUrl: "advanced.tidalUrl", tidalUrl: "advanced.tidalUrl",
controllerType: "advanced.controllerType",
}, },
api: "api", api: "api",
apiSettings: { apiSettings: {

View File

@@ -21,9 +21,7 @@
"/current": { "/current": {
"get": { "get": {
"summary": "Get current media info", "summary": "Get current media info",
"tags": [ "tags": ["current"],
"current"
],
"responses": { "responses": {
"200": { "200": {
"description": "Current media info", "description": "Current media info",
@@ -41,9 +39,7 @@
"/current/image": { "/current/image": {
"get": { "get": {
"summary": "Get current media image", "summary": "Get current media image",
"tags": [ "tags": ["current"],
"current"
],
"responses": { "responses": {
"200": { "200": {
"description": "Current media image", "description": "Current media image",
@@ -65,9 +61,7 @@
"/player/play": { "/player/play": {
"post": { "post": {
"summary": "Play the current media", "summary": "Play the current media",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -85,9 +79,7 @@
"/player/favorite/toggle": { "/player/favorite/toggle": {
"post": { "post": {
"summary": "Add the current media to your favorites, or remove it if its already added to your favorites", "summary": "Add the current media to your favorites, or remove it if its already added to your favorites",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -105,9 +97,7 @@
"/player/pause": { "/player/pause": {
"post": { "post": {
"summary": "Pause the current media", "summary": "Pause the current media",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -125,9 +115,7 @@
"/player/next": { "/player/next": {
"post": { "post": {
"summary": "Play the next song", "summary": "Play the next song",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -145,9 +133,7 @@
"/player/previous": { "/player/previous": {
"post": { "post": {
"summary": "Play the previous song", "summary": "Play the previous song",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -165,9 +151,7 @@
"/player/shuffle/toggle": { "/player/shuffle/toggle": {
"post": { "post": {
"summary": "Play the previous song", "summary": "Play the previous song",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -185,9 +169,7 @@
"/player/repeat/toggle": { "/player/repeat/toggle": {
"post": { "post": {
"summary": "Toggle the repeat status, toggles between \"off\" , \"single\" and \"all\"", "summary": "Toggle the repeat status, toggles between \"off\" , \"single\" and \"all\"",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -205,9 +187,7 @@
"/player/playpause": { "/player/playpause": {
"post": { "post": {
"summary": "Start playing the media if paused, or pause the media if playing", "summary": "Start playing the media if paused, or pause the media if playing",
"tags": [ "tags": ["player"],
"player"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok", "description": "Ok",
@@ -225,9 +205,7 @@
"/settings/skipped-artists": { "/settings/skipped-artists": {
"get": { "get": {
"summary": "get a list of artists that TIDAL Hi-Fi will skip if skipping is enabled", "summary": "get a list of artists that TIDAL Hi-Fi will skip if skipping is enabled",
"tags": [ "tags": ["settings"],
"settings"
],
"responses": { "responses": {
"200": { "200": {
"description": "The list book.", "description": "The list book.",
@@ -243,9 +221,7 @@
}, },
"post": { "post": {
"summary": "Add new artists to the list of skipped artists", "summary": "Add new artists to the list of skipped artists",
"tags": [ "tags": ["settings"],
"settings"
],
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@@ -266,9 +242,7 @@
"/settings/skipped-artists/delete": { "/settings/skipped-artists/delete": {
"post": { "post": {
"summary": "Remove artists from the list of skipped artists", "summary": "Remove artists from the list of skipped artists",
"tags": [ "tags": ["settings"],
"settings"
],
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@@ -289,9 +263,7 @@
"/settings/skipped-artists/current": { "/settings/skipped-artists/current": {
"post": { "post": {
"summary": "Add the current artist to the list of skipped artists", "summary": "Add the current artist to the list of skipped artists",
"tags": [ "tags": ["settings"],
"settings"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok" "description": "Ok"
@@ -300,9 +272,7 @@
}, },
"delete": { "delete": {
"summary": "Remove the current artist from the list of skipped artists", "summary": "Remove the current artist from the list of skipped artists",
"tags": [ "tags": ["settings"],
"settings"
],
"responses": { "responses": {
"200": { "200": {
"description": "Ok" "description": "Ok"
@@ -313,9 +283,7 @@
"/image": { "/image": {
"get": { "get": {
"summary": "Get current image", "summary": "Get current image",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -338,9 +306,7 @@
"/play": { "/play": {
"get": { "get": {
"summary": "Play the current media", "summary": "Play the current media",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -359,9 +325,7 @@
"/favorite/toggle": { "/favorite/toggle": {
"get": { "get": {
"summary": "Add the current media to your favorites, or remove it if its already added to your favorites", "summary": "Add the current media to your favorites, or remove it if its already added to your favorites",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -380,9 +344,7 @@
"/pause": { "/pause": {
"get": { "get": {
"summary": "Pause the current media", "summary": "Pause the current media",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -401,9 +363,7 @@
"/next": { "/next": {
"get": { "get": {
"summary": "Play the next song", "summary": "Play the next song",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -422,9 +382,7 @@
"/previous": { "/previous": {
"get": { "get": {
"summary": "Play the previous song", "summary": "Play the previous song",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -443,9 +401,7 @@
"/playpause": { "/playpause": {
"get": { "get": {
"summary": "Toggle play/pause", "summary": "Toggle play/pause",
"tags": [ "tags": ["legacy"],
"legacy"
],
"deprecated": true, "deprecated": true,
"responses": { "responses": {
"200": { "200": {
@@ -558,10 +514,7 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"example": [ "example": ["Artist1", "Artist2"]
"Artist1",
"Artist2"
]
} }
} }
}, },

View File

@@ -1,15 +0,0 @@
import { downloadFile } from "../../scripts/download";
import { Logger } from "../logger";
export const downloadIcon = async (imagePath: string, destination: string): Promise<string> => {
if (imagePath.startsWith("http")) {
try {
return await downloadFile(imagePath, destination);
} catch (error) {
Logger.log("Downloading file failed", { error });
return "";
}
}
return "";
};

View File

@@ -1,15 +0,0 @@
/**
* Build a track url given the id
*/
export const getTrackURL = (trackId: string) => {
return `https://tidal.com/browse/track/${trackId}`;
};
/**
* Retrieve the universal link given a regular track link
* @param trackLink
* @returns
*/
export const getUniversalLink = (trackLink: string) => {
return `${trackLink}?u`;
};

View File

@@ -32,8 +32,7 @@ const PROTOCOL_PREFIX = "tidal";
const windowPreferences = { const windowPreferences = {
sandbox: false, sandbox: false,
plugins: true, plugins: true,
devTools: true, // Ensure devTools is enabled for debugging devTools: true, // I like tinkering, others might too
contextIsolation: false, // Disable context isolation for debugging
}; };
setDefaultFlags(app); setDefaultFlags(app);

View File

@@ -1,11 +1,9 @@
import { MediaPlayerInfo } from "./mediaPlayerInfo"; import { MediaPlayerInfo } from "./mediaPlayerInfo";
import { MediaStatus } from "./mediaStatus"; import { MediaStatus } from "./mediaStatus";
import { RepeatState } from "./repeatState";
export interface MediaInfo { export interface MediaInfo {
title: string; title: string;
artists: string; artists: string;
artistsArray?: string[];
album: string; album: string;
icon: string; icon: string;
status: MediaStatus; status: MediaStatus;
@@ -19,30 +17,3 @@ export interface MediaInfo {
favorite: boolean; favorite: boolean;
player?: MediaPlayerInfo; player?: MediaPlayerInfo;
} }
export const getEmptyMediaInfo = () => {
const emptyState: MediaInfo = {
title: "",
artists: "",
artistsArray: [],
album: "",
playingFrom: "",
status: MediaStatus.playing,
url: "",
current: "00:00",
currentInSeconds: 100,
duration: "00:00",
durationInSeconds: 100,
image: "",
icon: "",
favorite: true,
player: {
status: MediaStatus.playing,
shuffle: true,
repeat: RepeatState.all,
},
};
return emptyState;
};

View File

@@ -61,8 +61,7 @@ let adBlock: HTMLInputElement,
discord_show_song: HTMLInputElement, discord_show_song: HTMLInputElement,
discord_show_idle: HTMLInputElement, discord_show_idle: HTMLInputElement,
discord_idle_text: HTMLInputElement, discord_idle_text: HTMLInputElement,
discord_using_text: HTMLInputElement, discord_using_text: HTMLInputElement;
controllerType: HTMLSelectElement;
addCustomCss(app); addCustomCss(app);
@@ -158,7 +157,6 @@ function refreshSettings() {
discord_show_idle.checked = settingsStore.get(settings.discord.showIdle); 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);
controllerType.value = settingsStore.get(settings.advanced.controllerType);
// set state of all switches with additional settings // set state of all switches with additional settings
Object.values(switchesWithSettings).forEach((settingSwitch) => { Object.values(switchesWithSettings).forEach((settingSwitch) => {
@@ -279,7 +277,6 @@ window.addEventListener("DOMContentLoaded", () => {
discord_show_idle = get("discord_show_idle"); 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");
controllerType = get<HTMLSelectElement>("controllerType");
refreshSettings(); refreshSettings();
addInputListener(adBlock, settings.adBlock); addInputListener(adBlock, settings.adBlock);
@@ -325,5 +322,4 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(discord_show_idle, settings.discord.showIdle); 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);
addSelectListener(controllerType, settings.advanced.controllerType);
}); });

View File

@@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -109,10 +109,7 @@
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>Static Window Title</h4> <h4>Static Window Title</h4>
<p> <p>Makes the window title "TIDAL Hi-Fi" instead of changing to the currently playing song.</p>
Makes the window title "TIDAL Hi-Fi" instead of changing to the currently
playing song.
</p>
</div> </div>
<label class="switch"> <label class="switch">
<input id="staticWindowTitle" type="checkbox" /> <input id="staticWindowTitle" type="checkbox" />
@@ -160,8 +157,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 media 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">
@@ -183,8 +180,7 @@
<div class="group__option"> <div class="group__option">
<div class="group__description"> <div class="group__description">
<h4>API hostname</h4> <h4>API hostname</h4>
<p> <p>By default (127.0.0.1) only local apps can interface with the API. <br />
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 /> Change to 0.0.0.0 to allow <strong>anyone</strong> to interact with it. <br />
Other options are available Other options are available
</p> </p>
@@ -240,6 +236,7 @@
</label> </label>
</div> </div>
<div id="discord_options"> <div id="discord_options">
<div class="group__option" class="hidden"> <div class="group__option" class="hidden">
<div class="group__description"> <div class="group__description">
<h4>Show Idle Text</h4> <h4>Show Idle Text</h4>
@@ -262,9 +259,7 @@
<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
</p>
<input id="discord_using_text" type="text" class="text-input" name="discord_using_text" /> <input id="discord_using_text" type="text" class="text-input" name="discord_using_text" />
</div> </div>
</div> </div>
@@ -281,6 +276,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>
@@ -308,6 +304,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="group"> <div class="group">
@@ -326,10 +323,7 @@
<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
API url below.
</p>
<input id="ListenBrainzAPI" type="text" class="text-input" name="ListenBrainzAPI" /> <input id="ListenBrainzAPI" type="text" class="text-input" name="ListenBrainzAPI" />
</div> </div>
</div> </div>
@@ -343,10 +337,8 @@
</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 listen 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
</p>
<input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" /> <input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" />
</div> </div>
</div> </div>
@@ -359,9 +351,10 @@
<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 id="updateFrequency" type="number" class="text-input" name="updateFrequency" /> <input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
</div> </div>
@@ -376,19 +369,7 @@
</p> </p>
<select class="select-input" id="channel" name="channel"> <select class="select-input" id="channel" name="channel">
<option value="https://listen.tidal.com">Stable (listen.tidal.com)</option> <option value="https://listen.tidal.com">Stable (listen.tidal.com)</option>
<option value="https://listen.stage.tidal.com"> <option value="https://listen.stage.tidal.com">Staging (listen.stage.tidal.com)</option>
Staging (listen.stage.tidal.com)
</option>
</select>
</div>
</div>
<div class="group__option">
<div class="group__description">
<h4>Controller Type</h4>
<p>Select the type of controller to use.</p>
<select id="controllerType" class="select-input">
<option value="domController">DOM Controller</option>
<option value="tidalApiController">Tidal Api Controller (beta)</option>
</select> </select>
</div> </div>
</div> </div>
@@ -434,8 +415,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">
@@ -453,8 +433,7 @@
<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>
@@ -469,7 +448,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>
@@ -477,14 +458,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 id="theme-files" class="file-input" type="file" accept=".css" multiple /> <input id="theme-files" class="file-input" type="file" accept=".css" multiple>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,8 @@
import { app, dialog, Notification } from "@electron/remote"; import { app, dialog, Notification } from "@electron/remote";
import { clipboard, ipcRenderer } from "electron"; import { clipboard, ipcRenderer } from "electron";
import Player from "mpris-service"; import Player from "mpris-service";
import { tidalControllers } from "./constants/controller";
import { globalEvents } from "./constants/globalEvents"; import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings"; import { settings } from "./constants/settings";
import { downloadIcon } from "./features/icon/downloadIcon";
import { import {
ListenBrainz, ListenBrainz,
ListenBrainzConstants, ListenBrainzConstants,
@@ -12,57 +10,185 @@ 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 { addCustomCss } from "./features/theming/theming"; import { addCustomCss } from "./features/theming/theming";
import { getTrackURL, getUniversalLink } from "./features/tidal/url";
import { convertDurationToSeconds } from "./features/time/parse"; import { convertDurationToSeconds } from "./features/time/parse";
import { getEmptyMediaInfo, MediaInfo } from "./models/mediaInfo"; import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus"; import { MediaStatus } from "./models/mediaStatus";
import { RepeatState } from "./models/repeatState";
import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys"; import { addHotkey } from "./scripts/hotkeys";
import { ObjectToDotNotation } from "./scripts/objectUtilities"; 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 { TidalApiController } from "./TidalControllers/apiController/TidalApiController";
import { DomControllerOptions } from "./TidalControllers/DomController/DomControllerOptions";
import { DomTidalController } from "./TidalControllers/DomController/DomTidalController";
import { TidalController } from "./TidalControllers/TidalController";
const notificationPath = `${app.getPath("userData")}/notification.jpg`; const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const staticTitle = "TIDAL Hi-Fi";
let currentSong = ""; let currentSong = "";
let player: Player; let player: Player;
let currentPlayStatus = MediaStatus.paused;
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>; let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
let scrobbleWaitingForDelay = false; let scrobbleWaitingForDelay = false;
let currentlyPlaying = MediaStatus.paused;
let currentRepeatState: RepeatState = RepeatState.off;
let currentShuffleState = false;
let currentMediaInfo: MediaInfo;
let currentNotification: Electron.Notification; let currentNotification: Electron.Notification;
let tidalController: TidalController; const elements = {
let controllerOptions = {}; play: '*[data-test="play"]',
let currentMediaInfo = getEmptyMediaInfo(); pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]',
settings: '*[data-test^="sidebar-menu-button"]',
openSettings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[class^=_playbackControlsContainer] *[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '*[class^="_playingFrom"] span:nth-child(2)',
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']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
favorite: '*[data-test="footer-favorite-button"]',
/**
* Get an element from the dom
* @param {*} key key in elements object to fetch
*/
get: function (key: string) {
return window.document.querySelector(this[key.toLowerCase()]);
},
switch (settingsStore.get(settings.advanced.controllerType)) { /**
case tidalControllers.tidalApiController: { * Get the icon of the current media
tidalController = new TidalApiController(); */
Logger.log("TidalApiController initialized"); getSongIcon: function () {
break; const figure = this.get("media");
}
default: { if (figure) {
tidalController = new DomTidalController(); const mediaElement = figure.querySelector(this["image"]);
const domControllerOptions: DomControllerOptions = { if (mediaElement) {
refreshInterval: getDomUpdateFrequency(), return mediaElement.src.replace("80x80", "640x640");
}; }
controllerOptions = domControllerOptions; }
Logger.log("domController initialized");
break; return "";
} },
}
/**
* returns an array of all artists in the current media
* @returns {Array} artists
*/
getArtistsArray: function () {
const footer = this.get("footer");
if (footer) {
const artists = footer.querySelectorAll(this.artists);
if (artists) return Array.from(artists).map((artist) => (artist as HTMLElement).textContent);
}
return [];
},
/**
* unify the artists array into a string separated by commas
* @param {Array} artistsArray
* @returns {String} artists
*/
getArtistsString: function (artistsArray: string[]) {
if (artistsArray.length > 0) return artistsArray.join(", ");
return "unknown artist(s)";
},
getAlbumName: function () {
try {
//If listening to an album, get its name from the header title
if (globalThis.location.href.includes("/album/")) {
const albumName = globalThis.document.querySelector(this.album_header_title);
if (albumName) {
return albumName.textContent;
}
//If listening to a playlist or a mix, get album name from the list
} else if (
globalThis.location.href.includes("/playlist/") ||
globalThis.location.href.includes("/mix/")
) {
if (this.currentlyPlaying === MediaStatus.playing) {
// find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell.
// document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent
const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem);
if (row) {
return row.querySelector(this.album_name_cell).textContent;
}
}
}
// see whether we're on the queue page and get it from there
const queueAlbumName = this.getText("queue_album");
if (queueAlbumName) {
return queueAlbumName;
}
return "";
} catch {
return "";
}
},
isMuted: function () {
return this.get("volume").getAttribute("aria-checked") === "false"; // it's muted if aria-checked is false
},
isFavorite: function () {
return this.get("favorite").getAttribute("aria-checked") === "true";
},
/**
* Shorthand function to get the text of a dom element
* @param {*} key key in elements object to fetch
*/
getText: function (key: string) {
const element = this.get(key);
return element ? element.textContent : "";
},
/**
* Shorthand function to click a dom element
* @param {*} key key in elements object to fetch
*/
click: function (key: string) {
this.get(key).click();
return this;
},
/**
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function (key: string) {
return this.get(key).focus();
},
};
/** /**
* Get the update frequency from the store * Get the update frequency from the store
* make sure it returns a number, if not use the default * make sure it returns a number, if not use the default
*/ */
function getDomUpdateFrequency() { function getUpdateFrequency() {
const storeValue = settingsStore.get<string, number>(settings.updateFrequency); const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
const defaultValue = 500; const defaultValue = 500;
@@ -73,6 +199,19 @@ function getDomUpdateFrequency() {
} }
} }
/**
* Play or pause the current media
*/
function playPause() {
const play = elements.get("play");
if (play) {
elements.click("play");
} else {
elements.click("pause");
}
}
/** /**
* Clears the old listenbrainz data on launch * Clears the old listenbrainz data on launch
*/ */
@@ -85,26 +224,30 @@ ListenBrainzStore.clear();
*/ */
function addHotKeys() { function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) { if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", () => { addHotkey("Control+p", function () {
tidalController.openSettings(); elements.click("settings");
setTimeout(() => {
elements.click("openSettings");
}, 100);
}); });
addHotkey("Control+l", () => { addHotkey("Control+l", function () {
handleLogout(); handleLogout();
}); });
addHotkey("Control+a", () => {
tidalController.toggleFavorite(); addHotkey("Control+a", function () {
elements.click("favorite");
}); });
addHotkey("Control+h", () => { addHotkey("Control+h", function () {
tidalController.goToHome(); elements.click("home");
}); });
addHotkey("backspace", function () { addHotkey("backspace", function () {
tidalController.back(); elements.click("back");
}); });
addHotkey("shift+backspace", function () { addHotkey("shift+backspace", function () {
tidalController.forward(); elements.click("forward");
}); });
addHotkey("control+u", function () { addHotkey("control+u", function () {
@@ -113,10 +256,10 @@ function addHotKeys() {
}); });
addHotkey("control+r", function () { addHotkey("control+r", function () {
tidalController.repeat(); elements.click("repeat");
}); });
addHotkey("control+w", async function () { addHotkey("control+w", async function () {
const url = getUniversalLink(getTrackURL(tidalController.getTrackId())); const url = SharingService.getUniversalLink(getTrackURL());
clipboard.writeText(url); clipboard.writeText(url);
new Notification({ new Notification({
title: `Universal link generated: `, title: `Universal link generated: `,
@@ -180,22 +323,22 @@ function addIPCEventListeners() {
case globalEvents.playPause: case globalEvents.playPause:
case globalEvents.play: case globalEvents.play:
case globalEvents.pause: case globalEvents.pause:
tidalController.playPause(); playPause();
break; break;
case globalEvents.next: case globalEvents.next:
tidalController.next(); elements.click("next");
break; break;
case globalEvents.previous: case globalEvents.previous:
tidalController.previous(); elements.click("previous");
break; break;
case globalEvents.toggleFavorite: case globalEvents.toggleFavorite:
tidalController.toggleFavorite(); elements.click("favorite");
break; break;
case globalEvents.toggleShuffle: case globalEvents.toggleShuffle:
tidalController.toggleShuffle(); elements.click("shuffle");
break; break;
case globalEvents.toggleRepeat: case globalEvents.toggleRepeat:
tidalController.repeat(); elements.click("repeat");
break; break;
default: default:
break; break;
@@ -204,6 +347,48 @@ function addIPCEventListeners() {
}); });
} }
/**
* Update the current status of tidal (e.g playing or paused)
*/
function getCurrentlyPlayingStatus() {
const pause = elements.get("pause");
let status = undefined;
// if pause button is visible tidal is playing
if (pause) {
status = MediaStatus.playing;
} else {
status = MediaStatus.paused;
}
return status;
}
function getCurrentShuffleState() {
const shuffle = elements.get("shuffle");
return shuffle?.getAttribute("aria-checked") === "true";
}
function getCurrentRepeatState() {
const repeat = elements.get("repeat");
switch (repeat?.getAttribute("data-type")) {
case "button__repeatAll":
return RepeatState.all;
case "button__repeatSingle":
return RepeatState.single;
default:
return RepeatState.off;
}
}
/**
* Convert the duration from MM:SS to seconds
* @param {*} duration
*/
function convertDuration(duration: string) {
const parts = duration.split(":");
return parseInt(parts[1]) + 60 * parseInt(parts[0]);
}
/** /**
* Update Tidal-hifi's media info * Update Tidal-hifi's media info
* *
@@ -211,6 +396,7 @@ function addIPCEventListeners() {
*/ */
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) { function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) {
if (mediaInfo) { if (mediaInfo) {
currentMediaInfo = mediaInfo;
ipcRenderer.send(globalEvents.updateInfo, mediaInfo); ipcRenderer.send(globalEvents.updateInfo, mediaInfo);
updateMpris(mediaInfo); updateMpris(mediaInfo);
updateListenBrainz(mediaInfo); updateListenBrainz(mediaInfo);
@@ -273,35 +459,16 @@ function addMPRIS() {
const eventValue = events[eventName]; const eventValue = events[eventName];
switch (events[eventValue]) { switch (events[eventValue]) {
case events.playpause: case events.playpause:
tidalController.playPause(); playPause();
break;
case events.next:
tidalController.next();
break;
case events.previous:
tidalController.previous();
break;
case events.pause:
tidalController.pause();
break;
case events.stop:
tidalController.stop();
break;
case events.play:
tidalController.play();
break;
case events.loopStatus:
tidalController.repeat();
break;
case events.shuffle:
tidalController.toggleShuffle();
break; break;
default:
elements.click(eventValue);
} }
}); });
}); });
// Override get position function // Override get position function
player.getPosition = function () { player.getPosition = function () {
return tidalController.getCurrentPositionInSeconds(); return convertDuration(elements.getText("current")) * 1000 * 1000;
}; };
player.on("quit", function () { player.on("quit", function () {
app.quit(); app.quit();
@@ -322,8 +489,8 @@ function updateMpris(mediaInfo: MediaInfo) {
"xesam:album": mediaInfo.album, "xesam:album": mediaInfo.album,
"xesam:url": mediaInfo.url, "xesam:url": mediaInfo.url,
"mpris:artUrl": mediaInfo.image, "mpris:artUrl": mediaInfo.image,
"mpris:length": convertDurationToSeconds(mediaInfo.duration) * 1000 * 1000, "mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + tidalController.getTrackId(), "mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
}, },
...ObjectToDotNotation(mediaInfo, "custom:"), ...ObjectToDotNotation(mediaInfo, "custom:"),
}; };
@@ -350,47 +517,124 @@ function updateListenBrainz(mediaInfo: MediaInfo) {
mediaInfo.title, mediaInfo.title,
mediaInfo.artists, mediaInfo.artists,
mediaInfo.status, mediaInfo.status,
convertDurationToSeconds(mediaInfo.duration), convertDuration(mediaInfo.duration)
); );
scrobbleWaitingForDelay = false; scrobbleWaitingForDelay = false;
}, },
settingsStore.get(settings.ListenBrainz.delay) ?? 0, settingsStore.get(settings.ListenBrainz.delay) ?? 0
); );
} }
} }
} }
} }
tidalController.bootstrap(controllerOptions); /**
tidalController.onMediaInfoUpdate(async (newState) => { * Checks if Tidal is playing a video or song by grabbing the "a" element from the title.
currentMediaInfo = { ...currentMediaInfo, ...newState }; * If it's a song it returns the track URL, if not it will return undefined
*/
function getTrackURL() {
const id = getTrackID();
return `https://tidal.com/browse/track/${id}`;
}
const songDashArtistTitle = `${currentMediaInfo.title} - ${currentMediaInfo.artists}`; function getTrackID() {
const isNewSong = currentSong !== songDashArtistTitle; const URLelement = elements.get("title").querySelector("a");
if (URLelement !== null) {
const id = URLelement.href.replace(/\D/g, "");
return id;
}
if (isNewSong) { return window.location;
// check whether one of the artists is in the "skip artist" array, if so, skip... }
skipArtistsIfFoundInSkippedArtistsList(currentMediaInfo.artistsArray ?? []);
// update the currently playing song /**
currentSong = songDashArtistTitle; * Watch for song changes and update title + notify
*/
setInterval(function () {
const title = elements.getText("title");
const artistsArray = elements.getArtistsArray();
const artistsString = elements.getArtistsString(artistsArray);
const songDashArtistTitle = `${title} - ${artistsString}`;
const staticTitle = "TIDAL Hi-Fi";
const titleOrArtistsChanged = currentSong !== songDashArtistTitle;
const current = elements.getText("current");
const currentStatus = getCurrentlyPlayingStatus();
const shuffleState = getCurrentShuffleState();
const repeatState = getCurrentRepeatState();
// update the window title with the new info const playStateChanged = currentStatus != currentlyPlaying;
const shuffleStateChanged = shuffleState != currentShuffleState;
const repeatStateChanged = repeatState != currentRepeatState;
const hasStateChanged = playStateChanged || shuffleStateChanged || repeatStateChanged;
// update info if song changed or was just paused/resumed
if (titleOrArtistsChanged || hasStateChanged) {
if (playStateChanged) currentlyPlaying = currentStatus;
if (shuffleStateChanged) currentShuffleState = shuffleState;
if (repeatStateChanged) currentRepeatState = repeatState;
skipArtistsIfFoundInSkippedArtistsList(artistsArray);
const album = elements.getAlbumName();
const duration = elements.getText("duration");
const options: MediaInfo = {
title,
artists: artistsString,
album: album,
playingFrom: elements.getText("playing_from"),
status: currentStatus,
url: getTrackURL(),
current,
currentInSeconds: convertDurationToSeconds(current),
duration,
durationInSeconds: convertDurationToSeconds(duration),
image: "",
icon: "",
favorite: elements.isFavorite(),
player: {
status: currentStatus,
shuffle: shuffleState,
repeat: repeatState,
},
};
// update title, url and play info with new info
settingsStore.get(settings.staticWindowTitle) settingsStore.get(settings.staticWindowTitle)
? setTitle(staticTitle) ? setTitle(staticTitle)
: setTitle(`${currentMediaInfo.title} - ${currentMediaInfo.artists}`); : setTitle(songDashArtistTitle);
getTrackURL();
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
// download a new icon and use it for the media info const image = elements.getSongIcon();
if (!newState.icon && newState.image) {
currentMediaInfo.icon = await downloadIcon(currentMediaInfo.image, notificationPath);
} else {
currentMediaInfo.icon = "";
}
updateMediaInfo(currentMediaInfo, true); new Promise<void>((resolve) => {
if (image.startsWith("http")) {
options.image = image;
downloadFile(image, notificationPath).then(
() => {
options.icon = notificationPath;
resolve();
},
() => {
// if the image can't be downloaded then continue without it
resolve();
}
);
} else {
// if the image can't be found on the page continue without it
resolve();
}
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
});
} else { } else {
// if titleOrArtists didn't change then only minor mediaInfo (like timings) changed, so don't bother the user with notifications // just update the time
updateMediaInfo(currentMediaInfo, false); updateMediaInfo(
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
false
);
} }
/** /**
@@ -405,12 +649,13 @@ tidalController.onMediaInfoUpdate(async (newState) => {
const artistNames = Object.values(artists).map((artist) => artist); const artistNames = Object.values(artists).map((artist) => artist);
const foundArtist = artistNames.some((artist) => artistsToSkip.includes(artist)); const foundArtist = artistNames.some((artist) => artistsToSkip.includes(artist));
if (foundArtist) { if (foundArtist) {
tidalController.next(); elements.click("next");
} }
} }
} }
} }
}); }, getUpdateFrequency());
addMPRIS(); addMPRIS();
addCustomCss(app); addCustomCss(app);
addHotKeys(); addHotKeys();

View File

@@ -26,7 +26,7 @@ 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, type: ACTIVITY_LISTENING
}; };
const updateActivity = () => { const updateActivity = () => {
@@ -117,17 +117,15 @@ const getActivity = (): SetActivity => {
const connectWithRetry = async (retryCount = 0) => { const connectWithRetry = async (retryCount = 0) => {
try { try {
await rpc.login(); await rpc.login();
Logger.log("Connected to Discord"); Logger.log('Connected to Discord');
rpc.on("ready", updateActivity); rpc.on("ready", updateActivity);
Object.values(globalEvents).forEach((event) => ipcMain.on(event, observer)); Object.values(globalEvents).forEach(event => ipcMain.on(event, observer));
} catch (error) { } catch (error) {
if (retryCount < MAX_RETRIES) { if (retryCount < MAX_RETRIES) {
Logger.log( Logger.log(`Failed to connect to Discord, retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
`Failed to connect to Discord, retrying in ${RETRY_DELAY / 1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`
);
setTimeout(() => connectWithRetry(retryCount + 1), RETRY_DELAY); setTimeout(() => connectWithRetry(retryCount + 1), RETRY_DELAY);
} else { } else {
Logger.log("Failed to connect to Discord after maximum retry attempts"); Logger.log('Failed to connect to Discord after maximum retry attempts');
} }
} }
}; };
@@ -136,7 +134,7 @@ 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: {type: "ipc"}, clientId });
connectWithRetry(); connectWithRetry();
}; };

View File

@@ -6,8 +6,8 @@ import request from "request";
* @param {string} fileUrl url to download * @param {string} fileUrl url to download
* @param {string} targetPath path to save it at * @param {string} targetPath path to save it at
*/ */
export const downloadFile = function (fileUrl: string, targetPath: string): Promise<string> { export const downloadFile = function (fileUrl: string, targetPath: string) {
return new Promise<string>((resolve, reject) => { return new Promise((resolve, reject) => {
const req = request({ const req = request({
method: "GET", method: "GET",
uri: fileUrl, uri: fileUrl,
@@ -16,9 +16,7 @@ export const downloadFile = function (fileUrl: string, targetPath: string): Prom
const out = fs.createWriteStream(targetPath); const out = fs.createWriteStream(targetPath);
req.pipe(out); req.pipe(out);
req.on("end", () => { req.on("end", resolve);
resolve(targetPath);
});
req.on("error", reject); req.on("error", reject);
}); });

View File

@@ -1,6 +1,28 @@
import { getEmptyMediaInfo, MediaInfo } from "../models/mediaInfo"; import { MediaInfo } from "../models/mediaInfo";
import { MediaStatus } from "../models/mediaStatus";
import { RepeatState } from "../models/repeatState";
const defaultInfo: MediaInfo = getEmptyMediaInfo(); const defaultInfo: MediaInfo = {
title: "",
artists: "",
album: "",
icon: "",
playingFrom: "",
status: MediaStatus.paused,
url: "",
current: "",
currentInSeconds: 0,
duration: "",
durationInSeconds: 0,
image: "tidal-hifi-icon",
favorite: false,
player: {
status: MediaStatus.paused,
shuffle: false,
repeat: RepeatState.off,
},
};
export let mediaInfo: MediaInfo = { ...defaultInfo }; export let mediaInfo: MediaInfo = { ...defaultInfo };

View File

@@ -30,7 +30,6 @@ export const settingsStore = new Store({
adBlock: false, adBlock: false,
advanced: { advanced: {
tidalUrl: "https://listen.tidal.com", tidalUrl: "https://listen.tidal.com",
controllerType: "domController",
}, },
api: true, api: true,
apiSettings: { apiSettings: {
@@ -121,11 +120,6 @@ export const settingsStore = new Store({
"5.16.0": (migrationStore) => { "5.16.0": (migrationStore) => {
buildMigration("5.16.0", migrationStore, [{ key: settings.discord.showIdle, value: "true" }]); buildMigration("5.16.0", migrationStore, [{ key: settings.discord.showIdle, value: "true" }]);
}, },
"5.19.0": (migrationStore) => {
buildMigration("5.19.0", migrationStore, [
{ key: settings.advanced.controllerType, value: "domController" },
]);
},
}, },
}); });