Compare commits

..

1 Commits

Author SHA1 Message Date
a689980049 feat: start implementing chromecast API 2024-02-13 11:39:23 +09:00
48 changed files with 833 additions and 1665 deletions

View File

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

View File

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

View File

@ -478,7 +478,7 @@
</a> </a>
</li> </li>
</ul> </ul>
<div class="footer-copyright">© 2024 th-ch</div> <div class="footer-copyright">© 2021 th-ch</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.3.2", "version": "3.2.2",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js", "main": "./dist/main/index.js",
"license": "MIT", "license": "MIT",
@ -132,7 +132,8 @@
}, },
"patchedDependencies": { "patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch", "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": { "dependencies": {
@ -145,6 +146,7 @@
"@floating-ui/dom": "1.6.3", "@floating-ui/dom": "1.6.3",
"@foobar404/wave": "2.0.5", "@foobar404/wave": "2.0.5",
"@jellybrick/electron-better-web-request": "1.0.4", "@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/electron-chromecast": "1.2.1",
"@jellybrick/mpris-service": "2.1.4", "@jellybrick/mpris-service": "2.1.4",
"@xhayper/discord-rpc": "1.1.2", "@xhayper/discord-rpc": "1.1.2",
"async-mutex": "0.4.1", "async-mutex": "0.4.1",
@ -160,13 +162,13 @@
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-unhandled": "4.0.1", "electron-unhandled": "4.0.1",
"electron-updater": "6.1.8", "electron-updater": "6.1.7",
"fast-average-color": "9.4.0", "fast-average-color": "9.4.0",
"fast-equals": "5.0.1", "fast-equals": "5.0.1",
"filenamify": "6.0.0", "filenamify": "6.0.0",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.9.0", "i18next": "23.8.2",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.12", "node-html-parser": "6.1.12",
@ -176,7 +178,7 @@
"serve": "14.2.1", "serve": "14.2.1",
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9", "simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
"solid-floating-ui": "0.3.1", "solid-floating-ui": "0.3.1",
"solid-js": "1.8.15", "solid-js": "1.8.14",
"solid-styled-components": "0.28.5", "solid-styled-components": "0.28.5",
"solid-transition-group": "0.2.3", "solid-transition-group": "0.2.3",
"ts-morph": "21.0.1", "ts-morph": "21.0.1",
@ -192,17 +194,17 @@
"@types/howler": "2.2.11", "@types/howler": "2.2.11",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@types/semver": "7.5.7", "@types/semver": "7.5.7",
"@typescript-eslint/eslint-plugin": "7.0.2", "@typescript-eslint/eslint-plugin": "7.0.1",
"bufferutil": "4.0.8", "bufferutil": "4.0.8",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"del-cli": "5.1.0", "del-cli": "5.1.0",
"discord-api-types": "0.37.70", "discord-api-types": "0.37.69",
"electron": "28.2.3", "electron": "28.2.2",
"electron-builder": "24.9.1", "electron-builder": "24.9.1",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "2.0.0", "electron-vite": "2.0.0",
"esbuild": "0.20.1", "esbuild": "0.20.0",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-import-resolver-exports": "1.0.0-beta.5", "eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",
@ -211,13 +213,13 @@
"glob": "10.3.10", "glob": "10.3.10",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"playwright": "1.41.2", "playwright": "1.41.2",
"rollup": "4.12.0", "rollup": "4.10.0",
"typescript": "5.3.3", "typescript": "5.3.3",
"utf-8-validate": "6.0.3", "utf-8-validate": "6.0.3",
"vite": "5.1.3", "vite": "5.1.1",
"vite-plugin-inspect": "0.8.3", "vite-plugin-inspect": "0.8.3",
"vite-plugin-resolve": "2.5.1", "vite-plugin-resolve": "2.5.1",
"vite-plugin-solid": "2.10.1", "vite-plugin-solid": "2.9.1",
"ws": "8.16.0" "ws": "8.16.0"
}, },
"auto-changelog": { "auto-changelog": {
@ -226,5 +228,5 @@
"unreleased": true, "unreleased": true,
"output": "changelog.md" "output": "changelog.md"
}, },
"packageManager": "pnpm@8.15.3" "packageManager": "pnpm@8.15.1"
} }

View 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

File diff suppressed because it is too large Load Diff

View File

@ -16,44 +16,25 @@ const migrations = {
secret?: string; secret?: string;
}; };
if (lastfmConfig) { if (lastfmConfig) {
let scrobblerConfig = store.get( const scrobblerConfig = store.get(
'plugins.scrobbler', 'plugins.scrobbler',
) as { ) as {
enabled?: boolean; enabled?: boolean;
scrobblers?: { scrobblers: {
lastfm?: { lastfm: {
enabled?: boolean; enabled?: boolean;
token?: string; token?: string;
sessionKey?: string; session_key?: string;
apiRoot?: string; api_root?: string;
apiKey?: string; api_key?: string;
secret?: 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.set('plugins.scrobbler', scrobblerConfig);
store.delete('plugins.lastfm');
} }
}, },
'>=3.0.0'(store: Conf<Record<string, unknown>>) { '>=3.0.0'(store: Conf<Record<string, unknown>>) {

14
src/electron-chromecast.d.ts vendored Normal file
View 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;
}

View File

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

View File

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

View File

@ -98,7 +98,7 @@
"submenu": { "submenu": {
"auto-reset-app-cache": "Mengatur ulang cache aplikasi saat aplikasi dimulai", "auto-reset-app-cache": "Mengatur ulang cache aplikasi saat aplikasi dimulai",
"disable-hardware-acceleration": "Menonaktifkan akselerasi perangkat keras", "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", "override-user-agent": "Mengesampingkan User-Agent",
"restart-on-config-changes": "Mulai ulang pada perubahan konfigurasi", "restart-on-config-changes": "Mulai ulang pada perubahan konfigurasi",
"set-proxy": { "set-proxy": {
@ -194,7 +194,7 @@
"show": "Tampilkan jendela", "show": "Tampilkan jendela",
"tooltip": { "tooltip": {
"default": "YouTube Musik", "default": "YouTube Musik",
"with-song-info": "YouTube Music: {{artist}} - {{title}}" "with-song-info": "YouTube Music: {{artist}} - {{Judul}}"
} }
} }
}, },
@ -207,7 +207,7 @@
"name": "Pemblokir Iklan" "name": "Pemblokir Iklan"
}, },
"album-actions": { "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" "name": "Tindakan Album"
}, },
"album-color-theme": { "album-color-theme": {
@ -398,285 +398,7 @@
"video-id-not-found": "Video tidak ditemukan", "video-id-not-found": "Video tidak ditemukan",
"writing-id3": "Menulis tanda ID3…" "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"
} }
} }
} }

View File

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

View File

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

View File

@ -212,13 +212,6 @@
}, },
"album-color-theme": { "album-color-theme": {
"description": "Stosuje dynamiczny motyw i efekty wizualne w oparciu o paletę kolorów albumu", "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" "name": "Motyw kolorów albumu"
}, },
"ambient-mode": { "ambient-mode": {
@ -576,24 +569,6 @@
"description": "Umożliwia zmianę jakości wideo za pomocą przycisku na nakładce wideo", "description": "Umożliwia zmianę jakości wideo za pomocą przycisku na nakładce wideo",
"name": "Zmieniacz jakości 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": { "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", "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": { "menu": {

View File

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

View File

@ -30,7 +30,7 @@ import {
import { fetchFromGenius } from '@/plugins/lyrics-genius/main'; import { fetchFromGenius } from '@/plugins/lyrics-genius/main';
import { isEnabled } from '@/config/plugins'; 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 { getNetFetchAsFetch } from '@/plugins/utils/main';
import { cache } from '@/providers/decorators'; import { cache } from '@/providers/decorators';
@ -686,7 +686,6 @@ const getMetadata = (info: TrackInfo): CustomSongInfo => ({
?.url, ?.url,
views: info.basic_info.view_count!, views: info.basic_info.view_count!,
songDuration: info.basic_info.duration!, songDuration: info.basic_info.duration!,
mediaType: MediaType.Audio,
}); });
// This is used to bypass age restrictions // This is used to bypass age restrictions

View File

@ -4,24 +4,8 @@ export interface InAppMenuConfig {
} }
export const defaultInAppMenuConfig: InAppMenuConfig = { export const defaultInAppMenuConfig: InAppMenuConfig = {
enabled: enabled:
( (typeof window !== 'undefined' &&
( !window.navigator?.userAgent?.includes('mac')) ||
typeof window !== 'undefined' && (typeof global !== 'undefined' && global.process?.platform !== 'darwin'),
!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'
)
),
hideDOMWindowControls: false, hideDOMWindowControls: false,
}; };

View File

@ -2,7 +2,7 @@ import is from 'electron-is';
import { t } from '@/i18n'; import { t } from '@/i18n';
import type { InAppMenuConfig } from './constants'; import type { InAppMenuConfig } from './index';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';

View File

@ -1,6 +1,5 @@
import { JSX, splitProps } from 'solid-js'; import { JSX, splitProps } from 'solid-js';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
import { cache } from '@/providers/decorators'; import { cache } from '@/providers/decorators';
const menuStyle = cache(() => css` const menuStyle = cache(() => css`

View File

@ -4,7 +4,6 @@ import { css } from 'solid-styled-components';
import { Transition } from 'solid-transition-group'; import { Transition } from 'solid-transition-group';
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom'; import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom';
import { useFloating } from 'solid-floating-ui'; import { useFloating } from 'solid-floating-ui';
import { cache } from '@/providers/decorators'; import { cache } from '@/providers/decorators';
const panelStyle = cache(() => css` const panelStyle = cache(() => css`
@ -132,7 +131,6 @@ export const Panel = (props: PanelProps) => {
<Show when={local.open}> <Show when={local.open}>
<ul <ul
{...leftProps} {...leftProps}
data-ytmd-sub-panel={true}
ref={setPanel} ref={setPanel}
class={panelStyle()} class={panelStyle()}
style={{ style={{

View File

@ -9,10 +9,9 @@ import { PanelItem } from './PanelItem';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { WindowController } from './WindowController'; import { WindowController } from './WindowController';
import { cache } from '@/providers/decorators';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '../constants'; import type { InAppMenuConfig } from '../constants';
import { cache } from '@/providers/decorators';
const titleStyle = cache(() => css` const titleStyle = cache(() => css`
-webkit-app-region: drag; -webkit-app-region: drag;
@ -244,19 +243,6 @@ export const TitleBar = (props: TitleBarProps) => {
props.ipc.on('window-maximize', refetchMaximize); props.ipc.on('window-maximize', refetchMaximize);
props.ipc.on('window-unmaximize', 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(() => { createEffect(() => {
@ -266,7 +252,7 @@ export const TitleBar = (props: TitleBarProps) => {
}); });
return ( return (
<nav data-ytmd-main-panel={true} class={titleStyle()} data-macos={props.isMacOS}> <nav class={titleStyle()} data-macos={props.isMacOS}>
<IconButton <IconButton
onClick={() => setCollapsed(!collapsed())} onClick={() => setCollapsed(!collapsed())}
style={{ style={{

View File

@ -38,7 +38,7 @@ export const fetchFromGenius = async (metadata: SongInfo) => {
const songArtist = `${cleanupName(metadata.artist)}`; const songArtist = `${cleanupName(metadata.artist)}`;
let lyrics: string | null; 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. Genius Lyrics behavior is observed.
*/ */
let hasAsianChars = false; let hasAsianChars = false;

View File

@ -74,7 +74,7 @@ export class Connection {
return conn; return conn;
} }
disconnect() { async disconnect() {
if (this._mode === 'disconnected') throw new Error('Already disconnected'); if (this._mode === 'disconnected') throw new Error('Already disconnected');
this._mode = 'disconnected'; this._mode = 'disconnected';

View File

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

View File

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

View File

@ -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 = { export type Profile = {
id: string; id: string;
handleId: string; handleId: string;

View File

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

View File

@ -11,7 +11,7 @@ export interface ScrobblerPluginConfig {
* *
* @default true * @default true
*/ */
scrobbleOtherMedia: boolean, scrobble_other_media: boolean,
scrobblers: { scrobblers: {
lastfm: { lastfm: {
/** /**
@ -27,19 +27,19 @@ export interface ScrobblerPluginConfig {
/** /**
* Session key used for scrobbling * Session key used for scrobbling
*/ */
sessionKey: string | undefined, session_key: string | undefined,
/** /**
* Root of the Last.fm API * Root of the Last.fm API
* *
* @default 'http://ws.audioscrobbler.com/2.0/' * @default 'http://ws.audioscrobbler.com/2.0/'
*/ */
apiRoot: string, api_root: string,
/** /**
* Last.fm api key registered by @semvis123 * Last.fm api key registered by @semvis123
* *
* @default '04d76faaac8726e60988e14c105d421a' * @default '04d76faaac8726e60988e14c105d421a'
*/ */
apiKey: string, api_key: string,
/** /**
* Last.fm api secret registered by @semvis123 * Last.fm api secret registered by @semvis123
* *
@ -63,27 +63,27 @@ export interface ScrobblerPluginConfig {
* *
* @default 'https://api.listenbrainz.org/1/' * @default 'https://api.listenbrainz.org/1/'
*/ */
apiRoot: string, api_root: string,
}, },
} }
} }
export const defaultConfig: ScrobblerPluginConfig = { export const defaultConfig: ScrobblerPluginConfig = {
enabled: false, enabled: false,
scrobbleOtherMedia: true, scrobble_other_media: true,
scrobblers: { scrobblers: {
lastfm: { lastfm: {
enabled: false, enabled: false,
token: undefined, token: undefined,
sessionKey: undefined, session_key: undefined,
apiRoot: 'https://ws.audioscrobbler.com/2.0/', api_root: 'http://ws.audioscrobbler.com/2.0/',
apiKey: '04d76faaac8726e60988e14c105d421a', api_key: '04d76faaac8726e60988e14c105d421a',
secret: 'a5d2a36fdf64819290f6982481eaffa2', secret: 'a5d2a36fdf64819290f6982481eaffa2',
}, },
listenbrainz: { listenbrainz: {
enabled: false, enabled: false,
token: undefined, token: undefined,
apiRoot: 'https://api.listenbrainz.org/1/', api_root: 'https://api.listenbrainz.org/1/',
}, },
}, },
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,10 @@ import { net } from 'electron';
import { ScrobblerBase } from './base'; import { ScrobblerBase } from './base';
import type { SetConfType } from '../main'; import { SetConfType } from '../main';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
import type { ScrobblerPluginConfig } from '../index'; import type { ScrobblerPluginConfig } from '../index';
interface ListenbrainzRequestBody { interface ListenbrainzRequestBody {
@ -25,16 +27,16 @@ interface ListenbrainzRequestBody {
} }
export class ListenbrainzScrobbler extends ScrobblerBase { export class ListenbrainzScrobbler extends ScrobblerBase {
override isSessionCreated(): boolean { isSessionCreated(): boolean {
return true; return true;
} }
override createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> { createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> {
return Promise.resolve(config); return Promise.resolve(config);
} }
override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void { setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) { if (!config.scrobblers.listenbrainz.api_root || !config.scrobblers.listenbrainz.token) {
return; return;
} }
@ -42,8 +44,8 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
submitListen(body, config); submitListen(body, config);
} }
override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void { addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) { if (!config.scrobblers.listenbrainz.api_root || !config.scrobblers.listenbrainz.token) {
return; return;
} }
@ -78,7 +80,7 @@ function createRequestBody(listenType: string, songInfo: SongInfo): Listenbrainz
} }
function submitListen(body: ListenbrainzRequestBody, config: ScrobblerPluginConfig) { 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', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),

View File

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

View File

@ -1,117 +1,37 @@
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import MprisPlayer, { import mpris, { Track } from '@jellybrick/mpris-service';
Track,
LoopStatus,
type PlayBackStatus,
type PlayerOptions,
PLAYBACK_STATUS_STOPPED,
PLAYBACK_STATUS_PAUSED,
PLAYBACK_STATUS_PLAYING,
LOOP_STATUS_NONE,
LOOP_STATUS_PLAYLIST,
LOOP_STATUS_TRACK,
type Position,
} from '@jellybrick/mpris-service';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback from '@/providers/song-info';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import config from '@/config'; import config from '@/config';
import { LoggerPrefix } from '@/utils';
import 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() { function setupMPRIS() {
const instance = new YTPlayer({ const instance = new mpris({
name: 'YoutubeMusic', name: 'youtube-music',
identity: 'YouTube Music', identity: 'YouTube Music',
supportedMimeTypes: ['audio/mpeg'], supportedMimeTypes: ['audio/mpeg'],
supportedInterfaces: ['player'], supportedInterfaces: ['player'],
}); });
instance.canRaise = true; instance.canRaise = true;
instance.canQuit = false; instance.supportedUriSchemes = ['https'];
instance.canSetFullscreen = true;
instance.supportedUriSchemes = ['http', 'https'];
instance.desktopEntry = 'youtube-music'; instance.desktopEntry = 'youtube-music';
return instance; return instance;
} }
function registerMPRIS(win: BrowserWindow) { function registerMPRIS(win: BrowserWindow) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } =
playPause, songControls;
next,
previous,
setVolume,
shuffle,
switchRepeat,
setFullscreen,
requestFullscreenInformation,
requestQueueInformation,
} = songControls;
try { try {
let currentSongInfo: SongInfo | null = null; // TODO: "Typing" for this arguments
const secToMicro = (n: number) => Math.round(Number(n) * 1e6); const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6);
const microToSec = (n: number) => Math.round(Number(n) / 1e6); const microToSec = (n: unknown) => Math.round(Number(n) / 1e6);
const correctId = (videoId: string) => { const seekTo = (e: { position: unknown }) =>
return videoId.replace('-', '_MINUS_'); win.webContents.send('ytmd:seek-to', microToSec(e.position));
}; const seekBy = (o: unknown) =>
win.webContents.send('ytmd:seek-by', microToSec(o));
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 player = setupMPRIS(); 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-time-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-fullscreen-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-autoplay-changed-listener', 'mpris');
requestFullscreenInformation();
requestQueueInformation();
}); });
ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t))); ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t)));
ipcMain.on('ytmd:time-changed', (_, t: number) => { let currentSeconds = 0;
player.setPosition(secToMicro(t)); 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) { switch (mode) {
case 'NONE': { case 'NONE': {
player.setLoopStatus(LOOP_STATUS_NONE); player.loopStatus = mpris.LOOP_STATUS_NONE;
break; break;
} }
case 'ONE': { case 'ONE': {
player.setLoopStatus(LOOP_STATUS_TRACK); player.loopStatus = mpris.LOOP_STATUS_TRACK;
break; break;
} }
case 'ALL': { case 'ALL': {
player.setLoopStatus(LOOP_STATUS_PLAYLIST); player.loopStatus = mpris.LOOP_STATUS_PLAYLIST;
// No default // No default
break; break;
} }
} }
requestQueueInformation();
}); });
player.on('loopStatus', (status: string) => {
ipcMain.on('ytmd:fullscreen-changed', (_, changedTo: boolean) => {
if (player.fullscreen === undefined || !player.canSetFullscreen) {
return;
}
player.fullscreen =
changedTo !== undefined ? changedTo : !player.fullscreen;
});
ipcMain.on(
'ytmd:set-fullscreen',
(_, isFullscreen: boolean | undefined) => {
if (!player.canSetFullscreen || isFullscreen === undefined) {
return;
}
player.fullscreen = isFullscreen;
},
);
ipcMain.on(
'ytmd:fullscreen-changed-supported',
(_, isFullscreenSupported: boolean) => {
player.canSetFullscreen = isFullscreenSupported;
},
);
ipcMain.on('ytmd:autoplay-changed', (_) => {
requestQueueInformation();
});
ipcMain.on('ytmd:get-queue-response', (_, queue: QueueResponse) => {
if (!queue) {
return;
}
const currentPosition = queue.items?.findIndex((it) =>
it?.playlistPanelVideoRenderer?.selected ||
it?.playlistPanelVideoWrapperRenderer?.primaryRenderer?.playlistPanelVideoRenderer?.selected
) ?? 0;
player.canGoPrevious = currentPosition !== 0;
let hasNext: boolean;
if (queue.autoPlaying) {
hasNext = true;
} else if (player.loopStatus === LOOP_STATUS_PLAYLIST) {
hasNext = true;
} else {
// Example: currentPosition = 0, queue.items.length = 29 -> hasNext = true
hasNext = !!(currentPosition - (queue?.items?.length ?? 0 - 1));
}
player.canGoNext = hasNext;
});
player.on('loopStatus', (status: LoopStatus) => {
// SwitchRepeat cycles between states in that order // SwitchRepeat cycles between states in that order
const switches = [ const switches = [
LOOP_STATUS_NONE, mpris.LOOP_STATUS_NONE,
LOOP_STATUS_PLAYLIST, mpris.LOOP_STATUS_PLAYLIST,
LOOP_STATUS_TRACK, mpris.LOOP_STATUS_TRACK,
]; ];
const currentIndex = switches.indexOf(player.loopStatus); const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status); const targetIndex = switches.indexOf(status);
// Get a delta in the range [0,2] // Get a delta in the range [0,2]
const delta = (targetIndex - currentIndex + 3) % 3; const delta = (targetIndex - currentIndex + 3) % 3;
switchRepeat(delta); songControls.switchRepeat(delta);
}); });
player.getPosition = () => secToMicro(currentSeconds);
player.on('raise', () => { player.on('raise', () => {
if (!player.canRaise) {
return;
}
win.setSkipTaskbar(false); win.setSkipTaskbar(false);
win.show(); win.show();
}); });
player.on('fullscreen', (fullscreenEnabled: boolean) => {
setFullscreen(fullscreenEnabled);
});
player.on('play', () => { player.on('play', () => {
if (!player.isPlaying()) { if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) {
player.setPlaybackStatus(PLAYBACK_STATUS_PLAYING); player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING;
playPause(); playPause();
} }
}); });
player.on('pause', () => { player.on('pause', () => {
if (!player.isPaused()) { if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) {
player.setPlaybackStatus(PLAYBACK_STATUS_PAUSED); player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED;
playPause(); playPause();
} }
}); });
player.on('playpause', () => { player.on('playpause', () => {
player.setPlaybackStatus( player.playbackStatus =
player.isPlaying() ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING, player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING
); ? mpris.PLAYBACK_STATUS_PAUSED
: mpris.PLAYBACK_STATUS_PLAYING;
playPause(); playPause();
}); });
player.on('next', () => { player.on('next', next);
next(); player.on('previous', previous);
});
player.on('previous', () => {
previous();
});
player.on('seek', seekBy); player.on('seek', seekBy);
player.on('position', seekTo); player.on('position', seekTo);
@ -266,18 +114,10 @@ function registerMPRIS(win: BrowserWindow) {
player.on('shuffle', (enableShuffle) => { player.on('shuffle', (enableShuffle) => {
if (enableShuffle) { if (enableShuffle) {
shuffle(); shuffle();
requestQueueInformation();
} }
}); });
player.on('open', (args: { uri: string }) => { player.on('open', (args: { uri: string }) => {
win.loadURL(args.uri).then(() => { win.loadURL(args.uri);
requestQueueInformation();
});
});
player.on('error', (error: Error) => {
console.error(LoggerPrefix, 'Error in MPRIS');
console.trace(error);
}); });
let mprisVolNewer = false; 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')) { if (config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value. // With precise volume we can set the volume to the exact value.
const newVol = ~~(newVolume * 100); const newVol = ~~(newVolume * 100);
@ -306,46 +146,45 @@ function registerMPRIS(win: BrowserWindow) {
win.webContents.send('setVolume', newVol); win.webContents.send('setVolume', newVol);
} }
} else { } 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) { if (player) {
const data: Track = { const data: Track = {
'mpris:length': secToMicro(songInfo.songDuration), 'mpris:length': secToMicro(songInfo.songDuration),
...(songInfo.imageSrc 'mpris:artUrl': songInfo.imageSrc ?? undefined,
? { 'mpris:artUrl': songInfo.imageSrc }
: undefined),
'xesam:title': songInfo.title, 'xesam:title': songInfo.title,
'xesam:url': songInfo.url, 'xesam:url': songInfo.url,
'xesam:artist': [songInfo.artist], 'xesam:artist': [songInfo.artist],
'mpris:trackid': player.objectPath( 'mpris:trackid': '/',
`Track/${correctId(songInfo.videoId)}`,
),
}; };
if (songInfo.album) { if (songInfo.album) {
data['xesam:album'] = songInfo.album; data['xesam:album'] = songInfo.album;
} }
currentSongInfo = songInfo;
player.metadata = data; player.metadata = data;
player.seeked(secToMicro(songInfo.elapsedSeconds));
const currentElapsedMicroSeconds = secToMicro( player.playbackStatus = songInfo.isPaused
songInfo.elapsedSeconds ?? 0, ? mpris.PLAYBACK_STATUS_PAUSED
); : mpris.PLAYBACK_STATUS_PLAYING;
player.setPosition(currentElapsedMicroSeconds);
player.seeked(currentElapsedMicroSeconds);
player.setPlaybackStatus(
songInfo.isPaused ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
);
} }
requestQueueInformation();
}); });
} catch (error) { } catch (error) {
console.error(LoggerPrefix, 'Error in MPRIS'); console.warn('Error in MPRIS', error);
console.trace(error);
} }
} }

View File

@ -1,37 +1,18 @@
import { t } from '@/i18n'; import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
export default createPlugin< export default createPlugin({
unknown,
unknown,
{
observer?: MutationObserver;
waitForElem(selector: string): Promise<HTMLElement>;
start(): void;
stop(): void;
}
>({
name: () => t('plugins.skip-disliked-songs.name'), name: () => t('plugins.skip-disliked-songs.name'),
description: () => t('plugins.skip-disliked-songs.description'), description: () => t('plugins.skip-disliked-songs.description'),
restartNeeded: false, restartNeeded: false,
renderer: { renderer: {
waitForElem(selector: string) { observer: null as MutationObserver | null,
return new Promise<HTMLElement>((resolve) => {
const interval = setInterval(() => {
const elem = document.querySelector<HTMLElement>(selector);
if (!elem) return;
clearInterval(interval);
resolve(elem);
});
});
},
start() { start() {
this.waitForElem('#like-button-renderer').then((likeBtn) => { this.waitForElem('#like-button-renderer').then((likeBtn) => {
this.observer = new MutationObserver(() => { this.observer = new MutationObserver(() => {
if (likeBtn?.getAttribute('like-status') == 'DISLIKE') { if (likeBtn?.getAttribute('like-status') == 'DISLIKE') {
document document
.querySelector<HTMLButtonElement>('tp-yt-paper-icon-button.next-button') .querySelector('tp-yt-paper-icon-button.next-button')
?.click(); ?.click();
} }
}); });
@ -45,5 +26,16 @@ export default createPlugin<
stop() { stop() {
this.observer?.disconnect(); this.observer?.disconnect();
}, },
waitForElem(selector) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const elem = document.querySelector(selector);
if (!elem) return;
clearInterval(interval);
resolve(elem);
});
});
},
}, },
}); });

View File

@ -1,6 +1,8 @@
import { contextBridge, ipcRenderer, IpcRendererEvent, webFrame } from 'electron'; import { contextBridge, ipcRenderer, IpcRendererEvent, webFrame } from 'electron';
import is from 'electron-is'; import is from 'electron-is';
import { injectChromeCompatToObject, chrome } from '@jellybrick/electron-chromecast';
import config from './config'; import config from './config';
import { import {
@ -53,6 +55,8 @@ contextBridge.exposeInMainWorld(
'ELECTRON_RENDERER_URL', 'ELECTRON_RENDERER_URL',
process.env.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]; const [path, script] = ipcRenderer.sendSync('get-renderer-script') as [string | null, string];
let blocked = true; let blocked = true;

View File

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

View File

@ -1,82 +1,39 @@
// This is used for to control the songs // This is used for to control the songs
import { BrowserWindow } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
// see protocol-handler.ts
type ArgsType<T> = T | string[] | undefined;
const parseNumberFromArgsType = (args: ArgsType<number>) => {
if (typeof args === 'number') {
return args;
} else if (Array.isArray(args)) {
return Number(args[0]);
} else {
return null;
}
};
const parseBooleanFromArgsType = (args: ArgsType<boolean>) => {
if (typeof args === 'boolean') {
return args;
} else if (Array.isArray(args)) {
return args[0] === 'true';
} else {
return null;
}
};
export default (win: BrowserWindow) => { export default (win: BrowserWindow) => {
const commands = {
// 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 { return {
// Playback ...commands,
previous: () => win.webContents.send('ytmd:previous-video'), play: commands.playPause,
next: () => win.webContents.send('ytmd:next-video'), pause: commands.playPause,
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: '/',
});
},
}; };
}; };

View File

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

View File

@ -7,7 +7,7 @@ import config from '@/config';
import type { GetPlayerResponse } from '@/types/get-player-response'; import type { GetPlayerResponse } from '@/types/get-player-response';
export enum MediaType { enum MediaType {
/** /**
* Audio uploaded by the original artist * Audio uploaded by the original artist
*/ */
@ -120,24 +120,11 @@ const handleData = async (
songInfo.mediaType = MediaType.PodcastEpisode; songInfo.mediaType = MediaType.PodcastEpisode;
// HACK: Podcast's participant is not the artist // HACK: Podcast's participant is not the artist
if (!config.get('options.usePodcastParticipantAsArtist')) { if (!config.get('options.usePodcastParticipantAsArtist')) {
songInfo.artist = cleanupName( songInfo.artist = cleanupName(data.microformat.microformatDataRenderer.pageOwnerDetails.name);
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
);
} }
break; break;
default: default:
songInfo.mediaType = MediaType.OtherVideo; 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; 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 // This will be called when the song-info-front finds a new request with song data
ipcMain.on('ytmd:video-src-changed', async (_, data: GetPlayerResponse) => { ipcMain.on('ytmd:video-src-changed', async (_, data: GetPlayerResponse) => {
const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>( const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(async () => {
async () => { songInfo = await handleData(data, win);
songInfo = await handleData(data, win); return songInfo;
return songInfo; });
},
);
if (tempSongInfo) { if (tempSongInfo) {
for (const c of callbacks) { for (const c of callbacks) {

View File

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

1
src/reset.d.ts vendored
View File

@ -22,6 +22,7 @@ declare global {
ipcRenderer: typeof electronIpcRenderer; ipcRenderer: typeof electronIpcRenderer;
mainConfig: typeof config; mainConfig: typeof config;
electronIs: typeof is; electronIs: typeof is;
caster: typeof window.chrome;
ELECTRON_RENDERER_URL: string | undefined; ELECTRON_RENDERER_URL: string | undefined;
/** /**
* YouTube Music internal variable (Last interaction time) * YouTube Music internal variable (Last interaction time)

View File

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

View File

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

View File

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

View File

@ -35,11 +35,6 @@ img {
user-select: none; 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 */ /* Remove useless inaccessible button on top-right corner of the video player */
.ytp-chrome-top-buttons { .ytp-chrome-top-buttons {
display: none !important; display: none !important;