Compare commits

...

285 Commits

Author SHA1 Message Date
a33a03f512 Bump version to 3.7.1 2024-12-27 01:44:11 +09:00
f8a53f0d61 fix(api-server): fix swagger 2024-12-27 01:43:03 +09:00
748d77d1c0 fix(downloader): fix #2234 2024-12-27 01:30:09 +09:00
725ad0d630 fix(deps): update dependency node-html-parser to v7 (#2776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 01:15:58 +09:00
bdbab17772 chore(deps): update dependency vite to v6.0.6 (#2774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 01:15:18 +09:00
57d2fa372d fix(synced-lyrics): use blur instead of static background color 2024-12-27 01:13:53 +09:00
80471b0ca4 fix: use networkManager.fetch instead of fetch 2024-12-27 01:05:55 +09:00
22fdfe3342 fix(downloader): fix #2769 2024-12-27 00:50:18 +09:00
5ecfa2a1f7 fix: fix #2645, fix #2741 2024-12-26 23:49:19 +09:00
b9beea810e fix(music-together): fix crash 2024-12-26 23:14:42 +09:00
f0e77812e7 fix: fix login CORS issue 2024-12-26 23:13:28 +09:00
6d1237c2a2 chore(i18n): Translated using Weblate (Hindi)
Currently translated at 29.8% (121 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hi/
2024-12-26 06:04:43 +00:00
b43c92386e chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (406 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-12-26 06:04:42 +00:00
017476a81b chore(i18n): Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (406 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-12-26 06:04:41 +00:00
9b047d9c54 chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (406 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-12-26 06:04:40 +00:00
31f009d3c4 chore(i18n): Translated using Weblate (Polish)
Currently translated at 98.7% (401 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-12-26 06:04:40 +00:00
8504f2c086 chore(i18n): Translated using Weblate (Italian)
Currently translated at 99.2% (403 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-12-26 06:04:39 +00:00
1d6251baea feat(api-server): Add queue api (#2767) 2024-12-25 18:55:24 +09:00
3ea13a2a22 fix(api-server): repeat event listener 2024-12-25 13:08:02 +09:00
1cc153084d Update changelog for v3.7.0 2024-12-25 01:45:32 +00:00
1c468b4054 Bump version to 3.7.0 2024-12-25 10:28:55 +09:00
1bad46890a fix: fix possible NPE 2024-12-25 10:28:24 +09:00
5829c8d0f7 chore(i18n): Translated using Weblate (Urdu)
Currently translated at 15.7% (64 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ur/
2024-12-25 02:17:53 +01:00
85aceaaae4 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (406 of 406 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-12-25 02:15:11 +01:00
24e593b22f fix: use HEAD instead of GET in songInfo.imageSrc validation step
Thanks to @daika7ana!
Close #2766
2024-12-25 09:49:30 +09:00
3f8ca6002e chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (403 of 403 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-12-25 01:45:27 +01:00
b62ccfe7b1 feat(amuse): song query api (add amuse plugin) (#2723)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-12-25 09:45:19 +09:00
237dde9765 feat(api-server): add absolute seek endpoint (#2748)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-12-25 08:46:51 +09:00
65f4339fd1 fix: flatpak baseVersion 2024-12-25 08:42:59 +09:00
109e9f8166 feat(api-server): Add repeat mode and seek time API (#2630)
Co-authored-by: sent44 <sent44@root533.premium-rootserver.net>
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-12-25 08:41:44 +09:00
9163b6f04b fix(ci): Bump flatpak runtime version 2024-12-25 08:17:40 +09:00
51da259c97 feat(synced-lyrics): Better-Lyrics Styling for Synced-Lyrics (#2554)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-12-25 08:15:36 +09:00
2bf67b941e chore(i18n): Translated using Weblate (Urdu)
Currently translated at 15.7% (63 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ur/
2024-12-24 23:44:35 +01:00
533b96d1f6 feat(synced-lyrics): multiple lyric sources (#2383)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-12-25 07:44:29 +09:00
5c9ded8779 chore(deps): update dependency typescript-eslint to v8.18.2 (#2763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-25 01:42:18 +09:00
6f389bb297 chore(i18n): Added translation using Weblate (Urdu) 2024-12-24 08:59:48 +01:00
8a209404d4 chore(i18n): Translated using Weblate (Hindi)
Currently translated at 26.1% (105 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hi/
2024-12-24 08:59:35 +01:00
6193fb487a chore(i18n): Translated using Weblate (Hindi)
Currently translated at 25.9% (104 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hi/
2024-12-23 17:00:41 +01:00
9aa7f7a023 chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2024-12-23 17:00:40 +01:00
5bfaa9a791 chore(i18n): Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-12-23 17:00:39 +01:00
py
d210ec8227 chore(i18n): Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-12-23 17:00:38 +01:00
dec7c5e95c chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 99.0% (397 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2024-12-23 17:00:37 +01:00
940d0beb84 chore(i18n): Translated using Weblate (French)
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-12-23 17:00:36 +01:00
cf98754276 chore(deps): update dependency discord-api-types to v0.37.114 (#2761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-24 00:37:14 +09:00
d91d493dd1 chore(deps): update dependency discord-api-types to v0.37.113 (#2759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 09:11:34 +09:00
7e1aea21db fix: Set correct window class for X11 and Wayland (#2758) 2024-12-23 09:10:57 +09:00
0179dfd311 feat: Specify flatpak runtime (#2755) 2024-12-22 23:39:05 +09:00
98ea26bbff chore(deps): update dependency rollup to v4.29.1 (#2749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-22 23:38:48 +09:00
0d9daaad66 chore(deps): update dependency esbuild to v0.24.2 (#2742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-22 23:38:33 +09:00
fe319daec1 fix: Add Flatpak permissions needed for MPRIS and tray icon (#2754) 2024-12-22 23:37:30 +09:00
929c58671a chore(deps): update dependency vite-plugin-inspect to v0.10.6 (#2756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-22 23:36:28 +09:00
4fb2350c2b chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-12-21 16:00:29 +01:00
a401bfa809 chore(i18n): Translated using Weblate (Japanese)
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ja/
2024-12-21 16:00:28 +01:00
fdeed76f6f chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (401 of 401 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-12-21 16:00:27 +01:00
0ab113816a chore(deps): update dependency vite to v6.0.5 (#2745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 23:33:52 +09:00
8a58b02c7b fix(deps): update dependency i18next to v24.2.0 (#2744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 23:32:16 +09:00
037b059b55 chore(deps): update dependency vite-plugin-inspect to v0.10.4 (#2743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 23:32:07 +09:00
bb0f9fb3d0 chore(deps): update dependency discord-api-types to v0.37.112 (#2740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 23:31:24 +09:00
d3c7848896 fix(discord): use net.fetch instead of fetch 2024-12-20 23:23:15 +09:00
ea50cb1e65 fix(discord): check imageSrc 2024-12-20 23:22:48 +09:00
5070fd88b5 fix(discord): Fix Album Art failing on Discord RPC (#2666) 2024-12-20 23:19:51 +09:00
21177478cb fix(equalizer): add addedVersion 2024-12-20 23:19:33 +09:00
26b8b38b89 feat: Add equalizer plugin with presets (e.g. bass booster) (#2575) 2024-12-20 23:17:11 +09:00
be04d66aa8 fix: use ghostry adblocker 2024-12-19 19:53:43 +09:00
a837987e70 chore(deps): update dependency vite to v6.0.4 (#2738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 19:28:44 +09:00
da99558163 fix: Fixed #1796 (#2736) 2024-12-19 19:24:32 +09:00
3b50cbcb6e Revert "feat: use swc and lightningcss"
This reverts commit ae3a289005.
2024-12-19 12:42:31 +09:00
595c011bce Revert "fix: fix minify size issue"
This reverts commit 458fe54063.
2024-12-19 12:42:15 +09:00
458fe54063 fix: fix minify size issue 2024-12-19 12:38:49 +09:00
ae3a289005 feat: use swc and lightningcss 2024-12-19 12:22:15 +09:00
a49eea9246 chore(deps): update dependency electron-devtools-installer to v4 (#2734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 22:02:22 +09:00
d675a175e9 Revert "chore(deps): update dependency electron-builder to v25" (#2732)
This reverts commit 6c510a71c2.
2024-12-18 16:14:39 +09:00
6c510a71c2 chore(deps): update dependency electron-builder to v25 (#2490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 13:23:56 +09:00
5503d2cbb8 fix(deps): update dependency i18next to v24.1.2 (#2727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 12:05:24 +09:00
ba4c7e1a0c chore(deps): update dependency electron-devtools-installer to v3.2.1 (#2731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 12:02:00 +09:00
e19c458441 chore(deps): update dependency typescript-eslint to v8.18.1 (#2724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-17 09:11:12 +09:00
ec5cf0cae8 chore(i18n): Translated using Weblate (Filipino)
Currently translated at 87.1% (346 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-12-16 10:00:36 +01:00
b3c4570f8c fix: tab misalignment (#2713) 2024-12-16 11:49:57 +09:00
112b6d893b fix(deps): update dependency @hono/zod-validator to v0.4.2 (#2709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 22:02:16 +09:00
52236907e4 chore(deps): update eslint monorepo to v9.17.0 (#2712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 22:00:38 +09:00
d449529ea7 fix(deps): update dependency hono to v4.6.14 (#2716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 22:00:28 +09:00
61c799f7d4 fix: discord rich presence connection status (#2714)
* fix: discord rich presence connection status

* fix: optional chaining already handles null case
2024-12-15 22:00:00 +09:00
1a4ee13e47 fix(CI): temporary disable title check 2024-12-13 12:33:17 +09:00
f91afb984a fix: Laggy scrolling behaviour in large playlists (#2708) 2024-12-13 12:28:48 +09:00
f9892b0eae fix(deps): update dependency youtubei.js to v12.2.0 (#2705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 12:19:20 +09:00
60c7885a3c fix(deps): update dependency i18next to v24.1.0 (#2698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 20:58:08 +09:00
63ca6aa533 chore(deps): update dependency @stylistic/eslint-plugin-js to v2.12.1 (#2697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 20:57:53 +09:00
e77a8c04e8 fix(deps): update dependency zod to v3.24.1 (#2694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 13:30:22 +09:00
0bfabf604c fix(deps): update dependency youtubei.js to v12.1.0 (#2695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 07:22:18 +09:00
4343c599cf chore(deps): update dependency discord-api-types to v0.37.111 (#2690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 12:06:17 +09:00
c251554c31 chore(deps): update dependency typescript-eslint to v8.18.0 (#2692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 12:06:09 +09:00
95e519bdc9 chore(deps): update playwright monorepo to v1.49.1 (#2693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 12:06:00 +09:00
79d38bfc8e fix(deps): update dependency hono to v4.6.13 (#2682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:26:40 +09:00
f5655b0ae6 chore(deps): update dependency rollup to v4.28.1 (#2683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:26:24 +09:00
12b4afc3ce fix(deps): update dependency conf to v13.1.0 (#2686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:26:10 +09:00
c104d47737 chore(deps): update dependency @stylistic/eslint-plugin-js to v2.12.0 (#2689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:25:59 +09:00
870cf6143c chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 97.9% (389 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2024-12-06 18:00:23 +01:00
1baed0e913 fix(deps): update dependency youtubei.js to v12 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:11:21 +09:00
02619c79bb chore(deps): update dependency vite to v6.0.3 (#2680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:11:10 +09:00
0faad538f3 fix(album-actions): Fixed #2312 (#2676)
* Fixed #2312

* Update src/plugins/album-actions/index.ts

Co-authored-by: JellyBrick <shlee1503@naver.com>

---------

Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-12-06 12:10:10 +09:00
Bai
e7de30c629 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-12-05 15:00:29 +01:00
c5d8333039 chore(deps): update dependency eslint-import-resolver-typescript to v3.7.0 (#2672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 22:41:35 +09:00
4da08e7c9b chore(deps): update dependency node-gyp to v11 (#2678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 22:41:25 +09:00
Bai
171387995a chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-12-04 02:00:59 +00:00
aeac020c9a fix(deps): update dependency i18next to v24.0.5 (#2669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 21:51:58 +09:00
5c05ddeb29 fix(deps): update dependency i18next to v24.0.4 (#2668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 16:30:34 +09:00
cbdd649365 chore(deps): update dependency vite to v6.0.2 (#2662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 16:30:23 +09:00
d2cf2ad71f chore(deps): update dependency node-gyp to v10.3.1 (#2665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 16:28:21 +09:00
7d33494097 chore(deps): update dependency typescript-eslint to v8.17.0 (#2664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 16:28:12 +09:00
ca83edabf3 chore(deps): update dependency vite-plugin-inspect to v0.10.3 (#2667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 16:27:33 +09:00
f84e77e814 chore(deps): update dependency rollup to v4.28.0 (#2661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 22:07:47 +09:00
14f2120a32 chore(deps): update dependency discord-api-types to v0.37.110 (#2653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 22:04:03 +09:00
1cbf14ee2a fix(deps): update dependency @hono/zod-openapi to v0.18.3 (#2654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 22:03:54 +09:00
b7cb167fc6 chore(deps): update eslint monorepo to v9.16.0 (#2656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 22:03:45 +09:00
41b9f8b967 chore(deps): update dependency vite-plugin-inspect to v0.10.2 (#2657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 22:03:34 +09:00
0b769ce287 fix(youtube-music.css): Fixed #2514 (#2659) 2024-12-01 22:03:11 +09:00
ad71ef8a68 chore(i18n): Translated using Weblate (Arabic)
Currently translated at 36.2% (144 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ar/
2024-11-30 15:00:35 +01:00
048a994f32 fix: Fixed Skip Disliked Song not working (#2651) 2024-11-28 20:21:41 +09:00
c2bd8ce188 fix(deps): update dependency @hono/zod-openapi to v0.18.2 (#2650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 20:20:39 +09:00
62ce4e818c chore(deps): update dependency vite-plugin-inspect to v0.10.1 (#2652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 20:20:24 +09:00
4ab8829a02 chore(deps): update dependency electron to v33.2.1 (#2649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 12:02:27 +09:00
36b3e2cb0c chore(deps): update dependency vite-plugin-inspect to v0.10.0 (#2646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 23:20:27 +09:00
9ba0614a7d chore(deps): update dependency vite to v6 (#2644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 23:17:55 +09:00
81431ad196 fix(deps): update dependency @hono/swagger-ui to v0.5.0 (#2643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 23:16:11 +09:00
a8e8d5afd7 chore(deps): update dependency discord-api-types to v0.37.109 (#2642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 23:16:00 +09:00
f3d86743ee chore(deps): update dependency vite-plugin-solid to v2.11.0 (#2641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 23:15:50 +09:00
6306968193 fix(deps): update dependency hono to v4.6.12 (#2636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 07:24:16 +09:00
7142a253d6 fix(deps): update dependency i18next to v24.0.2 (#2637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 07:24:07 +09:00
2a24588338 chore(deps): update dependency discord-api-types to v0.37.108 (#2638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 07:23:58 +09:00
2abaf54ac8 chore(deps): update dependency typescript-eslint to v8.16.0 (#2639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 07:23:26 +09:00
cc730ad55c chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-11-25 16:01:11 +00:00
d8581c5d69 chore(i18n): Translated using Weblate (Hebrew)
Currently translated at 10.3% (41 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/he/
2024-11-24 16:00:26 +01:00
3208bf4a6d fix: fix pnpm-lock.yaml 2024-11-24 14:46:02 +09:00
4109db1ad7 chore(deps): update dependency rollup to v4.27.4 (#2632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 14:36:40 +09:00
87a0ef5d54 fix(deps): update dependency i18next to v24 (#2633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 14:34:40 +09:00
cdc40f0c53 chore(deps): update dependency typescript to v5.7.2 (#2629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 14:32:08 +09:00
7a3b8082a2 chore(deps): update dependency discord-api-types to v0.37.107 (#2627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 14:31:57 +09:00
3e9039c97d fix(deps): update dependency @hono/zod-openapi to v0.18.0 (#2626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 14:31:48 +09:00
b9d1130468 fix(deps): update dependency i18next to v23.16.8 (#2625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 14:31:38 +09:00
605f0984e4 chore(deps): update dependency vite-plugin-inspect to v0.8.8 (#2623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 12:27:36 +09:00
44de7d9e98 fix(deps): update dependency hono to v4.6.11 (#2624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 12:27:25 +09:00
9926575744 chore(deps): update playwright monorepo to v1.49.0 (#2617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 12:26:28 +09:00
f16a99f6e4 chore(deps): update dependency rollup to v4.27.3 (#2610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 12:10:10 +09:00
a23c64b5b8 chore(deps): update dependency typescript-eslint to v8.15.0 (#2611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 12:10:00 +09:00
0899f76548 chore(deps): update dependency @stylistic/eslint-plugin-js to v2.11.0 (#2618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 12:09:44 +09:00
515dcdc7e3 chore(i18n): Translated using Weblate (Arabic)
Currently translated at 35.7% (142 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ar/
2024-11-19 20:20:51 +01:00
b2c4bc425b chore(deps): update dependency discord-api-types to v0.37.105 (#2603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:36:03 +09:00
363c3b3a67 chore(deps): update dependency rollup to v4.27.2 (#2604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:35:54 +09:00
c2dde3d78f chore(deps): update eslint monorepo to v9.15.0 (#2607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:35:45 +09:00
eb515cfc61 fix(deps): update dependency @hono/zod-openapi to v0.17.1 (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:35:34 +09:00
c208ca184f chore(i18n): Translated using Weblate (Estonian)
Currently translated at 19.3% (77 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/et/
2024-11-17 01:00:26 +01:00
c231fa7c44 fix(ambient-mode): fix ambient-mode overlapping other elements (#2609)
Co-authored-by: h-banii <h-banii@users.noreply.github.com>
2024-11-16 18:41:29 +09:00
9e1b8d43d0 fix: Allow media playback control (MPRIS) for flatpak (#2606) 2024-11-16 15:45:09 +09:00
f50ece88df chore(i18n): Translated using Weblate (Slovenian)
Currently translated at 69.2% (275 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/sl/
2024-11-16 00:00:34 +01:00
eeb780d190 fix(deps): update dependency @hono/node-server to v1.13.7 (#2598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 01:59:11 +09:00
cafdf654d3 chore(deps): update dependency rollup to v4.26.0 (#2600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 01:47:36 +09:00
2d665013e7 fix(deps): update dependency hono to v4.6.10 (#2601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 01:47:25 +09:00
451a46e208 chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-11-12 19:00:31 +01:00
490b901c34 fix(deps): update dependency @hono/node-server to v1.13.6 (#2594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 18:12:53 +09:00
b57b4a3454 chore(deps): update dependency vite to v5.4.11 (#2595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 18:12:43 +09:00
60c61e32b1 chore(deps): update dependency typescript-eslint to v8.14.0 (#2596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 18:12:30 +09:00
aa9052d449 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 99.7% (396 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-11-11 10:00:21 +00:00
67f3a38583 chore(deps): update dependency electron to v33.2.0 (#2591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 22:45:49 +09:00
a00ecc4729 fix(deps): update dependency @hono/zod-openapi to v0.17.0 (#2592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 22:44:18 +09:00
56d63fca52 fix(deps): update dependency i18next to v23.16.5 (#2589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:49:48 +09:00
759f3ba317 fix(deps): update dependency @hono/node-server to v1.13.5 (#2578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:49:41 +09:00
d8daf03f2c fix(deps): update dependency hono to v4.6.9 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:47:33 +09:00
a519c7c714 chore(deps): update dependency discord-api-types to v0.37.104 (#2588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:47:27 +09:00
933d12fdd1 chore(deps): update dependency typescript-eslint to v8.13.0 (#2581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:44:46 +09:00
61fb733550 chore(deps): update dependency rollup to v4.25.0 (#2580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 18:44:40 +09:00
7f05e3168d chore(docs): Update screenshot (#2587) 2024-11-10 18:44:31 +09:00
3b7697a90d chore(docs): Specify full path to xattr for macOS, fixes #2583 (#2586) 2024-11-10 18:43:34 +09:00
350b1467fe chore(i18n): Translated using Weblate (Sinhala)
Currently translated at 12.3% (49 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/si/
2024-11-07 17:00:17 +00:00
2f5d102f4d chore(i18n): Translated using Weblate (Turkish)
Currently translated at 98.9% (393 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-11-05 21:00:23 +01:00
66e296df1a fix(taskbar-mediacontrol): remove console.log 2024-11-03 19:21:47 +09:00
1e4cd699db fix: callback for time-changed event (#2577)
Co-authored-by: Derek Alsop <15299183+Azorant@users.noreply.github.com>
2024-11-03 19:18:06 +09:00
516fbff3d7 fix: innerHTML trusted-types 2024-11-03 18:22:33 +09:00
aab9358d67 chore(deps): update eslint monorepo to v9.14.0 (#2573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 15:48:00 +09:00
ae3939f857 chore(deps): update dependency utf-8-validate to v6.0.5 (#2572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 15:45:54 +09:00
79bafd1780 chore(deps): update dependency @stylistic/eslint-plugin-js to v2.10.1 (#2571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 15:44:46 +09:00
3f4f52a31f chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-11-01 15:00:19 +00:00
c2b7b29716 fix(deps): update dependency @hono/node-server to v1.13.4 (#2570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 07:19:44 +09:00
dab84b9cf9 chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-10-31 15:00:16 +01:00
e5980158eb chore(deps): update dependency @stylistic/eslint-plugin-js to v2.10.0 (#2569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 12:24:46 +09:00
647d4c9d99 fix(deps): update dependency @floating-ui/dom to v1.6.12 (#2568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 12:24:38 +09:00
30feb6128b chore(deps): update dependency rollup to v4.24.3 (#2565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 07:57:12 +09:00
0cf6923540 fix(deps): update dependency hono to v4.6.8 (#2564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 07:57:04 +09:00
cce9f0b462 chore(deps): update dependency typescript-eslint to v8.12.2 (#2563)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 07:56:55 +09:00
c0805fb758 chore(deps): update dependency typescript-eslint to v8.12.0 (#2561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 07:13:13 +09:00
04e5844301 fix(deps): update dependency youtubei.js to v11 (#2562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 07:13:03 +09:00
f28e663133 chore(i18n): Translated using Weblate (Slovenian)
Currently translated at 69.2% (275 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/sl/
2024-10-28 17:00:25 +01:00
2c84527c43 chore(i18n): Translated using Weblate (Czech)
Currently translated at 95.2% (378 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-10-28 17:00:23 +01:00
68511de727 chore(deps): update dependency rollup to v4.24.2 (#2559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 19:33:15 +09:00
e7ca9f129f fix(deps): update dependency @hono/node-server to v1.13.3 (#2560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 19:33:06 +09:00
259da70e4f fix(deps): update dependency i18next to v23.16.4 (#2550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 19:01:37 +09:00
9409d75ac7 chore(deps): update playwright monorepo to v1.48.2 (#2551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 19:01:24 +09:00
3ea923f56f fix(deps): update dependency hono to v4.6.7 (#2552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 19:01:13 +09:00
6d6c8c94cf chore(deps): update dependency @babel/runtime to v7.26.0 (#2548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 19:01:03 +09:00
29098758a9 chore(i18n): Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-10-27 16:00:14 +01:00
fdbb35e221 chore(deps): update dependency @types/color to v4 (#2547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 12:15:35 +09:00
6ddac62313 fix(deps): update dependency i18next to v23.16.3 (#2545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 07:18:08 +09:00
ae4b494300 chore(i18n): Translated using Weblate (Arabic)
Currently translated at 31.2% (124 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ar/
2024-10-24 21:13:04 +02:00
Adi
7d9eed88f4 chore(i18n): Translated using Weblate (French)
Currently translated at 98.7% (392 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-10-24 21:13:04 +02:00
4abf848f99 fix(deps): update dependency solid-js to v1.9.3 (#2541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 14:36:36 +09:00
cc0a6cfdce chore(deps): update dependency vite to v5.4.10 (#2542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 14:25:16 +09:00
a74d0dd0ca chore(deps): update dependency electron to v33.0.2 (#2537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 07:08:31 +09:00
18f15d4cce chore(deps): update dependency @babel/runtime to v7.25.9 (#2538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 07:08:22 +09:00
80e20c6579 chore(i18n): Translated using Weblate (German)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-10-22 19:15:42 +00:00
761026fd74 chore(i18n): Translated using Weblate (German)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-10-22 19:15:41 +00:00
95b75f020c chore(deps): update dependency discord-api-types to v0.37.103 (#2532)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 22:28:47 +09:00
574e4baef8 chore(deps): update dependency typescript-eslint to v8.11.0 (#2534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 22:28:39 +09:00
ec4871d5a8 fix(deps): update dependency hono to v4.6.6 (#2536)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 18:36:51 +09:00
ccd6bf9c3f chore(i18n): Translated using Weblate (Arabic)
Currently translated at 27.9% (111 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ar/
2024-10-21 12:16:16 +02:00
2975d4292c chore(i18n): Translated using Weblate (Romanian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ro/
2024-10-21 12:16:15 +02:00
ad9571550f fix(tuna-obs): Added song url to tuna-obs plugin (#2524) 2024-10-21 10:16:41 +09:00
980068217c fix(deps): update dependency i18next to v23.16.2 (#2530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 00:05:49 +09:00
18f69aea3f fix(deps): update dependency i18next to v23.16.1 (#2529)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-20 23:34:58 +09:00
add7cb9e48 chore(deps): update eslint monorepo to v9.13.0 (#2528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-20 23:34:48 +09:00
aa67a57971 chore(i18n): Translated using Weblate (Hindi)
Currently translated at 1.5% (6 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hi/
2024-10-20 10:16:29 +02:00
74f22b4474 chore(i18n): Translated using Weblate (Japanese)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ja/
2024-10-20 10:16:29 +02:00
DW
7ef4a23576 chore(i18n): Translated using Weblate (Hindi)
Currently translated at 0.2% (1 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hi/
2024-10-19 07:16:28 +02:00
3c90a1f459 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 97.4% (387 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-10-19 07:16:27 +02:00
DW
3793d36f36 chore(i18n): Added translation using Weblate (Hindi) 2024-10-18 07:01:23 +02:00
179f4b29db chore(i18n): Translated using Weblate (Persian)
Currently translated at 74.3% (295 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fa/
2024-10-18 07:01:22 +02:00
34f106896c chore(i18n): Translated using Weblate (Icelandic)
Currently translated at 97.2% (386 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/is/
2024-10-18 07:01:22 +02:00
06b581f499 chore(deps): update dependency typescript-eslint to v8.10.0 (#2527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 07:47:15 +09:00
3ec126628c chore(deps): update playwright monorepo to v1.48.1 (#2516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 07:47:06 +09:00
1d3bb60e0b fix(tuna-obs): lite mode logic 2024-10-18 07:46:50 +09:00
2e3ced6006 fix(tuna-obs): fix lite mode logic 2024-10-18 07:45:49 +09:00
e8efca5a3e chore(i18n): Translated using Weblate (Persian)
Currently translated at 74.3% (295 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fa/
2024-10-17 19:16:24 +02:00
5161c356f9 chore(i18n): Translated using Weblate (Thai)
Currently translated at 70.2% (279 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/th/
2024-10-17 19:16:23 +02:00
py
e917b30e64 chore(i18n): Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-10-17 19:16:23 +02:00
2d847f9808 chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-10-17 19:16:22 +02:00
21755fffc7 chore(deps): update dependency electron to v33.0.1 (#2523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 11:37:56 +09:00
5c9d3e3e67 fix: disable gpu memory buffer video frames (#2519) 2024-10-17 07:11:33 +09:00
ea801f65ef Update changelog for v3.6.2 2024-10-16 11:59:00 +00:00
27f4c0393e Bump version to 3.6.2 2024-10-16 20:43:19 +09:00
9bc42f836f fix: trustedTypes issue
- Close #2339
2024-10-16 20:40:35 +09:00
11b11ed966 fix(deps): update dependency serve to v14.2.4 (#2515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 20:15:01 +09:00
5f79b7e788 chore(i18n): Translated using Weblate (Icelandic)
Currently translated at 94.7% (376 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/is/
2024-10-15 22:15:39 +02:00
7d1d806797 chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2024-10-15 22:15:39 +02:00
836cedb0f3 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2024-10-15 22:15:39 +02:00
70349e13cc chore(i18n): Translated using Weblate (Russian)
Currently translated at 99.7% (396 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-10-15 22:15:39 +02:00
6d16b74471 fix(deps): update dependency hono to v4.6.5 (#2509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 22:33:46 +09:00
c211780c33 chore(deps): update dependency vite to v5.4.9 (#2500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 13:46:51 +09:00
2173ba0234 fix(api-server): properly implement next api call (#2505) 2024-10-15 13:34:19 +09:00
3a4cbc543b chore(deps): update dependency electron to v33 (#2507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 13:30:11 +09:00
2fef7f0246 chore(deps): update dependency typescript-eslint to v8.9.0 (#2503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 05:03:21 +09:00
12d693921e chore(i18n): Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-10-14 19:24:56 +02:00
d1b4879f51 chore(deps): update dependency discord-api-types to v0.37.102 (#2501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 23:32:47 +09:00
f14939fcd2 Update changelog for v3.6.1 2024-10-14 13:42:17 +00:00
4e66b0cedd fix: remove snap arm 2024-10-14 22:23:37 +09:00
11fe54d640 fix(style): fix youtube music player layout not aligned issue 2024-10-14 22:01:26 +09:00
446529f738 fix: remove pacman 2024-10-14 21:59:56 +09:00
ac6e9deeb9 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-10-14 14:42:58 +02:00
100873163f fix: remove "compression": "maximum" 2024-10-14 21:34:33 +09:00
e141e18bac fix: fix build 2024-10-14 21:27:42 +09:00
f4da0c2c95 fix: enum value must be one of xz,lzo, got 'maximum' 2024-10-14 21:17:15 +09:00
7507bce3cc fix: fix org.electronjs.Electron2.BaseApp/x86_64/20.08 2024-10-14 21:03:10 +09:00
2b970fade8 fix: fix org.freedesktop.Sdk/x86_64/20.08 2024-10-14 20:51:36 +09:00
35f0e43082 fix: remove duplicate flatpak 2024-10-14 20:38:25 +09:00
ae4410a613 fix(flatpak): remove armhf/arm64 2024-10-14 20:32:05 +09:00
5534174016 fix: fix flatpak build 2024-10-14 20:27:33 +09:00
15cf6c77c3 fix: fix flatpak release 2024-10-14 20:12:54 +09:00
18a8fc462d fix: fix pnpm-lock 2024-10-14 20:01:02 +09:00
d3acb4945a chore(flatpak-builder): Add more details when failing 2024-10-14 19:57:41 +09:00
32d3c58b44 fix: fix release workflow 2024-10-14 19:33:37 +09:00
bd8c2eb390 fix: configuration has an unknown property 'AppImage' 2024-10-14 19:26:24 +09:00
a81fa9c0d1 fix: apt update 2024-10-14 19:22:02 +09:00
1d0f7d7a48 fix: fix release 2024-10-14 19:20:04 +09:00
b6687307df Bump version to 3.6.1 2024-10-14 19:16:25 +09:00
7e07a44f68 fix(downloader): fix #2371 2024-10-14 18:27:04 +09:00
5ca66530ee fix(ytm-bugs): incorrect video ratio
- Close #2459
2024-10-14 18:16:22 +09:00
95acbe2b65 fix: fix conflict with adguard 2024-10-14 18:06:08 +09:00
534aeb163a fix(api-server): fix init/authentication error
- Close #2497
2024-10-14 17:51:59 +09:00
6abcbee290 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-10-14 09:54:33 +02:00
4457a043a4 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-10-14 09:54:05 +02:00
410a052fea chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-10-14 09:51:36 +02:00
e4287085a1 chore(i18n): Translated using Weblate (Filipino)
Currently translated at 87.1% (346 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-10-14 09:51:36 +02:00
b6cefef8fb fix(api-server): Various fixes and improvements (#2496) 2024-10-14 16:48:11 +09:00
9d7e2a06bc fix(deps): update dependency electron-debug to v4.1.0 (#2499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 16:45:19 +09:00
d516fc2153 fix(renderer): fix force like buttons display logic (#2493) 2024-10-14 04:27:23 +09:00
77bfe8e218 fix: RSS feed CORS issue
- Close #1620
2024-10-14 04:04:17 +09:00
0fcbe38837 fix(deps): update dependency i18next to v23.16.0 (#2492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 03:46:38 +09:00
b85a40f683 Update changelog for v3.6.0 2024-10-13 13:57:28 +00:00
121 changed files with 7121 additions and 2503 deletions

View File

@ -62,6 +62,12 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
sudo snap install snapcraft --classic sudo snap install snapcraft --classic
sudo apt update
sudo apt install -y flatpak flatpak-builder
sudo flatpak remote-add --if-not-exists --system flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install -y flathub org.freedesktop.Platform/x86_64/24.08
sudo flatpak install -y flathub org.freedesktop.Sdk/x86_64/24.08
sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp/x86_64/24.08
pnpm release:linux pnpm release:linux
- name: Build and release on Windows - name: Build and release on Windows

View File

@ -12,7 +12,7 @@
</div> </div>
![Screenshot](web/screenshot.jpg "Screenshot") ![Screenshot](web/screenshot.png "Screenshot")
<div align="center"> <div align="center">
@ -95,6 +95,8 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸]
- **Downloader**: downloads - **Downloader**: downloads
MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl) MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
- **Equalizer**: add filters to boost or cut specific range of frequencies (e.g. bass booster)
- **Exponential Volume**: Makes the volume - **Exponential Volume**: Makes the volume
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
select lower volumes select lower volumes
@ -192,7 +194,7 @@ brew install th-ch/youtube-music/youtube-music
If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal: If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal:
```bash ```bash
xattr -cr /Applications/YouTube\ Music.app /usr/bin/xattr -cr /Applications/YouTube\ Music.app
``` ```
### Windows ### Windows

View File

@ -2,8 +2,246 @@
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.7.0](https://github.com/th-ch/youtube-music/compare/v3.6.2...v3.7.0)
- feat(amuse): song query api (add amuse plugin) [`#2723`](https://github.com/th-ch/youtube-music/pull/2723)
- feat(api-server): add absolute seek endpoint [`#2748`](https://github.com/th-ch/youtube-music/pull/2748)
- feat(api-server): Add repeat mode and seek time API [`#2630`](https://github.com/th-ch/youtube-music/pull/2630)
- feat(synced-lyrics): Better-Lyrics Styling for Synced-Lyrics [`#2554`](https://github.com/th-ch/youtube-music/pull/2554)
- feat(synced-lyrics): multiple lyric sources [`#2383`](https://github.com/th-ch/youtube-music/pull/2383)
- chore(deps): update dependency typescript-eslint to v8.18.2 [`#2763`](https://github.com/th-ch/youtube-music/pull/2763)
- chore(deps): update dependency discord-api-types to v0.37.114 [`#2761`](https://github.com/th-ch/youtube-music/pull/2761)
- chore(deps): update dependency discord-api-types to v0.37.113 [`#2759`](https://github.com/th-ch/youtube-music/pull/2759)
- fix: Set correct window class for X11 and Wayland [`#2758`](https://github.com/th-ch/youtube-music/pull/2758)
- feat: Specify flatpak runtime [`#2755`](https://github.com/th-ch/youtube-music/pull/2755)
- chore(deps): update dependency rollup to v4.29.1 [`#2749`](https://github.com/th-ch/youtube-music/pull/2749)
- chore(deps): update dependency esbuild to v0.24.2 [`#2742`](https://github.com/th-ch/youtube-music/pull/2742)
- fix: Add Flatpak permissions needed for MPRIS and tray icon [`#2754`](https://github.com/th-ch/youtube-music/pull/2754)
- chore(deps): update dependency vite-plugin-inspect to v0.10.6 [`#2756`](https://github.com/th-ch/youtube-music/pull/2756)
- chore(deps): update dependency vite to v6.0.5 [`#2745`](https://github.com/th-ch/youtube-music/pull/2745)
- fix(deps): update dependency i18next to v24.2.0 [`#2744`](https://github.com/th-ch/youtube-music/pull/2744)
- chore(deps): update dependency vite-plugin-inspect to v0.10.4 [`#2743`](https://github.com/th-ch/youtube-music/pull/2743)
- chore(deps): update dependency discord-api-types to v0.37.112 [`#2740`](https://github.com/th-ch/youtube-music/pull/2740)
- fix(discord): Fix Album Art failing on Discord RPC [`#2666`](https://github.com/th-ch/youtube-music/pull/2666)
- feat: Add equalizer plugin with presets (e.g. bass booster) [`#2575`](https://github.com/th-ch/youtube-music/pull/2575)
- chore(deps): update dependency vite to v6.0.4 [`#2738`](https://github.com/th-ch/youtube-music/pull/2738)
- fix: Fixed #1796 [`#2736`](https://github.com/th-ch/youtube-music/pull/2736)
- chore(deps): update dependency electron-devtools-installer to v4 [`#2734`](https://github.com/th-ch/youtube-music/pull/2734)
- Revert "chore(deps): update dependency electron-builder to v25" [`#2732`](https://github.com/th-ch/youtube-music/pull/2732)
- chore(deps): update dependency electron-builder to v25 [`#2490`](https://github.com/th-ch/youtube-music/pull/2490)
- fix(deps): update dependency i18next to v24.1.2 [`#2727`](https://github.com/th-ch/youtube-music/pull/2727)
- chore(deps): update dependency electron-devtools-installer to v3.2.1 [`#2731`](https://github.com/th-ch/youtube-music/pull/2731)
- chore(deps): update dependency typescript-eslint to v8.18.1 [`#2724`](https://github.com/th-ch/youtube-music/pull/2724)
- fix: tab misalignment [`#2713`](https://github.com/th-ch/youtube-music/pull/2713)
- fix(deps): update dependency @hono/zod-validator to v0.4.2 [`#2709`](https://github.com/th-ch/youtube-music/pull/2709)
- chore(deps): update eslint monorepo to v9.17.0 [`#2712`](https://github.com/th-ch/youtube-music/pull/2712)
- fix(deps): update dependency hono to v4.6.14 [`#2716`](https://github.com/th-ch/youtube-music/pull/2716)
- fix: discord rich presence connection status [`#2714`](https://github.com/th-ch/youtube-music/pull/2714)
- fix: Laggy scrolling behaviour in large playlists [`#2708`](https://github.com/th-ch/youtube-music/pull/2708)
- fix(deps): update dependency youtubei.js to v12.2.0 [`#2705`](https://github.com/th-ch/youtube-music/pull/2705)
- fix(deps): update dependency i18next to v24.1.0 [`#2698`](https://github.com/th-ch/youtube-music/pull/2698)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.12.1 [`#2697`](https://github.com/th-ch/youtube-music/pull/2697)
- fix(deps): update dependency zod to v3.24.1 [`#2694`](https://github.com/th-ch/youtube-music/pull/2694)
- fix(deps): update dependency youtubei.js to v12.1.0 [`#2695`](https://github.com/th-ch/youtube-music/pull/2695)
- chore(deps): update dependency discord-api-types to v0.37.111 [`#2690`](https://github.com/th-ch/youtube-music/pull/2690)
- chore(deps): update dependency typescript-eslint to v8.18.0 [`#2692`](https://github.com/th-ch/youtube-music/pull/2692)
- chore(deps): update playwright monorepo to v1.49.1 [`#2693`](https://github.com/th-ch/youtube-music/pull/2693)
- fix(deps): update dependency hono to v4.6.13 [`#2682`](https://github.com/th-ch/youtube-music/pull/2682)
- chore(deps): update dependency rollup to v4.28.1 [`#2683`](https://github.com/th-ch/youtube-music/pull/2683)
- fix(deps): update dependency conf to v13.1.0 [`#2686`](https://github.com/th-ch/youtube-music/pull/2686)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.12.0 [`#2689`](https://github.com/th-ch/youtube-music/pull/2689)
- fix(deps): update dependency youtubei.js to v12 [`#2681`](https://github.com/th-ch/youtube-music/pull/2681)
- chore(deps): update dependency vite to v6.0.3 [`#2680`](https://github.com/th-ch/youtube-music/pull/2680)
- fix(album-actions): Fixed #2312 [`#2676`](https://github.com/th-ch/youtube-music/pull/2676)
- chore(deps): update dependency eslint-import-resolver-typescript to v3.7.0 [`#2672`](https://github.com/th-ch/youtube-music/pull/2672)
- chore(deps): update dependency node-gyp to v11 [`#2678`](https://github.com/th-ch/youtube-music/pull/2678)
- fix(deps): update dependency i18next to v24.0.5 [`#2669`](https://github.com/th-ch/youtube-music/pull/2669)
- fix(deps): update dependency i18next to v24.0.4 [`#2668`](https://github.com/th-ch/youtube-music/pull/2668)
- chore(deps): update dependency vite to v6.0.2 [`#2662`](https://github.com/th-ch/youtube-music/pull/2662)
- chore(deps): update dependency node-gyp to v10.3.1 [`#2665`](https://github.com/th-ch/youtube-music/pull/2665)
- chore(deps): update dependency typescript-eslint to v8.17.0 [`#2664`](https://github.com/th-ch/youtube-music/pull/2664)
- chore(deps): update dependency vite-plugin-inspect to v0.10.3 [`#2667`](https://github.com/th-ch/youtube-music/pull/2667)
- chore(deps): update dependency rollup to v4.28.0 [`#2661`](https://github.com/th-ch/youtube-music/pull/2661)
- chore(deps): update dependency discord-api-types to v0.37.110 [`#2653`](https://github.com/th-ch/youtube-music/pull/2653)
- fix(deps): update dependency @hono/zod-openapi to v0.18.3 [`#2654`](https://github.com/th-ch/youtube-music/pull/2654)
- chore(deps): update eslint monorepo to v9.16.0 [`#2656`](https://github.com/th-ch/youtube-music/pull/2656)
- chore(deps): update dependency vite-plugin-inspect to v0.10.2 [`#2657`](https://github.com/th-ch/youtube-music/pull/2657)
- fix(youtube-music.css): Fixed #2514 [`#2659`](https://github.com/th-ch/youtube-music/pull/2659)
- fix: Fixed Skip Disliked Song not working [`#2651`](https://github.com/th-ch/youtube-music/pull/2651)
- fix(deps): update dependency @hono/zod-openapi to v0.18.2 [`#2650`](https://github.com/th-ch/youtube-music/pull/2650)
- chore(deps): update dependency vite-plugin-inspect to v0.10.1 [`#2652`](https://github.com/th-ch/youtube-music/pull/2652)
- chore(deps): update dependency electron to v33.2.1 [`#2649`](https://github.com/th-ch/youtube-music/pull/2649)
- chore(deps): update dependency vite-plugin-inspect to v0.10.0 [`#2646`](https://github.com/th-ch/youtube-music/pull/2646)
- chore(deps): update dependency vite to v6 [`#2644`](https://github.com/th-ch/youtube-music/pull/2644)
- fix(deps): update dependency @hono/swagger-ui to v0.5.0 [`#2643`](https://github.com/th-ch/youtube-music/pull/2643)
- chore(deps): update dependency discord-api-types to v0.37.109 [`#2642`](https://github.com/th-ch/youtube-music/pull/2642)
- chore(deps): update dependency vite-plugin-solid to v2.11.0 [`#2641`](https://github.com/th-ch/youtube-music/pull/2641)
- fix(deps): update dependency hono to v4.6.12 [`#2636`](https://github.com/th-ch/youtube-music/pull/2636)
- fix(deps): update dependency i18next to v24.0.2 [`#2637`](https://github.com/th-ch/youtube-music/pull/2637)
- chore(deps): update dependency discord-api-types to v0.37.108 [`#2638`](https://github.com/th-ch/youtube-music/pull/2638)
- chore(deps): update dependency typescript-eslint to v8.16.0 [`#2639`](https://github.com/th-ch/youtube-music/pull/2639)
- chore(deps): update dependency rollup to v4.27.4 [`#2632`](https://github.com/th-ch/youtube-music/pull/2632)
- fix(deps): update dependency i18next to v24 [`#2633`](https://github.com/th-ch/youtube-music/pull/2633)
- chore(deps): update dependency typescript to v5.7.2 [`#2629`](https://github.com/th-ch/youtube-music/pull/2629)
- chore(deps): update dependency discord-api-types to v0.37.107 [`#2627`](https://github.com/th-ch/youtube-music/pull/2627)
- fix(deps): update dependency @hono/zod-openapi to v0.18.0 [`#2626`](https://github.com/th-ch/youtube-music/pull/2626)
- fix(deps): update dependency i18next to v23.16.8 [`#2625`](https://github.com/th-ch/youtube-music/pull/2625)
- chore(deps): update dependency vite-plugin-inspect to v0.8.8 [`#2623`](https://github.com/th-ch/youtube-music/pull/2623)
- fix(deps): update dependency hono to v4.6.11 [`#2624`](https://github.com/th-ch/youtube-music/pull/2624)
- chore(deps): update playwright monorepo to v1.49.0 [`#2617`](https://github.com/th-ch/youtube-music/pull/2617)
- chore(deps): update dependency rollup to v4.27.3 [`#2610`](https://github.com/th-ch/youtube-music/pull/2610)
- chore(deps): update dependency typescript-eslint to v8.15.0 [`#2611`](https://github.com/th-ch/youtube-music/pull/2611)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.11.0 [`#2618`](https://github.com/th-ch/youtube-music/pull/2618)
- chore(deps): update dependency discord-api-types to v0.37.105 [`#2603`](https://github.com/th-ch/youtube-music/pull/2603)
- chore(deps): update dependency rollup to v4.27.2 [`#2604`](https://github.com/th-ch/youtube-music/pull/2604)
- chore(deps): update eslint monorepo to v9.15.0 [`#2607`](https://github.com/th-ch/youtube-music/pull/2607)
- fix(deps): update dependency @hono/zod-openapi to v0.17.1 [`#2608`](https://github.com/th-ch/youtube-music/pull/2608)
- fix(ambient-mode): fix ambient-mode overlapping other elements [`#2609`](https://github.com/th-ch/youtube-music/pull/2609)
- fix: Allow media playback control (MPRIS) for flatpak [`#2606`](https://github.com/th-ch/youtube-music/pull/2606)
- fix(deps): update dependency @hono/node-server to v1.13.7 [`#2598`](https://github.com/th-ch/youtube-music/pull/2598)
- chore(deps): update dependency rollup to v4.26.0 [`#2600`](https://github.com/th-ch/youtube-music/pull/2600)
- fix(deps): update dependency hono to v4.6.10 [`#2601`](https://github.com/th-ch/youtube-music/pull/2601)
- fix(deps): update dependency @hono/node-server to v1.13.6 [`#2594`](https://github.com/th-ch/youtube-music/pull/2594)
- chore(deps): update dependency vite to v5.4.11 [`#2595`](https://github.com/th-ch/youtube-music/pull/2595)
- chore(deps): update dependency typescript-eslint to v8.14.0 [`#2596`](https://github.com/th-ch/youtube-music/pull/2596)
- chore(deps): update dependency electron to v33.2.0 [`#2591`](https://github.com/th-ch/youtube-music/pull/2591)
- fix(deps): update dependency @hono/zod-openapi to v0.17.0 [`#2592`](https://github.com/th-ch/youtube-music/pull/2592)
- fix(deps): update dependency i18next to v23.16.5 [`#2589`](https://github.com/th-ch/youtube-music/pull/2589)
- fix(deps): update dependency @hono/node-server to v1.13.5 [`#2578`](https://github.com/th-ch/youtube-music/pull/2578)
- fix(deps): update dependency hono to v4.6.9 [`#2579`](https://github.com/th-ch/youtube-music/pull/2579)
- chore(deps): update dependency discord-api-types to v0.37.104 [`#2588`](https://github.com/th-ch/youtube-music/pull/2588)
- chore(deps): update dependency typescript-eslint to v8.13.0 [`#2581`](https://github.com/th-ch/youtube-music/pull/2581)
- chore(deps): update dependency rollup to v4.25.0 [`#2580`](https://github.com/th-ch/youtube-music/pull/2580)
- chore(docs): Update screenshot [`#2587`](https://github.com/th-ch/youtube-music/pull/2587)
- chore(docs): Specify full path to xattr for macOS, fixes #2583 [`#2586`](https://github.com/th-ch/youtube-music/pull/2586)
- fix: callback for time-changed event [`#2577`](https://github.com/th-ch/youtube-music/pull/2577)
- chore(deps): update eslint monorepo to v9.14.0 [`#2573`](https://github.com/th-ch/youtube-music/pull/2573)
- chore(deps): update dependency utf-8-validate to v6.0.5 [`#2572`](https://github.com/th-ch/youtube-music/pull/2572)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.10.1 [`#2571`](https://github.com/th-ch/youtube-music/pull/2571)
- fix(deps): update dependency @hono/node-server to v1.13.4 [`#2570`](https://github.com/th-ch/youtube-music/pull/2570)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.10.0 [`#2569`](https://github.com/th-ch/youtube-music/pull/2569)
- fix(deps): update dependency @floating-ui/dom to v1.6.12 [`#2568`](https://github.com/th-ch/youtube-music/pull/2568)
- chore(deps): update dependency rollup to v4.24.3 [`#2565`](https://github.com/th-ch/youtube-music/pull/2565)
- fix(deps): update dependency hono to v4.6.8 [`#2564`](https://github.com/th-ch/youtube-music/pull/2564)
- chore(deps): update dependency typescript-eslint to v8.12.2 [`#2563`](https://github.com/th-ch/youtube-music/pull/2563)
- chore(deps): update dependency typescript-eslint to v8.12.0 [`#2561`](https://github.com/th-ch/youtube-music/pull/2561)
- fix(deps): update dependency youtubei.js to v11 [`#2562`](https://github.com/th-ch/youtube-music/pull/2562)
- chore(deps): update dependency rollup to v4.24.2 [`#2559`](https://github.com/th-ch/youtube-music/pull/2559)
- fix(deps): update dependency @hono/node-server to v1.13.3 [`#2560`](https://github.com/th-ch/youtube-music/pull/2560)
- fix(deps): update dependency i18next to v23.16.4 [`#2550`](https://github.com/th-ch/youtube-music/pull/2550)
- chore(deps): update playwright monorepo to v1.48.2 [`#2551`](https://github.com/th-ch/youtube-music/pull/2551)
- fix(deps): update dependency hono to v4.6.7 [`#2552`](https://github.com/th-ch/youtube-music/pull/2552)
- chore(deps): update dependency @babel/runtime to v7.26.0 [`#2548`](https://github.com/th-ch/youtube-music/pull/2548)
- chore(deps): update dependency @types/color to v4 [`#2547`](https://github.com/th-ch/youtube-music/pull/2547)
- fix(deps): update dependency i18next to v23.16.3 [`#2545`](https://github.com/th-ch/youtube-music/pull/2545)
- fix(deps): update dependency solid-js to v1.9.3 [`#2541`](https://github.com/th-ch/youtube-music/pull/2541)
- chore(deps): update dependency vite to v5.4.10 [`#2542`](https://github.com/th-ch/youtube-music/pull/2542)
- chore(deps): update dependency electron to v33.0.2 [`#2537`](https://github.com/th-ch/youtube-music/pull/2537)
- chore(deps): update dependency @babel/runtime to v7.25.9 [`#2538`](https://github.com/th-ch/youtube-music/pull/2538)
- chore(deps): update dependency discord-api-types to v0.37.103 [`#2532`](https://github.com/th-ch/youtube-music/pull/2532)
- chore(deps): update dependency typescript-eslint to v8.11.0 [`#2534`](https://github.com/th-ch/youtube-music/pull/2534)
- fix(deps): update dependency hono to v4.6.6 [`#2536`](https://github.com/th-ch/youtube-music/pull/2536)
- fix(tuna-obs): Added song url to tuna-obs plugin [`#2524`](https://github.com/th-ch/youtube-music/pull/2524)
- fix(deps): update dependency i18next to v23.16.2 [`#2530`](https://github.com/th-ch/youtube-music/pull/2530)
- fix(deps): update dependency i18next to v23.16.1 [`#2529`](https://github.com/th-ch/youtube-music/pull/2529)
- chore(deps): update eslint monorepo to v9.13.0 [`#2528`](https://github.com/th-ch/youtube-music/pull/2528)
- chore(deps): update dependency typescript-eslint to v8.10.0 [`#2527`](https://github.com/th-ch/youtube-music/pull/2527)
- chore(deps): update playwright monorepo to v1.48.1 [`#2516`](https://github.com/th-ch/youtube-music/pull/2516)
- chore(deps): update dependency electron to v33.0.1 [`#2523`](https://github.com/th-ch/youtube-music/pull/2523)
- fix: disable gpu memory buffer video frames [`#2519`](https://github.com/th-ch/youtube-music/pull/2519)
- fix: use HEAD instead of GET in songInfo.imageSrc validation step [`#2766`](https://github.com/th-ch/youtube-music/issues/2766)
- fix: Fixed #1796 (#2736) [`#1796`](https://github.com/th-ch/youtube-music/issues/1796)
- fix(album-actions): Fixed #2312 (#2676) [`#2312`](https://github.com/th-ch/youtube-music/issues/2312) [`#2312`](https://github.com/th-ch/youtube-music/issues/2312)
- fix(youtube-music.css): Fixed #2514 (#2659) [`#2514`](https://github.com/th-ch/youtube-music/issues/2514)
- chore(docs): Specify full path to xattr for macOS, fixes #2583 (#2586) [`#2583`](https://github.com/th-ch/youtube-music/issues/2583)
- fix: fix pnpm-lock.yaml [`3208bf4`](https://github.com/th-ch/youtube-music/commit/3208bf4a6d47d824875b06bd031299694482f02d)
- Revert "feat: use swc and lightningcss" [`3b50cbc`](https://github.com/th-ch/youtube-music/commit/3b50cbcb6e3163115d52f05075af5d6f25b80660)
- feat: use swc and lightningcss [`ae3a289`](https://github.com/th-ch/youtube-music/commit/ae3a28900576ea388666747bc4794577e1d57e23)
#### [v3.6.2](https://github.com/th-ch/youtube-music/compare/v3.6.1...v3.6.2)
> 16 October 2024
- fix(deps): update dependency serve to v14.2.4 [`#2515`](https://github.com/th-ch/youtube-music/pull/2515)
- fix(deps): update dependency hono to v4.6.5 [`#2509`](https://github.com/th-ch/youtube-music/pull/2509)
- chore(deps): update dependency vite to v5.4.9 [`#2500`](https://github.com/th-ch/youtube-music/pull/2500)
- fix(api-server): properly implement next api call [`#2505`](https://github.com/th-ch/youtube-music/pull/2505)
- chore(deps): update dependency electron to v33 [`#2507`](https://github.com/th-ch/youtube-music/pull/2507)
- chore(deps): update dependency typescript-eslint to v8.9.0 [`#2503`](https://github.com/th-ch/youtube-music/pull/2503)
- chore(deps): update dependency discord-api-types to v0.37.102 [`#2501`](https://github.com/th-ch/youtube-music/pull/2501)
- fix: trustedTypes issue [`#2339`](https://github.com/th-ch/youtube-music/issues/2339)
- chore(i18n): Translated using Weblate (Icelandic) [`5f79b7e`](https://github.com/th-ch/youtube-music/commit/5f79b7e788c47b0a27a4967c9f3a9e20b483cd75)
- chore(i18n): Translated using Weblate (Chinese (Traditional Han script)) [`12d6939`](https://github.com/th-ch/youtube-music/commit/12d693921e26a5c54015673a404e005d1a7175a4)
- chore(i18n): Translated using Weblate (Ukrainian) [`836cedb`](https://github.com/th-ch/youtube-music/commit/836cedb0f317b74bf2fc3ec2d1aa865719f46ec0)
#### [v3.6.1](https://github.com/th-ch/youtube-music/compare/v3.6.0...v3.6.1)
> 14 October 2024
- fix(api-server): Various fixes and improvements [`#2496`](https://github.com/th-ch/youtube-music/pull/2496)
- fix(deps): update dependency electron-debug to v4.1.0 [`#2499`](https://github.com/th-ch/youtube-music/pull/2499)
- fix(renderer): fix force like buttons display logic [`#2493`](https://github.com/th-ch/youtube-music/pull/2493)
- fix(deps): update dependency i18next to v23.16.0 [`#2492`](https://github.com/th-ch/youtube-music/pull/2492)
- fix(downloader): fix #2371 [`#2371`](https://github.com/th-ch/youtube-music/issues/2371)
- fix(ytm-bugs): incorrect video ratio [`#2459`](https://github.com/th-ch/youtube-music/issues/2459)
- fix(api-server): fix init/authentication error [`#2497`](https://github.com/th-ch/youtube-music/issues/2497)
- fix: RSS feed CORS issue [`#1620`](https://github.com/th-ch/youtube-music/issues/1620)
- chore(flatpak-builder): Add more details when failing [`d3acb49`](https://github.com/th-ch/youtube-music/commit/d3acb4945a8dcde6598c53d8207bbf16eda8c739)
- chore(i18n): Translated using Weblate (Filipino) [`e428708`](https://github.com/th-ch/youtube-music/commit/e4287085a11f30d141148ab0432cc684819fd0d0)
- Bump version to 3.6.1 [`b668730`](https://github.com/th-ch/youtube-music/commit/b6687307dfe7ef765517019093c8db3c2ad14417)
#### [v3.6.0](https://github.com/th-ch/youtube-music/compare/v3.5.3...v3.6.0)
> 13 October 2024
- feat(api-server): remote control api [`#1909`](https://github.com/th-ch/youtube-music/pull/1909)
- chore(deps): update playwright monorepo to v1.48.0 [`#2489`](https://github.com/th-ch/youtube-music/pull/2489)
- fix(`synced-lyrics`): Fix 2 issues [`#2441`](https://github.com/th-ch/youtube-music/pull/2441)
- chore(deps): update dependency typescript to v5.6.3 [`#2486`](https://github.com/th-ch/youtube-music/pull/2486)
- chore(deps): update dependency electron to v32.2.0 [`#2487`](https://github.com/th-ch/youtube-music/pull/2487)
- chore(deps): update dependency del-cli to v6 [`#2475`](https://github.com/th-ch/youtube-music/pull/2475)
- chore(deps): update dependency typescript-eslint to v8.8.1 [`#2477`](https://github.com/th-ch/youtube-music/pull/2477)
- fix(deps): update dependency solid-js to v1.9.2 [`#2480`](https://github.com/th-ch/youtube-music/pull/2480)
- Revert "chore(deps): update dependency electron-builder to v25" [`#2488`](https://github.com/th-ch/youtube-music/pull/2488)
- chore(deps): update dependency electron-builder to v25 [`#2406`](https://github.com/th-ch/youtube-music/pull/2406)
- fix(deps): update dependency deepmerge-ts to v7.1.3 [`#2481`](https://github.com/th-ch/youtube-music/pull/2481)
- fix(deps): update dependency ts-morph to v24 [`#2474`](https://github.com/th-ch/youtube-music/pull/2474)
- fix(deps): update dependency i18next to v23.15.2 [`#2471`](https://github.com/th-ch/youtube-music/pull/2471)
- chore(deps): update eslint monorepo to v9.12.0 [`#2470`](https://github.com/th-ch/youtube-music/pull/2470)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.9.0 [`#2469`](https://github.com/th-ch/youtube-music/pull/2469)
- chore(deps): bump micromatch from 4.0.5 to 4.0.8 [`#2465`](https://github.com/th-ch/youtube-music/pull/2465)
- chore(deps): bump braces from 3.0.2 to 3.0.3 [`#2466`](https://github.com/th-ch/youtube-music/pull/2466)
- fix(deps): update dependency electron-updater to v6.3.9 [`#2468`](https://github.com/th-ch/youtube-music/pull/2468)
- fix(deps): update dependency deepmerge-ts to v7.1.1 [`#2467`](https://github.com/th-ch/youtube-music/pull/2467)
- chore(deps): update dependency typescript-eslint to v8.8.0 [`#2457`](https://github.com/th-ch/youtube-music/pull/2457)
- chore(deps): update dependency @babel/runtime to v7.25.7 [`#2462`](https://github.com/th-ch/youtube-music/pull/2462)
- chore(deps): update dependency rollup to v4.24.0 [`#2458`](https://github.com/th-ch/youtube-music/pull/2458)
- chore(deps): update dependency eslint-plugin-import to v2.31.0 [`#2464`](https://github.com/th-ch/youtube-music/pull/2464)
- chore(deps): update dependency rollup to v4.22.5 [`#2448`](https://github.com/th-ch/youtube-music/pull/2448)
- chore(deps): update dependency typescript-eslint to v8.7.0 [`#2450`](https://github.com/th-ch/youtube-music/pull/2450)
- fix(deps): update dependency solid-js to v1.9.1 [`#2451`](https://github.com/th-ch/youtube-music/pull/2451)
- chore(deps): update dependency vite to v5.4.8 [`#2449`](https://github.com/th-ch/youtube-music/pull/2449)
- chore(deps): update dependency discord-api-types to v0.37.101 [`#2440`](https://github.com/th-ch/youtube-music/pull/2440)
- chore(deps): update dependency esbuild to v0.24.0 [`#2439`](https://github.com/th-ch/youtube-music/pull/2439)
- chore(deps): update eslint monorepo to v9.11.1 [`#2442`](https://github.com/th-ch/youtube-music/pull/2442)
- chore(deps): update dependency @types/howler to v2.2.12 [`#2443`](https://github.com/th-ch/youtube-music/pull/2443)
- chore(deps): update dependency vite to v5.4.7 [`#2434`](https://github.com/th-ch/youtube-music/pull/2434)
- chore(deps): update playwright monorepo to v1.47.2 [`#2436`](https://github.com/th-ch/youtube-music/pull/2436)
- chore(deps): update eslint monorepo to v9.11.0 [`#2437`](https://github.com/th-ch/youtube-music/pull/2437)
- fix(deps): update dependency youtubei.js to v10.5.0 [`#2431`](https://github.com/th-ch/youtube-music/pull/2431)
- chore(deps): update dependency rollup to v4.22.4 [`#2430`](https://github.com/th-ch/youtube-music/pull/2430)
- chore(deps): update dependency electron to v32.1.2 [`#2433`](https://github.com/th-ch/youtube-music/pull/2433)
- feat: ESLint Flat Config (v9 support #2229) [`#2426`](https://github.com/th-ch/youtube-music/pull/2426)
- fix(taskbar-mediacontrol): fix icon color [`#2485`](https://github.com/th-ch/youtube-music/issues/2485)
- chore(eslint): apply eslint-plugin-prettier [`#2438`](https://github.com/th-ch/youtube-music/issues/2438)
- fix: apply fix from eslint [`cb1381b`](https://github.com/th-ch/youtube-music/commit/cb1381bbb394e2bbb404f44817ef96411dabc8a9)
- chore(i18n): Translated using Weblate (Portuguese (Brazil)) [`bcff26c`](https://github.com/th-ch/youtube-music/commit/bcff26c85b18258806f3960309776bc860c3a54e)
- chore(i18n): Translated using Weblate (Persian) [`ead448e`](https://github.com/th-ch/youtube-music/commit/ead448ed98095339557903eb0f84c4a6d0f32058)
#### [v3.5.3](https://github.com/th-ch/youtube-music/compare/v3.5.2...v3.5.3) #### [v3.5.3](https://github.com/th-ch/youtube-music/compare/v3.5.2...v3.5.3)
> 17 September 2024
- fix: fix `trustedHTML` issue [`#2339`](https://github.com/th-ch/youtube-music/issues/2339) - fix: fix `trustedHTML` issue [`#2339`](https://github.com/th-ch/youtube-music/issues/2339)
- chore(deps): update dependency rollup to v4.21.3 [`6edc84a`](https://github.com/th-ch/youtube-music/commit/6edc84a8bd6c7e009041117ba0d2004783eb3a47) - chore(deps): update dependency rollup to v4.21.3 [`6edc84a`](https://github.com/th-ch/youtube-music/commit/6edc84a8bd6c7e009041117ba0d2004783eb3a47)
- chore(deps): update typescript-eslint monorepo to v8.6.0 [`d4c8a43`](https://github.com/th-ch/youtube-music/commit/d4c8a4320d733f7bddc4dcd1de93644790e71d66) - chore(deps): update typescript-eslint monorepo to v8.6.0 [`d4c8a43`](https://github.com/th-ch/youtube-music/commit/d4c8a4320d733f7bddc4dcd1de93644790e71d66)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 721 KiB

View File

@ -12,7 +12,7 @@
</div> </div>
![Screenshot](/web/screenshot.jpg "Screenshot") ![Screenshot](/web/screenshot.png "Screenshot")
<div align="center"> <div align="center">
@ -182,7 +182,7 @@ brew install th-ch/youtube-music/youtube-music
Si instalas la aplicación manualmente y obtienes un error "está dañado y no se puede abrir" al iniciar la aplicación, ejecuta lo siguiente en la Terminal: Si instalas la aplicación manualmente y obtienes un error "está dañado y no se puede abrir" al iniciar la aplicación, ejecuta lo siguiente en la Terminal:
```bash ```bash
xattr -cr /Applications/YouTube\ Music.app /usr/bin/xattr -cr /Applications/YouTube\ Music.app
``` ```
### Windows ### Windows

View File

@ -12,7 +12,7 @@
</div> </div>
![Capture d'écran](https://github.com/th-ch/youtube-music/raw/master/web/screenshot.jpg "Capture d'écran") ![Capture d'écran](/web/screenshot.png "Capture d'écran")
<div align="center"> <div align="center">
@ -185,7 +185,7 @@ brew install th-ch/youtube-music/youtube-music
Si vous installez l'application manuellement et obtenez une erreur "est endommagé et ne peut pas être ouvert." lors du lancement de l'application, exécutez ce qui suit dans le Terminal : Si vous installez l'application manuellement et obtenez une erreur "est endommagé et ne peut pas être ouvert." lors du lancement de l'application, exécutez ce qui suit dans le Terminal :
```bash ```bash
xattr -cr /Applications/YouTube\ Music.app /usr/bin/xattr -cr /Applications/YouTube\ Music.app
``` ```
### Windows ### Windows

View File

@ -12,7 +12,7 @@
</div> </div>
![Screenshot](../../web/screenshot.jpg "Screenshot") ![Screenshot](/web/screenshot.png "Screenshot")
<div align="center"> <div align="center">
@ -180,7 +180,7 @@ brew install th-ch/youtube-music/youtube-music
Ef þú setur upp forritið handvirkt og færð villu "er skemmd og ekki er hægt að opna það," þegar þú ræsir forritið skaltu keyra eftirfarandi í flugstöðinni: Ef þú setur upp forritið handvirkt og færð villu "er skemmd og ekki er hægt að opna það," þegar þú ræsir forritið skaltu keyra eftirfarandi í flugstöðinni:
```bash ```bash
xattr -cr /Applications/YouTube\ Music.app /usr/bin/xattr -cr /Applications/YouTube\ Music.app
``` ```
### Windows ### Windows

View File

@ -12,7 +12,7 @@
</div> </div>
![Screenshot](../../web/screenshot.jpg "Screenshot") ![Screenshot](/web/screenshot.png "Screenshot")
<div align="center"> <div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest"> <a href="https://github.com/th-ch/youtube-music/releases/latest">
@ -147,7 +147,7 @@ brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master
(앱을 수동으로 설치하고) 앱을 실행할 때 `손상되었기 때문에 열 수 없습니다.`라는 오류가 발생하면 터미널에서 다음을 실행하세요: (앱을 수동으로 설치하고) 앱을 실행할 때 `손상되었기 때문에 열 수 없습니다.`라는 오류가 발생하면 터미널에서 다음을 실행하세요:
```bash ```bash
xattr -cr /Applications/YouTube\ Music.app /usr/bin/xattr -cr /Applications/YouTube\ Music.app
``` ```
### Windows ### Windows

View File

@ -12,7 +12,7 @@
</div> </div>
![Screenshot](web/screenshot.jpg "Screenshot") ![Screenshot](/web/screenshot.png "Screenshot")
<div align="center"> <div align="center">
@ -168,7 +168,7 @@ brew install th-ch/youtube-music/youtube-music
Если вы устанавливаете приложение вручную и получаете ошибку "is damaged and cant be opened.", запустите в терминале следующую команду: Если вы устанавливаете приложение вручную и получаете ошибку "is damaged and cant be opened.", запустите в терминале следующую команду:
```bash ```bash
xattr -cr /Applications/YouTube\ Music.app /usr/bin/xattr -cr /Applications/YouTube\ Music.app
``` ```
### Windows ### Windows

View File

@ -1,7 +1,8 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"desktopName": "com.github.th_ch.youtube_music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.6.0", "version": "3.7.1",
"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",
@ -21,7 +22,7 @@
"license", "license",
"!node_modules", "!node_modules",
"node_modules/custom-electron-prompt/**", "node_modules/custom-electron-prompt/**",
"node_modules/@cliqz/adblocker-electron-preload/**", "node_modules/@ghostery/adblocker-electron-preload/**",
"node_modules/@ffmpeg.wasm/core-mt/**", "node_modules/@ffmpeg.wasm/core-mt/**",
"!node_modules/**/*.map", "!node_modules/**/*.map",
"!node_modules/**/*.ts" "!node_modules/**/*.ts"
@ -40,7 +41,8 @@
] ]
} }
], ],
"icon": "assets/generated/icons/mac/icon.icns" "icon": "assets/generated/icons/mac/icon.icns",
"compression": "maximum"
}, },
"win": { "win": {
"icon": "assets/generated/icons/win/icon.ico", "icon": "assets/generated/icons/win/icon.ico",
@ -61,7 +63,8 @@
"arm64" "arm64"
] ]
} }
] ],
"compression": "maximum"
}, },
"nsisWeb": { "nsisWeb": {
"runAfterFinish": false "runAfterFinish": false
@ -69,12 +72,84 @@
"linux": { "linux": {
"icon": "assets/generated/icons/png", "icon": "assets/generated/icons/png",
"category": "AudioVideo", "category": "AudioVideo",
"desktop": {
"StartupWMClass": "com.github.th_ch.youtube_music"
},
"target": [ "target": [
"AppImage", {
"snap", "target": "AppImage",
"freebsd", "arch": [
"deb", "x64",
"rpm" "arm64",
"armv7l"
]
},
{
"target": "flatpak",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64",
"arm64",
"armv7l"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64"
]
},
{
"target": "freebsd",
"arch": [
"x64",
"arm64",
"armv7l"
]
},
{
"target": "tar.gz",
"arch": [
"x64",
"arm64",
"armv7l"
]
}
]
},
"appImage": {
"description": "YouTube Music Desktop App bundled with custom plugins (and built-in ad blocker / downloader)",
"category": "AudioVideo"
},
"flatpak": {
"description": "YouTube Music Desktop App bundled with custom plugins (and built-in ad blocker / downloader)",
"category": "AudioVideo",
"runtimeVersion": "24.08",
"baseVersion": "24.08",
"finishArgs": [
"--socket=wayland",
"--socket=x11",
"--share=ipc",
"--device=dri",
"--socket=pulseaudio",
"--share=network",
"--filesystem=xdg-music:rw",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.gnome.SessionManager",
"--talk-name=org.kde.StatusNotifierWatcher",
"--own-name=org.mpris.MediaPlayer2.YoutubeMusic.*"
] ]
}, },
"deb": { "deb": {
@ -121,6 +196,7 @@
"start": "electron-vite preview", "start": "electron-vite preview",
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
"dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch", "dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
"dev:renderer": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev", "dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect", "clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never", "dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
@ -139,36 +215,37 @@
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"engines": { "engines": {
"node": ">=18.0.0", "node": ">=18",
"pnpm": ">=8" "pnpm": ">=8"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"usocket": "1.0.1", "usocket": "1.0.1",
"node-gyp": "10.2.0", "node-gyp": "11.0.0",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"@electron/universal": "2.0.1", "@electron/universal": "2.0.1",
"@babel/runtime": "7.25.7" "@babel/runtime": "7.26.0"
}, },
"patchedDependencies": { "patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch", "vudio@2.1.1": "patches/vudio@2.1.1.patch",
"app-builder-lib@24.13.3": "patches/app-builder-lib@24.13.3.patch" "app-builder-lib@24.13.3": "patches/app-builder-lib@24.13.3.patch",
"@malept/flatpak-bundler": "patches/@malept__flatpak-bundler.patch"
} }
}, },
"dependencies": { "dependencies": {
"@cliqz/adblocker-electron": "1.27.1",
"@cliqz/adblocker-electron-preload": "1.27.1",
"@electron-toolkit/tsconfig": "1.0.1", "@electron-toolkit/tsconfig": "1.0.1",
"@electron/remote": "2.1.2", "@electron/remote": "2.1.2",
"@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0", "@ffmpeg.wasm/main": "0.12.0",
"@floating-ui/dom": "1.6.11", "@floating-ui/dom": "1.6.12",
"@foobar404/wave": "2.0.5", "@foobar404/wave": "2.0.5",
"@hono/node-server": "1.13.2", "@ghostery/adblocker-electron": "2.3.1",
"@hono/swagger-ui": "0.4.1", "@ghostery/adblocker-electron-preload": "2.3.1",
"@hono/zod-openapi": "0.16.4", "@hono/node-server": "1.13.7",
"@hono/zod-validator": "0.4.1", "@hono/swagger-ui": "0.5.0",
"@hono/zod-openapi": "0.18.3",
"@hono/zod-validator": "0.4.2",
"@jellybrick/electron-better-web-request": "1.0.4", "@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4", "@jellybrick/mpris-service": "2.1.4",
"@jimp/plugin-invert": "0.22.12", "@jimp/plugin-invert": "0.22.12",
@ -178,11 +255,11 @@
"butterchurn": "3.0.0-beta.4", "butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"color": "4.2.3", "color": "4.2.3",
"conf": "13.0.1", "conf": "13.1.0",
"custom-electron-prompt": "1.5.8", "custom-electron-prompt": "1.5.8",
"dbus-next": "0.10.2", "dbus-next": "0.10.2",
"deepmerge-ts": "7.1.3", "deepmerge-ts": "7.1.3",
"electron-debug": "4.0.1", "electron-debug": "4.1.0",
"electron-is": "3.0.0", "electron-is": "3.0.0",
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "10.0.0", "electron-store": "10.0.0",
@ -191,35 +268,35 @@
"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",
"hono": "4.6.4", "hono": "4.6.14",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.15.2", "i18next": "24.2.0",
"jimp": "1.6.0", "jimp": "1.6.0",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.13", "node-html-parser": "7.0.1",
"node-id3": "0.2.6", "node-id3": "0.2.6",
"peerjs": "1.5.4", "peerjs": "1.5.4",
"semver": "7.6.3", "semver": "7.6.3",
"serve": "14.2.3", "serve": "14.2.4",
"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.9.2", "solid-js": "1.9.3",
"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": "24.0.0", "ts-morph": "24.0.0",
"vudio": "2.1.1", "vudio": "2.1.1",
"x11": "2.3.0", "x11": "2.3.0",
"youtubei.js": "10.5.0", "youtubei.js": "12.2.0",
"zod": "3.23.8" "zod": "3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.12.0", "@eslint/js": "9.17.0",
"@playwright/test": "1.48.0", "@playwright/test": "1.49.1",
"@stylistic/eslint-plugin-js": "2.9.0", "@stylistic/eslint-plugin-js": "2.12.1",
"@total-typescript/ts-reset": "0.6.1", "@total-typescript/ts-reset": "0.6.1",
"@types/color": "3.0.6", "@types/color": "4.2.0",
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@types/howler": "2.2.12", "@types/howler": "2.2.12",
@ -230,29 +307,29 @@
"builtin-modules": "4.0.0", "builtin-modules": "4.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"del-cli": "6.0.0", "del-cli": "6.0.0",
"discord-api-types": "0.37.101", "discord-api-types": "0.37.114",
"electron": "32.2.0", "electron": "33.2.1",
"electron-builder": "24.13.3", "electron-builder": "24.13.3",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "4.0.0",
"electron-vite": "2.3.0", "electron-vite": "2.3.0",
"esbuild": "0.24.0", "esbuild": "0.24.2",
"eslint": "9.12.0", "eslint": "9.17.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.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.3", "eslint-import-resolver-typescript": "3.7.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "5.2.1", "eslint-plugin-prettier": "5.2.1",
"glob": "11.0.0", "glob": "11.0.0",
"node-gyp": "10.2.0", "node-gyp": "11.0.0",
"playwright": "1.48.0", "playwright": "1.49.1",
"rollup": "4.24.0", "rollup": "4.29.1",
"typescript": "5.6.3", "typescript": "5.7.2",
"typescript-eslint": "8.8.1", "typescript-eslint": "8.18.2",
"utf-8-validate": "6.0.4", "utf-8-validate": "6.0.5",
"vite": "5.4.8", "vite": "6.0.6",
"vite-plugin-inspect": "0.8.7", "vite-plugin-inspect": "0.10.6",
"vite-plugin-resolve": "2.5.2", "vite-plugin-resolve": "2.5.2",
"vite-plugin-solid": "2.10.2", "vite-plugin-solid": "2.11.0",
"ws": "8.18.0" "ws": "8.18.0"
}, },
"auto-changelog": { "auto-changelog": {

View File

@ -0,0 +1,29 @@
diff --git a/index.js b/index.js
index 5968fcf47b69094993b0f861c03f5560e4a6a9b7..0fe16d4f40612c0abfa57898909ce0083f56944c 100644
--- a/index.js
+++ b/index.js
@@ -56,19 +56,23 @@ function getOptionsWithDefaults (options, manifest) {
async function spawnWithLogging (options, command, args, allowFail) {
return new Promise((resolve, reject) => {
logger(`$ ${command} ${args.join(' ')}`)
+ const output = []
const child = childProcess.spawn(command, args, { cwd: options['working-dir'] })
child.stdout.on('data', (data) => {
+ output.push(data)
logger(`1> ${data}`)
})
child.stderr.on('data', (data) => {
+ output.push(data)
logger(`2> ${data}`)
})
child.on('error', (error) => {
+ logger(`error - ${error.message} ${error.stack}`)
reject(error)
})
child.on('close', (code) => {
if (!allowFail && code !== 0) {
- reject(new Error(`${command} failed with status code ${code}`))
+ reject(new Error(`${command} ${args.join(' ')} failed with status code ${code} ${output.join(' ')}`))
}
resolve(code === 0)
})

3122
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -195,6 +195,7 @@
}, },
"tray": { "tray": {
"next": "التالي", "next": "التالي",
"play-pause": "تشغيل/إيقاف",
"previous": "السابق", "previous": "السابق",
"quit": "خروج", "quit": "خروج",
"restart": "إعادة تشغيل التطبيق", "restart": "إعادة تشغيل التطبيق",
@ -206,9 +207,120 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "إذا تم عرض إعلان, فإن الصوت سيتم كتمانه وسيتم وضع سرعة التشغيل الى 16x",
"name": "تسريع الإعلان"
},
"adblocker": { "adblocker": {
"description": "حجب جميع الإعلانات والمسارات خارج الصندوق", "description": "حجب جميع الإعلانات والمتتبعات جاهز للأستخدام",
"menu": {
"blocker": "حاجب الإعلانات"
},
"name": "حاجب الإعلانات" "name": "حاجب الإعلانات"
},
"album-actions": {
"description": "يضيف أزرار \"إلغاء عدم الإعجاب\"، \"عدم الإعجاب\"، \"الإعجاب\"، و\"إلغاء الإعجاب\" لتطبيقها على جميع الأغاني في قائمة التشغيل أو الألبوم",
"name": "إجراءات الألبوم"
},
"album-color-theme": {
"description": "يطبق ثيمًا ديناميكيًا وتأثيرات بصرية بناء على ألوان الألبوم",
"menu": {
"color-mix-ratio": {
"label": "‬نسبة قوة اللون",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "ثيم ألوان الألبوم"
},
"ambient-mode": {
"description": "يطبق تأثير إضاءة عن طريق إسقاط ألوان ناعمة من الفيديو على خلفية شاشتك",
"menu": {
"blur-amount": {
"label": "مقدار التمويه",
"submenu": {
"pixels": "{{blurAmount}} بكسل"
}
},
"buffer": {
"label": "تخزين الصوت الؤقت",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "الشفافية",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "الجودة",
"submenu": {
"pixels": "{{quality}} بكسل"
}
},
"size": {
"label": "الحجم",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "انتقال السلاسة",
"submenu": {
"during": "خلال {{interpolationTime}} ثانيه"
}
},
"use-fullscreen": {
"label": "استخدام شاشه كامله"
}
},
"name": "الوضع المحيطي"
},
"api-server": {
"description": "يضيف خادم للتحكم في المشغل",
"dialog": {
"request": {
"buttons": {
"allow": "سماح",
"deny": "رفض"
}
}
},
"menu": {
"hostname": {
"label": "اسم المضيف"
}
},
"prompt": {
"hostname": {
"title": "اسم الخادم"
}
}
},
"blur-nav-bar": {
"description": "يجعل شريط التنقل شفاف و ضبابي"
},
"bypass-age-restrictions": {
"description": "تجاوز تَحَقّق اليوتيوب من السن",
"name": "تجاوز التحقق من السن"
},
"downloader": {
"backend": {
"feedback": {
"downloading-counter": "تنزيل {{current}}/{{total}}…",
"error-while-downloading": "خطأ في تحميل \"{{author}} - {{title}}\": {{error}}",
"loading": "جار التحميل…",
"preparing-file": "يتم تجهيز الملف…",
"saving": "يتم الحفظ…",
"video-id-not-found": "لم يتم ايجاد الفيديو"
}
},
"menu": {
"choose-download-folder": "اختر مكان التحميل"
}
} }
} }
} }

View File

@ -220,7 +220,7 @@
}, },
"album-actions": { "album-actions": {
"description": "Přidává Undislike, Dislike, Like, a Unlike tlačítka k aplikování tohoto ke všem písničkám v seznamu písniček nebo albumu", "description": "Přidává Undislike, Dislike, Like, a Unlike tlačítka k aplikování tohoto ke všem písničkám v seznamu písniček nebo albumu",
"name": "Album akce" "name": "Možnosti Albumu"
}, },
"album-color-theme": { "album-color-theme": {
"description": "Používá dynamický motiv a vizuální efekty na základě palety barev alba", "description": "Používá dynamický motiv a vizuální efekty na základě palety barev alba",
@ -279,6 +279,49 @@
}, },
"name": "Ambientní režim" "name": "Ambientní režim"
}, },
"api-server": {
"description": "Vlož API server abys mohl ovládat přehrávač",
"dialog": {
"request": {
"buttons": {
"allow": "Povolit",
"deny": "Zakázat"
},
"message": "Povolit {{ID}} ({{origin}}) přístup k API?",
"title": "dotaz na přihlášení k API"
}
},
"menu": {
"auth-strategy": {
"label": "Možnosti přihlášení",
"submenu": {
"auth-at-first": {
"label": "Ověřit při prvním dotazu"
},
"none": {
"label": "Žádná autorizace"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API server [Beta]",
"prompt": {
"hostname": {
"label": "Zadej hostname API serveru (ve tvaru 0.0.0.0):",
"title": "Hostname"
},
"port": {
"label": "Zadej port API serveru:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Apply compression k audiu (snižuje hlasitost nejhlasitěších částí signálu and zvyšuje hlasitost nejjemnějších částí)", "description": "Apply compression k audiu (snižuje hlasitost nejhlasitěších částí signálu and zvyšuje hlasitost nejjemnějších částí)",
"name": "Audio kompresor" "name": "Audio kompresor"
@ -415,8 +458,18 @@
"menu": { "menu": {
"choose-download-folder": "Vybrat složku pro stahování", "choose-download-folder": "Vybrat složku pro stahování",
"download-finish-settings": { "download-finish-settings": {
"label": "Stáhnout po dokončení",
"prompt": {
"last-percent": "Po x procentech",
"last-seconds": "Posledních x vteřin",
"title": "Nastavit kdy stahovat"
},
"submenu": { "submenu": {
"advanced": "Pokoročile" "advanced": "Pokoročile",
"enabled": "Zapnuto",
"mode": "Časový režim",
"percent": "Procent",
"seconds": "Sekundy"
} }
}, },
"download-playlist": "Stáhnout seznam písniček", "download-playlist": "Stáhnout seznam písniček",
@ -616,11 +669,13 @@
"name": "Scrobbler", "name": "Scrobbler",
"prompt": { "prompt": {
"lastfm": { "lastfm": {
"api-key": "Last,fm API klíč" "api-key": "Last,fm API klíč",
"api-secret": "Tajný klíč API Last.fm"
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "Vložte svůj Listenbrainz user token:" "label": "Vložte svůj Listenbrainz user token:",
"title": "ListenBrainz token"
} }
} }
} }
@ -656,6 +711,22 @@
"description": "Automaticky přeskakuje nehudební části jako intro/outro nebo části hudebních videí, kde nehraje písnčka", "description": "Automaticky přeskakuje nehudební části jako intro/outro nebo části hudebních videí, kde nehraje písnčka",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Poskytuje synchronizaci textů do písní, pomocí poskytovatelů, jako je LRClib.",
"errors": {
"fetch": "Při hledání textu došlo k chybě. Prosím skuste to znovu později.",
"not-found": "Žáden text nebyl pro túto skladbu nalezen."
},
"menu": {
"default-text-string": {
"label": "Výchozí znak mezi texty",
"tooltip": "Vyberte výchozí znak pro mezeru mezi texty"
},
"line-effect": {
"label": "Efekt řádku"
}
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Ovládejte přehrávání z vašeho Windows hlavního panelu", "description": "Ovládejte přehrávání z vašeho Windows hlavního panelu",
"name": "Hlavní panel Media Control" "name": "Hlavní panel Media Control"

View File

@ -279,6 +279,49 @@
}, },
"name": "Ambiente-Modus" "name": "Ambiente-Modus"
}, },
"api-server": {
"description": "Fügt einen API-Server hinzu, um die Wiedergabe zu steuern",
"dialog": {
"request": {
"buttons": {
"allow": "Erlauben",
"deny": "Ablehnen"
},
"message": "{{ID}} ({{origin}}) den Zugriff zur API erlauben?",
"title": "API-Autorisierungs-Anfrage"
}
},
"menu": {
"auth-strategy": {
"label": "Autorisations-Methode",
"submenu": {
"auth-at-first": {
"label": "Beim ersten Zugriff autorisieren"
},
"none": {
"label": "Keine Autorisierung"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API-Server [Beta]",
"prompt": {
"hostname": {
"label": "Hostname des API-Servers vergeben (z. B. 0.0.0.0):",
"title": "Hostname"
},
"port": {
"label": "Port des API-Server:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Kompressor auf Audio anwenden (senkt die Lautstärke der lautesten Teile des Signals und hebt die Lautstärke der leisesten Teile an)", "description": "Kompressor auf Audio anwenden (senkt die Lautstärke der lautesten Teile des Signals und hebt die Lautstärke der leisesten Teile an)",
"name": "Audio-Komprimierer" "name": "Audio-Komprimierer"
@ -687,7 +730,8 @@
"tooltip": "Nur aktive Zeile weiß darstellen" "tooltip": "Nur aktive Zeile weiß darstellen"
}, },
"offset": { "offset": {
"label": "Versatz" "label": "Versatz",
"tooltip": "Verschiebe die aktuelle Zeile nach rechts"
}, },
"scale": { "scale": {
"label": "Skalieren", "label": "Skalieren",
@ -700,6 +744,10 @@
"label": "Den Songtext perfekt synchronisieren", "label": "Den Songtext perfekt synchronisieren",
"tooltip": "Auf die Millisekunde genau berechnen, wann die nächste Zeile angezeigt werden soll (Kann Einfluss auf die Leistung haben)" "tooltip": "Auf die Millisekunde genau berechnen, wann die nächste Zeile angezeigt werden soll (Kann Einfluss auf die Leistung haben)"
}, },
"show-lyrics-even-if-inexact": {
"label": "Zeige die Liedtexte, auch wenn sie ungenau sind.",
"tooltip": "Die Erweiterung sucht mit anderen Suchparameter nochmals, wenn der Song nicht gefunden wurde.\nEs kann sein, dass das Ergebnis von der zweiten Anfrage nicht genau ist."
},
"show-time-codes": { "show-time-codes": {
"label": "Zeitkodierungen anzeigen", "label": "Zeitkodierungen anzeigen",
"tooltip": "Zeitkodierungen neben Songtext anzeigen" "tooltip": "Zeitkodierungen neben Songtext anzeigen"

View File

@ -279,26 +279,26 @@
}, },
"name": "Ambient Mode" "name": "Ambient Mode"
}, },
"amuse": {
"description": "Adds YouTube Music support for the Amuse now playing widget by 6K Labs",
"name": "Amuse",
"response": {
"query": "Amuse API server is running. GET /query to get song info."
}
},
"api-server": { "api-server": {
"description": "Adds an API server to control the player", "description": "Adds an API server to control the player",
"name": "API Server [Beta]",
"dialog": { "dialog": {
"request": { "request": {
"title": "API authorization request",
"message": "Allow {{ID}} ({{origin}}) to access the API?",
"buttons": { "buttons": {
"allow": "Allow", "allow": "Allow",
"deny": "Deny" "deny": "Deny"
} },
"message": "Allow {{ID}} ({{origin}}) to access the API?",
"title": "API authorization request"
} }
}, },
"menu": { "menu": {
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
},
"auth-strategy": { "auth-strategy": {
"label": "Authorization strategy", "label": "Authorization strategy",
"submenu": { "submenu": {
@ -309,16 +309,23 @@
"label": "No authorization" "label": "No authorization"
} }
} }
} },
},
"prompt": {
"hostname": { "hostname": {
"title": "Hostname", "label": "Hostname"
"label": "Enter the hostname (like 0.0.0.0) for the API server:"
}, },
"port": { "port": {
"title": "Port", "label": "Port"
"label": "Enter the port for the API server:" }
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Enter the hostname (like 0.0.0.0) for the API server:",
"title": "Hostname"
},
"port": {
"label": "Enter the port for the API server:",
"title": "Port"
} }
} }
}, },
@ -714,8 +721,8 @@
"synced-lyrics": { "synced-lyrics": {
"description": "Provides synced lyrics to songs, using providers like LRClib.", "description": "Provides synced lyrics to songs, using providers like LRClib.",
"errors": { "errors": {
"fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.", "fetch": "⚠️\tAn error occurred while fetching the lyrics.\n\tPlease try again later.",
"not-found": "⚠️ - No lyrics found for this song." "not-found": "⚠️ No lyrics found for this song."
}, },
"menu": { "menu": {
"default-text-string": { "default-text-string": {
@ -725,6 +732,10 @@
"line-effect": { "line-effect": {
"label": "Line effect", "label": "Line effect",
"submenu": { "submenu": {
"fancy": {
"label": "Fancy",
"tooltip": "Use large, app-like effects on the current line"
},
"focus": { "focus": {
"label": "Focus", "label": "Focus",
"tooltip": "Make only the current line white" "tooltip": "Make only the current line white"
@ -808,6 +819,18 @@
"visualizer-type": "Visualizer Type" "visualizer-type": "Visualizer Type"
}, },
"name": "Visualizer" "name": "Visualizer"
},
"equalizer": {
"description": "Adds an equalizer to the player",
"name": "Equalizer",
"menu": {
"presets": {
"label": "Presets",
"list": {
"bass-booster": "Bass booster"
}
}
}
} }
} }
} }

View File

@ -293,6 +293,7 @@
}, },
"menu": { "menu": {
"auth-strategy": { "auth-strategy": {
"label": "Estrategia de autorización",
"submenu": { "submenu": {
"auth-at-first": { "auth-at-first": {
"label": "Autorizar la primera solicitud" "label": "Autorizar la primera solicitud"
@ -483,6 +484,18 @@
"button": "Descargar" "button": "Descargar"
} }
}, },
"equalizer": {
"description": "Añade un ecualizador al reproductor",
"menu": {
"presets": {
"label": "Ajustes preestablecidos",
"list": {
"bass-booster": "Amplificador de graves"
}
}
},
"name": "Ecualizador"
},
"exponential-volume": { "exponential-volume": {
"description": "Hace que el control deslizante de volumen sea exponencial para que sea más fácil seleccionar volúmenes más bajos.", "description": "Hace que el control deslizante de volumen sea exponencial para que sea más fácil seleccionar volúmenes más bajos.",
"name": "Volumen exponencial" "name": "Volumen exponencial"

View File

@ -67,6 +67,19 @@
"restart": "Käivita rakendus uuesti" "restart": "Käivita rakendus uuesti"
} }
}, },
"options": {
"label": "Seadistused",
"submenu": {
"advanced-options": {
"label": "Lisaseadistused",
"submenu": {
"auto-reset-app-cache": "Rakenduse käivitamisel lähtesta puhverdatud andmed",
"disable-hardware-acceleration": "Lülita raudvaraline kiirendamine välja",
"edit-config-json": "Muuda config.json faili"
}
}
}
},
"plugins": { "plugins": {
"label": "Lisamoodulid", "label": "Lisamoodulid",
"new": "UUS" "new": "UUS"

View File

@ -197,7 +197,7 @@
"next": "بعدی", "next": "بعدی",
"play-pause": "پخش/توقف", "play-pause": "پخش/توقف",
"previous": "قبلی", "previous": "قبلی",
"quit": "خروجی", "quit": "خروج",
"restart": "راه‌اندازی مجدد برنامه", "restart": "راه‌اندازی مجدد برنامه",
"show": "نمایش پنجره", "show": "نمایش پنجره",
"tooltip": { "tooltip": {
@ -226,7 +226,10 @@
"description": "اعمال یک تم پویا و جلوه‌های بصری بر اساس پالت رنگ آلبوم", "description": "اعمال یک تم پویا و جلوه‌های بصری بر اساس پالت رنگ آلبوم",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "نسبت ترکیب رنگ" "label": "نسبت ترکیب رنگ",
"submenu": {
"percent": "{{ratio}}%"
}
} }
}, },
"name": "تم رنگ آلبوم" "name": "تم رنگ آلبوم"
@ -241,7 +244,10 @@
} }
}, },
"buffer": { "buffer": {
"label": "بافر" "label": "بافر",
"submenu": {
"buffer": "{{buffer}}"
}
}, },
"opacity": { "opacity": {
"label": "شفافیت" "label": "شفافیت"
@ -267,6 +273,49 @@
}, },
"name": "حالت محیطی" "name": "حالت محیطی"
}, },
"api-server": {
"description": "افزودن یک سرور API برای کنترل پخش‌کننده",
"dialog": {
"request": {
"buttons": {
"allow": "اجازه",
"deny": "رد کردن"
},
"message": "اجازه دادن به {{ID}} ({{origin}}) برای دسترسی به API؟",
"title": "درخواست مجوز API"
}
},
"menu": {
"auth-strategy": {
"label": "استراتژی مجوز",
"submenu": {
"auth-at-first": {
"label": "مجوز در اولین درخواست"
},
"none": {
"label": "بدون نیاز به مجوز"
}
}
},
"hostname": {
"label": "نام میزبان"
},
"port": {
"label": "پورت"
}
},
"name": "سرور API [بتا]",
"prompt": {
"hostname": {
"label": "نام میزبان را برای سرور API وارد کنید (مثل 0.0.0.0):",
"title": "نام میزبان"
},
"port": {
"label": "پورت را برای سرور API وارد کنید:",
"title": "پورت"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "اعمال فشرده‌سازی به صدا (کاهش حجم بلندترین بخش‌های سیگنال و افزایش حجم بخش‌های نرم‌تر)", "description": "اعمال فشرده‌سازی به صدا (کاهش حجم بلندترین بخش‌های سیگنال و افزایش حجم بخش‌های نرم‌تر)",
"name": "فشرده‌ساز صدا" "name": "فشرده‌ساز صدا"
@ -389,8 +438,147 @@
"getting-playlist-info": "در حال دریافت اطلاعات فهرست پخش…", "getting-playlist-info": "در حال دریافت اطلاعات فهرست پخش…",
"loading": "در حال بارگذاری…", "loading": "در حال بارگذاری…",
"playlist-has-only-one-song": "فهرست پخش فقط یک آیتم دارد، به طور مستقیم دانلود می‌شود", "playlist-has-only-one-song": "فهرست پخش فقط یک آیتم دارد، به طور مستقیم دانلود می‌شود",
"playlist-id-not-found": "شناسه فهرست پخش یافت نشد" "playlist-id-not-found": "شناسه فهرست پخش یافت نشد",
"playlist-is-empty": "فهرست پخش خالی است",
"playlist-is-mix-or-private": "خطا در دریافت اطلاعات فهرست پخش: اطمینان حاصل کنید که فهرست پخش خصوصی یا \"مختص شما\" نباشد\n\n{{error}}",
"preparing-file": "در حال آماده‌سازی فایل…",
"saving": "در حال ذخیره‌سازی…",
"trying-to-get-playlist-id": "تلاش برای دریافت شناسه فهرست پخش: {{playlistId}}",
"video-id-not-found": "ویدئو یافت نشد",
"writing-id3": "در حال نوشتن تگ‌های ID3…"
} }
},
"description": "دانلود MP3 / صدای منبع به طور مستقیم از رابط",
"menu": {
"choose-download-folder": "انتخاب پوشه دانلود",
"download-finish-settings": {
"label": "دانلود پس از پایان",
"prompt": {
"last-percent": "پس از x درصد",
"last-seconds": "آخرین x ثانیه",
"title": "پیکربندی زمان دانلود"
},
"submenu": {
"advanced": "پیشرفته",
"enabled": "فعال",
"mode": "حالت زمان",
"percent": "درصد",
"seconds": "ثانیه"
}
},
"download-playlist": "دانلود فهرست پخش",
"presets": "پیش‌تنظیم‌ها",
"skip-existing": "رد کردن فایل‌های موجود"
},
"name": "دانلودر",
"renderer": {
"can-not-update-progress": "امکان به‌روزرسانی پیشرفت نیست"
},
"templates": {
"button": "دانلود"
}
},
"exponential-volume": {
"description": "نوار لغزنده حجم را به صورت نمایی می‌سازد تا انتخاب حجم‌های پایین‌تر آسان‌تر شود.",
"name": "حجم نمایی"
},
"in-app-menu": {
"description": "منوها را به صورت جذاب، تاریک یا با رنگ آلبوم نمایش می‌دهد",
"menu": {
"hide-dom-window-controls": "کنترل‌های پنجره DOM را مخفی کن"
},
"name": "منوی داخل برنامه"
},
"lumiastream": {
"description": "افزودن پشتیبانی از Lumia Stream",
"name": "Lumia Stream [بتا]"
},
"lyrics-genius": {
"description": "افزودن پشتیبانی از متن آهنگ برای بیشتر آهنگ‌ها",
"menu": {
"romanized-lyrics": "متن رومی‌شده"
},
"name": "متن آهنگ Genius",
"renderer": {
"fetched-lyrics": "متن آهنگ از Genius بازیابی شد"
}
},
"music-together": {
"description": "اشتراک‌گذاری فهرست پخش با دیگران. وقتی میزبان آهنگی را پخش می‌کند، همه بقیه همان آهنگ را می‌شنوند",
"dialog": {
"enter-host": "شناسه میزبان را وارد کنید"
},
"internal": {
"save": "ذخیره",
"track-source": "منبع آهنگ",
"unknown-user": "کاربر ناشناس"
},
"menu": {
"click-to-copy-id": "کپی کردن شناسه میزبان",
"close": "بستن Music Together",
"connected-users": "کاربران متصل",
"disconnect": "قطع اتصال Music Together",
"empty-user": "هیچ کاربر متصلی وجود ندارد",
"host": "میزبان Music Together",
"join": "پیوستن به Music Together",
"permission": {
"all": "اجازه دادن به مهمانان برای کنترل فهرست پخش و پخش‌کننده",
"host-only": "فقط میزبان می‌تواند فهرست پخش و پخش‌کننده را کنترل کند",
"playlist": "اجازه دادن به مهمانان برای کنترل فهرست پخش"
},
"set-permission": "تغییر مجوز کنترل",
"status": {
"disconnected": "قطع اتصال",
"guest": "متصل به عنوان مهمان",
"host": "متصل به عنوان میزبان"
}
},
"name": "Music Together [بتا]",
"toast": {
"add-song-failed": "افزودن آهنگ با شکست مواجه شد",
"closed": "Music Together بسته شد",
"disconnected": "قطع اتصال Music Together",
"host-failed": "میزبانی Music Together با شکست مواجه شد",
"id-copied": "شناسه میزبان به کلیپ‌بورد کپی شد",
"id-copy-failed": "کپی شناسه میزبان به کلیپ‌بورد با شکست مواجه شد",
"join-failed": "پیوستن به Music Together با شکست مواجه شد",
"joined": "به Music Together پیوست",
"permission-changed": "مجوز Music Together به \"{{permission}}\" تغییر یافت",
"remove-song-failed": "حذف آهنگ با شکست مواجه شد",
"user-connected": "{{name}} به Music Together پیوست",
"user-disconnected": "{{name}} Music Together را ترک کرد"
}
},
"navigation": {
"description": "بعدی/قبلی به طور مستقیم در رابط یکپارچه شده‌اند، مانند مرورگر مورد علاقه شما",
"name": "ناوبری"
},
"no-google-login": {
"description": "حذف دکمه‌های ورود به سیستم Google و لینک‌ها از رابط",
"name": "بدون ورود به Google"
},
"notifications": {
"description": "نمایش اعلان هنگامی که آهنگی شروع به پخش می‌کند (اعلان‌های تعاملی در ویندوز در دسترس هستند)",
"menu": {
"interactive": "اعلان‌های تعاملی",
"interactive-settings": {
"label": "تنظیمات تعاملی",
"submenu": {
"hide-button-text": "مخفی کردن متن دکمه",
"refresh-on-play-pause": "تازه‌سازی در پخش/توقف",
"tray-controls": "باز/بسته شدن با کلیک روی سینی"
}
},
"priority": "اولویت اعلان",
"toast-style": "سبک Toast",
"unpause-notification": "نمایش اعلان هنگام از سرگیری پخش"
},
"name": "اعلان‌ها"
},
"picture-in-picture": {
"description": "اجازه می‌دهد تا برنامه به حالت تصویر در تصویر تغییر کند",
"menu": {
"always-on-top": "همیشه در بالا"
} }
} }
} }

View File

@ -201,6 +201,7 @@
"restart": "I-restart ang App", "restart": "I-restart ang App",
"show": "Ipakita ang window", "show": "Ipakita ang window",
"tooltip": { "tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}" "with-song-info": "YouTube Music: {{artist}} - {{title}}"
} }
} }
@ -212,6 +213,9 @@
}, },
"adblocker": { "adblocker": {
"description": "I-block ang lahat ng ad at tracking", "description": "I-block ang lahat ng ad at tracking",
"menu": {
"blocker": "Blocker"
},
"name": "Pag-block ng Ad" "name": "Pag-block ng Ad"
}, },
"album-actions": { "album-actions": {
@ -222,7 +226,10 @@
"description": "Naglalapat ng dynamic na tema at visual effect batay sa color palette ng album", "description": "Naglalapat ng dynamic na tema at visual effect batay sa color palette ng album",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "Ratio ng paghahalo ng kulay" "label": "Ratio ng paghahalo ng kulay",
"submenu": {
"percent": "{{ratio}}%"
}
} }
}, },
"name": "Tema ng Kulay ng Album" "name": "Tema ng Kulay ng Album"
@ -261,6 +268,7 @@
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"label": "Ayos ng Transisyon",
"submenu": { "submenu": {
"during": "Habang {{interpolationTime}} s" "during": "Habang {{interpolationTime}} s"
} }
@ -268,6 +276,50 @@
"use-fullscreen": { "use-fullscreen": {
"label": "Gumamit ng fullscreen" "label": "Gumamit ng fullscreen"
} }
},
"name": "Ambient Mode"
},
"api-server": {
"description": "Nagdadagdag ng API Server upang kontrolin ang player",
"dialog": {
"request": {
"buttons": {
"allow": "Payagan",
"deny": "Tanggihan"
},
"message": "Payagan ang {{ID}} ({{origin}}) upang ma-access ang API?",
"title": "Awtorisasyon ng API request"
}
},
"menu": {
"auth-strategy": {
"label": "Estratehiya ng awtorisasyon",
"submenu": {
"auth-at-first": {
"label": "Mag-autorisa sa unang request"
},
"none": {
"label": "Walang awtorisasyon"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Itala ang hostname (tulad ng 0.0.0.0) para sa API server:",
"title": "Hostname"
},
"port": {
"label": "Itala ang port para sa API server:",
"title": "Port"
}
} }
}, },
"audio-compressor": { "audio-compressor": {
@ -301,7 +353,8 @@
} }
}, },
"compact-sidebar": { "compact-sidebar": {
"description": "Laging i-set ang sidebar sa compact mode" "description": "Laging i-set ang sidebar sa compact mode",
"name": "Pinaliit na Sidebar"
}, },
"crossfade": { "crossfade": {
"description": "I-crossfade kada kanta", "description": "I-crossfade kada kanta",
@ -355,7 +408,7 @@
"backend": { "backend": {
"dialog": { "dialog": {
"error": { "error": {
"message": "Argh! Paumanhin, nabigo ang pag-download…", "message": "Kainis! Paumanhin, nabigo ang pag-download…",
"title": "Nagkaroon ng error sa pag-download!" "title": "Nagkaroon ng error sa pag-download!"
}, },
"start-download-playlist": { "start-download-playlist": {
@ -412,7 +465,7 @@
"can-not-update-progress": "Hindi ma-update ang progress" "can-not-update-progress": "Hindi ma-update ang progress"
}, },
"templates": { "templates": {
"button": "Mag download" "button": "Mag-download"
} }
}, },
"exponential-volume": { "exponential-volume": {
@ -425,7 +478,8 @@
} }
}, },
"lumiastream": { "lumiastream": {
"description": "Nabibigay suporta sa Lumia Stream" "description": "Nabibigay suporta sa Lumia Stream",
"name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Nagdaragdag ng suporta sa lyrics para sa karamihan ng kanta", "description": "Nagdaragdag ng suporta sa lyrics para sa karamihan ng kanta",
@ -582,7 +636,8 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "Ilagay ang ListenBrainz user token:" "label": "Ilagay ang ListenBrainz user token:",
"title": "Token ng ListenBrainz"
} }
} }
} }
@ -696,7 +751,10 @@
} }
}, },
"visualizer": { "visualizer": {
"description": "Idaragdag ng visualizer sa player" "description": "Idaragdag ng visualizer sa player",
"menu": {
"visualizer-type": "Uri ng Visualizer"
}
} }
} }
} }

View File

@ -279,6 +279,49 @@
}, },
"name": "Mode ambiant" "name": "Mode ambiant"
}, },
"api-server": {
"description": "Ajouter un serveur API pour contrôler le lecteur",
"dialog": {
"request": {
"buttons": {
"allow": "Autoriser",
"deny": "Interdire"
},
"message": "Autoriser {{ID}} ({{origin}}) à accéder à l'API?",
"title": "Requête d'autorisation d'API"
}
},
"menu": {
"auth-strategy": {
"label": "Plan d'autorisation",
"submenu": {
"auth-at-first": {
"label": "Autoriser à la première requête"
},
"none": {
"label": "Pas d'autorisation"
}
}
},
"hostname": {
"label": "Nom de l'hôte"
},
"port": {
"label": "Port"
}
},
"name": "Serveur API [Beta]",
"prompt": {
"hostname": {
"label": "Entrer le nom de l'hôte (par exemple 0.0.0.0) pour le serveur API:",
"title": "Nom d'hôte"
},
"port": {
"label": "Entrer le port du serveur de l'API :",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Appliquer une compression à l'audio (diminue le volume des parties les plus fortes du signal et augmente le volume des parties les plus faibles)", "description": "Appliquer une compression à l'audio (diminue le volume des parties les plus fortes du signal et augmente le volume des parties les plus faibles)",
"name": "Compresseur audio" "name": "Compresseur audio"
@ -441,6 +484,18 @@
"button": "Télécharger" "button": "Télécharger"
} }
}, },
"equalizer": {
"description": "Ajoute un égaliseur au lecteur",
"menu": {
"presets": {
"label": "Préréglages",
"list": {
"bass-booster": "Amplificateur de basses"
}
}
},
"name": "Égaliseur"
},
"exponential-volume": { "exponential-volume": {
"description": "Rend le curseur de volume exponentiel afin qu'il soit plus facile de sélectionner des volumes plus faibles.", "description": "Rend le curseur de volume exponentiel afin qu'il soit plus facile de sélectionner des volumes plus faibles.",
"name": "Volume exponentiel" "name": "Volume exponentiel"

View File

@ -6,7 +6,7 @@
"executed-at-ms": "התוסף {{pluginName}}:{{contextName}} בוצע ב {{ms}}ms", "executed-at-ms": "התוסף {{pluginName}}:{{contextName}} בוצע ב {{ms}}ms",
"initialize-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה", "initialize-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה",
"load-all": "טוען את כל התוספים", "load-all": "טוען את כל התוספים",
"load-failed": "לא ניתן לטעון את התוסף {{pluginName}}", "load-failed": "שגיאה בטעינת התוסף \"{{pluginName}}\"",
"loaded": "התוסף \"{{pluginName}}\" נטען", "loaded": "התוסף \"{{pluginName}}\" נטען",
"unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה", "unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה",
"unloaded": "תוסף {{pluginName}} הורד" "unloaded": "תוסף {{pluginName}} הורד"
@ -52,6 +52,38 @@
"buttons": { "buttons": {
"later": "אחר כך", "later": "אחר כך",
"restart-now": "מתחיל את התוכנה מחדש עכשיו" "restart-now": "מתחיל את התוכנה מחדש עכשיו"
},
"title": "נדרשת הפעלה מחדש"
},
"unresponsive": {
"buttons": {
"quit": "יציאה",
"relaunch": "הפעל מחדש",
"wait": "המתן"
},
"detail": "אנו מצטערים על אי הנוחות! אנא בחר מה לעשות:",
"message": "האפליקציה אינה מגיבה",
"title": "החלון אינו מגיב"
},
"update-available": {
"buttons": {
"disable": "בטל עדכונים",
"download": "הורדה",
"ok": "אוקי"
},
"message": "גירסא חדשה זמינה כעת",
"title": "קיים עדכון חדש"
}
},
"menu": {
"about": "אודות",
"navigation": {
"label": "ניווט",
"submenu": {
"copy-current-url": "העתק את כתובת ה-URL",
"go-back": "חזור אחורה",
"go-forward": "לך קדימה",
"quit": "יציאה"
} }
} }
} }

251
src/i18n/resources/hi.json Normal file
View File

@ -0,0 +1,251 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "{{pluginName}}::{{contextName}} प्लगइन निष्पादित करने में विफल",
"executed-at-ms": "{{pluginName}}::{{contextName}} प्लगिन {{ms}} में निष्पाशित हुआ",
"initialize-failed": "\"{{pluginName}}\" प्लगिन इनिशियलाइज़ होने में असफल रहा",
"load-all": "सारे प्लगिन लोड हो चुके हैं",
"load-failed": "\"{{pluginName}}\" प्लगिन लोड होने में असफल रहा",
"loaded": "प्लगिन \"{{pluginName}}\" लोड हो चुका है",
"unload-failed": "\"{{pluginName}}\" अनलोड होने में असफल रहा",
"unloaded": "प्लगिन \"{{pluginName}}\" अनलोड हो गया है"
}
}
},
"language": {
"code": "hi",
"local-name": "हिंदी",
"name": "Hindi"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "लोडिंग समाप्त हुई । डेवटूल्स खोले गए हैं"
},
"i18n": {
"loaded": "i18n लोड हो गया है"
},
"second-instance": {
"receive-command": "प्रोटोकॉल पर आदेश प्राप्त हुआ \"{{command}}\""
},
"theme": {
"css-file-not-found": "सीएसएस फाइल \"{{cssFile}}\" मौजूद नही है, अनदेखा किया जा रहा है"
},
"unresponsive": {
"details": "अनरेस्पॉन्सिव एरर\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "एप कैश साफ़ किया जा रहा है"
},
"window": {
"tried-to-render-offscreen": "विंडो ने ऑफस्क्रीन रेंडर करने का प्रयास किया, विंडो साइज={{windowSize}}, डिस्प्ले साइज={{displaySize}}, पोजिशन={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "मेनू छिपा हुआ है, देखने के लिए 'Alt' का इस्तेमाल करें (या 'Escape' अगर आप इन-एप मेनू का उपयोग कर रहे हैं)",
"message": "मेनू छिपाएँ सक्षम है",
"title": "मेनू छिपाएँ सक्षम"
},
"need-to-restart": {
"buttons": {
"later": "बाद में",
"restart-now": "पुनः आरंभ करें"
},
"detail": "\"{{pluginName}}\" प्रभाव लेने के लिए प्लगिन को पुनः शुरू करें",
"message": "\"{{pluginName}}\" पुनः आरंभ करने की आवश्यकता है",
"title": "पुनः आरंभ करने की आवश्यकता है"
},
"unresponsive": {
"buttons": {
"quit": "बंद करें",
"relaunch": "पुनः लॉन्च करें",
"wait": "रुकें"
},
"detail": "असुविधाए के लिए खेद हैं! कृपया चुनें कि क्या करना है:",
"message": "एप्लीकेशन अनुत्तरदायी है",
"title": "विंडो अनुत्तरदायी है"
},
"update-available": {
"buttons": {
"disable": "अपडेट्स बंद करें",
"download": "डाउनलोड",
"ok": "ठीक है"
},
"detail": "एक नया वर्जन उपलब्ध है, {{downloadLink}} से डाउनलोड किया जा सकता है",
"message": "एक नया वर्जन उपलब्ध है",
"title": "अपडेट उपलब्ध है"
}
},
"menu": {
"about": "के बारे में",
"navigation": {
"label": "मार्गदर्शन",
"submenu": {
"copy-current-url": "मौजूदा यूआरएल कापी करें",
"go-back": "पीछे जाएं",
"go-forward": "आगे जाएं",
"quit": "निकास",
"restart": "एप को पुनः शुरू करें"
}
},
"options": {
"label": "विकल्प",
"submenu": {
"advanced-options": {
"label": "उन्नत विकल्प",
"submenu": {
"auto-reset-app-cache": "एप शुरू होते समय कैश रीसेट करें",
"disable-hardware-acceleration": "हार्डवेयर एक्सीलरेशन बंद करें",
"edit-config-json": "config.json को एडिट करें",
"override-user-agent": "यूजर-एजेंट को रद्द करें",
"restart-on-config-changes": "कनफिग बदलने पे पुनः शुरू करें",
"set-proxy": {
"label": "प्रॉक्सी तय करें",
"prompt": {
"label": "प्प्रॉक्सी पता डालें: (बंद करने के लिए खाली छोड़ें)",
"placeholder": "उदाहरण: SOCKS5://127.0.0.1:9999",
"title": "प्रॉक्सी तय करें"
}
},
"toggle-dev-tools": "डेवटूल्स को टॉगल करें"
}
},
"always-on-top": "हमेशा ऊपर",
"auto-update": "ऑटो अपडेट",
"hide-menu": {
"dialog": {
"message": "अगले लॉन्च पे मेनू छुपा दिया जायेगा, देखने के लिए [Alt] का प्रयोग करें (या बैकटिक [`] अगर आप इन एप मेनू का प्रयोग कर रहे हैं)",
"title": "मेनू छुपाना सक्रिय है"
},
"label": "मेनू छुपाएं"
},
"language": {
"dialog": {
"message": "पुनः शुरू करने के बाद भाषा बदल दी जाएगी",
"title": "भाषा बदल दी गई है"
},
"label": "भाषा",
"submenu": {
"to-help-translate": "अनुवाद करने में सहायता करना चाहते हैं? यहां दबाएं"
}
},
"resume-on-start": "एप शुरू होने पर आखरी गाना फिर शुरू करें",
"single-instance-lock": "सिंगल इंस्टेंस लॉक",
"start-at-login": "शुरू होने पे लॉगिन करें",
"starting-page": {
"label": "स्टार्टिंग पेज",
"unset": "अनसेट"
},
"tray": {
"label": "ट्रे",
"submenu": {
"disabled": "बंद किया गया है",
"enabled-and-hide-app": "सक्रिय है और एप छुपाएं",
"enabled-and-show-app": "सक्रिय है और एप दिखाएं",
"play-pause-on-click": "क्लिक पर प्ले/पोज"
}
},
"visual-tweaks": {
"label": "दृश्य परिवर्तन",
"submenu": {
"like-buttons": {
"default": "डिफॉल्ट",
"force-show": "बल पूर्वक दिखाएं",
"hide": "छुपाएं",
"label": "लाइक बटंस"
},
"remove-upgrade-button": "अपग्रेड बटन हटाएं",
"theme": {
"dialog": {
"button": {
"cancel": "रद्द करें",
"remove": "हटाएं"
},
"remove-theme": "क्या आप निश्चित है आपको कस्टम थीम हटानी है?",
"remove-theme-message": "यह कस्टम थीम को हटा देगा"
},
"label": "थीम",
"submenu": {
"import-css-file": "कस्टम सीएसएस फाइल को आयात करें",
"no-theme": "कोई थीम नही"
}
}
}
}
}
},
"plugins": {
"enabled": "सक्रिय",
"label": "प्लगिंस",
"new": "नया"
},
"view": {
"label": "देखें",
"submenu": {
"force-reload": "बल पूर्वक रिलोड करें",
"reload": "रिलोड करें",
"reset-zoom": "वास्तविक आकार",
"toggle-fullscreen": "टागल फुल स्क्रीन",
"zoom-in": "ज़ूम इन",
"zoom-out": "ज़ूम आउट"
}
}
},
"tray": {
"next": "अगला",
"play-pause": "चलाएँ/रोकें",
"previous": "पिछला",
"quit": "निकास",
"restart": "ऐप पुनः प्रारंभ करें",
"show": "ऐप दिखाए",
"tooltip": {
"default": "यूट्यूब म्यूजिक",
"with-song-info": "यूट्यूब म्यूजिक: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "यदि कोई विज्ञापन चलता है तो यह ऑडियो को म्यूट कर देता है और प्लेबैक गति 16x पर सेट कर देता है",
"name": "विज्ञापन की गति बढ़ाना"
},
"adblocker": {
"description": "डिफ़ॉल्ट रूप से सभी विज्ञापनों और ट्रैकिंग को ब्लॉक करें",
"menu": {
"blocker": "ब्लॉकर"
},
"name": "विज्ञापन अवरोधक"
},
"album-actions": {
"description": "प्लेलिस्ट या एल्बम के सभी गानों पर लागू करने के लिए \"अंडिसलाइक,\" \"डिसलाइक,\" \"लाइक,\" और \"अनलाइक\" बटन जोड़ता है",
"name": "एल्बम एक्शन"
},
"album-color-theme": {
"description": "एल्बम रंग पैलेट के आधार पर एक गतिशील थीम और दृश्य प्रभाव लागू करता है",
"menu": {
"color-mix-ratio": {
"submenu": {
"percent": "{{ratio}}%"
}
}
}
},
"video-toggle": {
"menu": {
"align": {
"submenu": {
"left": "बाएं",
"middle": "मध्य",
"right": "दाहिने"
}
},
"force-hide": "वीडियो टैब को बलपूर्वक हटाएं",
"mode": {
"label": "तरीका"
}
}
}
}
}

View File

@ -202,7 +202,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 Musik: {{artist}} - {{title}}"
} }
} }
}, },
@ -279,6 +279,49 @@
}, },
"name": "Mode ambient" "name": "Mode ambient"
}, },
"api-server": {
"description": "Menambahkan server API untuk mengontrol pemutar",
"dialog": {
"request": {
"buttons": {
"allow": "Izinkan",
"deny": "Menolak"
},
"message": "Izinkan {{ID}} ({{origin}}) untuk mengakses API?",
"title": "Permintaan otorisasi API"
}
},
"menu": {
"auth-strategy": {
"label": "Strategi otorisasi",
"submenu": {
"auth-at-first": {
"label": "Otorisasi pada permintaan pertama"
},
"none": {
"label": "Tidak ada otorisasi"
}
}
},
"hostname": {
"label": "Nama host"
},
"port": {
"label": "Port"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Masukkan nama host (seperti 0.0.0.0) untuk server API:",
"title": "Nama host"
},
"port": {
"label": "Masukkan port untuk server API:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Menerapkan kompresi pada audio (mengurangi volume pada bagian paling keras dari sinyal dan meningkatkan volume pada bagian paling lembut)", "description": "Menerapkan kompresi pada audio (mengurangi volume pada bagian paling keras dari sinyal dan meningkatkan volume pada bagian paling lembut)",
"name": "Kompresi suara" "name": "Kompresi suara"
@ -441,6 +484,18 @@
"button": "Unduh" "button": "Unduh"
} }
}, },
"equalizer": {
"description": "Menambahkan equalizer ke pemutar",
"menu": {
"presets": {
"label": "Prasetel",
"list": {
"bass-booster": "Penguat Bass"
}
}
},
"name": "Equalizer"
},
"exponential-volume": { "exponential-volume": {
"description": "Buat penggeser volume menjadi eksponen sehingga memudahkan memilih volume yang lebih rendah.", "description": "Buat penggeser volume menjadi eksponen sehingga memudahkan memilih volume yang lebih rendah.",
"name": "Volume Eksponen" "name": "Volume Eksponen"

View File

@ -279,6 +279,49 @@
}, },
"name": "Umhverfishamur" "name": "Umhverfishamur"
}, },
"api-server": {
"description": "Bætir API netþjóni til að stjórna spilaranum",
"dialog": {
"request": {
"buttons": {
"allow": "Leyfa",
"deny": "Óleyfa"
},
"message": "Leyfa {{ID}} ({{origin}}) að aðganga API-ið?",
"title": "API heimildarbeiðni"
}
},
"menu": {
"auth-strategy": {
"label": "Heimildarstefna",
"submenu": {
"auth-at-first": {
"label": "Heimila á fyrst beiðni"
},
"none": {
"label": "Nei heimild"
}
}
},
"hostname": {
"label": "Hýsitölvunafn"
},
"port": {
"label": "Tengi"
}
},
"name": "API-Netþjónn [Beta]",
"prompt": {
"hostname": {
"label": "Sláðu inn hýsitölvunafnið (eins og 0.0.0.0) fyrir API-netþjónninn:",
"title": "Hýsitölvunafn"
},
"port": {
"label": "Sláðu inn tengið fyrir API-netþjónninn:",
"title": "Tengi"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Notaðu þjöppun á hljóð (lækkar hljóðstyrk háværustu hluta merkis og hækkar hljóðstyrk í mýkstu hlutunum)", "description": "Notaðu þjöppun á hljóð (lækkar hljóðstyrk háværustu hluta merkis og hækkar hljóðstyrk í mýkstu hlutunum)",
"name": "Hljóðþjöppu" "name": "Hljóðþjöppu"
@ -670,7 +713,56 @@
}, },
"synced-lyrics": { "synced-lyrics": {
"description": "Veitir samstillta texta við lög, með því að nota veitur eins og LRClib.", "description": "Veitir samstillta texta við lög, með því að nota veitur eins og LRClib.",
"name": "Samstilltur texti" "errors": {
"fetch": "⚠️ - Villa kom upp við að sækja textann. Vinsamlegast reyndu aftur síðar.",
"not-found": "⚠️ - Enginn texti fannst við þetta lag."
},
"menu": {
"default-text-string": {
"label": "Sjálfgefið tákn á milli texta",
"tooltip": "Veldu sjálfgefna tákn til að nota fyrir bilið á milli texta"
},
"line-effect": {
"label": "Línuafleiðing",
"submenu": {
"focus": {
"label": "Brennidepill",
"tooltip": "Gerðu aðeins núverandi línu hvíta"
},
"offset": {
"label": "Fararbyrjun",
"tooltip": "Fararbyrjun á hægri af núverandi línan"
},
"scale": {
"label": "Skali",
"tooltip": "Skala núverandi línu"
}
},
"tooltip": "Veldu áhrif til að nota á núverandi línu"
},
"precise-timing": {
"label": "Gera textana fullkomlega samstillta",
"tooltip": "Reikna upp á millisekúndu birtingu næstu línu (getur haft lítil áhrif á frammistöðu)"
},
"show-lyrics-even-if-inexact": {
"label": "Sýna texta, jafnvel þótt hann sé ónákvæmur",
"tooltip": "Ef lagið finnst ekki reynir tengiforritið aftur með annarri leitarfyrirspurn.\nNiðurstaðan úr annarri tilraun er kannski ekki nákvæm."
},
"show-time-codes": {
"label": "Sýna tímikóðar",
"tooltip": "Sýna tímakóðana við hliðina á textanum"
}
},
"name": "Samstilltur texti",
"refetch-btn": {
"fetching": "Er að sækja",
"normal": "Endursækja texta"
},
"warnings": {
"duration-mismatch": "⚠️ - Textarnir gætu verið ekki samstilltir vegna tímalengdar.",
"inexact": "⚠️ - Textinn við þetta lag er kannski ekki nákvæmur",
"instrumental": "⚠️ - Þetta er hljóðfærilegt lag"
}
}, },
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Stjórnaðu spilun frá Windows verkefnastikunni þinni", "description": "Stjórnaðu spilun frá Windows verkefnastikunni þinni",

View File

@ -279,6 +279,56 @@
}, },
"name": "Modalità Ambiente" "name": "Modalità Ambiente"
}, },
"amuse": {
"description": "Aggiunge il supporto a YouTube Music per il widget Amuse Now Playing di 6K Labs",
"name": "Amuse",
"response": {
"query": "Il server API di Amuse è in funzione. GET /query per ottenere informazioni sui brani."
}
},
"api-server": {
"description": "Aggiunge un server API per controllare il player",
"dialog": {
"request": {
"buttons": {
"allow": "Permetti",
"deny": "Nega"
},
"message": "Consentire a {{ID}} ({{origin}}) di accedere all'API?",
"title": "Autorizzazione API richiesta"
}
},
"menu": {
"auth-strategy": {
"label": "Metodo di autorizzazione",
"submenu": {
"auth-at-first": {
"label": "Autorizza alla prima richiesta"
},
"none": {
"label": "Nessuna autorizzazione"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Porta"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Inserisci il nome host (ad esempio 0.0.0.0) per il server API:",
"title": "Hostname"
},
"port": {
"label": "Inserisci la porta per il server API:",
"title": "Porta"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Attiva la compressione audio (abbassa il volume delle parti più alte e alza quello delle parti più basse del segnale)", "description": "Attiva la compressione audio (abbassa il volume delle parti più alte e alza quello delle parti più basse del segnale)",
"name": "Compressore audio" "name": "Compressore audio"
@ -441,6 +491,18 @@
"button": "Scarica" "button": "Scarica"
} }
}, },
"equalizer": {
"description": "Aggiunge un equalizzatore al player",
"menu": {
"presets": {
"label": "Presets",
"list": {
"bass-booster": "Booster dei bassi"
}
}
},
"name": "Equalizzatore"
},
"exponential-volume": { "exponential-volume": {
"description": "Rende esponenziale il cursore del volume, in modo da facilitare la selezione di volumi più bassi.", "description": "Rende esponenziale il cursore del volume, in modo da facilitare la selezione di volumi più bassi.",
"name": "Volume esponenziale" "name": "Volume esponenziale"

View File

@ -279,6 +279,49 @@
}, },
"name": "アンビエント モード" "name": "アンビエント モード"
}, },
"api-server": {
"description": "プレイヤーを制御するAPIサーバーを追加",
"dialog": {
"request": {
"buttons": {
"allow": "許可",
"deny": "拒否"
},
"message": "{{ID}}が{{origin}}にアクセスすることを許可しますか?",
"title": "API承認リクエスト"
}
},
"menu": {
"auth-strategy": {
"label": "許可方法",
"submenu": {
"auth-at-first": {
"label": "初回リクエスト時に承認"
},
"none": {
"label": "不許可"
}
}
},
"hostname": {
"label": "ホスト名"
},
"port": {
"label": "ポート"
}
},
"name": "APIサーバー(Beta)",
"prompt": {
"hostname": {
"label": "APIサーバーのポート名(0.0.0.0など)を入力:",
"title": "ホスト名"
},
"port": {
"label": "APIサーバーのポートを入力:",
"title": "ポート"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "オーディオにコンプレッサーを適用します(信号での一番大きい部分の音量を下げ、小さい部分の音量を上げる)", "description": "オーディオにコンプレッサーを適用します(信号での一番大きい部分の音量を下げ、小さい部分の音量を上げる)",
"name": "オーディオコンプレッサー" "name": "オーディオコンプレッサー"
@ -441,6 +484,18 @@
"button": "ダウンロード" "button": "ダウンロード"
} }
}, },
"equalizer": {
"description": "プレイヤーにイコライザーを追加",
"menu": {
"presets": {
"label": "プリセット",
"list": {
"bass-booster": "ベースブースター"
}
}
},
"name": "イコライザー"
},
"exponential-volume": { "exponential-volume": {
"description": "音量スライダを指数関数的にさせ、低い音量に設定しやすくなります。", "description": "音量スライダを指数関数的にさせ、低い音量に設定しやすくなります。",
"name": "指数音量" "name": "指数音量"

View File

@ -279,6 +279,13 @@
}, },
"name": "앰비언트 모드" "name": "앰비언트 모드"
}, },
"amuse": {
"description": "6K Labs Amuse의 'now playing' 위젯에 YouTube Music 지원 추가",
"name": "Amuse",
"response": {
"query": "Amuse API 서버가 실행 중입니다. GET /query로 노래 정보를 가져오세요."
}
},
"api-server": { "api-server": {
"description": "플레이어를 제어하기 위한 API 서버를 추가합니다", "description": "플레이어를 제어하기 위한 API 서버를 추가합니다",
"dialog": { "dialog": {
@ -484,6 +491,18 @@
"button": "다운로드" "button": "다운로드"
} }
}, },
"equalizer": {
"description": "플레이어에 이퀄라이저를 추가합니다",
"menu": {
"presets": {
"label": "프리셋",
"list": {
"bass-booster": "베이스 부스터"
}
}
},
"name": "이퀄라이저"
},
"exponential-volume": { "exponential-volume": {
"description": "음량 슬라이더를 지수적으로 만들어 더 낮은 음량을 쉽게 선택할 수 있도록 합니다.", "description": "음량 슬라이더를 지수적으로 만들어 더 낮은 음량을 쉽게 선택할 수 있도록 합니다.",
"name": "지수 음량" "name": "지수 음량"
@ -714,8 +733,8 @@
"synced-lyrics": { "synced-lyrics": {
"description": "LRClib등의 가사 제공자에서 싱크 가사를 불러옵니다.", "description": "LRClib등의 가사 제공자에서 싱크 가사를 불러옵니다.",
"errors": { "errors": {
"fetch": "⚠️ - 가사를 불러오는 동안 오류가 발생했습니다. 나중에 다시 시도해 주세요.", "fetch": "⚠️\t가사를 불러오는 동안 오류가 발생했습니다.\n\t나중에 다시 시도해 주세요.",
"not-found": "⚠️ - 이 노래의 가사를 찾을 수 없습니다." "not-found": "⚠️ 이 노래의 가사를 찾을 수 없습니다."
}, },
"menu": { "menu": {
"default-text-string": { "default-text-string": {
@ -725,6 +744,10 @@
"line-effect": { "line-effect": {
"label": "줄 표시 효과", "label": "줄 표시 효과",
"submenu": { "submenu": {
"fancy": {
"label": "예쁘게",
"tooltip": "유튜브 뮤직 앱처럼 커다란 효과를 현재 라인에 사용합니다"
},
"focus": { "focus": {
"label": "포커스", "label": "포커스",
"tooltip": "현재 줄만 하얀색으로 표시" "tooltip": "현재 줄만 하얀색으로 표시"

View File

@ -279,6 +279,52 @@
}, },
"name": "Tryb otoczenia" "name": "Tryb otoczenia"
}, },
"amuse": {
"name": "Amuse"
},
"api-server": {
"description": "Pozwala na kontrolowanie YouTube Music poprzez podłączenie specjalnego serwera API",
"dialog": {
"request": {
"buttons": {
"allow": "Zezwól",
"deny": "Odmów"
},
"message": "Zezwolić {{ID}} (pochodzenie: {{origin}}) na dostęp do API?",
"title": "Prośba o autoryzację API"
}
},
"menu": {
"auth-strategy": {
"label": "Strategia autoryzacji",
"submenu": {
"auth-at-first": {
"label": "Autoryzuj przy pierwszej prośbie"
},
"none": {
"label": "Nie autoryzuj"
}
}
},
"hostname": {
"label": "Nazwa hosta (IP)"
},
"port": {
"label": "Port"
}
},
"name": "YouTube Music API",
"prompt": {
"hostname": {
"label": "Wpisz nazwę hosta (IP, np. 0.0.0.0), który będzie użyty do serwera API:",
"title": "Nazwa hosta"
},
"port": {
"label": "Wpisz port, z którego będzie korzystać serwer API:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Zastosuj kompresję do dźwięku (obniża głośność najgłośniejszych części sygnału i zwiększa głośność najcichszych części)", "description": "Zastosuj kompresję do dźwięku (obniża głośność najgłośniejszych części sygnału i zwiększa głośność najcichszych części)",
"name": "Kompresor dźwięku" "name": "Kompresor dźwięku"
@ -441,6 +487,18 @@
"button": "Pobierz" "button": "Pobierz"
} }
}, },
"equalizer": {
"description": "Dodaje equalizer do odtwarzacza",
"menu": {
"presets": {
"label": "Presety",
"list": {
"bass-booster": "Wzmacniacz basu"
}
}
},
"name": "Equalizer"
},
"exponential-volume": { "exponential-volume": {
"description": "Sprawia, że suwak głośności jest proporcjonalna, dzięki czemu łatwiej jest wybrać niższą głośność.", "description": "Sprawia, że suwak głośności jest proporcjonalna, dzięki czemu łatwiej jest wybrać niższą głośność.",
"name": "Proporcjonalna głośność" "name": "Proporcjonalna głośność"
@ -682,6 +740,9 @@
"line-effect": { "line-effect": {
"label": "Efekty linijki", "label": "Efekty linijki",
"submenu": { "submenu": {
"fancy": {
"label": "Facy"
},
"focus": { "focus": {
"label": "Fokus", "label": "Fokus",
"tooltip": "Spraw, aby tylko obecna linijka była biała" "tooltip": "Spraw, aby tylko obecna linijka była biała"

View File

@ -279,6 +279,56 @@
}, },
"name": "Modo ambiente" "name": "Modo ambiente"
}, },
"amuse": {
"description": "Adiciona suporte ao YouTube Music ao widget 'Reproduzindo agora' do Amuse da 6K Labs",
"name": "Amuse",
"response": {
"query": "Servidor API do Amuse em execução. GET /query para obter informações da música."
}
},
"api-server": {
"description": "Adiciona um servidor API para controlar o player",
"dialog": {
"request": {
"buttons": {
"allow": "Permitir",
"deny": "Negar"
},
"message": "Permitir que {{ID}} {{origin}} acesse o API?",
"title": "Pedido de autorização API"
}
},
"menu": {
"auth-strategy": {
"label": "Estratégia de autorização",
"submenu": {
"auth-at-first": {
"label": "Autorizar na primeira solicitação"
},
"none": {
"label": "Não autorizar"
}
}
},
"hostname": {
"label": "Nome do anfitrião"
},
"port": {
"label": "Porta"
}
},
"name": "Servidor API [Beta]",
"prompt": {
"hostname": {
"label": "Entre o nome do host (como 0.0.0.0) para o servidor API:",
"title": "Nome do anfitrião"
},
"port": {
"label": "Entre a porta do servidor API:",
"title": "Porta"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Aplicar compressão ao áudio (reduz o volume das partes mais altas e aumenta o volume das partes mais baixas)", "description": "Aplicar compressão ao áudio (reduz o volume das partes mais altas e aumenta o volume das partes mais baixas)",
"name": "Compressor de áudio" "name": "Compressor de áudio"
@ -441,6 +491,18 @@
"button": "Baixar" "button": "Baixar"
} }
}, },
"equalizer": {
"description": "Adiciona um equalizador ao player",
"menu": {
"presets": {
"label": "Predefinições",
"list": {
"bass-booster": "Reforço de graves"
}
}
},
"name": "Equalizador"
},
"exponential-volume": { "exponential-volume": {
"description": "Torna o controle deslizante de volume exponencial para que seja mais fácil selecionar volumes mais baixos.", "description": "Torna o controle deslizante de volume exponencial para que seja mais fácil selecionar volumes mais baixos.",
"name": "Volume Exponencial" "name": "Volume Exponencial"
@ -671,8 +733,8 @@
"synced-lyrics": { "synced-lyrics": {
"description": "Fornece letras sincronizadas para músicas, usando provedores como LRClib.", "description": "Fornece letras sincronizadas para músicas, usando provedores como LRClib.",
"errors": { "errors": {
"fetch": "⚠️ - Ocorreu um erro ao buscar a letra. Tente novamente mais tarde.", "fetch": "⚠️\tOcorreu um erro ao buscar a letra.\n\tTente novamente mais tarde.",
"not-found": "⚠️ - Nenhuma letra encontrada para esta música." "not-found": "⚠️ Nenhuma letra encontrada para esta música."
}, },
"menu": { "menu": {
"default-text-string": { "default-text-string": {
@ -682,6 +744,10 @@
"line-effect": { "line-effect": {
"label": "Efeito de linha", "label": "Efeito de linha",
"submenu": { "submenu": {
"fancy": {
"label": "Fancy",
"tooltip": "Use efeitos grandes, semelhantes a aplicativos, na linha atual"
},
"focus": { "focus": {
"label": "Foco", "label": "Foco",
"tooltip": "Deixe apenas a linha atual branca" "tooltip": "Deixe apenas a linha atual branca"

View File

@ -279,6 +279,37 @@
}, },
"name": "Modo Ambiente" "name": "Modo Ambiente"
}, },
"api-server": {
"dialog": {
"request": {
"buttons": {
"allow": "Permitir",
"deny": "Negar"
}
}
},
"menu": {
"auth-strategy": {
"label": "Estratégia de Autorização",
"submenu": {
"auth-at-first": {
"label": "Autorizar ao primeiro pedido"
},
"none": {
"label": "Sem autorização"
}
}
},
"port": {
"label": "Porta"
}
},
"prompt": {
"port": {
"title": "Porta"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Aplicar compressão ao áudio (diminui o volume das partes mais altas do sinal e aumenta o volume das partes mais suaves)", "description": "Aplicar compressão ao áudio (diminui o volume das partes mais altas do sinal e aumenta o volume das partes mais suaves)",
"name": "Compressor de áudio" "name": "Compressor de áudio"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Reclamele au sunetul dezactivat si viteza de redare este x16",
"name": "Accelerare reclame"
},
"adblocker": { "adblocker": {
"description": "Blocheaza toate reclamele si trackers", "description": "Blocheaza toate reclamele si trackers",
"menu": { "menu": {
@ -275,6 +279,49 @@
}, },
"name": "Mod ambiental" "name": "Mod ambiental"
}, },
"api-server": {
"description": "Adauga un server API pentru a controla player-ul",
"dialog": {
"request": {
"buttons": {
"allow": "Permite",
"deny": "Respinge"
},
"message": "Permite {{ID}} {{origin}} sa acceseze API-ul?",
"title": "Cerere autorizare API"
}
},
"menu": {
"auth-strategy": {
"label": "Strategie autorizare",
"submenu": {
"auth-at-first": {
"label": "Autorizare la prima cerere"
},
"none": {
"label": "Fara autorizare"
}
}
},
"hostname": {
"label": "Nume host"
},
"port": {
"label": "Port"
}
},
"name": "Server API [Beta]",
"prompt": {
"hostname": {
"label": "Introduceti nume host (0.0.0.0 de ex.) pentru server-ul API:",
"title": "Nume host"
},
"port": {
"label": "Introduceti port-ul pentru server-ul API:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Aplica compresie pe audio (scade volumul partilor cele mai sonore si creste volumul partilor mai putin sonore)", "description": "Aplica compresie pe audio (scade volumul partilor cele mai sonore si creste volumul partilor mai putin sonore)",
"name": "Compresor audio" "name": "Compresor audio"
@ -410,6 +457,21 @@
"description": "Descarca MP3 / sursa audio direct din interfata", "description": "Descarca MP3 / sursa audio direct din interfata",
"menu": { "menu": {
"choose-download-folder": "Alege folderul de descarcari", "choose-download-folder": "Alege folderul de descarcari",
"download-finish-settings": {
"label": "Descarcare la finalizare",
"prompt": {
"last-percent": "Dupa x la suta",
"last-seconds": "Ultimele x secunde",
"title": "Configureaza cand sa descarce"
},
"submenu": {
"advanced": "Avansat",
"enabled": "Activat",
"mode": "Mod timp",
"percent": "Procentaj",
"seconds": "Secunde"
}
},
"download-playlist": "Descarca playlist-ul", "download-playlist": "Descarca playlist-ul",
"presets": "Setari implicite", "presets": "Setari implicite",
"skip-existing": "Treci peste fisierele existente" "skip-existing": "Treci peste fisierele existente"
@ -649,6 +711,59 @@
"description": "Treci automat peste partile non-muzicale precum intro/outro sau parti din video-ul catecului, cand nu se aude cantecul", "description": "Treci automat peste partile non-muzicale precum intro/outro sau parti din video-ul catecului, cand nu se aude cantecul",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Furnizeaza versuri sincronizate melodiilor, folosind furnizori precum LRClib.",
"errors": {
"fetch": "⚠️ - A aparut o eroare in timpul incarcarii versurilor. Te rog incearca din nou mai tarziu.",
"not-found": "⚠️ - Nu au fost gasite versuri pentru aceasta melodie."
},
"menu": {
"default-text-string": {
"label": "Caracter implicit intre versuri",
"tooltip": "Alege caracterul implicit folosit pentru spatiul dintre versuri"
},
"line-effect": {
"label": "Efect de linie",
"submenu": {
"focus": {
"label": "Focalizare",
"tooltip": "Doar linia curenta este alba"
},
"offset": {
"label": "Offset",
"tooltip": "Deplasare la dreapta pentru linia curenta"
},
"scale": {
"label": "Marime",
"tooltip": "Schimba dimensiunea liniei curente"
}
},
"tooltip": "Alege efectul aplicat liniei curente"
},
"precise-timing": {
"label": "Sincronizeaza versurile perfect",
"tooltip": "Calculeaza afisarea urmatoarei linii pana la milisecunda (poate afecta performanta)"
},
"show-lyrics-even-if-inexact": {
"label": "Afiseaza versurile chiar daca sunt inexacte",
"tooltip": "Daca melodia nu este gasita, plugin-ul incearca din nou cu o cautare diferita.\nRezultatul acestei incercari poate sa nu fie exact."
},
"show-time-codes": {
"label": "Afiseaza timecode-urile",
"tooltip": "Afiseaza codurile de timp langa versuri"
}
},
"name": "Versuri Sincronizate",
"refetch-btn": {
"fetching": "Incarcare...",
"normal": "Reincarcare versuri"
},
"warnings": {
"duration-mismatch": "⚠️ - Versurile pot fi desincronizate din cauza unei nepotriviri de duratie.",
"inexact": "⚠️ - Versurile pentru aceasta melodie pot fi inexacte",
"instrumental": "⚠️ - Aceasta melodie este instrumentala"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Controleaza redarea din Bara de Activitati Windows", "description": "Controleaza redarea din Bara de Activitati Windows",
"name": "Control media in Bara de Activitate" "name": "Control media in Bara de Activitate"

View File

@ -279,6 +279,56 @@
}, },
"name": "Режим Ambient" "name": "Режим Ambient"
}, },
"amuse": {
"description": "Добавляет поддержку виджета Amuse „сейчас играет“ от 6K Labs",
"name": "Amuse",
"response": {
"query": "Сервер Amuse API запущен. GET /query чтобы получить информацию о треке."
}
},
"api-server": {
"description": "Добавляет API сервер для контроля за плеером",
"dialog": {
"request": {
"buttons": {
"allow": "Разрешить",
"deny": "Отказать"
},
"message": "Разрешить {{ID}} ({{origin}}) доступ к API?",
"title": "Запрос на авторизацию в API"
}
},
"menu": {
"auth-strategy": {
"label": "Способ авторизации",
"submenu": {
"auth-at-first": {
"label": "Авторизация при первом запросе"
},
"none": {
"label": "Без авторизации"
}
}
},
"hostname": {
"label": "Имя хоста"
},
"port": {
"label": "Порт"
}
},
"name": "API Сервер [БЕТА]",
"prompt": {
"hostname": {
"label": "Введите имя хоста (на подобии 0.0.0.0) для API сервера:",
"title": "Имя хоста"
},
"port": {
"label": "Введите порт для API сервера:",
"title": "Порт"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Применяет компрессию к аудио (уменьшает громкость самых громких частей сигнала и повышает громкость самых тихих частей)", "description": "Применяет компрессию к аудио (уменьшает громкость самых громких частей сигнала и повышает громкость самых тихих частей)",
"name": "Нормализация аудио" "name": "Нормализация аудио"
@ -414,6 +464,21 @@
"description": "Скачивать MP3 / исходное аудио напрямую из интерфейса", "description": "Скачивать MP3 / исходное аудио напрямую из интерфейса",
"menu": { "menu": {
"choose-download-folder": "Выберите папку для загрузок", "choose-download-folder": "Выберите папку для загрузок",
"download-finish-settings": {
"label": "Скачать по завершению",
"prompt": {
"last-percent": "После х процентов",
"last-seconds": "Осталось x сек",
"title": "Условия скачивания"
},
"submenu": {
"advanced": "Расширенные настройки",
"enabled": "Включено",
"mode": "Режим по времени",
"percent": "Процент",
"seconds": "Секунды"
}
},
"download-playlist": "Скачать плейлист", "download-playlist": "Скачать плейлист",
"presets": "Пресеты", "presets": "Пресеты",
"skip-existing": "Пропускать уже существующие файлы" "skip-existing": "Пропускать уже существующие файлы"
@ -426,6 +491,18 @@
"button": "Скачать" "button": "Скачать"
} }
}, },
"equalizer": {
"description": "Добавляет эквалайзер к плееру",
"menu": {
"presets": {
"label": "Предустановки",
"list": {
"bass-booster": "Усилитель баса"
}
}
},
"name": "Эквалайзер"
},
"exponential-volume": { "exponential-volume": {
"description": "Делает слайдер громкости расширенным чтобы было легче понижать громкость.", "description": "Делает слайдер громкости расширенным чтобы было легче понижать громкость.",
"name": "Расширенная громкость" "name": "Расширенная громкость"
@ -594,7 +671,7 @@
"dialog": { "dialog": {
"lastfm": { "lastfm": {
"auth-failed": { "auth-failed": {
"message": "Не удалось войти с помощью Last.fm\nСкрыть сообщение до следующего запуска", "message": "Не удалось войти с помощью Last.fm\nСкрыть сообщение до следующего запуска.",
"title": "Ошибка аунтефикации" "title": "Ошибка аунтефикации"
} }
} }
@ -656,8 +733,8 @@
"synced-lyrics": { "synced-lyrics": {
"description": "Предоставляет синхронизированные слова для песен из таких источников, как LRClib.", "description": "Предоставляет синхронизированные слова для песен из таких источников, как LRClib.",
"errors": { "errors": {
"fetch": "⚠️ - Возникла ошибка во время получения слов. Повторите попытку позже.", "fetch": "⚠️\tПроизошла ошибка во время получения слов.\n\tПовторите попытку позже.",
"not-found": "⚠️ - Для этой песни не найдено слов." "not-found": "⚠️ Для этой песни не найдено слов."
}, },
"menu": { "menu": {
"default-text-string": { "default-text-string": {
@ -667,6 +744,10 @@
"line-effect": { "line-effect": {
"label": "Эффект строки", "label": "Эффект строки",
"submenu": { "submenu": {
"fancy": {
"label": "Красивый",
"tooltip": "Использовать большие эффекты строки, как в приложении"
},
"focus": { "focus": {
"label": "Фокусировка", "label": "Фокусировка",
"tooltip": "Делает только текущую строку белой" "tooltip": "Делает только текущую строку белой"
@ -695,6 +776,7 @@
"tooltip": "Показывает временные метки рядом со словами" "tooltip": "Показывает временные метки рядом со словами"
} }
}, },
"name": "Синхронизированные тексты песен",
"refetch-btn": { "refetch-btn": {
"fetching": "Сбор данных...", "fetching": "Сбор данных...",
"normal": "Обновить слова" "normal": "Обновить слова"

View File

@ -31,11 +31,74 @@
}, },
"theme": { "theme": {
"css-file-not-found": "සීඑස්එස් ගොනුව \"{{cssFile}}\" නොපවතී, නොසලකා හැරීම" "css-file-not-found": "සීඑස්එස් ගොනුව \"{{cssFile}}\" නොපවතී, නොසලකා හැරීම"
},
"unresponsive": {
"details": "ප්‍රතිචාර නොදක්වන දෝෂයක් {{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "යෙදුම් කෑශ් නිදහස් කරමින්"
} }
}, },
"dialog": { "dialog": {
"hide-menu-enabled": {
"detail": "මෙනුව සැගවී ඇත, 'Alt' යතුර නැවත පෙන්වීමට භාවිතා කරන්න. (හෝ In-App මෙනුවේ 'Escape')",
"message": "මෙනුව සැගවීම සාර්තකයි",
"title": "මෙනුව සැගවීම සක්‍රීයයි"
},
"need-to-restart": { "need-to-restart": {
"buttons": {
"later": "පසුව",
"restart-now": "යෙදුම වසා නැවත ආරම්භ කරන්න"
},
"detail": "\"{{pluginName}}\" ප්ලගිනය යෙදුම නැවත ආරම්භ කිරීමක් ඉල්ලයි",
"message": "\"{{pluginName}}\" නැවත ආරම්භ කළ යුතුය",
"title": "නැවත ආරම්භ කිරීම අවශ්‍යයි" "title": "නැවත ආරම්භ කිරීම අවශ්‍යයි"
},
"unresponsive": {
"buttons": {
"quit": "ඉවත් වන්න",
"relaunch": "නැවත ආරම්භ කරන්න",
"wait": "රැදී සිටින්න"
},
"detail": "සිදු වූ දේ සම්බන්ධව අපගේ කණගාටුව! කළ යුතු දේ තෝරන්න:",
"message": "යෙදුම ප්‍රතිචාර නොදක්වයි",
"title": "වින්ඩෝව ප්‍රතිචාර නොදක්වයි"
},
"update-available": {
"buttons": {
"disable": "යාවත්කාලීන කිරීම් නවතන්න",
"download": "බාගත කිරීම",
"ok": "හරි"
},
"detail": "නව අනුවාදයක් ඇති අතර එය මෙයින් බාගන්න {{downloadLink}}",
"message": "නව අනුවාදයක් ඇත",
"title": "යාවත්කාලීන කිරීමක් ඇත"
}
},
"menu": {
"about": "පිළිබදව",
"navigation": {
"label": "සංචලනය",
"submenu": {
"copy-current-url": "යොමුව පිටපත් කරගන්න",
"go-back": "පිටුපසට",
"go-forward": "ඉදිරියට",
"quit": "පිටවන්න",
"restart": "යෙදුම යලි අරඹන්න"
}
},
"options": {
"label": "විකල්ප",
"submenu": {
"advanced-options": {
"label": "උසස් විකල්ප",
"submenu": {
"auto-reset-app-cache": "යෙදුම් කෑච් යෙදුම ආරම්භයේදී යලි පිහිටුවන්න",
"disable-hardware-acceleration": "දෘඩාංග භාවිත වේගවත් කිරීම් අක්‍රීය කරන්න",
"edit-config-json": "config.json සකසන්න"
}
}
}
} }
} }
} }

View File

@ -3,7 +3,7 @@
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "Napaka pri inicilizaciji dodatka {{pluginName}}::{{contextName}}", "execute-failed": "Napaka pri inicilizaciji dodatka {{pluginName}}::{{contextName}}",
"executed-at-ms": "Dodatek {{pluginName}}::{{contextName}} izvešen pri {{ms}}ms", "executed-at-ms": "Dodatek {{pluginName}}::{{contextName}} izvršen pri {{ms}}ms",
"initialize-failed": "Napaka pri inicilizaciji dodatka \"{{pluginName}}\"", "initialize-failed": "Napaka pri inicilizaciji dodatka \"{{pluginName}}\"",
"load-all": "Nalaganje dodatkov", "load-all": "Nalaganje dodatkov",
"load-failed": "Napaka pri nalaganju dodatka \"{{pluginName}}\"", "load-failed": "Napaka pri nalaganju dodatka \"{{pluginName}}\"",
@ -36,7 +36,7 @@
"details": "Neodzivna napaka!\n{{error}}" "details": "Neodzivna napaka!\n{{error}}"
}, },
"when-ready": { "when-ready": {
"clearing-cache-after-20s": "Čiščenje predpolnilnika" "clearing-cache-after-20s": "Čiščenje predpomnilnika"
}, },
"window": { "window": {
"tried-to-render-offscreen": "Okno se je poskusilo prikazati izven ekrana, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}" "tried-to-render-offscreen": "Okno se je poskusilo prikazati izven ekrana, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
@ -46,13 +46,14 @@
"hide-menu-enabled": { "hide-menu-enabled": {
"detail": "Meni je skrit, pritisni 'Alt' za odpiranje (ali 'Escape' če uporabljaš In-App Meni)", "detail": "Meni je skrit, pritisni 'Alt' za odpiranje (ali 'Escape' če uporabljaš In-App Meni)",
"message": "Skriti meni je prižgan", "message": "Skriti meni je prižgan",
"title": "Skrij meni uklopljen" "title": "Skrij meni vklopljen"
}, },
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
"later": "Kasneje",
"restart-now": "Ponovno zaženi zdaj" "restart-now": "Ponovno zaženi zdaj"
}, },
"detail": "\"{{pluginName}}\" dodatek potrebuje ponovni zagon za začetek", "detail": "\"{{pluginName}}\" dodatek potrebuje ponovni zagon",
"message": "\"{{pluginName}}\" je potrebno ponovno zagnati", "message": "\"{{pluginName}}\" je potrebno ponovno zagnati",
"title": "Potreben je ponovni zagon" "title": "Potreben je ponovni zagon"
}, },
@ -62,7 +63,7 @@
"relaunch": "Ponovno zaženi", "relaunch": "Ponovno zaženi",
"wait": "Počakaj" "wait": "Počakaj"
}, },
"detail": "Opravičujemo se za neprijetnost! Prosim odločite se kaj narediti:", "detail": "Opravičujemo se za nevšečnost! Prosim odločite se kaj narediti:",
"message": "Aplikacija se ne odziva", "message": "Aplikacija se ne odziva",
"title": "Okno se ne odziva" "title": "Okno se ne odziva"
}, },
@ -82,7 +83,7 @@
"navigation": { "navigation": {
"label": "Navigacija", "label": "Navigacija",
"submenu": { "submenu": {
"copy-current-url": "Kopiraj trenuten URL", "copy-current-url": "Kopiraj trenutni URL",
"go-back": "Nazaj", "go-back": "Nazaj",
"go-forward": "Naprej", "go-forward": "Naprej",
"quit": "Izhod", "quit": "Izhod",
@ -95,7 +96,7 @@
"advanced-options": { "advanced-options": {
"label": "Dodatne nastavitve", "label": "Dodatne nastavitve",
"submenu": { "submenu": {
"auto-reset-app-cache": "Resetiraj predpolnilnik aplikacije ob zagonu", "auto-reset-app-cache": "Resetiraj predpomnilnik aplikacije ob zagonu",
"disable-hardware-acceleration": "Izklopi strojno pospeševanje", "disable-hardware-acceleration": "Izklopi strojno pospeševanje",
"edit-config-json": "Spremeni config.json", "edit-config-json": "Spremeni config.json",
"override-user-agent": "Prepiši User-Agent", "override-user-agent": "Prepiši User-Agent",
@ -103,7 +104,7 @@
"set-proxy": { "set-proxy": {
"label": "Nastavi proxy", "label": "Nastavi proxy",
"prompt": { "prompt": {
"label": "Napiši naslov Proxy: (pusti prazno, da izklopiš)", "label": "Napiši Proxy naslov: (pusti prazno, da izklopiš)",
"placeholder": "Primer: SOCKS5://127.0.0.1:9999", "placeholder": "Primer: SOCKS5://127.0.0.1:9999",
"title": "Nastavi Proxy" "title": "Nastavi Proxy"
} }
@ -115,7 +116,7 @@
"auto-update": "Avtomatsko posodobi", "auto-update": "Avtomatsko posodobi",
"hide-menu": { "hide-menu": {
"dialog": { "dialog": {
"message": "Meni se bo skrit pri naslednjem zagonu, uporabi [Alt] da se prikaže (ali [`] v in-app-menu)", "message": "Meni se bo skrit pri naslednjem zagonu, uporabi [Alt] da se prikaže (ali [`] v meniju aplikacije)",
"title": "Skrij meni vklopljen" "title": "Skrij meni vklopljen"
}, },
"label": "Skrij meni" "label": "Skrij meni"
@ -127,12 +128,12 @@
}, },
"label": "Jezik", "label": "Jezik",
"submenu": { "submenu": {
"to-help-translate": "Želiš pomagati prevediti? Klikni tukaj" "to-help-translate": "Želiš pomagati pri prevajanju? Klikni tukaj"
} }
}, },
"resume-on-start": "Predvajaj zadnjo pesem, ko se aplikacija prižge", "resume-on-start": "Predvajaj zadnjo pesem, ko se aplikacija zažene",
"single-instance-lock": "Zaklep ene instance", "single-instance-lock": "Zaklep ene instance",
"start-at-login": "Prižgi pri zagonu", "start-at-login": "Zaženi pri zagonu",
"starting-page": { "starting-page": {
"label": "Začetna stran", "label": "Začetna stran",
"unset": "Ni nastavljeno" "unset": "Ni nastavljeno"
@ -153,7 +154,7 @@
"default": "Privzeto", "default": "Privzeto",
"force-show": "Prisilno pokaži", "force-show": "Prisilno pokaži",
"hide": "Skrij", "hide": "Skrij",
"label": "Gumb všeč mi je" "label": "Gumbi za všečkanje"
}, },
"remove-upgrade-button": "Odstrani gumb za nadgradnjo", "remove-upgrade-button": "Odstrani gumb za nadgradnjo",
"theme": { "theme": {
@ -162,8 +163,8 @@
"cancel": "Prekliči", "cancel": "Prekliči",
"remove": "Odstrani" "remove": "Odstrani"
}, },
"remove-theme": "Ali želite odstraniti poljubno temo?", "remove-theme": "Ali želite odstraniti temo po meri?",
"remove-theme-message": "Poljubna tema bo odtranjena" "remove-theme-message": "Tema po meri bo odstranjena"
}, },
"label": "Tema", "label": "Tema",
"submenu": { "submenu": {
@ -206,12 +207,16 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Če se predvaja oglas se zvok utišja. Prav tako se hitrost predvajanja nastavi na 16 krat",
"name": "Pospeševanje oglasov"
},
"adblocker": { "adblocker": {
"description": "Izklopi vse oglase od začetka", "description": "Izklopi vse oglase in sledenje",
"menu": { "menu": {
"blocker": "Blocker" "blocker": "Blokator"
}, },
"name": "Ad Blocker" "name": "Blokator reklam"
}, },
"album-actions": { "album-actions": {
"description": "Doda Undislike, Dislike, Like, in Unlike gumbe vsem glasbam v seznamu predvajanja ali albumu", "description": "Doda Undislike, Dislike, Like, in Unlike gumbe vsem glasbam v seznamu predvajanja ali albumu",
@ -221,31 +226,31 @@
"description": "Doda dinamično temo in vizualne efekte glede na barve albuma", "description": "Doda dinamično temo in vizualne efekte glede na barve albuma",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "Raznerje barv", "label": "Razmerje barv",
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
} }
}, },
"name": "Tema Brav Albuma" "name": "Barvna tema Albuma"
}, },
"ambient-mode": { "ambient-mode": {
"description": "Doda bravn efekt iz video posnetka na ozadje", "description": "Doda barvni učinek iz video posnetka na ozadje",
"menu": { "menu": {
"blur-amount": { "blur-amount": {
"label": "količina zameglitve", "label": "Stopnja zameglitve",
"submenu": { "submenu": {
"pixels": "{{blurAmount}} pikslov" "pixels": "{{blurAmount}} pikslov"
} }
}, },
"buffer": { "buffer": {
"label": "Medpolnilnik", "label": "Medpomnilnik",
"submenu": { "submenu": {
"buffer": "{{buffer}}" "buffer": "{{buffer}}"
} }
}, },
"opacity": { "opacity": {
"label": "Nepreglednost", "label": "Prozornost",
"submenu": { "submenu": {
"percent": "{{opacity}}%" "percent": "{{opacity}}%"
} }
@ -269,11 +274,54 @@
} }
}, },
"use-fullscreen": { "use-fullscreen": {
"label": "Uporablja cel zaslon" "label": "Uporablja celoten zaslon"
} }
}, },
"name": "Ambienten način" "name": "Ambienten način"
}, },
"api-server": {
"description": "Doda API strežnik za nadzor predvajalnika",
"dialog": {
"request": {
"buttons": {
"allow": "Dovoli",
"deny": "Zavrni"
},
"message": "Dovolite {{ID}} ({{origin}}) da dostopa do API-ja?",
"title": "Prošnja za avtomatizacijo API-ja"
}
},
"menu": {
"auth-strategy": {
"label": "Strategija avtorizacije",
"submenu": {
"auth-at-first": {
"label": "Avtorizacija ob prvem zahtevku"
},
"none": {
"label": "Ni avtorizacije"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API strežnik [Beta]",
"prompt": {
"hostname": {
"label": "Vnesite hostname (npr. 0.0.0.0) za API strežnik:",
"title": "Hostname"
},
"port": {
"label": "Vnesite port za API strežnik:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Doda kompresijo zvoka (izenači ravni zvoka, zniža glasnost najglasnejših delov in zviša najtišje)", "description": "Doda kompresijo zvoka (izenači ravni zvoka, zniža glasnost najglasnejših delov in zviša najtišje)",
"name": "Kompresija zvoka" "name": "Kompresija zvoka"
@ -289,7 +337,7 @@
"captions-selector": { "captions-selector": {
"description": "Izberi podnapise za YouTube Music zvočne posnetke", "description": "Izberi podnapise za YouTube Music zvočne posnetke",
"menu": { "menu": {
"autoload": "Avtomatsko uporabi zanje izbrane podnapise", "autoload": "Avtomatsko uporabi zadnje izbrane podnapise",
"disable-captions": "Avtomatsko brez podnapisov" "disable-captions": "Avtomatsko brez podnapisov"
}, },
"name": "Izberi podnapise", "name": "Izberi podnapise",
@ -301,7 +349,7 @@
} }
}, },
"templates": { "templates": {
"title": "Odpri izbir podnapisov" "title": "Odpri izbiro podnapisov"
} }
}, },
"compact-sidebar": { "compact-sidebar": {
@ -320,15 +368,186 @@
"fade-in-duration": "Čas zbledenja (v pesem) (ms)", "fade-in-duration": "Čas zbledenja (v pesem) (ms)",
"fade-out-duration": "Čas zbledenja (izven pesemi) (ms)", "fade-out-duration": "Čas zbledenja (izven pesemi) (ms)",
"fade-scaling": { "fade-scaling": {
"label": "Fade scaling", "label": "Zbledi skaliranje",
"linear": "Linearno", "linear": "Linearno",
"logarithmic": "Logaritmično" "logarithmic": "Logaritmično"
}, },
"seconds-before-end": "Crossfade N seconds before end" "seconds-before-end": "Bledenje (crossfade) N sekund pred koncem"
}, },
"title": "Možnosti zbledenja" "title": "Možnosti zbledenja"
} }
} }
},
"disable-autoplay": {
"description": "Začne pesem v zaustavljenem načinu",
"menu": {
"apply-once": "Uporabi samo ob zagonu"
},
"name": "Onemogoči samodejno predvajanje"
},
"discord": {
"backend": {
"already-connected": "Poizkus povezave z aktivno povezavo",
"connected": "Povezan na Discord",
"disconnected": "Povezava z Discord-om prekinjena"
},
"description": "Pokaži svojim prijateljem kaj poslušaš z bogato prisotnostjo (Rich Presence)",
"menu": {
"auto-reconnect": "Samodejna unovična povezava",
"clear-activity": "Počisti dejavnost",
"clear-activity-after-timeout": "Počisti dejavnost po časovni omejitvi",
"connected": "Povezan",
"disconnected": "Prekinjena povezava",
"hide-duration-left": "Skrij preostali čas",
"hide-github-button": "Skrij povezavo do GitHub-a",
"play-on-youtube-music": "Predvajaj v YouTube Music",
"set-inactivity-timeout": "Nastavite časovno omejitev neaktivnosti"
},
"name": "Discord bogata prisotnost (Rich Presence)",
"prompt": {
"set-inactivity-timeout": {
"label": "Vnesite časovno omejitev neaktivnosti v sekundah:",
"title": "Nastavite časovno omejitev nedejavnosti"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Uff! Se opravičujemo, prenos neuspešen…",
"title": "Napaka v prenosu!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} pesmi)",
"message": "Prenašanje seznama {{playlistTitle}}",
"title": "Prenos se je začel"
}
},
"feedback": {
"conversion-progress": "Konverzija: {{percent}}%",
"converting": "Pretvarjanje…",
"done": "Končano: {{filePath}}",
"download-info": "Prenašanje {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Prenos: {{percent}}%",
"downloading": "Prenašanje…",
"downloading-counter": "Prenašanje {{current}}/{{total}}…",
"downloading-playlist": "Prenašanje seznama \"{{playlistTitle}}\" - {{playlistSize}} pesmi ({{playlistId}})",
"error-while-downloading": "Napaka pri prenosu \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Ta mapa {{playlistFolder}} že obstaja",
"getting-playlist-info": "Pridobivam informacije o seznamu…",
"loading": "Nalaganje…",
"playlist-has-only-one-song": "Ta seznam ima samo eno pesem, uporabljam direkten prenos",
"playlist-id-not-found": "ID seznama ni najden",
"playlist-is-empty": "Seznam je prazen",
"playlist-is-mix-or-private": "Napaka v pridobivanju informacij o seznamu: poskrbite da seznam ni zaseben ali \"Mixed for you\" seznam\n\n{{error}}",
"preparing-file": "Pripravljanje datoteke…",
"saving": "Shranjujem…",
"trying-to-get-playlist-id": "Poizkušam pridobiti ID seznama: {{playlistId}}",
"video-id-not-found": "Videoposnetek ni najden",
"writing-id3": "Zapisujem ID3 oznake…"
}
},
"description": "Prenese MP3 / izviren zvok direktno iz vmesnika",
"menu": {
"choose-download-folder": "Izberite mapo s prenosi",
"download-finish-settings": {
"label": "Prenesi ob koncu",
"prompt": {
"last-percent": "Po x odstotkov",
"last-seconds": "Zadnjih x sekund",
"title": "Nastavite čas prenosa"
},
"submenu": {
"advanced": "Napredno",
"enabled": "Omogočen",
"mode": "Časovni način",
"percent": "Odstotek",
"seconds": "Sekunde"
}
},
"download-playlist": "Prenesi seznam",
"presets": "Prednastavitve",
"skip-existing": "Preskoči obstoječe datoteke"
},
"name": "Prenaševalec",
"renderer": {
"can-not-update-progress": "Nemorem osvežiti napredka"
},
"templates": {
"button": "Prenos"
}
},
"exponential-volume": {
"description": "Drsnik za glasnost naredi eksponenten, da bo lažje izbrati nižje glasnosti.",
"name": "Eksponentna glasnost"
},
"in-app-menu": {
"description": "Menijem doda eleganten videz v temnih barvah ali barvah albuma",
"menu": {
"hide-dom-window-controls": "Skrije DOM gumbe za okno"
},
"name": "Meni v aplikaciji"
},
"lumiastream": {
"description": "Doda podporo za Lumia pretočno predvajanje",
"name": "Lumina pretočno predavanje [Beta]"
},
"lyrics-genius": {
"description": "Doda podporo besedil za večino pesmi",
"menu": {
"romanized-lyrics": "Romanizerana besedila"
},
"name": "Besedila Genius",
"renderer": {
"fetched-lyrics": "Prestregel besedila za Genius"
}
},
"music-together": {
"description": "Delite seznam predvajanja z drugimi. Ko gostitelj predvaja skladbo, bodo vsi ostali slišali isto skladbo",
"dialog": {
"enter-host": "Vnesite Host ID"
},
"internal": {
"save": "Shrani",
"track-source": "Vir pesmi",
"unknown-user": "Neznan uporabnik"
},
"menu": {
"click-to-copy-id": "Kopiraj Host ID",
"close": "Zapri Glasba Skupaj",
"connected-users": "Povezani uporabniki",
"disconnect": "Prekini povezavo z Pesmi Skupaj",
"empty-user": "Ni povezanih uporabnikov",
"host": "Gkasba Skupaj gostitelj",
"join": "Pridruži se Glasba Skupaj",
"permission": {
"all": "Dovoli da gosti nadzorujejo seznam predvajanja in predvajalnik",
"host-only": "Samo gostitelj lahko spreminja seznam predvajanja in predvajalnik",
"playlist": "Dovoli da gostje nadzorujejo predvajalnik"
},
"set-permission": "Spremeni dovoljenje nadzora",
"status": {
"disconnected": "Odklopljen",
"guest": "Povezan kot Gost",
"host": "Povezan kot Gostitelj"
}
},
"name": "Gkasba Skupaj [Beta]",
"toast": {
"add-song-failed": "Skladba ni bila dodana",
"closed": "Glasba Skupaj zaprto",
"disconnected": "Gkasba Skupaj odklopljena",
"host-failed": "Gkasba Skupaj nisem mogel gostiti",
"id-copied": "Host ID je kopiran v odložišče",
"id-copy-failed": "Host ID ni bilo mogoče kopirati"
}
} }
} }
} }

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "หากมีการเล่นโฆษณา เสียงจะถูกปิดและตั้งค่าความเร็วในการเล่นเป็น 16x",
"name": "เพิ่มความเร็วโฆษณา"
},
"adblocker": { "adblocker": {
"description": "บล็อกโฆษณาและการติดตามทั้งหมดอย่างอัตโนมัติ", "description": "บล็อกโฆษณาและการติดตามทั้งหมดอย่างอัตโนมัติ",
"menu": { "menu": {
@ -275,6 +279,18 @@
}, },
"name": "โหมดสภาพแวดล้อม" "name": "โหมดสภาพแวดล้อม"
}, },
"api-server": {
"description": "เพิ่มเซิร์ฟเวอร์ API เพื่อควบคุมการเล่น",
"dialog": {
"request": {
"buttons": {
"allow": "อนุญาต",
"deny": "ปฏิเสธ"
},
"message": "อนุญาตให้ {{ID}} ({{origin}}) เข้าถึง API หรือไม่?"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "ใช้การบีบอัดเสียง (ลดระดับเสียงของส่วนที่ดังที่สุดของสัญญาณและเพิ่มระดับเสียงของส่วนที่เบาที่สุด)", "description": "ใช้การบีบอัดเสียง (ลดระดับเสียงของส่วนที่ดังที่สุดของสัญญาณและเพิ่มระดับเสียงของส่วนที่เบาที่สุด)",
"name": "เครื่องมือบีบอัดเสียง" "name": "เครื่องมือบีบอัดเสียง"

View File

@ -279,6 +279,49 @@
}, },
"name": "Ambiyans Modu" "name": "Ambiyans Modu"
}, },
"api-server": {
"description": "APİ ekle ve oynatıcıyı kontrol et",
"dialog": {
"request": {
"buttons": {
"allow": "İzin ver",
"deny": "Reddet"
},
"message": "{{ID}} ({{origin}}) 'nın APIye erişmesine izin verilsin mi?",
"title": "APİ yetkilendirme isteği"
}
},
"menu": {
"auth-strategy": {
"label": "Yetkilendirme stratejisi",
"submenu": {
"auth-at-first": {
"label": "İlk istekte yetkilendir"
},
"none": {
"label": "Yetkilendirme Yok"
}
}
},
"hostname": {
"label": "Ana bilgisayar adı"
},
"port": {
"label": "Port"
}
},
"name": "API sunucusu [Beta]",
"prompt": {
"hostname": {
"label": "API sunucusu için hostname (örneğin 0.0.0.0) girin:",
"title": "Hostname"
},
"port": {
"label": "API sunucusu için port girin:",
"title": "Bağlantı Noktası"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)", "description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)",
"name": "Ses Sıkıştırma" "name": "Ses Sıkıştırma"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "При програванні реклами звук вимикається і встановлюється швидкість відтворення 16х",
"name": "Пришвидшення релками"
},
"adblocker": { "adblocker": {
"description": "Блокувати всю рекламу та відстеження з коробки", "description": "Блокувати всю рекламу та відстеження з коробки",
"menu": { "menu": {
@ -275,6 +279,49 @@
}, },
"name": "Режим навколишнього середовища" "name": "Режим навколишнього середовища"
}, },
"api-server": {
"description": "Додає API сервер для контролю плеєра",
"dialog": {
"request": {
"buttons": {
"allow": "Дозволити",
"deny": "Відмінити"
},
"message": "Дозволити {{ID}} ({{origin}}) доступ до API?",
"title": "Запит авторизації до API"
}
},
"menu": {
"auth-strategy": {
"label": "Стратегія авторизації",
"submenu": {
"auth-at-first": {
"label": "Авторизувати при першому запиті"
},
"none": {
"label": "Немає авторизації"
}
}
},
"hostname": {
"label": "Назва серверу"
},
"port": {
"label": "Порт"
}
},
"name": "API сервер [Бета]",
"prompt": {
"hostname": {
"label": "Введіть ім'я хоста (наприклад 0.0.0.0) для API серверу:",
"title": "Ім'я хоста"
},
"port": {
"label": "Введіть порт API серверу:",
"title": "Порт"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Застосувати стиснення аудіо (зменшити гучність найгучніших фрагментів сигналу та збільшити гучність тихих фрагментів)", "description": "Застосувати стиснення аудіо (зменшити гучність найгучніших фрагментів сигналу та збільшити гучність тихих фрагментів)",
"name": "Аудіокомпресор" "name": "Аудіокомпресор"
@ -411,13 +458,17 @@
"menu": { "menu": {
"choose-download-folder": "Оберіть папку для завантаження", "choose-download-folder": "Оберіть папку для завантаження",
"download-finish-settings": { "download-finish-settings": {
"label": "Скачати по завершенню",
"prompt": { "prompt": {
"last-percent": "Після Х відсотків", "last-percent": "Після Х відсотків",
"last-seconds": "Останні Х секунд" "last-seconds": "Останні Х секунд",
"title": "Налаштувати коли завантажувати"
}, },
"submenu": { "submenu": {
"advanced": "Розширені",
"enabled": "Увімкнено", "enabled": "Увімкнено",
"percent": "Відсотків", "mode": "Режим часу",
"percent": "Відсоток",
"seconds": "Секунди" "seconds": "Секунди"
} }
}, },
@ -661,6 +712,7 @@
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": { "synced-lyrics": {
"description": "Додає синхронізовані тексти до пісень використовуючи провайдери, такі як LRClib.",
"errors": { "errors": {
"fetch": "⚠️ - При завантаженні тексту сталась помилка. Спробуйте ще раз пізніше.", "fetch": "⚠️ - При завантаженні тексту сталась помилка. Спробуйте ще раз пізніше.",
"not-found": "⚠️ - До цієї пісні текст не знайдено." "not-found": "⚠️ - До цієї пісні текст не знайдено."
@ -677,6 +729,10 @@
"label": "Зосереджитись", "label": "Зосереджитись",
"tooltip": "Зробити білим лише поточний рядок" "tooltip": "Зробити білим лише поточний рядок"
}, },
"offset": {
"label": "Офсет",
"tooltip": "Офсет з права від нинішньої лінії"
},
"scale": { "scale": {
"label": "Масштабувати", "label": "Масштабувати",
"tooltip": "Масштабуваты поточну лінію" "tooltip": "Масштабуваты поточну лінію"
@ -685,7 +741,8 @@
"tooltip": "Виберіть ефект, який потрібно застосувати до поточної лінії" "tooltip": "Виберіть ефект, який потрібно застосувати до поточної лінії"
}, },
"precise-timing": { "precise-timing": {
"label": "Зробити текст пісні ідеально синхронізованим" "label": "Зробити текст пісні ідеально синхронізованим",
"tooltip": "Обчисли до мілісекунд відображення наступного рядка (може мати невеликий вплив на продуктивність)"
}, },
"show-lyrics-even-if-inexact": { "show-lyrics-even-if-inexact": {
"label": "Показувати текст пісні, навіть якщо він неточний", "label": "Показувати текст пісні, навіть якщо він неточний",
@ -698,7 +755,7 @@
}, },
"name": "Синхронізовані тексти", "name": "Синхронізовані тексти",
"refetch-btn": { "refetch-btn": {
"fetching": "Отримання...", "fetching": "Завантаження...",
"normal": "Перезавантажити текст" "normal": "Перезавантажити текст"
}, },
"warnings": { "warnings": {

134
src/i18n/resources/ur.json Normal file
View File

@ -0,0 +1,134 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "پلگ ان {{pluginName}}::{{contextName}} پر عمل کرنے میں ناکام",
"executed-at-ms": "پلگ ان {{pluginName}}::{{contextName}} کو {{ms}}ms پر عمل میں لایا گیا",
"initialize-failed": "پلگ ان \"{{pluginName}}\" کو شروع کرنے میں ناکام",
"load-all": "تمام پلگ ان لوڈ ہو رہے ہیں",
"load-failed": "\"{{pluginName}}\" پلگ ان لوڈ کرنے میں ناکام",
"loaded": "پلگ ان \"{{pluginName}}\" لوڈ ہو گیا",
"unload-failed": "پلگ ان \"{{pluginName}}\" کو لوڈ کرنے میں ناکام",
"unloaded": "پلگ ان \"{{pluginName}}\" کو لوڈ نہیں کیا گیا"
}
}
},
"language": {
"code": "ur",
"local-name": "اردو",
"name": "Urdu"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "لوڈنگ مکمل ہو گئی۔ DevTools کھل گیا"
},
"i18n": {
"loaded": "i18n لوڈ ہو گیا"
},
"second-instance": {
"receive-command": "پروٹوکول پر کمانڈ موصول ہوئی: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS فائل \"{{cssFile}}\" موجود نہیں ہے، نظر انداز کر رہے ہیں"
},
"unresponsive": {
"details": "غیر جوابی غلطی!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "ایپ کیشے کو صاف کرنا"
},
"window": {
"tried-to-render-offscreen": "ونڈو نے آف اسکرین رینڈر کرنے کی کوشش کی، windowSize={{windowSize}}، displaySize={{displaySize}}، position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "مینو پوشیدہ ہے، اسے دکھانے کے لیے 'Alt' استعمال کریں (یا 'Escape' اگر ایپ مینیو استعمال کر رہے ہیں)",
"message": "پوشیدہ مینو فعال ہے",
"title": "پوشیدہ مینو فعال ہو گیا"
},
"need-to-restart": {
"buttons": {
"later": "بعد میں",
"restart-now": "ابھی دوبارہ شروع کریں"
},
"detail": "\"{{pluginName}}\" پلگ ان کو اثر انداز ہونے کے لیے دوبارہ شروع کرنے کی ضرورت ہے",
"message": "\"{{pluginName}}\" کو دوبارہ شروع کرنے کی ضرورت ہے",
"title": "دوبارہ شروع کرنے کی ضرورت ہے"
},
"unresponsive": {
"buttons": {
"quit": "چھوڑو",
"relaunch": "دوبارہ لانچ کریں",
"wait": "انتظار کرو"
},
"detail": "ہم زحمت کے لیے معذرت خواہ ہیں! براہ کرم منتخب کریں کہ کیا کرنا ہے:",
"message": "پروگرام غیر ذمہ دار ہے",
"title": "ونڈو غیر جوابدہ"
},
"update-available": {
"buttons": {
"disable": "اپ ڈیٹس کو غیر فعال کریں",
"download": "ڈاؤن لوڈ کریں",
"ok": "ٹھیک ہے"
},
"detail": "ایک نیا ورژن دستیاب ہے اور اسے {{downloadLink}} پر ڈاؤن لوڈ کیا جا سکتا ہے",
"message": "ایک نیا ورژن دستیاب ہے",
"title": "اپ ڈیٹ دستیاب ہے"
}
},
"menu": {
"about": "پروگرام کے بارے میں",
"navigation": {
"label": "نیویگیشن",
"submenu": {
"copy-current-url": "موجودہ URL کاپی کریں",
"go-back": "واپس جاؤ",
"go-forward": "آگے بڑھو",
"quit": "باہر نکلیں",
"restart": "ایپ کو دوبارہ شروع کریں"
}
},
"options": {
"label": "آپشنز",
"submenu": {
"advanced-options": {
"label": "اعلی درجے کے آپشنز",
"submenu": {
"auto-reset-app-cache": "ایپ شروع ہونے پر ایپ کیشے کو دوبارہ ترتیب دیں",
"disable-hardware-acceleration": "ہارڈ ویئر ایکسلریشن کو غیر فعال کریں",
"edit-config-json": "config.json میں ترمیم کریں",
"override-user-agent": "یوزر ایجنٹ کو اوور رائیڈ کریں",
"restart-on-config-changes": "کنفیگریشن تبدیلیوں پر دوبارہ شروع کریں",
"set-proxy": {
"label": "پراکسی سیٹ کریں",
"prompt": {
"label": "پراکسی ایڈریس درج کریں: (غیر فعال کرنے کے لیے خالی چھوڑ دیں)",
"placeholder": "مثال: SOCKS5://127.0.0.1:9999",
"title": "پراکسی سیٹ کریں"
}
},
"toggle-dev-tools": "DevTools ٹوگل کریں"
}
},
"always-on-top": "ہمیشہ اوپر",
"auto-update": "خودکار اپ ڈیٹ",
"hide-menu": {
"dialog": {
"message": "اگلے لانچ پر مینو کو چھپایا جائے گا، اسے دکھانے کے لیے [Alt] استعمال کریں (یا in-app-menu استعمال کرنے پر بیک ٹک [`] کریں)",
"title": "پوشیدہ مینو کو فعال کر دیا گیا"
},
"label": "مینو کو چھپائیں"
},
"language": {
"dialog": {
"message": "دوبارہ شروع کرنے کے بعد زبان بدل دی جائے گی",
"title": "زبان بدل گئی ہے"
}
}
}
}
}
}
}

View File

@ -279,6 +279,49 @@
}, },
"name": "Chế độ Môi trường xung quanh" "name": "Chế độ Môi trường xung quanh"
}, },
"api-server": {
"description": "Thêm máy chủ API để điều khiển trình phát",
"dialog": {
"request": {
"buttons": {
"allow": "Cho phép",
"deny": "Từ chối"
},
"message": "Cho phép {{ID}} ({{origin}}) truy cập API?",
"title": "Yêu cầu cho phép API"
}
},
"menu": {
"auth-strategy": {
"label": "Chiến thuật xác thực",
"submenu": {
"auth-at-first": {
"label": "Xác thực ngay yêu cầu đầu tiên"
},
"none": {
"label": "Không/Chưa xác thực (Need context)"
}
}
},
"hostname": {
"label": "Tên máy chủ"
},
"port": {
"label": "Cổng"
}
},
"name": "Máy chủ API [Beta]",
"prompt": {
"hostname": {
"label": "Điền tên máy chủ (như 0.0.0.0) cho máy chủ API:",
"title": "Tên máy chủ"
},
"port": {
"label": "Nhập cổng cho máy chủ API:",
"title": "Cổng"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Áp dụng tính năng nén cho âm thanh (giảm âm lượng của phần to nhất của tín hiệu và tăng âm lượng của phần nhỏ nhất)", "description": "Áp dụng tính năng nén cho âm thanh (giảm âm lượng của phần to nhất của tín hiệu và tăng âm lượng của phần nhỏ nhất)",
"name": "Bộ nén âm thanh" "name": "Bộ nén âm thanh"
@ -671,7 +714,7 @@
"synced-lyrics": { "synced-lyrics": {
"description": "Cung cấp lời bài hát được đồng bộ hoá với các bài hát, sử dụng những nhà cung cấp như LRClib.", "description": "Cung cấp lời bài hát được đồng bộ hoá với các bài hát, sử dụng những nhà cung cấp như LRClib.",
"errors": { "errors": {
"fetch": "⚠️ - Đã xảy ra lỗi khi tìm nạp lời bài hát, Vui lòng thử lại sau.", "fetch": "⚠️ - Đã xảy ra lỗi khi tìm lời bài hát, Vui lòng thử lại sau.",
"not-found": "⚠️ - Không tìm thấy lời cho bài hát này." "not-found": "⚠️ - Không tìm thấy lời cho bài hát này."
}, },
"menu": { "menu": {
@ -686,6 +729,10 @@
"label": "Tập trung", "label": "Tập trung",
"tooltip": "Chỉ làm cho dòng hiện tại có màu trắng" "tooltip": "Chỉ làm cho dòng hiện tại có màu trắng"
}, },
"offset": {
"label": "Độ lệch",
"tooltip": "Độ lệch bên phải của dòng hiện tại"
},
"scale": { "scale": {
"label": "Tỉ lệ", "label": "Tỉ lệ",
"tooltip": "Áp dụng tỉ lệ cho dòng hiện tại" "tooltip": "Áp dụng tỉ lệ cho dòng hiện tại"

View File

@ -39,7 +39,7 @@
"clearing-cache-after-20s": "正在清理应用缓存" "clearing-cache-after-20s": "正在清理应用缓存"
}, },
"window": { "window": {
"tried-to-render-offscreen": "窗口试图于屏幕外绘制, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}" "tried-to-render-offscreen": "窗口试图于屏幕外绘制,窗口大小={{windowSize}},显示尺寸={{displaySize}},位置={{position}}"
} }
}, },
"dialog": { "dialog": {
@ -279,6 +279,49 @@
}, },
"name": "沉浸模式" "name": "沉浸模式"
}, },
"api-server": {
"description": "添加一个 API 服务器来控制播放器",
"dialog": {
"request": {
"buttons": {
"allow": "允许",
"deny": "拒绝"
},
"message": "允许 {{ID}} {{origin}} 访问该 API 吗?",
"title": "API 授权请求"
}
},
"menu": {
"auth-strategy": {
"label": "授权策略",
"submenu": {
"auth-at-first": {
"label": "首次请求时授权"
},
"none": {
"label": "无需授权"
}
}
},
"hostname": {
"label": "主机名"
},
"port": {
"label": "端口号"
}
},
"name": "API 服务器 [测试]",
"prompt": {
"hostname": {
"label": "请输入 API 服务器的主机名(如 0.0.0.0",
"title": "主机名"
},
"port": {
"label": "请输入 API 服务器的端口号:",
"title": "端口号"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "对音频应用压缩(压低响亮部分,提升柔和部分)", "description": "对音频应用压缩(压低响亮部分,提升柔和部分)",
"name": "音频压缩器" "name": "音频压缩器"
@ -441,6 +484,18 @@
"button": "下载" "button": "下载"
} }
}, },
"equalizer": {
"description": "为播放器添加均衡器",
"menu": {
"presets": {
"label": "预设",
"list": {
"bass-booster": "低音增强器"
}
}
},
"name": "均衡器"
},
"exponential-volume": { "exponential-volume": {
"description": "让音量滑块指数化以便选择更低的音量。", "description": "让音量滑块指数化以便选择更低的音量。",
"name": "指数化音量" "name": "指数化音量"
@ -459,11 +514,11 @@
"lyrics-genius": { "lyrics-genius": {
"description": "为大多数歌曲添加歌词支持", "description": "为大多数歌曲添加歌词支持",
"menu": { "menu": {
"romanized-lyrics": "罗马化字幕" "romanized-lyrics": "罗马化歌词"
}, },
"name": "Genius 歌词", "name": "Genius 歌词",
"renderer": { "renderer": {
"fetched-lyrics": "已从 Genius 获取字幕" "fetched-lyrics": "已从 Genius 获取歌词"
} }
}, },
"music-together": { "music-together": {
@ -478,12 +533,12 @@
}, },
"menu": { "menu": {
"click-to-copy-id": "复制发起者 ID", "click-to-copy-id": "复制发起者 ID",
"close": "关闭 Music Together", "close": "关闭一起听",
"connected-users": "已连接用户", "connected-users": "已连接用户",
"disconnect": "断开 Music Together 连接", "disconnect": "断开一起听连接",
"empty-user": "没有已连接的用户", "empty-user": "没有已连接的用户",
"host": "Music Together 发起者", "host": "一起听发起者",
"join": "加入 Music Together", "join": "加入一起听",
"permission": { "permission": {
"all": "允许来宾控制播放列表与播放器", "all": "允许来宾控制播放列表与播放器",
"host-only": "仅发起人可以控制播放列表与播放器", "host-only": "仅发起人可以控制播放列表与播放器",
@ -496,20 +551,20 @@
"host": "已作为发起人连接" "host": "已作为发起人连接"
} }
}, },
"name": "Music Together [测试]", "name": "一起听 [测试]",
"toast": { "toast": {
"add-song-failed": "添加歌曲失败", "add-song-failed": "添加歌曲失败",
"closed": "Music Together 已关闭", "closed": "一起听已关闭",
"disconnected": "Music Together 已断开连接", "disconnected": "一起听已断开连接",
"host-failed": "发起 Music Together 失败", "host-failed": "发起一起听失败",
"id-copied": "已将发起者 ID 复制到剪切板", "id-copied": "已将发起者 ID 复制到剪切板",
"id-copy-failed": "复制发起者 ID 到剪贴板时失败", "id-copy-failed": "复制发起者 ID 到剪贴板时失败",
"join-failed": "加入 Music Together 失败", "join-failed": "加入一起听失败",
"joined": "已加入 Music Together", "joined": "已加入一起听",
"permission-changed": "Music Together 权限已改为 \"{{permission}}\"", "permission-changed": "一起听权限已改为 \"{{permission}}\"",
"remove-song-failed": "移除歌曲失败", "remove-song-failed": "移除歌曲失败",
"user-connected": "{{name}} 加入了 Music Together", "user-connected": "{{name}} 加入了一起听",
"user-disconnected": "{{name}} 离开了 Music Together" "user-disconnected": "{{name}} 离开了一起听"
} }
}, },
"navigation": { "navigation": {
@ -631,7 +686,7 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "输入您的v ListenBrainz 用户令牌:", "label": "输入您的 ListenBrainz 用户令牌:",
"title": "ListenBrainz 令牌" "title": "ListenBrainz 令牌"
} }
} }

View File

@ -279,6 +279,56 @@
}, },
"name": "微光效果" "name": "微光效果"
}, },
"amuse": {
"description": "加入支援 6K Labs 的 Amuse OBS 外掛以取得 Youtube Music 現正播放資訊",
"name": "Amuse",
"response": {
"query": "Amuse API 伺服器正在運行中,使用 /query 以取得歌曲資訊。"
}
},
"api-server": {
"description": "新增伺服器以使用 API 控制播放器",
"dialog": {
"request": {
"buttons": {
"allow": "允許",
"deny": "拒絕"
},
"message": "允許 {{ID}} ({{origin}}) 訪問 API 嗎?",
"title": "API 驗證請求"
}
},
"menu": {
"auth-strategy": {
"label": "驗證策略",
"submenu": {
"auth-at-first": {
"label": "首次請求時驗證"
},
"none": {
"label": "不要驗證"
}
}
},
"hostname": {
"label": "主機名稱"
},
"port": {
"label": "接口"
}
},
"name": "API 伺服器 [Beta]",
"prompt": {
"hostname": {
"label": "輸入 API 伺服器的主機名稱 例 (0.0.0.0)",
"title": "主機名稱"
},
"port": {
"label": "輸入 API 伺服器接口:",
"title": "接口"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "使用音效壓縮 (大聲部份的音量降低, 柔和部份的音量提高)", "description": "使用音效壓縮 (大聲部份的音量降低, 柔和部份的音量提高)",
"name": "音效壓縮器" "name": "音效壓縮器"
@ -300,7 +350,7 @@
"name": "標題選擇器", "name": "標題選擇器",
"prompt": { "prompt": {
"selector": { "selector": {
"label": "目前標題語言: {{language}}", "label": "目前標題語言{{language}}",
"none": "無", "none": "無",
"title": "選擇標題語言" "title": "選擇標題語言"
} }
@ -363,7 +413,7 @@
"name": "Discord 狀態", "name": "Discord 狀態",
"prompt": { "prompt": {
"set-inactivity-timeout": { "set-inactivity-timeout": {
"label": "設定多少秒後清除狀態:", "label": "設定多少秒後清除狀態",
"title": "設定閒置狀態時長" "title": "設定閒置狀態時長"
} }
} }
@ -388,11 +438,11 @@
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "轉檔進度: {{percent}}%", "conversion-progress": "轉檔進度{{percent}}%",
"converting": "轉檔中…", "converting": "轉檔中…",
"done": "完成下載: {{filePath}}", "done": "完成下載{{filePath}}",
"download-info": "正在下載 {{artist}} - {{title}} [{{videoId}}", "download-info": "正在下載 {{artist}} - {{title}} [{{videoId}}",
"download-progress": "下載進度: {{percent}}%", "download-progress": "下載進度{{percent}}%",
"downloading": "下載中…", "downloading": "下載中…",
"downloading-counter": "正在下載第 {{current}}/{{total}}…", "downloading-counter": "正在下載第 {{current}}/{{total}}…",
"downloading-playlist": "正在下載播放清單 \"{{playlistTitle}}\" - 共 {{playlistSize}} 首歌 ({{playlistId}})", "downloading-playlist": "正在下載播放清單 \"{{playlistTitle}}\" - 共 {{playlistSize}} 首歌 ({{playlistId}})",
@ -403,10 +453,10 @@
"playlist-has-only-one-song": "播放清單內只有一首歌曲, 將直接下載", "playlist-has-only-one-song": "播放清單內只有一首歌曲, 將直接下載",
"playlist-id-not-found": "沒有找到播放清單 ID", "playlist-id-not-found": "沒有找到播放清單 ID",
"playlist-is-empty": "播放清單是空的", "playlist-is-empty": "播放清單是空的",
"playlist-is-mix-or-private": "獲取播放清單資訊時發生錯誤: 請確認非私人播放清單或是\"為你推薦的合輯\"\n\n{{error}}", "playlist-is-mix-or-private": "獲取播放清單資訊時發生錯誤請確認非私人播放清單或是\"為你推薦的合輯\"\n\n{{error}}",
"preparing-file": "正在準備檔案…", "preparing-file": "正在準備檔案…",
"saving": "儲存中…", "saving": "儲存中…",
"trying-to-get-playlist-id": "正在嘗試獲取播放清單 ID: {{playlistId}}", "trying-to-get-playlist-id": "正在嘗試獲取播放清單 ID{{playlistId}}",
"video-id-not-found": "未能找到該影片", "video-id-not-found": "未能找到該影片",
"writing-id3": "正在寫入 ID3 標籤…" "writing-id3": "正在寫入 ID3 標籤…"
} }
@ -441,6 +491,18 @@
"button": "下載" "button": "下載"
} }
}, },
"equalizer": {
"description": "為播放器加入等化器",
"menu": {
"presets": {
"label": "預設格式",
"list": {
"bass-booster": "低音增強器"
}
}
},
"name": "等化器"
},
"exponential-volume": { "exponential-volume": {
"description": "使音量滑桿指數化,以便更容易選擇較低的音量。", "description": "使音量滑桿指數化,以便更容易選擇較低的音量。",
"name": "指數化音量調整" "name": "指數化音量調整"
@ -463,7 +525,7 @@
}, },
"name": "第三方字幕", "name": "第三方字幕",
"renderer": { "renderer": {
"fetched-lyrics": "為Genius獲取字幕" "fetched-lyrics": "為 Genius 獲取字幕"
} }
}, },
"music-together": { "music-together": {
@ -582,7 +644,7 @@
"decrease": "降低音量", "decrease": "降低音量",
"increase": "增加音量" "increase": "增加音量"
}, },
"label": "選擇全域音量控制快捷鍵:", "label": "選擇全域音量控制快捷鍵",
"title": "全域音量控制快捷鍵" "title": "全域音量控制快捷鍵"
}, },
"volume-steps": { "volume-steps": {
@ -595,8 +657,8 @@
"backend": { "backend": {
"dialog": { "dialog": {
"quality-changer": { "quality-changer": {
"detail": "目前畫質: {{quality}}", "detail": "目前畫質{{quality}}",
"message": "選擇影片畫質:", "message": "選擇影片畫質",
"title": "選擇影片畫質" "title": "選擇影片畫質"
} }
} }
@ -651,7 +713,7 @@
"play-pause": "播放/暫停", "play-pause": "播放/暫停",
"previous": "上一首" "previous": "上一首"
}, },
"label": "選擇全域音樂控制快捷鍵:", "label": "選擇全域音樂控制快捷鍵",
"title": "全域快捷鍵" "title": "全域快捷鍵"
} }
} }
@ -671,7 +733,7 @@
"synced-lyrics": { "synced-lyrics": {
"description": "使用 LRClib 等管道提供歌詞同步顯示。", "description": "使用 LRClib 等管道提供歌詞同步顯示。",
"errors": { "errors": {
"fetch": "⚠️擷取歌詞時發生錯誤請稍後再試。", "fetch": "⚠️\t擷取歌詞時發生錯誤\n請稍後再試。",
"not-found": "⚠️未找到該首歌曲的歌詞。" "not-found": "⚠️未找到該首歌曲的歌詞。"
}, },
"menu": { "menu": {
@ -682,6 +744,10 @@
"line-effect": { "line-effect": {
"label": "歌詞顯示效果", "label": "歌詞顯示效果",
"submenu": { "submenu": {
"fancy": {
"label": "絢麗",
"tooltip": "使用較為接近原生樣式並且放大當前該行歌詞"
},
"focus": { "focus": {
"label": "高亮", "label": "高亮",
"tooltip": "高亮當前的歌詞" "tooltip": "高亮當前的歌詞"
@ -703,7 +769,7 @@
}, },
"show-lyrics-even-if-inexact": { "show-lyrics-even-if-inexact": {
"label": "即使不精確依然強制顯示歌詞", "label": "即使不精確依然強制顯示歌詞",
"tooltip": "當找不到符合該歌曲的歌詞時,該功能會嘗試不同的搜尋方式\n使用不同的搜尋方式會導致不精確的結果。" "tooltip": "當找不到符合該歌曲的歌詞時,該功能會嘗試不同的搜尋方式\n使用不同的搜尋方式會導致不精確的結果。"
}, },
"show-time-codes": { "show-time-codes": {
"label": "顯示時間線", "label": "顯示時間線",
@ -718,7 +784,7 @@
"warnings": { "warnings": {
"duration-mismatch": "⚠️歌詞可能會出現不同步的情況。", "duration-mismatch": "⚠️歌詞可能會出現不同步的情況。",
"inexact": "⚠️該歌曲的歌詞可能並不精確", "inexact": "⚠️該歌曲的歌詞可能並不精確",
"instrumental": "⚠️該首歌曲並無人聲" "instrumental": "⚠️該首歌曲為純音樂"
} }
}, },
"taskbar-mediacontrol": { "taskbar-mediacontrol": {

View File

@ -11,6 +11,7 @@ import {
shell, shell,
dialog, dialog,
ipcMain, ipcMain,
protocol,
type BrowserWindowConstructorOptions, type BrowserWindowConstructorOptions,
} from 'electron'; } from 'electron';
import enhanceWebRequest, { import enhanceWebRequest, {
@ -83,6 +84,34 @@ if (!gotTheLock) {
app.exit(); app.exit();
} }
protocol.registerSchemesAsPrivileged([
{
scheme: 'http',
privileges: {
standard: true,
bypassCSP: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
codeCache: true,
},
},
{
scheme: 'https',
privileges: {
standard: true,
bypassCSP: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
codeCache: true,
},
},
{ scheme: 'mailto', privileges: { standard: true } },
]);
// Ozone platform hint: Required for Wayland support // Ozone platform hint: Required for Wayland support
app.commandLine.appendSwitch('ozone-platform-hint', 'auto'); app.commandLine.appendSwitch('ozone-platform-hint', 'auto');
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt) // SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
@ -102,17 +131,21 @@ if (config.get('options.disableHardwareAcceleration')) {
} }
if (is.linux()) { if (is.linux()) {
const disabledFeatures = [ // Overrides WM_CLASS for X11 to correspond to icon filename
// Workaround for issue #2248 app.setName('com.github.th_ch.youtube_music');
'UseMultiPlaneFormatForSoftwareVideo',
]; // Workaround for issue #2248
if (
process.env.XDG_SESSION_TYPE === 'wayland' ||
process.env.WAYLAND_DISPLAY
) {
app.commandLine.appendSwitch('disable-gpu-memory-buffer-video-frames');
}
// Stops chromium from launching its own MPRIS service // Stops chromium from launching its own MPRIS service
if (config.plugins.isEnabled('shortcuts')) { if (config.plugins.isEnabled('shortcuts')) {
disabledFeatures.push('MediaSessionService'); app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
} }
app.commandLine.appendSwitch('disable-features', disabledFeatures.join());
} }
if (config.get('options.proxy')) { if (config.get('options.proxy')) {
@ -472,10 +505,11 @@ app.once('browser-window-created', (_event, win) => {
// User agents are from https://developers.whatismybrowser.com/useragents/explore/ // User agents are from https://developers.whatismybrowser.com/useragents/explore/
const originalUserAgent = win.webContents.userAgent; const originalUserAgent = win.webContents.userAgent;
const userAgents = { const userAgents = {
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0', mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.152 Safari/537.36',
windows: windows:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.152 Safari/537.36',
linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0', linux:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.152 Safari/537.36',
}; };
const updatedUserAgent = is.macOS() const updatedUserAgent = is.macOS()
@ -531,7 +565,11 @@ app.once('browser-window-created', (_event, win) => {
console.log(log); console.log(log);
} }
if (errorCode !== -3) { if (
errorCode !== -3 &&
// Workaround for #2435
!new URL(validatedURL).hostname.includes('doubleclick.net')
) {
// -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(ErrorHtmlAsset);
@ -867,9 +905,21 @@ function removeContentSecurityPolicy(
betterSession.webRequest.onHeadersReceived((details, callback) => { betterSession.webRequest.onHeadersReceived((details, callback) => {
details.responseHeaders ??= {}; details.responseHeaders ??= {};
// Remove the content security policy // prettier-ignore
delete details.responseHeaders['content-security-policy-report-only']; if (new URL(details.url).protocol === 'https:') {
delete details.responseHeaders['content-security-policy']; // Remove the content security policy
delete details.responseHeaders['content-security-policy-report-only'];
delete details.responseHeaders['Content-Security-Policy-Report-Only'];
delete details.responseHeaders['content-security-policy'];
delete details.responseHeaders['Content-Security-Policy'];
if (
!details.responseHeaders['access-control-allow-origin'] &&
!details.responseHeaders['Access-Control-Allow-Origin']
) {
details.responseHeaders['access-control-allow-origin'] = ['https://music.youtube.com'];
}
}
callback({ cancel: false, responseHeaders: details.responseHeaders }); callback({ cancel: false, responseHeaders: details.responseHeaders });
}); });

View File

@ -2,7 +2,7 @@
import path from 'node:path'; import path from 'node:path';
import fs, { promises } from 'node:fs'; import fs, { promises } from 'node:fs';
import { ElectronBlocker } from '@cliqz/adblocker-electron'; import { ElectronBlocker } from '@ghostery/adblocker-electron';
import { app, net } from 'electron'; import { app, net } from 'electron';
const SOURCES = [ const SOURCES = [
@ -55,6 +55,7 @@ export const loadAdBlockerEngine = async (
(url: string) => net.fetch(url), (url: string) => net.fetch(url),
lists, lists,
{ {
enableCompression: true,
// When generating the engine for caching, do not load network filters // When generating the engine for caching, do not load network filters
// So that enhancing the session works as expected // So that enhancing the session works as expected
// Allowing to define multiple webRequest listeners // Allowing to define multiple webRequest listeners
@ -66,7 +67,7 @@ export const loadAdBlockerEngine = async (
blocker.enableBlockingInSession(session); blocker.enableBlockingInSession(session);
} }
} catch (error) { } catch (error) {
console.log('Error loading adBlocker engine', error); console.error('Error loading adBlocker engine', error);
} }
}; };

View File

@ -8,7 +8,6 @@ import {
unloadAdBlockerEngine, unloadAdBlockerEngine,
} from './blocker'; } from './blocker';
import injectCliqzPreload from './injectors/inject-cliqz-preload';
import { inject, isInjected } from './injectors/inject'; import { inject, isInjected } from './injectors/inject';
import { loadAdSpeedup } from './adSpeedup'; import { loadAdSpeedup } from './adSpeedup';
@ -134,18 +133,13 @@ export default createPlugin({
async start({ getConfig }) { async start({ getConfig }) {
const config = await getConfig(); const config = await getConfig();
if (config.blocker === blockers.WithBlocklists) { if (config.blocker === blockers.InPlayer && !isInjected()) {
// Preload adblocker to inject scripts/styles
await injectCliqzPreload();
} else if (config.blocker === blockers.InPlayer && !isInjected()) {
inject(contextBridge); inject(contextBridge);
await webFrame.executeJavaScript(this.script); await webFrame.executeJavaScript(this.script);
} }
}, },
async onConfigChange(newConfig) { async onConfigChange(newConfig) {
if (newConfig.blocker === blockers.WithBlocklists) { if (newConfig.blocker === blockers.InPlayer && !isInjected()) {
await injectCliqzPreload();
} else if (newConfig.blocker === blockers.InPlayer && !isInjected()) {
inject(contextBridge); inject(contextBridge);
await webFrame.executeJavaScript(this.script); await webFrame.executeJavaScript(this.script);
} }

View File

@ -1,3 +1,3 @@
export default async () => { export default async () => {
await import('@cliqz/adblocker-electron-preload'); await import('@ghostery/adblocker-electron-preload');
}; };

View File

@ -133,9 +133,11 @@ export default createPlugin<
} }
}, },
loadFullList(event: MouseEvent) { loadFullList(event: MouseEvent) {
if (event.currentTarget instanceof Element) { if (event.target instanceof Element) {
event.stopPropagation(); event.stopPropagation();
const id = event.currentTarget.id; const button = event.target.closest('button') as HTMLElement;
if (!button?.id) return;
const id = button.id;
const loader = document.getElementById('continuations')!; const loader = document.getElementById('continuations')!;
this.loadObserver = new MutationObserver(() => { this.loadObserver = new MutationObserver(() => {
this.applyToList(id, loader); this.applyToList(id, loader);

View File

@ -34,3 +34,12 @@
margin: 0 auto !important; margin: 0 auto !important;
overflow: visible; overflow: visible;
} }
/* Fix ambient mode overlapping other elements #2520 */
.song-button.ytmusic-av-toggle, .video-button.ytmusic-av-toggle {
z-index: 1;
background-color: transparent;
}
#side-panel.side-panel.ytmusic-player-page {
z-index: 0;
}

View File

@ -0,0 +1,71 @@
import { t } from 'i18next';
import { type Context, Hono } from 'hono';
import { cors } from 'hono/cors';
import { serve } from '@hono/node-server';
import registerCallback, { type SongInfo } from '@/providers/song-info';
import { createBackend } from '@/utils';
import type { AmuseSongInfo } from './types';
const amusePort = 9863;
const formatSongInfo = (info: SongInfo) => {
const formattedSongInfo: AmuseSongInfo = {
player: {
hasSong: !!(info.artist && info.title),
isPaused: info.isPaused ?? false,
seekbarCurrentPosition: info.elapsedSeconds ?? 0,
},
track: {
duration: info.songDuration,
title: info.title,
author: info.artist,
cover: info.imageSrc ?? '',
url: info.url ?? '',
id: info.videoId,
isAdvertisement: false,
},
};
return formattedSongInfo;
};
export default createBackend({
currentSongInfo: {} as SongInfo,
app: null as Hono | null,
server: null as ReturnType<typeof serve> | null,
start() {
registerCallback((songInfo) => {
this.currentSongInfo = songInfo;
});
this.app = new Hono();
this.app.use('*', cors());
this.app.get('/', (ctx) =>
ctx.body(t('plugins.amuse.response.query'), 200),
);
const queryAndApiHandler = (ctx: Context) => {
return ctx.json(formatSongInfo(this.currentSongInfo), 200);
};
this.app.get('/query', queryAndApiHandler);
this.app.get('/api', queryAndApiHandler);
try {
this.server = serve({
fetch: this.app.fetch.bind(this.app),
port: amusePort,
});
} catch (err) {
console.error(err);
}
},
stop() {
if (this.server) {
this.server?.close();
}
},
});

View File

@ -0,0 +1,20 @@
import { createPlugin } from '@/utils';
import backend from './backend';
import { t } from '@/i18n';
export interface MusicWidgetConfig {
enabled: boolean;
}
export const defaultConfig: MusicWidgetConfig = {
enabled: false,
};
export default createPlugin({
name: () => t('plugins.amuse.name'),
description: () => t('plugins.amuse.description'),
addedVersion: '3.7.X',
restartNeeded: true,
config: defaultConfig,
backend,
});

View File

@ -0,0 +1,20 @@
export interface PlayerInfo {
hasSong: boolean;
isPaused: boolean;
seekbarCurrentPosition: number;
}
export interface TrackInfo {
author: string;
title: string;
cover: string;
duration: number;
url: string;
id: string;
isAdvertisement: boolean;
}
export interface AmuseSongInfo {
player: PlayerInfo;
track: TrackInfo;
}

View File

@ -10,18 +10,30 @@ import { createBackend } from '@/utils';
import { JWTPayloadSchema } from './scheme'; import { JWTPayloadSchema } from './scheme';
import { registerAuth, registerControl } from './routes'; import { registerAuth, registerControl } from './routes';
import type { APIServerConfig } from '../config'; import { type APIServerConfig, AuthStrategy } from '../config';
import type { BackendType } from './types'; import type { BackendType } from './types';
import type { RepeatMode } from '@/types/datahost-get-state';
export const backend = createBackend<BackendType, APIServerConfig>({ export const backend = createBackend<BackendType, APIServerConfig>({
async start(ctx) { async start(ctx) {
const config = await ctx.getConfig(); const config = await ctx.getConfig();
this.init(ctx); await this.init(ctx);
registerCallback((songInfo) => { registerCallback((songInfo) => {
this.songInfo = songInfo; this.songInfo = songInfo;
}); });
ctx.ipc.on('ytmd:player-api-loaded', () => {
ctx.ipc.send('ytmd:setup-time-changed-listener');
ctx.ipc.send('ytmd:setup-repeat-changed-listener');
});
ctx.ipc.on(
'ytmd:repeat-changed',
(mode: RepeatMode) => (this.currentRepeatMode = mode),
);
this.run(config.hostname, config.port); this.run(config.hostname, config.port);
}, },
stop() { stop() {
@ -48,18 +60,27 @@ export const backend = createBackend<BackendType, APIServerConfig>({
this.app.use('*', cors()); this.app.use('*', cors());
// for web remote control
this.app.use('*', async (ctx, next) => {
ctx.header('Access-Control-Request-Private-Network', 'true');
await next();
});
// middlewares // middlewares
this.app.use( this.app.use('/api/*', async (ctx, next) => {
'/api/*', if (config.authStrategy !== AuthStrategy.NONE) {
jwt({ return await jwt({
secret: config.secret, secret: config.secret,
}), })(ctx, next);
); }
await next();
});
this.app.use('/api/*', async (ctx, next) => { this.app.use('/api/*', async (ctx, next) => {
const result = await JWTPayloadSchema.spa(await ctx.get('jwtPayload')); const result = await JWTPayloadSchema.spa(await ctx.get('jwtPayload'));
const isAuthorized = const isAuthorized =
result.success && config.authorizedClients.includes(result.data.id); config.authStrategy === AuthStrategy.NONE ||
(result.success && config.authorizedClients.includes(result.data.id));
if (!isAuthorized) { if (!isAuthorized) {
ctx.status(401); ctx.status(401);
return ctx.body('Unauthorized'); return ctx.body('Unauthorized');
@ -69,16 +90,35 @@ export const backend = createBackend<BackendType, APIServerConfig>({
}); });
// routes // routes
registerControl(this.app, ctx, () => this.songInfo); registerControl(
this.app,
ctx,
() => this.songInfo,
() => this.currentRepeatMode,
);
registerAuth(this.app, ctx); registerAuth(this.app, ctx);
// swagger // swagger
this.app.openAPIRegistry.registerComponent(
'securitySchemes',
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
);
this.app.doc('/doc', { this.app.doc('/doc', {
openapi: '3.1.0', openapi: '3.1.0',
info: { info: {
version: '1.0.0', version: '1.0.0',
title: 'Youtube Music API Server', title: 'Youtube Music API Server',
}, },
security: [
{
bearerAuth: [],
},
],
}); });
this.app.get('/swagger', swaggerUI({ url: '/doc' })); this.app.get('/swagger', swaggerUI({ url: '/doc' }));

View File

@ -6,9 +6,9 @@ import { getConnInfo } from '@hono/node-server/conninfo';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { APIServerConfig } from '../../config'; import { type APIServerConfig, AuthStrategy } from '../../config';
import { JWTPayload } from '../scheme';
import type { JWTPayload } from '../scheme';
import type { HonoApp } from '../types'; import type { HonoApp } from '../types';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
@ -18,6 +18,7 @@ const routes = {
path: '/auth/{id}', path: '/auth/{id}',
summary: '', summary: '',
description: '', description: '',
security: [],
request: { request: {
params: z.object({ params: z.object({
id: z.string(), id: z.string(),
@ -51,16 +52,16 @@ export const register = (
if (config.authorizedClients.includes(id)) { if (config.authorizedClients.includes(id)) {
// SKIP CHECK // SKIP CHECK
} else if (config.authStrategy === 'AUTH_AT_FIRST') { } else if (config.authStrategy === AuthStrategy.AUTH_AT_FIRST) {
const result = await dialog.showMessageBox({ const result = await dialog.showMessageBox({
title: t('plugins.api-server.dialog.request.title'), title: t('plugins.api-server.dialog.request.title'),
message: t('plugins.api-server.dialog.request.message', { message: t('plugins.api-server.dialog.request.message', {
origin: getConnInfo(ctx).remote.address, origin: getConnInfo(ctx).remote.address,
id, ID: id,
}), }),
buttons: [ buttons: [
t('plugins.api-server.dialog.request.buttons.allow'), t('plugins.api-server.dialog.request.buttons.allow'),
t('plugins.api-server.dialog.request.deny'), t('plugins.api-server.dialog.request.buttons.deny'),
], ],
defaultId: 1, defaultId: 1,
cancelId: 1, cancelId: 1,
@ -70,7 +71,7 @@ export const register = (
ctx.status(403); ctx.status(403);
return ctx.body(null); return ctx.body(null);
} }
} else if (config.authStrategy === 'NONE') { } else if (config.authStrategy === AuthStrategy.NONE) {
// SKIP CHECK // SKIP CHECK
} }

View File

@ -5,21 +5,28 @@ import { ipcMain } from 'electron';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import { import {
AuthHeadersSchema, AddSongToQueueSchema,
type ResponseSongInfo,
SongInfoSchema,
GoForwardScheme,
GoBackSchema, GoBackSchema,
SwitchRepeatSchema, GoForwardScheme,
SetVolumeSchema, MoveSongInQueueSchema,
QueueParamsSchema,
SearchSchema,
SeekSchema,
SetFullscreenSchema, SetFullscreenSchema,
SetQueueIndexSchema,
SetVolumeSchema,
SongInfoSchema,
SwitchRepeatSchema,
type ResponseSongInfo,
} from '../scheme'; } from '../scheme';
import type { RepeatMode } from '@/types/datahost-get-state';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
import type { APIServerConfig } from '../../config'; import type { APIServerConfig } from '../../config';
import type { HonoApp } from '../types'; import type { HonoApp } from '../types';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal'; import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
import type { Context } from 'hono';
const API_VERSION = 'v1'; const API_VERSION = 'v1';
@ -29,9 +36,6 @@ const routes = {
path: `/api/${API_VERSION}/previous`, path: `/api/${API_VERSION}/previous`,
summary: 'play previous song', summary: 'play previous song',
description: 'Plays the previous song in the queue', description: 'Plays the previous song in the queue',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -43,9 +47,6 @@ const routes = {
path: `/api/${API_VERSION}/next`, path: `/api/${API_VERSION}/next`,
summary: 'play next song', summary: 'play next song',
description: 'Plays the next song in the queue', description: 'Plays the next song in the queue',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -57,9 +58,6 @@ const routes = {
path: `/api/${API_VERSION}/play`, path: `/api/${API_VERSION}/play`,
summary: 'Play', summary: 'Play',
description: 'Change the state of the player to play', description: 'Change the state of the player to play',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -71,9 +69,6 @@ const routes = {
path: `/api/${API_VERSION}/pause`, path: `/api/${API_VERSION}/pause`,
summary: 'Pause', summary: 'Pause',
description: 'Change the state of the player to pause', description: 'Change the state of the player to pause',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -86,9 +81,6 @@ const routes = {
summary: 'Toggle play/pause', summary: 'Toggle play/pause',
description: description:
'Change the state of the player to play if paused, or pause if playing', 'Change the state of the player to play if paused, or pause if playing',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -100,9 +92,6 @@ const routes = {
path: `/api/${API_VERSION}/like`, path: `/api/${API_VERSION}/like`,
summary: 'like song', summary: 'like song',
description: 'Set the current song as liked', description: 'Set the current song as liked',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -114,8 +103,26 @@ const routes = {
path: `/api/${API_VERSION}/dislike`, path: `/api/${API_VERSION}/dislike`,
summary: 'dislike song', summary: 'dislike song',
description: 'Set the current song as disliked', description: 'Set the current song as disliked',
responses: {
204: {
description: 'Success',
},
},
}),
seekTo: createRoute({
method: 'post',
path: `/api/${API_VERSION}/seek-to`,
summary: 'seek',
description: 'Seek to a specific time in the current song',
request: { request: {
headers: AuthHeadersSchema, body: {
description: 'seconds to seek to',
content: {
'application/json': {
schema: SeekSchema,
},
},
},
}, },
responses: { responses: {
204: { 204: {
@ -123,14 +130,12 @@ const routes = {
}, },
}, },
}), }),
goBack: createRoute({ goBack: createRoute({
method: 'post', method: 'post',
path: `/api/${API_VERSION}/go-back`, path: `/api/${API_VERSION}/go-back`,
summary: 'go back', summary: 'go back',
description: 'Move the current song back by a number of seconds', description: 'Move the current song back by a number of seconds',
request: { request: {
headers: AuthHeadersSchema,
body: { body: {
description: 'seconds to go back', description: 'seconds to go back',
content: { content: {
@ -153,7 +158,6 @@ const routes = {
summary: 'go forward', summary: 'go forward',
description: 'Move the current song forward by a number of seconds', description: 'Move the current song forward by a number of seconds',
request: { request: {
headers: AuthHeadersSchema,
body: { body: {
description: 'seconds to go forward', description: 'seconds to go forward',
content: { content: {
@ -175,22 +179,36 @@ const routes = {
path: `/api/${API_VERSION}/shuffle`, path: `/api/${API_VERSION}/shuffle`,
summary: 'shuffle', summary: 'shuffle',
description: 'Shuffle the queue', description: 'Shuffle the queue',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
}, },
}, },
}), }),
repeatMode: createRoute({
method: 'get',
path: `/api/${API_VERSION}/repeat-mode`,
summary: 'get current repeat mode',
description: 'Get the current repeat mode (NONE, ALL, ONE)',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({
mode: z.enum(['ONE', 'NONE', 'ALL']).nullable(),
}),
},
},
},
},
}),
switchRepeat: createRoute({ switchRepeat: createRoute({
method: 'post', method: 'post',
path: `/api/${API_VERSION}/switch-repeat`, path: `/api/${API_VERSION}/switch-repeat`,
summary: 'switch repeat', summary: 'switch repeat',
description: 'Switch the repeat mode', description: 'Switch the repeat mode',
request: { request: {
headers: AuthHeadersSchema,
body: { body: {
description: 'number of times to click the repeat button', description: 'number of times to click the repeat button',
content: { content: {
@ -212,7 +230,6 @@ const routes = {
summary: 'set volume', summary: 'set volume',
description: 'Set the volume of the player', description: 'Set the volume of the player',
request: { request: {
headers: AuthHeadersSchema,
body: { body: {
description: 'volume to set', description: 'volume to set',
content: { content: {
@ -234,7 +251,6 @@ const routes = {
summary: 'set fullscreen', summary: 'set fullscreen',
description: 'Set the fullscreen state of the player', description: 'Set the fullscreen state of the player',
request: { request: {
headers: AuthHeadersSchema,
body: { body: {
description: 'fullscreen state', description: 'fullscreen state',
content: { content: {
@ -255,9 +271,6 @@ const routes = {
path: `/api/${API_VERSION}/toggle-mute`, path: `/api/${API_VERSION}/toggle-mute`,
summary: 'toggle mute', summary: 'toggle mute',
description: 'Toggle the mute state of the player', description: 'Toggle the mute state of the player',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
204: { 204: {
description: 'Success', description: 'Success',
@ -270,9 +283,6 @@ const routes = {
path: `/api/${API_VERSION}/fullscreen`, path: `/api/${API_VERSION}/fullscreen`,
summary: 'get fullscreen state', summary: 'get fullscreen state',
description: 'Get the current fullscreen state', description: 'Get the current fullscreen state',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
200: { 200: {
description: 'Success', description: 'Success',
@ -286,14 +296,12 @@ const routes = {
}, },
}, },
}), }),
queueInfo: createRoute({ oldQueueInfo: createRoute({
deprecated: true,
method: 'get', method: 'get',
path: `/api/${API_VERSION}/queue-info`, path: `/api/${API_VERSION}/queue-info`,
summary: 'get current queue info', summary: 'get current queue info',
description: 'Get the current queue info', description: 'Get the current queue info',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
200: { 200: {
description: 'Success', description: 'Success',
@ -308,14 +316,12 @@ const routes = {
}, },
}, },
}), }),
songInfo: createRoute({ oldSongInfo: createRoute({
deprecated: true,
method: 'get', method: 'get',
path: `/api/${API_VERSION}/song-info`, path: `/api/${API_VERSION}/song-info`,
summary: 'get current song info', summary: 'get current song info',
description: 'Get the current song info', description: 'Get the current song info',
request: {
headers: AuthHeadersSchema,
},
responses: { responses: {
200: { 200: {
description: 'Success', description: 'Success',
@ -330,12 +336,166 @@ const routes = {
}, },
}, },
}), }),
songInfo: createRoute({
method: 'get',
path: `/api/${API_VERSION}/song`,
summary: 'get current song info',
description: 'Get the current song info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: SongInfoSchema,
},
},
},
204: {
description: 'No song info',
},
},
}),
queueInfo: createRoute({
method: 'get',
path: `/api/${API_VERSION}/queue`,
summary: 'get current queue info',
description: 'Get the current queue info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({}),
},
},
},
204: {
description: 'No queue info',
},
},
}),
addSongToQueue: createRoute({
method: 'post',
path: `/api/${API_VERSION}/queue`,
summary: 'add song to queue',
description: 'Add a song to the queue',
request: {
body: {
description: 'video id of the song to add',
content: {
'application/json': {
schema: AddSongToQueueSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
moveSongInQueue: createRoute({
method: 'patch',
path: `/api/${API_VERSION}/queue/{index}`,
summary: 'move song in queue',
description: 'Move a song in the queue',
request: {
params: QueueParamsSchema,
body: {
description: 'index to move the song to',
content: {
'application/json': {
schema: MoveSongInQueueSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
removeSongFromQueue: createRoute({
method: 'delete',
path: `/api/${API_VERSION}/queue/{index}`,
summary: 'remove song from queue',
description: 'Remove a song from the queue',
request: {
params: QueueParamsSchema,
},
responses: {
204: {
description: 'Success',
},
},
}),
setQueueIndex: createRoute({
method: 'patch',
path: `/api/${API_VERSION}/queue`,
summary: 'set queue index',
description: 'Set the current index of the queue',
request: {
body: {
description: 'index to move the song to',
content: {
'application/json': {
schema: SetQueueIndexSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
clearQueue: createRoute({
method: 'delete',
path: `/api/${API_VERSION}/queue`,
summary: 'clear queue',
description: 'Clear the queue',
responses: {
204: {
description: 'Success',
},
},
}),
search: createRoute({
method: 'post',
path: `/api/${API_VERSION}/search`,
summary: 'search for a song',
description: 'search for a song',
request: {
body: {
description: 'search query',
content: {
'application/json': {
schema: SearchSchema,
},
},
},
},
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({}),
},
},
},
},
}),
}; };
export const register = ( export const register = (
app: HonoApp, app: HonoApp,
{ window }: BackendContext<APIServerConfig>, { window }: BackendContext<APIServerConfig>,
songInfoGetter: () => SongInfo | undefined, songInfoGetter: () => SongInfo | undefined,
repeatModeGetter: () => RepeatMode | undefined,
) => { ) => {
const controller = getSongControls(window); const controller = getSongControls(window);
@ -345,8 +505,8 @@ export const register = (
ctx.status(204); ctx.status(204);
return ctx.body(null); return ctx.body(null);
}); });
app.openapi(routes.previous, (ctx) => { app.openapi(routes.next, (ctx) => {
controller.previous(); controller.next();
ctx.status(204); ctx.status(204);
return ctx.body(null); return ctx.body(null);
@ -381,6 +541,13 @@ export const register = (
ctx.status(204); ctx.status(204);
return ctx.body(null); return ctx.body(null);
}); });
app.openapi(routes.seekTo, (ctx) => {
const { seconds } = ctx.req.valid('json');
controller.seekTo(seconds);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.goBack, (ctx) => { app.openapi(routes.goBack, (ctx) => {
const { seconds } = ctx.req.valid('json'); const { seconds } = ctx.req.valid('json');
controller.goBack(seconds); controller.goBack(seconds);
@ -401,6 +568,11 @@ export const register = (
ctx.status(204); ctx.status(204);
return ctx.body(null); return ctx.body(null);
}); });
app.openapi(routes.repeatMode, (ctx) => {
ctx.status(200);
return ctx.json({ mode: repeatModeGetter() ?? null });
});
app.openapi(routes.switchRepeat, (ctx) => { app.openapi(routes.switchRepeat, (ctx) => {
const { iteration } = ctx.req.valid('json'); const { iteration } = ctx.req.valid('json');
controller.switchRepeat(iteration); controller.switchRepeat(iteration);
@ -446,7 +618,26 @@ export const register = (
ctx.status(200); ctx.status(200);
return ctx.json({ state: fullscreen }); return ctx.json({ state: fullscreen });
}); });
app.openapi(routes.queueInfo, async (ctx) => {
const songInfo = (ctx: Context) => {
const info = songInfoGetter();
if (!info) {
ctx.status(204);
return ctx.body(null);
}
const body = { ...info };
delete body.image;
ctx.status(200);
return ctx.json(body satisfies ResponseSongInfo);
};
app.openapi(routes.oldSongInfo, songInfo);
app.openapi(routes.songInfo, songInfo);
// Queue
const queueInfo = async (ctx: Context) => {
const queueResponsePromise = new Promise<QueueResponse>((resolve) => { const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => { ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
return resolve(queue); return resolve(queue);
@ -464,19 +655,50 @@ export const register = (
ctx.status(200); ctx.status(200);
return ctx.json(info); return ctx.json(info);
};
app.openapi(routes.oldQueueInfo, queueInfo);
app.openapi(routes.queueInfo, queueInfo);
app.openapi(routes.addSongToQueue, (ctx) => {
const { videoId } = ctx.req.valid('json');
controller.addSongToQueue(videoId);
ctx.status(204);
return ctx.body(null);
}); });
app.openapi(routes.songInfo, (ctx) => { app.openapi(routes.moveSongInQueue, (ctx) => {
const info = songInfoGetter(); const index = Number(ctx.req.param('index'));
const { toIndex } = ctx.req.valid('json');
controller.moveSongInQueue(index, toIndex);
if (!info) { ctx.status(204);
ctx.status(204); return ctx.body(null);
return ctx.body(null); });
} app.openapi(routes.removeSongFromQueue, (ctx) => {
const index = Number(ctx.req.param('index'));
controller.removeSongFromQueue(index);
const body = { ...info }; ctx.status(204);
delete body.image; return ctx.body(null);
});
app.openapi(routes.setQueueIndex, (ctx) => {
const { index } = ctx.req.valid('json');
controller.setQueueIndex(index);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.clearQueue, (ctx) => {
controller.clearQueue();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.search, async (ctx) => {
const { query } = ctx.req.valid('json');
const response = await controller.search(query);
ctx.status(200); ctx.status(200);
return ctx.json(body satisfies ResponseSongInfo); return ctx.json(response as object);
}); });
}; };

View File

@ -1,11 +1,5 @@
import { z } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi';
export const AuthHeadersSchema = z.object({
authorization: z.string().openapi({
example: 'Bearer token',
}),
});
export type JWTPayload = z.infer<typeof JWTPayloadSchema>; export type JWTPayload = z.infer<typeof JWTPayloadSchema>;
export const JWTPayloadSchema = z.object({ export const JWTPayloadSchema = z.object({
id: z.string(), id: z.string(),

View File

@ -1,7 +1,10 @@
export * from './auth'; export * from './auth';
export * from './song-info'; export * from './song-info';
export * from './seek';
export * from './go-back'; export * from './go-back';
export * from './go-forward'; export * from './go-forward';
export * from './switch-repeat'; export * from './switch-repeat';
export * from './set-volume'; export * from './set-volume';
export * from './set-fullscreen'; export * from './set-fullscreen';
export * from './queue';
export * from './search';

View File

@ -0,0 +1,15 @@
import { z } from '@hono/zod-openapi';
export const QueueParamsSchema = z.object({
index: z.coerce.number().int().nonnegative(),
});
export const AddSongToQueueSchema = z.object({
videoId: z.string(),
});
export const MoveSongInQueueSchema = z.object({
toIndex: z.number(),
});
export const SetQueueIndexSchema = z.object({
index: z.number().int().nonnegative(),
});

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const SearchSchema = z.object({
query: z.string(),
});

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const SeekSchema = z.object({
seconds: z.number(),
});

View File

@ -3,6 +3,7 @@ import { serve } from '@hono/node-server';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
import type { RepeatMode } from '@/types/datahost-get-state';
import type { APIServerConfig } from '../config'; import type { APIServerConfig } from '../config';
export type HonoApp = Hono; export type HonoApp = Hono;
@ -11,8 +12,9 @@ export type BackendType = {
server?: ReturnType<typeof serve>; server?: ReturnType<typeof serve>;
oldConfig?: APIServerConfig; oldConfig?: APIServerConfig;
songInfo?: SongInfo; songInfo?: SongInfo;
currentRepeatMode?: RepeatMode;
init: (ctx: BackendContext<APIServerConfig>) => void; init: (ctx: BackendContext<APIServerConfig>) => Promise<void>;
run: (hostname: string, port: number) => void; run: (hostname: string, port: number) => void;
end: () => void; end: () => void;
}; };

View File

@ -1,8 +1,13 @@
export enum AuthStrategy {
AUTH_AT_FIRST = 'AUTH_AT_FIRST',
NONE = 'NONE',
}
export interface APIServerConfig { export interface APIServerConfig {
enabled: boolean; enabled: boolean;
hostname: string; hostname: string;
port: number; port: number;
authStrategy: 'AUTH_AT_FIRST' | 'NONE'; authStrategy: AuthStrategy;
secret: string; secret: string;
authorizedClients: string[]; authorizedClients: string[];
@ -12,7 +17,7 @@ export const defaultAPIServerConfig: APIServerConfig = {
enabled: true, enabled: true,
hostname: '0.0.0.0', hostname: '0.0.0.0',
port: 26538, port: 26538,
authStrategy: 'AUTH_AT_FIRST', authStrategy: AuthStrategy.AUTH_AT_FIRST,
secret: Date.now().toString(36), secret: Date.now().toString(36),
authorizedClients: [], authorizedClients: [],

View File

@ -3,7 +3,11 @@ import prompt from 'custom-electron-prompt';
import { t } from '@/i18n'; import { t } from '@/i18n';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '@/providers/prompt-options';
import { APIServerConfig, defaultAPIServerConfig } from './config'; import {
type APIServerConfig,
AuthStrategy,
defaultAPIServerConfig,
} from './config';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu'; import type { MenuTemplate } from '@/menu';
@ -74,17 +78,17 @@ export const onMenu = async ({
'plugins.api-server.menu.auth-strategy.submenu.auth-at-first.label', 'plugins.api-server.menu.auth-strategy.submenu.auth-at-first.label',
), ),
type: 'radio', type: 'radio',
checked: config.authStrategy === 'AUTH_AT_FIRST', checked: config.authStrategy === AuthStrategy.AUTH_AT_FIRST,
click() { click() {
setConfig({ ...config, authStrategy: 'AUTH_AT_FIRST' }); setConfig({ ...config, authStrategy: AuthStrategy.AUTH_AT_FIRST });
}, },
}, },
{ {
label: t('plugins.api-server.menu.auth-strategy.submenu.none.label'), label: t('plugins.api-server.menu.auth-strategy.submenu.none.label'),
type: 'radio', type: 'radio',
checked: config.authStrategy === 'NONE', checked: config.authStrategy === AuthStrategy.NONE,
click() { click() {
setConfig({ ...config, authStrategy: 'NONE' }); setConfig({ ...config, authStrategy: AuthStrategy.NONE });
}, },
}, },
], ],

View File

@ -5,7 +5,6 @@
} }
ytmusic-tabs { ytmusic-tabs {
top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px));
backdrop-filter: blur(8px) !important; backdrop-filter: blur(8px) !important;
} }

View File

@ -1,10 +1,13 @@
import { app, dialog, ipcMain } from 'electron'; import { app, dialog } from 'electron';
import { Client as DiscordClient } from '@xhayper/discord-rpc'; import { Client as DiscordClient } from '@xhayper/discord-rpc';
import { dev } from 'electron-is'; import { dev } from 'electron-is';
import { ActivityType, GatewayActivityButton } from 'discord-api-types/v10'; import { ActivityType, GatewayActivityButton } from 'discord-api-types/v10';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, {
type SongInfo,
SongInfoEvent,
} from '@/providers/song-info';
import { createBackend, LoggerPrefix } from '@/utils'; import { createBackend, LoggerPrefix } from '@/utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
@ -107,7 +110,7 @@ export const clear = () => {
}; };
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb); export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
export const isConnected = () => info.rpc !== null; export const isConnected = () => info.rpc?.isConnected;
export const backend = createBackend< export const backend = createBackend<
{ {
@ -243,25 +246,28 @@ export const backend = createBackend<
// If the page is ready, register the callback // If the page is ready, register the callback
ctx.window.once('ready-to-show', () => { ctx.window.once('ready-to-show', () => {
let lastSongInfo: SongInfo;
registerCallback((songInfo) => {
lastSongInfo = songInfo;
if (this.config) this.updateActivity(songInfo, this.config);
});
connect();
let lastSent = Date.now(); let lastSent = Date.now();
ipcMain.on('ytmd:time-changed', (_, t: number) => { registerCallback((songInfo, event) => {
const currentTime = Date.now(); if (event !== SongInfoEvent.TimeChanged) {
// if lastSent is more than 5 seconds ago, send the new time info.lastSongInfo = songInfo;
if (currentTime - lastSent > 5000) { if (this.config) this.updateActivity(songInfo, this.config);
lastSent = currentTime; } else {
if (lastSongInfo) { const currentTime = Date.now();
lastSongInfo.elapsedSeconds = t; // if lastSent is more than 5 seconds ago, send the new time
if (this.config) this.updateActivity(lastSongInfo, this.config); if (currentTime - lastSent > 5000) {
lastSent = currentTime;
if (songInfo) {
info.lastSongInfo = songInfo;
if (this.config) this.updateActivity(songInfo, this.config);
}
} }
} }
}); });
connect();
}); });
ctx.ipc.on('ytmd:player-api-loaded', () =>
ctx.ipc.send('ytmd:setup-time-changed-listener'),
);
app.on('window-all-closed', clear); app.on('window-all-closed', clear);
}, },
stop() { stop() {

View File

@ -30,12 +30,13 @@ import registerCallback, {
getImage, getImage,
MediaType, MediaType,
type SongInfo, type SongInfo,
SongInfoEvent,
} from '@/providers/song-info'; } from '@/providers/song-info';
import { getNetFetchAsFetch } from '@/plugins/utils/main'; import { getNetFetchAsFetch } from '@/plugins/utils/main';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types'; import { DefaultPresetList, type Preset, YoutubeFormatList } from '../types';
import type { DownloaderPluginConfig } from '../index'; import type { DownloaderPluginConfig } from '../index';
@ -62,13 +63,23 @@ let yt: Innertube;
let win: BrowserWindow; let win: BrowserWindow;
let playingUrl: string; let playingUrl: string;
const isYouTubePremium = () =>
win.webContents.executeJavaScript(
'!document.querySelector(\'#endpoint[href="/music_premium"]\')',
) as Promise<boolean>;
const sendError = (error: Error, source?: string) => { const sendError = (error: Error, source?: string) => {
win.setProgressBar(-1); // Close progress bar win.setProgressBar(-1); // Close progress bar
setBadge(0); // Close badge setBadge(0); // Close badge
sendFeedback_(win); // Reset feedback sendFeedback_(win); // Reset feedback
const songNameMessage = source ? `\nin ${source}` : ''; const songNameMessage = source ? `\nin ${source}` : '';
const cause = error.cause ? `\n\n${String(error.cause)}` : ''; const cause = error.cause
? `\n\n${
// eslint-disable-next-line @typescript-eslint/no-base-to-string,@typescript-eslint/restrict-template-expressions
error.cause instanceof Error ? error.cause.toString() : error.cause
}`
: '';
const message = `${error.toString()}${songNameMessage}${cause}`; const message = `${error.toString()}${songNameMessage}${cause}`;
console.error(message); console.error(message);
@ -172,7 +183,14 @@ function downloadSongOnFinishSetup({
let duration: number | undefined; let duration: number | undefined;
let time = 0; let time = 0;
registerCallback((songInfo: SongInfo) => { const defaultDownloadFolder = app.getPath('downloads');
registerCallback((songInfo: SongInfo, event) => {
if (event === SongInfoEvent.TimeChanged) {
const elapsedSeconds = songInfo.elapsedSeconds ?? 0;
if (elapsedSeconds > time) time = elapsedSeconds;
return;
}
if ( if (
!songInfo.isPaused && !songInfo.isPaused &&
songInfo.url !== currentUrl && songInfo.url !== currentUrl &&
@ -185,7 +203,9 @@ function downloadSongOnFinishSetup({
) { ) {
downloadSong( downloadSong(
currentUrl, currentUrl,
config.downloadOnFinish.folder ?? config.downloadFolder, config.downloadOnFinish.folder ??
config.downloadFolder ??
defaultDownloadFolder,
); );
} else if ( } else if (
config.downloadOnFinish.mode === 'percent' && config.downloadOnFinish.mode === 'percent' &&
@ -193,7 +213,9 @@ function downloadSongOnFinishSetup({
) { ) {
downloadSong( downloadSong(
currentUrl, currentUrl,
config.downloadOnFinish.folder ?? config.downloadFolder, config.downloadOnFinish.folder ??
config.downloadFolder ??
defaultDownloadFolder,
); );
} }
} }
@ -207,10 +229,6 @@ function downloadSongOnFinishSetup({
ipcMain.on('ytmd:player-api-loaded', () => { ipcMain.on('ytmd:player-api-loaded', () => {
ipc.send('ytmd:setup-time-changed-listener'); ipc.send('ytmd:setup-time-changed-listener');
}); });
ipcMain.on('ytmd:time-changed', (_, t: number) => {
if (t > time) time = t;
});
} }
async function downloadSongUnsafe( async function downloadSongUnsafe(
@ -300,7 +318,7 @@ async function downloadSongUnsafe(
} }
const downloadOptions: FormatOptions = { const downloadOptions: FormatOptions = {
type: 'audio', // Audio, video or video+audio type: (await isYouTubePremium()) ? 'audio' : 'video+audio', // Audio, video or video+audio
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on. quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
format: 'any', // Media container format format: 'any', // Media container format
}; };
@ -572,20 +590,17 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
return; return;
} }
if ( if (!playlist || !playlist.items || playlist.items.length === 0) {
!playlist ||
!playlist.items ||
playlist.items.length === 0 ||
!playlist.header ||
!('title' in playlist.header)
) {
sendError( sendError(
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')), new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
); );
return; return;
} }
const normalPlaylistTitle = playlist.header?.title?.text; const normalPlaylistTitle =
playlist.header && 'title' in playlist.header
? playlist.header?.title?.text
: undefined;
const playlistTitle = const playlistTitle =
normalPlaylistTitle ?? normalPlaylistTitle ??
playlist.page.contents_memo playlist.page.contents_memo

View File

@ -8,6 +8,8 @@ import { LoggerPrefix } from '@/utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
import { ElementFromHtml } from '../utils/renderer'; import { ElementFromHtml } from '../utils/renderer';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
@ -107,7 +109,10 @@ export const onRendererLoad = ({
ipc.on('downloader-feedback', (feedback: string) => { ipc.on('downloader-feedback', (feedback: string) => {
if (progress) { if (progress) {
progress.innerHTML = feedback || t('plugins.downloader.templates.button'); const targetHtml = feedback || t('plugins.downloader.templates.button');
(progress.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
? defaultTrustedTypePolicy.createHTML(targetHtml)
: targetHtml;
} else { } else {
console.warn( console.warn(
LoggerPrefix, LoggerPrefix,

View File

@ -0,0 +1,81 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
import { MenuContext } from '@/types/contexts';
import { MenuTemplate } from '@/menu';
import { defaultPresets, presetConfigs, Preset, FilterConfig } from './presets';
export type EqualizerPluginConfig = {
enabled: boolean;
filters: FilterConfig[];
presets: { [preset in Preset]: boolean };
};
let appliedFilters: BiquadFilterNode[] = [];
export default createPlugin({
name: () => t('plugins.equalizer.name'),
description: () => t('plugins.equalizer.description'),
restartNeeded: false,
addedVersion: '3.7.X',
config: {
enabled: false,
filters: [],
presets: { 'bass-booster': false },
} as EqualizerPluginConfig,
menu: async ({
getConfig,
setConfig,
}: MenuContext<EqualizerPluginConfig>): Promise<MenuTemplate> => {
const config = await getConfig();
return [
{
label: t('plugins.equalizer.menu.presets.label'),
type: 'submenu',
submenu: defaultPresets.map((preset) => ({
label: t(`plugins.equalizer.menu.presets.list.${preset}`),
type: 'radio',
checked: config.presets[preset],
click() {
setConfig({
presets: { ...config.presets, [preset]: !config.presets[preset] },
});
},
})),
},
];
},
renderer: {
async start({ getConfig }) {
const config = await getConfig();
document.addEventListener(
'ytmd:audio-can-play',
({ detail: { audioSource, audioContext } }) => {
const filtersToApply = config.filters.concat(
defaultPresets
.filter((preset) => config.presets[preset])
.map((preset) => presetConfigs[preset]),
);
filtersToApply.forEach((filter) => {
const biquadFilter = audioContext.createBiquadFilter();
biquadFilter.type = filter.type;
biquadFilter.frequency.value = filter.frequency; // filter frequency in Hz
biquadFilter.Q.value = filter.Q;
biquadFilter.gain.value = filter.gain; // filter gain in dB
audioSource.connect(biquadFilter);
biquadFilter.connect(audioContext.destination);
appliedFilters.push(biquadFilter);
});
},
{ once: true, passive: true },
);
},
stop() {
appliedFilters.forEach((filter) => filter.disconnect());
appliedFilters = [];
},
},
});

View File

@ -0,0 +1,18 @@
export const defaultPresets = ['bass-booster'] as const;
export type Preset = (typeof defaultPresets)[number];
export type FilterConfig = {
type: BiquadFilterType;
frequency: number;
Q: number;
gain: number;
};
export const presetConfigs: Record<Preset, FilterConfig> = {
'bass-booster': {
type: 'lowshelf',
frequency: 80,
Q: 100,
gain: 12.0,
},
};

View File

@ -5,9 +5,12 @@
/* youtube-music style */ /* youtube-music style */
ytmusic-app-layout { ytmusic-app-layout {
overflow: scroll; overflow: auto scroll;
height: calc(100vh - var(--menu-bar-height, 36px)); height: calc(100vh - var(--menu-bar-height, 36px));
margin-top: var(--menu-bar-height, 36px) !important; margin-top: var(--menu-bar-height, 36px) !important;
/* fixes laggy list scrolling in large playlists */
backface-visibility: hidden;
} }
ytmusic-app-layout#layout { ytmusic-app-layout#layout {
--ytmusic-nav-bar-offset: 0px; --ytmusic-nav-bar-offset: 0px;
@ -72,3 +75,8 @@ ytmusic-app-layout ytmusic-player-page[is-mweb-modernization-enabled] .side-pane
html { html {
scrollbar-color: unset; scrollbar-color: unset;
} }
/* fixes scrollbar lagging behind in large playlists */
ytmusic-browse-response .ytmusic-responsive-list-item-renderer {
will-change: transform;
}

View File

@ -30,7 +30,7 @@ export default createPlugin({
config: { config: {
enabled: false, enabled: false,
}, },
backend() { backend({ ipc }) {
const secToMilisec = (t?: number) => const secToMilisec = (t?: number) =>
t ? Math.round(Number(t) * 1e3) : undefined; t ? Math.round(Number(t) * 1e3) : undefined;
const previousStatePaused = null; const previousStatePaused = null;
@ -65,6 +65,10 @@ export default createPlugin({
}); });
}; };
ipc.on('ytmd:player-api-loaded', () =>
ipc.send('ytmd:setup-time-changed-listener'),
);
registerCallback((songInfo) => { registerCallback((songInfo) => {
if (!songInfo.title && !songInfo.artist) { if (!songInfo.title && !songInfo.artist) {
return; return;

View File

@ -2,6 +2,8 @@ import { LoggerPrefix } from '@/utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index'; import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
@ -10,7 +12,7 @@ export const onRendererLoad = ({
ipc: { invoke, on }, ipc: { invoke, on },
}: RendererContext<LyricsGeniusPluginConfig>) => { }: RendererContext<LyricsGeniusPluginConfig>) => {
const setLyrics = (lyricsContainer: Element, lyrics: string | null) => { const setLyrics = (lyricsContainer: Element, lyrics: string | null) => {
lyricsContainer.innerHTML = ` const targetHtml = `
<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics"> <div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
${ ${
lyrics?.replaceAll(/\r\n|\r|\n/g, '<br/>') ?? lyrics?.replaceAll(/\r\n|\r|\n/g, '<br/>') ??
@ -20,6 +22,10 @@ export const onRendererLoad = ({
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"> <yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline">
</yt-formatted-string> </yt-formatted-string>
`; `;
(lyricsContainer.innerHTML as string | TrustedHTML) =
defaultTrustedTypePolicy
? defaultTrustedTypePolicy.createHTML(targetHtml)
: targetHtml;
if (lyrics) { if (lyrics) {
const footer = lyricsContainer.querySelector('.footer'); const footer = lyricsContainer.querySelector('.footer');

View File

@ -18,8 +18,8 @@ export interface Section {
export interface Hit { export interface Hit {
highlights: Highlight[]; highlights: Highlight[];
index: Index; index: ResultType;
type: Index; type: ResultType;
result: Result; result: Result;
} }
@ -35,14 +35,10 @@ export interface Range {
end: number; end: number;
} }
export enum Index { export type ResultType = 'song' | 'album' | 'lyric';
Album = 'album',
Lyric = 'lyric',
Song = 'song',
}
export interface Result { export interface Result {
_type: Index; _type: ResultType;
annotation_count?: number; annotation_count?: number;
api_path: string; api_path: string;
artist_names?: string; artist_names?: string;

View File

@ -612,7 +612,9 @@ export default createPlugin<
const accountData = renderer.data as RawAccountData; const accountData = renderer.data as RawAccountData;
this.me = { this.me = {
handleId: accountData.channelHandle.runs[0].text, handleId:
accountData.channelHandle.runs[0].text ??
accountData.accountName.runs[0].text,
name: accountData.accountName.runs[0].text, name: accountData.accountName.runs[0].text,
thumbnail: accountData.accountPhoto.thumbnails[0].url, thumbnail: accountData.accountPhoto.thumbnails[0].url,
}; };

View File

@ -6,7 +6,7 @@ import { t } from '@/i18n';
import type { ConnectionEventUnion } from '@/plugins/music-together/connection'; import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
import type { Profile, 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'; import type { QueueElement, Store } from '@/types/queue';
const getHeaderPayload = (() => { const getHeaderPayload = (() => {
let payload: { let payload: {
@ -266,7 +266,8 @@ export class Queue {
} }
if (this.originalDispatch) if (this.originalDispatch)
this.queue.queue.store.store.dispatch = this.originalDispatch; this.queue.queue.store.store.dispatch = this
.originalDispatch as Store['dispatch'];
} }
injection() { injection() {
@ -295,7 +296,11 @@ export class Queue {
videoId: it!.videoId, videoId: it!.videoId,
ownerId: this.owner!.id, ownerId: this.owner!.id,
}) satisfies VideoData, }) satisfies VideoData,
event.payload!.items!, (
event.payload! as {
items: QueueItem[];
}
).items,
); );
const index = this._videoList.length + videoList.length - 1; const index = this._videoList.length + videoList.length - 1;
@ -334,7 +339,11 @@ export class Queue {
videoId: it!.videoId, videoId: it!.videoId,
ownerId: this.owner!.id, ownerId: this.owner!.id,
}) satisfies VideoData, }) satisfies VideoData,
event.payload!.items!, (
event.payload! as {
items: QueueItem[];
}
).items,
), ),
}, },
}); });
@ -407,7 +416,13 @@ export class Queue {
}, },
}, },
}; };
this.originalDispatch?.call(fakeContext, event); this.originalDispatch?.call(
fakeContext,
event as {
type: string;
payload?: { items?: QueueItem[] | undefined } | undefined;
},
);
}; };
} }

View File

@ -8,7 +8,10 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac
import { notificationImage, secondsToMinutes, ToastStyles } from './utils'; import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import registerCallback, { SongInfo } from '@/providers/song-info'; import registerCallback, {
type SongInfo,
SongInfoEvent,
} from '@/providers/song-info';
import { changeProtocolHandler } from '@/providers/protocol-handler'; import { changeProtocolHandler } from '@/providers/protocol-handler';
import { setTrayOnClick, setTrayOnDoubleClick } from '@/tray'; import { setTrayOnClick, setTrayOnDoubleClick } from '@/tray';
import { mediaIcons } from '@/types/media-icons'; import { mediaIcons } from '@/types/media-icons';
@ -258,15 +261,14 @@ export default (
let currentSeconds = 0; let currentSeconds = 0;
on('ytmd:player-api-loaded', () => send('ytmd:setup-time-changed-listener')); on('ytmd:player-api-loaded', () => send('ytmd:setup-time-changed-listener'));
on('ytmd:time-changed', (t: number) => {
currentSeconds = t;
});
let savedSongInfo: SongInfo; let savedSongInfo: SongInfo;
let lastUrl: string | undefined; let lastUrl: string | undefined;
// Register songInfoCallback // Register songInfoCallback
registerCallback((songInfo) => { registerCallback((songInfo, event) => {
if (event === SongInfoEvent.TimeChanged) {
currentSeconds = songInfo.elapsedSeconds ?? 0;
}
if (!songInfo.artist && !songInfo.title) { if (!songInfo.artist && !songInfo.title) {
return; return;
} }

View File

@ -5,7 +5,10 @@ import is from 'electron-is';
import { notificationImage } from './utils'; import { notificationImage } from './utils';
import interactive from './interactive'; import interactive from './interactive';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, {
type SongInfo,
SongInfoEvent,
} from '@/providers/song-info';
import type { NotificationsPluginConfig } from './index'; import type { NotificationsPluginConfig } from './index';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
@ -30,8 +33,9 @@ const setup = () => {
let oldNotification: Notification; let oldNotification: Notification;
let currentUrl: string | undefined; let currentUrl: string | undefined;
registerCallback((songInfo: SongInfo) => { registerCallback((songInfo: SongInfo, event) => {
if ( if (
event !== SongInfoEvent.TimeChanged &&
!songInfo.isPaused && !songInfo.isPaused &&
(songInfo.url !== currentUrl || config.unpauseNotification) (songInfo.url !== currentUrl || config.unpauseNotification)
) { ) {

View File

@ -3,6 +3,8 @@ import sliderHTML from './templates/slider.html?raw';
import { getSongMenu } from '@/providers/dom-elements'; import { getSongMenu } from '@/providers/dom-elements';
import { singleton } from '@/providers/decorators'; import { singleton } from '@/providers/decorators';
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
import { ElementFromHtml } from '../utils/renderer'; import { ElementFromHtml } from '../utils/renderer';
const slider = ElementFromHtml(sliderHTML); const slider = ElementFromHtml(sliderHTML);
@ -22,7 +24,11 @@ const updatePlayBackSpeed = () => {
const playbackSpeedElement = document.querySelector('#playback-speed-value'); const playbackSpeedElement = document.querySelector('#playback-speed-value');
if (playbackSpeedElement) { if (playbackSpeedElement) {
playbackSpeedElement.innerHTML = String(playbackSpeed); const targetHtml = String(playbackSpeed);
(playbackSpeedElement.innerHTML as string | TrustedHTML) =
defaultTrustedTypePolicy
? defaultTrustedTypePolicy.createHTML(targetHtml)
: targetHtml;
} }
}; };

View File

@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron';
import registerCallback, { import registerCallback, {
MediaType, MediaType,
type SongInfo, type SongInfo,
SongInfoEvent,
} from '@/providers/song-info'; } from '@/providers/song-info';
import { createBackend } from '@/utils'; import { createBackend } from '@/utils';
@ -70,7 +71,8 @@ export const backend = createBackend<
await this.createSessions(config, setConfig); await this.createSessions(config, setConfig);
this.setConfig = setConfig; this.setConfig = setConfig;
registerCallback((songInfo: SongInfo) => { registerCallback((songInfo: SongInfo, event) => {
if (event === SongInfoEvent.TimeChanged) return;
// Set remove the old scrobble timer // Set remove the old scrobble timer
clearTimeout(scrobbleTimer); clearTimeout(scrobbleTimer);
if (!songInfo.isPaused) { if (!songInfo.isPaused) {

View File

@ -1,20 +1,23 @@
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import MprisPlayer, { import MprisPlayer, {
Track,
LoopStatus,
type PlayBackStatus,
type PlayerOptions,
PLAYBACK_STATUS_STOPPED,
PLAYBACK_STATUS_PAUSED,
PLAYBACK_STATUS_PLAYING,
LOOP_STATUS_NONE, LOOP_STATUS_NONE,
LOOP_STATUS_PLAYLIST, LOOP_STATUS_PLAYLIST,
LOOP_STATUS_TRACK, LOOP_STATUS_TRACK,
LoopStatus,
PLAYBACK_STATUS_PAUSED,
PLAYBACK_STATUS_PLAYING,
PLAYBACK_STATUS_STOPPED,
type PlayBackStatus,
type PlayerOptions,
type Position, type Position,
Track,
} from '@jellybrick/mpris-service'; } from '@jellybrick/mpris-service';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, {
type SongInfo,
SongInfoEvent,
} from '@/providers/song-info';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import config from '@/config'; import config from '@/config';
import { LoggerPrefix } from '@/utils'; import { LoggerPrefix } from '@/utils';
@ -134,10 +137,6 @@ function registerMPRIS(win: BrowserWindow) {
player.seeked(secToMicro(t)); player.seeked(secToMicro(t));
}); });
ipcMain.on('ytmd:time-changed', (_, t: number) => {
player.setPosition(secToMicro(t));
});
ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => { ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
switch (mode) { switch (mode) {
case 'NONE': { case 'NONE': {
@ -319,7 +318,11 @@ function registerMPRIS(win: BrowserWindow) {
} }
}); });
registerCallback((songInfo: SongInfo) => { registerCallback((songInfo: SongInfo, event) => {
if (event === SongInfoEvent.TimeChanged) {
player.setPosition(secToMicro(songInfo.elapsedSeconds ?? 0));
return;
}
if (player) { if (player) {
const data: Track = { const data: Track = {
'mpris:length': secToMicro(songInfo.songDuration), 'mpris:length': secToMicro(songInfo.songDuration),

View File

@ -16,7 +16,7 @@ export default createPlugin<
restartNeeded: false, restartNeeded: false,
renderer: { renderer: {
start() { start() {
waitForElement<HTMLElement>('#dislike-button-renderer').then( waitForElement<HTMLElement>('#like-button-renderer').then(
(dislikeBtn) => { (dislikeBtn) => {
this.observer = new MutationObserver(() => { this.observer = new MutationObserver(() => {
if (dislikeBtn?.getAttribute('like-status') == 'DISLIKE') { if (dislikeBtn?.getAttribute('like-status') == 'DISLIKE') {

View File

@ -10,7 +10,7 @@ import type { SyncedLyricsPluginConfig } from './types';
export default createPlugin({ export default createPlugin({
name: () => t('plugins.synced-lyrics.name'), name: () => t('plugins.synced-lyrics.name'),
description: () => t('plugins.synced-lyrics.description'), description: () => t('plugins.synced-lyrics.description'),
authors: ['Non0reo', 'ArjixWasTaken'], authors: ['Non0reo', 'ArjixWasTaken', 'KimJammer'],
restartNeeded: true, restartNeeded: true,
addedVersion: '3.5.X', addedVersion: '3.5.X',
config: { config: {
@ -19,7 +19,7 @@ export default createPlugin({
showLyricsEvenIfInexact: true, showLyricsEvenIfInexact: true,
showTimeCodes: false, showTimeCodes: false,
defaultTextString: '♪', defaultTextString: '♪',
lineEffect: 'scale', lineEffect: 'fancy',
} satisfies SyncedLyricsPluginConfig, } satisfies SyncedLyricsPluginConfig,
menu, menu,

View File

@ -5,13 +5,10 @@ import { t } from '@/i18n';
import type { MenuContext } from '@/types/contexts'; import type { MenuContext } from '@/types/contexts';
import type { SyncedLyricsPluginConfig } from './types'; import type { SyncedLyricsPluginConfig } from './types';
export const menu = async ({ export const menu = async (
getConfig, ctx: MenuContext<SyncedLyricsPluginConfig>,
setConfig, ): Promise<MenuItemConstructorOptions[]> => {
}: MenuContext<SyncedLyricsPluginConfig>): Promise< const config = await ctx.getConfig();
MenuItemConstructorOptions[]
> => {
const config = await getConfig();
return [ return [
{ {
@ -20,7 +17,7 @@ export const menu = async ({
type: 'checkbox', type: 'checkbox',
checked: config.preciseTiming, checked: config.preciseTiming,
click(item) { click(item) {
setConfig({ ctx.setConfig({
preciseTiming: item.checked, preciseTiming: item.checked,
}); });
}, },
@ -30,6 +27,21 @@ export const menu = async ({
toolTip: t('plugins.synced-lyrics.menu.line-effect.tooltip'), toolTip: t('plugins.synced-lyrics.menu.line-effect.tooltip'),
type: 'submenu', type: 'submenu',
submenu: [ submenu: [
{
label: t(
'plugins.synced-lyrics.menu.line-effect.submenu.fancy.label',
),
toolTip: t(
'plugins.synced-lyrics.menu.line-effect.submenu.fancy.tooltip',
),
type: 'radio',
checked: config.lineEffect === 'fancy',
click() {
ctx.setConfig({
lineEffect: 'fancy',
});
},
},
{ {
label: t( label: t(
'plugins.synced-lyrics.menu.line-effect.submenu.scale.label', 'plugins.synced-lyrics.menu.line-effect.submenu.scale.label',
@ -40,7 +52,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.lineEffect === 'scale', checked: config.lineEffect === 'scale',
click() { click() {
setConfig({ ctx.setConfig({
lineEffect: 'scale', lineEffect: 'scale',
}); });
}, },
@ -55,7 +67,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.lineEffect === 'offset', checked: config.lineEffect === 'offset',
click() { click() {
setConfig({ ctx.setConfig({
lineEffect: 'offset', lineEffect: 'offset',
}); });
}, },
@ -70,7 +82,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.lineEffect === 'focus', checked: config.lineEffect === 'focus',
click() { click() {
setConfig({ ctx.setConfig({
lineEffect: 'focus', lineEffect: 'focus',
}); });
}, },
@ -87,7 +99,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.defaultTextString === '♪', checked: config.defaultTextString === '♪',
click() { click() {
setConfig({ ctx.setConfig({
defaultTextString: '♪', defaultTextString: '♪',
}); });
}, },
@ -97,7 +109,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.defaultTextString === ' ', checked: config.defaultTextString === ' ',
click() { click() {
setConfig({ ctx.setConfig({
defaultTextString: ' ', defaultTextString: ' ',
}); });
}, },
@ -107,7 +119,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.defaultTextString === '...', checked: config.defaultTextString === '...',
click() { click() {
setConfig({ ctx.setConfig({
defaultTextString: '...', defaultTextString: '...',
}); });
}, },
@ -117,7 +129,7 @@ export const menu = async ({
type: 'radio', type: 'radio',
checked: config.defaultTextString === '———', checked: config.defaultTextString === '———',
click() { click() {
setConfig({ ctx.setConfig({
defaultTextString: '———', defaultTextString: '———',
}); });
}, },
@ -130,7 +142,7 @@ export const menu = async ({
type: 'checkbox', type: 'checkbox',
checked: config.showTimeCodes, checked: config.showTimeCodes,
click(item) { click(item) {
setConfig({ ctx.setConfig({
showTimeCodes: item.checked, showTimeCodes: item.checked,
}); });
}, },
@ -143,7 +155,7 @@ export const menu = async ({
type: 'checkbox', type: 'checkbox',
checked: config.showLyricsEvenIfInexact, checked: config.showLyricsEvenIfInexact,
click(item) { click(item) {
setConfig({ ctx.setConfig({
showLyricsEvenIfInexact: item.checked, showLyricsEvenIfInexact: item.checked,
}); });
}, },

View File

@ -0,0 +1,89 @@
interface LRCTag {
tag: string;
value: string;
}
interface LRCLine {
time: string;
timeInMs: number;
duration: number;
text: string;
}
interface LRC {
tags: LRCTag[];
lines: LRCLine[];
}
const tagRegex = /^\[(?<tag>\w+):\s*(?<value>.+?)\s*\]$/;
// prettier-ignore
const lyricRegex = /^\[(?<minutes>\d+):(?<seconds>\d+)\.(?<milliseconds>\d+)\](?<text>.+)$/;
export const LRC = {
parse: (text: string): LRC => {
const lrc: LRC = {
tags: [],
lines: [],
};
let offset = 0;
let previousLine: LRCLine | null = null;
for (const line of text.split('\n')) {
if (!line.trim().startsWith('[')) continue;
const lyric = line.match(lyricRegex)?.groups;
if (!lyric) {
const tag = line.match(tagRegex)?.groups;
if (tag) {
if (tag.tag === 'offset') {
offset = parseInt(tag.value);
continue;
}
lrc.tags.push({
tag: tag.tag,
value: tag.value,
});
}
continue;
}
const { minutes, seconds, milliseconds, text } = lyric;
const timeInMs =
parseInt(minutes) * 60 * 1000 +
parseInt(seconds) * 1000 +
parseInt(milliseconds);
const currentLine: LRCLine = {
time: `${minutes}:${seconds}:${milliseconds}`,
timeInMs,
text: text.trim(),
duration: Infinity,
};
if (previousLine) {
previousLine.duration = timeInMs - previousLine.timeInMs;
}
previousLine = currentLine;
lrc.lines.push(currentLine);
}
for (const line of lrc.lines) {
line.timeInMs += offset;
}
const first = lrc.lines.at(0);
if (first && first.timeInMs > 300) {
lrc.lines.unshift({
time: '0:0:0',
timeInMs: 0,
duration: first.timeInMs,
text: '',
});
}
return lrc;
},
};

View File

@ -0,0 +1,137 @@
import { jaroWinkler } from '@skyra/jaro-winkler';
import { config } from '../renderer/renderer';
import { LRC } from '../parsers/lrc';
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
export class LRCLib implements LyricProvider {
name = 'LRCLib';
baseUrl = 'https://lrclib.net';
async search({
title,
artist,
album,
songDuration,
}: SearchSongInfo): Promise<LyricResult | null> {
let query = new URLSearchParams({
artist_name: artist,
track_name: title,
});
query.set('album_name', album!);
if (query.get('album_name') === 'undefined') {
query.delete('album_name');
}
let url = `${this.baseUrl}/api/search?${query.toString()}`;
let response = await fetch(url);
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
let data = (await response.json()) as LRCLIBSearchResponse;
if (!data || !Array.isArray(data)) {
throw new Error(`Expected an array, instead got ${typeof data}`);
}
if (data.length === 0) {
if (!config()?.showLyricsEvenIfInexact) {
return null;
}
query = new URLSearchParams({ q: title });
url = `${this.baseUrl}/api/search?${query.toString()}`;
response = await fetch(url);
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
data = (await response.json()) as LRCLIBSearchResponse;
if (!Array.isArray(data)) {
throw new Error(`Expected an array, instead got ${typeof data}`);
}
}
const filteredResults = [];
for (const item of data) {
const { artistName } = item;
const artists = artist.split(/[&,]/g).map((i) => i.trim());
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
const permutations = [];
for (const artistA of artists) {
for (const artistB of itemArtists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}
for (const artistA of itemArtists) {
for (const artistB of artists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}
const ratio = Math.max(
...permutations.map(([x, y]) => jaroWinkler(x, y)),
);
if (ratio <= 0.9) continue;
filteredResults.push(item);
}
filteredResults.sort(({ duration: durationA }, { duration: durationB }) => {
const left = Math.abs(durationA - songDuration);
const right = Math.abs(durationB - songDuration);
return left - right;
});
const closestResult = filteredResults[0];
if (!closestResult) {
return null;
}
if (Math.abs(closestResult.duration - songDuration) > 15) {
return null;
}
if (closestResult.instrumental) {
return null;
}
const raw = closestResult.syncedLyrics;
const plain = closestResult.plainLyrics;
if (!raw && !plain) {
return null;
}
return {
title: closestResult.trackName,
artists: closestResult.artistName.split(/[&,]/g),
lines: raw
? LRC.parse(raw).lines.map((l) => ({
...l,
status: 'upcoming' as const,
}))
: undefined,
lyrics: plain,
};
}
}
type LRCLIBSearchResponse = {
id: number;
name: string;
trackName: string;
artistName: string;
albumName: string;
duration: number;
instrumental: boolean;
plainLyrics: string;
syncedLyrics: string;
}[];

View File

@ -0,0 +1,132 @@
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
const preloadedStateRegex = /__PRELOADED_STATE__ = JSON\.parse\('(.*?)'\);/;
const preloadHtmlRegex = /body":{"html":"(.*?)","children"/;
export class LyricsGenius implements LyricProvider {
public name = 'Genius';
public baseUrl = 'https://genius.com';
private domParser = new DOMParser();
// prettier-ignore
async search({ title, artist }: SearchSongInfo): Promise<LyricResult | null> {
const query = new URLSearchParams({
q: `${artist} ${title}`,
page: '1',
per_page: '10',
});
const response = await fetch(`${this.baseUrl}/api/search/song?${query}`);
if (!response.ok) {
return null;
}
const data = (await response.json()) as LyricsGeniusSearch;
const hits = data.response.sections[0].hits;
hits.sort(
({
result: {
title: titleA,
primary_artist: { name: artistA },
},
},
{
result: {
title: titleB,
primary_artist: { name: artistB },
},
}) => {
const pointsA = (titleA === title ? 1 : 0) + (artistA.includes(artist) ? 1 : 0);
const pointsB = (titleB === title ? 1 : 0) + (artistB.includes(artist) ? 1 : 0);
return pointsB - pointsA;
},
);
const closestHit = hits.at(0);
if (!closestHit) {
return null;
}
const { result: { path } } = closestHit;
const html = await fetch(`${this.baseUrl}${path}`).then((res) => res.text());
const doc = this.domParser.parseFromString(html, 'text/html');
const preloadedStateScript = Array.prototype.find.call(doc.querySelectorAll('script'), (script: HTMLScriptElement) => {
return script.textContent?.includes('window.__PRELOADED_STATE__');
}) as HTMLScriptElement;
const preloadedState = preloadedStateScript.textContent?.match(preloadedStateRegex)?.[1]?.replace(/\\"/g, '"');
const lyricsHtml = preloadedState?.match(preloadHtmlRegex)?.[1]
?.replace(/\\\//g, '/')
?.replace(/\\\\/g, '\\')
?.replace(/\\n/g, '\n')
?.replace(/\\'/g, "'")
?.replace(/\\"/g, '"');
if (!lyricsHtml) throw new Error('Failed to extract lyrics from preloaded state.');
const lyricsDoc = this.domParser.parseFromString(lyricsHtml, 'text/html');
const lyrics = lyricsDoc.body.innerText;
if (lyrics.trim().toLowerCase().replace(/[[\]]/g, '') === 'instrumental') return null;
return {
title: closestHit.result.title,
artists: closestHit.result.primary_artists.map(({ name }) => name),
lyrics,
};
}
}
interface LyricsGeniusSearch {
response: Response;
}
interface Response {
sections: Section[];
}
interface Section {
hits: {
highlights: unknown[];
index: string;
type: string;
result: Result;
}[];
}
interface Result {
api_path: string;
artist_names: string;
full_title: string;
id: number;
instrumental: boolean;
path: string;
release_date_components: ReleaseDateComponents;
title: string;
title_with_featured: string;
updated_by_human_at: number;
url: string;
featured_artists: Artist[];
primary_artist: Artist;
primary_artists: Artist[];
}
interface Artist {
api_path: string;
id: number;
image_url: string;
name: string;
slug: string;
url: string;
}
interface ReleaseDateComponents {
year: number;
month: number;
day: number;
}

View File

@ -0,0 +1,110 @@
import { jaroWinkler } from '@skyra/jaro-winkler';
import { LRC } from '../parsers/lrc';
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
const removeNoise = (text: string) => {
return text
.replace(/\[.*?\]/g, '')
.replace(/\(.*?\)/g, '')
.trim()
.replace(/(^[-•])|([-•]$)/g, '')
.trim()
.replace(/\s+by$/, '');
};
export class Megalobiz implements LyricProvider {
public name = 'Megalobiz';
public baseUrl = 'https://www.megalobiz.com';
private domParser = new DOMParser();
// prettier-ignore
async search({ title, artist, songDuration }: SearchSongInfo): Promise<LyricResult | null> {
const query = new URLSearchParams({
qry: `${artist} ${title}`,
});
const response = await fetch(`${this.baseUrl}/search/all?${query}`, {
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
const data = await response.text();
const searchDoc = this.domParser.parseFromString(data, 'text/html');
// prettier-ignore
const searchResults: MegalobizSearchResult[] = Array.prototype.map
.call(searchDoc.querySelectorAll('a.entity_name[href^="/lrc/maker/"][name][title]'),
(anchor: HTMLAnchorElement) => {
const { minutes, seconds, millis } = anchor
.getAttribute('title')!
.match(/\[(?<minutes>\d+):(?<seconds>\d+)\.(?<millis>\d+)\]/)!
.groups!;
let name = anchor.getAttribute('name')!;
const artists = [
removeNoise(name.match(/\(?[Ff]eat\. (.+)\)?/)?.[1] ?? ''),
...(removeNoise(name).match(/(?<artists>.*?) [-•] (?<title>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []),
...(removeNoise(name).match(/(?<title>.*) by (?<artists>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []),
].filter(Boolean);
for (const artist of artists) {
name = name.replace(artist, '');
name = removeNoise(name);
}
if (jaroWinkler(title, name) < 0.8) return null;
return {
title: name,
artists,
href: anchor.getAttribute('href')!,
duration:
parseInt(minutes) * 60 +
parseInt(seconds) +
parseInt(millis) / 1000,
};
},
)
.filter(Boolean);
const sortedResults = searchResults.sort(
({ duration: durationA }, { duration: durationB }) => {
const left = Math.abs(durationA - songDuration);
const right = Math.abs(durationB - songDuration);
return left - right;
},
);
const closestResult = sortedResults[0];
if (!closestResult) return null;
if (Math.abs(closestResult.duration - songDuration) > 15) {
return null;
}
const html = await fetch(`${this.baseUrl}${closestResult.href}`).then((r) => r.text());
const lyricsDoc = this.domParser.parseFromString(html, 'text/html');
const raw = lyricsDoc.querySelector('span[id^="lrc_"][id$="_lyrics"]')?.textContent;
if (!raw) throw new Error('Failed to extract lyrics from page.');
const lyrics = LRC.parse(raw);
return {
title: closestResult.title,
artists: closestResult.artists,
lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' })),
};
}
}
interface MegalobizSearchResult {
title: string;
artists: string[];
href: string;
duration: number;
}

View File

@ -0,0 +1,10 @@
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
export class MusixMatch implements LyricProvider {
name = 'MusixMatch';
baseUrl = 'https://www.musixmatch.com/';
search(_: SearchSongInfo): Promise<LyricResult | null> {
throw new Error('Not implemented');
}
}

View File

@ -0,0 +1,200 @@
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
import type { YouTubeMusicAppElement } from '@/types/youtube-music-app-element';
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};
const client = {
clientName: '26',
clientVersion: '7.01.05',
};
export class YTMusic implements LyricProvider {
public name = 'YTMusic';
public baseUrl = 'https://music.youtube.com/';
// prettier-ignore
public async search(
{ videoId, title, artist }: SearchSongInfo,
): Promise<LyricResult | null> {
const data = await this.fetchNext(videoId);
const { tabs } =
data?.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer
?.watchNextTabbedResultsRenderer ?? {};
if (!Array.isArray(tabs)) return null;
const lyricsTab = tabs.find((it) => {
const pageType = it?.tabRenderer?.endpoint?.browseEndpoint
?.browseEndpointContextSupportedConfigs
?.browseEndpointContextMusicConfig?.pageType;
return pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS';
});
if (!lyricsTab) return null;
const { browseId } = lyricsTab?.tabRenderer?.endpoint?.browseEndpoint ?? {};
if (!browseId) return null;
const { contents } = await this.fetchBrowse(browseId);
if (!contents) return null;
/*
NOTE: Due to the nature of Youtubei, the json responses are not consistent,
this means we have to check for multiple possible paths to get the lyrics.
*/
const syncedLines = contents?.elementRenderer?.newElement?.type
?.componentType?.model?.timedLyricsModel?.lyricsData?.timedLyricsData;
const synced = syncedLines?.length && syncedLines[0]?.cueRange
? syncedLines.map((it) => ({
time: this.millisToTime(parseInt(it.cueRange.startTimeMilliseconds)),
timeInMs: parseInt(it.cueRange.startTimeMilliseconds),
duration: parseInt(it.cueRange.endTimeMilliseconds) -
parseInt(it.cueRange.startTimeMilliseconds),
text: it.lyricLine.trim() === '♪' ? '' : it.lyricLine.trim(),
status: 'upcoming' as const,
}))
: undefined;
const plain = !synced
? syncedLines?.length
? syncedLines.map((it) => it.lyricLine).join('\n')
: contents?.messageRenderer
? contents?.messageRenderer?.text?.runs?.map((it) => it.text).join('\n')
: contents?.sectionListRenderer?.contents?.[0]
?.musicDescriptionShelfRenderer?.description?.runs?.map((it) =>
it.text
)?.join('\n')
: undefined;
if (typeof plain === 'string' && plain === 'Lyrics not available') {
return null;
}
if (synced?.length && synced[0].timeInMs > 300) {
synced.unshift({
duration: 0,
text: '',
time: '00:00.00',
timeInMs: 0,
status: 'upcoming' as const,
});
}
return {
title,
artists: [artist],
lyrics: plain,
lines: synced,
};
}
private millisToTime(millis: number) {
const minutes = Math.floor(millis / 60000);
const seconds = Math.floor((millis - minutes * 60 * 1000) / 1000);
const remaining = (millis - minutes * 60 * 1000 - seconds * 1000) / 10;
return `${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}.${remaining.toString().padStart(2, '0')}`;
}
// RATE LIMITED (2 req per sec)
private PROXIED_ENDPOINT = 'https://ytmbrowseproxy.zvz.be/';
private fetchNext(videoId: string) {
const app = document.querySelector<YouTubeMusicAppElement>('ytmusic-app');
if (!app) return null;
return app.networkManager.fetch('/next?prettyPrint=false', {
videoId,
}) as Promise<NextData>;
}
private fetchBrowse(browseId: string) {
return fetch(this.PROXIED_ENDPOINT + 'browse?prettyPrint=false', {
headers,
method: 'POST',
body: JSON.stringify({
browseId,
context: { client },
}),
}).then((res) => res.json()) as Promise<BrowseData>;
}
}
interface NextData {
contents: {
singleColumnMusicWatchNextResultsRenderer: {
tabbedRenderer: {
watchNextTabbedResultsRenderer: {
tabs: {
tabRenderer: {
endpoint: {
browseEndpoint: {
browseId: string;
browseEndpointContextSupportedConfigs: {
browseEndpointContextMusicConfig: {
pageType: string;
};
};
};
};
};
}[];
};
};
};
};
}
interface BrowseData {
contents: {
elementRenderer: {
newElement: {
type: {
componentType: {
model: {
timedLyricsModel: {
lyricsData: {
timedLyricsData: SyncedLyricLine[];
};
};
};
};
};
};
};
messageRenderer: {
text: PlainLyricsTextRenderer;
};
sectionListRenderer: {
contents: {
musicDescriptionShelfRenderer: {
description: PlainLyricsTextRenderer;
};
}[];
};
};
}
interface SyncedLyricLine {
lyricLine: string;
cueRange: CueRange;
}
interface CueRange {
startTimeMilliseconds: string;
endTimeMilliseconds: string;
}
interface PlainLyricsTextRenderer {
runs: {
text: string;
}[];
}

View File

@ -0,0 +1,189 @@
import { createStore } from 'solid-js/store';
import { createMemo } from 'solid-js';
import { SongInfo } from '@/providers/song-info';
import { LRCLib } from './LRCLib';
import { LyricsGenius } from './LyricsGenius';
import { YTMusic } from './YTMusic';
import { getSongInfo } from '@/providers/song-info-front';
import type { LyricProvider, LyricResult } from '../types';
export const providers = {
YTMusic: new YTMusic(),
LRCLib: new LRCLib(),
LyricsGenius: new LyricsGenius(),
// MusixMatch: new MusixMatch(),
// Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow
} as const;
export type ProviderName = keyof typeof providers;
export const providerNames = Object.keys(providers) as ProviderName[];
export type ProviderState = {
state: 'fetching' | 'done' | 'error';
data: LyricResult | null;
error: Error | null;
};
type LyricsStore = {
provider: ProviderName;
current: ProviderState;
lyrics: Record<ProviderName, ProviderState>;
};
const initialData = () =>
providerNames.reduce(
(acc, name) => {
acc[name] = { state: 'fetching', data: null, error: null };
return acc;
},
{} as LyricsStore['lyrics'],
);
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
provider: providerNames[0],
lyrics: initialData(),
get current(): ProviderState {
return this.lyrics[this.provider];
},
});
export const currentLyrics = createMemo(() => {
const provider = lyricsStore.provider;
return lyricsStore.lyrics[provider];
});
type VideoId = string;
type SearchCacheData = Record<ProviderName, ProviderState>;
interface SearchCache {
state: 'loading' | 'done';
data: SearchCacheData;
}
// TODO: Maybe use localStorage for the cache.
const searchCache = new Map<VideoId, SearchCache>();
export const fetchLyrics = (info: SongInfo) => {
if (searchCache.has(info.videoId)) {
const cache = searchCache.get(info.videoId)!;
if (cache.state === 'loading') {
setTimeout(() => {
fetchLyrics(info);
});
return;
}
if (getSongInfo().videoId === info.videoId) {
setLyricsStore('lyrics', () => {
// weird bug with solid-js
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
});
}
return;
}
const cache: SearchCache = {
state: 'loading',
data: initialData(),
};
searchCache.set(info.videoId, cache);
if (getSongInfo().videoId === info.videoId) {
setLyricsStore('lyrics', () => {
// weird bug with solid-js
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
});
}
const tasks: Promise<void>[] = [];
// prettier-ignore
for (
const [providerName, provider] of Object.entries(providers) as [
ProviderName,
LyricProvider,
][]
) {
const pCache = cache.data[providerName];
tasks.push(
provider
.search(info)
.then((res) => {
pCache.state = 'done';
pCache.data = res;
if (getSongInfo().videoId === info.videoId) {
setLyricsStore('lyrics', (old) => {
return {
...old,
[providerName]: {
state: 'done',
data: res ? { ...res } : null,
error: null,
},
};
});
}
})
.catch((error: Error) => {
pCache.state = 'error';
pCache.error = error;
if (getSongInfo().videoId === info.videoId) {
setLyricsStore('lyrics', (old) => {
return {
...old,
[providerName]: { state: 'error', error, data: null },
};
});
}
}),
);
}
Promise.allSettled(tasks).then(() => {
cache.state = 'done';
searchCache.set(info.videoId, cache);
});
};
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
setLyricsStore('lyrics', (old) => {
const pCache = {
state: 'fetching',
data: null,
error: null,
};
return {
...old,
[provider]: pCache,
};
});
providers[provider]
.search(info)
.then((res) => {
setLyricsStore('lyrics', (old) => {
return {
...old,
[provider]: { state: 'done', data: res, error: null },
};
});
})
.catch((error) => {
setLyricsStore('lyrics', (old) => {
return {
...old,
[provider]: { state: 'error', data: null, error },
};
});
});
};

View File

@ -0,0 +1,64 @@
import { t } from '@/i18n';
import { getSongInfo } from '@/providers/song-info-front';
import { lyricsStore, retrySearch } from '../../providers';
interface ErrorDisplayProps {
error: Error;
}
// prettier-ignore
export const ErrorDisplay = (props: ErrorDisplayProps) => {
return (
<div style={{ 'margin-bottom': '5%' }}>
<pre
style={{
'background-color': 'var(--ytmusic-color-black1)',
'border-radius': '8px',
'color': '#58f000',
'max-width': '100%',
'margin-top': '1em',
'margin-bottom': '0',
'padding': '0.5em',
'font-family': 'serif',
'font-size': 'large',
}}
>
{t('plugins.synced-lyrics.errors.fetch')}
</pre>
<pre
style={{
'background-color': 'var(--ytmusic-color-black1)',
'border-radius': '8px',
'color': '#f0a500',
'white-space': 'pre',
'overflow-x': 'auto',
'max-width': '100%',
'margin-top': '0.5em',
'padding': '0.5em',
'font-family': 'monospace',
'font-size': 'large',
}}
>
{props.error.stack}
</pre>
<yt-button-renderer
onClick={() => retrySearch(lyricsStore.provider, getSongInfo())}
data={{
icon: { iconType: 'REFRESH' },
isDisabled: false,
style: 'STYLE_DEFAULT',
text: {
simpleText: t('plugins.synced-lyrics.refetch-btn.normal')
},
}}
style={{
'margin-top': '1em',
'width': '100%'
}}
/>
</div>
);
};

View File

@ -0,0 +1,33 @@
import { createSignal, onMount } from 'solid-js';
const states = [
'(>_<)',
'{ (>_<) }',
'{{ (>_<) }}',
'{{{ (>_<) }}}',
'{{ (>_<) }}',
'{ (>_<) }',
];
export const LoadingKaomoji = () => {
const [counter, setCounter] = createSignal(0);
onMount(() => {
const interval = setInterval(() => setCounter((old) => old + 1), 500);
return () => clearInterval(interval);
});
return (
<yt-formatted-string
class="text-lyrics description ytmusic-description-shelf-renderer"
style={{
'display': 'inline-flex',
'justify-content': 'center',
'width': '100%',
'user-select': 'none',
}}
text={{
runs: [{ text: states[counter() % states.length] }],
}}
/>
);
};

View File

@ -2,145 +2,54 @@ import { createSignal, For, Match, Show, Switch } from 'solid-js';
import { SyncedLine } from './SyncedLine'; import { SyncedLine } from './SyncedLine';
import { t } from '@/i18n'; import { ErrorDisplay } from './ErrorDisplay';
import { getSongInfo } from '@/providers/song-info-front'; import { LoadingKaomoji } from './LoadingKaomoji';
import { PlainLyrics } from './PlainLyrics';
import { import { currentLyrics, lyricsStore } from '../../providers';
differentDuration,
hadSecondAttempt,
isFetching,
isInstrumental,
makeLyricsRequest,
} from '../lyrics/fetch';
import type { LineLyrics } from '../../types';
export const [debugInfo, setDebugInfo] = createSignal<string>(); export const [debugInfo, setDebugInfo] = createSignal<string>();
export const [lineLyrics, setLineLyrics] = createSignal<LineLyrics[]>([]);
export const [currentTime, setCurrentTime] = createSignal<number>(-1); export const [currentTime, setCurrentTime] = createSignal<number>(-1);
// prettier-ignore
export const LyricsContainer = () => { export const LyricsContainer = () => {
const [error, setError] = createSignal('');
const onRefetch = async () => {
if (isFetching()) return;
setError('');
const info = getSongInfo();
await makeLyricsRequest(info).catch((err) => {
setError(String(err));
});
};
return ( return (
<div class={'lyric-container'}> <div class="lyric-container">
<Switch> <Switch>
<Match when={isFetching()}> <Match when={currentLyrics()?.state === 'fetching'}>
<div style="margin-bottom: 8px;"> <LoadingKaomoji />
<tp-yt-paper-spinner-lite
active
class="loading-indicator style-scope"
/>
</div>
</Match> </Match>
<Match when={error()}> <Match when={!currentLyrics().data?.lines && !currentLyrics().data?.lyrics}>
<yt-formatted-string <yt-formatted-string
class="warning-lyrics description ytmusic-description-shelf-renderer" class="text-lyrics description ytmusic-description-shelf-renderer"
style={{
'display': 'inline-flex',
'justify-content': 'center',
'width': '100%',
'user-select': 'none',
}}
text={{ text={{
runs: [ runs: [{ text: '(_)' }],
{
text: t('plugins.synced-lyrics.errors.fetch'),
},
],
}} }}
/> />
</Match> </Match>
</Switch> </Switch>
<Show when={lyricsStore.current.error}>
<ErrorDisplay error={lyricsStore.current.error!} />
</Show>
<Switch> <Switch>
<Match when={!lineLyrics().length}> <Match when={currentLyrics().data?.lines}>
<Show <For each={currentLyrics().data?.lines}>
when={isInstrumental()} {(item) => <SyncedLine line={item} />}
fallback={ </For>
<>
<yt-formatted-string
class="warning-lyrics description ytmusic-description-shelf-renderer"
text={{
runs: [
{
text: t('plugins.synced-lyrics.errors.not-found'),
},
],
}}
style={'margin-bottom: 16px;'}
/>
<yt-button-renderer
disabled={isFetching()}
data={{
icon: { iconType: 'REFRESH' },
isDisabled: false,
style: 'STYLE_DEFAULT',
text: {
simpleText: isFetching()
? t('plugins.synced-lyrics.refetch-btn.fetching')
: t('plugins.synced-lyrics.refetch-btn.normal'),
},
}}
onClick={onRefetch}
/>
</>
}
>
<yt-formatted-string
class="warning-lyrics description ytmusic-description-shelf-renderer"
text={{
runs: [
{
text: t('plugins.synced-lyrics.warnings.instrumental'),
},
],
}}
/>
</Show>
</Match> </Match>
<Match when={lineLyrics().length && !hadSecondAttempt()}>
<yt-formatted-string <Match when={currentLyrics().data?.lyrics}>
class="warning-lyrics description ytmusic-description-shelf-renderer" <PlainLyrics lyrics={currentLyrics().data?.lyrics!} />
text={{
runs: [
{
text: t('plugins.synced-lyrics.warnings.inexact'),
},
],
}}
/>
</Match>
<Match when={lineLyrics().length && !differentDuration()}>
<yt-formatted-string
class="warning-lyrics description ytmusic-description-shelf-renderer"
text={{
runs: [
{
text: t('plugins.synced-lyrics.warnings.duration-mismatch'),
},
],
}}
/>
</Match> </Match>
</Switch> </Switch>
<For each={lineLyrics()}>{(item) => <SyncedLine line={item} />}</For>
<yt-formatted-string
class="footer style-scope ytmusic-description-shelf-renderer"
text={{
runs: [
{
text: 'Source: LRCLIB',
},
],
}}
/>
</div> </div>
); );
}; };

View File

@ -0,0 +1,198 @@
import {
createEffect,
createMemo,
createSignal,
For,
Index,
Match,
onMount,
Switch,
} from 'solid-js';
import {
currentLyrics,
lyricsStore,
ProviderName,
providerNames,
ProviderState,
setLyricsStore,
} from '../../providers';
import { _ytAPI } from '../index';
import type { YtIcons } from '@/types/icons';
export const providerIdx = createMemo(() =>
providerNames.indexOf(lyricsStore.provider),
);
const shouldSwitchProvider = (providerData: ProviderState) => {
if (providerData.state === 'error') return true;
if (providerData.state === 'fetching') return true;
return (
providerData.state === 'done' &&
!providerData.data?.lines &&
!providerData.data?.lyrics
);
};
const providerBias = (p: ProviderName) =>
(lyricsStore.lyrics[p].state === 'done' ? 1 : -1) +
(lyricsStore.lyrics[p].data?.lines?.length ? 2 : -1) +
(lyricsStore.lyrics[p].data?.lines?.length && p === 'YTMusic' ? 1 : 0) +
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
// prettier-ignore
const pickBestProvider = () => {
const providers = Array.from(providerNames);
providers.sort((a, b) => providerBias(b) - providerBias(a));
return providers[0];
};
// prettier-ignore
export const LyricsPicker = () => {
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] = createSignal(false);
createEffect(() => {
// fallback to the next source, if the current one has an error
if (!hasManuallySwitchedProvider()
) {
const bestProvider = pickBestProvider();
const allProvidersFailed = providerNames.every((p) => shouldSwitchProvider(lyricsStore.lyrics[p]));
if (allProvidersFailed) return;
if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
setLyricsStore('provider', bestProvider);
}
}
});
onMount(() => {
const listener = (name: string) => {
if (name !== 'dataloaded') return;
setHasManuallySwitchedProvider(false);
};
_ytAPI?.addEventListener('videodatachange', listener);
return () => _ytAPI?.removeEventListener('videodatachange', listener);
});
const next = (automatic: boolean = false) => {
if (!automatic) setHasManuallySwitchedProvider(true);
setLyricsStore('provider', (prevProvider) => {
const idx = providerNames.indexOf(prevProvider);
return providerNames[(idx + 1) % providerNames.length];
});
};
const previous = (automatic: boolean = false) => {
if (!automatic) setHasManuallySwitchedProvider(true);
setLyricsStore('provider', (prevProvider) => {
const idx = providerNames.indexOf(prevProvider);
return providerNames[(idx + providerNames.length - 1) % providerNames.length];
});
};
const chevronLeft: YtIcons = 'yt-icons:chevron_left';
const chevronRight: YtIcons = 'yt-icons:chevron_right';
const successIcon: YtIcons = 'yt-icons:check-circle';
const errorIcon: YtIcons = 'yt-icons:error';
const notFoundIcon: YtIcons = 'yt-icons:warning';
return (
<div class="lyrics-picker">
<div class="lyrics-picker-left">
<tp-yt-paper-icon-button icon={chevronLeft} onClick={() => previous()} />
</div>
<div class="lyrics-picker-content">
<div class="lyrics-picker-content-label">
<Index each={providerNames}>
{(provider) => (
<div
class="lyrics-picker-item"
tabindex="-1"
style={{
transform: `translateX(${providerIdx() * -100 - 5}%)`,
}}
>
<Switch>
<Match
when={
// prettier-ignore
currentLyrics().state === 'fetching'
}
>
<tp-yt-paper-spinner-lite
active
tabindex="-1"
class="loading-indicator style-scope"
style={{ padding: '5px', transform: 'scale(0.5)' }}
/>
</Match>
<Match when={currentLyrics().state === 'error'}>
<tp-yt-paper-icon-button
icon={errorIcon}
tabindex="-1"
style={{ padding: '5px', transform: 'scale(0.5)' }}
/>
</Match>
<Match
when={
currentLyrics().state === 'done' &&
(currentLyrics().data?.lines ||
currentLyrics().data?.lyrics)
}
>
<tp-yt-paper-icon-button
icon={successIcon}
tabindex="-1"
style={{ padding: '5px', transform: 'scale(0.5)' }}
/>
</Match>
<Match when={
currentLyrics().state === 'done'
&& !currentLyrics().data?.lines
&& !currentLyrics().data?.lyrics
}>
<tp-yt-paper-icon-button
icon={notFoundIcon}
tabindex="-1"
style={{ padding: '5px', transform: 'scale(0.5)' }}
/>
</Match>
</Switch>
<yt-formatted-string
class="description ytmusic-description-shelf-renderer"
text={{ runs: [{ text: provider() }] }}
/>
</div>
)}
</Index>
</div>
<ul class="lyrics-picker-content-dots">
<For each={providerNames}>
{(_, idx) => (
<li
class="lyrics-picker-dot"
onClick={() => setLyricsStore('provider', providerNames[idx()])}
style={{
background: idx() === providerIdx() ? 'white' : 'black',
}}
/>
)}
</For>
</ul>
</div>
<div class="lyrics-picker-right">
<tp-yt-paper-icon-button icon={chevronRight} onClick={() => next()} />
</div>
</div>
);
};

View File

@ -0,0 +1,30 @@
import { createMemo, For } from 'solid-js';
interface PlainLyricsProps {
lyrics: string;
}
export const PlainLyrics = (props: PlainLyricsProps) => {
const lines = createMemo(() => props.lyrics.split('\n'));
return (
<div class="plain-lyrics">
<For each={lines()}>
{(line) => {
if (line.trim() === '') {
return <br />;
} else {
return (
<yt-formatted-string
class="text-lyrics description ytmusic-description-shelf-renderer"
text={{
runs: [{ text: line }],
}}
/>
);
}
}}
</For>
</div>
);
};

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo } from 'solid-js'; import { createEffect, createMemo, For } from 'solid-js';
import { currentTime } from './LyricsContainer'; import { currentTime } from './LyricsContainer';
@ -20,34 +20,63 @@ export const SyncedLine = ({ line }: SyncedLineProps) => {
return 'current'; return 'current';
}); });
let ref: HTMLDivElement; let ref: HTMLDivElement | undefined;
createEffect(() => { createEffect(() => {
if (status() === 'current') { if (status() === 'current') {
ref.scrollIntoView({ behavior: 'smooth', block: 'center' }); ref?.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
}); });
const text = createMemo(() => {
if (line.text.trim()) return line.text;
return config()?.defaultTextString ?? '';
});
if (!text()) {
return (
<yt-formatted-string
text={{
runs: [{ text: '' }],
}}
/>
);
}
return ( return (
<div <div
ref={ref!} ref={ref}
class={`synced-line ${status()}`} class={`synced-line ${status()}`}
onClick={() => { onClick={() => {
_ytAPI?.seekTo(line.timeInMs / 1000); _ytAPI?.seekTo(line.timeInMs / 1000);
}} }}
> >
<yt-formatted-string <div class="text-lyrics description ytmusic-description-shelf-renderer">
class="text-lyrics description ytmusic-description-shelf-renderer" <yt-formatted-string
text={{ text={{
runs: [ runs: [{ text: config()?.showTimeCodes ? `[${line.time}] ` : '' }],
{ }}
text: '', />
},
{ <For each={text().split(' ')}>
text: `${config()?.showTimeCodes ? `[${line.time}] ` : ''}${line.text}`, {(word, index) => {
}, return (
], <span
}} style={{
/> 'transition-delay': `${index() * 0.05}s`,
'animation-delay': `${index() * 0.05}s`,
'--lyrics-duration:': `${line.duration / 1000}s;`,
}}
>
<yt-formatted-string
text={{
runs: [{ text: `${word} ` }],
}}
/>
</span>
);
}}
</For>
</div>
</div> </div>
); );
}; };

View File

@ -1,15 +1,15 @@
import { createRenderer } from '@/utils'; import { createRenderer } from '@/utils';
import { waitForElement } from '@/utils/wait-for-element'; import { waitForElement } from '@/utils/wait-for-element';
import { makeLyricsRequest } from './lyrics';
import { selectors, tabStates } from './utils'; import { selectors, tabStates } from './utils';
import { setConfig } from './renderer'; import { setConfig } from './renderer';
import { setCurrentTime } from './components/LyricsContainer'; import { setCurrentTime } from './components/LyricsContainer';
import { fetchLyrics } from '../providers';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { YoutubePlayer } from '@/types/youtube-player'; import type { YoutubePlayer } from '@/types/youtube-player';
import type { SongInfo } from '@/providers/song-info'; import type { SongInfo } from '@/providers/song-info';
import type { SyncedLyricsPluginConfig } from '../types'; import type { SyncedLyricsPluginConfig } from '../types';
export let _ytAPI: YoutubePlayer | null = null; export let _ytAPI: YoutubePlayer | null = null;
@ -36,9 +36,7 @@ export const renderer = createRenderer<
header.removeAttribute('disabled'); header.removeAttribute('disabled');
break; break;
case 'aria-selected': case 'aria-selected':
tabStates[header.ariaSelected as 'true' | 'false']?.( tabStates[header.ariaSelected ?? 'false']();
_ytAPI?.getVideoData(),
);
break; break;
} }
} }
@ -51,7 +49,6 @@ export const renderer = createRenderer<
await this.videoDataChange(); await this.videoDataChange();
}, },
async videoDataChange() { async videoDataChange() {
if (!this.updateTimestampInterval) { if (!this.updateTimestampInterval) {
this.updateTimestampInterval = setInterval( this.updateTimestampInterval = setInterval(
@ -60,12 +57,17 @@ export const renderer = createRenderer<
); );
} }
// prettier-ignore
this.observer ??= new MutationObserver(this.observerCallback); this.observer ??= new MutationObserver(this.observerCallback);
// Force the lyrics tab to be enabled at all times.
this.observer.disconnect(); this.observer.disconnect();
// Force the lyrics tab to be enabled at all times.
const header = await waitForElement<HTMLElement>(selectors.head); const header = await waitForElement<HTMLElement>(selectors.head);
{
header.removeAttribute('disabled');
tabStates[header.ariaSelected ?? 'false']();
}
this.observer.observe(header, { attributes: true }); this.observer.observe(header, { attributes: true });
header.removeAttribute('disabled'); header.removeAttribute('disabled');
}, },
@ -73,8 +75,8 @@ export const renderer = createRenderer<
async start(ctx: RendererContext<SyncedLyricsPluginConfig>) { async start(ctx: RendererContext<SyncedLyricsPluginConfig>) {
setConfig(await ctx.getConfig()); setConfig(await ctx.getConfig());
ctx.ipc.on('ytmd:update-song-info', async (info: SongInfo) => { ctx.ipc.on('ytmd:update-song-info', (info: SongInfo) => {
await makeLyricsRequest(info); fetchLyrics(info);
}); });
}, },
}); });

View File

@ -1,198 +0,0 @@
import { createSignal } from 'solid-js';
import { jaroWinkler } from '@skyra/jaro-winkler';
import { config } from '../renderer';
import { setDebugInfo, setLineLyrics } from '../components/LyricsContainer';
import type { SongInfo } from '@/providers/song-info';
import type { LineLyrics, LRCLIBSearchResponse } from '../../types';
export const [isInstrumental, setIsInstrumental] = createSignal(false);
export const [isFetching, setIsFetching] = createSignal(false);
export const [hadSecondAttempt, setHadSecondAttempt] = createSignal(false);
export const [differentDuration, setDifferentDuration] = createSignal(false);
export const extractTimeAndText = (
line: string,
index: number,
): LineLyrics | null => {
const groups = /\[(\d+):(\d+)\.(\d+)](.+)/.exec(line);
if (!groups) return null;
const [, rMinutes, rSeconds, rMillis, text] = groups;
const [minutes, seconds, millis] = [
parseInt(rMinutes),
parseInt(rSeconds),
parseInt(rMillis),
];
const timeInMs = minutes * 60 * 1000 + seconds * 1000 + millis;
return {
index,
timeInMs,
time: `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${millis}`,
text: text?.trim() || config()!.defaultTextString,
status: 'upcoming',
duration: 0,
};
};
export const makeLyricsRequest = async (extractedSongInfo: SongInfo) => {
setIsFetching(true);
setLineLyrics([]);
const songData: Parameters<typeof getLyricsList>[0] = {
title: `${extractedSongInfo.title}`,
artist: `${extractedSongInfo.artist}`,
songDuration: extractedSongInfo.songDuration,
};
if (extractedSongInfo.album) {
songData.album = extractedSongInfo.album;
}
let lyrics;
try {
lyrics = await getLyricsList(songData);
} catch {}
setLineLyrics(lyrics ?? []);
setIsFetching(false);
};
export const getLyricsList = async (
songData: Pick<SongInfo, 'title' | 'artist' | 'album' | 'songDuration'>,
): Promise<LineLyrics[] | null> => {
setIsInstrumental(false);
setHadSecondAttempt(false);
setDifferentDuration(false);
setDebugInfo('Searching for lyrics...');
let query = new URLSearchParams({
artist_name: songData.artist,
track_name: songData.title,
});
if (songData.album) {
query.set('album_name', songData.album);
}
let url = `https://lrclib.net/api/search?${query.toString()}`;
let response = await fetch(url);
if (!response.ok) {
setDebugInfo('Got non-OK response from server.');
return null;
}
let data = (await response.json()) as LRCLIBSearchResponse;
if (!data || !Array.isArray(data)) {
setDebugInfo('Unexpected server response.');
return null;
}
// Note: If no lyrics are found, try again with a different search query
if (data.length === 0) {
if (!config()?.showLyricsEvenIfInexact) {
return null;
}
query = new URLSearchParams({ q: songData.title });
url = `https://lrclib.net/api/search?${query.toString()}`;
response = await fetch(url);
if (!response.ok) {
setDebugInfo('Got non-OK response from server. (2)');
return null;
}
data = (await response.json()) as LRCLIBSearchResponse;
if (!Array.isArray(data)) {
setDebugInfo('Unexpected server response. (2)');
return null;
}
setHadSecondAttempt(true);
}
const filteredResults: LRCLIBSearchResponse = [];
for (const item of data) {
const { artist } = songData;
const { artistName } = item;
const artists = artist.split(/[&,]/g).map((i) => i.trim());
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
const permutations = artists.flatMap((artistA) =>
itemArtists.map((artistB) => [
artistA.toLowerCase(),
artistB.toLowerCase(),
]),
);
const ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y)));
if (ratio > 0.9) filteredResults.push(item);
}
const duration = songData.songDuration;
filteredResults.sort(({ duration: durationA }, { duration: durationB }) => {
const left = Math.abs(durationA - duration);
const right = Math.abs(durationB - duration);
return left - right;
});
const closestResult = filteredResults[0];
if (!closestResult) {
setDebugInfo('No search result matched the criteria.');
return null;
}
setDebugInfo(JSON.stringify(closestResult, null, 4));
if (Math.abs(closestResult.duration - duration) > 15) {
return null;
}
if (Math.abs(closestResult.duration - duration) > 5) {
// show message that the timings may be wrong
setDifferentDuration(true);
}
setIsInstrumental(closestResult.instrumental);
if (closestResult.instrumental) {
return null;
}
// Separate the lyrics into lines
const raw = closestResult.syncedLyrics?.split('\n') ?? [];
if (!raw.length) {
return null;
}
// Add a blank line at the beginning
raw.unshift('[0:0.0] ');
const syncedLyricList = raw.reduce<LineLyrics[]>((acc, line) => {
const syncedLine = extractTimeAndText(line, acc.length);
if (syncedLine) {
acc.push(syncedLine);
}
return acc;
}, []);
for (const line of syncedLyricList) {
const next = syncedLyricList[line.index + 1];
if (!next) {
line.duration = Infinity;
break;
}
line.duration = next.timeInMs - line.timeInMs;
}
return syncedLyricList;
};

View File

@ -1,44 +0,0 @@
import { createEffect } from 'solid-js';
import { config } from '../renderer';
export { makeLyricsRequest } from './fetch';
createEffect(() => {
if (!config()?.enabled) return;
const root = document.documentElement;
// Set the line effect
switch (config()?.lineEffect) {
case 'scale':
root.style.setProperty(
'--previous-lyrics',
'var(--ytmusic-text-primary)',
);
root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)');
root.style.setProperty('--size-lyrics', '1.2');
root.style.setProperty('--offset-lyrics', '0');
root.style.setProperty('--lyric-width', '83%');
break;
case 'offset':
root.style.setProperty(
'--previous-lyrics',
'var(--ytmusic-text-primary)',
);
root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)');
root.style.setProperty('--size-lyrics', '1');
root.style.setProperty('--offset-lyrics', '5%');
root.style.setProperty('--lyric-width', '100%');
break;
case 'focus':
root.style.setProperty(
'--previous-lyrics',
'var(--ytmusic-text-secondary)',
);
root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)');
root.style.setProperty('--size-lyrics', '1');
root.style.setProperty('--offset-lyrics', '0');
root.style.setProperty('--lyric-width', '100%');
break;
}
});

View File

@ -1,22 +1,143 @@
import { createSignal, Show } from 'solid-js'; import { createEffect, createSignal, onMount, Show } from 'solid-js';
import { LyricsContainer } from './components/LyricsContainer'; import { LyricsContainer } from './components/LyricsContainer';
import { LyricsPicker } from './components/LyricsPicker';
import { selectors } from './utils';
import type { VideoDetails } from '@/types/video-details';
import type { SyncedLyricsPluginConfig } from '../types'; import type { SyncedLyricsPluginConfig } from '../types';
export const [isVisible, setIsVisible] = createSignal<boolean>(false); export const [isVisible, setIsVisible] = createSignal<boolean>(false);
export const [config, setConfig] = export const [config, setConfig] =
createSignal<SyncedLyricsPluginConfig | null>(null); createSignal<SyncedLyricsPluginConfig | null>(null);
export const [playerState, setPlayerState] = createSignal<VideoDetails | null>(
null, createEffect(() => {
); if (!config()?.enabled) return;
const root = document.documentElement;
// Set the line effect
switch (config()?.lineEffect) {
case 'fancy':
root.style.setProperty('--lyrics-font-size', '3rem');
root.style.setProperty('--lyrics-line-height', '1.333');
root.style.setProperty('--lyrics-width', '100%');
root.style.setProperty('--lyrics-padding', '2rem');
root.style.setProperty(
'--lyrics-animations',
'lyrics-glow var(--lyrics-glow-duration) forwards, lyrics-wobble var(--lyrics-wobble-duration) forwards',
);
root.style.setProperty('--lyrics-inactive-font-weight', '700');
root.style.setProperty('--lyrics-inactive-opacity', '0.33');
root.style.setProperty('--lyrics-inactive-scale', '0.95');
root.style.setProperty('--lyrics-inactive-offset', '0');
root.style.setProperty('--lyrics-active-font-weight', '700');
root.style.setProperty('--lyrics-active-opacity', '1');
root.style.setProperty('--lyrics-active-scale', '1');
root.style.setProperty('--lyrics-active-offset', '0');
break;
case 'scale':
root.style.setProperty('--lyrics-font-size', '1.4rem');
root.style.setProperty(
'--lyrics-line-height',
'var(--ytmusic-body-line-height)',
);
root.style.setProperty('--lyrics-width', '83%');
root.style.setProperty('--lyrics-padding', '0');
root.style.setProperty('--lyrics-animations', 'none');
root.style.setProperty('--lyrics-inactive-font-weight', '400');
root.style.setProperty('--lyrics-inactive-opacity', '0.33');
root.style.setProperty('--lyrics-inactive-scale', '1');
root.style.setProperty('--lyrics-inactive-offset', '0');
root.style.setProperty('--lyrics-active-font-weight', '700');
root.style.setProperty('--lyrics-active-opacity', '1');
root.style.setProperty('--lyrics-active-scale', '1.2');
root.style.setProperty('--lyrics-active-offset', '0');
break;
case 'offset':
root.style.setProperty('--lyrics-font-size', '1.4rem');
root.style.setProperty(
'--lyrics-line-height',
'var(--ytmusic-body-line-height)',
);
root.style.setProperty('--lyrics-width', '100%');
root.style.setProperty('--lyrics-padding', '0');
root.style.setProperty('--lyrics-animations', 'none');
root.style.setProperty('--lyrics-inactive-font-weight', '400');
root.style.setProperty('--lyrics-inactive-opacity', '0.33');
root.style.setProperty('--lyrics-inactive-scale', '1');
root.style.setProperty('--lyrics-inactive-offset', '0');
root.style.setProperty('--lyrics-active-font-weight', '700');
root.style.setProperty('--lyrics-active-opacity', '1');
root.style.setProperty('--lyrics-active-scale', '1');
root.style.setProperty('--lyrics-active-offset', '5%');
break;
case 'focus':
root.style.setProperty('--lyrics-font-size', '1.4rem');
root.style.setProperty(
'--lyrics-line-height',
'var(--ytmusic-body-line-height)',
);
root.style.setProperty('--lyrics-width', '100%');
root.style.setProperty('--lyrics-padding', '0');
root.style.setProperty('--lyrics-animations', 'none');
root.style.setProperty('--lyrics-inactive-font-weight', '400');
root.style.setProperty('--lyrics-inactive-opacity', '0.33');
root.style.setProperty('--lyrics-inactive-scale', '1');
root.style.setProperty('--lyrics-inactive-offset', '0');
root.style.setProperty('--lyrics-active-font-weight', '700');
root.style.setProperty('--lyrics-active-opacity', '1');
root.style.setProperty('--lyrics-active-scale', '1');
root.style.setProperty('--lyrics-active-offset', '0');
break;
}
});
export const LyricsRenderer = () => { export const LyricsRenderer = () => {
const [stickyRef, setStickRef] = createSignal<HTMLElement | null>(null);
// prettier-ignore
onMount(() => {
const tab = document.querySelector<HTMLElement>(selectors.body.tabRenderer)!;
const mousemoveListener = (e: MouseEvent) => {
const { top } = tab.getBoundingClientRect();
const { clientHeight: height } = stickyRef()!;
const showPicker = (e.clientY - top - 5) <= height;
if (showPicker) {
// picker visible
stickyRef()!.style.setProperty('--top', '0');
} else {
// picker hidden
stickyRef()!.style.setProperty('--top', '-50%');
}
};
tab.addEventListener('mousemove', mousemoveListener);
return () => tab.removeEventListener('mousemove', mousemoveListener);
});
return ( return (
<Show when={isVisible()}> <Show when={isVisible()}>
<LyricsContainer /> <div class="lyrics-renderer">
<div class="lyrics-renderer-sticky" ref={setStickRef}>
<LyricsPicker />
<div
id="divider"
class="style-scope ytmusic-guide-section-renderer"
style={{ width: '100%', margin: '0' }}
></div>
</div>
<LyricsContainer />
</div>
</Show> </Show>
); );
}; };

View File

@ -1,10 +1,7 @@
import { render } from 'solid-js/web'; import { render } from 'solid-js/web';
import { waitForElement } from '@/utils/wait-for-element'; import { waitForElement } from '@/utils/wait-for-element';
import { LyricsRenderer, setIsVisible } from './renderer';
import { LyricsRenderer, setIsVisible, setPlayerState } from './renderer';
import type { VideoDetails } from '@/types/video-details';
export const selectors = { export const selectors = {
head: '#tabsContent > .tab-header:nth-of-type(2)', head: '#tabsContent > .tab-header:nth-of-type(2)',
@ -14,10 +11,9 @@ export const selectors = {
}, },
}; };
export const tabStates = { export const tabStates: Record<string, () => void> = {
true: async (data?: VideoDetails) => { true: async () => {
setIsVisible(true); setIsVisible(true);
setPlayerState(data ?? null);
let container = document.querySelector('#synced-lyrics-container'); let container = document.querySelector('#synced-lyrics-container');
if (container) return; if (container) return;

Some files were not shown because too many files have changed in this diff Show More