Compare commits

..

255 Commits

Author SHA1 Message Date
40907f7b7f changed mediaSessionController to API Controller 2025-09-01 13:08:42 +02:00
9cbb55fae7 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/tidalControllers 2025-09-01 11:33:40 +02:00
8d4dd1ec9d Merge pull request #672 from Mastermindzh/renovate/sass-1.x
fix(deps): update sass to 1.91.0
2025-08-26 09:15:51 +02:00
d012ecb04e Merge pull request #671 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.41.0
2025-08-26 09:15:34 +02:00
Renovate Bot
fa4b4da9a5 fix(deps): update sass to 1.91.0 2025-08-26 01:30:43 +02:00
Renovate Bot
8f1a5dc344 chore(deps): update typescript-eslint monorepo to 8.41.0 2025-08-26 01:30:30 +02:00
604f34b31f Merge pull request #670 from Mastermindzh/next
electron update to 37.2.5
2025-08-25 12:11:33 +02:00
3ff37f78e8 version 5.20.1 2025-08-25 11:42:55 +02:00
8eaca2221f Merge pull request #669 from Mastermindzh/renovate/eslint-monorepo
chore(deps): update eslint to 9.34.0
2025-08-25 11:13:10 +02:00
8e9e9fed4f electron update to 37.2.5 2025-08-25 11:06:41 +02:00
Renovate Bot
d839dba58d chore(deps): update eslint to 9.34.0 2025-08-25 09:31:58 +02:00
bbbd919655 Merge pull request #645 from Mastermindzh/renovate/font-awesome-7.x
chore(deps): update font-awesome to 7.0.0
2025-08-25 09:14:16 +02:00
d4104b21e4 Merge pull request #658 from Mastermindzh/renovate/typescript-5.x-lockfile
chore(deps): update typescript to 5.9.2
2025-08-25 09:13:42 +02:00
6a23873d3d Merge pull request #659 from Mastermindzh/renovate/eslint-monorepo
chore(deps): update eslint to 9.33.0
2025-08-25 09:11:04 +02:00
8d6f08a7bb Merge pull request #662 from Mastermindzh/snyk-upgrade-5cdb32eaab1d0704488b8d5ad4e3ff42
[Snyk] Upgrade @xhayper/discord-rpc from 1.2.1 to 1.3.0
2025-08-25 09:06:49 +02:00
521b8b2170 Merge pull request #660 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.40.0
2025-08-25 09:05:06 +02:00
Renovate Bot
99e4cb7f48 chore(deps): update typescript-eslint monorepo to 8.40.0 2025-08-18 20:01:08 +02:00
snyk-bot
4cd130ee0d fix: upgrade @xhayper/discord-rpc from 1.2.1 to 1.3.0
Snyk has created this PR to upgrade @xhayper/discord-rpc from 1.2.1 to 1.3.0.

See this package in npm:
@xhayper/discord-rpc

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
2025-08-16 09:19:45 +00:00
Renovate Bot
a2a2c800b6 chore(deps): update eslint to 9.33.0 2025-08-09 06:31:41 +02:00
Renovate Bot
83210f9e1d chore(deps): update typescript to 5.9.2 2025-08-08 12:47:13 +02:00
5d40d2d736 Merge pull request #654 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.39.0
2025-08-08 10:03:47 +02:00
405c2d7c9f Merge pull request #657 from Mastermindzh/renovate/stylelint-16.x-lockfile
chore(deps): update stylelint to 16.23.1
2025-08-08 09:58:46 +02:00
Renovate Bot
9f860d2eec chore(deps): update stylelint to 16.23.1 2025-08-07 20:39:11 +02:00
Renovate Bot
86487863f8 chore(deps): update typescript-eslint monorepo to 8.39.0 2025-08-06 12:42:07 +02:00
3312211df2 Merge pull request #656 from Mastermindzh/renovate/sass-1.x
fix(deps): update sass to 1.90.0
2025-08-06 11:04:03 +02:00
Renovate Bot
a84bb9efde fix(deps): update sass to 1.90.0 2025-08-06 04:43:00 +02:00
31c37b6c7f Merge pull request #650 from Mastermindzh/renovate/stylelint-16.x-lockfile
chore(deps): update stylelint to 16.23.0
2025-07-30 13:23:38 +02:00
be9724f0cb Merge pull request #651 from Mastermindzh/renovate/stylelint-config-standard-39.x
chore(deps): update stylelint-config-standard to 39.0.0
2025-07-30 13:23:14 +02:00
Renovate Bot
7277482c06 chore(deps): update stylelint-config-standard to 39.0.0 2025-07-29 20:42:09 +02:00
Renovate Bot
323b591f4f chore(deps): update stylelint to 16.23.0 2025-07-29 20:41:47 +02:00
c63b46ee06 Merge pull request #649 from Mastermindzh/renovate/request-2.x-lockfile
chore(deps): update @types/request to 2.48.13
2025-07-29 10:05:43 +02:00
Renovate Bot
b4a8e155af chore(deps): update @types/request to 2.48.13 2025-07-29 04:43:02 +02:00
2702d99aca Merge pull request #648 from Mastermindzh/renovate/eslint-monorepo
chore(deps): update eslint to 9.32.0
2025-07-28 09:39:07 +02:00
Renovate Bot
7cf9ae5c36 chore(deps): update eslint to 9.32.0 2025-07-25 20:56:13 +02:00
3a1402b47a Merge pull request #647 from Mastermindzh/renovate/npm-axios-vulnerability
fix(deps): update axios to 1.11.0 [security]
2025-07-24 10:22:05 +02:00
Renovate Bot
4366caa16a fix(deps): update axios to 1.11.0 [security] 2025-07-23 20:52:01 +02:00
Renovate Bot
9389b4195e chore(deps): update font-awesome to 7.0.0 2025-07-22 20:35:28 +02:00
87d8bc07ad Merge pull request #644 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.38.0
2025-07-22 01:40:04 +02:00
Renovate Bot
316bf054f5 chore(deps): update typescript-eslint monorepo to 8.38.0 2025-07-21 20:47:06 +02:00
0d4975ce62 Merge pull request #641 from Mastermindzh/renovate/stylelint-16.x-lockfile
chore(deps): update stylelint to 16.22.0
2025-07-18 13:52:11 +02:00
Renovate Bot
912f064fed chore(deps): update stylelint to 16.22.0 2025-07-18 13:18:24 +02:00
bff95bcf10 Merge pull request #639 from Mastermindzh/renovate/mastermindzh-prettier-config-1.x-lockfile
chore(deps): update @mastermindzh/prettier-config to 1.1.0
2025-07-16 00:49:23 +02:00
b59dce9c9b Merge pull request #640 from Mastermindzh/renovate/electron-35.x
chore(deps): update electron to v35.7.2+wvcus
2025-07-16 00:49:14 +02:00
Renovate Bot
f71fb60d38 chore(deps): update electron to v35.7.2+wvcus 2025-07-15 22:35:31 +02:00
Renovate Bot
fab566e9da chore(deps): update @mastermindzh/prettier-config to 1.1.0 2025-07-15 14:50:26 +02:00
d2e93e7062 Merge pull request #638 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.37.0
2025-07-15 09:23:16 +02:00
Renovate Bot
52a78a6a01 chore(deps): update typescript-eslint monorepo to 8.37.0 2025-07-14 19:46:27 +02:00
f878371fab Merge pull request #634 from Mastermindzh/renovate/eslint-monorepo
chore(deps): update eslint to 9.31.0
2025-07-14 10:51:12 +02:00
e81aaf1384 Merge pull request #635 from Mastermindzh/renovate/electron-35.x
chore(deps): update electron to v35.7.1+wvcus
2025-07-14 10:51:04 +02:00
Renovate Bot
98b4068b62 chore(deps): update electron to v35.7.1+wvcus 2025-07-12 01:50:08 +02:00
Renovate Bot
b7c1cee64d chore(deps): update eslint to 9.31.0 2025-07-11 22:50:18 +02:00
350b4434be Merge pull request #633 from Mastermindzh/next
Next
2025-07-10 14:57:19 +02:00
702a16357f chore: copilot review changes 2025-07-10 11:18:46 +02:00
c6619be068 chore: update version numbers 2025-07-10 11:11:57 +02:00
bf7cd70f20 updated some dev deps 2025-07-10 11:10:43 +02:00
03cb14e31e added nvmrc 2025-07-10 11:05:26 +02:00
697298da38 fix: updated Electron to 37, fixes #580 2025-07-10 10:56:58 +02:00
5f5b3b11a8 pulled in edocli PR and updated dependencies 2025-07-10 10:44:57 +02:00
d474b7b78f Merge pull request #632 from Mastermindzh/renovate/hotkeys-js-3.x-lockfile
fix(deps): update hotkeys-js to 3.13.15
2025-07-10 10:31:59 +02:00
ef374ea283 Merge pull request #631 from Mastermindzh/renovate/node-22.x-lockfile
chore(deps): update @types/node to 22.16.2
2025-07-10 10:31:44 +02:00
Renovate Bot
96ff46737b fix(deps): update hotkeys-js to 3.13.15 2025-07-09 11:52:31 +02:00
Renovate Bot
dee0044876 chore(deps): update @types/node to 22.16.2 2025-07-09 02:28:17 +02:00
3b1f456d4c Merge pull request #630 from Mastermindzh/renovate/node-22.x-lockfile
chore(deps): update @types/node to 22.16.1
2025-07-08 20:24:05 +02:00
Renovate Bot
f380839631 chore(deps): update @types/node to 22.16.1 2025-07-08 19:21:47 +02:00
fca5194873 Merge pull request #628 from Mastermindzh/renovate/electron-remote-2.x-lockfile
fix(deps): update @electron/remote to 2.1.3
2025-07-07 23:24:38 +02:00
Renovate Bot
92d1fc159d fix(deps): update @electron/remote to 2.1.3 2025-07-07 20:31:42 +02:00
b632c287b0 Merge pull request #622 from Mastermindzh/renovate/stylelint-config-standard-scss-15.x
chore(deps): update stylelint-config-standard-scss to 15.0.1
2025-07-07 20:27:40 +02:00
5a98b13e70 Merge pull request #627 from Mastermindzh/renovate/major-eslint-monorepo
chore(deps): update eslint to 9.30.1
2025-07-07 20:19:51 +02:00
3543253f3a Merge pull request #626 from Mastermindzh/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.36.0
2025-07-07 20:19:22 +02:00
Renovate Bot
fff8399c9b chore(deps): update stylelint-config-standard-scss to 15.0.1 2025-07-07 19:46:47 +02:00
Renovate Bot
78b9b32dbd chore(deps): update eslint to 9.30.1 2025-07-07 19:46:30 +02:00
Renovate Bot
c773e59432 chore(deps): update typescript-eslint monorepo to 8.36.0 2025-07-07 19:46:15 +02:00
535de65e17 Merge pull request #624 from Mastermindzh/renovate/major-typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to 8.36.0 (major)
2025-07-07 19:38:53 +02:00
b80599eab2 Merge pull request #623 from Mastermindzh/renovate/tsc-watch-7.x
chore(deps): update tsc-watch to 7.1.1
2025-07-07 19:32:21 +02:00
c161ef8f60 Merge pull request #621 from Mastermindzh/renovate/stylelint-config-standard-38.x
chore(deps): update stylelint-config-standard to 38.0.0
2025-07-07 19:02:14 +02:00
7ed160f266 Merge pull request #620 from Mastermindzh/renovate/node-22.x
chore(deps): update node.js to v22.17.0
2025-07-07 18:41:31 +02:00
b23975227f Merge pull request #619 from Mastermindzh/renovate/font-awesome-6.x
chore(deps): update font-awesome to 6.7.2
2025-07-07 18:37:38 +02:00
314e69dd4b Merge pull request #618 from Mastermindzh/renovate/major-eslint-monorepo
chore(deps): update eslint to 9.30.1
2025-07-07 18:37:18 +02:00
Renovate Bot
10251e2f30 chore(deps): update typescript-eslint monorepo to 8.35.1 2025-07-07 18:10:32 +02:00
Renovate Bot
cf06655008 chore(deps): update tsc-watch to 7.1.1 2025-07-07 18:10:17 +02:00
Renovate Bot
d5804ed4da chore(deps): update stylelint-config-standard to 38.0.0 2025-07-07 18:09:52 +02:00
Renovate Bot
76500ca6b8 chore(deps): update node.js to v22.17.0 2025-07-07 18:07:30 +02:00
Renovate Bot
f5c80e97c5 chore(deps): update font-awesome to 6.7.2 2025-07-07 18:05:39 +02:00
Renovate Bot
154698c084 chore(deps): update eslint to 9.30.1 2025-07-07 17:50:09 +02:00
9837e77768 Merge pull request #616 from Mastermindzh/renovate/axios-1.x-lockfile
fix(deps): update axios to 1.10.0
2025-07-07 16:25:54 +02:00
340bd82fe6 Merge pull request #615 from Mastermindzh/renovate/tsc-watch-6.x-lockfile
chore(deps): update tsc-watch to 6.3.1
2025-07-07 16:24:04 +02:00
b765ab9f48 Merge pull request #614 from Mastermindzh/renovate/stylelint-16.x-lockfile
chore(deps): update stylelint to 16.21.1
2025-07-07 16:23:28 +02:00
6562549897 Merge pull request #612 from Mastermindzh/renovate/prettier-3.x-lockfile
chore(deps): update prettier to 3.6.2
2025-07-07 16:23:02 +02:00
9089bc1d4e Merge pull request #607 from Mastermindzh/renovate/hotkeys-js-3.x-lockfile
fix(deps): update hotkeys-js to 3.13.14
2025-07-07 16:22:42 +02:00
5a3609676e Merge pull request #609 from Mastermindzh/renovate/node-20.x-lockfile
chore(deps): update @types/node to 20.19.4
2025-07-07 16:22:26 +02:00
76c3082d5c Merge pull request #605 from Mastermindzh/renovate/nodemon-3.x-lockfile
chore(deps): update nodemon to 3.1.10
2025-07-07 15:49:18 +02:00
7f7a8b7f5b Merge pull request #604 from Mastermindzh/renovate/express-5.x-lockfile
chore(deps): update @types/express to 5.0.3
2025-07-07 15:22:11 +02:00
Renovate Bot
2d49504818 fix(deps): update axios to 1.10.0 2025-07-07 14:32:14 +02:00
Renovate Bot
5f4f0c67d5 chore(deps): update tsc-watch to 6.3.1 2025-07-07 14:32:10 +02:00
Renovate Bot
3a5f1f155a chore(deps): update stylelint to 16.21.1 2025-07-07 14:32:06 +02:00
Renovate Bot
3c9328fb92 chore(deps): update prettier to 3.6.2 2025-07-07 14:32:02 +02:00
Renovate Bot
f262c54be2 chore(deps): update @types/node to 20.19.4 2025-07-07 14:31:58 +02:00
Renovate Bot
cb6e3e8b18 fix(deps): update hotkeys-js to 3.13.14 2025-07-07 14:31:54 +02:00
Renovate Bot
7f9b8dcee4 chore(deps): update nodemon to 3.1.10 2025-07-07 14:31:45 +02:00
Renovate Bot
99740f4335 chore(deps): update @types/express to 5.0.3 2025-07-07 14:31:41 +02:00
985bc0f1d5 Merge pull request #610 from Mastermindzh/renovate/electron-35.x
chore(deps): update electron to v35.7.0+wvcus
2025-07-07 14:31:22 +02:00
1f9b3a297a Merge pull request #611 from Mastermindzh/renovate/node-19.x
chore(deps): update node.js to v19.9.0
2025-07-07 14:28:41 +02:00
98ca73d17d Merge pull request #608 from Mastermindzh/renovate/sass-1.x
fix(deps): update sass to 1.89.2
2025-07-07 14:27:35 +02:00
396ed223d3 Merge pull request #603 from Mastermindzh/renovate/cors-2.x-lockfile
chore(deps): update @types/cors to 2.8.19
2025-07-07 13:27:08 +02:00
Renovate Bot
b0458f4de5 chore(deps): update node.js to v19.9.0 2025-07-07 13:09:34 +02:00
Renovate Bot
b875d091ec chore(deps): update electron to v35.7.0+wvcus 2025-07-07 13:09:30 +02:00
Renovate Bot
1a0b69d042 fix(deps): update sass to 1.89.2 2025-07-07 13:09:16 +02:00
Renovate Bot
e23a71a1a6 chore(deps): update @types/cors to 2.8.19 2025-07-07 13:08:49 +02:00
510ffa1509 Merge pull request #602 from Mastermindzh/renovate/configure
chore: Configure Renovate
2025-07-07 13:05:04 +02:00
Renovate Bot
e17ef32cde Add renovate.json 2025-07-07 12:00:56 +02:00
d4c3999d9c Merge pull request #593 from edocli/master
feat(build): Update Wayland launch flags for Linux
2025-07-07 09:11:38 +02:00
b0695600b1 Merge pull request #599 from Mastermindzh/snyk-upgrade-c222f288d19352ce1096b7bea79acbab
[Snyk] Upgrade hotkeys-js from 3.13.10 to 3.13.11
2025-07-07 09:10:17 +02:00
snyk-bot
22ecc7ade7 fix: upgrade hotkeys-js from 3.13.10 to 3.13.11
Snyk has created this PR to upgrade hotkeys-js from 3.13.10 to 3.13.11.

See this package in npm:
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
2025-07-05 06:52:09 +00:00
a5102ebd03 Merge pull request #596 from Mastermindzh/snyk-upgrade-8471eaf4e087a1ad71b342b81e6741ae
[Snyk] Upgrade sass from 1.89.0 to 1.89.1
2025-06-23 12:46:05 +02:00
snyk-bot
d01f08508f fix: upgrade sass from 1.89.0 to 1.89.1
Snyk has created this PR to upgrade sass from 1.89.0 to 1.89.1.

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
2025-06-21 09:48:24 +00:00
e87d8f6922 Merge pull request #594 from Mastermindzh/snyk-upgrade-2487864f695c4105f8d0392b9d406127
[Snyk] Upgrade sass from 1.87.0 to 1.89.0
2025-06-15 10:13:13 +02:00
snyk-bot
c044476014 fix: upgrade sass from 1.87.0 to 1.89.0
Snyk has created this PR to upgrade sass from 1.87.0 to 1.89.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
2025-06-14 11:15:21 +00:00
75df43864b Merge pull request #588 from Mastermindzh/snyk-upgrade-5a5f2fcd2f171f53dcc34ab5933a3c09
[Snyk] Upgrade sass from 1.86.3 to 1.87.0
2025-06-09 11:00:33 +03:00
Edoc
174d0a38a1 Update electron-builder.base.yml
Adds the `--use-angle` flag to work around driver compatibility issues that cause the NVIDIA GPU process to crash repeatedly on Wayland. The native EGL/GBM backend fails with "Cannot create bo" errors.
2025-06-09 03:09:37 -04:00
Edoc
cf1aeefe67 Update electron-builder.base.yml
- Remove obsolete --enable-features=UseOzonePlatform flag
- Add --enable-wayland-ime for IME support on Wayland
2025-06-08 18:46:01 -04:00
280d927a03 Merge pull request #590 from Mastermindzh/snyk-upgrade-218353a4201f743fe837fca092089385
[Snyk] Upgrade hotkeys-js from 3.13.9 to 3.13.10
2025-05-31 14:16:42 +02:00
snyk-bot
f62294ef20 fix: upgrade hotkeys-js from 3.13.9 to 3.13.10
Snyk has created this PR to upgrade hotkeys-js from 3.13.9 to 3.13.10.

See this package in npm:
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
2025-05-31 08:20:25 +00:00
237e354f80 Merge pull request #587 from Mastermindzh/snyk-upgrade-7cf34416a89cb7ffe31756c1f2c4b62d
[Snyk] Upgrade axios from 1.8.4 to 1.9.0
2025-05-26 09:46:38 +02:00
snyk-bot
3a5172cb23 fix: upgrade sass from 1.86.3 to 1.87.0
Snyk has created this PR to upgrade sass from 1.86.3 to 1.87.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
2025-05-24 08:12:39 +00:00
snyk-bot
f8f12cca62 fix: upgrade axios from 1.8.4 to 1.9.0
Snyk has created this PR to upgrade axios from 1.8.4 to 1.9.0.

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
2025-05-24 08:12:36 +00:00
c5082e546b Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/tidalControllers 2025-04-22 15:59:22 +02:00
79bbab6a82 Merge pull request #584 from Mastermindzh/next
albumInfo fix, updated dependencies
2025-04-22 11:40:41 +02:00
7376df7f30 albumInfo fix, updated dependencies 2025-04-22 11:14:19 +02:00
53e7f42c59 chore: giving up on state, reverted to MediaSessionController 2025-04-20 14:25:09 +02:00
7f8760c4e9 Merge pull request #583 from Mastermindzh/snyk-upgrade-36ee0aede4ca42df3db4bef87530e654
[Snyk] Upgrade sass from 1.85.1 to 1.86.0
2025-04-20 13:24:05 +02:00
4330994b32 chore: added debug possibilities 2025-04-20 13:04:01 +02:00
90d81d2178 feat: replace hardcoded selector with setting implements #543 2025-04-20 12:15:03 +02:00
snyk-bot
1cf5a01ad7 fix: upgrade sass from 1.85.1 to 1.86.0
Snyk has created this PR to upgrade sass from 1.85.1 to 1.86.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
2025-04-19 09:04:54 +00:00
6385375fd6 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/tidalControllers 2025-04-18 14:53:12 +02:00
8fc49f500e Merge pull request #577 from Mastermindzh/snyk-upgrade-b144be84df6292449bfe4eacd9662b97
[Snyk] Upgrade sass from 1.79.4 to 1.85.1
2025-04-18 14:50:52 +02:00
snyk-bot
49f5a01b58 fix: upgrade sass from 1.79.4 to 1.85.1
Snyk has created this PR to upgrade sass from 1.79.4 to 1.85.1.

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
2025-04-05 07:45:32 +00:00
48f4fe47ef Merge pull request #573 from Mastermindzh/next
Next
2025-03-30 13:10:28 +02:00
a819e1eb45 removing mac/win builds, they aren't targetted anyway 2025-03-30 12:52:48 +02:00
57e6342b5f Merge branch 'next' of github.com:Mastermindzh/tidal-hifi into next 2025-03-30 12:50:31 +02:00
58a543a3c8 fixed .desktop file 2025-03-30 12:50:27 +02:00
1c915d99fe Merge pull request #572 from QF0xB/fix/nix-build-temporarily
Reverted sass to 1.79.4
2025-03-30 12:49:29 +02:00
Quirin Brändli
8994360415 Reverted sass to 1.79.4 2025-03-30 00:06:46 +01:00
a5c0d9e6e8 Merge pull request #570 from Mastermindzh/fix/login
Fix/login
2025-03-29 20:45:40 +01:00
f8eb36f4c7 bumped versions 2025-03-29 17:12:28 +01:00
5ca90c25d3 update builder to v35 as well 2025-03-29 17:05:17 +01:00
cf86969cfc updated builder, removed abstract-socket 2025-03-29 16:27:24 +01:00
afd05ae88b updated electron, deps, and moved remote calls after start 2025-03-29 15:58:28 +01:00
72c25dfdc1 Merge pull request #564 from Mastermindzh/snyk-fix-1995dd90a05e86ea7c2e4f0f7a284930
[Snyk] Security upgrade @xhayper/discord-rpc from 1.2.0 to 1.2.1
2025-03-23 21:32:20 +01:00
snyk-bot
25b30edf31 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UNDICI-8641354
2025-03-22 08:54:58 +00:00
a08fad421a Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/tidalControllers 2025-03-18 10:55:07 +01:00
35ed4807e3 Merge pull request #558 from Mastermindzh/next
Next
2025-03-18 09:10:40 +01:00
deff9524a8 album & playingfrom info added to mediaInfo again. fixes #548 2025-03-17 11:05:10 +01:00
c975ecb16b incorporated next into this branch 2025-03-17 10:28:08 +01:00
34c7777eeb Merge pull request #554 from Dianoga/duration-fix
fix duration selector
2025-03-17 10:27:42 +01:00
048e949f30 Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into next 2025-03-17 09:54:06 +01:00
Brian Steere
4a976bc58c fix duration selector
Should fix the MPRIS issue as long as duration doesn't break again.
2025-03-12 14:21:13 -05:00
f553fe98e7 finished interface for TidalController and moved most domLogic to the domController 2025-03-09 14:47:30 +01:00
aed40ec4ef Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into feature/tidalControllers 2025-03-07 09:28:20 +01:00
ff4c51234b Merge pull request #524 from Mastermindzh/snyk-upgrade-278f4ec84db5f4ebc26d987909c6c73f
[Snyk] Upgrade hotkeys-js from 3.13.7 to 3.13.9
2025-01-06 17:48:43 +01:00
snyk-bot
c4ee6b51b9 fix: upgrade hotkeys-js from 3.13.7 to 3.13.9
Snyk has created this PR to upgrade hotkeys-js from 3.13.7 to 3.13.9.

See this package in npm:
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
2025-01-04 01:22:24 +00:00
45fe336598 Merge pull request #522 from Mastermindzh/snyk-upgrade-7f82ddec0efc65c72c6f7f2f182ace7c
[Snyk] Upgrade axios from 1.7.8 to 1.7.9
2024-12-28 12:45:52 +01:00
snyk-bot
fe9f50aaf5 fix: upgrade axios from 1.7.8 to 1.7.9
Snyk has created this PR to upgrade axios from 1.7.8 to 1.7.9.

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-12-27 23:17:27 +00:00
aaf7a1b662 Merge pull request #504 from darkiox/master
Fix Discord RPC Timestamp jitter
2024-12-09 11:21:27 +01:00
755be0ee30 Merge pull request #515 from Mastermindzh/snyk-fix-82857ea1745fc2cfbc5f06a87a8ff0b8
[Snyk] Security upgrade express from 4.21.1 to 4.21.2
2024-12-09 11:05:23 +01:00
snyk-bot
9d736b2bd9 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-PATHTOREGEXP-8482416
2024-12-06 20:33:54 +00:00
f4d4b1a1df Merge pull request #508 from Mastermindzh/snyk-fix-a6630fbdb3c8f0161d0b939cf0ec3ca3
[Snyk] Security upgrade axios from 1.7.7 to 1.7.8
2024-12-02 09:03:11 +01:00
snyk-bot
0c27c815f5 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-6671926
2024-11-30 06:57:38 +00:00
Darkiox
a3aa45a96b fix: discord rpc timestamp jitter 2024-11-25 19:15:26 -03:00
d3e126d86b chore: bump modules 2024-10-28 22:52:33 +01:00
d36bc7480f extracted yet more of the domControl into the new tidalController 2024-10-28 22:49:00 +01:00
f608d42747 rename to TidalController 2024-10-27 23:07:52 +01:00
286c7789c0 Merge branch 'develop' of github.com:Mastermindzh/tidal-hifi into feature/tidalControllers 2024-10-27 23:06:40 +01:00
d67f62c0dc Merge branch 'master' of github.com:Mastermindzh/tidal-hifi into develop 2024-10-27 23:06:20 +01:00
01c5ec7ebb PoC: separated tidal interface controller into separate controllers 2024-10-27 23:05:55 +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
48 changed files with 6341 additions and 2828 deletions

View File

@@ -4,12 +4,12 @@ name: default
steps:
- name: install
image: node:19.4.0
image: node:22.17.0
commands:
- npm install
- name: build_with_linux
image: node:19.4.0
image: node:22.17.0
commands:
- apt-get update && apt-get upgrade -y
- apt-get install -y libarchive-tools rpm

View File

@@ -21,26 +21,30 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
node-version: 22.12.0
- run: npm install
- run: npm run build
# - uses: actions/upload-artifact@master
# with:
# name: linux-builds
# path: dist/
build_on_mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
- run: npm install
- run: npm run build
# build_on_mac:
# runs-on: macos-latest
# steps:
# - uses: actions/checkout@master
# - uses: actions/setup-node@master
# with:
# node-version: 22.4
# - run: npm install
# - run: npm run build
build_on_win:
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 19
- run: npm install
- run: npm run build
# build_on_win:
# runs-on: windows-latest
# steps:
# - uses: actions/checkout@master
# - uses: actions/setup-node@master
# with:
# node-version: 22.4
# - run: npm install
# - run: npm run build

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
19.8.1
v22.12.0

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,

36
.vscode/launch.json vendored Normal file
View File

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

View File

@@ -2,6 +2,7 @@
"cSpell.words": [
"Brainz",
"Castlabs",
"Fi's",
"flac",
"Flatpak",
"geqnfr",
@@ -13,11 +14,12 @@
"rescrobbler",
"scrobble",
"scrobbling",
"Songwhip",
"trackid",
"tracklist",
"widevine",
"xesam"
"wvcus",
"xesam",
"xhayper"
],
"sonarlint.connectedMode.project": {
"connectionId": "public-sonarcloud",

View File

@@ -4,6 +4,80 @@ 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.20.1]
- Updated electron to 37.2.5
## [5.20.0]
- Removes the `--enable-features=UseOzonePlatform` flag, as the Ozone platform has been the default on Linux since Electron 28 and this flag is no longer necessary.
- Adds the `--enable-wayland-ime` flag to enable Input Method Editor (IME) support in Wayland environments, improving the input experience for CJK and other users.
- Updated various dependencies
- Updated Electron to 37, potentially fixing [#580](https://github.com/Mastermindzh/tidal-hifi/issues/580)
## [5.19.0]
- Fixed the issue where media updates would cease to work after album names can't be found
- Will simply report an empty string when it can't find the album
- Updated various dependencies
## [5.18.2]
- Reverted to sass 1.79.4 to fix `Nix` builds
- Changed electron-builder.base.yml to now generate the correct .desktop entries again
- Should fix flatpak build
## [5.18.1]
- Fixed the login bug
- Upgraded electron to 35.1.1
- Added Widevine/CDM info to startup
- delayed remote electron initializer
## [5.18.0]
- [Dianoga](https://github.com/Dianoga) fixed the duration selector, restoring mpris & partial API data.
- PR: #554
- Added `xesam:url` property to mpris metadata fixes [#506](https://github.com/Mastermindzh/tidal-hifi/issues/506)
## [5.17.0]
- Added an option to disable the dynamic title and set it to a static one, [#491](https://github.com/Mastermindzh/tidal-hifi/pull/491)
- Discord integration now says "Listening to" instead of "playing" [#488](https://github.com/Mastermindzh/tidal-hifi/pull/488) && [#454](https://github.com/Mastermindzh/tidal-hifi/pull/454)
- Fixed several element names in the dom scraper
- Removed the Songwhip (they shut down) integration and replaced it with TIDAL's universal link system
## [5.16.0]
- Fix issue #449 Discord RPC stuck on "Browsing Tidal".
- Fix issue #448 Add option to disable the discord rpc idle text
- Notifications are now send at the end of the update process, allowing other events to happen sooner.
## [5.15.0]
- Added all missing swagger/openApi info with the help of [Times-Z](https://github.com/Times-Z)
- Updated most dependency versions
- This includes Electron 31!
- Added a channel selector so we can now use Tidal's staging environment directly from the app
- implements [#437](https://github.com/Mastermindzh/tidal-hifi/issues/437)
## [5.14.1]
- Fixed `getAlbumName` not finding album name whilst on queue page
- Added all mediaInfo to mpris interface using the `custom:` prefix
## [5.14]
- Simplified `MediaInfo` & `Options` types
- Added `playingFrom` information to the info API
- also changed the way we update Album info since Playing From now shows the correct Album.
- API now allows you to set the `hostname` so you can control who can interact with the API.
- Reworked swagger generation hotfix to properly generate `swagger.json` during the compile step
- Might switch to tsoa in the future, idk yet.
- Added [Tidal Magazine](https://tidal.com/magazine/) integration (in the menubar or use `Ctrl + M`)
## [5.13.1]
- removed Swagger generation step in favor of pre-generated file.
@@ -75,10 +149,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`
@@ -99,10 +175,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added settings to customize the Discord rich presence information
- Discord settings are now also collapsible like the ListenBrainz ones are
- Restyled settings menu to include version number and useful links on the about page
![The new about page](./docs/images/new-about.png)
![The new about page](./docs/images/new-about.png)
- The ListenBrainz integration has been extended with a configurable (5 seconds by default) delay in song reporting so that it doesn't spam the API when you are cycling through songs.
- Custom CSS now also applies to settings window
![Tokyo Night theme on settings window](./docs/images/customcss-menu.png)
![Tokyo Night theme on settings window](./docs/images/customcss-menu.png)
## [5.6.0]

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)
@@ -48,8 +48,8 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- 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,7 +130,7 @@ 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)`
- `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.
@@ -152,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

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

6326
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
{
"name": "tidal-hifi",
"version": "5.13.1",
"version": "5.20.1",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js",
"scripts": {
"start": "electron --inspect=0.0.0.0:5858 .",
"start": "electron --inspect=0.0.0.0:5858 --remote-debugging-port=8315 --remote-allow-origins=* .",
"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,44 +41,45 @@
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT",
"dependencies": {
"@electron/remote": "^2.1.2",
"@electron/remote": "^2.1.3",
"@types/swagger-jsdoc": "^6.0.4",
"axios": "^1.6.8",
"@xhayper/discord-rpc": "1.3.0",
"axios": "^1.10.0",
"cors": "^2.8.5",
"discord-rpc": "^4.0.1",
"electron-store": "^8.2.0",
"express": "^4.19.2",
"hotkeys-js": "^3.13.7",
"express": "^5.1.0",
"hotkeys-js": "^3.13.15",
"mpris-service": "^2.1.2",
"request": "^2.88.2",
"sass": "^1.75.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0"
"sass": "1.91.0",
"swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0",
"@types/cors": "^2.8.17",
"@types/discord-rpc": "^4.0.8",
"@types/express": "^4.17.21",
"@types/node": "^20.12.12",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/node": "^22.16.2",
"@types/request": "^2.48.12",
"@types/swagger-ui-express": "^4.1.6",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/eslint-plugin": "^8.36.0",
"@typescript-eslint/parser": "^8.36.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": "github:castlabs/electron-releases#v37.2.5+wvcus",
"electron-builder": "~26.0.12",
"eslint": "^9.30.1",
"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",
"stylelint-prettier": "^5.0.0",
"tsc-watch": "^6.0.4",
"typescript": "^5.3.3"
"node-abi": "^4.12.0",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"stylelint": "^16.21.1",
"stylelint-config-standard": "^39.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"stylelint-prettier": "^5.0.3",
"swagger-jsdoc": "^6.2.8",
"ts-node": "^10.9.2",
"tsc-watch": "^7.1.1",
"typescript": "^5.8.3"
},
"prettier": "@mastermindzh/prettier-config"
}

4
renovate.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"ignoreDeps": ["@types/node", "electron-store", "@xhayper/discord-rpc"]
}

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,16 +16,19 @@
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"]',
duration: '*[class^=playbackControlsContainer] *[data-test="duration"]',
duration: '*[class^=_playbackControlsContainer] *[data-test="duration"]',
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"]',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,111 @@ import fs from "fs";
import { mediaInfo } from "../../../scripts/mediaInfo";
export const addCurrentInfo = (expressApp: Router) => {
expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists }));
/**
* @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);
};

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { BrowserWindow } from "electron";
import { Router } from "express";
import { globalEvents } from "../../../constants/globalEvents";
@@ -12,19 +11,148 @@ 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);

View File

@@ -1,7 +1,6 @@
import cors from "cors";
import { BrowserWindow, dialog } from "electron";
import express from "express";
import swaggerSpec from "./swagger.json";
import cors from "cors";
import swaggerUi from "swagger-ui-express";
import { settingsStore } from "../../scripts/settings";
import { settings } from "./../../constants/settings";
@@ -9,19 +8,25 @@ 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));
expressApp.get("/", (req, res) => {
res.send("Hello World!");
});
expressApp.get("/swagger.json", (req, res) => {
res.json(swaggerSpec);
});
// add features
addLegacyApi(expressApp, mainWindow);
@@ -29,7 +34,7 @@ export const startApi = (mainWindow: BrowserWindow) => {
addCurrentInfo(expressApp);
addSettingsAPI(expressApp, mainWindow);
const expressInstance = expressApp.listen(port, "127.0.0.1");
const expressInstance = expressApp.listen(port, hostname);
expressInstance.on("error", function (e: { code: string }) {
let message = e.code;
if (e.code === "EADDRINUSE") {

View File

@@ -13,19 +13,132 @@ import { getCurrentImage } from "./features/current";
* @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);

View File

@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "TIDAL Hi-Fi API",
"version": "5.13.1",
"version": "5.20.1",
"description": "",
"license": {
"name": "MIT",
@@ -13,16 +13,215 @@
"url": "https://www.rickvanlieshout.com"
}
},
"servers": [
{
"url": "http://localhost:47836"
}
],
"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",
@@ -110,10 +309,250 @@
}
}
}
},
"/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": {
@@ -127,6 +566,14 @@
}
},
"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

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

View File

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

15
src/features/tidal/url.ts Normal file
View File

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

View File

@@ -10,7 +10,7 @@ import {
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";
@@ -24,22 +24,24 @@ import {
showSettingsWindow,
} from "./scripts/settings";
import { addTray, refreshTray } from "./scripts/tray";
const tidalUrl = "https://listen.tidal.com";
let mainInhibitorId = -1;
initialize();
let mainWindow: BrowserWindow;
const icon = path.join(__dirname, "../assets/icon.png");
const PROTOCOL_PREFIX = "tidal";
const windowPreferences = {
sandbox: false,
plugins: true,
devTools: true, // I like tinkering, others might too
devTools: true, // Ensure devTools is enabled for debugging
contextIsolation: false, // Disable context isolation for debugging
};
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
*
@@ -96,6 +98,7 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
},
},
});
enable(mainWindow.webContents);
registerHttpProtocols();
syncMenuBarWithStore();
@@ -124,6 +127,7 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
}
return false;
});
// Emitted when the window is closed.
mainWindow.on("closed", function () {
releaseInhibitorIfActive(mainInhibitorId);
@@ -176,6 +180,7 @@ app.on("ready", async () => {
if (isMainInstance() || isMultipleInstancesAllowed()) {
await components.whenReady();
initialize();
// Adblock
if (settingsStore.get(settings.adBlock)) {
@@ -186,6 +191,8 @@ app.on("ready", async () => {
});
}
Logger.log("components ready:", components.status());
createWindow();
addMenu(mainWindow);
createSettingsWindow();
@@ -248,8 +255,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,13 +1,16 @@
import { MediaStatus } from "./mediaStatus";
import { MediaPlayerInfo } from "./mediaPlayerInfo";
import { MediaStatus } from "./mediaStatus";
import { RepeatState } from "./repeatState";
export interface MediaInfo {
title: string;
artists: string;
artistsArray?: string[];
album: string;
icon: string;
status: MediaStatus;
url: string;
playingFrom: string;
current: string;
currentInSeconds?: number;
duration: string;
@@ -16,3 +19,30 @@ export interface MediaInfo {
favorite: boolean;
player?: MediaPlayerInfo;
}
export const getEmptyMediaInfo = () => {
const emptyState: MediaInfo = {
title: "",
artists: "",
artistsArray: [],
album: "",
playingFrom: "",
status: MediaStatus.playing,
url: "",
current: "00:00",
currentInSeconds: 100,
duration: "00:00",
durationInSeconds: 100,
image: "",
icon: "",
favorite: true,
player: {
status: MediaStatus.playing,
shuffle: true,
repeat: RepeatState.all,
},
};
return emptyState;
};

View File

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

View File

@@ -24,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,8 +59,10 @@ 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;
discord_using_text: HTMLInputElement,
controllerType: HTMLSelectElement;
addCustomCss(app);
@@ -120,6 +125,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 +133,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 +142,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,8 +155,10 @@ 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);
controllerType.value = settingsStore.get(settings.advanced.controllerType);
// set state of all switches with additional settings
Object.values(switchesWithSettings).forEach((settingSwitch) => {
@@ -236,6 +246,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 +254,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 +265,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 +276,15 @@ 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");
controllerType = get<HTMLSelectElement>("controllerType");
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 +292,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 +301,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 +317,13 @@ 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);
addSelectListener(controllerType, settings.advanced.controllerType);
});

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
@@ -7,7 +7,7 @@
<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">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/font-awesome.min.css">
</head>
<body class="settings-window">
@@ -49,7 +49,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" />
@@ -106,6 +106,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>
@@ -147,8 +160,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 +180,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,6 +240,16 @@
</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">
@@ -228,15 +262,17 @@
<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>
<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 +281,6 @@
</div>
<div id="discord_show_song_options" class="hidden">
<div class="group__option" class="hidden">
<div class="group__description">
<h4>Include timestamps</h4>
@@ -268,12 +303,11 @@
<div class="group__option">
<div class="group__description">
<h4>Button text</h4>
<p>Text to display on the button below the song information.</p>
<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,7 +326,10 @@
<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>
<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>
@@ -306,8 +343,10 @@
</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>
<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>
@@ -320,14 +359,39 @@
<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" />
</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 class="group__option">
<div class="group__description">
<h4>Controller Type</h4>
<p>Select the type of controller to use.</p>
<select id="controllerType" class="select-input">
<option value="domController">DOM Controller</option>
<option value="tidalApiController">Tidal Api Controller (beta)</option>
</select>
</div>
</div>
</div>
<div class="group">
<p class="group__title">Flags</p>
@@ -370,7 +434,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,7 +453,8 @@
<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>
@@ -403,9 +469,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 +477,14 @@
<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>
@@ -433,7 +497,7 @@
<h4>TIDAL Hi-Fi</h4>
<div class="about-section__version">
<a target="_blank" rel="noopener"
href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.13.1">5.13.1</a>
href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.20.1">5.20.1</a>
</div>
<div class="about-section__links">
<a target="_blank" rel="noopener" href="https://github.com/mastermindzh/tidal-hifi/"

View File

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

View File

@@ -1,4 +1,4 @@
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";
@@ -12,9 +12,13 @@ const clientId = "833617820704440341";
export let rpc: Client;
const ACTIVITY_LISTENING = 2;
const MAX_RETRIES = 5;
const RETRY_DELAY = 10000;
const observer = () => {
if (rpc) {
rpc.setActivity(getActivity());
updateActivity();
}
};
@@ -22,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 =
@@ -41,6 +55,7 @@ const getActivity = (): Presence => {
settingsStore.get<string, string>(settings.discord.usingText) ?? "Playing media on TIDAL";
}
}
return presence;
function getFromStore() {
@@ -54,18 +69,33 @@ const getActivity = (): Presence => {
return { includeTimestamps, detailsPrefix, buttonText };
}
/**
* Pad a string using spaces to at least 2 characters
* @param input string to pad with 2 characters
* @returns
*/
function pad(input: string): string {
return input.padEnd(2, " ");
}
function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) {
// 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}${mediaInfo.title}`;
presence.state = mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)";
presence.details = `${detailsPrefix}${title}`;
presence.state = artists ? artists : "unknown artist(s)";
presence.largeImageKey = mediaInfo.image;
if (mediaInfo.album) {
presence.largeImageText = mediaInfo.album;
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;
}
}
@@ -73,11 +103,31 @@ const getActivity = (): Presence => {
if (includeTimestamps) {
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.trunc((Date.now() + 500) / 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");
}
}
};
@@ -86,18 +136,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();
};
/**
@@ -105,7 +145,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

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

View File

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

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

@@ -28,9 +28,14 @@ const buildMigration = (
export const settingsStore = new Store({
defaults: {
adBlock: false,
advanced: {
tidalUrl: "https://listen.tidal.com",
controllerType: "domController",
},
api: true,
apiSettings: {
port: 47836,
hostname: "127.0.0.1",
},
customCSS: [],
disableBackgroundThrottle: true,
@@ -39,6 +44,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 +70,7 @@ export const settingsStore = new Store({
singleInstance: true,
skipArtists: false,
skippedArtists: [""],
staticWindowTitle: false,
theme: "none",
trayIcon: true,
updateFrequency: 500,
@@ -101,6 +108,24 @@ 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" }]);
},
"5.19.0": (migrationStore) => {
buildMigration("5.19.0", migrationStore, [
{ key: settings.advanced.controllerType, value: "domController" },
]);
},
},
});