Compare commits

...

150 Commits

Author SHA1 Message Date
d67f62c0dc Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into develop 2024-10-27 23:06:20 +01:00
ae699887b2 Merge pull request #494 from Mastermindzh/develop
Develop
2024-10-27 21:51:50 +01:00
b2afd44dd6 prettier :) 2024-10-27 21:31:31 +01:00
a8c635932f Merge branch 'develop' of github.com:Mastermindzh/tidal-hifi into develop 2024-10-27 21:27:38 +01:00
66d0d004bf discord listening to 2024-10-27 21:27:23 +01:00
d1e0321058 Merge pull request #488 from 3top1a/master
"Listening to" Discord RPC
2024-10-27 21:24:08 +01:00
2ab5a556ab Removed Songwhip (they shut down) and replaced it with TIDAL's universal link system 2024-10-27 21:01:37 +01:00
baf719fc60 Merge branch 'develop' of github.com:Mastermindzh/tidal-hifi into develop 2024-10-27 20:24:38 +01:00
15c8f6a418 prepping 5.17 2024-10-27 20:24:32 +01:00
f73521e2e5 Merge pull request #491 from darkiox/master
feat: Added ability to keep a static window title.
2024-10-27 20:20:23 +01:00
3top1a
0f5e00c4df Revert "Removed redundant RPC settings" 2024-10-23 19:14:36 +02:00
Darío Marmie
974877ea4f staticWindowTitle(fix): Order alphabetically on all appearances, change if to a ternary 2024-10-23 09:39:13 -03:00
3top1a
6be5774001 Added RPC connection retrying 2024-10-22 22:29:56 +02:00
3top1a
f96cf2e8da Removed redundant RPC settings 2024-10-22 22:27:29 +02:00
3top1a
f832bd2712 replaced discord rpc library 2024-10-22 17:20:06 +02:00
Darío Marmie
cb8667bd41 feat: Added ability to keep a static window title. 2024-10-21 18:28:03 -03:00
91156e5936 Merge pull request #490 from Mastermindzh/snyk-upgrade-aca3a223a3167341470045eeca25ec6e
[Snyk] Upgrade sass from 1.78.0 to 1.79.4
2024-10-21 09:12:32 +02:00
snyk-bot
57e9a2dcb8 fix: upgrade sass from 1.78.0 to 1.79.4
Snyk has created this PR to upgrade sass from 1.78.0 to 1.79.4.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-10-19 06:05:50 +00:00
3top1a
21edcd6ad5 reverted more ugly/unnecessary changes 2024-10-17 20:51:23 +02:00
3top1a
3dc42eceb0 Reverted bad changes 2024-10-17 20:47:40 +02:00
3top1a
b1830f5684 Fixed impl 2024-10-17 20:16:22 +02:00
3top1a
1a0c15e17f Maybe? Can't build 2024-10-16 20:37:47 +02:00
6e59d59a1d Merge pull request #484 from Mastermindzh/snyk-fix-a313b5a6ce537f9549a8ae3a45f1033f
[Snyk] Security upgrade express from 4.21.0 to 4.21.1
2024-10-16 09:51:56 +02:00
snyk-bot
fab7497311 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-COOKIE-8163060
2024-10-12 06:26:33 +00:00
fccbcc77ea Merge pull request #478 from Mastermindzh/snyk-upgrade-28ef48ed574610f887d2ae8544baaf7d
[Snyk] Upgrade axios from 1.7.5 to 1.7.7
2024-09-26 11:09:22 +02:00
snyk-bot
041c19fb52 fix: upgrade axios from 1.7.5 to 1.7.7
Snyk has created this PR to upgrade axios from 1.7.5 to 1.7.7.

See this package in npm:
axios

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-09-26 08:54:14 +00:00
e36c562afa Merge pull request #472 from Mastermindzh/snyk-upgrade-6ce11eb8281ab57e87d8cb068d3cf64d
[Snyk] Upgrade axios from 1.7.4 to 1.7.5
2024-09-25 16:36:18 +02:00
1589aa5251 Merge pull request #473 from Mastermindzh/dependabot/npm_and_yarn/multi-cf87d80143
chore(deps): bump send and express
2024-09-25 16:35:39 +02:00
8c672dd1eb Merge pull request #476 from Mastermindzh/snyk-upgrade-3215721a01f05f1d0592d13bf3fb865f
[Snyk] Upgrade sass from 1.77.8 to 1.78.0
2024-09-25 16:34:49 +02:00
snyk-bot
3769550f24 fix: upgrade sass from 1.77.8 to 1.78.0
Snyk has created this PR to upgrade sass from 1.77.8 to 1.78.0.

See this package in npm:
sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-09-25 09:23:53 +00:00
dependabot[bot]
5a06b6c53f chore(deps): bump send and express
Bumps [send](https://github.com/pillarjs/send) to 0.19.0 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.20.0 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.20.0...4.21.0)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 08:12:34 +00:00
snyk-bot
1d4ef66d27 fix: upgrade axios from 1.7.4 to 1.7.5
Snyk has created this PR to upgrade axios from 1.7.4 to 1.7.5.

See this package in npm:
axios

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-09-14 08:18:21 +00:00
e02f07401b Merge pull request #459 from Mastermindzh/snyk-fix-0bde81baefef700af2dc83cf83bf83d4
[Snyk] Security upgrade axios from 1.7.2 to 1.7.4
2024-09-10 15:27:26 +02:00
118f92e75a Merge pull request #469 from Mastermindzh/dependabot/npm_and_yarn/multi-ceff1a497b
chore(deps): bump path-to-regexp and express
2024-09-10 15:05:56 +02:00
dependabot[bot]
788d302ce8 chore(deps): bump path-to-regexp and express
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.10 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `path-to-regexp` from 0.1.7 to 0.1.10
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.10)

Updates `express` from 4.19.2 to 4.20.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-10 10:21:44 +00:00
snyk-bot
cc09f35b49 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-7361793
2024-08-14 22:44:20 +00:00
ef13933c66 Merge pull request #456 from Mastermindzh/develop
Develop
2024-08-10 14:49:42 +02:00
4f72e1b35d fix: Notifications are now send at the end of the update process, allowing other events to happen sooner. 2024-08-10 14:46:08 +02:00
2c1c76d2d0 fix: all discord fields are now padded to 2+ chars 2024-08-10 14:20:13 +02:00
e2a84e119a Merge pull request #448 from autumn-puffin/master
Add option to disable the discord rpc idle text
2024-08-05 18:28:53 +02:00
5b85e59fc3 Merge pull request #450 from HurleybirdJr/master
Fix issue #449 Discord RPC stuck on "Browsing Tidal"
2024-08-05 18:28:44 +02:00
c3b772919a Merge pull request #451 from Mastermindzh/snyk-upgrade-ceb71bfecd1c7652906d5640e4d56a9b
[Snyk] Upgrade sass from 1.77.6 to 1.77.8
2024-08-02 14:27:00 +02:00
snyk-bot
70f2f5c248 fix: upgrade sass from 1.77.6 to 1.77.8
Snyk has created this PR to upgrade sass from 1.77.6 to 1.77.8.

See this package in npm:
sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-08-02 08:17:34 +00:00
Will Hurley
ffcb563b35 Fix Discord RPC album length < 2 2024-08-01 12:10:37 +01:00
Autumn
2dd96dd48e Add option to disable the discord rpc idle text 2024-07-30 21:26:35 +10:00
65f3b251f4 Merge pull request #435 from Mastermindzh/feature/docs
feat: dependency udpate & openapi extension
2024-07-15 23:54:03 +02:00
45f8c13c5b feat: Added a channel selector so we can now use Tidal's staging environment directly from the app 2024-07-15 12:36:55 +02:00
9dc7267a4d renamed 'song' to 'media' in html 2024-07-15 09:40:08 +02:00
f5c56ae8c5 deps: bumpies 2024-07-10 14:57:00 +02:00
308b527469 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/docs 2024-07-10 14:56:07 +02:00
f4aa8e070e Merge pull request #434 from Mastermindzh/snyk-upgrade-ef2733c0cae4459854fe8a974ba4cb47
[Snyk] Upgrade sass from 1.77.5 to 1.77.6
2024-07-10 08:53:12 +02:00
19c12b57de ci: change to node 22.4 2024-07-09 11:06:57 +02:00
b5713651de Merge branch 'feature/docs' of github.com:Mastermindzh/tidal-hifi into feature/docs 2024-07-09 10:51:53 +02:00
268027734c feat: dependency udpate & openapi extension 2024-07-09 10:51:40 +02:00
f711ea9000 feat: dependency udpate & openapi extension 2024-07-09 10:41:31 +02:00
snyk-bot
506f86f014 fix: upgrade sass from 1.77.5 to 1.77.6
Snyk has created this PR to upgrade sass from 1.77.5 to 1.77.6.

See this package in npm:
sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-07-09 06:24:36 +00:00
4e827e120f Merge pull request #431 from Mastermindzh/snyk-upgrade-de4b5154b7445fb3cf5ec389f4c5715f
[Snyk] Upgrade sass from 1.77.4 to 1.77.5
2024-07-04 10:23:34 +02:00
snyk-bot
5b62154ebc fix: upgrade sass from 1.77.4 to 1.77.5
Snyk has created this PR to upgrade sass from 1.77.4 to 1.77.5.

See this package in npm:
sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-07-03 05:18:29 +00:00
15cc6bb6d4 Merge pull request #425 from Mastermindzh/snyk-upgrade-f3026437cbeaad8aa72daea7d39ede0c
[Snyk] Upgrade sass from 1.77.2 to 1.77.4
2024-06-24 11:26:17 +02:00
e0e9d99173 Merge branch 'master' into snyk-upgrade-f3026437cbeaad8aa72daea7d39ede0c 2024-06-24 11:13:10 +02:00
daa797fc00 Merge pull request #427 from Mastermindzh/snyk-upgrade-fc345622b53226751fadf4ff2a3da0b1
[Snyk] Upgrade swagger-ui-express from 5.0.0 to 5.0.1
2024-06-24 10:56:04 +02:00
snyk-bot
4e011a47d8 fix: upgrade swagger-ui-express from 5.0.0 to 5.0.1
Snyk has created this PR to upgrade swagger-ui-express from 5.0.0 to 5.0.1.

See this package in npm:
swagger-ui-express

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-06-22 20:28:34 +00:00
snyk-bot
965d19318e fix: upgrade sass from 1.77.2 to 1.77.4
Snyk has created this PR to upgrade sass from 1.77.2 to 1.77.4.

See this package in npm:
sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-06-21 20:55:14 +00:00
9cd89c9f31 Merge pull request #423 from Mastermindzh/dependabot/npm_and_yarn/ws-7.5.10
chore(deps): bump ws from 7.5.9 to 7.5.10
2024-06-20 15:10:02 +02:00
dependabot[bot]
c7b97a49c4 chore(deps): bump ws from 7.5.9 to 7.5.10
Bumps [ws](https://github.com/websockets/ws) from 7.5.9 to 7.5.10.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 19:01:45 +00:00
c63e05357e Update README.md 2024-06-16 05:23:28 +02:00
42dac4a7f1 Merge pull request #420 from Mastermindzh/snyk-upgrade-9156bcae7d2be2477cf56414a066139f
[Snyk] Upgrade axios from 1.7.1 to 1.7.2
2024-06-13 13:38:10 +02:00
snyk-bot
595895dbc1 fix: upgrade axios from 1.7.1 to 1.7.2
Snyk has created this PR to upgrade axios from 1.7.1 to 1.7.2.

See this package in npm:
axios

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-06-13 05:53:24 +00:00
1ff94e45a7 Merge pull request #418 from Mastermindzh/dependabot/npm_and_yarn/braces-3.0.3
chore(deps): bump braces from 3.0.2 to 3.0.3
2024-06-11 10:28:50 +02:00
dependabot[bot]
95776d1aab chore(deps): bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 07:43:23 +00:00
5669cb4c6a Merge pull request #417 from Mastermindzh/snyk-upgrade-338b304353e209764e7b225a59b9a6ea
[Snyk] Upgrade sass from 1.75.0 to 1.77.2
2024-06-11 09:42:56 +02:00
5946c47442 Merge pull request #416 from Mastermindzh/snyk-upgrade-81ed892136e18f6714c0baec6619f4b2
[Snyk] Upgrade axios from 1.6.8 to 1.7.1
2024-06-11 09:42:39 +02:00
snyk-bot
111238f6b2 fix: upgrade sass from 1.75.0 to 1.77.2
Snyk has created this PR to upgrade sass from 1.75.0 to 1.77.2.

See this package in npm:
sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-06-11 04:40:11 +00:00
snyk-bot
65a4600c4d fix: upgrade axios from 1.6.8 to 1.7.1
Snyk has created this PR to upgrade axios from 1.6.8 to 1.7.1.

See this package in npm:
axios

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-06-11 04:40:07 +00:00
dd6f81386f Merge pull request #414 from Mastermindzh/version/next
fix: Fixed  not finding album name whilst on queue page
2024-06-09 18:51:37 +02:00
54316d31b5 Added all mediaInfo to mpris interface using the prefix 2024-06-09 16:16:25 +02:00
28a9458dfc fix: Fixed not finding album name whilst on queue page 2024-06-09 15:50:49 +02:00
3641f07558 Merge pull request #413 from Mastermindzh/version/next
Version/next
2024-06-09 14:07:13 +02:00
ecbfa7e226 feat: Added [Tidal Magazine](https://tidal.com/magazine/) integration (in the menubar or use ) 2024-06-09 13:51:55 +02:00
9321acc06e fix: Reworked swagger generation hotfix to properly generate during the compile step 2024-06-09 13:28:43 +02:00
5b656ae229 feat: API now allows you to set the so you can control who can interact with the API. 2024-06-09 13:07:49 +02:00
0a8efc730d simplified mediaInfo & Options 2024-06-09 12:33:48 +02:00
b49bd925da Merge pull request #410 from Mastermindzh/hotfix/swagger-issues
Hotfix/swagger issues
2024-05-27 13:05:24 +02:00
1e6b9f7dcf ci: added manual trigger to actions 2024-05-27 12:33:34 +02:00
51f7a96634 hotfix: fixed api not working due to swagger 2024-05-27 12:28:45 +02:00
46074c5de5 Merge pull request #407 from Mastermindzh/new-version
5.13.0
2024-05-20 16:01:28 +02:00
2667f62674 chore: changelog update 2024-05-20 15:48:46 +02:00
ac949dc211 Merge pull request #401 from Mjokfox/api/add_cors
Add cors to the express api
2024-05-20 15:47:56 +02:00
40bc20582f Merge branch 'new-version' of github.com:Mastermindzh/tidal-hifi into api/add_cors 2024-05-20 15:46:02 +02:00
mjokfox
180d9c97a7 Add the cors module and use it with express api 2024-05-20 15:42:49 +02:00
5dc136138b fix: possible type confusion 2024-05-20 15:34:38 +02:00
1edc6a1b2b chore: versioning 2024-05-20 15:24:15 +02:00
7c6831c771 added swagger docs 2024-05-20 15:23:26 +02:00
d47da91e93 Added an API to add & delete entries from the skippedArtists list in the settings. fixes [#405] 2024-05-20 14:24:47 +02:00
b481108af1 fix: fixes #403 - cannot read shuffle of undefined error 2024-05-20 12:18:52 +02:00
3740ce5a12 Merge pull request #402 from Mastermindzh/5.12
5.12
2024-05-14 23:08:55 +02:00
a0f9faa753 chore: updating versions 2024-05-14 22:53:07 +02:00
5e3583534b Merge pull request #396 from ThatGravyBoat/api/shuffle-repeat-state
Api Feature: Add shuffle and repeat state to API
2024-05-14 22:50:59 +02:00
ThatGravyBoat
5f8cf33249 Fix mismatched import styling 2024-05-06 03:55:09 -02:30
ThatGravyBoat
2d94b4bf49 Add shuffle and repeat to current state api 2024-05-06 03:50:41 -02:30
6e43cbb4d7 Merge pull request #395 from Mastermindzh/next-version
Next version
2024-05-05 20:46:21 +02:00
f95f13b44a chore: doc fix 2024-05-05 20:35:58 +02:00
f911564d8a chore: version increase 2024-05-05 20:34:58 +02:00
db8a2c2741 feat: reworked the api, added duration/current in seconds + shuffle & repeat 2024-05-05 20:12:10 +02:00
000853414e feat: switched to TIDAL's universal link format in the entire app 2024-05-05 15:09:54 +02:00
53603c4cad Merge pull request #392 from TheRockYT/MediaSessionService
Media session service
2024-05-05 14:25:02 +02:00
0b595f920f Merge pull request #391 from Mastermindzh/dependabot/npm_and_yarn/ejs-3.1.10
chore(deps-dev): bump ejs from 3.1.9 to 3.1.10
2024-05-05 13:57:59 +02:00
81143af3fa Merge pull request #394 from Mastermindzh/snyk-upgrade-0d65fc86b872e70883e2ebe344b05405
[Snyk] Upgrade sass from 1.74.1 to 1.75.0
2024-05-05 13:57:41 +02:00
snyk-bot
8d1ac3be3b fix: upgrade sass from 1.74.1 to 1.75.0
Snyk has created this PR to upgrade sass from 1.74.1 to 1.75.0.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-05-03 22:13:17 +00:00
dependabot[bot]
666e602c02 chore(deps-dev): bump ejs from 3.1.9 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 10:01:32 +00:00
TheRockYT
04ec850005 Merge branch 'Mastermindzh:master' into MediaSessionService 2024-05-01 20:55:39 +02:00
943d9b5bd8 Merge pull request #386 from Mastermindzh/snyk-upgrade-2399381ec4ed8a254535fddca6093bb9
[Snyk] Upgrade sass from 1.72.0 to 1.74.1
2024-04-25 10:26:22 +02:00
snyk-bot
755816c2b8 fix: upgrade sass from 1.72.0 to 1.74.1
Snyk has created this PR to upgrade sass from 1.72.0 to 1.74.1.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-04-25 04:29:39 +00:00
25afd05ad7 Merge pull request #374 from TheRockYT/custom_protocol_fix
Custom protocol fix (tidal://...)
2024-04-22 11:23:47 +02:00
6e5024742a Merge pull request #382 from Mastermindzh/snyk-upgrade-e8583a1804fc58a60a199bdcdfddb237
[Snyk] Upgrade sass from 1.71.1 to 1.72.0
2024-04-18 12:34:41 +02:00
417afaab85 Merge pull request #381 from Mastermindzh/snyk-upgrade-eddeada86cf707578042602e7a9acfd4
[Snyk] Upgrade electron-store from 8.1.0 to 8.2.0
2024-04-18 12:34:27 +02:00
d225c0056b Merge pull request #380 from Mastermindzh/snyk-upgrade-636acae2850bc80d0d46d524748be780
[Snyk] Upgrade axios from 1.6.5 to 1.6.8
2024-04-18 12:34:18 +02:00
snyk-bot
a75b0336db fix: upgrade sass from 1.71.1 to 1.72.0
Snyk has created this PR to upgrade sass from 1.71.1 to 1.72.0.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-04-16 20:25:03 +00:00
snyk-bot
29465ce13a fix: upgrade electron-store from 8.1.0 to 8.2.0
Snyk has created this PR to upgrade electron-store from 8.1.0 to 8.2.0.

See this package in npm:
https://www.npmjs.com/package/electron-store

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-04-16 20:25:00 +00:00
snyk-bot
d333047269 fix: upgrade axios from 1.6.5 to 1.6.8
Snyk has created this PR to upgrade axios from 1.6.5 to 1.6.8.

See this package in npm:
https://www.npmjs.com/package/axios

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-04-16 20:24:56 +00:00
TheRockYT
712330f8f1 Enable MediaSessionService flag to allow listen.tidal.com to control it. 2024-04-04 23:08:18 +02:00
TheRockYT
84fd35ce0e Remove implementation of global shortcuts for media control. 2024-04-04 23:06:29 +02:00
TheRockYT
326038f262 Remove custom implementation of MediaSessionService. It is disabled anyway. 2024-04-04 23:05:09 +02:00
a6c1d35a60 Merge pull request #375 from Mastermindzh/snyk-fix-a6b2d7614f87b9d818d7aaf7e6d7650d
[Snyk] Security upgrade express from 4.18.3 to 4.19.2
2024-03-28 14:09:29 +01:00
snyk-bot
c09a4bc4a8 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-EXPRESS-6474509
2024-03-26 21:37:31 +00:00
TheRockYT
554cb12a01 Fix Custom Protocol Handling
Fixed an issue where the custom protocol ('tidal://...') wasn't being recognized correctly. Now, the system checks for the presence of the custom protocol before the usual URL ('https://listen.tidal.com') is loaded.

Also implemented functionality to ensure that if only one instance is allowed at a time, the first instance changes its page accordingly.
2024-03-26 00:16:09 +01:00
2e31b5d913 Merge pull request #373 from Mastermindzh/develop
5.10.0
2024-03-24 21:11:58 +01:00
2fd29c1b83 added rel='noopener' to external links 2024-03-24 21:00:49 +01:00
b2f27a2afe Enabled wayland platform flags by default when launching through .desktop file fixes #273 #347 2024-03-24 20:54:46 +01:00
8e11fd7f09 Reverted to using old icon syntax with icons in the build directory. fixes #350 2024-03-24 16:17:46 +01:00
17b2818b70 Refactored nowPlaying code to always display the current state, even when the built-in UI is updated. fixes #351 #356 #370 2024-03-24 16:13:21 +01:00
4ef76c262e Links in the about window now open in the user's default browser. fixes #360 2024-03-24 15:55:33 +01:00
fd0dae2762 fix: rewrote docs to fix #365 2024-03-24 15:42:50 +01:00
aa59bdc6dd Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into develop 2024-03-24 15:34:18 +01:00
5b5b6ecb38 Merge pull request #364 from lennart-k/master
feature: Track current notification to replace it on track change
2024-03-24 15:34:00 +01:00
5983145857 Merge pull request #369 from Mastermindzh/dependabot/npm_and_yarn/follow-redirects-1.15.6
chore(deps): bump follow-redirects from 1.15.4 to 1.15.6
2024-03-24 15:32:14 +01:00
0c7d579951 Merge pull request #371 from Mastermindzh/snyk-upgrade-30d8b55c8b39463bad9d0408a76bfa86
[Snyk] Upgrade express from 4.18.2 to 4.18.3
2024-03-24 15:32:02 +01:00
snyk-bot
235d916749 fix: upgrade express from 4.18.2 to 4.18.3
Snyk has created this PR to upgrade express from 4.18.2 to 4.18.3.

See this package in npm:
https://www.npmjs.com/package/express

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-03-21 17:32:39 +00:00
dependabot[bot]
2d9f268866 chore(deps): bump follow-redirects from 1.15.4 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 22:30:30 +00:00
ae65e57e32 Merge pull request #367 from Mastermindzh/snyk-upgrade-0829d5b61286f530a986191886019e0f
[Snyk] Upgrade sass from 1.71.0 to 1.71.1
2024-03-14 16:08:02 +01:00
snyk-bot
3f2d69f2f4 fix: upgrade sass from 1.71.0 to 1.71.1
Snyk has created this PR to upgrade sass from 1.71.0 to 1.71.1.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-03-14 01:58:53 +00:00
5ff2cc68d3 Merge pull request #359 from Mastermindzh/snyk-upgrade-f5ef319e06f8e24c1358e777b2415e49
[Snyk] Upgrade hotkeys-js from 3.13.6 to 3.13.7
2024-03-12 09:21:24 +01:00
daabe5bdbb Merge pull request #363 from Mastermindzh/snyk-upgrade-63565ec96f33448467289d767c1fb965
[Snyk] Upgrade sass from 1.70.0 to 1.71.0
2024-03-12 09:20:35 +01:00
Lennart
456727c0e0 feature: Track current notification to replace it on track change 2024-03-09 17:49:18 +01:00
snyk-bot
ba50e0c095 fix: upgrade sass from 1.70.0 to 1.71.0
Snyk has created this PR to upgrade sass from 1.70.0 to 1.71.0.

See this package in npm:
https://www.npmjs.com/package/sass

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-03-08 19:44:36 +00:00
snyk-bot
312e90e8cb fix: upgrade hotkeys-js from 3.13.6 to 3.13.7
Snyk has created this PR to upgrade hotkeys-js from 3.13.6 to 3.13.7.

See this package in npm:
https://www.npmjs.com/package/hotkeys-js

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-03-01 19:11:09 +00:00
76769dfab3 Merge pull request #354 from Mastermindzh/snyk-upgrade-d64f8964b9d005d97354597114dfafa3
[Snyk] Upgrade hotkeys-js from 3.13.5 to 3.13.6
2024-02-29 09:11:22 +01:00
snyk-bot
565d32ae3d fix: upgrade hotkeys-js from 3.13.5 to 3.13.6
Snyk has created this PR to upgrade hotkeys-js from 3.13.5 to 3.13.6.

See this package in npm:
https://www.npmjs.com/package/hotkeys-js

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-02-24 02:43:08 +00:00
7be6f79040 Merge pull request #349 from Mastermindzh/snyk-upgrade-f0f8b19cd2b7f7662ced7cf1b914e52a
[Snyk] Upgrade @electron/remote from 2.1.1 to 2.1.2
2024-02-16 13:36:45 +01:00
snyk-bot
f894c82b12 fix: upgrade @electron/remote from 2.1.1 to 2.1.2
Snyk has created this PR to upgrade @electron/remote from 2.1.1 to 2.1.2.

See this package in npm:
https://www.npmjs.com/package/@electron/remote

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/dade8f03-2064-49a3-8957-edbacec3887c?utm_source=github&utm_medium=referral&page=upgrade-pr
2024-02-16 02:47:27 +00:00
59 changed files with 3929 additions and 1371 deletions

View File

@@ -9,6 +9,7 @@ on:
branches-ignore:
- master
- develop
workflow_dispatch:
jobs:
build_on_linux:
runs-on: ubuntu-latest
@@ -20,7 +21,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.4
- run: npm install
- run: npm run build
@@ -30,7 +31,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.4
- run: npm install
- run: npm run build
@@ -40,6 +41,6 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.4
- run: npm install
- run: npm run build

View File

@@ -8,6 +8,7 @@ on:
pull_request:
branches:
- master
workflow_dispatch:
jobs:
build_on_linux:
@@ -20,7 +21,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.4
- run: npm install
- run: npm run build
- uses: actions/upload-artifact@master
@@ -34,7 +35,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.4
- run: npm install
- run: npm run build
- uses: actions/upload-artifact@master
@@ -48,7 +49,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.4
- run: npm install
- run: npm run build
- uses: actions/upload-artifact@master

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,96 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.17.0]
- Added an option to disable the dynamic title and set it to a static one, [#491](https://github.com/Mastermindzh/tidal-hifi/pull/491)
- Discord integration now says "Listening to" instead of "playing" [#488](https://github.com/Mastermindzh/tidal-hifi/pull/488) && [#454](https://github.com/Mastermindzh/tidal-hifi/pull/454)
- Fixed several element names in the dom scraper
- Removed the Songwhip (they shut down) integration and replaced it with TIDAL's universal link system
## [5.16.0]
- Fix issue #449 Discord RPC stuck on "Browsing Tidal".
- Fix issue #448 Add option to disable the discord rpc idle text
- Notifications are now send at the end of the update process, allowing other events to happen sooner.
## [5.15.0]
- Added all missing swagger/openApi info with the help of [Times-Z](https://github.com/Times-Z)
- Updated most dependency versions
- This includes Electron 31!
- Added a channel selector so we can now use Tidal's staging environment directly from the app
- implements [#437](https://github.com/Mastermindzh/tidal-hifi/issues/437)
## [5.14.1]
- Fixed `getAlbumName` not finding album name whilst on queue page
- Added all mediaInfo to mpris interface using the `custom:` prefix
## [5.14]
- Simplified `MediaInfo` & `Options` types
- Added `playingFrom` information to the info API
- also changed the way we update Album info since Playing From now shows the correct Album.
- API now allows you to set the `hostname` so you can control who can interact with the API.
- Reworked swagger generation hotfix to properly generate `swagger.json` during the compile step
- Might switch to tsoa in the future, idk yet.
- Added [Tidal Magazine](https://tidal.com/magazine/) integration (in the menubar or use `Ctrl + M`)
## [5.13.1]
- removed Swagger generation step in favor of pre-generated file.
- This also fixes the API issue [#409](https://github.com/Mastermindzh/tidal-hifi/issues/409)
- This also stops TIDAL-hifi from scanning your entire home directory... the glob was very broad apparently.
## [5.13.0]
- Fixed [#403](https://github.com/Mastermindzh/tidal-hifi/issues/403) "cannot read shuffle of undefined" error
- Added an API to add & delete entries from the skippedArtists list in the settings. fixes [#405](https://github.com/Mastermindzh/tidal-hifi/issues/405)
- `GET /settings/skipped-artists` -> get list of skipped artists
- `POST /settings/skipped-artists` -> add to the list of skipped artists
- `POST /settings/skipped-artists/delete` -> delete from the list of skipped artists
- `POST /settings/skipped-artists/current` -> skip the current artist
- `DELETE /settings/skipped-artists/current` -> delete the current artist from the skip list
- Added Swagger documentation to the new endpoints:
![picture of swagger documentation](./docs/images/swagger.png)
- CORS support added by [Mjokfox](https://github.com/Mjokfox)
## [5.12.0]
- Added Shuffle and Repeat state to API response - By [ThatGravyBoat](https://github.com/ThatGravyBoat)
## [5.11.0]
- Re-implemented the API, added support for duration/current in seconds & shuffle+repeat
- made the original API "legacy" (still works the same)
- Now using the correct HTTP verb for all new endpoints
- Implemented TIDAL's universal links. All links are now universal.
- Custom `tidal://` protocol fixed - By [TheRockYT](https://github.com/TheRockYT)
- Global media shortcuts removed since TIDAL includes them by default - By [TheRockYT](https://github.com/TheRockYT)
- Fixes
- [#390](https://github.com/Mastermindzh/tidal-hifi/issues/390)
- [#376](https://github.com/Mastermindzh/tidal-hifi/issues/376)
- [#383](https://github.com/Mastermindzh/tidal-hifi/issues/383)
- [#393](https://github.com/Mastermindzh/tidal-hifi/issues/393)
## [5.10.0]
- TIDAL will now close the previous notification if a new one is sent whilst the old is still visible. [#364](https://github.com/Mastermindzh/tidal-hifi/pull/364)
- Updated developer documentation to get started in README [#365](https://github.com/Mastermindzh/tidal-hifi/pull/365)
- Links in the about window now open in the user's default browser. fixes [#360](https://github.com/Mastermindzh/tidal-hifi/issues/360)
- Refactored "nowPlaying" code to always display the current state, even when the built-in UI is updated.
- fixes [#351](https://github.com/Mastermindzh/tidal-hifi/issues/351)
- fixes [#356](https://github.com/Mastermindzh/tidal-hifi/issues/356)
- fixes [#370](https://github.com/Mastermindzh/tidal-hifi/issues/370)
- Reverted to using old icon syntax with icons in the build directory. fixes [#350](https://github.com/Mastermindzh/tidal-hifi/issues/350)
- Enabled wayland platform flags by default when launching through .desktop file
- fixes [#273](https://github.com/Mastermindzh/tidal-hifi/issues/273)
- fixes [#347](https://github.com/Mastermindzh/tidal-hifi/issues/347)
## [5.9.0]
- More Discord options:
@@ -23,10 +113,12 @@ 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 dependencies to latest
- added theme files to stylelint ignore
- fixed other stylelint errors
- 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 the "favorite" field in the `mediaInfo` and the API `/current` endpoint
- 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)
- [Integrations](#integrations)
- [Known bugs](#known-bugs)
- [DRM not working on Windows](#drm-not-working-on-windows)
- [DRM not working on Windows (error S6007)](#drm-not-working-on-windows-error-s6007)
- [Special thanks to](#special-thanks-to)
- [Donations](#donations)
- [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))
- 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`)
- API for status and playback
- API for status, playback and settings (see the [/docs](http://localhost:47836/docs/) route)
- Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495))
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
- Custom [integrations](#integrations)
- [ListenBrainz](https://listenbrainz.org/?redirect=false) integration
- Songwhip.com integration (hotkey `ctrl + w`)
- Discord RPC integration (showing "now listening", "Browsing", etc)
- Flatpak version only works if both Discord and Tidal-HiFi are flatpaks
- MPRIS integration
- UI + Json config (`~/.config/tidal-hifi/`, or `~/.var/app/com.mastermindzh.tidal-hifi/` for Flatpak)
@@ -130,10 +130,13 @@ nix-env -iA nixpkgs.tidal-hifi
To install and work with the code on this project follow these steps:
- git clone [https://github.com/Mastermindzh/tidal-hifi.git](https://github.com/Mastermindzh/tidal-hifi.git)
- cd TIDAL Hi-Fi
- npm install
- npm start
- `git clone https://github.com/Mastermindzh/tidal-hifi.git`
- `cd tidal-hifi`
- `npm install`
- `npm run watch` to watch for auto-reload of Typescript/SCSS changes.
- `npm run compile` can be used to trigger it once
- `npm watchStart` to auto watch for any updates files and reload Tidal Hi-Fi
- `npm start` can be used to run Tidal Hi-Fi manually once
## Integrations
@@ -149,11 +152,13 @@ Integrations with other projects that are not included natively:
## Known bugs
### DRM not working on Windows
### DRM not working on Windows (error S6007)
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.
Until then you'll have to use the official app unfortunately.
## Special thanks to
- [Castlabs](https://castlabs.com/)

View File

@@ -8,4 +8,3 @@ Only the very latest 😄.
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).

View File

@@ -11,10 +11,16 @@ extraResources:
- "themes/**"
linux:
category: AudioVideo
icon: build/icons/256x256.png
icon: build/icons
target:
- dir
executableName: tidal-hifi
executableArgs:
[
"--enable-features=UseOzonePlatform",
"--ozone-platform-hint=auto",
"--enable-features=WaylandWindowDecorations",
]
desktop:
Encoding: UTF-8
Name: TIDAL Hi-Fi

BIN
build/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
build/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
build/icons/22x22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
build/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
build/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
build/icons/384x384.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
build/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
build/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
docs/images/swagger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

2165
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tidal-hifi",
"version": "5.9.0",
"version": "5.17.0",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js",
"scripts": {
@@ -8,9 +8,10 @@
"watchStart": "nodemon dist -x \"npm run start\"",
"compile": "tsc && npm run sass-and-copy",
"deps": "npm run watch",
"watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"watch": "tsc-watch --onSuccess \"npm run compile-all\"",
"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",
"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",
"build": "npm run builder -- -c ./build/electron-builder.yml",
"build-deb": "npm run builder -- -c ./build/electron-builder.deb.yml",
@@ -22,6 +23,7 @@
"build-mac": "npm run builder -- -c ./build/electron-builder.yml -m",
"build-base": "npm run builder -- -c ./build/electron-builder.base.yml",
"prebuilder": "npm run compile",
"prettier": "prettier . --write",
"builder": "electron-builder --publish=never",
"sass": "sass ./src/pages/settings/settings.scss ./src/pages/settings/settings.css && sass --no-source-map src/themes:themes",
"style-lint": "npx stylelint **/*.scss",
@@ -39,38 +41,44 @@
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT",
"dependencies": {
"@electron/remote": "^2.1.1",
"axios": "^1.6.5",
"discord-rpc": "^4.0.1",
"electron-store": "^8.1.0",
"express": "^4.18.2",
"hotkeys-js": "^3.13.5",
"@electron/remote": "^2.1.2",
"@types/swagger-jsdoc": "^6.0.4",
"@xhayper/discord-rpc": "^1.2.0",
"axios": "^1.7.7",
"cors": "^2.8.5",
"electron-store": "^8.2.0",
"express": "^4.21.1",
"hotkeys-js": "^3.13.7",
"mpris-service": "^2.1.2",
"request": "^2.88.2",
"sass": "^1.70.0"
"sass": "^1.79.4",
"swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0",
"@types/discord-rpc": "^4.0.8",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.10.6",
"@types/node": "^20.14.10",
"@types/request": "^2.48.12",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"@types/swagger-ui-express": "^4.1.6",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.15.0",
"copyfiles": "^2.4.1",
"electron": "git+https://github.com/castlabs/electron-releases#v28.1.1+wvcus",
"electron-builder": "^24.9.1",
"eslint": "^8.56.0",
"electron": "git+https://github.com/castlabs/electron-releases#v31.1.0+wvcus",
"electron-builder": "~24.9.4",
"eslint": "^8.57.0",
"js-yaml": "^4.1.0",
"markdown-toc": "^1.2.0",
"nodemon": "^3.0.2",
"prettier": "^3.1.1",
"stylelint": "^16.1.0",
"stylelint-config-standard": "^36.0.0",
"stylelint-config-standard-scss": "^13.0.0",
"nodemon": "^3.1.4",
"prettier": "^3.3.2",
"stylelint": "^16.6.1",
"stylelint-config-standard": "^36.0.1",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-prettier": "^5.0.0",
"tsc-watch": "^6.0.4",
"typescript": "^5.3.3"
"swagger-jsdoc": "^6.2.8",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.5.3"
},
"prettier": "@mastermindzh/prettier-config"
}

View File

@@ -0,0 +1,30 @@
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,8 +16,9 @@
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[class^="profileOptions"]',
settings: '*[data-test^="open-settings"]',
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"]',
@@ -25,7 +26,8 @@
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
album_header_title: '*[class^="playingFrom"] span:nth-child(2)',
playingFrom: '*[class^="playingFrom"] span:nth-child(2)',
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,121 @@
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

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

42
src/features/api/index.ts Normal file
View File

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

160
src/features/api/legacy.ts Normal file
View File

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

View File

@@ -0,0 +1,535 @@
{
"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

@@ -9,7 +9,6 @@ import { Logger } from "../logger";
*/
export function setDefaultFlags(app: App) {
setFlag(app, "disable-seccomp-filter-sandbox");
setFlag(app, "disable-features", "MediaSessionService");
}
/**

View File

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

View File

@@ -0,0 +1,10 @@
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

@@ -1,21 +0,0 @@
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

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

View File

@@ -1,27 +0,0 @@
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

@@ -1,32 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,17 +24,19 @@ const switchesWithSettings = {
switch: "discord_show_song",
classToHide: "discord_show_song_options",
settingsKey: settings.discord.showSong,
}
},
};
let adBlock: HTMLInputElement,
api: HTMLInputElement,
channel: HTMLSelectElement,
customCSS: HTMLInputElement,
disableBackgroundThrottle: HTMLInputElement,
disableHardwareMediaKeys: HTMLInputElement,
enableCustomHotkeys: HTMLInputElement,
enableDiscord: HTMLInputElement,
gpuRasterization: HTMLInputElement,
hostname: HTMLInputElement,
menuBar: HTMLInputElement,
minimizeOnClose: HTMLInputElement,
mpris: HTMLInputElement,
@@ -44,6 +46,7 @@ let adBlock: HTMLInputElement,
singleInstance: HTMLInputElement,
skipArtists: HTMLInputElement,
skippedArtists: HTMLInputElement,
staticWindowTitle: HTMLInputElement,
theme: HTMLSelectElement,
trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement,
@@ -56,6 +59,7 @@ let adBlock: HTMLInputElement,
discord_include_timestamps: HTMLInputElement,
discord_button_text: HTMLInputElement,
discord_show_song: HTMLInputElement,
discord_show_idle: HTMLInputElement,
discord_idle_text: HTMLInputElement,
discord_using_text: HTMLInputElement;
@@ -120,6 +124,7 @@ function refreshSettings() {
try {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
channel.value = settingsStore.get(settings.advanced.tidalUrl);
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
@@ -127,6 +132,7 @@ function refreshSettings() {
enableDiscord.checked = settingsStore.get(settings.enableDiscord);
enableWaylandSupport.checked = settingsStore.get(settings.flags.enableWaylandSupport);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
hostname.value = settingsStore.get(settings.apiSettings.hostname);
menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris);
@@ -135,8 +141,9 @@ function refreshSettings() {
port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists);
theme.value = settingsStore.get(settings.theme);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
staticWindowTitle.checked = settingsStore.get(settings.staticWindowTitle);
theme.value = settingsStore.get(settings.theme);
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
@@ -147,6 +154,7 @@ function refreshSettings() {
discord_include_timestamps.checked = settingsStore.get(settings.discord.includeTimestamps);
discord_button_text.value = settingsStore.get(settings.discord.buttonText);
discord_show_song.checked = settingsStore.get(settings.discord.showSong);
discord_show_idle.checked = settingsStore.get(settings.discord.showIdle);
discord_idle_text.value = settingsStore.get(settings.discord.idleText);
discord_using_text.value = settingsStore.get(settings.discord.usingText);
@@ -236,6 +244,7 @@ window.addEventListener("DOMContentLoaded", () => {
adBlock = get("adBlock");
api = get("apiCheckbox");
channel = get<HTMLSelectElement>("channel");
customCSS = get("customCSS");
disableBackgroundThrottle = get("disableBackgroundThrottle");
disableHardwareMediaKeys = get("disableHardwareMediaKeys");
@@ -243,6 +252,7 @@ window.addEventListener("DOMContentLoaded", () => {
enableDiscord = get("enableDiscord");
enableWaylandSupport = get("enableWaylandSupport");
gpuRasterization = get("gpuRasterization");
hostname = get("hostname");
menuBar = get("menuBar");
minimizeOnClose = get("minimizeOnClose");
mpris = get("mprisCheckbox");
@@ -253,6 +263,7 @@ window.addEventListener("DOMContentLoaded", () => {
trayIcon = get("trayIcon");
skipArtists = get("skipArtists");
skippedArtists = get("skippedArtists");
staticWindowTitle = get("staticWindowTitle");
singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency");
enableListenBrainz = get("enableListenBrainz");
@@ -263,12 +274,14 @@ window.addEventListener("DOMContentLoaded", () => {
listenbrainz_delay = get("listenbrainz_delay");
discord_button_text = get("discord_button_text");
discord_show_song = get("discord_show_song");
discord_show_idle = get("discord_show_idle");
discord_using_text = get("discord_using_text");
discord_idle_text = get("discord_idle_text")
discord_idle_text = get("discord_idle_text");
refreshSettings();
addInputListener(adBlock, settings.adBlock);
addInputListener(api, settings.api);
addSelectListener(channel, settings.advanced.tidalUrl);
addTextAreaListener(customCSS, settings.customCSS);
addInputListener(disableBackgroundThrottle, settings.disableBackgroundThrottle);
addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys);
@@ -276,6 +289,7 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(enableDiscord, settings.enableDiscord, switchesWithSettings.discord);
addInputListener(enableWaylandSupport, settings.flags.enableWaylandSupport);
addInputListener(gpuRasterization, settings.flags.gpuRasterization);
addInputListener(hostname, settings.apiSettings.hostname);
addInputListener(menuBar, settings.menuBar);
addInputListener(minimizeOnClose, settings.minimizeOnClose);
addInputListener(mpris, settings.mpris);
@@ -284,6 +298,7 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(port, settings.apiSettings.port);
addInputListener(skipArtists, settings.skipArtists);
addTextAreaListener(skippedArtists, settings.skippedArtists);
addInputListener(staticWindowTitle, settings.staticWindowTitle);
addInputListener(singleInstance, settings.singleInstance);
addSelectListener(theme, settings.theme);
addInputListener(trayIcon, settings.trayIcon);
@@ -299,7 +314,12 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(discord_details_prefix, settings.discord.detailsPrefix);
addInputListener(discord_include_timestamps, settings.discord.includeTimestamps);
addInputListener(discord_button_text, settings.discord.buttonText);
addInputListener(discord_show_song, settings.discord.showSong, switchesWithSettings.discord_show_song);
addInputListener(
discord_show_song,
settings.discord.showSong,
switchesWithSettings.discord_show_song
);
addInputListener(discord_show_idle, settings.discord.showIdle);
addInputListener(discord_idle_text, settings.discord.idleText);
addInputListener(discord_using_text, settings.discord.usingText);
});

View File

@@ -1,25 +1,34 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<head>
<title>Tidal Hi-Fi settings</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="./settings.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
</head>
<body class="settings-window">
<body class="settings-window">
<div class="settings-window__wrapper">
<div class="settings-window__drag-area"></div>
<a id="close" class="settings-window__close-button" title="Close settings">
<svg xmlns="http://www.w3.org/2000/svg" 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
<svg
xmlns="http://www.w3.org/2000/svg"
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.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
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>
</a>
@@ -49,7 +58,7 @@
<div class="group__option">
<div class="group__description">
<h4>Notifications</h4>
<p>Show a notification when a new song starts.</p>
<p>Show a notification when new media starts.</p>
</div>
<label class="switch">
<input id="notifications" type="checkbox" />
@@ -66,7 +75,13 @@
<span class="switch__slider"></span>
</label>
</div>
<textarea id="skippedArtists" class="textarea" cols="40" rows="5" spellcheck="false"></textarea>
<textarea
id="skippedArtists"
class="textarea"
cols="40"
rows="5"
spellcheck="false"
></textarea>
<div class="group__option">
<div class="group__description">
<h4>Block ads</h4>
@@ -106,6 +121,19 @@
<span class="switch__slider"></span>
</label>
</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__description">
<h4>Minimize on Close</h4>
@@ -121,7 +149,9 @@
<h4>Hotkeys</h4>
<p>
Enable extra hotkeys to achieve feature parity with the
<a class="external-link" data-url="https://defkey.com/tidal-desktop-shortcuts">desktop apps</a>.
<a class="external-link" data-url="https://defkey.com/tidal-desktop-shortcuts"
>desktop apps</a
>.
</p>
</div>
<label class="switch">
@@ -147,8 +177,8 @@
<p class="group__title">API</p>
<div class="group__description">
<p>
TIDAL Hi-Fi has a built-in web API to allow users to get current song information.
You can optionally enable playback control as well.
TIDAL Hi-Fi has a built-in web API to allow users to get current media
information. You can optionally enable playback control as well.
</p>
</div>
<div class="group__option">
@@ -167,6 +197,17 @@
<input id="port" type="number" class="text-input" name="port" />
</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__description">
<h4>Playback control</h4>
@@ -216,27 +257,49 @@
</label>
</div>
<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__description">
<h4>Idle Text</h4>
<p>The text displayed on Discord's rich presence while idling in the app.</p>
<input id="discord_idle_text" type="text" class="text-input" name="discord_idle_text" />
<input
id="discord_idle_text"
type="text"
class="text-input"
name="discord_idle_text"
/>
</div>
</div>
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Using Tidal Text</h4>
<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" />
<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"
/>
</div>
</div>
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Show song</h4>
<p>Show the current song in the Discord client</p>
<h4>Show media</h4>
<p>Show the current media in the Discord client</p>
</div>
<label class="switch">
<input id="discord_show_song" type="checkbox" />
@@ -245,7 +308,6 @@
</div>
<div id="discord_show_song_options" class="hidden">
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Include timestamps</h4>
@@ -261,19 +323,28 @@
<div class="group__description">
<h4>Details prefix</h4>
<p>Prefix for the "details" field of Discord's rich presence.</p>
<input id="discord_details_prefix" type="text" class="text-input" name="discord_details_prefix" />
<input
id="discord_details_prefix"
type="text"
class="text-input"
name="discord_details_prefix"
/>
</div>
</div>
<div class="group__option">
<div class="group__description">
<h4>Button text</h4>
<p>Text to display on the button below the song information.</p>
<input id="discord_button_text" type="text" class="text-input" name="discord_button_text" />
<p>Text to display on the button below the media information.</p>
<input
id="discord_button_text"
type="text"
class="text-input"
name="discord_button_text"
/>
</div>
</div>
</div>
</div>
</div>
<div class="group">
@@ -292,23 +363,43 @@
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz API Url</h4>
<p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p>
<input id="ListenBrainzAPI" type="text" class="text-input" name="ListenBrainzAPI" />
<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"
/>
</div>
</div>
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz User Token</h4>
<p>Provide the user token you can get from the settings page.</p>
<input id="ListenBrainzToken" type="text" class="text-input" name="ListenBrainzToken" />
<input
id="ListenBrainzToken"
type="text"
class="text-input"
name="ListenBrainzToken"
/>
</div>
</div>
</div>
<div class="group__description">
<h4>ScrobbleDelay</h4>
<p>The delay (in ms) to send a song to ListenBrainz. Prevents spamming the API when you fast forward
immediately</p>
<input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" />
<p>
The delay (in ms) to send a listen to ListenBrainz. Prevents spamming the API when
you fast forward immediately
</p>
<input
id="listenbrainz_delay"
type="number"
class="text-input"
name="listenbrainz_delay"
/>
</div>
</div>
</section>
@@ -320,12 +411,32 @@
<div class="group__description">
<h4>Update frequency</h4>
<p>
The amount of time, in milliseconds, that TIDAL Hi-Fi will refresh its playback info by scraping the
website.
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.
The amount of time, in milliseconds, that TIDAL Hi-Fi will refresh its playback
info by scraping the website. 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>
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
<input
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>
@@ -370,7 +481,8 @@
<div class="group__description">
<h4>Wayland support</h4>
<p>
Adds a couple of Electron flags to help TIDAL Hi-Fi run smoothly on the Wayland window system.
Adds a couple of Electron flags to help TIDAL Hi-Fi run smoothly on the Wayland
window system.
</p>
</div>
<label class="switch">
@@ -388,12 +500,19 @@
<div class="group__description">
<h4>Custom CSS</h4>
<p>
The css that you put in here will be injected into a style tag in the head of the document.
The css that you put in here will be injected into a style tag in the head of
the document.
</p>
</div>
</div>
</div>
<textarea id="customCSS" class="textarea" cols="40" rows="8" spellcheck="false"></textarea>
<textarea
id="customCSS"
class="textarea"
cols="40"
rows="8"
spellcheck="false"
></textarea>
<div class="group">
<p class="group__title">Theme files</p>
@@ -403,9 +522,7 @@
<p>
Select a theme below or "Tidal - Default" to return to the original Tidal look.
</p>
<select class="select-input" id="themesList" name="themesList">
</select>
<select class="select-input" id="themesList" name="themesList"></select>
</div>
</div>
@@ -413,14 +530,20 @@
<div class="group__description">
<h4>Upload new themes</h4>
<p>
Click the button and select the css files to import. They will be added to the theme list
automatically.
Click the button and select the css files to import. They will be added to the
theme list automatically.
</p>
<div class="file-drop-area">
<div>
<span class="file-btn">Choose files</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>
@@ -432,15 +555,35 @@
<img alt="tidal icon" class="about-section__icon" src="./icon.png" />
<h4>TIDAL Hi-Fi</h4>
<div class="about-section__version">
<a href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.9.0">5.9.0</a>
<a
target="_blank"
rel="noopener"
href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.17.0"
>5.17.0</a
>
</div>
<div class="about-section__links">
<a href="https://github.com/mastermindzh/tidal-hifi/" class="about-section__button">Github <i
class="fa fa-external-link"></i></a>
<a href="https://github.com/Mastermindzh/tidal-hifi/issues" class="about-section__button">Report an issue <i
class="fa fa-external-link"></i></a>
<a href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors"
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/"
class="about-section__button"
>Github <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>
</section>
@@ -452,6 +595,5 @@
</div>
</main>
</div>
</body>
</body>
</html>

View File

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

View File

@@ -10,24 +10,30 @@ import {
} from "./features/listenbrainz/listenbrainz";
import { StoreData } from "./features/listenbrainz/models/storeData";
import { Logger } from "./features/logger";
import { Songwhip } from "./features/songwhip/songwhip";
import { SharingService } from "./features/sharingService/sharingService";
import { addCustomCss } from "./features/theming/theming";
import { convertDurationToSeconds } from "./features/time/parse";
import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus";
import { Options } from "./models/options";
import { RepeatState } from "./models/repeatState";
import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys";
import { ObjectToDotNotation } from "./scripts/objectUtilities";
import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions";
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "TIDAL Hi-Fi";
let currentSong = "";
let player: Player;
let currentPlayStatus = MediaStatus.paused;
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
let scrobbleWaitingForDelay = false;
let wasJustPausedOrResumed = false;
let currentMediaInfo: Options;
let currentlyPlaying = MediaStatus.paused;
let currentRepeatState: RepeatState = RepeatState.off;
let currentShuffleState = false;
let currentMediaInfo: MediaInfo;
let currentNotification: Electron.Notification;
const elements = {
play: '*[data-test="play"]',
@@ -42,8 +48,9 @@ const elements = {
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[class^="profileOptions"]',
settings: '*[data-test^="open-settings"]',
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"]',
@@ -51,7 +58,9 @@ const elements = {
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
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"]',
@@ -66,7 +75,7 @@ const elements = {
},
/**
* Get the icon of the current song
* Get the icon of the current media
*/
getSongIcon: function () {
const figure = this.get("media");
@@ -82,7 +91,7 @@ const elements = {
},
/**
* returns an array of all artists in the current song
* returns an array of all artists in the current media
* @returns {Array} artists
*/
getArtistsArray: function () {
@@ -127,6 +136,12 @@ 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 "";
},
@@ -181,10 +196,9 @@ function getUpdateFrequency() {
}
/**
* Play or pause the current song
* Play or pause the current media
*/
function playPause() {
wasJustPausedOrResumed = true;
const play = elements.get("play");
if (play) {
@@ -207,9 +221,9 @@ ListenBrainzStore.clear();
function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () {
elements.click("account");
setTimeout(() => {
elements.click("settings");
setTimeout(() => {
elements.click("openSettings");
}, 100);
});
addHotkey("Control+l", function () {
@@ -241,11 +255,10 @@ function addHotKeys() {
elements.click("repeat");
});
addHotkey("control+w", async function () {
const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL());
const url = Songwhip.getWhipUrl(result);
const url = SharingService.getUniversalLink(getTrackURL());
clipboard.writeText(url);
new Notification({
title: `Successfully whipped: `,
title: `Universal link generated: `,
body: `URL copied to clipboard: ${url}`,
}).show();
});
@@ -317,6 +330,12 @@ function addIPCEventListeners() {
case globalEvents.toggleFavorite:
elements.click("favorite");
break;
case globalEvents.toggleShuffle:
elements.click("shuffle");
break;
case globalEvents.toggleRepeat:
elements.click("repeat");
break;
default:
break;
}
@@ -340,6 +359,23 @@ function getCurrentlyPlayingStatus() {
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
@@ -352,17 +388,36 @@ function convertDuration(duration: string) {
/**
* Update Tidal-hifi's media info
*
* @param {*} options
* @param {*} mediaInfo
*/
function updateMediaInfo(options: Options, notify: boolean) {
if (options) {
currentMediaInfo = options;
ipcRenderer.send(globalEvents.updateInfo, options);
if (settingsStore.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
function updateMediaInfo(mediaInfo: MediaInfo, notify: boolean) {
if (mediaInfo) {
currentMediaInfo = mediaInfo;
ipcRenderer.send(globalEvents.updateInfo, mediaInfo);
updateMpris(mediaInfo);
updateListenBrainz(mediaInfo);
if (notify) {
sendNotification(mediaInfo);
}
updateMpris(options);
updateListenBrainz(options);
}
}
/**
* send a desktop notification if enabled in settings
* @param mediaInfo
* @param notify Whether to notify
*/
async function sendNotification(mediaInfo: MediaInfo) {
if (settingsStore.get(settings.notifications)) {
if (currentNotification) {
currentNotification.close();
}
currentNotification = new Notification({
title: mediaInfo.title,
body: mediaInfo.artists,
icon: mediaInfo.icon,
});
currentNotification.show();
}
}
@@ -420,32 +475,33 @@ function addMPRIS() {
}
}
function updateMpris(options: Options) {
function updateMpris(mediaInfo: MediaInfo) {
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"xesam:title": mediaInfo.title,
"xesam:artist": [mediaInfo.artists],
"xesam:album": mediaInfo.album,
"mpris:artUrl": mediaInfo.image,
"mpris:length": convertDuration(mediaInfo.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
...ObjectToDotNotation(mediaInfo, "custom:"),
};
player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing";
player.playbackStatus = mediaInfo.status === MediaStatus.paused ? "Paused" : "Playing";
}
}
/**
* Update the listenbrainz service with new data based on a few conditions
*/
function updateListenBrainz(options: Options) {
function updateListenBrainz(mediaInfo: MediaInfo) {
if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
if (
(!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== options.title)
(!oldData && mediaInfo.status === MediaStatus.playing) ||
(oldData && oldData.title !== mediaInfo.title)
) {
if (!scrobbleWaitingForDelay) {
scrobbleWaitingForDelay = true;
@@ -453,10 +509,10 @@ function updateListenBrainz(options: Options) {
currentListenBrainzDelayId = setTimeout(
() => {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
mediaInfo.title,
mediaInfo.artists,
mediaInfo.status,
convertDuration(mediaInfo.duration)
);
scrobbleWaitingForDelay = false;
},
@@ -486,23 +542,6 @@ function getTrackID() {
return window.location;
}
function updateMediaSession(options: Options) {
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: options.title,
artist: options.artists,
album: options.album,
artwork: [
{
src: options.icon,
sizes: "640x640",
type: "image/png",
},
],
});
}
}
/**
* Watch for song changes and update title + notify
*/
@@ -511,35 +550,54 @@ setInterval(function () {
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();
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 || wasJustPausedOrResumed) {
if (wasJustPausedOrResumed) {
wasJustPausedOrResumed = false;
}
skipArtistsIfFoundInSkippedArtistsList(artistsArray);
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 = {
const options: MediaInfo = {
title,
artists: artistsString,
album: album,
playingFrom: elements.getText("playing_from"),
status: currentStatus,
url: getTrackURL(),
current,
currentInSeconds: convertDurationToSeconds(current),
duration,
"app-name": appName,
durationInSeconds: convertDurationToSeconds(duration),
image: "",
icon: "",
favorite: elements.isFavorite(),
player: {
status: currentStatus,
shuffle: shuffleState,
repeat: repeatState,
},
};
// update title, url and play info with new info
setTitle(songDashArtistTitle);
settingsStore.get(settings.staticWindowTitle)
? setTitle(staticTitle)
: setTitle(songDashArtistTitle);
getTrackURL();
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
@@ -565,13 +623,13 @@ setInterval(function () {
}
}).then(() => {
updateMediaInfo(options, titleOrArtistsChanged);
if (titleOrArtistsChanged) {
updateMediaSession(options);
}
});
} else {
// just update the time
updateMediaInfo({ ...currentMediaInfo, ...{ current } }, false);
updateMediaInfo(
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
false
);
}
/**

View File

@@ -1,25 +1,24 @@
import { Client, Presence } from "discord-rpc";
import { Client, SetActivity } from "@xhayper/discord-rpc";
import { app, ipcMain } from "electron";
import { globalEvents } from "../constants/globalEvents";
import { settings } from "../constants/settings";
import { Logger } from "../features/logger";
import { convertDurationToSeconds } from "../features/time/parse";
import { MediaStatus } from "../models/mediaStatus";
import { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings";
const clientId = "833617820704440341";
function timeToSeconds(timeArray: string[]) {
const minutes = parseInt(timeArray[0]) * 1;
const seconds = minutes * 60 + parseInt(timeArray[1]) * 1;
return seconds;
}
export let rpc: Client;
const ACTIVITY_LISTENING = 2;
const MAX_RETRIES = 5;
const RETRY_DELAY = 10000;
const observer = () => {
if (rpc) {
rpc.setActivity(getActivity());
updateActivity();
}
};
@@ -27,10 +26,20 @@ const defaultPresence = {
largeImageKey: "tidal-hifi-icon",
largeImageText: `TIDAL Hi-Fi ${app.getVersion()}`,
instance: false,
type: ACTIVITY_LISTENING,
};
const getActivity = (): Presence => {
const presence: Presence = { ...defaultPresence };
const updateActivity = () => {
const showIdle = settingsStore.get<string, boolean>(settings.discord.showIdle) ?? true;
if (mediaInfo.status === MediaStatus.paused && !showIdle) {
rpc.user?.clearActivity();
} else {
rpc.user?.setActivity(getActivity());
}
};
const getActivity = (): SetActivity => {
const presence: SetActivity = { ...defaultPresence };
if (mediaInfo.status === MediaStatus.paused) {
presence.details =
@@ -46,6 +55,7 @@ const getActivity = (): Presence => {
settingsStore.get<string, string>(settings.discord.usingText) ?? "Playing media on TIDAL";
}
}
return presence;
function getFromStore() {
@@ -59,30 +69,66 @@ const getActivity = (): Presence => {
return { includeTimestamps, detailsPrefix, buttonText };
}
function setPresenceFromMediaInfo(detailsPrefix: any, buttonText: any) {
if (mediaInfo.url) {
presence.details = `${detailsPrefix}${mediaInfo.title}`;
presence.state = mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)";
presence.largeImageKey = mediaInfo.image;
if (mediaInfo.album) {
presence.largeImageText = mediaInfo.album;
/**
* 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) {
// 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) {
presence.details = `${detailsPrefix}${title}`;
presence.state = artists ? artists : "unknown artist(s)";
presence.largeImageKey = mediaInfo.image;
if (album) {
presence.largeImageText = album;
}
presence.buttons = [{ label: buttonText, url: mediaInfo.url }];
} else {
presence.details = `Watching ${mediaInfo.title}`;
presence.state = mediaInfo.artists;
presence.details = `Watching ${title}`;
presence.state = artists;
}
}
function includeTimeStamps(includeTimestamps: any) {
function includeTimeStamps(includeTimestamps: boolean) {
if (includeTimestamps) {
const currentSeconds = timeToSeconds(mediaInfo.current.split(":"));
const durationSeconds = timeToSeconds(mediaInfo.duration.split(":"));
const currentSeconds = convertDurationToSeconds(mediaInfo.current);
const durationSeconds = convertDurationToSeconds(mediaInfo.duration);
const date = new Date();
const now = (date.getTime() / 1000) | 0;
const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
presence.startTimestamp = now;
presence.endTimestamp = remaining;
const now = Math.floor(date.getTime() / 1000);
presence.startTimestamp = now - currentSeconds;
presence.endTimestamp = presence.startTimestamp + durationSeconds;
}
}
};
/**
* 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");
}
}
};
@@ -91,18 +137,8 @@ const getActivity = (): Presence => {
* Set up the discord rpc and listen on globalEvents.updateInfo
*/
export const initRPC = () => {
rpc = new Client({ transport: "ipc" });
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?");
}
);
rpc = new Client({ transport: { type: "ipc" }, clientId });
connectWithRetry();
};
/**
@@ -110,7 +146,7 @@ export const initRPC = () => {
*/
export const unRPC = () => {
if (rpc) {
rpc.clearActivity();
rpc.user?.clearActivity();
rpc.destroy();
rpc = null;
ipcMain.removeListener(globalEvents.updateInfo, observer);

View File

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

View File

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

View File

@@ -11,6 +11,23 @@ const settingsMenuEntry = {
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 = {
label: "Quit",
click() {
@@ -41,6 +58,7 @@ export const getMenu = function (mainWindow: BrowserWindow) {
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
tidalMagazineEntry,
quitMenuEntry,
],
},
@@ -48,7 +66,7 @@ export const getMenu = function (mainWindow: BrowserWindow) {
: []),
{
label: "File",
submenu: [settingsMenuEntry, isMac ? { role: "close" } : quitMenuEntry],
submenu: [settingsMenuEntry, tidalMagazineEntry, isMac ? { role: "close" } : quitMenuEntry],
},
{
label: "Edit",
@@ -101,6 +119,7 @@ export const getMenu = function (mainWindow: BrowserWindow) {
},
settingsMenuEntry,
toggleWindow,
tidalMagazineEntry,
quitMenuEntry,
];

View File

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

View File

@@ -1,6 +1,6 @@
import Store from "electron-store";
import { BrowserWindow } from "electron";
import { BrowserWindow, shell } from "electron";
import path from "path";
import { settings } from "../constants/settings";
@@ -28,9 +28,13 @@ const buildMigration = (
export const settingsStore = new Store({
defaults: {
adBlock: false,
advanced: {
tidalUrl: "https://listen.tidal.com",
},
api: true,
apiSettings: {
port: 47836,
hostname: "127.0.0.1",
},
customCSS: [],
disableBackgroundThrottle: true,
@@ -39,6 +43,7 @@ export const settingsStore = new Store({
enableDiscord: false,
discord: {
showSong: true,
showIdle: true,
idleText: "Browsing Tidal",
usingText: "Playing media on TIDAL",
includeTimestamps: true,
@@ -64,6 +69,7 @@ export const settingsStore = new Store({
singleInstance: true,
skipArtists: false,
skippedArtists: [""],
staticWindowTitle: false,
theme: "none",
trayIcon: true,
updateFrequency: 500,
@@ -101,6 +107,19 @@ 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" }]);
},
},
});
@@ -134,6 +153,10 @@ export const createSettingsWindow = function () {
settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`);
settingsWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: "deny" };
});
settingsModule.settingsWindow = settingsWindow;
};
@@ -155,3 +178,25 @@ export const hideSettingsWindow = function () {
export const closeSettingsWindow = function () {
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,6 +8,7 @@
"sourceMap": true,
"allowJs": true,
"outDir": "ts-dist",
"resolveJsonModule": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {