Compare commits

...

23 Commits

Author SHA1 Message Date
9eeb1c986a release 3.3.2 2024-02-20 20:59:48 +09:00
d37cd2418c fix: fix bugs in MPRIS, and improve MPRIS (#1760)
Co-authored-by: JellyBrick <shlee1503@naver.com>
Co-authored-by: Totto <32566573+Totto16@users.noreply.github.com>
2024-02-20 20:50:55 +09:00
8bd05f525d chore(deps): rollback dependency electron-builder to v24.9.1 2024-02-20 15:24:27 +09:00
47b23b414c chore(deps): update dependency electron-builder to v24.13.1 2024-02-20 14:43:04 +09:00
6f70d179c7 fix: fixed an issue that caused infinite loops when using Music Together
fix #1752
2024-02-20 12:57:49 +09:00
62a86e9267 fix(deps): update dependency electron-updater to v6.1.8 (#1770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-20 12:55:26 +09:00
6358a2d0b1 chore(deps): update dependency electron-builder to v24.12.0 (#1771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-20 12:55:17 +09:00
273633c2ce chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (342 of 342 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-02-20 04:15:04 +01:00
8b1209ef73 chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (340 of 340 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-02-20 03:55:03 +01:00
47505e9748 chore(i18n): Translated using Weblate (German)
Currently translated at 100.0% (340 of 340 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-02-20 03:55:03 +01:00
5178cc6bd8 feat(scrobblers): use BrowserWindow instead of shell.openExternal (#1758) 2024-02-20 11:54:57 +09:00
d9a27fff42 chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.0.2 (#1763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-20 11:54:37 +09:00
9e6560b814 chore(i18n): Translated using Weblate (Italian)
Currently translated at 99.7% (339 of 340 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-02-19 19:01:59 +01:00
afdb19a742 chore(deps): update dependency esbuild to v0.20.1 (#1759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-19 18:23:04 +09:00
0ae5b668f5 fix(in-app-menu): default config 2024-02-19 17:06:01 +09:00
10533e28fa fix: error.html path 2024-02-19 16:53:32 +09:00
6189e67819 fix(deps): update dependency i18next to v23.9.0 (#1754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-19 14:29:22 +09:00
f9ad505e40 fix: scrobbler migration 2024-02-19 14:27:24 +09:00
9b011101ed Update changelog for v3.3.1 2024-02-18 12:43:37 +00:00
a6ed8bf3aa release 3.3.1 (HOTFIX) 2024-02-18 21:36:04 +09:00
87acf4cf04 hotfix: in-app-menu position issue 2024-02-18 21:35:41 +09:00
b6fe2afd75 chore: Update README.md 2024-02-18 20:57:38 +09:00
6d9bb8eb1c Update changelog for v3.3.0 2024-02-18 11:40:17 +00:00
33 changed files with 1026 additions and 498 deletions

View File

@ -33,7 +33,7 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md)
| Player Screen (album color theme & ambient light) | | Player Screen (album color theme & ambient light) |
|:---------------------------------------------------------------------------------------------------------:| |:---------------------------------------------------------------------------------------------------------:|
|![Screenshot2](https://github.com/th-ch/youtube-music/assets/16558115/28ed8f08-c8c4-48ad-811b-7722093e9d81)| |![Screenshot1](https://github.com/th-ch/youtube-music/assets/16558115/53efdf73-b8fa-4d7b-a235-b96b91ea77fc)|
## Translation ## Translation

View File

@ -2,8 +2,114 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v3.3.1](https://github.com/th-ch/youtube-music/compare/v3.3.0...v3.3.1)
- Update changelog for v3.3.0 [`6d9bb8e`](https://github.com/th-ch/youtube-music/commit/6d9bb8eb1cc2d892a5552ffb1f7c20859aa80f67)
- hotfix: in-app-menu position issue [`87acf4c`](https://github.com/th-ch/youtube-music/commit/87acf4cf042ba32a000a4aeaec5c17c93501d333)
- release 3.3.1 (HOTFIX) [`a6ed8bf`](https://github.com/th-ch/youtube-music/commit/a6ed8bf3aa20ca8e950e85d88f981ccf9edc7498)
#### [v3.3.0](https://github.com/th-ch/youtube-music/compare/v3.2.2...v3.3.0)
> 18 February 2024
- fix(deps): update dependency i18next to v23.8.3 [`#1751`](https://github.com/th-ch/youtube-music/pull/1751)
- import fixed ./constants [`#1748`](https://github.com/th-ch/youtube-music/pull/1748)
- chore(deps): update dependency rollup to v4.12.0 [`#1743`](https://github.com/th-ch/youtube-music/pull/1743)
- chore(deps): bump undici from 5.28.2 to 5.28.3 [`#1747`](https://github.com/th-ch/youtube-music/pull/1747)
- chore(deps): update dependency vite to v5.1.3 [`#1742`](https://github.com/th-ch/youtube-music/pull/1742)
- chore(deps): update dependency vite-plugin-solid to v2.10.1 [`#1734`](https://github.com/th-ch/youtube-music/pull/1734)
- chore(deps): update dependency discord-api-types to v0.37.70 [`#1740`](https://github.com/th-ch/youtube-music/pull/1740)
- chore(deps): update dependency electron to v28.2.3 [`#1736`](https://github.com/th-ch/youtube-music/pull/1736)
- chore(deps): update pnpm to v8.15.3 [`#1739`](https://github.com/th-ch/youtube-music/pull/1739)
- chore(deps): update dependency rollup to v4.11.0 [`#1738`](https://github.com/th-ch/youtube-music/pull/1738)
- fix(deps): update dependency solid-js to v1.8.15 [`#1735`](https://github.com/th-ch/youtube-music/pull/1735)
- chore(deps): update dependency vite to v5.1.2 [`#1733`](https://github.com/th-ch/youtube-music/pull/1733)
- chore(deps): update dependency vite-plugin-solid to v2.10.0 [`#1732`](https://github.com/th-ch/youtube-music/pull/1732)
- chore(deps): update pnpm to v8.15.2 [`#1729`](https://github.com/th-ch/youtube-music/pull/1729)
- Update Copyright - 2024 [`#1730`](https://github.com/th-ch/youtube-music/pull/1730)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7 [`#1728`](https://github.com/th-ch/youtube-music/pull/1728)
- fix(deps): update dependency @floating-ui/dom to v1.6.3 [`#1727`](https://github.com/th-ch/youtube-music/pull/1727)
- chore(deps): update dependency electron to v28.2.2 [`#1717`](https://github.com/th-ch/youtube-music/pull/1717)
- chore(deps): update dependency vite to v5.1.1 [`#1718`](https://github.com/th-ch/youtube-music/pull/1718)
- chore(deps): update dependency @types/semver to v7.5.7 [`#1724`](https://github.com/th-ch/youtube-music/pull/1724)
- fix(deps): update dependency @floating-ui/dom to v1.6.2 [`#1725`](https://github.com/th-ch/youtube-music/pull/1725)
- chore(deps): update dependency rollup to v4.10.0 [`#1719`](https://github.com/th-ch/youtube-music/pull/1719)
- fix(deps): update dependency solid-js to v1.8.14 [`#1713`](https://github.com/th-ch/youtube-music/pull/1713)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.21.0 [`#1711`](https://github.com/th-ch/youtube-music/pull/1711)
- fix(deps): update dependency semver to v7.6.0 [`#1712`](https://github.com/th-ch/youtube-music/pull/1712)
- refactor(in-app-menu): refactor `in-app-menu` plugin [`#1710`](https://github.com/th-ch/youtube-music/pull/1710)
- chore(deps): update playwright monorepo to v1.41.2 [`#1706`](https://github.com/th-ch/youtube-music/pull/1706)
- chore(deps): update dependency electron to v29.0.0-beta.5 [`#1707`](https://github.com/th-ch/youtube-music/pull/1707)
- feat(album-color-theme): support album color theme in all pages [`#1685`](https://github.com/th-ch/youtube-music/pull/1685)
- fix(deps): update dependency youtubei.js to v9.0.2 [`#1704`](https://github.com/th-ch/youtube-music/pull/1704)
- fix(deps): update dependency i18next to v23.8.2 [`#1702`](https://github.com/th-ch/youtube-music/pull/1702)
- feat: Support disabling scrobbling for non-music content [`#1665`](https://github.com/th-ch/youtube-music/pull/1665)
- fix(deps): update dependency youtubei.js to v9 [`#1682`](https://github.com/th-ch/youtube-music/pull/1682)
- chore(deps): update dependency electron to v29.0.0-beta.4 [`#1698`](https://github.com/th-ch/youtube-music/pull/1698)
- fix(deps): update dependency i18next to v23.8.1 [`#1694`](https://github.com/th-ch/youtube-music/pull/1694)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.20.0 [`#1700`](https://github.com/th-ch/youtube-music/pull/1700)
- chore(deps): update pnpm to v8.15.1 [`#1699`](https://github.com/th-ch/youtube-music/pull/1699)
- chore(deps): update dependency esbuild to v0.20.0 [`#1691`](https://github.com/th-ch/youtube-music/pull/1691)
- chore(deps): update pnpm to v8.15.0 [`#1692`](https://github.com/th-ch/youtube-music/pull/1692)
- fix(deps): update dependency i18next to v23.7.20 [`#1684`](https://github.com/th-ch/youtube-music/pull/1684)
- chore(deps): update dependency electron to v29.0.0-beta.3 [`#1683`](https://github.com/th-ch/youtube-music/pull/1683)
- chore(deps): update dependency electron to v29.0.0-beta.2 [`#1681`](https://github.com/th-ch/youtube-music/pull/1681)
- chore(deps): update dependency rollup to v4.9.6 [`#1663`](https://github.com/th-ch/youtube-music/pull/1663)
- chore(deps): update dependency electron to v29.0.0-beta.1 [`#1670`](https://github.com/th-ch/youtube-music/pull/1670)
- fix(deps): update dependency i18next to v23.7.19 [`#1680`](https://github.com/th-ch/youtube-music/pull/1680)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.19.1 [`#1669`](https://github.com/th-ch/youtube-music/pull/1669)
- chore(deps): update pnpm to v8.14.3 [`#1668`](https://github.com/th-ch/youtube-music/pull/1668)
- chore(deps): update dependency vite-plugin-inspect to v0.8.3 [`#1672`](https://github.com/th-ch/youtube-music/pull/1672)
- chore(deps): update dependency esbuild to v0.19.12 [`#1673`](https://github.com/th-ch/youtube-music/pull/1673)
- fix(deps): update dependency @electron/remote to v2.1.2 [`#1676`](https://github.com/th-ch/youtube-music/pull/1676)
- chore: Update issue templates [`#1661`](https://github.com/th-ch/youtube-music/pull/1661)
- chore(deps): update playwright monorepo to v1.41.1 [`#1660`](https://github.com/th-ch/youtube-music/pull/1660)
- fix(deps): update dependency i18next to v23.7.18 [`#1662`](https://github.com/th-ch/youtube-music/pull/1662)
- chore(deps): update actions/dependency-review-action action to v4 [`#1654`](https://github.com/th-ch/youtube-music/pull/1654)
- chore(deps): update dependency electron to v29.0.0-alpha.11 [`#1656`](https://github.com/th-ch/youtube-music/pull/1656)
- chore(deps): update dependency vite to v5.0.12 [security] [`#1659`](https://github.com/th-ch/youtube-music/pull/1659)
- fix(deps): update dependency async-mutex to v0.4.1 [`#1653`](https://github.com/th-ch/youtube-music/pull/1653)
- chore(deps): update playwright monorepo to v1.41.0 [`#1651`](https://github.com/th-ch/youtube-music/pull/1651)
- feat: Better Scrobbler Plugin [`#1640`](https://github.com/th-ch/youtube-music/pull/1640)
- chore(deps): update dependency electron to v29.0.0-alpha.10 [`#1645`](https://github.com/th-ch/youtube-music/pull/1645)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.19.0 [`#1643`](https://github.com/th-ch/youtube-music/pull/1643)
- chore(README): Fix plugins names and add plugins in/to Readme (in menu too) [`#1624`](https://github.com/th-ch/youtube-music/pull/1624)
- fix(album-actions): Fixed album actions [`#1639`](https://github.com/th-ch/youtube-music/pull/1639)
- chore(deps): update playwright monorepo to v1.41.0-beta-1705101589000 [`#1638`](https://github.com/th-ch/youtube-music/pull/1638)
- fix(#1543): fix song control doesn't work [`#1637`](https://github.com/th-ch/youtube-music/pull/1637)
- chore(deps): update playwright monorepo to v1.41.0-beta-1705092460000 [`#1635`](https://github.com/th-ch/youtube-music/pull/1635)
- chore(deps): update dependency rollup to v4.9.5 [`#1629`](https://github.com/th-ch/youtube-music/pull/1629)
- chore(deps): update dependency electron to v29.0.0-alpha.9 [`#1627`](https://github.com/th-ch/youtube-music/pull/1627)
- chore(deps): update dependency electron to v29.0.0-alpha.8 [`#1608`](https://github.com/th-ch/youtube-music/pull/1608)
- fix(deps): update dependency @cliqz/adblocker-electron to v1.26.15 [`#1615`](https://github.com/th-ch/youtube-music/pull/1615)
- chore(deps): update dependency rollup to v4.9.4 [`#1591`](https://github.com/th-ch/youtube-music/pull/1591)
- fix(deps): update dependency @cliqz/adblocker-electron-preload to v1.26.15 [`#1616`](https://github.com/th-ch/youtube-music/pull/1616)
- chore(deps): update pnpm to v8.14.1 [`#1619`](https://github.com/th-ch/youtube-music/pull/1619)
- chore(deps): update dependency eslint-plugin-prettier to v5.1.3 [`#1618`](https://github.com/th-ch/youtube-music/pull/1618)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.18.1 [`#1612`](https://github.com/th-ch/youtube-music/pull/1612)
- fix(deps): update dependency youtubei.js to v8.2.0 [`#1614`](https://github.com/th-ch/youtube-music/pull/1614)
- chore(deps): update dependency electron-vite to v2.0.0 [`#1609`](https://github.com/th-ch/youtube-music/pull/1609)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.18.0 [`#1603`](https://github.com/th-ch/youtube-music/pull/1603)
- chore(deps): update dependency electron-vite to v2.0.0-beta.4 [`#1602`](https://github.com/th-ch/youtube-music/pull/1602)
- fix: fix upgrade button [`#1199`](https://github.com/th-ch/youtube-music/issues/1199)
- fix(mpris): fix mpris invalid position [`#1726`](https://github.com/th-ch/youtube-music/issues/1726)
- fix: discord RPC (fix #1664) [`#1664`](https://github.com/th-ch/youtube-music/issues/1664)
- fix: remove sign-in button (fix #1199) [`#1199`](https://github.com/th-ch/youtube-music/issues/1199)
- Fix #1617 [`#1617`](https://github.com/th-ch/youtube-music/issues/1617)
- fix(crossfade): fix #1633 [`#1633`](https://github.com/th-ch/youtube-music/issues/1633)
- fix: fix #1621 [`#1621`](https://github.com/th-ch/youtube-music/issues/1621)
- fix(tuna-obs): partially fix #1596 [`#1596`](https://github.com/th-ch/youtube-music/issues/1596)
- fix(discord): fix hide duration button [`#1644`](https://github.com/th-ch/youtube-music/issues/1644)
- fix(in-app-menu): fix invalid `margin-top` [`#1597`](https://github.com/th-ch/youtube-music/issues/1597)
- fix(README): fix `plugins` path [`#1598`](https://github.com/th-ch/youtube-music/issues/1598)
- chore(i18n): Translated using Weblate (Vietnamese) [`0528637`](https://github.com/th-ch/youtube-music/commit/05286371353e8b4c36a5b9fe9011ae5dfdc7ee82)
- chore: update pnpm-lock [`fd8d59b`](https://github.com/th-ch/youtube-music/commit/fd8d59bada56dab4e156d22394fe0c5efec5abc4)
- fix(in-app-menu): fix app crash in production [`febc63e`](https://github.com/th-ch/youtube-music/commit/febc63edef375bd82db48b7fb460ec5a601ab872)
#### [v3.2.2](https://github.com/th-ch/youtube-music/compare/v3.2.1...v3.2.2) #### [v3.2.2](https://github.com/th-ch/youtube-music/compare/v3.2.1...v3.2.2)
> 5 January 2024
- feat(tray): Add song info and paused icon [`#1592`](https://github.com/th-ch/youtube-music/pull/1592) - feat(tray): Add song info and paused icon [`#1592`](https://github.com/th-ch/youtube-music/pull/1592)
- fix(skip-silences): fix audio distorted [`#1141`](https://github.com/th-ch/youtube-music/issues/1141) - fix(skip-silences): fix audio distorted [`#1141`](https://github.com/th-ch/youtube-music/issues/1141)
- chore(deps): update dependency rollup to v4.9.3 [`0c3c380`](https://github.com/th-ch/youtube-music/commit/0c3c3805918adf2a185a7f1dc67ea3af8135863d) - chore(deps): update dependency rollup to v4.9.3 [`0c3c380`](https://github.com/th-ch/youtube-music/commit/0c3c3805918adf2a185a7f1dc67ea3af8135863d)

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.3.0", "version": "3.3.2",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js", "main": "./dist/main/index.js",
"license": "MIT", "license": "MIT",
@ -160,13 +160,13 @@
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-unhandled": "4.0.1", "electron-unhandled": "4.0.1",
"electron-updater": "6.1.7", "electron-updater": "6.1.8",
"fast-average-color": "9.4.0", "fast-average-color": "9.4.0",
"fast-equals": "5.0.1", "fast-equals": "5.0.1",
"filenamify": "6.0.0", "filenamify": "6.0.0",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.8.3", "i18next": "23.9.0",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.12", "node-html-parser": "6.1.12",
@ -192,7 +192,7 @@
"@types/howler": "2.2.11", "@types/howler": "2.2.11",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@types/semver": "7.5.7", "@types/semver": "7.5.7",
"@typescript-eslint/eslint-plugin": "7.0.1", "@typescript-eslint/eslint-plugin": "7.0.2",
"bufferutil": "4.0.8", "bufferutil": "4.0.8",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
@ -202,7 +202,7 @@
"electron-builder": "24.9.1", "electron-builder": "24.9.1",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "2.0.0", "electron-vite": "2.0.0",
"esbuild": "0.20.0", "esbuild": "0.20.1",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-import-resolver-exports": "1.0.0-beta.5", "eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",

240
pnpm-lock.yaml generated
View File

@ -94,8 +94,8 @@ dependencies:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1 version: 4.0.1
electron-updater: electron-updater:
specifier: 6.1.7 specifier: 6.1.8
version: 6.1.7 version: 6.1.8
fast-average-color: fast-average-color:
specifier: 9.4.0 specifier: 9.4.0
version: 9.4.0 version: 9.4.0
@ -112,8 +112,8 @@ dependencies:
specifier: 9.0.5 specifier: 9.0.5
version: 9.0.5 version: 9.0.5
i18next: i18next:
specifier: 23.8.3 specifier: 23.9.0
version: 23.8.3 version: 23.9.0
keyboardevent-from-electron-accelerator: keyboardevent-from-electron-accelerator:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
@ -186,8 +186,8 @@ devDependencies:
specifier: 7.5.7 specifier: 7.5.7
version: 7.5.7 version: 7.5.7
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: 7.0.1 specifier: 7.0.2
version: 7.0.1(@typescript-eslint/parser@7.0.1)(eslint@8.56.0)(typescript@5.3.3) version: 7.0.2(@typescript-eslint/parser@7.0.1)(eslint@8.56.0)(typescript@5.3.3)
bufferutil: bufferutil:
specifier: 4.0.8 specifier: 4.0.8
version: 4.0.8 version: 4.0.8
@ -216,8 +216,8 @@ devDependencies:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0(vite@5.1.3) version: 2.0.0(vite@5.1.3)
esbuild: esbuild:
specifier: 0.20.0 specifier: 0.20.1
version: 0.20.0 version: 0.20.1
eslint: eslint:
specifier: 8.56.0 specifier: 8.56.0
version: 8.56.0 version: 8.56.0
@ -715,8 +715,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/aix-ppc64@0.20.0: /@esbuild/aix-ppc64@0.20.1:
resolution: {integrity: sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==} resolution: {integrity: sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ppc64] cpu: [ppc64]
os: [aix] os: [aix]
@ -733,8 +733,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/android-arm64@0.20.0: /@esbuild/android-arm64@0.20.1:
resolution: {integrity: sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==} resolution: {integrity: sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
@ -751,8 +751,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/android-arm@0.20.0: /@esbuild/android-arm@0.20.1:
resolution: {integrity: sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==} resolution: {integrity: sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
@ -769,8 +769,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/android-x64@0.20.0: /@esbuild/android-x64@0.20.1:
resolution: {integrity: sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==} resolution: {integrity: sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
@ -787,8 +787,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/darwin-arm64@0.20.0: /@esbuild/darwin-arm64@0.20.1:
resolution: {integrity: sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==} resolution: {integrity: sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@ -805,8 +805,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/darwin-x64@0.20.0: /@esbuild/darwin-x64@0.20.1:
resolution: {integrity: sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==} resolution: {integrity: sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@ -823,8 +823,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/freebsd-arm64@0.20.0: /@esbuild/freebsd-arm64@0.20.1:
resolution: {integrity: sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==} resolution: {integrity: sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
@ -841,8 +841,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/freebsd-x64@0.20.0: /@esbuild/freebsd-x64@0.20.1:
resolution: {integrity: sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==} resolution: {integrity: sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
@ -859,8 +859,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-arm64@0.20.0: /@esbuild/linux-arm64@0.20.1:
resolution: {integrity: sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==} resolution: {integrity: sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -877,8 +877,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-arm@0.20.0: /@esbuild/linux-arm@0.20.1:
resolution: {integrity: sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==} resolution: {integrity: sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
@ -895,8 +895,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-ia32@0.20.0: /@esbuild/linux-ia32@0.20.1:
resolution: {integrity: sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==} resolution: {integrity: sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
@ -913,8 +913,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-loong64@0.20.0: /@esbuild/linux-loong64@0.20.1:
resolution: {integrity: sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==} resolution: {integrity: sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
@ -931,8 +931,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-mips64el@0.20.0: /@esbuild/linux-mips64el@0.20.1:
resolution: {integrity: sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==} resolution: {integrity: sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
@ -949,8 +949,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-ppc64@0.20.0: /@esbuild/linux-ppc64@0.20.1:
resolution: {integrity: sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==} resolution: {integrity: sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
@ -967,8 +967,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-riscv64@0.20.0: /@esbuild/linux-riscv64@0.20.1:
resolution: {integrity: sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==} resolution: {integrity: sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
@ -985,8 +985,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-s390x@0.20.0: /@esbuild/linux-s390x@0.20.1:
resolution: {integrity: sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==} resolution: {integrity: sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
@ -1003,8 +1003,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/linux-x64@0.20.0: /@esbuild/linux-x64@0.20.1:
resolution: {integrity: sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==} resolution: {integrity: sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -1021,8 +1021,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/netbsd-x64@0.20.0: /@esbuild/netbsd-x64@0.20.1:
resolution: {integrity: sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==} resolution: {integrity: sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
@ -1039,8 +1039,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/openbsd-x64@0.20.0: /@esbuild/openbsd-x64@0.20.1:
resolution: {integrity: sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==} resolution: {integrity: sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [openbsd] os: [openbsd]
@ -1057,8 +1057,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/sunos-x64@0.20.0: /@esbuild/sunos-x64@0.20.1:
resolution: {integrity: sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==} resolution: {integrity: sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
@ -1075,8 +1075,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/win32-arm64@0.20.0: /@esbuild/win32-arm64@0.20.1:
resolution: {integrity: sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==} resolution: {integrity: sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@ -1093,8 +1093,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/win32-ia32@0.20.0: /@esbuild/win32-ia32@0.20.1:
resolution: {integrity: sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==} resolution: {integrity: sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
@ -1111,8 +1111,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@esbuild/win32-x64@0.20.0: /@esbuild/win32-x64@0.20.1:
resolution: {integrity: sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==} resolution: {integrity: sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -1749,8 +1749,8 @@ packages:
'@types/node': 20.11.0 '@types/node': 20.11.0
optional: true optional: true
/@typescript-eslint/eslint-plugin@7.0.1(@typescript-eslint/parser@7.0.1)(eslint@8.56.0)(typescript@5.3.3): /@typescript-eslint/eslint-plugin@7.0.2(@typescript-eslint/parser@7.0.1)(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==} resolution: {integrity: sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^7.0.0 '@typescript-eslint/parser': ^7.0.0
@ -1762,10 +1762,10 @@ packages:
dependencies: dependencies:
'@eslint-community/regexpp': 4.10.0 '@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.0.1(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 7.0.1(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 7.0.1 '@typescript-eslint/scope-manager': 7.0.2
'@typescript-eslint/type-utils': 7.0.1(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/type-utils': 7.0.2(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/utils': 7.0.1(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 7.0.2(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 7.0.1 '@typescript-eslint/visitor-keys': 7.0.2
debug: 4.3.4 debug: 4.3.4
eslint: 8.56.0 eslint: 8.56.0
graphemer: 1.4.0 graphemer: 1.4.0
@ -1807,8 +1807,16 @@ packages:
'@typescript-eslint/visitor-keys': 7.0.1 '@typescript-eslint/visitor-keys': 7.0.1
dev: true dev: true
/@typescript-eslint/type-utils@7.0.1(eslint@8.56.0)(typescript@5.3.3): /@typescript-eslint/scope-manager@7.0.2:
resolution: {integrity: sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==} resolution: {integrity: sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 7.0.2
'@typescript-eslint/visitor-keys': 7.0.2
dev: true
/@typescript-eslint/type-utils@7.0.2(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
@ -1817,8 +1825,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 7.0.1(typescript@5.3.3) '@typescript-eslint/typescript-estree': 7.0.2(typescript@5.3.3)
'@typescript-eslint/utils': 7.0.1(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 7.0.2(eslint@8.56.0)(typescript@5.3.3)
debug: 4.3.4 debug: 4.3.4
eslint: 8.56.0 eslint: 8.56.0
ts-api-utils: 1.0.3(typescript@5.3.3) ts-api-utils: 1.0.3(typescript@5.3.3)
@ -1832,6 +1840,11 @@ packages:
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
dev: true dev: true
/@typescript-eslint/types@7.0.2:
resolution: {integrity: sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==}
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
/@typescript-eslint/typescript-estree@7.0.1(typescript@5.3.3): /@typescript-eslint/typescript-estree@7.0.1(typescript@5.3.3):
resolution: {integrity: sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==} resolution: {integrity: sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
@ -1854,8 +1867,30 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/utils@7.0.1(eslint@8.56.0)(typescript@5.3.3): /@typescript-eslint/typescript-estree@7.0.2(typescript@5.3.3):
resolution: {integrity: sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==} resolution: {integrity: sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 7.0.2
'@typescript-eslint/visitor-keys': 7.0.2
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
semver: 7.6.0
ts-api-utils: 1.0.3(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@7.0.2(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies: peerDependencies:
eslint: ^8.56.0 eslint: ^8.56.0
@ -1863,9 +1898,9 @@ packages:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@types/semver': 7.5.7 '@types/semver': 7.5.7
'@typescript-eslint/scope-manager': 7.0.1 '@typescript-eslint/scope-manager': 7.0.2
'@typescript-eslint/types': 7.0.1 '@typescript-eslint/types': 7.0.2
'@typescript-eslint/typescript-estree': 7.0.1(typescript@5.3.3) '@typescript-eslint/typescript-estree': 7.0.2(typescript@5.3.3)
eslint: 8.56.0 eslint: 8.56.0
semver: 7.6.0 semver: 7.6.0
transitivePeerDependencies: transitivePeerDependencies:
@ -1881,6 +1916,14 @@ packages:
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
dev: true dev: true
/@typescript-eslint/visitor-keys@7.0.2:
resolution: {integrity: sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 7.0.2
eslint-visitor-keys: 3.4.3
dev: true
/@ungap/structured-clone@1.2.0: /@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true dev: true
@ -3214,8 +3257,8 @@ packages:
serialize-error: 8.1.0 serialize-error: 8.1.0
dev: false dev: false
/electron-updater@6.1.7: /electron-updater@6.1.8:
resolution: {integrity: sha512-SNOhYizjkm4ET+Y8ilJyUzcVsFJDtINzVN1TyHnZeMidZEG3YoBebMyXc/J6WSiXdUaOjC7ngekN6rNp6ardHA==} resolution: {integrity: sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==}
dependencies: dependencies:
builder-util-runtime: 9.2.3 builder-util-runtime: 9.2.3
fs-extra: 10.1.0 fs-extra: 10.1.0
@ -3435,35 +3478,35 @@ packages:
'@esbuild/win32-x64': 0.19.12 '@esbuild/win32-x64': 0.19.12
dev: true dev: true
/esbuild@0.20.0: /esbuild@0.20.1:
resolution: {integrity: sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==} resolution: {integrity: sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.20.0 '@esbuild/aix-ppc64': 0.20.1
'@esbuild/android-arm': 0.20.0 '@esbuild/android-arm': 0.20.1
'@esbuild/android-arm64': 0.20.0 '@esbuild/android-arm64': 0.20.1
'@esbuild/android-x64': 0.20.0 '@esbuild/android-x64': 0.20.1
'@esbuild/darwin-arm64': 0.20.0 '@esbuild/darwin-arm64': 0.20.1
'@esbuild/darwin-x64': 0.20.0 '@esbuild/darwin-x64': 0.20.1
'@esbuild/freebsd-arm64': 0.20.0 '@esbuild/freebsd-arm64': 0.20.1
'@esbuild/freebsd-x64': 0.20.0 '@esbuild/freebsd-x64': 0.20.1
'@esbuild/linux-arm': 0.20.0 '@esbuild/linux-arm': 0.20.1
'@esbuild/linux-arm64': 0.20.0 '@esbuild/linux-arm64': 0.20.1
'@esbuild/linux-ia32': 0.20.0 '@esbuild/linux-ia32': 0.20.1
'@esbuild/linux-loong64': 0.20.0 '@esbuild/linux-loong64': 0.20.1
'@esbuild/linux-mips64el': 0.20.0 '@esbuild/linux-mips64el': 0.20.1
'@esbuild/linux-ppc64': 0.20.0 '@esbuild/linux-ppc64': 0.20.1
'@esbuild/linux-riscv64': 0.20.0 '@esbuild/linux-riscv64': 0.20.1
'@esbuild/linux-s390x': 0.20.0 '@esbuild/linux-s390x': 0.20.1
'@esbuild/linux-x64': 0.20.0 '@esbuild/linux-x64': 0.20.1
'@esbuild/netbsd-x64': 0.20.0 '@esbuild/netbsd-x64': 0.20.1
'@esbuild/openbsd-x64': 0.20.0 '@esbuild/openbsd-x64': 0.20.1
'@esbuild/sunos-x64': 0.20.0 '@esbuild/sunos-x64': 0.20.1
'@esbuild/win32-arm64': 0.20.0 '@esbuild/win32-arm64': 0.20.1
'@esbuild/win32-ia32': 0.20.0 '@esbuild/win32-ia32': 0.20.1
'@esbuild/win32-x64': 0.20.0 '@esbuild/win32-x64': 0.20.1
dev: true dev: true
/escalade@3.1.1: /escalade@3.1.1:
@ -4327,8 +4370,8 @@ packages:
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
dev: false dev: false
/i18next@23.8.3: /i18next@23.9.0:
resolution: {integrity: sha512-IQn6Tfn+XkIRHjC/z3uQSGLhsRC6Y14kgyrsgoPqnFD9MqbNt2B9MF3Ch4p114pEVPQ2qktE2nd0aYr7UxRLKA==} resolution: {integrity: sha512-f3MUciKqwzNV//mHG6EtdSlC65+nqH/3zK8sOSWqNV6FVu2tmHhF/rFOp9UF8S4m1odojtuipKaKJrP0Loh60g==}
dependencies: dependencies:
'@babel/runtime': 7.23.8 '@babel/runtime': 7.23.8
dev: false dev: false
@ -5615,6 +5658,7 @@ packages:
/punycode@2.3.1: /punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
requiresBuild: true
/queue-microtask@1.2.3: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}

View File

@ -53,6 +53,7 @@ const migrations = {
secret: lastfmConfig.secret, secret: lastfmConfig.secret,
}; };
store.set('plugins.scrobbler', scrobblerConfig); store.set('plugins.scrobbler', scrobblerConfig);
store.delete('plugins.lastfm');
} }
}, },
'>=3.0.0'(store: Conf<Record<string, unknown>>) { '>=3.0.0'(store: Conf<Record<string, unknown>>) {

View File

@ -46,7 +46,7 @@
"hide-menu-enabled": { "hide-menu-enabled": {
"detail": "Das Menü ist versteckt, nutze 'Alt', um es zu aufzurufen (oder 'Escape' beim Verwenden des In-App-Menüs)", "detail": "Das Menü ist versteckt, nutze 'Alt', um es zu aufzurufen (oder 'Escape' beim Verwenden des In-App-Menüs)",
"message": "Menü verstecken ist aktiviert", "message": "Menü verstecken ist aktiviert",
"title": "Menü Verstecken Aktiviert" "title": "Menü verstecken aktiviert"
}, },
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
@ -55,7 +55,7 @@
}, },
"detail": "\"{{pluginName}}\"-Erweiterung erfordert einen Neustart, um in Kraft zu treten", "detail": "\"{{pluginName}}\"-Erweiterung erfordert einen Neustart, um in Kraft zu treten",
"message": "\"{{pluginName}}\" muss neugestartet werden", "message": "\"{{pluginName}}\" muss neugestartet werden",
"title": "Neustart Erforderlich" "title": "Neustart erforderlich"
}, },
"unresponsive": { "unresponsive": {
"buttons": { "buttons": {
@ -75,7 +75,7 @@
}, },
"detail": "Eine neue Version ist verfügbar und kann unter {{downloadLink}} heruntergeladen werden", "detail": "Eine neue Version ist verfügbar und kann unter {{downloadLink}} heruntergeladen werden",
"message": "Eine neue Version ist verfügbar", "message": "Eine neue Version ist verfügbar",
"title": "Aktualisierung Verfügbar" "title": "Aktualisierung verfügbar"
} }
}, },
"menu": { "menu": {
@ -87,7 +87,7 @@
"go-back": "Zurück gehen", "go-back": "Zurück gehen",
"go-forward": "Vorwärts gehen", "go-forward": "Vorwärts gehen",
"quit": "Beenden", "quit": "Beenden",
"restart": "Anwendung Neustarten" "restart": "Anwendung neustarten"
} }
}, },
"options": { "options": {
@ -124,7 +124,7 @@
"language": { "language": {
"dialog": { "dialog": {
"message": "Sprache wird nach Neustart geändert", "message": "Sprache wird nach Neustart geändert",
"title": "Sprache Geändert" "title": "Sprache geändert"
}, },
"label": "Sprache", "label": "Sprache",
"submenu": { "submenu": {
@ -212,6 +212,14 @@
}, },
"album-color-theme": { "album-color-theme": {
"description": "Wendet ein dynamisches Farbthema und visuelle Effekte auf Basis der Farbpalette des Albumcovers an", "description": "Wendet ein dynamisches Farbthema und visuelle Effekte auf Basis der Farbpalette des Albumcovers an",
"menu": {
"color-mix-ratio": {
"label": "Farbmischungsverhältnis",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Thema aus Albumfarbe" "name": "Thema aus Albumfarbe"
}, },
"ambient-mode": { "ambient-mode": {
@ -230,7 +238,7 @@
} }
}, },
"opacity": { "opacity": {
"label": "Durchsichtigkeit", "label": "Transparenz",
"submenu": { "submenu": {
"percent": "{{opacity}}%" "percent": "{{opacity}}%"
} }
@ -275,7 +283,7 @@
"description": "Untertitelwähler für YouTube Music-Audio-Lieder", "description": "Untertitelwähler für YouTube Music-Audio-Lieder",
"menu": { "menu": {
"autoload": "Wähle automatisch den zuletzt verwendeten Untertitel", "autoload": "Wähle automatisch den zuletzt verwendeten Untertitel",
"disable-captions": "Standartmäßig keine Untertitel" "disable-captions": "Standardmäßig keine Untertitel"
}, },
"name": "Untertitelwähler", "name": "Untertitelwähler",
"prompt": { "prompt": {
@ -577,23 +585,25 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": "ListenBrainz-Benutzer-Token eintragen" "token": "ListenBrainz-Benutzer-Token eintragen"
} },
"scrobble-other-media": "Andere Medien scrobbeln"
}, },
"name": "Scrobbler", "name": "Scrobbler",
"prompt": { "prompt": {
"lastfm": { "lastfm": {
"api-key": "Last.fm API-Schlüssel", "api-key": "Last.fm API-Schlüssel",
"api-secret": "Last.fm API secret" "api-secret": "Last.fm API-Kennwort"
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "ListenBrainz-Benutzer-Token eintragen" "label": "ListenBrainz-Benutzer-Token eintragen:",
"title": "ListenBrainz-Token"
} }
} }
} }
}, },
"shortcuts": { "shortcuts": {
"description": "Ermöglicht das Festlegen globaler Hotkeys für die Wiedergabe (Abspielen/Pause/Nächster/Vorheriger) + Deaktivieren des Medien-OSD durch Überschreiben der Medientasten + Aktivieren von Strg/CMD + F zum Suchen + Aktivieren der Linux mpris-Unterstützung für Medientasten + Angepasste Tastenkürzel für fortgeschrittene Benutzer.", "description": "Ermöglicht das Festlegen globaler Hotkeys für die Wiedergabe (Abspielen/Pause/Nächster/Vorheriger) + Deaktivieren des Medien-OSD durch Überschreiben der Medientasten + Aktivieren von Strg/CMD + F zum Suchen + Aktivieren der Linux mpris-Unterstützung für Medientasten + Angepasste Tastenkürzel für fortgeschrittene Benutzer",
"menu": { "menu": {
"override-media-keys": "Medientasten überschreiben", "override-media-keys": "Medientasten überschreiben",
"set-keybinds": "Globale Liedsteuerung setzen" "set-keybinds": "Globale Liedsteuerung setzen"

View File

@ -579,6 +579,14 @@
}, },
"scrobbler": { "scrobbler": {
"description": "Add scrobbling support (etc. last.fm, Listenbrainz)", "description": "Add scrobbling support (etc. last.fm, Listenbrainz)",
"dialog": {
"lastfm": {
"auth-failed": {
"title": "Authentication Failed",
"message": "Failed to authenticate with Last.fm\nHide the popup until the next restart."
}
}
},
"menu": { "menu": {
"scrobble-other-media": "Scrobble other media", "scrobble-other-media": "Scrobble other media",
"lastfm": { "lastfm": {

View File

@ -212,6 +212,14 @@
}, },
"album-color-theme": { "album-color-theme": {
"description": "Applica un tema dinamico e degli effetti visivi basandosi sul colore dell'album", "description": "Applica un tema dinamico e degli effetti visivi basandosi sul colore dell'album",
"menu": {
"color-mix-ratio": {
"label": "Percentiuale colore",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Tema abbinato a colore album" "name": "Tema abbinato a colore album"
}, },
"ambient-mode": { "ambient-mode": {
@ -577,7 +585,8 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": "Inserire il token utente per ListenBrainz" "token": "Inserire il token utente per ListenBrainz"
} },
"scrobble-other-media": "Scrobble altri media"
}, },
"name": "Scrobbler", "name": "Scrobbler",
"prompt": { "prompt": {

View File

@ -579,6 +579,14 @@
}, },
"scrobbler": { "scrobbler": {
"description": "스크로블링 지원을 추가합니다 (예: last.fm, Listenbrainz)", "description": "스크로블링 지원을 추가합니다 (예: last.fm, Listenbrainz)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "Last.fm 인증에 실패했습니다\n다음에 다시 시작할 때까지 팝업을 숨깁니다.",
"title": "인증 실패"
}
}
},
"menu": { "menu": {
"lastfm": { "lastfm": {
"api-settings": "Last.fm API 설정" "api-settings": "Last.fm API 설정"

View File

@ -53,6 +53,8 @@ import {
import { LoggerPrefix } from '@/utils'; import { LoggerPrefix } from '@/utils';
import { loadI18n, setLanguage, t } from '@/i18n'; import { loadI18n, setLanguage, t } from '@/i18n';
import ErrorHtmlAsset from '@assets/error.html?asset';
import type { PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
if (!is.macOS()) { if (!is.macOS()) {
@ -505,7 +507,7 @@ app.once('browser-window-created', (_event, win) => {
if (errorCode !== -3) { if (errorCode !== -3) {
// -3 is a false positive // -3 is a false positive
win.webContents.send('log', log); win.webContents.send('log', log);
win.webContents.loadFile(path.join(__dirname, 'error.html')); win.webContents.loadFile(ErrorHtmlAsset);
} }
}, },
); );
@ -671,7 +673,9 @@ app.whenReady().then(async () => {
); );
} }
handleProtocol(command); const splited = decodeURIComponent(command).split(' ');
handleProtocol(splited.shift()!, splited);
return; return;
} }

View File

@ -7,7 +7,7 @@ export const defaultInAppMenuConfig: InAppMenuConfig = {
( (
( (
typeof window !== 'undefined' && typeof window !== 'undefined' &&
!window.navigator?.userAgent?.includes('mac') !window.navigator?.userAgent?.toLowerCase().includes('mac')
) || ) ||
( (
typeof global !== 'undefined' && typeof global !== 'undefined' &&
@ -16,7 +16,7 @@ export const defaultInAppMenuConfig: InAppMenuConfig = {
) && ( ) && (
( (
typeof window !== 'undefined' && typeof window !== 'undefined' &&
!window.navigator?.userAgent?.includes('linux') !window.navigator?.userAgent?.toLowerCase().includes('linux')
) || ) ||
( (
typeof global !== 'undefined' && typeof global !== 'undefined' &&

View File

@ -132,7 +132,7 @@ export const Panel = (props: PanelProps) => {
<Show when={local.open}> <Show when={local.open}>
<ul <ul
{...leftProps} {...leftProps}
id={'sub-panel'} data-ytmd-sub-panel={true}
ref={setPanel} ref={setPanel}
class={panelStyle()} class={panelStyle()}
style={{ style={{

View File

@ -250,8 +250,8 @@ export const TitleBar = (props: TitleBarProps) => {
if ( if (
e.target instanceof HTMLElement && e.target instanceof HTMLElement &&
!( !(
e.target.closest('#main-panel') || e.target.closest('nav[data-ytmd-main-panel]') ||
e.target.closest('#sub-panel') e.target.closest('ul[data-ytmd-sub-panel]')
) )
) { ) {
setOpenTarget(null); setOpenTarget(null);
@ -266,7 +266,7 @@ export const TitleBar = (props: TitleBarProps) => {
}); });
return ( return (
<nav id={'main-panel'} class={titleStyle()} data-macos={props.isMacOS}> <nav data-ytmd-main-panel={true} class={titleStyle()} data-macos={props.isMacOS}>
<IconButton <IconButton
onClick={() => setCollapsed(!collapsed())} onClick={() => setCollapsed(!collapsed())}
style={{ style={{

View File

@ -6,9 +6,9 @@ import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '@/providers/prompt-options';
import { AppAPI, getDefaultProfile, Permission, Profile, VideoData } from './types'; import { getDefaultProfile, type Permission, type Profile, type VideoData } from './types';
import { Queue } from './queue'; import { Queue } from './queue';
import { Connection, ConnectionEventUnion } from './connection'; import { Connection, type ConnectionEventUnion } from './connection';
import { createHostPopup } from './ui/host'; import { createHostPopup } from './ui/host';
import { createGuestPopup } from './ui/guest'; import { createGuestPopup } from './ui/guest';
import { createSettingPopup } from './ui/setting'; import { createSettingPopup } from './ui/setting';
@ -19,6 +19,7 @@ import style from './style.css?inline';
import type { YoutubePlayer } from '@/types/youtube-player'; import type { YoutubePlayer } from '@/types/youtube-player';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { VideoDataChanged } from '@/types/video-data-changed'; import type { VideoDataChanged } from '@/types/video-data-changed';
import type { AppElement } from '@/types/queue';
type RawAccountData = { type RawAccountData = {
accountName: { accountName: {
@ -41,7 +42,7 @@ export default createPlugin<
{ {
connection?: Connection; connection?: Connection;
ipc?: RendererContext<never>['ipc']; ipc?: RendererContext<never>['ipc'];
api: HTMLElement & AppAPI | null; api: AppElement | null;
queue?: Queue; queue?: Queue;
playerApi?: YoutubePlayer; playerApi?: YoutubePlayer;
showPrompt: (title: string, label: string) => Promise<string>; showPrompt: (title: string, label: string) => Promise<string>;
@ -557,7 +558,7 @@ export default createPlugin<
start({ ipc }) { start({ ipc }) {
this.ipc = ipc; this.ipc = ipc;
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>; this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>;
this.api = document.querySelector<HTMLElement & AppAPI>('ytmusic-app'); this.api = document.querySelector<AppElement>('ytmusic-app');
/* setup */ /* setup */
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML); document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML);

View File

@ -1,11 +1,12 @@
import { getMusicQueueRenderer } from './song'; import { getMusicQueueRenderer } from './song';
import { mapQueueItem } from './utils'; import { mapQueueItem } from './utils';
import { ConnectionEventUnion } from '@/plugins/music-together/connection';
import { t } from '@/i18n'; import { t } from '@/i18n';
import type { Profile, QueueAPI, VideoData } from '../types'; import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
import type { Profile, VideoData } from '../types';
import type { QueueItem } from '@/types/datahost-get-state'; import type { QueueItem } from '@/types/datahost-get-state';
import type { QueueElement } from '@/types/queue';
const getHeaderPayload = (() => { const getHeaderPayload = (() => {
let payload: { let payload: {
@ -103,26 +104,29 @@ const getHeaderPayload = (() => {
export type QueueOptions = { export type QueueOptions = {
videoList?: VideoData[]; videoList?: VideoData[];
owner?: Profile; owner?: Profile;
queue?: HTMLElement & QueueAPI; queue?: QueueElement;
getProfile: (id: string) => Profile | undefined; getProfile: (id: string) => Profile | undefined;
} }
export type QueueEventListener = (event: ConnectionEventUnion) => void; export type QueueEventListener = (event: ConnectionEventUnion) => void;
export class Queue { export class Queue {
private queue: (HTMLElement & QueueAPI); private readonly queue: QueueElement;
private originalDispatch?: (obj: { private originalDispatch?: (obj: {
type: string; type: string;
payload?: { items?: QueueItem[] | undefined; }; payload?: { items?: QueueItem[] | undefined; };
}) => void; }) => void;
private internalDispatch = false; private internalDispatch = false;
private ignoreFlag = false; private ignoreFlag = false;
private listeners: QueueEventListener[] = []; private listeners: QueueEventListener[] = [];
private owner: Profile | null = null;
private getProfile: (id: string) => Profile | undefined; private owner: Profile | null;
private readonly getProfile: (id: string) => Profile | undefined;
constructor(options: QueueOptions) { constructor(options: QueueOptions) {
this.getProfile = options.getProfile; this.getProfile = options.getProfile;
this.queue = options.queue ?? document.querySelector<HTMLElement & QueueAPI>('#queue')!; this.queue = options.queue ?? (document.querySelector<QueueElement>('#queue')!);
this.owner = options.owner ?? null; this.owner = options.owner ?? null;
this._videoList = options.videoList ?? []; this._videoList = options.videoList ?? [];
} }
@ -135,11 +139,11 @@ export class Queue {
} }
get selectedIndex() { get selectedIndex() {
return mapQueueItem((it) => it?.selected, this.queue.store.getState().queue.items).findIndex(Boolean) ?? 0; return mapQueueItem((it) => it?.selected, this.queue.queue.store.store.getState().queue.items).findIndex(Boolean) ?? 0;
} }
get rawItems() { get rawItems() {
return this.queue?.store.getState().queue.items; return this.queue?.queue.store.store.getState().queue.items;
} }
get flatItems() { get flatItems() {
@ -169,8 +173,8 @@ export class Queue {
this.queue?.dispatch({ this.queue?.dispatch({
type: 'ADD_ITEMS', type: 'ADD_ITEMS',
payload: { payload: {
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId, nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
index: index ?? this.queue.store.getState().queue.items.length ?? 0, index: index ?? this.queue.queue.store.store.getState().queue.items.length ?? 0,
items, items,
shuffleEnabled: false, shuffleEnabled: false,
shouldAssignIds: true shouldAssignIds: true
@ -249,7 +253,7 @@ export class Queue {
return; return;
} }
if (this.originalDispatch) this.queue.store.dispatch = this.originalDispatch; if (this.originalDispatch) this.queue.queue.store.store.dispatch = this.originalDispatch;
} }
injection() { injection() {
@ -258,8 +262,8 @@ export class Queue {
return; return;
} }
this.originalDispatch = this.queue.store.dispatch; this.originalDispatch = this.queue.queue.store.store.dispatch;
this.queue.store.dispatch = (event) => { this.queue.queue.store.store.dispatch = (event) => {
if (!this.queue || !this.owner) { if (!this.queue || !this.owner) {
console.error('Queue is not initialized!'); console.error('Queue is not initialized!');
return; return;
@ -361,10 +365,13 @@ export class Queue {
const fakeContext = { const fakeContext = {
...this.queue, ...this.queue,
store: { queue: {
...this.queue.store, ...this.queue.queue,
dispatch: this.originalDispatch store: {
} ...this.queue.queue.store,
dispatch: this.originalDispatch,
}
},
}; };
this.originalDispatch?.call(fakeContext, event); this.originalDispatch?.call(fakeContext, event);
}; };
@ -400,7 +407,7 @@ export class Queue {
type: 'UPDATE_ITEMS', type: 'UPDATE_ITEMS',
payload: { payload: {
items: items, items: items,
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId, nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
shouldAssignIds: true, shouldAssignIds: true,
currentIndex: -1 currentIndex: -1
} }

View File

@ -1,37 +1,3 @@
import { YoutubePlayer } from '@/types/youtube-player';
import { GetState, QueueItem } from '@/types/datahost-get-state';
type StoreState = GetState;
type Store = {
dispatch: (obj: {
type: string;
payload?: {
items?: QueueItem[];
};
}) => void;
getState: () => StoreState;
replaceReducer: (param1: unknown) => unknown;
subscribe: (callback: () => void) => unknown;
}
export type QueueAPI = {
dispatch(obj: {
type: string;
payload?: unknown;
}): void;
getItems(): unknown[];
store: Store;
continuation?: string;
autoPlaying?: boolean;
};
export type AppAPI = {
queue_: QueueAPI;
playerApi_: YoutubePlayer;
openToast: (message: string) => void;
// TODO: Add more
};
export type Profile = { export type Profile = {
id: string; id: string;
handleId: string; handleId: string;

View File

@ -307,9 +307,9 @@ export default (
savedNotification?.close(); savedNotification?.close();
}); });
changeProtocolHandler((cmd) => { changeProtocolHandler((cmd, args) => {
if (Object.keys(songControls).includes(cmd)) { if (Object.keys(songControls).includes(cmd)) {
songControls[cmd as keyof typeof songControls](); songControls[cmd as keyof typeof songControls](args as never);
if ( if (
config().refreshOnPlayPause && config().refreshOnPlayPause &&
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification)) (cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))

View File

@ -1,10 +1,13 @@
import { BrowserWindow } from 'electron';
import registerCallback, { MediaType, type SongInfo } from '@/providers/song-info'; import registerCallback, { MediaType, type SongInfo } from '@/providers/song-info';
import { createBackend } from '@/utils'; import { createBackend } from '@/utils';
import { ScrobblerPluginConfig } from './index';
import { LastFmScrobbler } from './services/lastfm'; import { LastFmScrobbler } from './services/lastfm';
import { ListenbrainzScrobbler } from './services/listenbrainz'; import { ListenbrainzScrobbler } from './services/listenbrainz';
import { ScrobblerBase } from './services/base';
import type { ScrobblerPluginConfig } from './index';
import type { ScrobblerBase } from './services/base';
export type SetConfType = ( export type SetConfType = (
conf: Partial<Omit<ScrobblerPluginConfig, 'enabled'>>, conf: Partial<Omit<ScrobblerPluginConfig, 'enabled'>>,
@ -12,14 +15,17 @@ export type SetConfType = (
export const backend = createBackend<{ export const backend = createBackend<{
config?: ScrobblerPluginConfig; config?: ScrobblerPluginConfig;
window?: BrowserWindow;
enabledScrobblers: Map<string, ScrobblerBase>; enabledScrobblers: Map<string, ScrobblerBase>;
toggleScrobblers(config: ScrobblerPluginConfig): void; toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow): void;
createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType): Promise<void>;
setConfig?: SetConfType;
}, ScrobblerPluginConfig>({ }, ScrobblerPluginConfig>({
enabledScrobblers: new Map(), enabledScrobblers: new Map(),
toggleScrobblers(config: ScrobblerPluginConfig) { toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow) {
if (config.scrobblers.lastfm && config.scrobblers.lastfm.enabled) { if (config.scrobblers.lastfm && config.scrobblers.lastfm.enabled) {
this.enabledScrobblers.set('lastfm', new LastFmScrobbler()); this.enabledScrobblers.set('lastfm', new LastFmScrobbler(window));
} else { } else {
this.enabledScrobblers.delete('lastfm'); this.enabledScrobblers.delete('lastfm');
} }
@ -31,20 +37,27 @@ export const backend = createBackend<{
} }
}, },
async start({ async createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType) {
getConfig,
setConfig,
}) {
const config = this.config = await getConfig();
// This will store the timeout that will trigger addScrobble
let scrobbleTimer: NodeJS.Timeout | undefined;
this.toggleScrobblers(config);
for (const [, scrobbler] of this.enabledScrobblers) { for (const [, scrobbler] of this.enabledScrobblers) {
if (!scrobbler.isSessionCreated(config)) { if (!scrobbler.isSessionCreated(config)) {
await scrobbler.createSession(config, setConfig); await scrobbler.createSession(config, setConfig);
} }
} }
},
async start({
getConfig,
setConfig,
window,
}) {
const config = this.config = await getConfig();
// This will store the timeout that will trigger addScrobble
let scrobbleTimer: NodeJS.Timeout | undefined;
this.window = window;
this.toggleScrobblers(config, window);
await this.createSessions(config, setConfig);
this.setConfig = setConfig;
registerCallback((songInfo: SongInfo) => { registerCallback((songInfo: SongInfo) => {
// Set remove the old scrobble timer // Set remove the old scrobble timer
@ -52,7 +65,7 @@ export const backend = createBackend<{
if (!songInfo.isPaused) { if (!songInfo.isPaused) {
const configNonnull = this.config!; const configNonnull = this.config!;
// Scrobblers normally have no trouble working with official music videos // Scrobblers normally have no trouble working with official music videos
if (!configNonnull.scrobble_other_media && (songInfo.mediaType !== MediaType.Audio && songInfo.mediaType !== MediaType.OriginalMusicVideo)) { if (!configNonnull.scrobbleOtherMedia && (songInfo.mediaType !== MediaType.Audio && songInfo.mediaType !== MediaType.OriginalMusicVideo)) {
return; return;
} }
@ -71,12 +84,25 @@ export const backend = createBackend<{
}); });
}, },
onConfigChange(newConfig: ScrobblerPluginConfig) { async onConfigChange(newConfig: ScrobblerPluginConfig) {
this.enabledScrobblers.clear(); this.enabledScrobblers.clear();
this.config = newConfig; this.toggleScrobblers(newConfig, this.window!);
for (const [scrobblerName, scrobblerConfig] of Object.entries(newConfig.scrobblers)) {
if (scrobblerConfig.enabled) {
const scrobbler = this.enabledScrobblers.get(scrobblerName);
if (
this.config?.scrobblers?.[scrobblerName as keyof typeof newConfig.scrobblers]?.enabled !== scrobblerConfig.enabled &&
scrobbler &&
!scrobbler.isSessionCreated(newConfig) &&
this.setConfig
) {
await scrobbler.createSession(newConfig, this.setConfig);
}
}
}
this.toggleScrobblers(this.config); this.config = newConfig;
} }
}); });

View File

@ -20,7 +20,7 @@ async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: Se
multiInputOptions: [ multiInputOptions: [
{ {
label: t('plugins.scrobbler.prompt.lastfm.api-key'), label: t('plugins.scrobbler.prompt.lastfm.api-key'),
value: options.scrobblers.lastfm?.api_key, value: options.scrobblers.lastfm?.apiKey,
inputAttrs: { inputAttrs: {
type: 'text' type: 'text'
} }
@ -42,7 +42,7 @@ async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: Se
if (output) { if (output) {
if (output[0]) { if (output[0]) {
options.scrobblers.lastfm.api_key = output[0]; options.scrobblers.lastfm.apiKey = output[0];
} }
if (output[1]) { if (output[1]) {
@ -82,9 +82,9 @@ export const onMenu = async ({
{ {
label: t('plugins.scrobbler.menu.scrobble-other-media'), label: t('plugins.scrobbler.menu.scrobble-other-media'),
type: 'checkbox', type: 'checkbox',
checked: Boolean(config.scrobble_other_media), checked: Boolean(config.scrobbleOtherMedia),
click(item) { click(item) {
config.scrobble_other_media = item.checked; config.scrobbleOtherMedia = item.checked;
setConfig(config); setConfig(config);
}, },
}, },
@ -96,7 +96,7 @@ export const onMenu = async ({
type: 'checkbox', type: 'checkbox',
checked: Boolean(config.scrobblers.lastfm?.enabled), checked: Boolean(config.scrobblers.lastfm?.enabled),
click(item) { click(item) {
backend.toggleScrobblers(config); backend.toggleScrobblers(config, window);
config.scrobblers.lastfm.enabled = item.checked; config.scrobblers.lastfm.enabled = item.checked;
setConfig(config); setConfig(config);
}, },
@ -117,7 +117,7 @@ export const onMenu = async ({
type: 'checkbox', type: 'checkbox',
checked: Boolean(config.scrobblers.listenbrainz?.enabled), checked: Boolean(config.scrobblers.listenbrainz?.enabled),
click(item) { click(item) {
backend.toggleScrobblers(config); backend.toggleScrobblers(config, window);
config.scrobblers.listenbrainz.enabled = item.checked; config.scrobblers.listenbrainz.enabled = item.checked;
setConfig(config); setConfig(config);
}, },

View File

@ -1,6 +1,5 @@
import { ScrobblerPluginConfig } from '../index'; import type { ScrobblerPluginConfig } from '../index';
import { SetConfType } from '../main'; import type { SetConfType } from '../main';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
export abstract class ScrobblerBase { export abstract class ScrobblerBase {

View File

@ -1,12 +1,13 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { net, shell } from 'electron'; import { BrowserWindow, dialog, net } from 'electron';
import { ScrobblerBase } from './base'; import { ScrobblerBase } from './base';
import { ScrobblerPluginConfig } from '../index'; import { t } from '@/i18n';
import { SetConfType } from '../main';
import type { ScrobblerPluginConfig } from '../index';
import type { SetConfType } from '../main';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
interface LastFmData { interface LastFmData {
@ -28,11 +29,22 @@ interface LastFmSongData {
} }
export class LastFmScrobbler extends ScrobblerBase { export class LastFmScrobbler extends ScrobblerBase {
isSessionCreated(config: ScrobblerPluginConfig): boolean { mainWindow: BrowserWindow;
constructor(mainWindow: BrowserWindow) {
super();
this.mainWindow = mainWindow;
}
override isSessionCreated(config: ScrobblerPluginConfig): boolean {
return !!config.scrobblers.lastfm.sessionKey; return !!config.scrobblers.lastfm.sessionKey;
} }
async createSession(config: ScrobblerPluginConfig, setConfig: SetConfType): Promise<ScrobblerPluginConfig> { override async createSession(
config: ScrobblerPluginConfig,
setConfig: SetConfType,
): Promise<ScrobblerPluginConfig> {
// Get and store the session key // Get and store the session key
const data = { const data = {
api_key: config.scrobblers.lastfm.apiKey, api_key: config.scrobblers.lastfm.apiKey,
@ -52,8 +64,15 @@ export class LastFmScrobbler extends ScrobblerBase {
}; };
if (json.error) { if (json.error) {
config.scrobblers.lastfm.token = await createToken(config); config.scrobblers.lastfm.token = await createToken(config);
await authenticate(config); // If is successful, we need retry the request
setConfig(config); authenticate(config, this.mainWindow).then((it) => {
if (it) {
this.createSession(config, setConfig);
} else {
// failed
setConfig(config);
}
});
} }
if (json.session) { if (json.session) {
config.scrobblers.lastfm.sessionKey = json.session.key; config.scrobblers.lastfm.sessionKey = json.session.key;
@ -62,7 +81,7 @@ export class LastFmScrobbler extends ScrobblerBase {
return config; return config;
} }
setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void { override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
if (!config.scrobblers.lastfm.sessionKey) { if (!config.scrobblers.lastfm.sessionKey) {
return; return;
} }
@ -74,7 +93,7 @@ export class LastFmScrobbler extends ScrobblerBase {
this.postSongDataToAPI(songInfo, config, data, setConfig); this.postSongDataToAPI(songInfo, config, data, setConfig);
} }
addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void { override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
if (!config.scrobblers.lastfm.sessionKey) { if (!config.scrobblers.lastfm.sessionKey) {
return; return;
} }
@ -87,7 +106,7 @@ export class LastFmScrobbler extends ScrobblerBase {
this.postSongDataToAPI(songInfo, config, data, setConfig); this.postSongDataToAPI(songInfo, config, data, setConfig);
} }
async postSongDataToAPI( private async postSongDataToAPI(
songInfo: SongInfo, songInfo: SongInfo,
config: ScrobblerPluginConfig, config: ScrobblerPluginConfig,
data: LastFmData, data: LastFmData,
@ -128,8 +147,14 @@ export class LastFmScrobbler extends ScrobblerBase {
// Session key is invalid, so remove it from the config and reauthenticate // Session key is invalid, so remove it from the config and reauthenticate
config.scrobblers.lastfm.sessionKey = undefined; config.scrobblers.lastfm.sessionKey = undefined;
config.scrobblers.lastfm.token = await createToken(config); config.scrobblers.lastfm.token = await createToken(config);
await authenticate(config); authenticate(config, this.mainWindow).then((it) => {
setConfig(config); if (it) {
this.createSession(config, setConfig);
} else {
// failed
setConfig(config);
}
});
} else { } else {
console.error(error); console.error(error);
} }
@ -168,17 +193,17 @@ const createQueryString = (
const createApiSig = (parameters: LastFmSongData, secret: string) => { const createApiSig = (parameters: LastFmSongData, secret: string) => {
// This function creates the api signature, see: https://www.last.fm/api/authspec // This function creates the api signature, see: https://www.last.fm/api/authspec
const keys = Object.keys(parameters);
keys.sort();
let sig = ''; let sig = '';
for (const key of keys) {
if (key === 'format') {
continue;
}
sig += `${key}${parameters[key as keyof LastFmSongData]}`; Object
} .entries(parameters)
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([key, value]) => {
if (key === 'format') {
return;
}
sig += key + value;
});
sig += secret; sig += secret;
sig = crypto.createHash('md5').update(sig, 'utf-8').digest('hex'); sig = crypto.createHash('md5').update(sig, 'utf-8').digest('hex');
@ -195,7 +220,11 @@ const createToken = async ({
} }
}: ScrobblerPluginConfig) => { }: ScrobblerPluginConfig) => {
// Creates and stores the auth token // Creates and stores the auth token
const data = { const data: {
method: string;
api_key: string;
format: string;
} = {
method: 'auth.gettoken', method: 'auth.gettoken',
api_key: apiKey, api_key: apiKey,
format: 'json', format: 'json',
@ -208,9 +237,68 @@ const createToken = async ({
return json?.token; return json?.token;
}; };
const authenticate = async (config: ScrobblerPluginConfig) => { let authWindowOpened = false;
// Asks the user for authentication let latestAuthResult = false;
await shell.openExternal(
`https://www.last.fm/api/auth/?api_key=${config.scrobblers.lastfm.apiKey}&token=${config.scrobblers.lastfm.token}`, const authenticate = async (config: ScrobblerPluginConfig, mainWindow: BrowserWindow) => {
); return new Promise<boolean>((resolve) => {
if (!authWindowOpened) {
authWindowOpened = true;
const url = `https://www.last.fm/api/auth/?api_key=${config.scrobblers.lastfm.apiKey}&token=${config.scrobblers.lastfm.token}`;
const browserWindow = new BrowserWindow({
width: 500,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
},
autoHideMenuBar: true,
parent: mainWindow,
minimizable: false,
maximizable: false,
paintWhenInitiallyHidden: true,
modal: true,
center: true,
});
browserWindow.loadURL(url).then(() => {
browserWindow.show();
browserWindow.webContents.on('did-navigate', async (_, newUrl) => {
const url = new URL(newUrl);
if (url.hostname.endsWith('last.fm')) {
if (url.pathname === '/api/auth') {
const isApproveScreen = await browserWindow.webContents.executeJavaScript(
'!!document.getElementsByName(\'confirm\').length'
) as boolean;
// successful authentication
if (!isApproveScreen) {
resolve(true);
latestAuthResult = true;
browserWindow.close();
}
} else if (url.pathname === '/api/None') {
resolve(false);
latestAuthResult = false;
browserWindow.close();
}
}
});
browserWindow.on('closed', () => {
if (!latestAuthResult) {
dialog.showMessageBox({
title: t('plugins.scrobbler.dialog.lastfm.auth-failed.title'),
message: t('plugins.scrobbler.dialog.lastfm.auth-failed.message'),
type: 'error'
});
}
authWindowOpened = false;
});
});
} else {
// wait for the previous window to close
while (authWindowOpened) {
// wait
}
resolve(latestAuthResult);
}
});
}; };

View File

@ -2,10 +2,8 @@ import { net } from 'electron';
import { ScrobblerBase } from './base'; import { ScrobblerBase } from './base';
import { SetConfType } from '../main'; import type { SetConfType } from '../main';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
import type { ScrobblerPluginConfig } from '../index'; import type { ScrobblerPluginConfig } from '../index';
interface ListenbrainzRequestBody { interface ListenbrainzRequestBody {
@ -27,15 +25,15 @@ interface ListenbrainzRequestBody {
} }
export class ListenbrainzScrobbler extends ScrobblerBase { export class ListenbrainzScrobbler extends ScrobblerBase {
isSessionCreated(): boolean { override isSessionCreated(): boolean {
return true; return true;
} }
createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> { override createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> {
return Promise.resolve(config); return Promise.resolve(config);
} }
setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void { override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) { if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) {
return; return;
} }
@ -44,7 +42,7 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
submitListen(body, config); submitListen(body, config);
} }
addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void { override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) { if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) {
return; return;
} }

View File

@ -4,10 +4,10 @@ declare module '@jellybrick/mpris-service' {
import { interface as dbusInterface } from 'dbus-next'; import { interface as dbusInterface } from 'dbus-next';
interface RootInterfaceOptions { interface RootInterfaceOptions {
identity: string; identity?: string;
supportedUriSchemes: string[]; supportedUriSchemes?: string[];
supportedMimeTypes: string[]; supportedMimeTypes?: string[];
desktopEntry: string; desktopEntry?: string;
} }
export interface Track { export interface Track {
@ -35,6 +35,32 @@ declare module '@jellybrick/mpris-service' {
'xesam:userRating'?: number; 'xesam:userRating'?: number;
} }
export type PlayBackStatus = 'Playing' | 'Paused' | 'Stopped';
export type LoopStatus = 'None' | 'Track' | 'Playlist';
export const PLAYBACK_STATUS_PLAYING: 'Playing';
export const PLAYBACK_STATUS_PAUSED: 'Paused';
export const PLAYBACK_STATUS_STOPPED: 'Stopped';
export const LOOP_STATUS_NONE: 'None';
export const LOOP_STATUS_TRACK: 'Track';
export const LOOP_STATUS_PLAYLIST: 'Playlist';
export type Interfaces = 'player' | 'trackList' | 'playlists';
export interface AdditionalPlayerOptions {
name: string;
supportedInterfaces: Interfaces[];
}
export type PlayerOptions = RootInterfaceOptions & AdditionalPlayerOptions;
export interface Position {
trackId: string;
position: number;
}
declare class Player extends EventEmitter { declare class Player extends EventEmitter {
constructor(opts: { constructor(opts: {
name: string; name: string;
@ -43,18 +69,44 @@ declare module '@jellybrick/mpris-service' {
supportedInterfaces?: string[]; supportedInterfaces?: string[];
}); });
//RootInterface
on(event: 'quit', listener: () => void): this;
on(event: 'raise', listener: () => void): this;
on(
event: 'fullscreen',
listener: (fullscreenEnabled: boolean) => void,
): this;
emit(type: string, ...args: unknown[]): unknown;
name: string; name: string;
identity: string; identity: string;
fullscreen: boolean; fullscreen?: boolean;
supportedUriSchemes: string[]; supportedUriSchemes: string[];
supportedMimeTypes: string[]; supportedMimeTypes: string[];
canQuit: boolean; canQuit: boolean;
canRaise: boolean; canRaise: boolean;
canSetFullscreen: boolean; canSetFullscreen?: boolean;
desktopEntry?: string;
hasTrackList: boolean; hasTrackList: boolean;
desktopEntry: string;
playbackStatus: string; // PlayerInterface
loopStatus: string; on(event: 'next', listener: () => void): this;
on(event: 'previous', listener: () => void): this;
on(event: 'pause', listener: () => void): this;
on(event: 'playpause', listener: () => void): this;
on(event: 'stop', listener: () => void): this;
on(event: 'play', listener: () => void): this;
on(event: 'seek', listener: (offset: number) => void): this;
on(event: 'open', listener: ({ uri: string }) => void): this;
on(event: 'loopStatus', listener: (status: LoopStatus) => void): this;
on(event: 'rate', listener: () => void): this;
on(event: 'shuffle', listener: (enableShuffle: boolean) => void): this;
on(event: 'volume', listener: (newVolume: number) => void): this;
on(event: 'position', listener: (position: Position) => void): this;
playbackStatus: PlayBackStatus;
loopStatus: LoopStatus;
shuffle: boolean; shuffle: boolean;
metadata: Track; metadata: Track;
volume: number; volume: number;
@ -67,9 +119,40 @@ declare module '@jellybrick/mpris-service' {
rate: number; rate: number;
minimumRate: number; minimumRate: number;
maximumRate: number; maximumRate: number;
playlists: unknown[];
abstract getPosition(): number;
seeked(position: number): void;
// TracklistInterface
on(event: 'addTrack', listener: () => void): this;
on(event: 'removeTrack', listener: () => void): this;
on(event: 'goTo', listener: () => void): this;
tracks: Track[];
canEditTracks: boolean;
on(event: '*', a: unknown[]): this;
addTrack(track: string): void;
removeTrack(trackId: string): void;
// PlaylistsInterface
on(event: 'activatePlaylist', listener: () => void): this;
playlists: Playlist[];
activePlaylist: string; activePlaylist: string;
setPlaylists(playlists: Playlist[]): void;
setActivePlaylist(playlistId: string): void;
// Player methods
constructor(opts: PlayerOptions);
on(event: 'error', listener: (error: Error) => void): this;
init(opts: RootInterfaceOptions): void; init(opts: RootInterfaceOptions): void;
objectPath(subpath?: string): string; objectPath(subpath?: string): string;
@ -91,13 +174,6 @@ declare module '@jellybrick/mpris-service' {
setPlaylists(playlists: Track[]): void; setPlaylists(playlists: Track[]): void;
setActivePlaylist(playlistId: string): void; setActivePlaylist(playlistId: string): void;
static PLAYBACK_STATUS_PLAYING: 'Playing';
static PLAYBACK_STATUS_PAUSED: 'Paused';
static PLAYBACK_STATUS_STOPPED: 'Stopped';
static LOOP_STATUS_NONE: 'None';
static LOOP_STATUS_TRACK: 'Track';
static LOOP_STATUS_PLAYLIST: 'Playlist';
} }
interface MprisInterface extends dbusInterface.Interface { interface MprisInterface extends dbusInterface.Interface {

View File

@ -1,12 +1,27 @@
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import MprisPlayer, { Track } from '@jellybrick/mpris-service'; import MprisPlayer, {
Track,
LoopStatus,
type PlayBackStatus,
type PlayerOptions,
PLAYBACK_STATUS_STOPPED,
PLAYBACK_STATUS_PAUSED,
PLAYBACK_STATUS_PLAYING,
LOOP_STATUS_NONE,
LOOP_STATUS_PLAYLIST,
LOOP_STATUS_TRACK,
type Position,
} from '@jellybrick/mpris-service';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, { type SongInfo } from '@/providers/song-info';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import config from '@/config'; import config from '@/config';
import { LoggerPrefix } from '@/utils'; import { LoggerPrefix } from '@/utils';
import type { RepeatMode } from '@/types/datahost-get-state';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
class YTPlayer extends MprisPlayer { class YTPlayer extends MprisPlayer {
/** /**
* @type {number} The current position in microseconds * @type {number} The current position in microseconds
@ -14,12 +29,7 @@ class YTPlayer extends MprisPlayer {
*/ */
private currentPosition: number; private currentPosition: number;
constructor(opts: { constructor(opts: PlayerOptions) {
name: string;
identity: string;
supportedMimeTypes?: string[];
supportedInterfaces?: string[];
}) {
super(opts); super(opts);
this.currentPosition = 0; this.currentPosition = 0;
@ -33,35 +43,38 @@ class YTPlayer extends MprisPlayer {
return this.currentPosition; return this.currentPosition;
} }
setLoopStatus(status: string) { setLoopStatus(status: LoopStatus) {
this.loopStatus = status; this.loopStatus = status;
} }
isPlaying(): boolean { isPlaying(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PLAYING; return this.playbackStatus === PLAYBACK_STATUS_PLAYING;
} }
isPaused(): boolean { isPaused(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PAUSED; return this.playbackStatus === PLAYBACK_STATUS_PAUSED;
} }
isStopped(): boolean { isStopped(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_STOPPED; return this.playbackStatus === PLAYBACK_STATUS_STOPPED;
} }
setPlaybackStatus(status: string) { setPlaybackStatus(status: PlayBackStatus) {
this.playbackStatus = status; this.playbackStatus = status;
} }
} }
function setupMPRIS() { function setupMPRIS() {
const instance = new YTPlayer({ const instance = new YTPlayer({
name: 'youtube-music', name: 'YoutubeMusic',
identity: 'YouTube Music', identity: 'YouTube Music',
supportedMimeTypes: ['audio/mpeg'], supportedMimeTypes: ['audio/mpeg'],
supportedInterfaces: ['player'], supportedInterfaces: ['player'],
}); });
instance.canRaise = true; instance.canRaise = true;
instance.canQuit = false;
instance.canSetFullscreen = true;
instance.supportedUriSchemes = ['http', 'https']; instance.supportedUriSchemes = ['http', 'https'];
instance.desktopEntry = 'youtube-music'; instance.desktopEntry = 'youtube-music';
return instance; return instance;
@ -73,21 +86,27 @@ function registerMPRIS(win: BrowserWindow) {
playPause, playPause,
next, next,
previous, previous,
volumeMinus10, setVolume,
volumePlus10,
shuffle, shuffle,
switchRepeat, switchRepeat,
setFullscreen,
requestFullscreenInformation,
requestQueueInformation,
} = songControls; } = songControls;
try { try {
let currentSongInfo: SongInfo | null = null; let currentSongInfo: SongInfo | null = null;
const secToMicro = (n: number) => Math.round(Number(n) * 1e6); const secToMicro = (n: number) => Math.round(Number(n) * 1e6);
const microToSec = (n: number) => Math.round(Number(n) / 1e6); const microToSec = (n: number) => Math.round(Number(n) / 1e6);
const seekTo = (event: { const correctId = (videoId: string) => {
trackId: string; return videoId.replace('-', '_MINUS_');
position: number; };
}) => {
if (event.trackId === currentSongInfo?.videoId) { const seekTo = (event: Position) => {
if (
currentSongInfo?.videoId &&
event.trackId.endsWith(correctId(currentSongInfo.videoId))
) {
win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0)); win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0));
} }
}; };
@ -101,6 +120,10 @@ function registerMPRIS(win: BrowserWindow) {
win.webContents.send('ytmd:setup-time-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-time-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-fullscreen-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-autoplay-changed-listener', 'mpris');
requestFullscreenInformation();
requestQueueInformation();
}); });
ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t))); ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t)));
@ -109,29 +132,85 @@ function registerMPRIS(win: BrowserWindow) {
player.setPosition(secToMicro(t)); player.setPosition(secToMicro(t));
}); });
ipcMain.on('ytmd:repeat-changed', (_, mode: string) => { ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
switch (mode) { switch (mode) {
case 'NONE': { case 'NONE': {
player.setLoopStatus(YTPlayer.LOOP_STATUS_NONE); player.setLoopStatus(LOOP_STATUS_NONE);
break; break;
} }
case 'ONE': { case 'ONE': {
player.setLoopStatus(YTPlayer.LOOP_STATUS_TRACK); player.setLoopStatus(LOOP_STATUS_TRACK);
break; break;
} }
case 'ALL': { case 'ALL': {
player.setLoopStatus(YTPlayer.LOOP_STATUS_PLAYLIST); player.setLoopStatus(LOOP_STATUS_PLAYLIST);
// No default // No default
break; break;
} }
} }
requestQueueInformation();
}); });
player.on('loopStatus', (status: string) => {
ipcMain.on('ytmd:fullscreen-changed', (_, changedTo: boolean) => {
if (player.fullscreen === undefined || !player.canSetFullscreen) {
return;
}
player.fullscreen =
changedTo !== undefined ? changedTo : !player.fullscreen;
});
ipcMain.on(
'ytmd:set-fullscreen',
(_, isFullscreen: boolean | undefined) => {
if (!player.canSetFullscreen || isFullscreen === undefined) {
return;
}
player.fullscreen = isFullscreen;
},
);
ipcMain.on(
'ytmd:fullscreen-changed-supported',
(_, isFullscreenSupported: boolean) => {
player.canSetFullscreen = isFullscreenSupported;
},
);
ipcMain.on('ytmd:autoplay-changed', (_) => {
requestQueueInformation();
});
ipcMain.on('ytmd:get-queue-response', (_, queue: QueueResponse) => {
if (!queue) {
return;
}
const currentPosition = queue.items?.findIndex((it) =>
it?.playlistPanelVideoRenderer?.selected ||
it?.playlistPanelVideoWrapperRenderer?.primaryRenderer?.playlistPanelVideoRenderer?.selected
) ?? 0;
player.canGoPrevious = currentPosition !== 0;
let hasNext: boolean;
if (queue.autoPlaying) {
hasNext = true;
} else if (player.loopStatus === LOOP_STATUS_PLAYLIST) {
hasNext = true;
} else {
// Example: currentPosition = 0, queue.items.length = 29 -> hasNext = true
hasNext = !!(currentPosition - (queue?.items?.length ?? 0 - 1));
}
player.canGoNext = hasNext;
});
player.on('loopStatus', (status: LoopStatus) => {
// SwitchRepeat cycles between states in that order // SwitchRepeat cycles between states in that order
const switches = [ const switches = [
YTPlayer.LOOP_STATUS_NONE, LOOP_STATUS_NONE,
YTPlayer.LOOP_STATUS_PLAYLIST, LOOP_STATUS_PLAYLIST,
YTPlayer.LOOP_STATUS_TRACK, LOOP_STATUS_TRACK,
]; ];
const currentIndex = switches.indexOf(player.loopStatus); const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status); const targetIndex = switches.indexOf(status);
@ -142,33 +221,44 @@ function registerMPRIS(win: BrowserWindow) {
}); });
player.on('raise', () => { player.on('raise', () => {
if (!player.canRaise) {
return;
}
win.setSkipTaskbar(false); win.setSkipTaskbar(false);
win.show(); win.show();
}); });
player.on('fullscreen', (fullscreenEnabled: boolean) => {
setFullscreen(fullscreenEnabled);
});
player.on('play', () => { player.on('play', () => {
if (!player.isPlaying()) { if (!player.isPlaying()) {
player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PLAYING); player.setPlaybackStatus(PLAYBACK_STATUS_PLAYING);
playPause(); playPause();
} }
}); });
player.on('pause', () => { player.on('pause', () => {
if (player.playbackStatus !== YTPlayer.PLAYBACK_STATUS_PAUSED) { if (!player.isPaused()) {
player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PAUSED); player.setPlaybackStatus(PLAYBACK_STATUS_PAUSED);
playPause(); playPause();
} }
}); });
player.on('playpause', () => { player.on('playpause', () => {
player.setPlaybackStatus( player.setPlaybackStatus(
player.isPlaying() player.isPlaying() ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
? YTPlayer.PLAYBACK_STATUS_PAUSED
: YTPlayer.PLAYBACK_STATUS_PLAYING
); );
playPause(); playPause();
}); });
player.on('next', next); player.on('next', () => {
player.on('previous', previous); next();
});
player.on('previous', () => {
previous();
});
player.on('seek', seekBy); player.on('seek', seekBy);
player.on('position', seekTo); player.on('position', seekTo);
@ -176,10 +266,18 @@ function registerMPRIS(win: BrowserWindow) {
player.on('shuffle', (enableShuffle) => { player.on('shuffle', (enableShuffle) => {
if (enableShuffle) { if (enableShuffle) {
shuffle(); shuffle();
requestQueueInformation();
} }
}); });
player.on('open', (args: { uri: string }) => { player.on('open', (args: { uri: string }) => {
win.loadURL(args.uri); win.loadURL(args.uri).then(() => {
requestQueueInformation();
});
});
player.on('error', (error: Error) => {
console.error(LoggerPrefix, 'Error in MPRIS');
console.trace(error);
}); });
let mprisVolNewer = false; let mprisVolNewer = false;
@ -198,7 +296,7 @@ function registerMPRIS(win: BrowserWindow) {
} }
}); });
player.on('volume', (newVolume) => { player.on('volume', (newVolume: number) => {
if (config.plugins.isEnabled('precise-volume')) { if (config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value. // With precise volume we can set the volume to the exact value.
const newVol = ~~(newVolume * 100); const newVol = ~~(newVolume * 100);
@ -208,31 +306,23 @@ function registerMPRIS(win: BrowserWindow) {
win.webContents.send('setVolume', newVol); win.webContents.send('setVolume', newVol);
} }
} else { } else {
// With keyboard shortcuts we can only change the volume in increments of 10, so round it. setVolume(newVolume * 100);
let deltaVolume = Math.round((newVolume - player.volume) * 10);
while (deltaVolume !== 0 && deltaVolume > 0) {
volumePlus10();
player.volume += 0.1;
deltaVolume--;
}
while (deltaVolume !== 0 && deltaVolume < 0) {
volumeMinus10();
player.volume -= 0.1;
deltaVolume++;
}
} }
}); });
registerCallback((songInfo) => { registerCallback((songInfo: SongInfo) => {
if (player) { if (player) {
const data: Track = { const data: Track = {
'mpris:length': secToMicro(songInfo.songDuration), 'mpris:length': secToMicro(songInfo.songDuration),
'mpris:artUrl': songInfo.imageSrc ?? undefined, ...(songInfo.imageSrc
? { 'mpris:artUrl': songInfo.imageSrc }
: undefined),
'xesam:title': songInfo.title, 'xesam:title': songInfo.title,
'xesam:url': songInfo.url, 'xesam:url': songInfo.url,
'xesam:artist': [songInfo.artist], 'xesam:artist': [songInfo.artist],
'mpris:trackid': songInfo.videoId, 'mpris:trackid': player.objectPath(
`Track/${correctId(songInfo.videoId)}`,
),
}; };
if (songInfo.album) { if (songInfo.album) {
data['xesam:album'] = songInfo.album; data['xesam:album'] = songInfo.album;
@ -241,22 +331,20 @@ function registerMPRIS(win: BrowserWindow) {
player.metadata = data; player.metadata = data;
const currentElapsedMicroSeconds = secToMicro(songInfo.elapsedSeconds ?? 0); const currentElapsedMicroSeconds = secToMicro(
songInfo.elapsedSeconds ?? 0,
);
player.setPosition(currentElapsedMicroSeconds); player.setPosition(currentElapsedMicroSeconds);
player.seeked(currentElapsedMicroSeconds); player.seeked(currentElapsedMicroSeconds);
player.setPlaybackStatus( player.setPlaybackStatus(
songInfo.isPaused ? songInfo.isPaused ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
YTPlayer.PLAYBACK_STATUS_PAUSED :
YTPlayer.PLAYBACK_STATUS_PLAYING
); );
} }
requestQueueInformation();
}); });
} catch (error) { } catch (error) {
console.error( console.error(LoggerPrefix, 'Error in MPRIS');
LoggerPrefix,
'Error in MPRIS'
);
console.trace(error); console.trace(error);
} }
} }

View File

@ -6,7 +6,7 @@ import getSongControls from './song-controls';
export const APP_PROTOCOL = 'youtubemusic'; export const APP_PROTOCOL = 'youtubemusic';
let protocolHandler: ((cmd: string) => void) | undefined; let protocolHandler: ((cmd: string, args: string[] | undefined) => void) | undefined;
export function setupProtocolHandler(win: BrowserWindow) { export function setupProtocolHandler(win: BrowserWindow) {
if (process.defaultApp && process.argv.length >= 2) { if (process.defaultApp && process.argv.length >= 2) {
@ -19,18 +19,18 @@ export function setupProtocolHandler(win: BrowserWindow) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
protocolHandler = ((cmd: keyof typeof songControls) => { protocolHandler = ((cmd: keyof typeof songControls, args: string[] | undefined = undefined) => {
if (Object.keys(songControls).includes(cmd)) { if (Object.keys(songControls).includes(cmd)) {
songControls[cmd](); songControls[cmd](args as never);
} }
}) as (cmd: string) => void; }) as (cmd: string) => void;
} }
export function handleProtocol(cmd: string) { export function handleProtocol(cmd: string, args: string[] | undefined) {
protocolHandler?.(cmd); protocolHandler?.(cmd, args);
} }
export function changeProtocolHandler(f: (cmd: string) => void) { export function changeProtocolHandler(f: (cmd: string, args: string[] | undefined) => void) {
protocolHandler = f; protocolHandler = f;
} }

View File

@ -1,43 +1,82 @@
// This is used for to control the songs // This is used for to control the songs
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow } from 'electron';
// see protocol-handler.ts
type ArgsType<T> = T | string[] | undefined;
const parseNumberFromArgsType = (args: ArgsType<number>) => {
if (typeof args === 'number') {
return args;
} else if (Array.isArray(args)) {
return Number(args[0]);
} else {
return null;
}
};
const parseBooleanFromArgsType = (args: ArgsType<boolean>) => {
if (typeof args === 'boolean') {
return args;
} else if (Array.isArray(args)) {
return args[0] === 'true';
} else {
return null;
}
};
export default (win: BrowserWindow) => { export default (win: BrowserWindow) => {
const commands = { return {
// Playback // Playback
previous: () => win.webContents.send('ytmd:previous-video'), previous: () => win.webContents.send('ytmd:previous-video'),
next: () => win.webContents.send('ytmd:next-video'), next: () => win.webContents.send('ytmd:next-video'),
playPause: () => win.webContents.send('ytmd:toggle-play'), playPause: () => win.webContents.send('ytmd:toggle-play'),
like: () => win.webContents.send('ytmd:update-like', 'LIKE'), like: () => win.webContents.send('ytmd:update-like', 'LIKE'),
dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'), dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'),
go10sBack: () => win.webContents.send('ytmd:seek-by', -10), goBack: (seconds: ArgsType<number>) => {
go10sForward: () => win.webContents.send('ytmd:seek-by', 10), const secondsNumber = parseNumberFromArgsType(seconds);
go1sBack: () => win.webContents.send('ytmd:seek-by', -1), if (secondsNumber !== null) {
go1sForward: () => win.webContents.send('ytmd:seek-by', 1), win.webContents.send('ytmd:seek-by', -secondsNumber);
}
},
goForward: (seconds: ArgsType<number>) => {
const secondsNumber = parseNumberFromArgsType(seconds);
if (secondsNumber !== null) {
win.webContents.send('ytmd:seek-by', seconds);
}
},
shuffle: () => win.webContents.send('ytmd:shuffle'), shuffle: () => win.webContents.send('ytmd:shuffle'),
switchRepeat: (n = 1) => win.webContents.send('ytmd:switch-repeat', n), switchRepeat: (n: ArgsType<number> = 1) => {
const repeat = parseNumberFromArgsType(n);
if (repeat !== null) {
win.webContents.send('ytmd:switch-repeat', n);
}
},
// General // General
volumeMinus10: () => { setVolume: (volume: ArgsType<number>) => {
ipcMain.once('ytmd:get-volume-return', (_, volume) => { const volumeNumber = parseNumberFromArgsType(volume);
win.webContents.send('ytmd:update-volume', volume - 10); if (volumeNumber !== null) {
}); win.webContents.send('ytmd:update-volume', volume);
win.webContents.send('ytmd:get-volume'); }
}, },
volumePlus10: () => { setFullscreen: (isFullscreen: ArgsType<boolean>) => {
ipcMain.once('ytmd:get-volume-return', (_, volume) => { const isFullscreenValue = parseBooleanFromArgsType(isFullscreen);
win.webContents.send('ytmd:update-volume', volume + 10); if (isFullscreenValue !== null) {
}); win.setFullScreen(isFullscreenValue);
win.webContents.send('ytmd:get-volume'); win.webContents.send('ytmd:click-fullscreen-button', isFullscreenValue);
}
},
requestFullscreenInformation: () => {
win.webContents.send('ytmd:get-fullscreen');
},
requestQueueInformation: () => {
win.webContents.send('ytmd:get-queue');
}, },
fullscreen: () => win.webContents.send('ytmd:toggle-fullscreen'),
muteUnmute: () => win.webContents.send('ytmd:toggle-mute'), muteUnmute: () => win.webContents.send('ytmd:toggle-mute'),
search: () => win.webContents.sendInputEvent({ search: () => {
type: 'keyDown', win.webContents.sendInputEvent({
keyCode: '/', type: 'keyDown',
}), keyCode: '/',
}; });
return { },
...commands,
play: commands.playPause,
pause: commands.playPause,
}; };
}; };

View File

@ -62,11 +62,13 @@ export const setupRepeatChangedListener = singleton(() => {
// provided by YouTube Music // provided by YouTube Music
window.ipcRenderer.send( window.ipcRenderer.send(
'ytmd:repeat-changed', 'ytmd:repeat-changed',
document.querySelector< document
HTMLElement & { .querySelector<
getState: () => GetState; HTMLElement & {
} getState: () => GetState;
>('ytmusic-player-bar')?.getState().queue.repeatMode, }
>('ytmusic-player-bar')
?.getState().queue.repeatMode,
); );
}); });
@ -78,6 +80,46 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
window.ipcRenderer.send('ytmd:volume-changed', api.getVolume()); window.ipcRenderer.send('ytmd:volume-changed', api.getVolume());
}); });
export const setupFullScreenChangedListener = singleton(() => {
const playerBar = document.querySelector('ytmusic-player-bar');
if (!playerBar) {
window.ipcRenderer.send('ytmd:fullscreen-changed-supported', false);
return;
}
const observer = new MutationObserver(() => {
window.ipcRenderer.send(
'ytmd:fullscreen-changed',
(
playerBar?.attributes.getNamedItem('player-fullscreened') ?? null
) !== null,
);
});
observer.observe(playerBar, {
attributes: true,
childList: false,
subtree: false,
});
});
export const setupAutoPlayChangedListener = singleton(() => {
const autoplaySlider = document.querySelector<HTMLInputElement>(
'.autoplay > tp-yt-paper-toggle-button',
);
const observer = new MutationObserver(() => {
window.ipcRenderer.send('ytmd:autoplay-changed');
});
observer.observe(autoplaySlider!, {
attributes: true,
childList: false,
subtree: false,
});
});
export default (api: YoutubePlayer) => { export default (api: YoutubePlayer) => {
window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => { window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => {
setupTimeChangedListener(); setupTimeChangedListener();
@ -91,6 +133,14 @@ export default (api: YoutubePlayer) => {
setupVolumeChangedListener(api); setupVolumeChangedListener(api);
}); });
window.ipcRenderer.on('ytmd:setup-fullscreen-changed-listener', () => {
setupFullScreenChangedListener();
});
window.ipcRenderer.on('ytmd:setup-autoplay-changed-listener', () => {
setupAutoPlayChangedListener();
});
window.ipcRenderer.on('ytmd:setup-seeked-listener', () => { window.ipcRenderer.on('ytmd:setup-seeked-listener', () => {
setupSeekedListener(); setupSeekedListener();
}); });
@ -155,13 +205,13 @@ export default (api: YoutubePlayer) => {
function sendSongInfo(videoData: VideoDataChangeValue) { function sendSongInfo(videoData: VideoDataChangeValue) {
const data = api.getPlayerResponse(); const data = api.getPlayerResponse();
data.videoDetails.album = data.videoDetails.album = (
( Object.entries(videoData).find(
Object.entries(videoData) ([, value]) => value && Object.hasOwn(value, 'playerOverlays'),
.find(([, value]) => value && Object.hasOwn(value, 'playerOverlays')) as [string, AlbumDetails | undefined] ) as [string, AlbumDetails | undefined]
)?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at( )?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at(
0, 0,
)?.text; )?.text;
data.videoDetails.elapsedSeconds = 0; data.videoDetails.elapsedSeconds = 0;
data.videoDetails.isPaused = false; data.videoDetails.isPaused = false;

View File

@ -120,7 +120,9 @@ const handleData = async (
songInfo.mediaType = MediaType.PodcastEpisode; songInfo.mediaType = MediaType.PodcastEpisode;
// HACK: Podcast's participant is not the artist // HACK: Podcast's participant is not the artist
if (!config.get('options.usePodcastParticipantAsArtist')) { if (!config.get('options.usePodcastParticipantAsArtist')) {
songInfo.artist = cleanupName(data.microformat.microformatDataRenderer.pageOwnerDetails.name); songInfo.artist = cleanupName(
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
);
} }
break; break;
default: default:
@ -128,14 +130,13 @@ const handleData = async (
// HACK: This is a workaround for "podcast" types where "musicVideoType" doesn't exist. Google :facepalm: // HACK: This is a workaround for "podcast" types where "musicVideoType" doesn't exist. Google :facepalm:
if ( if (
!config.get('options.usePodcastParticipantAsArtist') && !config.get('options.usePodcastParticipantAsArtist') &&
( (data.responseContext.serviceTrackingParams
data.responseContext.serviceTrackingParams ?.at(0)
?.at(0) ?.params?.find((it) => it.key === 'ipcc')?.value ?? '1') != '0'
?.params
?.find((it) => it.key === 'ipcc')?.value ?? '1'
) != '0'
) { ) {
songInfo.artist = cleanupName(data.microformat.microformatDataRenderer.pageOwnerDetails.name); songInfo.artist = cleanupName(
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
);
} }
break; break;
} }
@ -165,10 +166,12 @@ const registerProvider = (win: BrowserWindow) => {
// This will be called when the song-info-front finds a new request with song data // This will be called when the song-info-front finds a new request with song data
ipcMain.on('ytmd:video-src-changed', async (_, data: GetPlayerResponse) => { ipcMain.on('ytmd:video-src-changed', async (_, data: GetPlayerResponse) => {
const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(async () => { const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(
songInfo = await handleData(data, win); async () => {
return songInfo; songInfo = await handleData(data, win);
}); return songInfo;
},
);
if (tempSongInfo) { if (tempSongInfo) {
for (const c of callbacks) { for (const c of callbacks) {

View File

@ -15,6 +15,8 @@ import { loadI18n, setLanguage, t as i18t } from '@/i18n';
import type { PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
import type { YoutubePlayer } from '@/types/youtube-player'; import type { YoutubePlayer } from '@/types/youtube-player';
import type { QueueElement } from '@/types/queue';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
let api: (Element & YoutubePlayer) | null = null; let api: (Element & YoutubePlayer) | null = null;
let isPluginLoaded = false; let isPluginLoaded = false;
@ -61,18 +63,56 @@ async function onApiLoaded() {
} }
}); });
window.ipcRenderer.on('ytmd:update-volume', (_, volume: number) => { window.ipcRenderer.on('ytmd:update-volume', (_, volume: number) => {
document.querySelector<HTMLElement & { updateVolume: (volume: number) => void }>('ytmusic-player-bar')?.updateVolume(volume); document
.querySelector<
HTMLElement & { updateVolume: (volume: number) => void }
>('ytmusic-player-bar')
?.updateVolume(volume);
}); });
window.ipcRenderer.on('ytmd:get-volume', (event) => {
event.sender.emit('ytmd:get-volume-return', api?.getVolume()); const isFullscreen = () => {
const isFullscreen =
document
.querySelector<HTMLElement>('ytmusic-player-bar')
?.attributes.getNamedItem('player-fullscreened') ?? null;
return isFullscreen !== null;
};
const clickFullscreenButton = (isFullscreenValue: boolean) => {
const fullscreen = isFullscreen();
if (isFullscreenValue === fullscreen) {
return;
}
if (fullscreen) {
document.querySelector<HTMLElement>('.exit-fullscreen-button')?.click();
} else {
document.querySelector<HTMLElement>('.fullscreen-button')?.click();
}
};
window.ipcRenderer.on('ytmd:get-fullscreen', (event) => {
event.sender.send('ytmd:set-fullscreen', isFullscreen());
}); });
window.ipcRenderer.on('ytmd:toggle-fullscreen', (_) => {
document.querySelector<HTMLElement & { toggleFullscreen: () => void }>('ytmusic-player-bar')?.toggleFullscreen(); window.ipcRenderer.on('ytmd:click-fullscreen-button', (_, fullscreen: boolean | undefined) => {
clickFullscreenButton(fullscreen ?? false);
}); });
window.ipcRenderer.on('ytmd:toggle-mute', (_) => { window.ipcRenderer.on('ytmd:toggle-mute', (_) => {
document.querySelector<HTMLElement & { onVolumeTap: () => void }>('ytmusic-player-bar')?.onVolumeTap(); document.querySelector<HTMLElement & { onVolumeTap: () => void }>('ytmusic-player-bar')?.onVolumeTap();
}); });
window.ipcRenderer.on('ytmd:get-queue', (event) => {
const queue = document.querySelector<QueueElement>('#queue');
event.sender.send('ytmd:get-queue-response', {
items: queue?.queue.getItems(),
autoPlaying: queue?.queue.autoPlaying,
continuation: queue?.queue.continuation,
} satisfies QueueResponse);
});
const video = document.querySelector('video')!; const video = document.querySelector('video')!;
const audioContext = new AudioContext(); const audioContext = new AudioContext();
const audioSource = audioContext.createMediaElementSource(video); const audioSource = audioContext.createMediaElementSource(video);
@ -236,7 +276,9 @@ const initObserver = async () => {
// check document.documentElement is ready // check document.documentElement is ready
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }); document.addEventListener('DOMContentLoaded', () => resolve(), {
once: true,
});
} else { } else {
resolve(); resolve();
} }

View File

@ -1,3 +1,5 @@
import type { PlayerConfig } from '@/types/get-player-response';
export interface GetState { export interface GetState {
castStatus: CastStatus; castStatus: CastStatus;
entities: Entities; entities: Entities;
@ -32,17 +34,11 @@ export interface Download {
export interface Entities {} export interface Entities {}
export interface LikeStatus { export interface LikeStatus {
videos: Videos; videos: Record<string, LikeType>;
playlists: Entities; playlists: Entities;
} }
export interface Videos { export enum LikeType {
tNVTuUEeWP0: Kqp1PyPRBzA;
KQP1PyPrBzA: Kqp1PyPRBzA;
'o1iz4L-5zkQ': Kqp1PyPRBzA;
}
export enum Kqp1PyPRBzA {
Dislike = 'DISLIKE', Dislike = 'DISLIKE',
Indifferent = 'INDIFFERENT', Indifferent = 'INDIFFERENT',
Like = 'LIKE', Like = 'LIKE',
@ -195,14 +191,10 @@ export interface Target {
export interface CommandWatchEndpoint { export interface CommandWatchEndpoint {
videoId: string; videoId: string;
params: PurpleParams; params: string;
watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs; watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs;
} }
export enum PurpleParams {
WAEB = 'wAEB',
}
export interface PurpleWatchEndpointMusicSupportedConfigs { export interface PurpleWatchEndpointMusicSupportedConfigs {
watchEndpointMusicConfig: PurpleWatchEndpointMusicConfig; watchEndpointMusicConfig: PurpleWatchEndpointMusicConfig;
} }
@ -381,7 +373,7 @@ export enum SharePanelType {
export interface PurpleWatchEndpoint { export interface PurpleWatchEndpoint {
videoId: string; videoId: string;
playlistId: string; playlistId: string;
params: PurpleParams; params: string;
loggingContext: LoggingContext; loggingContext: LoggingContext;
watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs; watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs;
} }
@ -466,7 +458,7 @@ export interface FeedbackEndpoint {
} }
export interface PurpleLikeEndpoint { export interface PurpleLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: Target; target: Target;
actions?: LikeEndpointAction[]; actions?: LikeEndpointAction[];
} }
@ -488,7 +480,7 @@ export interface PurpleToggledServiceEndpoint {
} }
export interface FluffyLikeEndpoint { export interface FluffyLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: Target; target: Target;
} }
@ -690,7 +682,7 @@ export interface FluffyDefaultServiceEndpoint {
} }
export interface TentacledLikeEndpoint { export interface TentacledLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: AddToPlaylistEndpoint; target: AddToPlaylistEndpoint;
actions?: LikeEndpointAction[]; actions?: LikeEndpointAction[];
} }
@ -702,7 +694,7 @@ export interface FluffyToggledServiceEndpoint {
} }
export interface StickyLikeEndpoint { export interface StickyLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: AddToPlaylistEndpoint; target: AddToPlaylistEndpoint;
} }
@ -1185,81 +1177,6 @@ export interface PtrackingURLClass {
headers: HeaderElement[]; headers: HeaderElement[];
} }
export interface PlayerConfig {
audioConfig: AudioConfig;
streamSelectionConfig: StreamSelectionConfig;
mediaCommonConfig: MediaCommonConfig;
webPlayerConfig: WebPlayerConfig;
}
export interface AudioConfig {
loudnessDb: number;
perceptualLoudnessDb: number;
enablePerFormatLoudness: boolean;
}
export interface MediaCommonConfig {
dynamicReadaheadConfig: DynamicReadaheadConfig;
}
export interface DynamicReadaheadConfig {
maxReadAheadMediaTimeMs: number;
minReadAheadMediaTimeMs: number;
readAheadGrowthRateMs: number;
}
export interface StreamSelectionConfig {
maxBitrate: string;
}
export interface WebPlayerConfig {
useCobaltTvosDash: boolean;
webPlayerActionsPorting: WebPlayerActionsPorting;
gatewayExperimentGroup: string;
}
export interface WebPlayerActionsPorting {
subscribeCommand: SubscribeCommand;
unsubscribeCommand: UnsubscribeCommand;
addToWatchLaterCommand: AddToWatchLaterCommand;
removeFromWatchLaterCommand: RemoveFromWatchLaterCommand;
}
export interface AddToWatchLaterCommand {
clickTrackingParams: string;
playlistEditEndpoint: AddToWatchLaterCommandPlaylistEditEndpoint;
}
export interface AddToWatchLaterCommandPlaylistEditEndpoint {
playlistId: string;
actions: PurpleAction[];
}
export interface PurpleAction {
addedVideoId: string;
action: string;
}
export interface RemoveFromWatchLaterCommand {
clickTrackingParams: string;
playlistEditEndpoint: RemoveFromWatchLaterCommandPlaylistEditEndpoint;
}
export interface RemoveFromWatchLaterCommandPlaylistEditEndpoint {
playlistId: string;
actions: FluffyAction[];
}
export interface FluffyAction {
action: string;
removedVideoId: string;
}
export interface SubscribeCommand {
clickTrackingParams: string;
subscribeEndpoint: SubscribeEndpoint;
}
export interface Storyboards { export interface Storyboards {
playerStoryboardSpecRenderer: PlayerStoryboardSpecRenderer; playerStoryboardSpecRenderer: PlayerStoryboardSpecRenderer;
} }
@ -1384,7 +1301,7 @@ export interface PlayerOverlayRendererAction {
export interface LikeButtonRenderer { export interface LikeButtonRenderer {
target: Target; target: Target;
likeStatus: Kqp1PyPRBzA; likeStatus: LikeType;
trackingParams: string; trackingParams: string;
likesAllowed: boolean; likesAllowed: boolean;
serviceEndpoints: ServiceEndpoint[]; serviceEndpoints: ServiceEndpoint[];
@ -1396,13 +1313,14 @@ export interface ServiceEndpoint {
} }
export interface ServiceEndpointLikeEndpoint { export interface ServiceEndpointLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: Target; target: Target;
likeParams?: LikeParams; likeParams?: LikeParams;
dislikeParams?: LikeParams; dislikeParams?: LikeParams;
removeLikeParams?: LikeParams; removeLikeParams?: LikeParams;
} }
// TODO: Add more
export enum LikeParams { export enum LikeParams {
Oai3D = 'OAI%3D', Oai3D = 'OAI%3D',
} }
@ -1467,16 +1385,12 @@ export interface CurrentVideoEndpoint {
export interface CurrentVideoEndpointWatchEndpoint { export interface CurrentVideoEndpointWatchEndpoint {
videoId: string; videoId: string;
playlistId: PlaylistID; playlistId: string;
index: number; index: number;
playlistSetVideoId: string; playlistSetVideoId: string;
loggingContext: LoggingContext; loggingContext: LoggingContext;
} }
export enum PlaylistID {
RDAMVMrkaNKAvksDE = 'RDAMVMrkaNKAvksDE',
}
export interface PlayerPageWatchNextResponseResponseContext { export interface PlayerPageWatchNextResponseResponseContext {
serviceTrackingParams: ServiceTrackingParam[]; serviceTrackingParams: ServiceTrackingParam[];
} }
@ -1536,6 +1450,8 @@ export interface FlagEndpoint {
flagAction: string; flagAction: string;
} }
export type RepeatMode = 'NONE' | 'ONE' | 'ALL';
export interface Queue { export interface Queue {
automixItems: unknown[]; automixItems: unknown[];
autoplay: boolean; autoplay: boolean;
@ -1553,7 +1469,7 @@ export interface Queue {
nextQueueItemId: number; nextQueueItemId: number;
playbackContentMode: string; playbackContentMode: string;
queueContextParams: string; queueContextParams: string;
repeatMode: string; repeatMode: RepeatMode;
responsiveSignals: ResponsiveSignals; responsiveSignals: ResponsiveSignals;
selectedItemIndex: number; selectedItemIndex: number;
shuffleEnabled: boolean; shuffleEnabled: boolean;
@ -1642,23 +1558,15 @@ export interface PlaylistPanelVideoRendererNavigationEndpoint {
export interface FluffyWatchEndpoint { export interface FluffyWatchEndpoint {
videoId: string; videoId: string;
playlistId?: PlaylistID; playlistId?: string;
index: number; index: number;
params: FluffyParams; params: string;
playerParams?: PlayerParams; playerParams?: string;
playlistSetVideoId?: string; playlistSetVideoId?: string;
loggingContext?: LoggingContext; loggingContext?: LoggingContext;
watchEndpointMusicSupportedConfigs: FluffyWatchEndpointMusicSupportedConfigs; watchEndpointMusicSupportedConfigs: FluffyWatchEndpointMusicSupportedConfigs;
} }
export enum FluffyParams {
OAHyAQIIAQ3D3D = 'OAHyAQIIAQ%3D%3D',
}
export enum PlayerParams {
The8Aub = '8AUB',
}
export interface FluffyWatchEndpointMusicSupportedConfigs { export interface FluffyWatchEndpointMusicSupportedConfigs {
watchEndpointMusicConfig: FluffyWatchEndpointMusicConfig; watchEndpointMusicConfig: FluffyWatchEndpointMusicConfig;
} }

40
src/types/queue.ts Normal file
View File

@ -0,0 +1,40 @@
import type { YoutubePlayer } from '@/types/youtube-player';
import type { GetState, QueueItem } from '@/types/datahost-get-state';
type StoreState = GetState;
type Store = {
dispatch: (obj: {
type: string;
payload?: {
items?: QueueItem[];
};
}) => void;
getState: () => StoreState;
replaceReducer: (param1: unknown) => unknown;
subscribe: (callback: () => void) => unknown;
}
export type QueueElement = HTMLElement & {
dispatch(obj: {
type: string;
payload?: unknown;
}): void;
queue: QueueAPI;
};
export type QueueAPI = {
getItems(): QueueItem[];
store: {
store: Store,
};
continuation?: string;
autoPlaying?: boolean;
};
export type AppElement = HTMLElement & AppAPI;
export type AppAPI = {
queue_: QueueAPI;
playerApi_: YoutubePlayer;
openToast: (message: string) => void;
// TODO: Add more
};

View File

@ -0,0 +1,7 @@
import type { QueueItem } from '@/types/datahost-get-state';
export interface QueueResponse {
items?: QueueItem[];
autoPlaying?: boolean;
continuation?: string;
}