mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Compare commits
1 Commits
v3.3.2
...
feat/chrom
| Author | SHA1 | Date | |
|---|---|---|---|
| a689980049 |
@ -33,7 +33,7 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md)
|
||||
|
||||
| Player Screen (album color theme & ambient light) |
|
||||
|:---------------------------------------------------------------------------------------------------------:|
|
||||
||
|
||||
||
|
||||
|
||||
## Translation
|
||||
|
||||
|
||||
106
changelog.md
106
changelog.md
@ -2,114 +2,8 @@
|
||||
|
||||
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)
|
||||
|
||||
> 5 January 2024
|
||||
|
||||
- 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)
|
||||
- chore(deps): update dependency rollup to v4.9.3 [`0c3c380`](https://github.com/th-ch/youtube-music/commit/0c3c3805918adf2a185a7f1dc67ea3af8135863d)
|
||||
|
||||
@ -478,7 +478,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="footer-copyright">© 2024 th-ch</div>
|
||||
<div class="footer-copyright">© 2021 th-ch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
28
package.json
28
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "3.3.2",
|
||||
"version": "3.2.2",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/main/index.js",
|
||||
"license": "MIT",
|
||||
@ -132,7 +132,8 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
||||
"@xhayper/discord-rpc@1.1.2": "patches/@xhayper__discord-rpc@1.1.2.patch"
|
||||
"@xhayper/discord-rpc@1.1.2": "patches/@xhayper__discord-rpc@1.1.2.patch",
|
||||
"@astronautlabs/mdns@1.0.10": "patches/@astronautlabs__mdns@1.0.10.patch"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@ -145,6 +146,7 @@
|
||||
"@floating-ui/dom": "1.6.3",
|
||||
"@foobar404/wave": "2.0.5",
|
||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||
"@jellybrick/electron-chromecast": "1.2.1",
|
||||
"@jellybrick/mpris-service": "2.1.4",
|
||||
"@xhayper/discord-rpc": "1.1.2",
|
||||
"async-mutex": "0.4.1",
|
||||
@ -160,13 +162,13 @@
|
||||
"electron-localshortcut": "3.2.1",
|
||||
"electron-store": "8.1.0",
|
||||
"electron-unhandled": "4.0.1",
|
||||
"electron-updater": "6.1.8",
|
||||
"electron-updater": "6.1.7",
|
||||
"fast-average-color": "9.4.0",
|
||||
"fast-equals": "5.0.1",
|
||||
"filenamify": "6.0.0",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "23.9.0",
|
||||
"i18next": "23.8.2",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
"keyboardevents-areequal": "0.2.2",
|
||||
"node-html-parser": "6.1.12",
|
||||
@ -176,7 +178,7 @@
|
||||
"serve": "14.2.1",
|
||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||
"solid-floating-ui": "0.3.1",
|
||||
"solid-js": "1.8.15",
|
||||
"solid-js": "1.8.14",
|
||||
"solid-styled-components": "0.28.5",
|
||||
"solid-transition-group": "0.2.3",
|
||||
"ts-morph": "21.0.1",
|
||||
@ -192,17 +194,17 @@
|
||||
"@types/howler": "2.2.11",
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@types/semver": "7.5.7",
|
||||
"@typescript-eslint/eslint-plugin": "7.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "7.0.1",
|
||||
"bufferutil": "4.0.8",
|
||||
"builtin-modules": "3.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "5.1.0",
|
||||
"discord-api-types": "0.37.70",
|
||||
"electron": "28.2.3",
|
||||
"discord-api-types": "0.37.69",
|
||||
"electron": "28.2.2",
|
||||
"electron-builder": "24.9.1",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"electron-vite": "2.0.0",
|
||||
"esbuild": "0.20.1",
|
||||
"esbuild": "0.20.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
@ -211,13 +213,13 @@
|
||||
"glob": "10.3.10",
|
||||
"node-gyp": "10.0.1",
|
||||
"playwright": "1.41.2",
|
||||
"rollup": "4.12.0",
|
||||
"rollup": "4.10.0",
|
||||
"typescript": "5.3.3",
|
||||
"utf-8-validate": "6.0.3",
|
||||
"vite": "5.1.3",
|
||||
"vite": "5.1.1",
|
||||
"vite-plugin-inspect": "0.8.3",
|
||||
"vite-plugin-resolve": "2.5.1",
|
||||
"vite-plugin-solid": "2.10.1",
|
||||
"vite-plugin-solid": "2.9.1",
|
||||
"ws": "8.16.0"
|
||||
},
|
||||
"auto-changelog": {
|
||||
@ -226,5 +228,5 @@
|
||||
"unreleased": true,
|
||||
"output": "changelog.md"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.3"
|
||||
"packageManager": "pnpm@8.15.1"
|
||||
}
|
||||
|
||||
33
patches/@astronautlabs__mdns@1.0.10.patch
Normal file
33
patches/@astronautlabs__mdns@1.0.10.patch
Normal file
@ -0,0 +1,33 @@
|
||||
diff --git a/dist/Responder.js b/dist/Responder.js
|
||||
index 7bb0e4e51f131cf257efc44190cf892ca0f1414e..f71688b8d7dc85bb6e23c9ec2aeec5cb98adc70a 100644
|
||||
--- a/dist/Responder.js
|
||||
+++ b/dist/Responder.js
|
||||
@@ -32,6 +32,7 @@ const StateMachine_1 = require("./StateMachine");
|
||||
const Probe_1 = require("./Probe");
|
||||
const Response_1 = require("./Response");
|
||||
const constants_1 = require("./constants");
|
||||
+const { setTimeout, clearTimeout } = require('node:timers');
|
||||
const ONE_SECOND = 1000;
|
||||
/**
|
||||
* Make ids, just to keep track of which responder is which in debug messages
|
||||
@@ -43,7 +44,7 @@ const uniqueId = () => `id#${++counter}`;
|
||||
* a responder has more than 15 conflicts in a small window then the responder
|
||||
* should be throttled to prevent it from spamming everyone. Conflict count
|
||||
* gets cleared after 15s w/o any conflicts
|
||||
- */
|
||||
+*/
|
||||
class ConflictCounter {
|
||||
constructor() {
|
||||
this._count = 0;
|
||||
diff --git a/dist/sleep.js b/dist/sleep.js
|
||||
index 8e11b3900747a68814697943ec16af3280bca8b3..7896d16b43d3eb8fff175c30ea5903d6237cc634 100644
|
||||
--- a/dist/sleep.js
|
||||
+++ b/dist/sleep.js
|
||||
@@ -5,6 +5,7 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.resetSleep = exports.sleep = void 0;
|
||||
const node_events_1 = require("node:events");
|
||||
+const { setInterval, clearInterval } = require('node:timers');
|
||||
exports.sleep = new node_events_1.EventEmitter();
|
||||
exports.sleep.setMaxListeners(100);
|
||||
let interval;
|
||||
570
pnpm-lock.yaml
generated
570
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -16,44 +16,25 @@ const migrations = {
|
||||
secret?: string;
|
||||
};
|
||||
if (lastfmConfig) {
|
||||
let scrobblerConfig = store.get(
|
||||
const scrobblerConfig = store.get(
|
||||
'plugins.scrobbler',
|
||||
) as {
|
||||
enabled?: boolean;
|
||||
scrobblers?: {
|
||||
lastfm?: {
|
||||
scrobblers: {
|
||||
lastfm: {
|
||||
enabled?: boolean;
|
||||
token?: string;
|
||||
sessionKey?: string;
|
||||
apiRoot?: string;
|
||||
apiKey?: string;
|
||||
session_key?: string;
|
||||
api_root?: string;
|
||||
api_key?: string;
|
||||
secret?: string;
|
||||
};
|
||||
};
|
||||
} | undefined;
|
||||
|
||||
if (!scrobblerConfig) {
|
||||
scrobblerConfig = {
|
||||
enabled: lastfmConfig.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
if (!scrobblerConfig.scrobblers) {
|
||||
scrobblerConfig.scrobblers = {
|
||||
lastfm: {},
|
||||
};
|
||||
}
|
||||
|
||||
scrobblerConfig.scrobblers.lastfm = {
|
||||
enabled: lastfmConfig.enabled,
|
||||
token: lastfmConfig.token,
|
||||
sessionKey: lastfmConfig.session_key,
|
||||
apiRoot: lastfmConfig.api_root,
|
||||
apiKey: lastfmConfig.api_key,
|
||||
secret: lastfmConfig.secret,
|
||||
};
|
||||
|
||||
scrobblerConfig.enabled = lastfmConfig.enabled;
|
||||
scrobblerConfig.scrobblers.lastfm = lastfmConfig;
|
||||
store.set('plugins.scrobbler', scrobblerConfig);
|
||||
store.delete('plugins.lastfm');
|
||||
}
|
||||
},
|
||||
'>=3.0.0'(store: Conf<Record<string, unknown>>) {
|
||||
|
||||
14
src/electron-chromecast.d.ts
vendored
Normal file
14
src/electron-chromecast.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
declare module '@jellybrick/electron-chromecast' {
|
||||
export const chrome: typeof window.chrome;
|
||||
export const requestHandler: (receiverList: Array<object>) => Promise<unknown>;
|
||||
export const castSetting: {
|
||||
devMode: boolean;
|
||||
};
|
||||
export const castConsole: {
|
||||
log: (message: unknown[]) => void;
|
||||
info: (message: unknown[]) => void;
|
||||
warn: (message: unknown[]) => void;
|
||||
error: (message: unknown[]) => void;
|
||||
};
|
||||
export const injectChromeCompatToObject: (obj: object) => void;
|
||||
}
|
||||
@ -46,7 +46,7 @@
|
||||
"hide-menu-enabled": {
|
||||
"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",
|
||||
"title": "Menü verstecken aktiviert"
|
||||
"title": "Menü Verstecken Aktiviert"
|
||||
},
|
||||
"need-to-restart": {
|
||||
"buttons": {
|
||||
@ -55,7 +55,7 @@
|
||||
},
|
||||
"detail": "\"{{pluginName}}\"-Erweiterung erfordert einen Neustart, um in Kraft zu treten",
|
||||
"message": "\"{{pluginName}}\" muss neugestartet werden",
|
||||
"title": "Neustart erforderlich"
|
||||
"title": "Neustart Erforderlich"
|
||||
},
|
||||
"unresponsive": {
|
||||
"buttons": {
|
||||
@ -75,7 +75,7 @@
|
||||
},
|
||||
"detail": "Eine neue Version ist verfügbar und kann unter {{downloadLink}} heruntergeladen werden",
|
||||
"message": "Eine neue Version ist verfügbar",
|
||||
"title": "Aktualisierung verfügbar"
|
||||
"title": "Aktualisierung Verfügbar"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@ -87,7 +87,7 @@
|
||||
"go-back": "Zurück gehen",
|
||||
"go-forward": "Vorwärts gehen",
|
||||
"quit": "Beenden",
|
||||
"restart": "Anwendung neustarten"
|
||||
"restart": "Anwendung Neustarten"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@ -124,7 +124,7 @@
|
||||
"language": {
|
||||
"dialog": {
|
||||
"message": "Sprache wird nach Neustart geändert",
|
||||
"title": "Sprache geändert"
|
||||
"title": "Sprache Geändert"
|
||||
},
|
||||
"label": "Sprache",
|
||||
"submenu": {
|
||||
@ -212,14 +212,6 @@
|
||||
},
|
||||
"album-color-theme": {
|
||||
"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"
|
||||
},
|
||||
"ambient-mode": {
|
||||
@ -238,7 +230,7 @@
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"label": "Transparenz",
|
||||
"label": "Durchsichtigkeit",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
@ -283,7 +275,7 @@
|
||||
"description": "Untertitelwähler für YouTube Music-Audio-Lieder",
|
||||
"menu": {
|
||||
"autoload": "Wähle automatisch den zuletzt verwendeten Untertitel",
|
||||
"disable-captions": "Standardmäßig keine Untertitel"
|
||||
"disable-captions": "Standartmäßig keine Untertitel"
|
||||
},
|
||||
"name": "Untertitelwähler",
|
||||
"prompt": {
|
||||
@ -585,25 +577,23 @@
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": "ListenBrainz-Benutzer-Token eintragen"
|
||||
},
|
||||
"scrobble-other-media": "Andere Medien scrobbeln"
|
||||
}
|
||||
},
|
||||
"name": "Scrobbler",
|
||||
"prompt": {
|
||||
"lastfm": {
|
||||
"api-key": "Last.fm API-Schlüssel",
|
||||
"api-secret": "Last.fm API-Kennwort"
|
||||
"api-secret": "Last.fm API secret"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": {
|
||||
"label": "ListenBrainz-Benutzer-Token eintragen:",
|
||||
"title": "ListenBrainz-Token"
|
||||
"label": "ListenBrainz-Benutzer-Token eintragen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"override-media-keys": "Medientasten überschreiben",
|
||||
"set-keybinds": "Globale Liedsteuerung setzen"
|
||||
|
||||
@ -579,14 +579,6 @@
|
||||
},
|
||||
"scrobbler": {
|
||||
"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": {
|
||||
"scrobble-other-media": "Scrobble other media",
|
||||
"lastfm": {
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
"submenu": {
|
||||
"auto-reset-app-cache": "Mengatur ulang cache aplikasi saat aplikasi dimulai",
|
||||
"disable-hardware-acceleration": "Menonaktifkan akselerasi perangkat keras",
|
||||
"edit-config-json": "Ubah config.json",
|
||||
"edit-config-json": "Edit config.json",
|
||||
"override-user-agent": "Mengesampingkan User-Agent",
|
||||
"restart-on-config-changes": "Mulai ulang pada perubahan konfigurasi",
|
||||
"set-proxy": {
|
||||
@ -194,7 +194,7 @@
|
||||
"show": "Tampilkan jendela",
|
||||
"tooltip": {
|
||||
"default": "YouTube Musik",
|
||||
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
|
||||
"with-song-info": "YouTube Music: {{artist}} - {{Judul}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -207,7 +207,7 @@
|
||||
"name": "Pemblokir Iklan"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Tambah tombol Suka, Batal Suka, Tidak Suka dan Batal Tidak Suka untuk diterapkan ke semua lagu dalam daftar putar atau album",
|
||||
"description": "Menambahkan tombol Tidak Suka, Tidak Suka, Suka, dan Tidak Suka untuk menerapkannya ke semua lagu dalam daftar putar atau album",
|
||||
"name": "Tindakan Album"
|
||||
},
|
||||
"album-color-theme": {
|
||||
@ -398,285 +398,7 @@
|
||||
"video-id-not-found": "Video tidak ditemukan",
|
||||
"writing-id3": "Menulis tanda ID3…"
|
||||
}
|
||||
},
|
||||
"description": "Unduh MP3 / sumber suara secara langsung via antarmuka",
|
||||
"menu": {
|
||||
"choose-download-folder": "Pilih folder unduhan",
|
||||
"download-playlist": "Unduh daftar putar",
|
||||
"presets": "Prasetel",
|
||||
"skip-existing": "Lewati berkas yang sudah ada"
|
||||
},
|
||||
"name": "Pengunduh",
|
||||
"renderer": {
|
||||
"can-not-update-progress": "Tidak dapat memperbarui proses"
|
||||
},
|
||||
"templates": {
|
||||
"button": "Unduh"
|
||||
}
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "Buat penggeser volume menjadi eksponen sehingga memudahkan memilih volume yang lebih rendah.",
|
||||
"name": "Volume Eksponen"
|
||||
},
|
||||
"in-app-menu": {
|
||||
"description": "Buat bilah-menu terlihat indah, gelap atau serupa dengan album",
|
||||
"menu": {
|
||||
"hide-dom-window-controls": "Sembunyikan DOM pengendali jendela"
|
||||
},
|
||||
"name": "Menu di Aplikasi"
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Tambah dukungan Lumia Stream",
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Tambah dukungan lirik untuk kebanyakan lagu",
|
||||
"menu": {
|
||||
"romanized-lyrics": "Romanisasi Lirik"
|
||||
},
|
||||
"name": "Lirik Genius",
|
||||
"renderer": {
|
||||
"fetched-lyrics": "Lirik yang diambil untuk Genius"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "Bagikan daftar putar dengan yang lain. Saat host memainkan lagu, semua orang akan mendengarkan lagu yang sama",
|
||||
"dialog": {
|
||||
"enter-host": "Masukkan ID Host"
|
||||
},
|
||||
"internal": {
|
||||
"save": "Simpan",
|
||||
"track-source": "Sumber Trek",
|
||||
"unknown-user": "Pengguna Tidak Diketahui"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "Salin ID Host",
|
||||
"close": "Tutup Musik Bersama",
|
||||
"connected-users": "Pengguna Terhubung",
|
||||
"disconnect": "Putuskan Musik Bersama",
|
||||
"empty-user": "Tidak ada pengguna terhubung",
|
||||
"host": "Host Musik Bersama",
|
||||
"join": "Gabung Musik Bersama",
|
||||
"permission": {
|
||||
"all": "Izinkan tamu untuk mengendalikan daftar putar dan pemutar",
|
||||
"host-only": "Hanya host yang dapat mengendalikan daftar putar dan pemutar",
|
||||
"playlist": "Izinkan tamu untuk mengendalikan daftar putar"
|
||||
},
|
||||
"set-permission": "Ubah Pengendali Izin",
|
||||
"status": {
|
||||
"disconnected": "Terputus",
|
||||
"guest": "Terhubung sebagai Tamu",
|
||||
"host": "Terhubung sebagai Host"
|
||||
}
|
||||
},
|
||||
"name": "Musik Bersama [Beta]",
|
||||
"toast": {
|
||||
"add-song-failed": "Gagal untuk menambahkan lagu",
|
||||
"closed": "Musik Bersama ditutup",
|
||||
"disconnected": "Musik Bersama terputus",
|
||||
"host-failed": "Gagal untuk memulai Musik Bersama",
|
||||
"id-copied": "ID Host tersalin ke papan klip",
|
||||
"id-copy-failed": "Gagal menyalin ID Host ke papan klip",
|
||||
"join-failed": "Gagal untuk bergabung ke Musik Bersama",
|
||||
"joined": "Bergabung ke Musik Bersama",
|
||||
"permission-changed": "Perizinan Musik Bersama diubah ke \"{{permission}}\"",
|
||||
"remove-song-failed": "Gagal menghapus lagu",
|
||||
"user-connected": "{{name}} bergabung ke Musik Bersama",
|
||||
"user-disconnected": "{{name}} meninggalkan Musik Bersama"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "panah navigasi Selanjutnya/Sebelumnya terintegrasi pada antarmuka, layaknya peramban kesukaan Anda",
|
||||
"name": "Navigasi"
|
||||
},
|
||||
"no-google-login": {
|
||||
"description": "Hapus tombol dan tautan masuk Google dari antarmuka",
|
||||
"name": "Tanpa Google Login"
|
||||
},
|
||||
"notifications": {
|
||||
"description": "Tampilkan pemberitahuan saat lagu dimainkan (pemberitahuan interaktif tersedia di Windows)",
|
||||
"menu": {
|
||||
"interactive": "Pemberitahuan Interaktif",
|
||||
"interactive-settings": {
|
||||
"label": "Pengaturan Interaktif",
|
||||
"submenu": {
|
||||
"hide-button-text": "Sembunyikan teks tombol",
|
||||
"refresh-on-play-pause": "Segarkan saat Putar/Jeda",
|
||||
"tray-controls": "Buka/Tutup saat baki ditekan"
|
||||
}
|
||||
},
|
||||
"priority": "Prioritas Pemberitahuan",
|
||||
"toast-style": "Gaya Toast",
|
||||
"unpause-notification": "Tampilkan pemberitahuan saat tidak dijeda"
|
||||
},
|
||||
"name": "Pemberitahuan"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Izinkan untuk memindahkan aplikasi ke mode gambar-dalam-gambar",
|
||||
"menu": {
|
||||
"always-on-top": "Selalu di atas",
|
||||
"hotkey": {
|
||||
"label": "Pintasan",
|
||||
"prompt": {
|
||||
"keybind-options": {
|
||||
"hotkey": "Pintasan"
|
||||
},
|
||||
"label": "Pilih pintasan untuk beralih ke gambar-dalam-gambar",
|
||||
"title": "Pintasan gambar-dalam-gambar"
|
||||
}
|
||||
},
|
||||
"save-window-position": "Simpan posisi jendela",
|
||||
"save-window-size": "Simpan ukuran jendela",
|
||||
"use-native-pip": "Gunakan PiP bawaan peramban"
|
||||
},
|
||||
"name": "Gambar-dalam-gambar",
|
||||
"templates": {
|
||||
"button": "Gambar-dalam-gambar"
|
||||
}
|
||||
},
|
||||
"playback-speed": {
|
||||
"description": "Dengarkan cepat, dengarkan perlahan! Tambahkan penggeser untuk mengendalikan kecepatan lagu",
|
||||
"name": "Kecepatan Pemutar",
|
||||
"templates": {
|
||||
"button": "Kecepatan"
|
||||
}
|
||||
},
|
||||
"precise-volume": {
|
||||
"description": "Kendalikan volume secara presisi menggunakan roda tetikus/pintasan, dengan HUD kustom dan langkah volume yang dapat diatur",
|
||||
"menu": {
|
||||
"arrows-shortcuts": "Kendali Tombol Panah Lokal",
|
||||
"custom-volume-steps": "Atur Langkah Volume Kustom",
|
||||
"global-shortcuts": "Pintasan Global"
|
||||
},
|
||||
"name": "Volume Presisi",
|
||||
"prompt": {
|
||||
"global-shortcuts": {
|
||||
"keybind-options": {
|
||||
"decrease": "Kurangi Volume",
|
||||
"increase": "Tingkatkan Volume"
|
||||
},
|
||||
"label": "Pilih Pintasan Volume Global:",
|
||||
"title": "Pintasan Volume Global"
|
||||
},
|
||||
"volume-steps": {
|
||||
"label": "Pilih Langkah Peningkatan/Pengurangan Volume",
|
||||
"title": "Langkah Volume"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quality-changer": {
|
||||
"backend": {
|
||||
"dialog": {
|
||||
"quality-changer": {
|
||||
"detail": "Kualitas Terkini: {{quality}}",
|
||||
"message": "Pilih Kualitas Video:",
|
||||
"title": "Pilih Kualitas Video"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Izinkan untuk mengubah kualitas video dengan tombol pada hamparan video",
|
||||
"name": "Pengubah Kualitas Video"
|
||||
},
|
||||
"scrobbler": {
|
||||
"description": "Tambahkan dukungan scrobbling (mis. last.fm, Listenbrainz)",
|
||||
"menu": {
|
||||
"lastfm": {
|
||||
"api-settings": "Pengaturan API Last.fm"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": "Masukkan token pengguna ListenBrainz"
|
||||
},
|
||||
"scrobble-other-media": "Scrobble media lain"
|
||||
},
|
||||
"name": "Scrobbler",
|
||||
"prompt": {
|
||||
"lastfm": {
|
||||
"api-key": "Kunci API Last.fm",
|
||||
"api-secret": "Secret API Last.fm"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": {
|
||||
"label": "Masukkan token pengguna ListenBrainz Anda:",
|
||||
"title": "Token ListenBrainz"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"description": "Izinkan pengaturan pintasan global untuk pemutar (main/jeda/selanjutnya/sebelumnya) dan mematikan OSD media dengan mengesampingkan tombol media, mengaktifkan Ctrl/CMD + F untuk pencarian, mengaktifkan dukungan MPRIS Linux untuk tombol media, dan tombol pintasan kustom untuk pengguna lanjutan",
|
||||
"menu": {
|
||||
"override-media-keys": "Timpa Tombol Media",
|
||||
"set-keybinds": "Atur Pengendali Lagu Global"
|
||||
},
|
||||
"name": "Pintasan (& MPRIS)",
|
||||
"prompt": {
|
||||
"keybind": {
|
||||
"keybind-options": {
|
||||
"next": "Selanjutnya",
|
||||
"play-pause": "Main / Jeda",
|
||||
"previous": "Sebelumnya"
|
||||
},
|
||||
"label": "Pilih Pintasan Global untuk Pengendali Lagu:",
|
||||
"title": "Pintasan Global"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skip-disliked-songs": {
|
||||
"description": "Lewati lagu yang tidak disukai",
|
||||
"name": "Lewati Lagu yang Tidak Disukai"
|
||||
},
|
||||
"skip-silences": {
|
||||
"description": "Otomatis lewati bagian hening dari lagu",
|
||||
"name": "Lewati Keheningan"
|
||||
},
|
||||
"sponsorblock": {
|
||||
"description": "Otomatis Melewati bagian yang bukan musik seperti intro/outro atau bagian dari video musik di mana lagu tidak dimainkan",
|
||||
"name": "SponsorBlock"
|
||||
},
|
||||
"taskbar-mediacontrol": {
|
||||
"description": "Kendalikan pemutaran dari bilah alat Windows",
|
||||
"name": "Pengendali Media di Bilah Alat"
|
||||
},
|
||||
"touchbar": {
|
||||
"description": "Tambahkan widget TouchBar untuk pengguna macOS",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Integrasi dengan plugin Tuna OBS",
|
||||
"name": "Tuna OBS"
|
||||
},
|
||||
"video-toggle": {
|
||||
"description": "Tambahkan tombol untuk beralih antara mode Lagu/Video. secara opsional juga dapat menghapus keseluruhan tab video",
|
||||
"menu": {
|
||||
"align": {
|
||||
"label": "Perataan",
|
||||
"submenu": {
|
||||
"left": "Kiri",
|
||||
"middle": "Tengah",
|
||||
"right": "Kanan"
|
||||
}
|
||||
},
|
||||
"force-hide": "Paksa hapus tab video",
|
||||
"mode": {
|
||||
"label": "Mode",
|
||||
"submenu": {
|
||||
"custom": "Peralih kustom",
|
||||
"disabled": "Mati",
|
||||
"native": "Peralih bawaan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Peralih Video",
|
||||
"templates": {
|
||||
"button": "Lagu"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
"description": "Tambahkan visualisator ke pemutar",
|
||||
"menu": {
|
||||
"visualizer-type": "Tipe Visualisator"
|
||||
},
|
||||
"name": "Visualisator"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,14 +212,6 @@
|
||||
},
|
||||
"album-color-theme": {
|
||||
"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"
|
||||
},
|
||||
"ambient-mode": {
|
||||
@ -585,8 +577,7 @@
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": "Inserire il token utente per ListenBrainz"
|
||||
},
|
||||
"scrobble-other-media": "Scrobble altri media"
|
||||
}
|
||||
},
|
||||
"name": "Scrobbler",
|
||||
"prompt": {
|
||||
|
||||
@ -579,14 +579,6 @@
|
||||
},
|
||||
"scrobbler": {
|
||||
"description": "스크로블링 지원을 추가합니다 (예: last.fm, Listenbrainz)",
|
||||
"dialog": {
|
||||
"lastfm": {
|
||||
"auth-failed": {
|
||||
"message": "Last.fm 인증에 실패했습니다\n다음에 다시 시작할 때까지 팝업을 숨깁니다.",
|
||||
"title": "인증 실패"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"lastfm": {
|
||||
"api-settings": "Last.fm API 설정"
|
||||
|
||||
@ -212,13 +212,6 @@
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Stosuje dynamiczny motyw i efekty wizualne w oparciu o paletę kolorów albumu",
|
||||
"menu": {
|
||||
"color-mix-ratio": {
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Motyw kolorów albumu"
|
||||
},
|
||||
"ambient-mode": {
|
||||
@ -576,24 +569,6 @@
|
||||
"description": "Umożliwia zmianę jakości wideo za pomocą przycisku na nakładce wideo",
|
||||
"name": "Zmieniacz jakości wideo"
|
||||
},
|
||||
"scrobbler": {
|
||||
"menu": {
|
||||
"listenbrainz": {
|
||||
"token": "Podaj token użytkownika ListenBrainz"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
"lastfm": {
|
||||
"api-key": "klucz API Last.fm"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": {
|
||||
"label": "Podaj swój token użytkownika ListenBrainz:",
|
||||
"title": "Token ListenBrainz"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"description": "Umożliwia ustawienie globalnych skrótów klawiszowych do odtwarzania (odtwarzanie/pauza/następny/poprzedni) + wyłączanie OSD multimediów poprzez zastąpienie klawiszy multimediów, włączając kombinację klawiszy Ctrl/CMD + F w celu wyszukiwania, obsługę Linux MPRIS dla klawiszy multimediów oraz niestandardowe skróty klawiszowe dla zaawansowanych użytkowników",
|
||||
"menu": {
|
||||
|
||||
10
src/index.ts
10
src/index.ts
@ -53,8 +53,6 @@ import {
|
||||
import { LoggerPrefix } from '@/utils';
|
||||
import { loadI18n, setLanguage, t } from '@/i18n';
|
||||
|
||||
import ErrorHtmlAsset from '@assets/error.html?asset';
|
||||
|
||||
import type { PluginConfig } from '@/types/plugins';
|
||||
|
||||
if (!is.macOS()) {
|
||||
@ -507,7 +505,7 @@ app.once('browser-window-created', (_event, win) => {
|
||||
if (errorCode !== -3) {
|
||||
// -3 is a false positive
|
||||
win.webContents.send('log', log);
|
||||
win.webContents.loadFile(ErrorHtmlAsset);
|
||||
win.webContents.loadFile(path.join(__dirname, 'error.html'));
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -588,7 +586,7 @@ app.whenReady().then(async () => {
|
||||
);
|
||||
try {
|
||||
// Check if shortcut is registered and valid
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if it doesn't exist yet
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
|
||||
if (
|
||||
shortcutDetails.target !== appLocation ||
|
||||
shortcutDetails.appUserModelId !== appID
|
||||
@ -673,9 +671,7 @@ app.whenReady().then(async () => {
|
||||
);
|
||||
}
|
||||
|
||||
const splited = decodeURIComponent(command).split(' ');
|
||||
|
||||
handleProtocol(splited.shift()!, splited);
|
||||
handleProtocol(command);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
|
||||
import { fetchFromGenius } from '@/plugins/lyrics-genius/main';
|
||||
import { isEnabled } from '@/config/plugins';
|
||||
import { cleanupName, getImage, MediaType, type SongInfo } from '@/providers/song-info';
|
||||
import { cleanupName, getImage, SongInfo } from '@/providers/song-info';
|
||||
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
||||
import { cache } from '@/providers/decorators';
|
||||
|
||||
@ -686,7 +686,6 @@ const getMetadata = (info: TrackInfo): CustomSongInfo => ({
|
||||
?.url,
|
||||
views: info.basic_info.view_count!,
|
||||
songDuration: info.basic_info.duration!,
|
||||
mediaType: MediaType.Audio,
|
||||
});
|
||||
|
||||
// This is used to bypass age restrictions
|
||||
|
||||
@ -4,24 +4,8 @@ export interface InAppMenuConfig {
|
||||
}
|
||||
export const defaultInAppMenuConfig: InAppMenuConfig = {
|
||||
enabled:
|
||||
(
|
||||
(
|
||||
typeof window !== 'undefined' &&
|
||||
!window.navigator?.userAgent?.toLowerCase().includes('mac')
|
||||
) ||
|
||||
(
|
||||
typeof global !== 'undefined' &&
|
||||
global.process?.platform !== 'darwin'
|
||||
)
|
||||
) && (
|
||||
(
|
||||
typeof window !== 'undefined' &&
|
||||
!window.navigator?.userAgent?.toLowerCase().includes('linux')
|
||||
) ||
|
||||
(
|
||||
typeof global !== 'undefined' &&
|
||||
global.process?.platform !== 'linux'
|
||||
)
|
||||
),
|
||||
(typeof window !== 'undefined' &&
|
||||
!window.navigator?.userAgent?.includes('mac')) ||
|
||||
(typeof global !== 'undefined' && global.process?.platform !== 'darwin'),
|
||||
hideDOMWindowControls: false,
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import is from 'electron-is';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { InAppMenuConfig } from './constants';
|
||||
import type { InAppMenuConfig } from './index';
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { JSX, splitProps } from 'solid-js';
|
||||
import { css } from 'solid-styled-components';
|
||||
|
||||
import { cache } from '@/providers/decorators';
|
||||
|
||||
const menuStyle = cache(() => css`
|
||||
|
||||
@ -4,7 +4,6 @@ import { css } from 'solid-styled-components';
|
||||
import { Transition } from 'solid-transition-group';
|
||||
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom';
|
||||
import { useFloating } from 'solid-floating-ui';
|
||||
|
||||
import { cache } from '@/providers/decorators';
|
||||
|
||||
const panelStyle = cache(() => css`
|
||||
@ -132,7 +131,6 @@ export const Panel = (props: PanelProps) => {
|
||||
<Show when={local.open}>
|
||||
<ul
|
||||
{...leftProps}
|
||||
data-ytmd-sub-panel={true}
|
||||
ref={setPanel}
|
||||
class={panelStyle()}
|
||||
style={{
|
||||
|
||||
@ -9,10 +9,9 @@ import { PanelItem } from './PanelItem';
|
||||
import { IconButton } from './IconButton';
|
||||
import { WindowController } from './WindowController';
|
||||
|
||||
import { cache } from '@/providers/decorators';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { InAppMenuConfig } from '../constants';
|
||||
import { cache } from '@/providers/decorators';
|
||||
|
||||
const titleStyle = cache(() => css`
|
||||
-webkit-app-region: drag;
|
||||
@ -244,19 +243,6 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
|
||||
props.ipc.on('window-maximize', refetchMaximize);
|
||||
props.ipc.on('window-unmaximize', refetchMaximize);
|
||||
|
||||
// close menu when the outside of the panel or sub-panel is clicked
|
||||
document.body.addEventListener('click', (e) => {
|
||||
if (
|
||||
e.target instanceof HTMLElement &&
|
||||
!(
|
||||
e.target.closest('nav[data-ytmd-main-panel]') ||
|
||||
e.target.closest('ul[data-ytmd-sub-panel]')
|
||||
)
|
||||
) {
|
||||
setOpenTarget(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
@ -266,7 +252,7 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<nav data-ytmd-main-panel={true} class={titleStyle()} data-macos={props.isMacOS}>
|
||||
<nav class={titleStyle()} data-macos={props.isMacOS}>
|
||||
<IconButton
|
||||
onClick={() => setCollapsed(!collapsed())}
|
||||
style={{
|
||||
|
||||
@ -38,7 +38,7 @@ export const fetchFromGenius = async (metadata: SongInfo) => {
|
||||
const songArtist = `${cleanupName(metadata.artist)}`;
|
||||
let lyrics: string | null;
|
||||
|
||||
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise, normal
|
||||
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
|
||||
Genius Lyrics behavior is observed.
|
||||
*/
|
||||
let hasAsianChars = false;
|
||||
|
||||
@ -74,7 +74,7 @@ export class Connection {
|
||||
return conn;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
async disconnect() {
|
||||
if (this._mode === 'disconnected') throw new Error('Already disconnected');
|
||||
|
||||
this._mode = 'disconnected';
|
||||
|
||||
@ -6,9 +6,9 @@ import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
|
||||
import { getDefaultProfile, type Permission, type Profile, type VideoData } from './types';
|
||||
import { AppAPI, getDefaultProfile, Permission, Profile, VideoData } from './types';
|
||||
import { Queue } from './queue';
|
||||
import { Connection, type ConnectionEventUnion } from './connection';
|
||||
import { Connection, ConnectionEventUnion } from './connection';
|
||||
import { createHostPopup } from './ui/host';
|
||||
import { createGuestPopup } from './ui/guest';
|
||||
import { createSettingPopup } from './ui/setting';
|
||||
@ -19,7 +19,6 @@ import style from './style.css?inline';
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||
import type { AppElement } from '@/types/queue';
|
||||
|
||||
type RawAccountData = {
|
||||
accountName: {
|
||||
@ -42,7 +41,7 @@ export default createPlugin<
|
||||
{
|
||||
connection?: Connection;
|
||||
ipc?: RendererContext<never>['ipc'];
|
||||
api: AppElement | null;
|
||||
api: HTMLElement & AppAPI | null;
|
||||
queue?: Queue;
|
||||
playerApi?: YoutubePlayer;
|
||||
showPrompt: (title: string, label: string) => Promise<string>;
|
||||
@ -558,7 +557,7 @@ export default createPlugin<
|
||||
start({ ipc }) {
|
||||
this.ipc = ipc;
|
||||
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>;
|
||||
this.api = document.querySelector<AppElement>('ytmusic-app');
|
||||
this.api = document.querySelector<HTMLElement & AppAPI>('ytmusic-app');
|
||||
|
||||
/* setup */
|
||||
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML);
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { getMusicQueueRenderer } from './song';
|
||||
import { mapQueueItem } from './utils';
|
||||
|
||||
import { ConnectionEventUnion } from '@/plugins/music-together/connection';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
|
||||
import type { Profile, VideoData } from '../types';
|
||||
import type { Profile, QueueAPI, VideoData } from '../types';
|
||||
import type { QueueItem } from '@/types/datahost-get-state';
|
||||
import type { QueueElement } from '@/types/queue';
|
||||
|
||||
const getHeaderPayload = (() => {
|
||||
let payload: {
|
||||
@ -104,29 +103,26 @@ const getHeaderPayload = (() => {
|
||||
export type QueueOptions = {
|
||||
videoList?: VideoData[];
|
||||
owner?: Profile;
|
||||
queue?: QueueElement;
|
||||
queue?: HTMLElement & QueueAPI;
|
||||
getProfile: (id: string) => Profile | undefined;
|
||||
}
|
||||
export type QueueEventListener = (event: ConnectionEventUnion) => void;
|
||||
|
||||
export class Queue {
|
||||
private readonly queue: QueueElement;
|
||||
|
||||
private queue: (HTMLElement & QueueAPI);
|
||||
private originalDispatch?: (obj: {
|
||||
type: string;
|
||||
payload?: { items?: QueueItem[] | undefined; };
|
||||
}) => void;
|
||||
|
||||
private internalDispatch = false;
|
||||
private ignoreFlag = false;
|
||||
private listeners: QueueEventListener[] = [];
|
||||
|
||||
private owner: Profile | null;
|
||||
private readonly getProfile: (id: string) => Profile | undefined;
|
||||
private owner: Profile | null = null;
|
||||
private getProfile: (id: string) => Profile | undefined;
|
||||
|
||||
constructor(options: QueueOptions) {
|
||||
this.getProfile = options.getProfile;
|
||||
this.queue = options.queue ?? (document.querySelector<QueueElement>('#queue')!);
|
||||
this.queue = options.queue ?? document.querySelector<HTMLElement & QueueAPI>('#queue')!;
|
||||
this.owner = options.owner ?? null;
|
||||
this._videoList = options.videoList ?? [];
|
||||
}
|
||||
@ -139,11 +135,11 @@ export class Queue {
|
||||
}
|
||||
|
||||
get selectedIndex() {
|
||||
return mapQueueItem((it) => it?.selected, this.queue.queue.store.store.getState().queue.items).findIndex(Boolean) ?? 0;
|
||||
return mapQueueItem((it) => it?.selected, this.queue.store.getState().queue.items).findIndex(Boolean) ?? 0;
|
||||
}
|
||||
|
||||
get rawItems() {
|
||||
return this.queue?.queue.store.store.getState().queue.items;
|
||||
return this.queue?.store.getState().queue.items;
|
||||
}
|
||||
|
||||
get flatItems() {
|
||||
@ -173,8 +169,8 @@ export class Queue {
|
||||
this.queue?.dispatch({
|
||||
type: 'ADD_ITEMS',
|
||||
payload: {
|
||||
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
||||
index: index ?? this.queue.queue.store.store.getState().queue.items.length ?? 0,
|
||||
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId,
|
||||
index: index ?? this.queue.store.getState().queue.items.length ?? 0,
|
||||
items,
|
||||
shuffleEnabled: false,
|
||||
shouldAssignIds: true
|
||||
@ -253,7 +249,7 @@ export class Queue {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.originalDispatch) this.queue.queue.store.store.dispatch = this.originalDispatch;
|
||||
if (this.originalDispatch) this.queue.store.dispatch = this.originalDispatch;
|
||||
}
|
||||
|
||||
injection() {
|
||||
@ -262,8 +258,8 @@ export class Queue {
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalDispatch = this.queue.queue.store.store.dispatch;
|
||||
this.queue.queue.store.store.dispatch = (event) => {
|
||||
this.originalDispatch = this.queue.store.dispatch;
|
||||
this.queue.store.dispatch = (event) => {
|
||||
if (!this.queue || !this.owner) {
|
||||
console.error('Queue is not initialized!');
|
||||
return;
|
||||
@ -365,13 +361,10 @@ export class Queue {
|
||||
|
||||
const fakeContext = {
|
||||
...this.queue,
|
||||
queue: {
|
||||
...this.queue.queue,
|
||||
store: {
|
||||
...this.queue.queue.store,
|
||||
dispatch: this.originalDispatch,
|
||||
}
|
||||
},
|
||||
store: {
|
||||
...this.queue.store,
|
||||
dispatch: this.originalDispatch
|
||||
}
|
||||
};
|
||||
this.originalDispatch?.call(fakeContext, event);
|
||||
};
|
||||
@ -407,7 +400,7 @@ export class Queue {
|
||||
type: 'UPDATE_ITEMS',
|
||||
payload: {
|
||||
items: items,
|
||||
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
||||
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId,
|
||||
shouldAssignIds: true,
|
||||
currentIndex: -1
|
||||
}
|
||||
|
||||
@ -1,3 +1,37 @@
|
||||
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 = {
|
||||
id: string;
|
||||
handleId: string;
|
||||
|
||||
@ -307,9 +307,9 @@ export default (
|
||||
savedNotification?.close();
|
||||
});
|
||||
|
||||
changeProtocolHandler((cmd, args) => {
|
||||
changeProtocolHandler((cmd) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd as keyof typeof songControls](args as never);
|
||||
songControls[cmd as keyof typeof songControls]();
|
||||
if (
|
||||
config().refreshOnPlayPause &&
|
||||
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))
|
||||
|
||||
@ -11,7 +11,7 @@ export interface ScrobblerPluginConfig {
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
scrobbleOtherMedia: boolean,
|
||||
scrobble_other_media: boolean,
|
||||
scrobblers: {
|
||||
lastfm: {
|
||||
/**
|
||||
@ -27,19 +27,19 @@ export interface ScrobblerPluginConfig {
|
||||
/**
|
||||
* Session key used for scrobbling
|
||||
*/
|
||||
sessionKey: string | undefined,
|
||||
session_key: string | undefined,
|
||||
/**
|
||||
* Root of the Last.fm API
|
||||
*
|
||||
* @default 'http://ws.audioscrobbler.com/2.0/'
|
||||
*/
|
||||
apiRoot: string,
|
||||
api_root: string,
|
||||
/**
|
||||
* Last.fm api key registered by @semvis123
|
||||
*
|
||||
* @default '04d76faaac8726e60988e14c105d421a'
|
||||
*/
|
||||
apiKey: string,
|
||||
api_key: string,
|
||||
/**
|
||||
* Last.fm api secret registered by @semvis123
|
||||
*
|
||||
@ -63,27 +63,27 @@ export interface ScrobblerPluginConfig {
|
||||
*
|
||||
* @default 'https://api.listenbrainz.org/1/'
|
||||
*/
|
||||
apiRoot: string,
|
||||
api_root: string,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultConfig: ScrobblerPluginConfig = {
|
||||
enabled: false,
|
||||
scrobbleOtherMedia: true,
|
||||
scrobble_other_media: true,
|
||||
scrobblers: {
|
||||
lastfm: {
|
||||
enabled: false,
|
||||
token: undefined,
|
||||
sessionKey: undefined,
|
||||
apiRoot: 'https://ws.audioscrobbler.com/2.0/',
|
||||
apiKey: '04d76faaac8726e60988e14c105d421a',
|
||||
session_key: undefined,
|
||||
api_root: 'http://ws.audioscrobbler.com/2.0/',
|
||||
api_key: '04d76faaac8726e60988e14c105d421a',
|
||||
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
||||
},
|
||||
listenbrainz: {
|
||||
enabled: false,
|
||||
token: undefined,
|
||||
apiRoot: 'https://api.listenbrainz.org/1/',
|
||||
api_root: 'https://api.listenbrainz.org/1/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import registerCallback, { MediaType, type SongInfo } from '@/providers/song-info';
|
||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||
import { createBackend } from '@/utils';
|
||||
|
||||
import { ScrobblerPluginConfig } from './index';
|
||||
import { LastFmScrobbler } from './services/lastfm';
|
||||
import { ListenbrainzScrobbler } from './services/listenbrainz';
|
||||
|
||||
import type { ScrobblerPluginConfig } from './index';
|
||||
import type { ScrobblerBase } from './services/base';
|
||||
import { ScrobblerBase } from './services/base';
|
||||
|
||||
export type SetConfType = (
|
||||
conf: Partial<Omit<ScrobblerPluginConfig, 'enabled'>>,
|
||||
@ -15,17 +12,14 @@ export type SetConfType = (
|
||||
|
||||
export const backend = createBackend<{
|
||||
config?: ScrobblerPluginConfig;
|
||||
window?: BrowserWindow;
|
||||
enabledScrobblers: Map<string, ScrobblerBase>;
|
||||
toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow): void;
|
||||
createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType): Promise<void>;
|
||||
setConfig?: SetConfType;
|
||||
toggleScrobblers(config: ScrobblerPluginConfig): void;
|
||||
}, ScrobblerPluginConfig>({
|
||||
enabledScrobblers: new Map(),
|
||||
|
||||
toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow) {
|
||||
toggleScrobblers(config: ScrobblerPluginConfig) {
|
||||
if (config.scrobblers.lastfm && config.scrobblers.lastfm.enabled) {
|
||||
this.enabledScrobblers.set('lastfm', new LastFmScrobbler(window));
|
||||
this.enabledScrobblers.set('lastfm', new LastFmScrobbler());
|
||||
} else {
|
||||
this.enabledScrobblers.delete('lastfm');
|
||||
}
|
||||
@ -37,27 +31,20 @@ export const backend = createBackend<{
|
||||
}
|
||||
},
|
||||
|
||||
async createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType) {
|
||||
for (const [, scrobbler] of this.enabledScrobblers) {
|
||||
if (!scrobbler.isSessionCreated(config)) {
|
||||
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;
|
||||
this.toggleScrobblers(config);
|
||||
for (const [, scrobbler] of this.enabledScrobblers) {
|
||||
if (!scrobbler.isSessionCreated(config)) {
|
||||
await scrobbler.createSession(config, setConfig);
|
||||
}
|
||||
}
|
||||
|
||||
registerCallback((songInfo: SongInfo) => {
|
||||
// Set remove the old scrobble timer
|
||||
@ -65,7 +52,7 @@ export const backend = createBackend<{
|
||||
if (!songInfo.isPaused) {
|
||||
const configNonnull = this.config!;
|
||||
// Scrobblers normally have no trouble working with official music videos
|
||||
if (!configNonnull.scrobbleOtherMedia && (songInfo.mediaType !== MediaType.Audio && songInfo.mediaType !== MediaType.OriginalMusicVideo)) {
|
||||
if (!configNonnull.scrobble_other_media && (songInfo.mediaType !== 'AUDIO' && songInfo.mediaType !== 'ORIGINAL_MUSIC_VIDEO')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -84,25 +71,12 @@ export const backend = createBackend<{
|
||||
});
|
||||
},
|
||||
|
||||
async onConfigChange(newConfig: ScrobblerPluginConfig) {
|
||||
onConfigChange(newConfig: ScrobblerPluginConfig) {
|
||||
this.enabledScrobblers.clear();
|
||||
|
||||
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.config = newConfig;
|
||||
|
||||
this.toggleScrobblers(this.config);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: Se
|
||||
multiInputOptions: [
|
||||
{
|
||||
label: t('plugins.scrobbler.prompt.lastfm.api-key'),
|
||||
value: options.scrobblers.lastfm?.apiKey,
|
||||
value: options.scrobblers.lastfm?.api_key,
|
||||
inputAttrs: {
|
||||
type: 'text'
|
||||
}
|
||||
@ -42,7 +42,7 @@ async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: Se
|
||||
|
||||
if (output) {
|
||||
if (output[0]) {
|
||||
options.scrobblers.lastfm.apiKey = output[0];
|
||||
options.scrobblers.lastfm.api_key = output[0];
|
||||
}
|
||||
|
||||
if (output[1]) {
|
||||
@ -82,9 +82,9 @@ export const onMenu = async ({
|
||||
{
|
||||
label: t('plugins.scrobbler.menu.scrobble-other-media'),
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.scrobbleOtherMedia),
|
||||
checked: Boolean(config.scrobble_other_media),
|
||||
click(item) {
|
||||
config.scrobbleOtherMedia = item.checked;
|
||||
config.scrobble_other_media = item.checked;
|
||||
setConfig(config);
|
||||
},
|
||||
},
|
||||
@ -96,7 +96,7 @@ export const onMenu = async ({
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.scrobblers.lastfm?.enabled),
|
||||
click(item) {
|
||||
backend.toggleScrobblers(config, window);
|
||||
backend.toggleScrobblers(config);
|
||||
config.scrobblers.lastfm.enabled = item.checked;
|
||||
setConfig(config);
|
||||
},
|
||||
@ -117,7 +117,7 @@ export const onMenu = async ({
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.scrobblers.listenbrainz?.enabled),
|
||||
click(item) {
|
||||
backend.toggleScrobblers(config, window);
|
||||
backend.toggleScrobblers(config);
|
||||
config.scrobblers.listenbrainz.enabled = item.checked;
|
||||
setConfig(config);
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ScrobblerPluginConfig } from '../index';
|
||||
import type { SetConfType } from '../main';
|
||||
import { ScrobblerPluginConfig } from '../index';
|
||||
import { SetConfType } from '../main';
|
||||
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
export abstract class ScrobblerBase {
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { BrowserWindow, dialog, net } from 'electron';
|
||||
import { net, shell } from 'electron';
|
||||
|
||||
import { ScrobblerBase } from './base';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { ScrobblerPluginConfig } from '../index';
|
||||
import { SetConfType } from '../main';
|
||||
|
||||
import type { ScrobblerPluginConfig } from '../index';
|
||||
import type { SetConfType } from '../main';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
interface LastFmData {
|
||||
@ -29,32 +28,21 @@ interface LastFmSongData {
|
||||
}
|
||||
|
||||
export class LastFmScrobbler extends ScrobblerBase {
|
||||
mainWindow: BrowserWindow;
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
super();
|
||||
|
||||
this.mainWindow = mainWindow;
|
||||
isSessionCreated(config: ScrobblerPluginConfig): boolean {
|
||||
return !!config.scrobblers.lastfm.session_key;
|
||||
}
|
||||
|
||||
override isSessionCreated(config: ScrobblerPluginConfig): boolean {
|
||||
return !!config.scrobblers.lastfm.sessionKey;
|
||||
}
|
||||
|
||||
override async createSession(
|
||||
config: ScrobblerPluginConfig,
|
||||
setConfig: SetConfType,
|
||||
): Promise<ScrobblerPluginConfig> {
|
||||
async createSession(config: ScrobblerPluginConfig, setConfig: SetConfType): Promise<ScrobblerPluginConfig> {
|
||||
// Get and store the session key
|
||||
const data = {
|
||||
api_key: config.scrobblers.lastfm.apiKey,
|
||||
api_key: config.scrobblers.lastfm.api_key,
|
||||
format: 'json',
|
||||
method: 'auth.getsession',
|
||||
token: config.scrobblers.lastfm.token,
|
||||
};
|
||||
const apiSignature = createApiSig(data, config.scrobblers.lastfm.secret);
|
||||
const response = await net.fetch(
|
||||
`${config.scrobblers.lastfm.apiRoot}${createQueryString(data, apiSignature)}`,
|
||||
`${config.scrobblers.lastfm.api_root}${createQueryString(data, apiSignature)}`,
|
||||
);
|
||||
const json = (await response.json()) as {
|
||||
error?: string;
|
||||
@ -64,25 +52,18 @@ export class LastFmScrobbler extends ScrobblerBase {
|
||||
};
|
||||
if (json.error) {
|
||||
config.scrobblers.lastfm.token = await createToken(config);
|
||||
// If is successful, we need retry the request
|
||||
authenticate(config, this.mainWindow).then((it) => {
|
||||
if (it) {
|
||||
this.createSession(config, setConfig);
|
||||
} else {
|
||||
// failed
|
||||
setConfig(config);
|
||||
}
|
||||
});
|
||||
await authenticate(config);
|
||||
setConfig(config);
|
||||
}
|
||||
if (json.session) {
|
||||
config.scrobblers.lastfm.sessionKey = json.session.key;
|
||||
config.scrobblers.lastfm.session_key = json.session.key;
|
||||
}
|
||||
setConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.lastfm.sessionKey) {
|
||||
setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.lastfm.session_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -93,8 +74,8 @@ export class LastFmScrobbler extends ScrobblerBase {
|
||||
this.postSongDataToAPI(songInfo, config, data, setConfig);
|
||||
}
|
||||
|
||||
override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.lastfm.sessionKey) {
|
||||
addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.lastfm.session_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -106,14 +87,14 @@ export class LastFmScrobbler extends ScrobblerBase {
|
||||
this.postSongDataToAPI(songInfo, config, data, setConfig);
|
||||
}
|
||||
|
||||
private async postSongDataToAPI(
|
||||
async postSongDataToAPI(
|
||||
songInfo: SongInfo,
|
||||
config: ScrobblerPluginConfig,
|
||||
data: LastFmData,
|
||||
setConfig: SetConfType,
|
||||
): Promise<void> {
|
||||
// This sends a post request to the api, and adds the common data
|
||||
if (!config.scrobblers.lastfm.sessionKey) {
|
||||
if (!config.scrobblers.lastfm.session_key) {
|
||||
await this.createSession(config, setConfig);
|
||||
}
|
||||
|
||||
@ -122,8 +103,8 @@ export class LastFmScrobbler extends ScrobblerBase {
|
||||
duration: songInfo.songDuration,
|
||||
artist: songInfo.artist,
|
||||
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
|
||||
api_key: config.scrobblers.lastfm.apiKey,
|
||||
sk: config.scrobblers.lastfm.sessionKey,
|
||||
api_key: config.scrobblers.lastfm.api_key,
|
||||
sk: config.scrobblers.lastfm.session_key,
|
||||
format: 'json',
|
||||
...data,
|
||||
};
|
||||
@ -145,16 +126,10 @@ export class LastFmScrobbler extends ScrobblerBase {
|
||||
}) => {
|
||||
if (error?.response?.data?.error === 9) {
|
||||
// Session key is invalid, so remove it from the config and reauthenticate
|
||||
config.scrobblers.lastfm.sessionKey = undefined;
|
||||
config.scrobblers.lastfm.session_key = undefined;
|
||||
config.scrobblers.lastfm.token = await createToken(config);
|
||||
authenticate(config, this.mainWindow).then((it) => {
|
||||
if (it) {
|
||||
this.createSession(config, setConfig);
|
||||
} else {
|
||||
// failed
|
||||
setConfig(config);
|
||||
}
|
||||
});
|
||||
await authenticate(config);
|
||||
setConfig(config);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
@ -193,17 +168,17 @@ const createQueryString = (
|
||||
|
||||
const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
||||
// This function creates the api signature, see: https://www.last.fm/api/authspec
|
||||
let sig = '';
|
||||
const keys = Object.keys(parameters);
|
||||
|
||||
Object
|
||||
.entries(parameters)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.forEach(([key, value]) => {
|
||||
if (key === 'format') {
|
||||
return;
|
||||
}
|
||||
sig += key + value;
|
||||
});
|
||||
keys.sort();
|
||||
let sig = '';
|
||||
for (const key of keys) {
|
||||
if (key === 'format') {
|
||||
continue;
|
||||
}
|
||||
|
||||
sig += `${key}${parameters[key as keyof LastFmSongData]}`;
|
||||
}
|
||||
|
||||
sig += secret;
|
||||
sig = crypto.createHash('md5').update(sig, 'utf-8').digest('hex');
|
||||
@ -213,18 +188,14 @@ const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
||||
const createToken = async ({
|
||||
scrobblers: {
|
||||
lastfm: {
|
||||
apiKey,
|
||||
apiRoot,
|
||||
api_key: apiKey,
|
||||
api_root: apiRoot,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
}: ScrobblerPluginConfig) => {
|
||||
// Creates and stores the auth token
|
||||
const data: {
|
||||
method: string;
|
||||
api_key: string;
|
||||
format: string;
|
||||
} = {
|
||||
const data = {
|
||||
method: 'auth.gettoken',
|
||||
api_key: apiKey,
|
||||
format: 'json',
|
||||
@ -237,68 +208,9 @@ const createToken = async ({
|
||||
return json?.token;
|
||||
};
|
||||
|
||||
let authWindowOpened = false;
|
||||
let latestAuthResult = false;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
const authenticate = async (config: ScrobblerPluginConfig) => {
|
||||
// Asks the user for authentication
|
||||
await shell.openExternal(
|
||||
`https://www.last.fm/api/auth/?api_key=${config.scrobblers.lastfm.api_key}&token=${config.scrobblers.lastfm.token}`,
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,8 +2,10 @@ import { net } from 'electron';
|
||||
|
||||
import { ScrobblerBase } from './base';
|
||||
|
||||
import type { SetConfType } from '../main';
|
||||
import { SetConfType } from '../main';
|
||||
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
import type { ScrobblerPluginConfig } from '../index';
|
||||
|
||||
interface ListenbrainzRequestBody {
|
||||
@ -25,16 +27,16 @@ interface ListenbrainzRequestBody {
|
||||
}
|
||||
|
||||
export class ListenbrainzScrobbler extends ScrobblerBase {
|
||||
override isSessionCreated(): boolean {
|
||||
isSessionCreated(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> {
|
||||
createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> {
|
||||
return Promise.resolve(config);
|
||||
}
|
||||
|
||||
override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) {
|
||||
setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.listenbrainz.api_root || !config.scrobblers.listenbrainz.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -42,8 +44,8 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
|
||||
submitListen(body, config);
|
||||
}
|
||||
|
||||
override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) {
|
||||
addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
|
||||
if (!config.scrobblers.listenbrainz.api_root || !config.scrobblers.listenbrainz.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -78,7 +80,7 @@ function createRequestBody(listenType: string, songInfo: SongInfo): Listenbrainz
|
||||
}
|
||||
|
||||
function submitListen(body: ListenbrainzRequestBody, config: ScrobblerPluginConfig) {
|
||||
net.fetch(config.scrobblers.listenbrainz.apiRoot + 'submit-listens',
|
||||
net.fetch(config.scrobblers.listenbrainz.api_root + 'submit-listens',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
|
||||
112
src/plugins/shortcuts/mpris-service.d.ts
vendored
112
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -4,10 +4,10 @@ declare module '@jellybrick/mpris-service' {
|
||||
import { interface as dbusInterface } from 'dbus-next';
|
||||
|
||||
interface RootInterfaceOptions {
|
||||
identity?: string;
|
||||
supportedUriSchemes?: string[];
|
||||
supportedMimeTypes?: string[];
|
||||
desktopEntry?: string;
|
||||
identity: string;
|
||||
supportedUriSchemes: string[];
|
||||
supportedMimeTypes: string[];
|
||||
desktopEntry: string;
|
||||
}
|
||||
|
||||
export interface Track {
|
||||
@ -35,32 +35,6 @@ declare module '@jellybrick/mpris-service' {
|
||||
'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 {
|
||||
constructor(opts: {
|
||||
name: string;
|
||||
@ -69,46 +43,20 @@ declare module '@jellybrick/mpris-service' {
|
||||
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;
|
||||
identity: string;
|
||||
fullscreen?: boolean;
|
||||
fullscreen: boolean;
|
||||
supportedUriSchemes: string[];
|
||||
supportedMimeTypes: string[];
|
||||
canQuit: boolean;
|
||||
canRaise: boolean;
|
||||
canSetFullscreen?: boolean;
|
||||
desktopEntry?: string;
|
||||
canSetFullscreen: boolean;
|
||||
hasTrackList: boolean;
|
||||
|
||||
// PlayerInterface
|
||||
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;
|
||||
desktopEntry: string;
|
||||
playbackStatus: string;
|
||||
loopStatus: string;
|
||||
shuffle: boolean;
|
||||
metadata: Track;
|
||||
metadata: object;
|
||||
volume: number;
|
||||
canControl: boolean;
|
||||
canPause: boolean;
|
||||
@ -119,40 +67,9 @@ declare module '@jellybrick/mpris-service' {
|
||||
rate: number;
|
||||
minimumRate: number;
|
||||
maximumRate: number;
|
||||
|
||||
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[];
|
||||
playlists: unknown[];
|
||||
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;
|
||||
|
||||
objectPath(subpath?: string): string;
|
||||
@ -174,6 +91,13 @@ declare module '@jellybrick/mpris-service' {
|
||||
setPlaylists(playlists: Track[]): 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 {
|
||||
|
||||
@ -1,117 +1,37 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
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 mpris, { Track } from '@jellybrick/mpris-service';
|
||||
|
||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import config from '@/config';
|
||||
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 {
|
||||
/**
|
||||
* @type {number} The current position in microseconds
|
||||
* @private
|
||||
*/
|
||||
private currentPosition: number;
|
||||
|
||||
constructor(opts: PlayerOptions) {
|
||||
super(opts);
|
||||
|
||||
this.currentPosition = 0;
|
||||
}
|
||||
|
||||
setPosition(t: number) {
|
||||
this.currentPosition = t;
|
||||
}
|
||||
|
||||
override getPosition(): number {
|
||||
return this.currentPosition;
|
||||
}
|
||||
|
||||
setLoopStatus(status: LoopStatus) {
|
||||
this.loopStatus = status;
|
||||
}
|
||||
|
||||
isPlaying(): boolean {
|
||||
return this.playbackStatus === PLAYBACK_STATUS_PLAYING;
|
||||
}
|
||||
|
||||
isPaused(): boolean {
|
||||
return this.playbackStatus === PLAYBACK_STATUS_PAUSED;
|
||||
}
|
||||
|
||||
isStopped(): boolean {
|
||||
return this.playbackStatus === PLAYBACK_STATUS_STOPPED;
|
||||
}
|
||||
|
||||
setPlaybackStatus(status: PlayBackStatus) {
|
||||
this.playbackStatus = status;
|
||||
}
|
||||
}
|
||||
|
||||
function setupMPRIS() {
|
||||
const instance = new YTPlayer({
|
||||
name: 'YoutubeMusic',
|
||||
const instance = new mpris({
|
||||
name: 'youtube-music',
|
||||
identity: 'YouTube Music',
|
||||
supportedMimeTypes: ['audio/mpeg'],
|
||||
supportedInterfaces: ['player'],
|
||||
});
|
||||
|
||||
instance.canRaise = true;
|
||||
instance.canQuit = false;
|
||||
instance.canSetFullscreen = true;
|
||||
instance.supportedUriSchemes = ['http', 'https'];
|
||||
instance.supportedUriSchemes = ['https'];
|
||||
instance.desktopEntry = 'youtube-music';
|
||||
return instance;
|
||||
}
|
||||
|
||||
function registerMPRIS(win: BrowserWindow) {
|
||||
const songControls = getSongControls(win);
|
||||
const {
|
||||
playPause,
|
||||
next,
|
||||
previous,
|
||||
setVolume,
|
||||
shuffle,
|
||||
switchRepeat,
|
||||
setFullscreen,
|
||||
requestFullscreenInformation,
|
||||
requestQueueInformation,
|
||||
} = songControls;
|
||||
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } =
|
||||
songControls;
|
||||
try {
|
||||
let currentSongInfo: SongInfo | null = null;
|
||||
const secToMicro = (n: number) => Math.round(Number(n) * 1e6);
|
||||
const microToSec = (n: number) => Math.round(Number(n) / 1e6);
|
||||
// TODO: "Typing" for this arguments
|
||||
const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6);
|
||||
const microToSec = (n: unknown) => Math.round(Number(n) / 1e6);
|
||||
|
||||
const correctId = (videoId: string) => {
|
||||
return videoId.replace('-', '_MINUS_');
|
||||
};
|
||||
|
||||
const seekTo = (event: Position) => {
|
||||
if (
|
||||
currentSongInfo?.videoId &&
|
||||
event.trackId.endsWith(correctId(currentSongInfo.videoId))
|
||||
) {
|
||||
win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0));
|
||||
}
|
||||
};
|
||||
const seekBy = (offset: number) =>
|
||||
win.webContents.send('ytmd:seek-by', microToSec(offset));
|
||||
const seekTo = (e: { position: unknown }) =>
|
||||
win.webContents.send('ytmd:seek-to', microToSec(e.position));
|
||||
const seekBy = (o: unknown) =>
|
||||
win.webContents.send('ytmd:seek-by', microToSec(o));
|
||||
|
||||
const player = setupMPRIS();
|
||||
|
||||
@ -120,145 +40,73 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
win.webContents.send('ytmd:setup-time-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-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:time-changed', (_, t: number) => {
|
||||
player.setPosition(secToMicro(t));
|
||||
});
|
||||
let currentSeconds = 0;
|
||||
ipcMain.on('ytmd:time-changed', (_, t: number) => (currentSeconds = t));
|
||||
|
||||
ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
|
||||
ipcMain.on('ytmd:repeat-changed', (_, mode: string) => {
|
||||
switch (mode) {
|
||||
case 'NONE': {
|
||||
player.setLoopStatus(LOOP_STATUS_NONE);
|
||||
player.loopStatus = mpris.LOOP_STATUS_NONE;
|
||||
break;
|
||||
}
|
||||
case 'ONE': {
|
||||
player.setLoopStatus(LOOP_STATUS_TRACK);
|
||||
player.loopStatus = mpris.LOOP_STATUS_TRACK;
|
||||
break;
|
||||
}
|
||||
case 'ALL': {
|
||||
player.setLoopStatus(LOOP_STATUS_PLAYLIST);
|
||||
player.loopStatus = mpris.LOOP_STATUS_PLAYLIST;
|
||||
// No default
|
||||
break;
|
||||
}
|
||||
}
|
||||
requestQueueInformation();
|
||||
});
|
||||
|
||||
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) => {
|
||||
player.on('loopStatus', (status: string) => {
|
||||
// SwitchRepeat cycles between states in that order
|
||||
const switches = [
|
||||
LOOP_STATUS_NONE,
|
||||
LOOP_STATUS_PLAYLIST,
|
||||
LOOP_STATUS_TRACK,
|
||||
mpris.LOOP_STATUS_NONE,
|
||||
mpris.LOOP_STATUS_PLAYLIST,
|
||||
mpris.LOOP_STATUS_TRACK,
|
||||
];
|
||||
const currentIndex = switches.indexOf(player.loopStatus);
|
||||
const targetIndex = switches.indexOf(status);
|
||||
|
||||
// Get a delta in the range [0,2]
|
||||
const delta = (targetIndex - currentIndex + 3) % 3;
|
||||
switchRepeat(delta);
|
||||
songControls.switchRepeat(delta);
|
||||
});
|
||||
player.getPosition = () => secToMicro(currentSeconds);
|
||||
|
||||
player.on('raise', () => {
|
||||
if (!player.canRaise) {
|
||||
return;
|
||||
}
|
||||
|
||||
win.setSkipTaskbar(false);
|
||||
win.show();
|
||||
});
|
||||
|
||||
player.on('fullscreen', (fullscreenEnabled: boolean) => {
|
||||
setFullscreen(fullscreenEnabled);
|
||||
});
|
||||
|
||||
player.on('play', () => {
|
||||
if (!player.isPlaying()) {
|
||||
player.setPlaybackStatus(PLAYBACK_STATUS_PLAYING);
|
||||
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) {
|
||||
player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING;
|
||||
playPause();
|
||||
}
|
||||
});
|
||||
player.on('pause', () => {
|
||||
if (!player.isPaused()) {
|
||||
player.setPlaybackStatus(PLAYBACK_STATUS_PAUSED);
|
||||
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) {
|
||||
player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED;
|
||||
playPause();
|
||||
}
|
||||
});
|
||||
player.on('playpause', () => {
|
||||
player.setPlaybackStatus(
|
||||
player.isPlaying() ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
|
||||
);
|
||||
player.playbackStatus =
|
||||
player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING
|
||||
? mpris.PLAYBACK_STATUS_PAUSED
|
||||
: mpris.PLAYBACK_STATUS_PLAYING;
|
||||
playPause();
|
||||
});
|
||||
|
||||
player.on('next', () => {
|
||||
next();
|
||||
});
|
||||
|
||||
player.on('previous', () => {
|
||||
previous();
|
||||
});
|
||||
player.on('next', next);
|
||||
player.on('previous', previous);
|
||||
|
||||
player.on('seek', seekBy);
|
||||
player.on('position', seekTo);
|
||||
@ -266,18 +114,10 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
player.on('shuffle', (enableShuffle) => {
|
||||
if (enableShuffle) {
|
||||
shuffle();
|
||||
requestQueueInformation();
|
||||
}
|
||||
});
|
||||
player.on('open', (args: { uri: string }) => {
|
||||
win.loadURL(args.uri).then(() => {
|
||||
requestQueueInformation();
|
||||
});
|
||||
});
|
||||
|
||||
player.on('error', (error: Error) => {
|
||||
console.error(LoggerPrefix, 'Error in MPRIS');
|
||||
console.trace(error);
|
||||
win.loadURL(args.uri);
|
||||
});
|
||||
|
||||
let mprisVolNewer = false;
|
||||
@ -296,7 +136,7 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
}
|
||||
});
|
||||
|
||||
player.on('volume', (newVolume: number) => {
|
||||
player.on('volume', (newVolume) => {
|
||||
if (config.plugins.isEnabled('precise-volume')) {
|
||||
// With precise volume we can set the volume to the exact value.
|
||||
const newVol = ~~(newVolume * 100);
|
||||
@ -306,46 +146,45 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
win.webContents.send('setVolume', newVol);
|
||||
}
|
||||
} else {
|
||||
setVolume(newVolume * 100);
|
||||
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
|
||||
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: SongInfo) => {
|
||||
registerCallback((songInfo) => {
|
||||
if (player) {
|
||||
const data: Track = {
|
||||
'mpris:length': secToMicro(songInfo.songDuration),
|
||||
...(songInfo.imageSrc
|
||||
? { 'mpris:artUrl': songInfo.imageSrc }
|
||||
: undefined),
|
||||
'mpris:artUrl': songInfo.imageSrc ?? undefined,
|
||||
'xesam:title': songInfo.title,
|
||||
'xesam:url': songInfo.url,
|
||||
'xesam:artist': [songInfo.artist],
|
||||
'mpris:trackid': player.objectPath(
|
||||
`Track/${correctId(songInfo.videoId)}`,
|
||||
),
|
||||
'mpris:trackid': '/',
|
||||
};
|
||||
if (songInfo.album) {
|
||||
data['xesam:album'] = songInfo.album;
|
||||
}
|
||||
currentSongInfo = songInfo;
|
||||
|
||||
player.metadata = data;
|
||||
|
||||
const currentElapsedMicroSeconds = secToMicro(
|
||||
songInfo.elapsedSeconds ?? 0,
|
||||
);
|
||||
player.setPosition(currentElapsedMicroSeconds);
|
||||
player.seeked(currentElapsedMicroSeconds);
|
||||
|
||||
player.setPlaybackStatus(
|
||||
songInfo.isPaused ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
|
||||
);
|
||||
player.seeked(secToMicro(songInfo.elapsedSeconds));
|
||||
player.playbackStatus = songInfo.isPaused
|
||||
? mpris.PLAYBACK_STATUS_PAUSED
|
||||
: mpris.PLAYBACK_STATUS_PLAYING;
|
||||
}
|
||||
requestQueueInformation();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(LoggerPrefix, 'Error in MPRIS');
|
||||
console.trace(error);
|
||||
console.warn('Error in MPRIS', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +1,18 @@
|
||||
import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin<
|
||||
unknown,
|
||||
unknown,
|
||||
{
|
||||
observer?: MutationObserver;
|
||||
waitForElem(selector: string): Promise<HTMLElement>;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
>({
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.skip-disliked-songs.name'),
|
||||
description: () => t('plugins.skip-disliked-songs.description'),
|
||||
restartNeeded: false,
|
||||
renderer: {
|
||||
waitForElem(selector: string) {
|
||||
return new Promise<HTMLElement>((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
const elem = document.querySelector<HTMLElement>(selector);
|
||||
if (!elem) return;
|
||||
|
||||
clearInterval(interval);
|
||||
resolve(elem);
|
||||
});
|
||||
});
|
||||
},
|
||||
observer: null as MutationObserver | null,
|
||||
start() {
|
||||
this.waitForElem('#like-button-renderer').then((likeBtn) => {
|
||||
this.observer = new MutationObserver(() => {
|
||||
if (likeBtn?.getAttribute('like-status') == 'DISLIKE') {
|
||||
document
|
||||
.querySelector<HTMLButtonElement>('tp-yt-paper-icon-button.next-button')
|
||||
.querySelector('tp-yt-paper-icon-button.next-button')
|
||||
?.click();
|
||||
}
|
||||
});
|
||||
@ -45,5 +26,16 @@ export default createPlugin<
|
||||
stop() {
|
||||
this.observer?.disconnect();
|
||||
},
|
||||
waitForElem(selector) {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
const elem = document.querySelector(selector);
|
||||
if (!elem) return;
|
||||
|
||||
clearInterval(interval);
|
||||
resolve(elem);
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,8 @@
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent, webFrame } from 'electron';
|
||||
import is from 'electron-is';
|
||||
|
||||
import { injectChromeCompatToObject, chrome } from '@jellybrick/electron-chromecast';
|
||||
|
||||
import config from './config';
|
||||
|
||||
import {
|
||||
@ -53,6 +55,8 @@ contextBridge.exposeInMainWorld(
|
||||
'ELECTRON_RENDERER_URL',
|
||||
process.env.ELECTRON_RENDERER_URL,
|
||||
);
|
||||
injectChromeCompatToObject(global);
|
||||
contextBridge.exposeInMainWorld('caster', chrome);
|
||||
|
||||
const [path, script] = ipcRenderer.sendSync('get-renderer-script') as [string | null, string];
|
||||
let blocked = true;
|
||||
|
||||
@ -6,7 +6,7 @@ import getSongControls from './song-controls';
|
||||
|
||||
export const APP_PROTOCOL = 'youtubemusic';
|
||||
|
||||
let protocolHandler: ((cmd: string, args: string[] | undefined) => void) | undefined;
|
||||
let protocolHandler: ((cmd: string) => void) | undefined;
|
||||
|
||||
export function setupProtocolHandler(win: BrowserWindow) {
|
||||
if (process.defaultApp && process.argv.length >= 2) {
|
||||
@ -19,18 +19,18 @@ export function setupProtocolHandler(win: BrowserWindow) {
|
||||
|
||||
const songControls = getSongControls(win);
|
||||
|
||||
protocolHandler = ((cmd: keyof typeof songControls, args: string[] | undefined = undefined) => {
|
||||
protocolHandler = ((cmd: keyof typeof songControls) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd](args as never);
|
||||
songControls[cmd]();
|
||||
}
|
||||
}) as (cmd: string) => void;
|
||||
}
|
||||
|
||||
export function handleProtocol(cmd: string, args: string[] | undefined) {
|
||||
protocolHandler?.(cmd, args);
|
||||
export function handleProtocol(cmd: string) {
|
||||
protocolHandler?.(cmd);
|
||||
}
|
||||
|
||||
export function changeProtocolHandler(f: (cmd: string, args: string[] | undefined) => void) {
|
||||
export function changeProtocolHandler(f: (cmd: string) => void) {
|
||||
protocolHandler = f;
|
||||
}
|
||||
|
||||
|
||||
@ -1,82 +1,39 @@
|
||||
// This is used for to control the songs
|
||||
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;
|
||||
}
|
||||
};
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
const commands = {
|
||||
// Playback
|
||||
previous: () => win.webContents.send('ytmd:previous-video'),
|
||||
next: () => win.webContents.send('ytmd:next-video'),
|
||||
playPause: () => win.webContents.send('ytmd:toggle-play'),
|
||||
like: () => win.webContents.send('ytmd:update-like', 'LIKE'),
|
||||
dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'),
|
||||
go10sBack: () => win.webContents.send('ytmd:seek-by', -10),
|
||||
go10sForward: () => win.webContents.send('ytmd:seek-by', 10),
|
||||
go1sBack: () => win.webContents.send('ytmd:seek-by', -1),
|
||||
go1sForward: () => win.webContents.send('ytmd:seek-by', 1),
|
||||
shuffle: () => win.webContents.send('ytmd:shuffle'),
|
||||
switchRepeat: (n = 1) => win.webContents.send('ytmd:switch-repeat', n),
|
||||
// General
|
||||
volumeMinus10: () => {
|
||||
ipcMain.once('ytmd:get-volume-return', (_, volume) => {
|
||||
win.webContents.send('ytmd:update-volume', volume - 10);
|
||||
});
|
||||
win.webContents.send('ytmd:get-volume');
|
||||
},
|
||||
volumePlus10: () => {
|
||||
ipcMain.once('ytmd:get-volume-return', (_, volume) => {
|
||||
win.webContents.send('ytmd:update-volume', volume + 10);
|
||||
});
|
||||
win.webContents.send('ytmd:get-volume');
|
||||
},
|
||||
fullscreen: () => win.webContents.send('ytmd:toggle-fullscreen'),
|
||||
muteUnmute: () => win.webContents.send('ytmd:toggle-mute'),
|
||||
};
|
||||
return {
|
||||
// Playback
|
||||
previous: () => win.webContents.send('ytmd:previous-video'),
|
||||
next: () => win.webContents.send('ytmd:next-video'),
|
||||
playPause: () => win.webContents.send('ytmd:toggle-play'),
|
||||
like: () => win.webContents.send('ytmd:update-like', 'LIKE'),
|
||||
dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'),
|
||||
goBack: (seconds: ArgsType<number>) => {
|
||||
const secondsNumber = parseNumberFromArgsType(seconds);
|
||||
if (secondsNumber !== null) {
|
||||
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'),
|
||||
switchRepeat: (n: ArgsType<number> = 1) => {
|
||||
const repeat = parseNumberFromArgsType(n);
|
||||
if (repeat !== null) {
|
||||
win.webContents.send('ytmd:switch-repeat', n);
|
||||
}
|
||||
},
|
||||
// General
|
||||
setVolume: (volume: ArgsType<number>) => {
|
||||
const volumeNumber = parseNumberFromArgsType(volume);
|
||||
if (volumeNumber !== null) {
|
||||
win.webContents.send('ytmd:update-volume', volume);
|
||||
}
|
||||
},
|
||||
setFullscreen: (isFullscreen: ArgsType<boolean>) => {
|
||||
const isFullscreenValue = parseBooleanFromArgsType(isFullscreen);
|
||||
if (isFullscreenValue !== null) {
|
||||
win.setFullScreen(isFullscreenValue);
|
||||
win.webContents.send('ytmd:click-fullscreen-button', isFullscreenValue);
|
||||
}
|
||||
},
|
||||
requestFullscreenInformation: () => {
|
||||
win.webContents.send('ytmd:get-fullscreen');
|
||||
},
|
||||
requestQueueInformation: () => {
|
||||
win.webContents.send('ytmd:get-queue');
|
||||
},
|
||||
muteUnmute: () => win.webContents.send('ytmd:toggle-mute'),
|
||||
search: () => {
|
||||
win.webContents.sendInputEvent({
|
||||
type: 'keyDown',
|
||||
keyCode: '/',
|
||||
});
|
||||
},
|
||||
...commands,
|
||||
play: commands.playPause,
|
||||
pause: commands.playPause,
|
||||
};
|
||||
};
|
||||
|
||||
@ -29,9 +29,8 @@ export const setupTimeChangedListener = singleton(() => {
|
||||
const progressObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
const target = mutation.target as Node & { value: string };
|
||||
const numberValue = Number(target.value);
|
||||
window.ipcRenderer.send('ytmd:time-changed', numberValue);
|
||||
songInfo.elapsedSeconds = numberValue;
|
||||
window.ipcRenderer.send('ytmd:time-changed', target.value);
|
||||
songInfo.elapsedSeconds = Number(target.value);
|
||||
}
|
||||
});
|
||||
const progressBar = document.querySelector('#progress-bar');
|
||||
@ -62,13 +61,11 @@ export const setupRepeatChangedListener = singleton(() => {
|
||||
// provided by YouTube Music
|
||||
window.ipcRenderer.send(
|
||||
'ytmd:repeat-changed',
|
||||
document
|
||||
.querySelector<
|
||||
HTMLElement & {
|
||||
getState: () => GetState;
|
||||
}
|
||||
>('ytmusic-player-bar')
|
||||
?.getState().queue.repeatMode,
|
||||
document.querySelector<
|
||||
HTMLElement & {
|
||||
getState: () => GetState;
|
||||
}
|
||||
>('ytmusic-player-bar')?.getState().queue.repeatMode,
|
||||
);
|
||||
});
|
||||
|
||||
@ -80,46 +77,6 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
|
||||
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) => {
|
||||
window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => {
|
||||
setupTimeChangedListener();
|
||||
@ -133,14 +90,6 @@ export default (api: YoutubePlayer) => {
|
||||
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', () => {
|
||||
setupSeekedListener();
|
||||
});
|
||||
@ -205,13 +154,13 @@ export default (api: YoutubePlayer) => {
|
||||
function sendSongInfo(videoData: VideoDataChangeValue) {
|
||||
const data = api.getPlayerResponse();
|
||||
|
||||
data.videoDetails.album = (
|
||||
Object.entries(videoData).find(
|
||||
([, value]) => value && Object.hasOwn(value, 'playerOverlays'),
|
||||
) as [string, AlbumDetails | undefined]
|
||||
)?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at(
|
||||
0,
|
||||
)?.text;
|
||||
data.videoDetails.album =
|
||||
(
|
||||
Object.entries(videoData)
|
||||
.find(([, value]) => value && Object.hasOwn(value, 'playerOverlays')) as [string, AlbumDetails | undefined]
|
||||
)?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at(
|
||||
0,
|
||||
)?.text;
|
||||
data.videoDetails.elapsedSeconds = 0;
|
||||
data.videoDetails.isPaused = false;
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import config from '@/config';
|
||||
|
||||
import type { GetPlayerResponse } from '@/types/get-player-response';
|
||||
|
||||
export enum MediaType {
|
||||
enum MediaType {
|
||||
/**
|
||||
* Audio uploaded by the original artist
|
||||
*/
|
||||
@ -120,24 +120,11 @@ const handleData = async (
|
||||
songInfo.mediaType = MediaType.PodcastEpisode;
|
||||
// HACK: Podcast's participant is not the artist
|
||||
if (!config.get('options.usePodcastParticipantAsArtist')) {
|
||||
songInfo.artist = cleanupName(
|
||||
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
|
||||
);
|
||||
songInfo.artist = cleanupName(data.microformat.microformatDataRenderer.pageOwnerDetails.name);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
songInfo.mediaType = MediaType.OtherVideo;
|
||||
// HACK: This is a workaround for "podcast" types where "musicVideoType" doesn't exist. Google :facepalm:
|
||||
if (
|
||||
!config.get('options.usePodcastParticipantAsArtist') &&
|
||||
(data.responseContext.serviceTrackingParams
|
||||
?.at(0)
|
||||
?.params?.find((it) => it.key === 'ipcc')?.value ?? '1') != '0'
|
||||
) {
|
||||
songInfo.artist = cleanupName(
|
||||
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@ -166,12 +153,10 @@ const registerProvider = (win: BrowserWindow) => {
|
||||
|
||||
// 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) => {
|
||||
const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(
|
||||
async () => {
|
||||
songInfo = await handleData(data, win);
|
||||
return songInfo;
|
||||
},
|
||||
);
|
||||
const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(async () => {
|
||||
songInfo = await handleData(data, win);
|
||||
return songInfo;
|
||||
});
|
||||
|
||||
if (tempSongInfo) {
|
||||
for (const c of callbacks) {
|
||||
|
||||
@ -15,8 +15,8 @@ import { loadI18n, setLanguage, t as i18t } from '@/i18n';
|
||||
|
||||
import type { PluginConfig } from '@/types/plugins';
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
import type { QueueElement } from '@/types/queue';
|
||||
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
||||
|
||||
window.chrome.cast = window.caster.cast;
|
||||
|
||||
let api: (Element & YoutubePlayer) | null = null;
|
||||
let isPluginLoaded = false;
|
||||
@ -63,56 +63,18 @@ async function onApiLoaded() {
|
||||
}
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
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:get-volume', (event) => {
|
||||
event.sender.emit('ytmd:get-volume-return', api?.getVolume());
|
||||
});
|
||||
|
||||
window.ipcRenderer.on('ytmd:click-fullscreen-button', (_, fullscreen: boolean | undefined) => {
|
||||
clickFullscreenButton(fullscreen ?? false);
|
||||
window.ipcRenderer.on('ytmd:toggle-fullscreen', (_) => {
|
||||
document.querySelector<HTMLElement & { toggleFullscreen: () => void }>('ytmusic-player-bar')?.toggleFullscreen();
|
||||
});
|
||||
|
||||
window.ipcRenderer.on('ytmd:toggle-mute', (_) => {
|
||||
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 audioContext = new AudioContext();
|
||||
const audioSource = audioContext.createMediaElementSource(video);
|
||||
@ -173,7 +135,7 @@ async function onApiLoaded() {
|
||||
// Remove upgrade button
|
||||
if (window.mainConfig.get('options.removeUpgradeButton')) {
|
||||
const styles = document.createElement('style');
|
||||
styles.innerHTML = `ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer:last-child {
|
||||
styles.innerHTML = `ytmusic-guide-signin-promo-renderer {
|
||||
display: none;
|
||||
}`;
|
||||
document.head.appendChild(styles);
|
||||
@ -276,9 +238,7 @@ const initObserver = async () => {
|
||||
// check document.documentElement is ready
|
||||
await new Promise<void>((resolve) => {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
1
src/reset.d.ts
vendored
1
src/reset.d.ts
vendored
@ -22,6 +22,7 @@ declare global {
|
||||
ipcRenderer: typeof electronIpcRenderer;
|
||||
mainConfig: typeof config;
|
||||
electronIs: typeof is;
|
||||
caster: typeof window.chrome;
|
||||
ELECTRON_RENDERER_URL: string | undefined;
|
||||
/**
|
||||
* YouTube Music internal variable (Last interaction time)
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import type { PlayerConfig } from '@/types/get-player-response';
|
||||
|
||||
export interface GetState {
|
||||
castStatus: CastStatus;
|
||||
entities: Entities;
|
||||
@ -34,11 +32,17 @@ export interface Download {
|
||||
export interface Entities {}
|
||||
|
||||
export interface LikeStatus {
|
||||
videos: Record<string, LikeType>;
|
||||
videos: Videos;
|
||||
playlists: Entities;
|
||||
}
|
||||
|
||||
export enum LikeType {
|
||||
export interface Videos {
|
||||
tNVTuUEeWP0: Kqp1PyPRBzA;
|
||||
KQP1PyPrBzA: Kqp1PyPRBzA;
|
||||
'o1iz4L-5zkQ': Kqp1PyPRBzA;
|
||||
}
|
||||
|
||||
export enum Kqp1PyPRBzA {
|
||||
Dislike = 'DISLIKE',
|
||||
Indifferent = 'INDIFFERENT',
|
||||
Like = 'LIKE',
|
||||
@ -191,10 +195,14 @@ export interface Target {
|
||||
|
||||
export interface CommandWatchEndpoint {
|
||||
videoId: string;
|
||||
params: string;
|
||||
params: PurpleParams;
|
||||
watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs;
|
||||
}
|
||||
|
||||
export enum PurpleParams {
|
||||
WAEB = 'wAEB',
|
||||
}
|
||||
|
||||
export interface PurpleWatchEndpointMusicSupportedConfigs {
|
||||
watchEndpointMusicConfig: PurpleWatchEndpointMusicConfig;
|
||||
}
|
||||
@ -373,7 +381,7 @@ export enum SharePanelType {
|
||||
export interface PurpleWatchEndpoint {
|
||||
videoId: string;
|
||||
playlistId: string;
|
||||
params: string;
|
||||
params: PurpleParams;
|
||||
loggingContext: LoggingContext;
|
||||
watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs;
|
||||
}
|
||||
@ -458,7 +466,7 @@ export interface FeedbackEndpoint {
|
||||
}
|
||||
|
||||
export interface PurpleLikeEndpoint {
|
||||
status: LikeType;
|
||||
status: Kqp1PyPRBzA;
|
||||
target: Target;
|
||||
actions?: LikeEndpointAction[];
|
||||
}
|
||||
@ -480,7 +488,7 @@ export interface PurpleToggledServiceEndpoint {
|
||||
}
|
||||
|
||||
export interface FluffyLikeEndpoint {
|
||||
status: LikeType;
|
||||
status: Kqp1PyPRBzA;
|
||||
target: Target;
|
||||
}
|
||||
|
||||
@ -682,7 +690,7 @@ export interface FluffyDefaultServiceEndpoint {
|
||||
}
|
||||
|
||||
export interface TentacledLikeEndpoint {
|
||||
status: LikeType;
|
||||
status: Kqp1PyPRBzA;
|
||||
target: AddToPlaylistEndpoint;
|
||||
actions?: LikeEndpointAction[];
|
||||
}
|
||||
@ -694,7 +702,7 @@ export interface FluffyToggledServiceEndpoint {
|
||||
}
|
||||
|
||||
export interface StickyLikeEndpoint {
|
||||
status: LikeType;
|
||||
status: Kqp1PyPRBzA;
|
||||
target: AddToPlaylistEndpoint;
|
||||
}
|
||||
|
||||
@ -1177,6 +1185,81 @@ export interface PtrackingURLClass {
|
||||
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 {
|
||||
playerStoryboardSpecRenderer: PlayerStoryboardSpecRenderer;
|
||||
}
|
||||
@ -1301,7 +1384,7 @@ export interface PlayerOverlayRendererAction {
|
||||
|
||||
export interface LikeButtonRenderer {
|
||||
target: Target;
|
||||
likeStatus: LikeType;
|
||||
likeStatus: Kqp1PyPRBzA;
|
||||
trackingParams: string;
|
||||
likesAllowed: boolean;
|
||||
serviceEndpoints: ServiceEndpoint[];
|
||||
@ -1313,14 +1396,13 @@ export interface ServiceEndpoint {
|
||||
}
|
||||
|
||||
export interface ServiceEndpointLikeEndpoint {
|
||||
status: LikeType;
|
||||
status: Kqp1PyPRBzA;
|
||||
target: Target;
|
||||
likeParams?: LikeParams;
|
||||
dislikeParams?: LikeParams;
|
||||
removeLikeParams?: LikeParams;
|
||||
}
|
||||
|
||||
// TODO: Add more
|
||||
export enum LikeParams {
|
||||
Oai3D = 'OAI%3D',
|
||||
}
|
||||
@ -1385,12 +1467,16 @@ export interface CurrentVideoEndpoint {
|
||||
|
||||
export interface CurrentVideoEndpointWatchEndpoint {
|
||||
videoId: string;
|
||||
playlistId: string;
|
||||
playlistId: PlaylistID;
|
||||
index: number;
|
||||
playlistSetVideoId: string;
|
||||
loggingContext: LoggingContext;
|
||||
}
|
||||
|
||||
export enum PlaylistID {
|
||||
RDAMVMrkaNKAvksDE = 'RDAMVMrkaNKAvksDE',
|
||||
}
|
||||
|
||||
export interface PlayerPageWatchNextResponseResponseContext {
|
||||
serviceTrackingParams: ServiceTrackingParam[];
|
||||
}
|
||||
@ -1450,8 +1536,6 @@ export interface FlagEndpoint {
|
||||
flagAction: string;
|
||||
}
|
||||
|
||||
export type RepeatMode = 'NONE' | 'ONE' | 'ALL';
|
||||
|
||||
export interface Queue {
|
||||
automixItems: unknown[];
|
||||
autoplay: boolean;
|
||||
@ -1469,7 +1553,7 @@ export interface Queue {
|
||||
nextQueueItemId: number;
|
||||
playbackContentMode: string;
|
||||
queueContextParams: string;
|
||||
repeatMode: RepeatMode;
|
||||
repeatMode: string;
|
||||
responsiveSignals: ResponsiveSignals;
|
||||
selectedItemIndex: number;
|
||||
shuffleEnabled: boolean;
|
||||
@ -1558,15 +1642,23 @@ export interface PlaylistPanelVideoRendererNavigationEndpoint {
|
||||
|
||||
export interface FluffyWatchEndpoint {
|
||||
videoId: string;
|
||||
playlistId?: string;
|
||||
playlistId?: PlaylistID;
|
||||
index: number;
|
||||
params: string;
|
||||
playerParams?: string;
|
||||
params: FluffyParams;
|
||||
playerParams?: PlayerParams;
|
||||
playlistSetVideoId?: string;
|
||||
loggingContext?: LoggingContext;
|
||||
watchEndpointMusicSupportedConfigs: FluffyWatchEndpointMusicSupportedConfigs;
|
||||
}
|
||||
|
||||
export enum FluffyParams {
|
||||
OAHyAQIIAQ3D3D = 'OAHyAQIIAQ%3D%3D',
|
||||
}
|
||||
|
||||
export enum PlayerParams {
|
||||
The8Aub = '8AUB',
|
||||
}
|
||||
|
||||
export interface FluffyWatchEndpointMusicSupportedConfigs {
|
||||
watchEndpointMusicConfig: FluffyWatchEndpointMusicConfig;
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
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
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import type { QueueItem } from '@/types/datahost-get-state';
|
||||
|
||||
export interface QueueResponse {
|
||||
items?: QueueItem[];
|
||||
autoPlaying?: boolean;
|
||||
continuation?: string;
|
||||
}
|
||||
@ -35,11 +35,6 @@ img {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Hide cast button which doesn't work */
|
||||
ytmusic-cast-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove useless inaccessible button on top-right corner of the video player */
|
||||
.ytp-chrome-top-buttons {
|
||||
display: none !important;
|
||||
|
||||
Reference in New Issue
Block a user