From 3f8ead8a0503b92e77c9c0cd486429c2fdfcce35 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Thu, 16 May 2024 19:25:03 -0700 Subject: [PATCH] Refactor preload script --- package-lock.json | 602 ++---------------- package.json | 8 +- src/constants/globalEvents.ts | 3 +- src/constants/settings.ts | 2 +- src/constants/values.ts | 3 - src/features/api/features/current.ts | 21 +- src/features/api/features/player.ts | 19 +- src/features/api/helpers/handleWindowEvent.ts | 12 - src/features/api/legacy.ts | 10 +- src/features/listenbrainz/listenbrainz.ts | 140 ++-- src/features/state.ts | 29 + src/features/time/parse.ts | 14 - src/main.ts | 33 +- src/models/mediaInfo.ts | 16 - src/models/mediaStatus.ts | 4 - src/models/options.ts | 15 - src/models/tidalState.ts | 14 + src/pages/settings/preload.ts | 23 +- src/preload.ts | 601 ----------------- src/preload/index.ts | 15 + src/preload/integrations/hotkeys.ts | 96 +++ src/preload/integrations/ipc.ts | 35 + src/preload/integrations/listenbrainz.ts | 38 ++ src/preload/integrations/mpris.ts | 77 +++ src/preload/integrations/notifications.ts | 23 + src/preload/integrations/skipArtists.ts | 15 + src/preload/redux.ts | 188 ++++++ src/preload/state.ts | 155 +++++ src/scripts/discord.ts | 30 +- src/scripts/download.ts | 23 - src/scripts/mediaInfo.ts | 56 +- src/scripts/menu.ts | 4 +- src/scripts/settings.ts | 1 + src/scripts/window-functions.ts | 7 - src/types/mpris-service.d.ts | 121 ++-- tsconfig.json | 4 +- 36 files changed, 962 insertions(+), 1495 deletions(-) delete mode 100644 src/constants/values.ts delete mode 100644 src/features/api/helpers/handleWindowEvent.ts create mode 100644 src/features/state.ts delete mode 100644 src/features/time/parse.ts delete mode 100644 src/models/mediaInfo.ts delete mode 100644 src/models/mediaStatus.ts delete mode 100644 src/models/options.ts create mode 100644 src/models/tidalState.ts delete mode 100644 src/preload.ts create mode 100644 src/preload/index.ts create mode 100644 src/preload/integrations/hotkeys.ts create mode 100644 src/preload/integrations/ipc.ts create mode 100644 src/preload/integrations/listenbrainz.ts create mode 100644 src/preload/integrations/mpris.ts create mode 100644 src/preload/integrations/notifications.ts create mode 100644 src/preload/integrations/skipArtists.ts create mode 100644 src/preload/redux.ts create mode 100644 src/preload/state.ts delete mode 100644 src/scripts/download.ts delete mode 100644 src/scripts/window-functions.ts diff --git a/package-lock.json b/package-lock.json index 79084a3..d6a04b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,12 @@ "discord-rpc": "^4.0.1", "electron-store": "^8.2.0", "express": "^4.19.2", + "fast-deep-equal": "^3.1.3", "hotkeys-js": "^3.13.7", "mpris-service": "^2.1.2", "request": "^2.88.2", - "sass": "^1.75.0" + "sass": "^1.75.0", + "zustand": "^4.5.2" }, "devDependencies": { "@mastermindzh/prettier-config": "^1.0.0", @@ -31,8 +33,6 @@ "electron": "git+https://github.com/castlabs/electron-releases#v28.1.1+wvcus", "electron-builder": "^24.9.1", "eslint": "^8.56.0", - "js-yaml": "^4.1.0", - "markdown-toc": "^1.2.0", "nodemon": "^3.0.2", "prettier": "^3.1.1", "stylelint": "^16.1.0", @@ -1563,18 +1563,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", - "dev": true, - "dependencies": { - "ansi-wrap": "0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1599,15 +1587,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1867,15 +1846,6 @@ "node": ">=10.12.0" } }, - "node_modules/autolinker": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz", - "integrity": "sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==", - "dev": true, - "dependencies": { - "gulp-header": "^1.7.1" - } - }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -2397,20 +2367,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/coffee-script": { - "version": "1.12.7", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", - "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", - "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", - "dev": true, - "bin": { - "cake": "bin/cake", - "coffee": "bin/coffee" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2501,30 +2457,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-with-sourcemaps": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", - "dev": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, "node_modules/conf": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", @@ -2984,15 +2916,6 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "optional": true }, - "node_modules/diacritics-map": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz", - "integrity": "sha512-3omnDTYrGigU0i4cJjvaKwD52B8aoqyX/NEIkukFFkogBemsIbhSa1O414fpTp5nuszJG6lvQ5vBvDVNCbSsaQ==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/dir-compare": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", @@ -3666,19 +3589,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -3743,18 +3653,6 @@ "through": "~2.3.1" } }, - "node_modules/expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", - "dev": true, - "dependencies": { - "fill-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -3814,18 +3712,6 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -3968,22 +3854,6 @@ "node": ">=10" } }, - "node_modules/fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "dependencies": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -4069,15 +3939,6 @@ } } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -4497,62 +4358,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/gray-matter": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz", - "integrity": "sha512-vbmvP1Fe/fxuT2QuLVcqb2BfK7upGhhbLIt9/owWEvPYrZZEkelLcq2HqzxosV+PQ67dUFLaAeNpH7C4hhICAA==", - "dev": true, - "dependencies": { - "ansi-red": "^0.1.1", - "coffee-script": "^1.12.4", - "extend-shallow": "^2.0.1", - "js-yaml": "^3.8.1", - "toml": "^2.3.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/gray-matter/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/gulp-header": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", - "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", - "deprecated": "Removed event-stream from gulp-header", - "dev": true, - "dependencies": { - "concat-with-sourcemaps": "*", - "lodash.template": "^4.4.0", - "through2": "^2.0.0" - } - }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -4909,12 +4714,6 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -4941,15 +4740,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4978,18 +4768,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -5060,18 +4838,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -5138,8 +4904,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5260,36 +5025,12 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/known-css-properties": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", "dev": true }, - "node_modules/lazy-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", - "dev": true, - "dependencies": { - "set-getter": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -5328,21 +5069,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/list-item": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", - "integrity": "sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw==", - "dev": true, - "dependencies": { - "expand-range": "^1.8.1", - "extend-shallow": "^2.0.1", - "is-number": "^2.1.0", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5364,12 +5090,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", - "dev": true - }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -5404,25 +5124,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } - }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -5441,6 +5142,18 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -5465,41 +5178,6 @@ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" }, - "node_modules/markdown-link": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", - "integrity": "sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-toc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz", - "integrity": "sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==", - "dev": true, - "dependencies": { - "concat-stream": "^1.5.2", - "diacritics-map": "^0.1.0", - "gray-matter": "^2.1.0", - "lazy-cache": "^2.0.2", - "list-item": "^1.1.1", - "markdown-link": "^0.1.1", - "minimist": "^1.2.0", - "mixin-deep": "^1.1.3", - "object.pick": "^1.2.0", - "remarkable": "^1.7.1", - "repeat-string": "^1.6.1", - "strip-color": "^0.1.0" - }, - "bin": { - "markdown-toc": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -5512,12 +5190,6 @@ "node": ">=10" } }, - "node_modules/math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "dev": true - }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -5694,52 +5366,6 @@ "node": ">=8" } }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -6014,27 +5640,6 @@ "node": ">= 0.4" } }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick/node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6638,38 +6243,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "dev": true, - "dependencies": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/randomatic/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/randomatic/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6703,6 +6276,18 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-config-file": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", @@ -6802,55 +6387,6 @@ "node-addon-api": "^1.3.0" } }, - "node_modules/remarkable": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz", - "integrity": "sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==", - "dev": true, - "dependencies": { - "argparse": "^1.0.10", - "autolinker": "~0.28.0" - }, - "bin": { - "remarkable": "bin/remarkable.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/remarkable/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/remarkable/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -7215,18 +6751,6 @@ "node": ">= 0.4" } }, - "node_modules/set-getter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", - "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==", - "dev": true, - "dependencies": { - "to-object-path": "^0.3.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7498,15 +7022,6 @@ "node": ">=8" } }, - "node_modules/strip-color": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", - "integrity": "sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8020,18 +7535,6 @@ "tmp": "^0.2.0" } }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8059,12 +7562,6 @@ "node": ">=0.6" } }, - "node_modules/toml": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz", - "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==", - "dev": true - }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -8187,12 +7684,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -8250,6 +7741,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -8562,6 +8061,33 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index d5d33e0..44941ce 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,12 @@ "discord-rpc": "^4.0.1", "electron-store": "^8.2.0", "express": "^4.19.2", + "fast-deep-equal": "^3.1.3", "hotkeys-js": "^3.13.7", "mpris-service": "^2.1.2", "request": "^2.88.2", - "sass": "^1.75.0" + "sass": "^1.75.0", + "zustand": "^4.5.2" }, "devDependencies": { "@mastermindzh/prettier-config": "^1.0.0", @@ -61,8 +63,6 @@ "electron": "git+https://github.com/castlabs/electron-releases#v28.1.1+wvcus", "electron-builder": "^24.9.1", "eslint": "^8.56.0", - "js-yaml": "^4.1.0", - "markdown-toc": "^1.2.0", "nodemon": "^3.0.2", "prettier": "^3.1.1", "stylelint": "^16.1.0", @@ -73,4 +73,4 @@ "typescript": "^5.3.3" }, "prettier": "@mastermindzh/prettier-config" -} \ No newline at end of file +} diff --git a/src/constants/globalEvents.ts b/src/constants/globalEvents.ts index c08e12e..423eeb2 100644 --- a/src/constants/globalEvents.ts +++ b/src/constants/globalEvents.ts @@ -11,8 +11,9 @@ export const globalEvents = { storeChanged: "storeChanged", error: "error", whip: "whip", + downloadCover: "downloadCover", log: "log", toggleFavorite: "toggleFavorite", toggleShuffle: "toggleShuffle", toggleRepeat: "toggleRepeat", -}; +} as const; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index c435f76..610e3b9 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -57,4 +57,4 @@ export const settings = { width: "windowBounds.width", height: "windowBounds.height", }, -}; +} as const; diff --git a/src/constants/values.ts b/src/constants/values.ts deleted file mode 100644 index 57465a6..0000000 --- a/src/constants/values.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default { - name: "TIDAL Hi-Fi", -}; diff --git a/src/features/api/features/current.ts b/src/features/api/features/current.ts index 8569114..44cbf2b 100644 --- a/src/features/api/features/current.ts +++ b/src/features/api/features/current.ts @@ -1,20 +1,15 @@ import { Request, Response, Router } from "express"; -import fs from "fs"; -import { mediaInfo } from "../../../scripts/mediaInfo"; +import { getLegacyMediaInfo, mainTidalState } from "../../state"; export const addCurrentInfo = (expressApp: Router) => { - expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists })); + expressApp.get("/current", (_, res) => res.json(getLegacyMediaInfo())); expressApp.get("/current/image", getCurrentImage); }; -export const getCurrentImage = (req: Request, res: Response) => { - const stream = fs.createReadStream(mediaInfo.icon); - stream.on("open", function () { - res.set("Content-Type", "image/png"); - stream.pipe(res); - }); - stream.on("error", function () { - res.set("Content-Type", "text/plain"); - res.status(404).end("Not found"); - }); +export const getCurrentImage = (_: Request, res: Response) => { + if (!mainTidalState.currentTrack) { + res.sendStatus(404).end("No song is playing"); + return; + } + res.redirect(mainTidalState.currentTrack.image); }; diff --git a/src/features/api/features/player.ts b/src/features/api/features/player.ts index 05c9e56..9d1859c 100644 --- a/src/features/api/features/player.ts +++ b/src/features/api/features/player.ts @@ -3,17 +3,14 @@ import { BrowserWindow } from "electron"; import { Router } from "express"; import { globalEvents } from "../../../constants/globalEvents"; import { settings } from "../../../constants/settings"; -import { MediaStatus } from "../../../models/mediaStatus"; -import { mediaInfo } from "../../../scripts/mediaInfo"; import { settingsStore } from "../../../scripts/settings"; -import { handleWindowEvent } from "../helpers/handleWindowEvent"; export const addPlaybackControl = (expressApp: Router, mainWindow: BrowserWindow) => { - const windowEvent = handleWindowEvent(mainWindow); - const createRoute = (route: string) => `/player${route}`; - const createPlayerAction = (route: string, action: string) => { - expressApp.post(createRoute(route), (req, res) => windowEvent(res, action)); + expressApp.post(`/player${route}`, (_, res) => { + mainWindow.webContents.send("globalEvent", action); + res.sendStatus(200); + }); }; if (settingsStore.get(settings.playBackControl)) { @@ -25,12 +22,6 @@ export const addPlaybackControl = (expressApp: Router, mainWindow: BrowserWindow createPlayerAction("/shuffle/toggle", globalEvents.toggleShuffle); createPlayerAction("/repeat/toggle", globalEvents.toggleRepeat); - expressApp.post(createRoute("/playpause"), (req, res) => { - if (mediaInfo.status === MediaStatus.playing) { - windowEvent(res, globalEvents.pause); - } else { - windowEvent(res, globalEvents.play); - } - }); + createPlayerAction("/playpause", globalEvents.playPause); } }; diff --git a/src/features/api/helpers/handleWindowEvent.ts b/src/features/api/helpers/handleWindowEvent.ts deleted file mode 100644 index 907b143..0000000 --- a/src/features/api/helpers/handleWindowEvent.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BrowserWindow } from "electron"; -import { Response } from "express"; - -/** - * Shorthand to handle a fire and forget global event - * @param {*} res - * @param {*} action - */ -export const handleWindowEvent = (mainWindow: BrowserWindow) => (res: Response, action: string) => { - mainWindow.webContents.send("globalEvent", action); - res.sendStatus(200); -}; diff --git a/src/features/api/legacy.ts b/src/features/api/legacy.ts index 866013f..52b912f 100644 --- a/src/features/api/legacy.ts +++ b/src/features/api/legacy.ts @@ -2,8 +2,6 @@ import { BrowserWindow } from "electron"; import { Response, Router } from "express"; import { globalEvents } from "../../constants/globalEvents"; import { settings } from "../../constants/settings"; -import { MediaStatus } from "../../models/mediaStatus"; -import { mediaInfo } from "../../scripts/mediaInfo"; import { settingsStore } from "../../scripts/settings"; import { getCurrentImage } from "./features/current"; @@ -26,13 +24,7 @@ export const addLegacyApi = (expressApp: Router, mainWindow: BrowserWindow) => { expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause)); expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next)); expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous)); - expressApp.get("/playpause", (req, res) => { - if (mediaInfo.status === MediaStatus.playing) { - handleGlobalEvent(res, globalEvents.pause); - } else { - handleGlobalEvent(res, globalEvents.play); - } - }); + expressApp.get("/playpause", (req, res) => handleGlobalEvent(res, globalEvents.playPause)); } /** diff --git a/src/features/listenbrainz/listenbrainz.ts b/src/features/listenbrainz/listenbrainz.ts index ae6ecc0..7a0ea2e 100644 --- a/src/features/listenbrainz/listenbrainz.ts +++ b/src/features/listenbrainz/listenbrainz.ts @@ -1,7 +1,6 @@ import axios from "axios"; import Store from "electron-store"; import { settings } from "../../constants/settings"; -import { MediaStatus } from "../../models/mediaStatus"; import { settingsStore } from "../../scripts/settings"; import { Logger } from "../logger"; import { StoreData } from "./models/storeData"; @@ -43,84 +42,81 @@ export class ListenBrainz { duration: number ): Promise { try { - if (status === MediaStatus.paused) { - return; - } 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 playing_data = { - listen_type: "playing_now", - payload: [ - { - track_metadata: { - additional_info: { - media_player: "Tidal Hi-Fi", - submission_client: "Tidal Hi-Fi", - music_service: "tidal.com", - duration: duration, - }, - artist_name: artists, - track_name: title, - }, - }, - ], - }; - - await axios.post( - `${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, - playing_data, + if (status === "Paused") return; + // 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 playing_data = { + listen_type: "playing_now", + payload: [ { - headers: { - "Content-Type": "application/json", - Authorization: `Token ${settingsStore.get( - settings.ListenBrainz.token - )}`, + track_metadata: { + additional_info: { + media_player: "Tidal Hi-Fi", + submission_client: "Tidal Hi-Fi", + music_service: "tidal.com", + duration: duration, + }, + artist_name: artists, + track_name: title, }, - } + }, + ], + }; + + await axios.post( + `${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, + playing_data, + { + headers: { + "Content-Type": "application/json", + Authorization: `Token ${settingsStore.get( + settings.ListenBrainz.token + )}`, + }, + } + ); + if (!oldData) { + ListenBrainzStore.set( + ListenBrainzConstants.oldData, + this.constructStoreData(title, artists, duration) ); - if (!oldData) { + } else { + if (oldData.title !== title) { + // This constructs the data required to scrobble the data after the song finishes + const scrobble_data = { + listen_type: "single", + payload: [ + { + listened_at: oldData.listenedAt, + track_metadata: { + additional_info: { + media_player: "Tidal Hi-Fi", + submission_client: "Tidal Hi-Fi", + music_service: "listen.tidal.com", + duration: oldData.duration, + }, + artist_name: oldData.artists, + track_name: oldData.title, + }, + }, + ], + }; + await axios.post( + `${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, + scrobble_data, + { + headers: { + "Content-Type": "application/json", + Authorization: `Token ${settingsStore.get( + settings.ListenBrainz.token + )}`, + }, + } + ); ListenBrainzStore.set( ListenBrainzConstants.oldData, this.constructStoreData(title, artists, duration) ); - } else { - if (oldData.title !== title) { - // This constructs the data required to scrobble the data after the song finishes - const scrobble_data = { - listen_type: "single", - payload: [ - { - listened_at: oldData.listenedAt, - track_metadata: { - additional_info: { - media_player: "Tidal Hi-Fi", - submission_client: "Tidal Hi-Fi", - music_service: "listen.tidal.com", - duration: oldData.duration, - }, - artist_name: oldData.artists, - track_name: oldData.title, - }, - }, - ], - }; - await axios.post( - `${settingsStore.get(settings.ListenBrainz.api)}/1/submit-listens`, - scrobble_data, - { - headers: { - "Content-Type": "application/json", - Authorization: `Token ${settingsStore.get( - settings.ListenBrainz.token - )}`, - }, - } - ); - ListenBrainzStore.set( - ListenBrainzConstants.oldData, - this.constructStoreData(title, artists, duration) - ); - } } } } catch (error) { diff --git a/src/features/state.ts b/src/features/state.ts new file mode 100644 index 0000000..6a58251 --- /dev/null +++ b/src/features/state.ts @@ -0,0 +1,29 @@ +import { TidalState } from "../models/tidalState"; + +export const mainTidalState: TidalState = { + status: "Stopped", +}; + +export function getLegacyMediaInfo() { + function formatDuration(seconds: number) { + const minutes = Math.floor(seconds / 60); + const secondsLeft = seconds % 60; + return `${minutes}:${secondsLeft < 10 ? "0" : ""}${secondsLeft}`; + } + + return { + title: mainTidalState.currentTrack?.title ?? "", + artists: mainTidalState.currentTrack?.artists.join(", ") ?? "", + artist: mainTidalState.currentTrack?.artists.join(", ") ?? "", + album: mainTidalState.currentTrack?.album ?? "", + icon: mainTidalState.currentTrack?.image ?? "", + status: mainTidalState.status.toLowerCase(), + url: mainTidalState.currentTrack?.url ?? "", + current: formatDuration(mainTidalState.currentTrack?.current ?? 0), + currentInSeconds: mainTidalState.currentTrack?.current ?? 0, + duration: formatDuration(mainTidalState.currentTrack?.duration ?? 0), + durationInSeconds: mainTidalState.currentTrack?.duration ?? 0, + image: "tidal-hifi-icon", + favorite: false, + }; +} diff --git a/src/features/time/parse.ts b/src/features/time/parse.ts deleted file mode 100644 index 2fe17d5..0000000 --- a/src/features/time/parse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Convert a HH:MM:SS string (or variants such as MM:SS or SS) to plain seconds - * @param duration in HH:MM:SS format - * @returns number of seconds in duration - */ -export const convertDurationToSeconds = (duration: string) => { - return duration - .split(":") - .reverse() - .map((val) => Number(val)) - .reduce((previous, current, index) => { - return index === 0 ? current : previous + current * Math.pow(60, index); - }, 0); -}; diff --git a/src/main.ts b/src/main.ts index f0c0064..6b64c24 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,10 +11,7 @@ import { } from "./features/idleInhibitor/idleInhibitor"; import { Logger } from "./features/logger"; import { Songwhip } from "./features/songwhip/songwhip"; -import { MediaInfo } from "./models/mediaInfo"; -import { MediaStatus } from "./models/mediaStatus"; import { initRPC, rpc, unRPC } from "./scripts/discord"; -import { updateMediaInfo } from "./scripts/mediaInfo"; import { addMenu } from "./scripts/menu"; import { closeSettingsWindow, @@ -24,6 +21,10 @@ import { showSettingsWindow, } from "./scripts/settings"; import { addTray, refreshTray } from "./scripts/tray"; +import axios from "axios"; +import { existsSync, createWriteStream } from "fs"; +import { mainTidalState } from "./features/state"; +import { TidalState } from "./models/tidalState"; const tidalUrl = "https://listen.tidal.com"; let mainInhibitorId = -1; @@ -91,9 +92,8 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) { autoHideMenuBar: true, webPreferences: { ...windowPreferences, - ...{ - preload: path.join(__dirname, "preload.js"), - }, + preload: path.join(__dirname, "preload/index.js"), + contextIsolation: false, }, }); enable(mainWindow.webContents); @@ -213,9 +213,9 @@ app.on("browser-window-created", (_, window) => { }); // IPC -ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => { - updateMediaInfo(arg); - if (arg.status === MediaStatus.playing) { +ipcMain.on(globalEvents.updateInfo, (_event, arg: TidalState) => { + Object.assign(mainTidalState, arg); + if (arg.status === "Playing") { mainInhibitorId = acquireInhibitorIfInactive(mainInhibitorId); } else { releaseInhibitorIfActive(mainInhibitorId); @@ -248,8 +248,21 @@ ipcMain.on(globalEvents.error, (event) => { console.log(event); }); -ipcMain.handle(globalEvents.whip, async (event, url) => { +ipcMain.handle(globalEvents.whip, async (_, url) => { return Songwhip.whip(url); }); +ipcMain.handle(globalEvents.downloadCover, async (_, id, url) => { + const targetPath = `${app.getPath("userData")}/cover-${id}.jpg`; + if (existsSync(targetPath)) return targetPath; + const res = await axios.get(url, { + responseType: "stream", + }); + res.data.pipe(createWriteStream(targetPath)); + return new Promise((resolve, reject) => { + res.data.on("end", () => resolve(targetPath)); + res.data.on("error", reject); + }); +}); + Logger.watch(ipcMain); diff --git a/src/models/mediaInfo.ts b/src/models/mediaInfo.ts deleted file mode 100644 index 7a87760..0000000 --- a/src/models/mediaInfo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MediaStatus } from "./mediaStatus"; - -export interface MediaInfo { - title: string; - artists: string; - album: string; - icon: string; - status: MediaStatus; - url: string; - current: string; - currentInSeconds?: number; - duration: string; - durationInSeconds?: number; - image: string; - favorite: boolean; -} diff --git a/src/models/mediaStatus.ts b/src/models/mediaStatus.ts deleted file mode 100644 index 5d66392..0000000 --- a/src/models/mediaStatus.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MediaStatus { - playing = "playing", - paused = "paused", -} diff --git a/src/models/options.ts b/src/models/options.ts deleted file mode 100644 index 1aece34..0000000 --- a/src/models/options.ts +++ /dev/null @@ -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; -} diff --git a/src/models/tidalState.ts b/src/models/tidalState.ts new file mode 100644 index 0000000..bad091f --- /dev/null +++ b/src/models/tidalState.ts @@ -0,0 +1,14 @@ +export type TidalState = { + status: "Playing" | "Paused" | "Stopped"; + currentTrack?: { + id: number; + title: string; + // undefined for videos + album?: string; + artists: string[]; + current: number; + duration: number; + url: string; + image: string; + }; +}; diff --git a/src/pages/settings/preload.ts b/src/pages/settings/preload.ts index c8573e5..33c9a6f 100644 --- a/src/pages/settings/preload.ts +++ b/src/pages/settings/preload.ts @@ -24,8 +24,8 @@ const switchesWithSettings = { switch: "discord_show_song", classToHide: "discord_show_song_options", settingsKey: settings.discord.showSong, - } -}; + }, +} as const; let adBlock: HTMLInputElement, api: HTMLInputElement, @@ -138,7 +138,7 @@ function refreshSettings() { theme.value = settingsStore.get(settings.theme); skippedArtists.value = settingsStore.get(settings.skippedArtists).join("\n"); trayIcon.checked = settingsStore.get(settings.trayIcon); - updateFrequency.value = settingsStore.get(settings.updateFrequency); + updateFrequency.value = settingsStore.get(settings.updateFrequency).toString(); enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled); ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api); ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token); @@ -151,9 +151,12 @@ function refreshSettings() { discord_using_text.value = settingsStore.get(settings.discord.usingText); // set state of all switches with additional settings - Object.values(switchesWithSettings).forEach((settingSwitch) => { - setElementHidden(settingsStore.get(settingSwitch.settingsKey), settingSwitch); - }); + for (const settingSwitch of Object.values(switchesWithSettings)) { + setElementHidden( + settingsStore.get(settingSwitch.settingsKey as any) as boolean, + settingSwitch + ); + } } catch (error) { Logger.log("Refreshing settings failed.", error); } @@ -264,7 +267,7 @@ window.addEventListener("DOMContentLoaded", () => { discord_button_text = get("discord_button_text"); discord_show_song = get("discord_show_song"); discord_using_text = get("discord_using_text"); - discord_idle_text = get("discord_idle_text") + discord_idle_text = get("discord_idle_text"); refreshSettings(); addInputListener(adBlock, settings.adBlock); @@ -299,7 +302,11 @@ 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_idle_text, settings.discord.idleText); addInputListener(discord_using_text, settings.discord.usingText); }); diff --git a/src/preload.ts b/src/preload.ts deleted file mode 100644 index 4127a7a..0000000 --- a/src/preload.ts +++ /dev/null @@ -1,601 +0,0 @@ -import { app, dialog, Notification } from "@electron/remote"; -import { clipboard, ipcRenderer } from "electron"; -import Player from "mpris-service"; -import { globalEvents } from "./constants/globalEvents"; -import { settings } from "./constants/settings"; -import { - ListenBrainz, - ListenBrainzConstants, - ListenBrainzStore, -} 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 { convertDurationToSeconds } from "./features/time/parse"; -import { MediaStatus } from "./models/mediaStatus"; -import { Options } from "./models/options"; -import { downloadFile } from "./scripts/download"; -import { addHotkey } from "./scripts/hotkeys"; -import { settingsStore } from "./scripts/settings"; -import { setTitle } from "./scripts/window-functions"; - -const notificationPath = `${app.getPath("userData")}/notification.jpg`; -const appName = "TIDAL Hi-Fi"; -let currentSong = ""; -let player: Player; -let currentPlayStatus = MediaStatus.paused; -let currentListenBrainzDelayId: ReturnType; -let scrobbleWaitingForDelay = false; - -let currentlyPlaying = MediaStatus.paused; -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()]); - }, - - /** - * Get the icon of the current song - */ - 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 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(); - }, -}; - -/** - * Get the update frequency from the store - * make sure it returns a number, if not use the default - */ -function getUpdateFrequency() { - const storeValue = settingsStore.get(settings.updateFrequency); - const defaultValue = 500; - - if (!isNaN(storeValue)) { - return storeValue; - } else { - return defaultValue; - } -} - -/** - * 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 - */ -ListenBrainzStore.clear(); - -/** - * Add hotkeys for when tidal is focused - * Reflects the desktop hotkeys found on: - * https://defkey.com/tidal-desktop-shortcuts - */ -function addHotKeys() { - if (settingsStore.get(settings.enableCustomHotkeys)) { - addHotkey("Control+p", function () { - elements.click("account"); - setTimeout(() => { - elements.click("settings"); - }, 100); - }); - addHotkey("Control+l", function () { - handleLogout(); - }); - - addHotkey("Control+a", function () { - elements.click("favorite"); - }); - - addHotkey("Control+h", function () { - elements.click("home"); - }); - - addHotkey("backspace", function () { - elements.click("back"); - }); - - addHotkey("shift+backspace", function () { - elements.click("forward"); - }); - - addHotkey("control+u", function () { - // reloading window without cache should show the update bar if applicable - window.location.reload(); - }); - - addHotkey("control+r", function () { - elements.click("repeat"); - }); - addHotkey("control+w", async function () { - const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL()); - const url = Songwhip.getWhipUrl(result); - clipboard.writeText(url); - new Notification({ - title: `Successfully whipped: `, - body: `URL copied to clipboard: ${url}`, - }).show(); - }); - } - - // always add the hotkey for the settings window - addHotkey("control+=", function () { - ipcRenderer.send(globalEvents.showSettings); - }); - addHotkey("control+0", function () { - ipcRenderer.send(globalEvents.showSettings); - }); -} - -/** - * This function will ask the user whether he/she wants to log out. - * It will log the user out if he/she selects "yes" - */ -function handleLogout() { - const logoutOptions = ["Cancel", "Yes, please", "No, thanks"]; - - dialog - .showMessageBox(null, { - type: "question", - title: "Logging out", - message: "Are you sure you want to log out?", - buttons: logoutOptions, - defaultId: 2, - }) - .then((result: { response: number }) => { - if (logoutOptions.indexOf("Yes, please") === result.response) { - for (let i = 0; i < window.localStorage.length; i++) { - const key = window.localStorage.key(i); - if (key.startsWith("_TIDAL_activeSession")) { - window.localStorage.removeItem(key); - break; - } - } - window.location.reload(); - } - }); -} - -function addFullScreenListeners() { - window.document.addEventListener("fullscreenchange", () => { - ipcRenderer.send(globalEvents.refreshMenuBar); - }); -} - -/** - * Add ipc event listeners. - * Some actions triggered outside of the site need info from the site. - */ -function addIPCEventListeners() { - window.addEventListener("DOMContentLoaded", () => { - ipcRenderer.on("globalEvent", (_event, args) => { - switch (args) { - case globalEvents.playPause: - case globalEvents.play: - case globalEvents.pause: - playPause(); - break; - case globalEvents.next: - elements.click("next"); - break; - case globalEvents.previous: - elements.click("previous"); - break; - case globalEvents.toggleFavorite: - elements.click("favorite"); - break; - case globalEvents.toggleShuffle: - elements.click("shuffle"); - break; - case globalEvents.toggleRepeat: - elements.click("repeat"); - break; - default: - break; - } - }); - }); -} - -/** - * 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; -} - -/** - * 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 - */ -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(); - } - updateMpris(options); - updateListenBrainz(options); - } -} - -function addMPRIS() { - if (process.platform === "linux" && settingsStore.get(settings.mpris)) { - try { - player = Player({ - name: "tidal-hifi", - identity: "tidal-hifi", - supportedUriSchemes: ["file"], - supportedMimeTypes: [ - "audio/mpeg", - "audio/flac", - "audio/x-flac", - "application/ogg", - "audio/wav", - ], - supportedInterfaces: ["player"], - desktopEntry: "tidal-hifi", - }); - // Events - const events = { - next: "next", - previous: "previous", - pause: "pause", - playpause: "playpause", - stop: "stop", - play: "play", - loopStatus: "repeat", - shuffle: "shuffle", - seek: "seek", - } as { [key: string]: string }; - Object.keys(events).forEach(function (eventName) { - player.on(eventName, function () { - const eventValue = events[eventName]; - switch (events[eventValue]) { - case events.playpause: - playPause(); - break; - default: - elements.click(eventValue); - } - }); - }); - // Override get position function - player.getPosition = function () { - return convertDuration(elements.getText("current")) * 1000 * 1000; - }; - player.on("quit", function () { - app.quit(); - }); - } catch (exception) { - Logger.log("MPRIS player api not working", exception); - } - } -} - -function updateMpris(options: Options) { - 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(), - }, - }; - player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing"; - } -} - -/** - * Update the listenbrainz service with new data based on a few conditions - */ -function updateListenBrainz(options: Options) { - 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) - ) { - if (!scrobbleWaitingForDelay) { - scrobbleWaitingForDelay = true; - clearTimeout(currentListenBrainzDelayId); - currentListenBrainzDelayId = setTimeout( - () => { - ListenBrainz.scrobble( - options.title, - options.artists, - options.status, - convertDuration(options.duration) - ); - scrobbleWaitingForDelay = false; - }, - 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}`; -} - -function getTrackID() { - const URLelement = elements.get("title").querySelector("a"); - if (URLelement !== null) { - const id = URLelement.href.replace(/\D/g, ""); - return id; - } - - return window.location; -} - -/** - * 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 playStateChanged = currentStatus != currentlyPlaying; - - // update info if song changed or was just paused/resumed - if (titleOrArtistsChanged || playStateChanged) { - if (playStateChanged) { - currentlyPlaying = currentStatus; - } - 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(), - }; - - // update title, url and play info with new info - setTitle(songDashArtistTitle); - getTrackURL(); - currentSong = songDashArtistTitle; - currentPlayStatus = currentStatus; - - const image = elements.getSongIcon(); - - new Promise((resolve) => { - if (image.startsWith("http")) { - options.image = image; - downloadFile(image, notificationPath).then( - () => { - options.icon = notificationPath; - resolve(); - }, - () => { - // if the image can't be downloaded then continue without it - resolve(); - } - ); - } else { - // if the image can't be found on the page continue without it - resolve(); - } - }).then(() => { - updateMediaInfo(options, titleOrArtistsChanged); - }); - } else { - // just update the time - updateMediaInfo( - { ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } }, - false - ); - } - - /** - * automatically skip a song if the artists are found in the list of artists to skip - * @param {*} artists array of artists - */ - function skipArtistsIfFoundInSkippedArtistsList(artists: string[]) { - if (settingsStore.get(settings.skipArtists)) { - const skippedArtists = settingsStore.get(settings.skippedArtists); - if (skippedArtists.length > 0) { - const artistsToSkip = skippedArtists.map((artist) => artist); - const artistNames = Object.values(artists).map((artist) => artist); - const foundArtist = artistNames.some((artist) => artistsToSkip.includes(artist)); - if (foundArtist) { - elements.click("next"); - } - } - } - } -}, getUpdateFrequency()); - -addMPRIS(); -addCustomCss(app); -addHotKeys(); -addIPCEventListeners(); -addFullScreenListeners(); diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..3850e9d --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,15 @@ +import "./integrations/mpris"; +import "./integrations/listenbrainz"; +import "./integrations/hotkeys"; +import "./integrations/ipc"; +import "./integrations/notifications"; +import "./integrations/skipArtists"; +import { ipcRenderer } from "electron"; +import { globalEvents } from "../constants/globalEvents"; +import { addCustomCss } from "../features/theming/theming"; +import { app } from "@electron/remote"; + +window.document.addEventListener("fullscreenchange", () => { + ipcRenderer.send(globalEvents.refreshMenuBar); +}); +addCustomCss(app); diff --git a/src/preload/integrations/hotkeys.ts b/src/preload/integrations/hotkeys.ts new file mode 100644 index 0000000..16dce5a --- /dev/null +++ b/src/preload/integrations/hotkeys.ts @@ -0,0 +1,96 @@ +import { ipcRenderer, clipboard } from "electron"; +import { Notification, dialog } from "@electron/remote"; +import { addHotkey } from "../../scripts/hotkeys"; +import { globalEvents } from "../../constants/globalEvents"; +import { settingsStore } from "../../scripts/settings"; +import { settings } from "../../constants/settings"; +import { $tidalState, favoriteCurrentTrack, reduxStore, toggleRepeat } from "../state"; +import { Songwhip } from "../../features/songwhip/songwhip"; + +/** + * Add hotkeys for when tidal is focused + * Reflects the desktop hotkeys found on: + * https://defkey.com/tidal-desktop-shortcuts + */ +if (settingsStore.get(settings.enableCustomHotkeys)) { + addHotkey("Control+l", handleLogout); + + addHotkey("Control+a", favoriteCurrentTrack); + + addHotkey("Control+h", () => { + if (!reduxStore) return; + reduxStore.dispatch({ + type: "ROUTER_PUSH", + payload: { + pathname: "/", + options: {}, + hash: "", + }, + }); + }); + + addHotkey("backspace", () => { + if (!reduxStore) return; + reduxStore.dispatch({ type: "ROUTER_GO_BACK" }); + }); + + addHotkey("shift+backspace", () => { + if (!reduxStore) return; + reduxStore.dispatch({ type: "ROUTER_GO_FORWARD" }); + }); + + addHotkey("control+u", () => { + // reloading window without cache should show the update bar if applicable + window.location.reload(); + }); + + addHotkey("control+r", toggleRepeat); + addHotkey("control+w", async () => { + const trackUrl = $tidalState.getState().currentTrack?.url; + if (!trackUrl) return; + const result = await ipcRenderer.invoke(globalEvents.whip, trackUrl); + const url = Songwhip.getWhipUrl(result); + clipboard.writeText(url); + new Notification({ + title: `Successfully whipped: `, + body: `URL copied to clipboard: ${url}`, + }).show(); + }); +} + +// always add the hotkey for the settings window +addHotkey("control+=", function () { + ipcRenderer.send(globalEvents.showSettings); +}); +addHotkey("control+0", function () { + ipcRenderer.send(globalEvents.showSettings); +}); + +/** + * This function will ask the user whether he/she wants to log out. + * It will log the user out if he/she selects "yes" + */ +function handleLogout() { + const logoutOptions = ["Cancel", "Yes, please", "No, thanks"]; + + dialog + .showMessageBox(null, { + type: "question", + title: "Logging out", + message: "Are you sure you want to log out?", + buttons: logoutOptions, + defaultId: 2, + }) + .then((result: { response: number }) => { + if (logoutOptions.indexOf("Yes, please") === result.response) { + for (let i = 0; i < window.localStorage.length; i++) { + const key = window.localStorage.key(i); + if (key.startsWith("_TIDAL_activeSession")) { + window.localStorage.removeItem(key); + break; + } + } + window.location.reload(); + } + }); +} diff --git a/src/preload/integrations/ipc.ts b/src/preload/integrations/ipc.ts new file mode 100644 index 0000000..a45bc84 --- /dev/null +++ b/src/preload/integrations/ipc.ts @@ -0,0 +1,35 @@ +import { ipcRenderer } from "electron"; +import { globalEvents } from "../../constants/globalEvents"; +import { + $tidalState, + favoriteCurrentTrack, + next, + pause, + play, + playPause, + previous, + toggleRepeat, + toggleShuffle, +} from "../state"; + +/** + * Add ipc event listeners. + * Some actions triggered outside of the site need info from the site. + */ +const handlers: Partial void>> = { + [globalEvents.playPause]: playPause, + [globalEvents.play]: play, + [globalEvents.pause]: pause, + [globalEvents.next]: next, + [globalEvents.previous]: previous, + [globalEvents.toggleFavorite]: favoriteCurrentTrack, + [globalEvents.toggleShuffle]: toggleShuffle, + [globalEvents.toggleRepeat]: toggleRepeat, +}; +ipcRenderer.on("globalEvent", (_, event) => { + handlers[event as keyof typeof globalEvents]?.(); +}); + +$tidalState.subscribe((state) => { + ipcRenderer.send(globalEvents.updateInfo, state); +}); diff --git a/src/preload/integrations/listenbrainz.ts b/src/preload/integrations/listenbrainz.ts new file mode 100644 index 0000000..ea9ac7b --- /dev/null +++ b/src/preload/integrations/listenbrainz.ts @@ -0,0 +1,38 @@ +import { settingsStore } from "../../scripts/settings"; +import { + ListenBrainz, + ListenBrainzConstants, + ListenBrainzStore, +} from "../../features/listenbrainz/listenbrainz"; +import { settings } from "../../constants/settings"; +import { StoreData } from "../../features/listenbrainz/models/storeData"; +import { $tidalState } from "../state"; + +ListenBrainzStore.clear(); + +let delayTimeout: ReturnType | null = null; + +$tidalState.subscribe((state) => { + if (!settingsStore.get(settings.ListenBrainz.enabled)) return; + if (delayTimeout !== null) return; + + const track = state.currentTrack; + if (!track) return; + + const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData; + if ((!oldData && state.status === "Playing") || (oldData && oldData.title !== track.title)) { + clearTimeout(delayTimeout); + delayTimeout = setTimeout( + async () => { + await ListenBrainz.scrobble( + track.title, + track.artists.join(), + state.status, + track.duration + ); + delayTimeout = null; + }, + settingsStore.get(settings.ListenBrainz.delay) ?? 0 + ); + } +}); diff --git a/src/preload/integrations/mpris.ts b/src/preload/integrations/mpris.ts new file mode 100644 index 0000000..8366bfe --- /dev/null +++ b/src/preload/integrations/mpris.ts @@ -0,0 +1,77 @@ +import Player from "mpris-service"; +import { settings } from "../../constants/settings"; +import { settingsStore } from "../../scripts/settings"; +import { Logger } from "../../features/logger"; +import { + $tidalState, + coverArtPaths, + next, + pause, + play, + playPause, + previous, + stop, + toggleRepeat, + toggleShuffle, +} from "../state"; +import { app } from "@electron/remote"; + +function toMicroseconds(seconds: number) { + return BigInt(seconds) * 1000_000n; +} + +if (settingsStore.get(settings.mpris) && process.platform === "linux") { + try { + const player = Player({ + name: "tidal-hifi2", + identity: "tidal-hifi2", + supportedUriSchemes: ["file"], + supportedMimeTypes: [ + "audio/mpeg", + "audio/flac", + "audio/x-flac", + "application/ogg", + "audio/wav", + ], + supportedInterfaces: ["player"], + desktopEntry: "tidal-hifi2", + }); + player.on("playPause", playPause); + player.on("next", next); + player.on("previous", previous); + player.on("pause", pause); + player.on("play", play); + player.on("stop", stop); + player.on("loopStatus", toggleRepeat); + player.on("shuffle", toggleShuffle); + player.on("quit", app.quit); + + player.getPosition = function () { + return toMicroseconds($tidalState.getState().currentTrack?.current ?? 0); + }; + + $tidalState.subscribe(async (state) => { + if (!player) return; + + if (state.currentTrack) { + const coverUrl = await coverArtPaths.get(state.currentTrack.image); + player.metadata = { + "xesam:title": state.currentTrack.title, + "xesam:artist": state.currentTrack.artists, + "xesam:album": state.currentTrack.album, + "mpris:artUrl": coverUrl, + "mpris:length": toMicroseconds(state.currentTrack.duration), + "mpris:trackid": "/org/mpris/MediaPlayer2/track/" + state.currentTrack.id, + }; + } else { + player.metadata = { + "mpris:trackid": "/org/mpris/MediaPlayer2/TrackList/NoTrack", + }; + } + player.playbackStatus = state.status; + }); + } catch (exception) { + console.error(exception); + Logger.log("MPRIS player api not working", exception); + } +} diff --git a/src/preload/integrations/notifications.ts b/src/preload/integrations/notifications.ts new file mode 100644 index 0000000..364364b --- /dev/null +++ b/src/preload/integrations/notifications.ts @@ -0,0 +1,23 @@ +import { settingsStore } from "../../scripts/settings"; +import { $tidalState, coverArtPaths } from "../state"; +import { settings } from "../../constants/settings"; +import { Notification } from "@electron/remote"; + +let currentNotification: Electron.Notification | undefined; + +$tidalState.subscribe(async (state, prevState) => { + if (!settingsStore.get(settings.notifications)) return; + if (!state.currentTrack) return; + + if (state.currentTrack.id === prevState.currentTrack?.id) return; + + currentNotification?.close(); + if (state.status !== "Playing") return; + const icon = await coverArtPaths.get(state.currentTrack.image); + currentNotification = new Notification({ + title: state.currentTrack.title, + body: state.currentTrack.artists.join(", "), + icon, + }); + currentNotification.show(); +}); diff --git a/src/preload/integrations/skipArtists.ts b/src/preload/integrations/skipArtists.ts new file mode 100644 index 0000000..d245b20 --- /dev/null +++ b/src/preload/integrations/skipArtists.ts @@ -0,0 +1,15 @@ +import { settingsStore } from "../../scripts/settings"; +import { $tidalState, next } from "../state"; +import { settings } from "../../constants/settings"; + +$tidalState.subscribe((state) => { + // don't skip when paused, as it can cause a loop + if (!state.currentTrack || state.status !== "Playing") return; + if (!settingsStore.get(settings.skipArtists)) return; + const artistsToSkip = settingsStore.get(settings.skippedArtists) as string[]; + if (artistsToSkip.length === 0) return; + + const shouldSkip = state.currentTrack?.artists.some((artist) => artistsToSkip.includes(artist)); + + if (shouldSkip) next(); +}); diff --git a/src/preload/redux.ts b/src/preload/redux.ts new file mode 100644 index 0000000..cf5d2ae --- /dev/null +++ b/src/preload/redux.ts @@ -0,0 +1,188 @@ +export function getTidalReduxStore() { + // Find the react container + let reactContainer: Record | null = null; + for (const child of document.body?.children ?? []) { + const container = Object.entries(child).find(([key]) => key.startsWith("__reactContainer$")); + // console.log(container); + if (!container) continue; + reactContainer = container[1]; + break; + } + if (!reactContainer) { + throw new Error("Could not find React root"); + } + // Traverse the react tree until we find the redux store + const seen = new Set(); + const queue = [reactContainer]; + let store; + + const properties = ["children", "child", "pendingProps", "memoizedProps", "props"]; + while (!store && queue.length) { + const node = queue.shift(); + if (!node) break; + if ( + "store" in node && + typeof node.store === "object" && + node.store !== null && + "getState" in node.store && + typeof node.store.getState === "function" + ) { + store = node.store; + break; + } + for (const property of properties) { + const value = node[property]; + if (typeof value === "object" && value !== null) { + if (seen.has(value)) continue; + seen.add(value); + queue.push(value as Record); + } + } + } + if (!store) throw new Error("Could not find Redux store"); + return store as TidalReduxStore; +} + +export type TidalReduxStore = { + getState: () => ReduxState; + dispatch: (action: Action) => void; + subscribe: (listener: () => void) => () => void; +}; + +export type ReduxState = { + [key: string]: unknown; + content: { + mediaItems: Record; + }; + favorites: { + albums: number[]; + artists: number[]; + mixes: number[]; + playlists: number[]; + tracks: number[]; + users: number[]; + videos: number[]; + }; + playbackControls: { + desiredPlaybackState: "NOT_PLAYING" | "PLAYING" | "IDLE" | string; + latestCurrentTime: number; + latestCurrentTimeSyncTimestamp: number; + muted: boolean; + playbackState: "NOT_PLAYING" | "PLAYING" | "IDLE" | "STALLED"; + startAt: number; + volume: number; + volumeUnmute: number; + mediaProduct: { + productId: string; + productType: "track" | string; + sourceId: string; + sourceType: "PLAYLIST" | string; + }; + }; + playQueue: { + shuffleModeEnabled: boolean; + repeatMode: RepeatMode; + }; +}; + +const enum RepeatMode { + REPEAT_OFF = 0, + REPEAT_ALL = 1, + REPEAT_SINGLE = 2, +} +type MediaItem = + | { + type: "track"; + item: { + album: { + id: number; + title: string; + cover: string; + vibrantColor: string; + releaseDate: string; + }; + artist: Artist; + artists: Array; + audioModes: Array<"STEREO" | string>; + audioQuality: "LOSSLESS" | string; + bpm: number | null; + copyright: string; + dateAdded: string; + description: string | null; + duration: number; + explicit: boolean; + id: number; + isrc: string; + itemUuid: string; + peak: number; + popularity: number; + title: string; + trackNumber: number; + url: string; + }; + } + | { + type: "video"; + item: { + artists: Array; + contentType: "video"; + duration: number; + id: number; + imageId: string; + explicit: boolean; + title: string; + type: string; + url: string; + vibrantColor: string; + }; + }; + +type Artist = { + id: number; + name: string; + type: "MAIN" | string; + picture: string; +}; + +type Action = + | { + type: + | "playbackControls/PAUSE" + | "playbackControls/PLAY" + | "playbackControls/STOP" + | "playbackControls/SKIP_PREVIOUS" + | "playbackControls/SKIP_NEXT" + | "playQueue/TOGGLE_SHUFFLE" + | "playQueue/TOGGLE_REPEAT_MODE" + | "ROUTER_GO_BACK" + | "ROUTER_GO_FORWARD"; + } + | { + type: "playbackControls/SET_VOLUME"; + payload: { + /** 0 - 100 */ + volume: number; + }; + } + | { + type: "playbackControls/SET_MUTE"; + payload: { + mute: boolean; + }; + } + | { + type: "ROUTER_PUSH"; + payload: { + pathname: string; + options: Record; + hash: string; + }; + } + | { + type: "content/TOGGLE_FAVORITE_ITEMS"; + payload: { + from: "heart"; + items: Array<{ itemId: number; itemType: "track" }>; + moduleId?: string; + }; + }; diff --git a/src/preload/state.ts b/src/preload/state.ts new file mode 100644 index 0000000..dcda18b --- /dev/null +++ b/src/preload/state.ts @@ -0,0 +1,155 @@ +import { getTidalReduxStore, ReduxState, TidalReduxStore } from "./redux"; +import { createStore } from "zustand/vanilla"; +import { ipcRenderer } from "electron"; +import { globalEvents } from "../constants/globalEvents"; +import equal from "fast-deep-equal"; +import { TidalState } from "../models/tidalState"; + +export const $tidalState = createStore(() => ({ + status: "Stopped", +})); + +export let reduxStore: TidalReduxStore | undefined; + +export function playPause() { + if (!reduxStore) return; + + const state = $tidalState.getState(); + if (state.status === "Playing") { + reduxStore.dispatch({ type: "playbackControls/PAUSE" }); + } else { + reduxStore.dispatch({ type: "playbackControls/PLAY" }); + } +} +export function next() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playbackControls/SKIP_NEXT" }); +} +export function previous() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playbackControls/SKIP_PREVIOUS" }); +} +export function pause() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playbackControls/PAUSE" }); +} +export function play() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playbackControls/PLAY" }); +} +export function stop() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playbackControls/STOP" }); +} +export function toggleRepeat() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playQueue/TOGGLE_REPEAT_MODE" }); +} +export function toggleShuffle() { + if (!reduxStore) return; + reduxStore.dispatch({ type: "playQueue/TOGGLE_SHUFFLE" }); +} +export function favoriteCurrentTrack() { + if (!reduxStore) return; + const track = $tidalState.getState().currentTrack; + if (!track) return; + + reduxStore.dispatch({ + type: "content/TOGGLE_FAVORITE_ITEMS", + payload: { + from: "heart", + items: [{ itemId: track.id, itemType: "track" }], + moduleId: undefined, + }, + }); +} + +export const coverArtPaths = new Map>(); + +(async () => { + while (!reduxStore) { + try { + reduxStore = getTidalReduxStore(); + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + // Update currentTime + let rawCurrentTime: ReduxState["playbackControls"] = reduxStore.getState().playbackControls; + setInterval(() => { + const state = $tidalState.getState(); + const track = state.currentTrack; + if (!track) return; + const oldCurrentTime = track.current; + let newCurrentTime: number; + + if (state.status === "Playing") { + newCurrentTime = Math.trunc( + rawCurrentTime.latestCurrentTime + + Math.abs(rawCurrentTime.latestCurrentTimeSyncTimestamp - Date.now()) / 1000 + ); + } else { + newCurrentTime = rawCurrentTime.latestCurrentTime; + } + if (newCurrentTime !== oldCurrentTime) { + $tidalState.setState({ + ...state, + currentTrack: { + ...track, + current: newCurrentTime, + }, + }); + } + }, 1000); + + reduxStore.subscribe(async () => { + const state = reduxStore.getState(); + rawCurrentTime = state.playbackControls; + const currentItem = getCurrentTrack(state); + let track: TidalState["currentTrack"]; + if (currentItem) { + const imageId = + currentItem.type === "track" ? currentItem.item.album.cover : currentItem.item.imageId; + const coverUrl = `https://resources.tidal.com/images/${imageId.replace( + /-/g, + "/" + )}/640x640.jpg`; + if (!coverArtPaths.has(coverUrl)) { + coverArtPaths.set( + coverUrl, + ipcRenderer.invoke(globalEvents.downloadCover, imageId, coverUrl).catch(() => "") // ignore errors if the cover can't be downloaded + ); + } + track = { + id: currentItem.item.id, + title: currentItem.item.title, + album: currentItem.type === "track" ? currentItem.item.album.title : undefined, + artists: currentItem.item.artists.map((artist) => artist.name), + current: state.playbackControls.latestCurrentTime, + duration: currentItem.item.duration, + url: currentItem.item.url, + image: coverUrl, + }; + } + const oldState = $tidalState.getState(); + const newState = { + status: playbackStatusMap[state.playbackControls.playbackState] ?? "Stopped", + currentTrack: track, + }; + if (!equal(oldState, newState)) { + $tidalState.setState(newState); + } + }); +})(); + +function getCurrentTrack(state: ReduxState) { + return state.content.mediaItems[state.playbackControls.mediaProduct?.productId]; +} + +const playbackStatusMap = { + PLAYING: "Playing", + NOT_PLAYING: "Paused", + IDLE: "Stopped", + STALLED: "Stopped", +} as const; diff --git a/src/scripts/discord.ts b/src/scripts/discord.ts index 8e7c943..7bb8c28 100644 --- a/src/scripts/discord.ts +++ b/src/scripts/discord.ts @@ -3,10 +3,8 @@ import { app, ipcMain } from "electron"; import { globalEvents } from "../constants/globalEvents"; import { settings } from "../constants/settings"; import { Logger } from "../features/logger"; -import { convertDurationToSeconds } from "../features/time/parse"; -import { MediaStatus } from "../models/mediaStatus"; -import { mediaInfo } from "./mediaInfo"; import { settingsStore } from "./settings"; +import { mainTidalState } from "../features/state"; const clientId = "833617820704440341"; @@ -27,7 +25,7 @@ const defaultPresence = { const getActivity = (): Presence => { const presence: Presence = { ...defaultPresence }; - if (mediaInfo.status === MediaStatus.paused) { + if (mainTidalState.status === "Paused") { presence.details = settingsStore.get(settings.discord.idleText) ?? "Browsing Tidal"; } else { @@ -55,24 +53,26 @@ const getActivity = (): Presence => { } function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) { - if (mediaInfo.url) { - presence.details = `${detailsPrefix}${mediaInfo.title}`; - presence.state = mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)"; - presence.largeImageKey = mediaInfo.image; - if (mediaInfo.album) { - presence.largeImageText = mediaInfo.album; + const track = mainTidalState.currentTrack; + if (!track) return; + if (track.url) { + presence.details = `${detailsPrefix}${track.title}`; + presence.state = track.artists.join(", "); + presence.largeImageKey = track.image; + if (track.album) { + presence.largeImageText = track.album; } - presence.buttons = [{ label: buttonText, url: mediaInfo.url }]; + presence.buttons = [{ label: buttonText, url: track.url }]; } else { - presence.details = `Watching ${mediaInfo.title}`; - presence.state = mediaInfo.artists; + presence.details = `Watching ${track.title}`; + presence.state = track.artists.join(", "); } } function includeTimeStamps(includeTimestamps: boolean) { if (includeTimestamps) { - const currentSeconds = convertDurationToSeconds(mediaInfo.current); - const durationSeconds = convertDurationToSeconds(mediaInfo.duration); + const currentSeconds = mainTidalState.currentTrack?.current ?? 0; + const durationSeconds = mainTidalState.currentTrack?.duration ?? 0; const date = new Date(); const now = (date.getTime() / 1000) | 0; const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds)); diff --git a/src/scripts/download.ts b/src/scripts/download.ts deleted file mode 100644 index 01f365c..0000000 --- a/src/scripts/download.ts +++ /dev/null @@ -1,23 +0,0 @@ -import fs from "fs"; -import request from "request"; - -/** - * download and save a file - * @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) => { - const req = request({ - method: "GET", - uri: fileUrl, - }); - - const out = fs.createWriteStream(targetPath); - req.pipe(out); - - req.on("end", resolve); - - req.on("error", reject); - }); -}; diff --git a/src/scripts/mediaInfo.ts b/src/scripts/mediaInfo.ts index 9cfada4..2dc7aaa 100644 --- a/src/scripts/mediaInfo.ts +++ b/src/scripts/mediaInfo.ts @@ -1,54 +1,6 @@ -import { MediaInfo } from "../models/mediaInfo"; -import { MediaStatus } from "../models/mediaStatus"; +import { TidalState } from "../models/tidalState"; -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, +// This object is globally mutated +export const tidalState: TidalState = { + status: "Stopped", }; - -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; -}; - -/** - * 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 - * @returns url with `?u` appended, or the original value of url if falsy - */ -function toUniversalUrl(url: string) { - if (url) { - const queryParamsSet = url.indexOf("?"); - return queryParamsSet > -1 ? `${url}&u` : `${url}?u`; - } - return url; -} diff --git a/src/scripts/menu.ts b/src/scripts/menu.ts index 7dd90aa..c1f0096 100644 --- a/src/scripts/menu.ts +++ b/src/scripts/menu.ts @@ -1,7 +1,7 @@ import { BrowserWindow, Menu, app } from "electron"; import { showSettingsWindow } from "./settings"; + const isMac = process.platform === "darwin"; -import name from "./../constants/values"; const settingsMenuEntry = { label: "Settings", @@ -31,7 +31,7 @@ export const getMenu = function (mainWindow: BrowserWindow) { ...(isMac ? [ { - label: name, + label: "TIDAL Hi-Fi", submenu: [ settingsMenuEntry, { type: "separator" }, diff --git a/src/scripts/settings.ts b/src/scripts/settings.ts index 7c0536d..50a8434 100644 --- a/src/scripts/settings.ts +++ b/src/scripts/settings.ts @@ -117,6 +117,7 @@ export const createSettingsWindow = function () { show: false, transparent: true, frame: false, + type: "dialog", title: "TIDAL Hi-Fi settings", webPreferences: { preload: path.join(__dirname, "../pages/settings/preload.js"), diff --git a/src/scripts/window-functions.ts b/src/scripts/window-functions.ts deleted file mode 100644 index 9e780dd..0000000 --- a/src/scripts/window-functions.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const setTitle = function (title: string) { - window.document.title = title; -}; - -export const getTitle = function () { - return window.document.title; -}; diff --git a/src/types/mpris-service.d.ts b/src/types/mpris-service.d.ts index 40bf530..5bded5e 100644 --- a/src/types/mpris-service.d.ts +++ b/src/types/mpris-service.d.ts @@ -1,60 +1,63 @@ -declare class InitOptions { - name: string; - identity: string; - supportedUriSchemes: string[]; - supportedMimeTypes: string[]; - supportedInterfaces: string[]; - desktopEntry: string; -} - -declare class Player { - metadata: { - "xesam:title": string; - "xesam:artist": string[]; - "xesam:album": string; - "mpris:artUrl": string; - "mpris:length": number; - "mpris:trackid": string; - // other options - [key: string]: string | number | string[] | object; - }; - playbackStatus: string; - identity: string; - fullscreen: boolean; - supportedUriSchemes: string[]; - supportedMimeTypes: string[]; - canQuit: boolean; - canRaise: boolean; - canSetFullscreen: boolean; - hasTrackList: boolean; - desktopEntry: string; - loopStatus: string; - shuffle: boolean; - volume: number; - canControl: boolean; - canPause: boolean; - canPlay: boolean; - canSeek: boolean; - canGoNext: boolean; - canGoPrevious: boolean; - rate: number; - minimumRate: number; - maximumRate: number; - playlists: string[]; - activePlaylist: string; - - constructor(opts: { name: string; supportedInterfaces?: string[] }); - constructor(opts: InitOptions); - - getPosition(): number; - seeked(): void; - getTrackIndex(trackId: number): number; - getTrack(trackId: number): string; - addTrack(track: object): void; - removeTrack(trackId: number): number; - getPlaylistIndex(playlistId: number): number; - setPlaylists(playlists: object): void; - setActivePlaylist(playlistId: number): void; - - on(event: string | symbol, listener: (...args: object[]) => void): this; +declare module "mpris-service" { + export interface InitOptions { + name: string; + identity: string; + supportedUriSchemes: string[]; + supportedMimeTypes: string[]; + supportedInterfaces: string[]; + desktopEntry: string; + } + export interface Player { + metadata: { + "xesam:title"?: string; + "xesam:artist"?: string[]; + "xesam:album"?: string; + "mpris:artUrl"?: string; + "mpris:length"?: number | bigint; + "mpris:trackid": string; + // other options + [key: string]: string | number | string[] | bigint | object; + }; + playbackStatus: "Playing" | "Paused" | "Stopped"; + identity: string; + fullscreen: boolean; + supportedUriSchemes: string[]; + supportedMimeTypes: string[]; + canQuit: boolean; + canRaise: boolean; + canSetFullscreen: boolean; + hasTrackList: boolean; + desktopEntry: string; + loopStatus: string; + shuffle: boolean; + volume: number; + canControl: boolean; + canPause: boolean; + canPlay: boolean; + canSeek: boolean; + canGoNext: boolean; + canGoPrevious: boolean; + rate: number; + minimumRate: number; + maximumRate: number; + playlists: string[]; + activePlaylist: string; + + getPosition(): number | bigint; + seeked(): void; + getTrackIndex(trackId: number): number; + getTrack(trackId: number): string; + addTrack(track: object): void; + removeTrack(trackId: number): number; + getPlaylistIndex(playlistId: number): number; + setPlaylists(playlists: object): void; + setActivePlaylist(playlistId: number): void; + objectPath(path: string): string; + + on(event: string | symbol, listener: (...args: object[]) => void): this; + _bus: import("dbus-next").MessageBus; + } + + export default function Player(opts: { name: string; supportedInterfaces?: string[] }): Player; + export default function Player(opts: InitOptions): Player; } diff --git a/tsconfig.json b/tsconfig.json index 8a1c58b..337aa00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { "typeRoots": ["src/types", "node_modules/@types"], "module": "commonjs", - "target": "ES6", - "lib": ["ES2020", "DOM"], + "target": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "noImplicitAny": true, "sourceMap": true, "allowJs": true,