mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eeb1c986a | |||
| d37cd2418c | |||
| 8bd05f525d | |||
| 47b23b414c | |||
| 6f70d179c7 | |||
| 62a86e9267 | |||
| 6358a2d0b1 | |||
| 273633c2ce | |||
| 8b1209ef73 | |||
| 47505e9748 | |||
| 5178cc6bd8 | |||
| d9a27fff42 | |||
| 9e6560b814 | |||
| afdb19a742 | |||
| 0ae5b668f5 | |||
| 10533e28fa | |||
| 6189e67819 | |||
| f9ad505e40 | |||
| 9b011101ed | |||
| a6ed8bf3aa | |||
| 87acf4cf04 | |||
| b6fe2afd75 | |||
| 6d9bb8eb1c |
@ -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) |
|
||||||
|:---------------------------------------------------------------------------------------------------------:|
|
|:---------------------------------------------------------------------------------------------------------:|
|
||||||
||
|
||
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
|
|||||||
106
changelog.md
106
changelog.md
@ -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)
|
||||||
|
|||||||
10
package.json
10
package.json
@ -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
240
pnpm-lock.yaml
generated
@ -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==}
|
||||||
|
|||||||
@ -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>>) {
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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 설정"
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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' &&
|
||||||
|
|||||||
@ -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={{
|
||||||
|
|||||||
@ -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={{
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/plugins/shortcuts/mpris-service.d.ts
vendored
110
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
40
src/types/queue.ts
Normal 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
|
||||||
|
};
|
||||||
7
src/types/youtube-music-desktop-internal.ts
Normal file
7
src/types/youtube-music-desktop-internal.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { QueueItem } from '@/types/datahost-get-state';
|
||||||
|
|
||||||
|
export interface QueueResponse {
|
||||||
|
items?: QueueItem[];
|
||||||
|
autoPlaying?: boolean;
|
||||||
|
continuation?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user