mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
Compare commits
236 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a33a03f512 | |||
| f8a53f0d61 | |||
| 748d77d1c0 | |||
| 725ad0d630 | |||
| bdbab17772 | |||
| 57d2fa372d | |||
| 80471b0ca4 | |||
| 22fdfe3342 | |||
| 5ecfa2a1f7 | |||
| b9beea810e | |||
| f0e77812e7 | |||
| 6d1237c2a2 | |||
| b43c92386e | |||
| 017476a81b | |||
| 9b047d9c54 | |||
| 31f009d3c4 | |||
| 8504f2c086 | |||
| 1d6251baea | |||
| 3ea13a2a22 | |||
| 1cc153084d | |||
| 1c468b4054 | |||
| 1bad46890a | |||
| 5829c8d0f7 | |||
| 85aceaaae4 | |||
| 24e593b22f | |||
| 3f8ca6002e | |||
| b62ccfe7b1 | |||
| 237dde9765 | |||
| 65f4339fd1 | |||
| 109e9f8166 | |||
| 9163b6f04b | |||
| 51da259c97 | |||
| 2bf67b941e | |||
| 533b96d1f6 | |||
| 5c9ded8779 | |||
| 6f389bb297 | |||
| 8a209404d4 | |||
| 6193fb487a | |||
| 9aa7f7a023 | |||
| 5bfaa9a791 | |||
| d210ec8227 | |||
| dec7c5e95c | |||
| 940d0beb84 | |||
| cf98754276 | |||
| d91d493dd1 | |||
| 7e1aea21db | |||
| 0179dfd311 | |||
| 98ea26bbff | |||
| 0d9daaad66 | |||
| fe319daec1 | |||
| 929c58671a | |||
| 4fb2350c2b | |||
| a401bfa809 | |||
| fdeed76f6f | |||
| 0ab113816a | |||
| 8a58b02c7b | |||
| 037b059b55 | |||
| bb0f9fb3d0 | |||
| d3c7848896 | |||
| ea50cb1e65 | |||
| 5070fd88b5 | |||
| 21177478cb | |||
| 26b8b38b89 | |||
| be04d66aa8 | |||
| a837987e70 | |||
| da99558163 | |||
| 3b50cbcb6e | |||
| 595c011bce | |||
| 458fe54063 | |||
| ae3a289005 | |||
| a49eea9246 | |||
| d675a175e9 | |||
| 6c510a71c2 | |||
| 5503d2cbb8 | |||
| ba4c7e1a0c | |||
| e19c458441 | |||
| ec5cf0cae8 | |||
| b3c4570f8c | |||
| 112b6d893b | |||
| 52236907e4 | |||
| d449529ea7 | |||
| 61c799f7d4 | |||
| 1a4ee13e47 | |||
| f91afb984a | |||
| f9892b0eae | |||
| 60c7885a3c | |||
| 63ca6aa533 | |||
| e77a8c04e8 | |||
| 0bfabf604c | |||
| 4343c599cf | |||
| c251554c31 | |||
| 95e519bdc9 | |||
| 79d38bfc8e | |||
| f5655b0ae6 | |||
| 12b4afc3ce | |||
| c104d47737 | |||
| 870cf6143c | |||
| 1baed0e913 | |||
| 02619c79bb | |||
| 0faad538f3 | |||
| e7de30c629 | |||
| c5d8333039 | |||
| 4da08e7c9b | |||
| 171387995a | |||
| aeac020c9a | |||
| 5c05ddeb29 | |||
| cbdd649365 | |||
| d2cf2ad71f | |||
| 7d33494097 | |||
| ca83edabf3 | |||
| f84e77e814 | |||
| 14f2120a32 | |||
| 1cbf14ee2a | |||
| b7cb167fc6 | |||
| 41b9f8b967 | |||
| 0b769ce287 | |||
| ad71ef8a68 | |||
| 048a994f32 | |||
| c2bd8ce188 | |||
| 62ce4e818c | |||
| 4ab8829a02 | |||
| 36b3e2cb0c | |||
| 9ba0614a7d | |||
| 81431ad196 | |||
| a8e8d5afd7 | |||
| f3d86743ee | |||
| 6306968193 | |||
| 7142a253d6 | |||
| 2a24588338 | |||
| 2abaf54ac8 | |||
| cc730ad55c | |||
| d8581c5d69 | |||
| 3208bf4a6d | |||
| 4109db1ad7 | |||
| 87a0ef5d54 | |||
| cdc40f0c53 | |||
| 7a3b8082a2 | |||
| 3e9039c97d | |||
| b9d1130468 | |||
| 605f0984e4 | |||
| 44de7d9e98 | |||
| 9926575744 | |||
| f16a99f6e4 | |||
| a23c64b5b8 | |||
| 0899f76548 | |||
| 515dcdc7e3 | |||
| b2c4bc425b | |||
| 363c3b3a67 | |||
| c2dde3d78f | |||
| eb515cfc61 | |||
| c208ca184f | |||
| c231fa7c44 | |||
| 9e1b8d43d0 | |||
| f50ece88df | |||
| eeb780d190 | |||
| cafdf654d3 | |||
| 2d665013e7 | |||
| 451a46e208 | |||
| 490b901c34 | |||
| b57b4a3454 | |||
| 60c61e32b1 | |||
| aa9052d449 | |||
| 67f3a38583 | |||
| a00ecc4729 | |||
| 56d63fca52 | |||
| 759f3ba317 | |||
| d8daf03f2c | |||
| a519c7c714 | |||
| 933d12fdd1 | |||
| 61fb733550 | |||
| 7f05e3168d | |||
| 3b7697a90d | |||
| 350b1467fe | |||
| 2f5d102f4d | |||
| 66e296df1a | |||
| 1e4cd699db | |||
| 516fbff3d7 | |||
| aab9358d67 | |||
| ae3939f857 | |||
| 79bafd1780 | |||
| 3f4f52a31f | |||
| c2b7b29716 | |||
| dab84b9cf9 | |||
| e5980158eb | |||
| 647d4c9d99 | |||
| 30feb6128b | |||
| 0cf6923540 | |||
| cce9f0b462 | |||
| c0805fb758 | |||
| 04e5844301 | |||
| f28e663133 | |||
| 2c84527c43 | |||
| 68511de727 | |||
| e7ca9f129f | |||
| 259da70e4f | |||
| 9409d75ac7 | |||
| 3ea923f56f | |||
| 6d6c8c94cf | |||
| 29098758a9 | |||
| fdbb35e221 | |||
| 6ddac62313 | |||
| ae4b494300 | |||
| 7d9eed88f4 | |||
| 4abf848f99 | |||
| cc0a6cfdce | |||
| a74d0dd0ca | |||
| 18f15d4cce | |||
| 80e20c6579 | |||
| 761026fd74 | |||
| 95b75f020c | |||
| 574e4baef8 | |||
| ec4871d5a8 | |||
| ccd6bf9c3f | |||
| 2975d4292c | |||
| ad9571550f | |||
| 980068217c | |||
| 18f69aea3f | |||
| add7cb9e48 | |||
| aa67a57971 | |||
| 74f22b4474 | |||
| 7ef4a23576 | |||
| 3c90a1f459 | |||
| 3793d36f36 | |||
| 179f4b29db | |||
| 34f106896c | |||
| 06b581f499 | |||
| 3ec126628c | |||
| 1d3bb60e0b | |||
| 2e3ced6006 | |||
| e8efca5a3e | |||
| 5161c356f9 | |||
| e917b30e64 | |||
| 2d847f9808 | |||
| 21755fffc7 | |||
| 5c9d3e3e67 | |||
| ea801f65ef |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -65,9 +65,9 @@ jobs:
|
||||
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/20.08
|
||||
sudo flatpak install -y flathub org.freedesktop.Sdk/x86_64/20.08
|
||||
sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp/x86_64/20.08
|
||||
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
|
||||
|
||||
- name: Build and release on Windows
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
@ -67,7 +67,7 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸]
|
||||
## Available plugins:
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
|
||||
|
||||
- **Album Actions**: Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album
|
||||
|
||||
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
|
||||
@ -95,6 +95,8 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸]
|
||||
- **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)
|
||||
|
||||
- **Equalizer**: add filters to boost or cut specific range of frequencies (e.g. bass booster)
|
||||
|
||||
- **Exponential Volume**: Makes the volume
|
||||
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
|
||||
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 can’t be opened." when launching the app, run the following in the Terminal:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
/usr/bin/xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
174
changelog.md
174
changelog.md
@ -2,8 +2,182 @@
|
||||
|
||||
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)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 721 KiB |
@ -12,7 +12,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<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:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
/usr/bin/xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
@ -66,7 +66,7 @@
|
||||
## Plugins disponibles :
|
||||
|
||||
- **Bloqueur de publicités** : Bloquez toutes les publicités et le suivi dès le départ
|
||||
|
||||
|
||||
- **Actions d'album** : Ajoute des boutons Je n'aime pas, Dislike, J'aime, et Unlike pour appliquer cela à toutes les chansons dans une playlist ou un album
|
||||
|
||||
- **Thème de couleur d'album** : Applique un thème dynamique et des effets visuels basés sur la palette de couleurs de l'album
|
||||
@ -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 :
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
/usr/bin/xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
@ -385,4 +385,4 @@ MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
### Pourquoi le menu de l'application ne s'affiche-t-il pas ?
|
||||
|
||||
Si l'option `Masquer le menu` est activée - vous pouvez afficher le menu avec la touche <kbd>alt</kbd> (ou <kbd>\`</kbd> [backtick] si vous utilisez le plugin du menu intégré)
|
||||
Si l'option `Masquer le menu` est activée - vous pouvez afficher le menu avec la touche <kbd>alt</kbd> (ou <kbd>\`</kbd> [backtick] si vous utilisez le plugin du menu intégré)
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
@ -65,7 +65,7 @@
|
||||
## Tiltæk tengiforrit:
|
||||
|
||||
- **Auglýsingablokkari**: Lokaðu fyrir allar auglýsingar og rakningar úr kassanum
|
||||
|
||||
|
||||
- **Albúmsaðgerðir**: Bætir Ódíslika, Mislíkt, Líkt, og Ólíkt til að nota þetta á öll lög á spilunarlista eða albúm
|
||||
|
||||
- **Albúmslitaþema**: Beitir kraftmikið þema og sjónrænum áhrifum sem byggjast á litavali albúmsins
|
||||
@ -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:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
/usr/bin/xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
<div align="center">
|
||||
<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
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
/usr/bin/xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
</div>
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
@ -65,7 +65,7 @@
|
||||
## Доступные плагины:
|
||||
|
||||
- **Блокировщик рекламы**: Блокирует всю рекламу и трекеры
|
||||
|
||||
|
||||
- **Действия с альбомом**: Добавляет кнопки "Убрать дизлайк", "Дизлайк", "Лайк", "Убрать лайк" и применяет их действия ко всем трекам в плейлисте или альбоме
|
||||
|
||||
- **Цветовая тема альбома**: Применяет динамическую тему и эффекты, основываясь на цветовой палитре альбома
|
||||
@ -137,7 +137,7 @@
|
||||
|
||||
- **Визуализатор**: Различные визуализаторы музыки
|
||||
|
||||
- **Synced Lyrics**:
|
||||
- **Synced Lyrics**:
|
||||
Предоставляет синхронизированные слова для песен из таких источников, как [LRClib](https://lrclib.net).
|
||||
|
||||
## Перевод
|
||||
@ -168,7 +168,7 @@ brew install th-ch/youtube-music/youtube-music
|
||||
Если вы устанавливаете приложение вручную и получаете ошибку "is damaged and can’t be opened.", запустите в терминале следующую команду:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
/usr/bin/xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
96
package.json
96
package.json
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"desktopName": "com.github.th_ch.youtube_music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "3.6.2",
|
||||
"version": "3.7.1",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/main/index.js",
|
||||
"license": "MIT",
|
||||
@ -21,7 +22,7 @@
|
||||
"license",
|
||||
"!node_modules",
|
||||
"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/**/*.map",
|
||||
"!node_modules/**/*.ts"
|
||||
@ -71,6 +72,9 @@
|
||||
"linux": {
|
||||
"icon": "assets/generated/icons/png",
|
||||
"category": "AudioVideo",
|
||||
"desktop": {
|
||||
"StartupWMClass": "com.github.th_ch.youtube_music"
|
||||
},
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
@ -131,7 +135,22 @@
|
||||
},
|
||||
"flatpak": {
|
||||
"description": "YouTube Music Desktop App bundled with custom plugins (and built-in ad blocker / downloader)",
|
||||
"category": "AudioVideo"
|
||||
"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": {
|
||||
"depends": [
|
||||
@ -177,6 +196,7 @@
|
||||
"start": "electron-vite preview",
|
||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||
"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",
|
||||
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||
@ -201,11 +221,11 @@
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"usocket": "1.0.1",
|
||||
"node-gyp": "10.2.0",
|
||||
"node-gyp": "11.0.0",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "2.0.1",
|
||||
"@babel/runtime": "7.25.7"
|
||||
"@babel/runtime": "7.26.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
||||
@ -214,18 +234,18 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "1.27.1",
|
||||
"@cliqz/adblocker-electron-preload": "1.27.1",
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@ffmpeg.wasm/core-mt": "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",
|
||||
"@hono/node-server": "1.13.2",
|
||||
"@hono/swagger-ui": "0.4.1",
|
||||
"@hono/zod-openapi": "0.16.4",
|
||||
"@hono/zod-validator": "0.4.1",
|
||||
"@ghostery/adblocker-electron": "2.3.1",
|
||||
"@ghostery/adblocker-electron-preload": "2.3.1",
|
||||
"@hono/node-server": "1.13.7",
|
||||
"@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/mpris-service": "2.1.4",
|
||||
"@jimp/plugin-invert": "0.22.12",
|
||||
@ -235,7 +255,7 @@
|
||||
"butterchurn": "3.0.0-beta.4",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"color": "4.2.3",
|
||||
"conf": "13.0.1",
|
||||
"conf": "13.1.0",
|
||||
"custom-electron-prompt": "1.5.8",
|
||||
"dbus-next": "0.10.2",
|
||||
"deepmerge-ts": "7.1.3",
|
||||
@ -248,35 +268,35 @@
|
||||
"fast-average-color": "9.4.0",
|
||||
"fast-equals": "5.0.1",
|
||||
"filenamify": "6.0.0",
|
||||
"hono": "4.6.5",
|
||||
"hono": "4.6.14",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "23.16.0",
|
||||
"i18next": "24.2.0",
|
||||
"jimp": "1.6.0",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
"keyboardevents-areequal": "0.2.2",
|
||||
"node-html-parser": "6.1.13",
|
||||
"node-html-parser": "7.0.1",
|
||||
"node-id3": "0.2.6",
|
||||
"peerjs": "1.5.4",
|
||||
"semver": "7.6.3",
|
||||
"serve": "14.2.4",
|
||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||
"solid-floating-ui": "0.3.1",
|
||||
"solid-js": "1.9.2",
|
||||
"solid-js": "1.9.3",
|
||||
"solid-styled-components": "0.28.5",
|
||||
"solid-transition-group": "0.2.3",
|
||||
"ts-morph": "24.0.0",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "10.5.0",
|
||||
"zod": "3.23.8"
|
||||
"youtubei.js": "12.2.0",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.12.0",
|
||||
"@playwright/test": "1.48.0",
|
||||
"@stylistic/eslint-plugin-js": "2.9.0",
|
||||
"@eslint/js": "9.17.0",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@stylistic/eslint-plugin-js": "2.12.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/eslint__js": "8.42.3",
|
||||
"@types/howler": "2.2.12",
|
||||
@ -287,29 +307,29 @@
|
||||
"builtin-modules": "4.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "6.0.0",
|
||||
"discord-api-types": "0.37.102",
|
||||
"electron": "33.0.0",
|
||||
"discord-api-types": "0.37.114",
|
||||
"electron": "33.2.1",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"electron-devtools-installer": "4.0.0",
|
||||
"electron-vite": "2.3.0",
|
||||
"esbuild": "0.24.0",
|
||||
"eslint": "9.12.0",
|
||||
"esbuild": "0.24.2",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"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-prettier": "5.2.1",
|
||||
"glob": "11.0.0",
|
||||
"node-gyp": "10.2.0",
|
||||
"playwright": "1.48.0",
|
||||
"rollup": "4.24.0",
|
||||
"typescript": "5.6.3",
|
||||
"typescript-eslint": "8.9.0",
|
||||
"utf-8-validate": "6.0.4",
|
||||
"vite": "5.4.9",
|
||||
"vite-plugin-inspect": "0.8.7",
|
||||
"node-gyp": "11.0.0",
|
||||
"playwright": "1.49.1",
|
||||
"rollup": "4.29.1",
|
||||
"typescript": "5.7.2",
|
||||
"typescript-eslint": "8.18.2",
|
||||
"utf-8-validate": "6.0.5",
|
||||
"vite": "6.0.6",
|
||||
"vite-plugin-inspect": "0.10.6",
|
||||
"vite-plugin-resolve": "2.5.2",
|
||||
"vite-plugin-solid": "2.10.2",
|
||||
"vite-plugin-solid": "2.11.0",
|
||||
"ws": "8.18.0"
|
||||
},
|
||||
"auto-changelog": {
|
||||
|
||||
3070
pnpm-lock.yaml
generated
3070
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -195,6 +195,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"next": "التالي",
|
||||
"play-pause": "تشغيل/إيقاف",
|
||||
"previous": "السابق",
|
||||
"quit": "خروج",
|
||||
"restart": "إعادة تشغيل التطبيق",
|
||||
@ -206,9 +207,120 @@
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"ad-speedup": {
|
||||
"description": "إذا تم عرض إعلان, فإن الصوت سيتم كتمانه وسيتم وضع سرعة التشغيل الى 16x",
|
||||
"name": "تسريع الإعلان"
|
||||
},
|
||||
"adblocker": {
|
||||
"description": "حجب جميع الإعلانات والمسارات خارج الصندوق",
|
||||
"description": "حجب جميع الإعلانات والمتتبعات جاهز للأستخدام",
|
||||
"menu": {
|
||||
"blocker": "حاجب الإعلانات"
|
||||
},
|
||||
"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": "اختر مكان التحميل"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +220,7 @@
|
||||
},
|
||||
"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",
|
||||
"name": "Album akce"
|
||||
"name": "Možnosti Albumu"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Používá dynamický motiv a vizuální efekty na základě palety barev alba",
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "Apply compression k audiu (snižuje hlasitost nejhlasitěších částí signálu and zvyšuje hlasitost nejjemnějších částí)",
|
||||
"name": "Audio kompresor"
|
||||
@ -415,8 +458,18 @@
|
||||
"menu": {
|
||||
"choose-download-folder": "Vybrat složku pro stahování",
|
||||
"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": {
|
||||
"advanced": "Pokoročile"
|
||||
"advanced": "Pokoročile",
|
||||
"enabled": "Zapnuto",
|
||||
"mode": "Časový režim",
|
||||
"percent": "Procent",
|
||||
"seconds": "Sekundy"
|
||||
}
|
||||
},
|
||||
"download-playlist": "Stáhnout seznam písniček",
|
||||
@ -616,11 +669,13 @@
|
||||
"name": "Scrobbler",
|
||||
"prompt": {
|
||||
"lastfm": {
|
||||
"api-key": "Last,fm API klíč"
|
||||
"api-key": "Last,fm API klíč",
|
||||
"api-secret": "Tajný klíč API Last.fm"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"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",
|
||||
"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": {
|
||||
"description": "Ovládejte přehrávání z vašeho Windows hlavního panelu",
|
||||
"name": "Hlavní panel Media Control"
|
||||
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
@ -687,7 +730,8 @@
|
||||
"tooltip": "Nur aktive Zeile weiß darstellen"
|
||||
},
|
||||
"offset": {
|
||||
"label": "Versatz"
|
||||
"label": "Versatz",
|
||||
"tooltip": "Verschiebe die aktuelle Zeile nach rechts"
|
||||
},
|
||||
"scale": {
|
||||
"label": "Skalieren",
|
||||
@ -700,6 +744,10 @@
|
||||
"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)"
|
||||
},
|
||||
"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": {
|
||||
"label": "Zeitkodierungen anzeigen",
|
||||
"tooltip": "Zeitkodierungen neben Songtext anzeigen"
|
||||
|
||||
@ -279,6 +279,13 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "Adds an API server to control the player",
|
||||
"dialog": {
|
||||
@ -714,8 +721,8 @@
|
||||
"synced-lyrics": {
|
||||
"description": "Provides synced lyrics to songs, using providers like LRClib.",
|
||||
"errors": {
|
||||
"fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.",
|
||||
"not-found": "⚠️ - No lyrics found for this song."
|
||||
"fetch": "⚠️\tAn error occurred while fetching the lyrics.\n\tPlease try again later.",
|
||||
"not-found": "⚠️ No lyrics found for this song."
|
||||
},
|
||||
"menu": {
|
||||
"default-text-string": {
|
||||
@ -725,6 +732,10 @@
|
||||
"line-effect": {
|
||||
"label": "Line effect",
|
||||
"submenu": {
|
||||
"fancy": {
|
||||
"label": "Fancy",
|
||||
"tooltip": "Use large, app-like effects on the current line"
|
||||
},
|
||||
"focus": {
|
||||
"label": "Focus",
|
||||
"tooltip": "Make only the current line white"
|
||||
@ -808,6 +819,18 @@
|
||||
"visualizer-type": "Visualizer Type"
|
||||
},
|
||||
"name": "Visualizer"
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "Adds an equalizer to the player",
|
||||
"name": "Equalizer",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "Presets",
|
||||
"list": {
|
||||
"bass-booster": "Bass booster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,6 +484,18 @@
|
||||
"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": {
|
||||
"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"
|
||||
|
||||
@ -67,6 +67,19 @@
|
||||
"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": {
|
||||
"label": "Lisamoodulid",
|
||||
"new": "UUS"
|
||||
|
||||
@ -197,7 +197,7 @@
|
||||
"next": "بعدی",
|
||||
"play-pause": "پخش/توقف",
|
||||
"previous": "قبلی",
|
||||
"quit": "خروجی",
|
||||
"quit": "خروج",
|
||||
"restart": "راهاندازی مجدد برنامه",
|
||||
"show": "نمایش پنجره",
|
||||
"tooltip": {
|
||||
@ -226,7 +226,10 @@
|
||||
"description": "اعمال یک تم پویا و جلوههای بصری بر اساس پالت رنگ آلبوم",
|
||||
"menu": {
|
||||
"color-mix-ratio": {
|
||||
"label": "نسبت ترکیب رنگ"
|
||||
"label": "نسبت ترکیب رنگ",
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "تم رنگ آلبوم"
|
||||
@ -241,7 +244,10 @@
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"label": "بافر"
|
||||
"label": "بافر",
|
||||
"submenu": {
|
||||
"buffer": "{{buffer}}"
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"label": "شفافیت"
|
||||
@ -267,6 +273,49 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "اعمال فشردهسازی به صدا (کاهش حجم بلندترین بخشهای سیگنال و افزایش حجم بخشهای نرمتر)",
|
||||
"name": "فشردهساز صدا"
|
||||
@ -389,8 +438,147 @@
|
||||
"getting-playlist-info": "در حال دریافت اطلاعات فهرست پخش…",
|
||||
"loading": "در حال بارگذاری…",
|
||||
"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": "همیشه در بالا"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -408,7 +408,7 @@
|
||||
"backend": {
|
||||
"dialog": {
|
||||
"error": {
|
||||
"message": "Argh! Paumanhin, nabigo ang pag-download…",
|
||||
"message": "Kainis! Paumanhin, nabigo ang pag-download…",
|
||||
"title": "Nagkaroon ng error sa pag-download!"
|
||||
},
|
||||
"start-download-playlist": {
|
||||
@ -465,7 +465,7 @@
|
||||
"can-not-update-progress": "Hindi ma-update ang progress"
|
||||
},
|
||||
"templates": {
|
||||
"button": "Mag download"
|
||||
"button": "Mag-download"
|
||||
}
|
||||
},
|
||||
"exponential-volume": {
|
||||
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
@ -441,6 +484,18 @@
|
||||
"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": {
|
||||
"description": "Rend le curseur de volume exponentiel afin qu'il soit plus facile de sélectionner des volumes plus faibles.",
|
||||
"name": "Volume exponentiel"
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"executed-at-ms": "התוסף {{pluginName}}:{{contextName}} בוצע ב {{ms}}ms",
|
||||
"initialize-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה",
|
||||
"load-all": "טוען את כל התוספים",
|
||||
"load-failed": "לא ניתן לטעון את התוסף {{pluginName}}",
|
||||
"load-failed": "שגיאה בטעינת התוסף \"{{pluginName}}\"",
|
||||
"loaded": "התוסף \"{{pluginName}}\" נטען",
|
||||
"unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה",
|
||||
"unloaded": "תוסף {{pluginName}} הורד"
|
||||
@ -52,6 +52,38 @@
|
||||
"buttons": {
|
||||
"later": "אחר כך",
|
||||
"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
251
src/i18n/resources/hi.json
Normal 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": "तरीका"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -484,6 +484,18 @@
|
||||
"button": "Unduh"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "Menambahkan equalizer ke pemutar",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "Prasetel",
|
||||
"list": {
|
||||
"bass-booster": "Penguat Bass"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Equalizer"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "Buat penggeser volume menjadi eksponen sehingga memudahkan memilih volume yang lebih rendah.",
|
||||
"name": "Volume Eksponen"
|
||||
|
||||
@ -714,9 +714,14 @@
|
||||
"synced-lyrics": {
|
||||
"description": "Veitir samstillta texta við lög, með því að nota veitur eins og LRClib.",
|
||||
"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": {
|
||||
@ -725,20 +730,37 @@
|
||||
"tooltip": "Gerðu aðeins núverandi línu hvíta"
|
||||
},
|
||||
"offset": {
|
||||
"label": "Fararbyrjun"
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
||||
@ -279,6 +279,56 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "Attiva la compressione audio (abbassa il volume delle parti più alte e alza quello delle parti più basse del segnale)",
|
||||
"name": "Compressore audio"
|
||||
@ -441,6 +491,18 @@
|
||||
"button": "Scarica"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "Aggiunge un equalizzatore al player",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "Presets",
|
||||
"list": {
|
||||
"bass-booster": "Booster dei bassi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Equalizzatore"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "Rende esponenziale il cursore del volume, in modo da facilitare la selezione di volumi più bassi.",
|
||||
"name": "Volume esponenziale"
|
||||
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "オーディオにコンプレッサーを適用します(信号での一番大きい部分の音量を下げ、小さい部分の音量を上げる)",
|
||||
"name": "オーディオコンプレッサー"
|
||||
@ -441,6 +484,18 @@
|
||||
"button": "ダウンロード"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "プレイヤーにイコライザーを追加",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "プリセット",
|
||||
"list": {
|
||||
"bass-booster": "ベースブースター"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "イコライザー"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "音量スライダを指数関数的にさせ、低い音量に設定しやすくなります。",
|
||||
"name": "指数音量"
|
||||
|
||||
@ -279,6 +279,13 @@
|
||||
},
|
||||
"name": "앰비언트 모드"
|
||||
},
|
||||
"amuse": {
|
||||
"description": "6K Labs Amuse의 'now playing' 위젯에 YouTube Music 지원 추가",
|
||||
"name": "Amuse",
|
||||
"response": {
|
||||
"query": "Amuse API 서버가 실행 중입니다. GET /query로 노래 정보를 가져오세요."
|
||||
}
|
||||
},
|
||||
"api-server": {
|
||||
"description": "플레이어를 제어하기 위한 API 서버를 추가합니다",
|
||||
"dialog": {
|
||||
@ -484,6 +491,18 @@
|
||||
"button": "다운로드"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "플레이어에 이퀄라이저를 추가합니다",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "프리셋",
|
||||
"list": {
|
||||
"bass-booster": "베이스 부스터"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "이퀄라이저"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "음량 슬라이더를 지수적으로 만들어 더 낮은 음량을 쉽게 선택할 수 있도록 합니다.",
|
||||
"name": "지수 음량"
|
||||
@ -714,8 +733,8 @@
|
||||
"synced-lyrics": {
|
||||
"description": "LRClib등의 가사 제공자에서 싱크 가사를 불러옵니다.",
|
||||
"errors": {
|
||||
"fetch": "⚠️ - 가사를 불러오는 동안 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"not-found": "⚠️ - 이 노래의 가사를 찾을 수 없습니다."
|
||||
"fetch": "⚠️\t가사를 불러오는 동안 오류가 발생했습니다.\n\t나중에 다시 시도해 주세요.",
|
||||
"not-found": "⚠️ 이 노래의 가사를 찾을 수 없습니다."
|
||||
},
|
||||
"menu": {
|
||||
"default-text-string": {
|
||||
@ -725,6 +744,10 @@
|
||||
"line-effect": {
|
||||
"label": "줄 표시 효과",
|
||||
"submenu": {
|
||||
"fancy": {
|
||||
"label": "예쁘게",
|
||||
"tooltip": "유튜브 뮤직 앱처럼 커다란 효과를 현재 라인에 사용합니다"
|
||||
},
|
||||
"focus": {
|
||||
"label": "포커스",
|
||||
"tooltip": "현재 줄만 하얀색으로 표시"
|
||||
|
||||
@ -279,6 +279,52 @@
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
@ -441,6 +487,18 @@
|
||||
"button": "Pobierz"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "Dodaje equalizer do odtwarzacza",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "Presety",
|
||||
"list": {
|
||||
"bass-booster": "Wzmacniacz basu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Equalizer"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"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ść"
|
||||
@ -682,6 +740,9 @@
|
||||
"line-effect": {
|
||||
"label": "Efekty linijki",
|
||||
"submenu": {
|
||||
"fancy": {
|
||||
"label": "Facy"
|
||||
},
|
||||
"focus": {
|
||||
"label": "Fokus",
|
||||
"tooltip": "Spraw, aby tylko obecna linijka była biała"
|
||||
|
||||
@ -279,6 +279,13 @@
|
||||
},
|
||||
"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": {
|
||||
@ -484,6 +491,18 @@
|
||||
"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": {
|
||||
"description": "Torna o controle deslizante de volume exponencial para que seja mais fácil selecionar volumes mais baixos.",
|
||||
"name": "Volume Exponencial"
|
||||
@ -714,8 +733,8 @@
|
||||
"synced-lyrics": {
|
||||
"description": "Fornece letras sincronizadas para músicas, usando provedores como LRClib.",
|
||||
"errors": {
|
||||
"fetch": "⚠️ - Ocorreu um erro ao buscar a letra. Tente novamente mais tarde.",
|
||||
"not-found": "⚠️ - Nenhuma letra encontrada para esta música."
|
||||
"fetch": "⚠️\tOcorreu um erro ao buscar a letra.\n\tTente novamente mais tarde.",
|
||||
"not-found": "⚠️ Nenhuma letra encontrada para esta música."
|
||||
},
|
||||
"menu": {
|
||||
"default-text-string": {
|
||||
@ -725,6 +744,10 @@
|
||||
"line-effect": {
|
||||
"label": "Efeito de linha",
|
||||
"submenu": {
|
||||
"fancy": {
|
||||
"label": "Fancy",
|
||||
"tooltip": "Use efeitos grandes, semelhantes a aplicativos, na linha atual"
|
||||
},
|
||||
"focus": {
|
||||
"label": "Foco",
|
||||
"tooltip": "Deixe apenas a linha atual branca"
|
||||
|
||||
@ -279,6 +279,37 @@
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
|
||||
@ -207,6 +207,10 @@
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"ad-speedup": {
|
||||
"description": "Reclamele au sunetul dezactivat si viteza de redare este x16",
|
||||
"name": "Accelerare reclame"
|
||||
},
|
||||
"adblocker": {
|
||||
"description": "Blocheaza toate reclamele si trackers",
|
||||
"menu": {
|
||||
@ -275,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "Aplica compresie pe audio (scade volumul partilor cele mai sonore si creste volumul partilor mai putin sonore)",
|
||||
"name": "Compresor audio"
|
||||
@ -410,6 +457,21 @@
|
||||
"description": "Descarca MP3 / sursa audio direct din interfata",
|
||||
"menu": {
|
||||
"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",
|
||||
"presets": "Setari implicite",
|
||||
"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",
|
||||
"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": {
|
||||
"description": "Controleaza redarea din Bara de Activitati Windows",
|
||||
"name": "Control media in Bara de Activitate"
|
||||
|
||||
@ -279,6 +279,13 @@
|
||||
},
|
||||
"name": "Режим Ambient"
|
||||
},
|
||||
"amuse": {
|
||||
"description": "Добавляет поддержку виджета Amuse „сейчас играет“ от 6K Labs",
|
||||
"name": "Amuse",
|
||||
"response": {
|
||||
"query": "Сервер Amuse API запущен. GET /query чтобы получить информацию о треке."
|
||||
}
|
||||
},
|
||||
"api-server": {
|
||||
"description": "Добавляет API сервер для контроля за плеером",
|
||||
"dialog": {
|
||||
@ -458,7 +465,7 @@
|
||||
"menu": {
|
||||
"choose-download-folder": "Выберите папку для загрузок",
|
||||
"download-finish-settings": {
|
||||
"label": "Скачать по завершении",
|
||||
"label": "Скачать по завершению",
|
||||
"prompt": {
|
||||
"last-percent": "После х процентов",
|
||||
"last-seconds": "Осталось x сек",
|
||||
@ -467,8 +474,8 @@
|
||||
"submenu": {
|
||||
"advanced": "Расширенные настройки",
|
||||
"enabled": "Включено",
|
||||
"mode": "Врмеменной режим",
|
||||
"percent": "Проценты",
|
||||
"mode": "Режим по времени",
|
||||
"percent": "Процент",
|
||||
"seconds": "Секунды"
|
||||
}
|
||||
},
|
||||
@ -484,6 +491,18 @@
|
||||
"button": "Скачать"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "Добавляет эквалайзер к плееру",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "Предустановки",
|
||||
"list": {
|
||||
"bass-booster": "Усилитель баса"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Эквалайзер"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "Делает слайдер громкости расширенным чтобы было легче понижать громкость.",
|
||||
"name": "Расширенная громкость"
|
||||
@ -652,7 +671,7 @@
|
||||
"dialog": {
|
||||
"lastfm": {
|
||||
"auth-failed": {
|
||||
"message": "Не удалось войти с помощью Last.fm\nСкрыть сообщение до следующего запуска",
|
||||
"message": "Не удалось войти с помощью Last.fm\nСкрыть сообщение до следующего запуска.",
|
||||
"title": "Ошибка аунтефикации"
|
||||
}
|
||||
}
|
||||
@ -714,8 +733,8 @@
|
||||
"synced-lyrics": {
|
||||
"description": "Предоставляет синхронизированные слова для песен из таких источников, как LRClib.",
|
||||
"errors": {
|
||||
"fetch": "⚠️ - Возникла ошибка во время получения слов. Повторите попытку позже.",
|
||||
"not-found": "⚠️ - Для этой песни не найдено слов."
|
||||
"fetch": "⚠️\tПроизошла ошибка во время получения слов.\n\tПовторите попытку позже.",
|
||||
"not-found": "⚠️ Для этой песни не найдено слов."
|
||||
},
|
||||
"menu": {
|
||||
"default-text-string": {
|
||||
@ -725,6 +744,10 @@
|
||||
"line-effect": {
|
||||
"label": "Эффект строки",
|
||||
"submenu": {
|
||||
"fancy": {
|
||||
"label": "Красивый",
|
||||
"tooltip": "Использовать большие эффекты строки, как в приложении"
|
||||
},
|
||||
"focus": {
|
||||
"label": "Фокусировка",
|
||||
"tooltip": "Делает только текущую строку белой"
|
||||
@ -753,6 +776,7 @@
|
||||
"tooltip": "Показывает временные метки рядом со словами"
|
||||
}
|
||||
},
|
||||
"name": "Синхронизированные тексты песен",
|
||||
"refetch-btn": {
|
||||
"fetching": "Сбор данных...",
|
||||
"normal": "Обновить слова"
|
||||
|
||||
@ -31,11 +31,74 @@
|
||||
},
|
||||
"theme": {
|
||||
"css-file-not-found": "සීඑස්එස් ගොනුව \"{{cssFile}}\" නොපවතී, නොසලකා හැරීම"
|
||||
},
|
||||
"unresponsive": {
|
||||
"details": "ප්රතිචාර නොදක්වන දෝෂයක් {{error}}"
|
||||
},
|
||||
"when-ready": {
|
||||
"clearing-cache-after-20s": "යෙදුම් කෑශ් නිදහස් කරමින්"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"hide-menu-enabled": {
|
||||
"detail": "මෙනුව සැගවී ඇත, 'Alt' යතුර නැවත පෙන්වීමට භාවිතා කරන්න. (හෝ In-App මෙනුවේ '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 සකසන්න"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"console": {
|
||||
"plugins": {
|
||||
"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}}\"",
|
||||
"load-all": "Nalaganje dodatkov",
|
||||
"load-failed": "Napaka pri nalaganju dodatka \"{{pluginName}}\"",
|
||||
@ -36,7 +36,7 @@
|
||||
"details": "Neodzivna napaka!\n{{error}}"
|
||||
},
|
||||
"when-ready": {
|
||||
"clearing-cache-after-20s": "Čiščenje predpolnilnika"
|
||||
"clearing-cache-after-20s": "Čiščenje predpomnilnika"
|
||||
},
|
||||
"window": {
|
||||
"tried-to-render-offscreen": "Okno se je poskusilo prikazati izven ekrana, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
|
||||
@ -46,13 +46,14 @@
|
||||
"hide-menu-enabled": {
|
||||
"detail": "Meni je skrit, pritisni 'Alt' za odpiranje (ali 'Escape' če uporabljaš In-App Meni)",
|
||||
"message": "Skriti meni je prižgan",
|
||||
"title": "Skrij meni uklopljen"
|
||||
"title": "Skrij meni vklopljen"
|
||||
},
|
||||
"need-to-restart": {
|
||||
"buttons": {
|
||||
"later": "Kasneje",
|
||||
"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",
|
||||
"title": "Potreben je ponovni zagon"
|
||||
},
|
||||
@ -62,7 +63,7 @@
|
||||
"relaunch": "Ponovno zaženi",
|
||||
"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",
|
||||
"title": "Okno se ne odziva"
|
||||
},
|
||||
@ -82,7 +83,7 @@
|
||||
"navigation": {
|
||||
"label": "Navigacija",
|
||||
"submenu": {
|
||||
"copy-current-url": "Kopiraj trenuten URL",
|
||||
"copy-current-url": "Kopiraj trenutni URL",
|
||||
"go-back": "Nazaj",
|
||||
"go-forward": "Naprej",
|
||||
"quit": "Izhod",
|
||||
@ -95,7 +96,7 @@
|
||||
"advanced-options": {
|
||||
"label": "Dodatne nastavitve",
|
||||
"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",
|
||||
"edit-config-json": "Spremeni config.json",
|
||||
"override-user-agent": "Prepiši User-Agent",
|
||||
@ -103,7 +104,7 @@
|
||||
"set-proxy": {
|
||||
"label": "Nastavi proxy",
|
||||
"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",
|
||||
"title": "Nastavi Proxy"
|
||||
}
|
||||
@ -115,7 +116,7 @@
|
||||
"auto-update": "Avtomatsko posodobi",
|
||||
"hide-menu": {
|
||||
"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"
|
||||
},
|
||||
"label": "Skrij meni"
|
||||
@ -127,12 +128,12 @@
|
||||
},
|
||||
"label": "Jezik",
|
||||
"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",
|
||||
"start-at-login": "Prižgi pri zagonu",
|
||||
"start-at-login": "Zaženi pri zagonu",
|
||||
"starting-page": {
|
||||
"label": "Začetna stran",
|
||||
"unset": "Ni nastavljeno"
|
||||
@ -153,7 +154,7 @@
|
||||
"default": "Privzeto",
|
||||
"force-show": "Prisilno pokaži",
|
||||
"hide": "Skrij",
|
||||
"label": "Gumb všeč mi je"
|
||||
"label": "Gumbi za všečkanje"
|
||||
},
|
||||
"remove-upgrade-button": "Odstrani gumb za nadgradnjo",
|
||||
"theme": {
|
||||
@ -162,8 +163,8 @@
|
||||
"cancel": "Prekliči",
|
||||
"remove": "Odstrani"
|
||||
},
|
||||
"remove-theme": "Ali želite odstraniti poljubno temo?",
|
||||
"remove-theme-message": "Poljubna tema bo odtranjena"
|
||||
"remove-theme": "Ali želite odstraniti temo po meri?",
|
||||
"remove-theme-message": "Tema po meri bo odstranjena"
|
||||
},
|
||||
"label": "Tema",
|
||||
"submenu": {
|
||||
@ -206,12 +207,16 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"description": "Izklopi vse oglase od začetka",
|
||||
"description": "Izklopi vse oglase in sledenje",
|
||||
"menu": {
|
||||
"blocker": "Blocker"
|
||||
"blocker": "Blokator"
|
||||
},
|
||||
"name": "Ad Blocker"
|
||||
"name": "Blokator reklam"
|
||||
},
|
||||
"album-actions": {
|
||||
"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",
|
||||
"menu": {
|
||||
"color-mix-ratio": {
|
||||
"label": "Raznerje barv",
|
||||
"label": "Razmerje barv",
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Tema Brav Albuma"
|
||||
"name": "Barvna tema Albuma"
|
||||
},
|
||||
"ambient-mode": {
|
||||
"description": "Doda bravn efekt iz video posnetka na ozadje",
|
||||
"description": "Doda barvni učinek iz video posnetka na ozadje",
|
||||
"menu": {
|
||||
"blur-amount": {
|
||||
"label": "količina zameglitve",
|
||||
"label": "Stopnja zameglitve",
|
||||
"submenu": {
|
||||
"pixels": "{{blurAmount}} pikslov"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"label": "Medpolnilnik",
|
||||
"label": "Medpomnilnik",
|
||||
"submenu": {
|
||||
"buffer": "{{buffer}}"
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"label": "Nepreglednost",
|
||||
"label": "Prozornost",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
@ -269,11 +274,54 @@
|
||||
}
|
||||
},
|
||||
"use-fullscreen": {
|
||||
"label": "Uporablja cel zaslon"
|
||||
"label": "Uporablja celoten zaslon"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"description": "Doda kompresijo zvoka (izenači ravni zvoka, zniža glasnost najglasnejših delov in zviša najtišje)",
|
||||
"name": "Kompresija zvoka"
|
||||
@ -289,7 +337,7 @@
|
||||
"captions-selector": {
|
||||
"description": "Izberi podnapise za YouTube Music zvočne posnetke",
|
||||
"menu": {
|
||||
"autoload": "Avtomatsko uporabi zanje izbrane podnapise",
|
||||
"autoload": "Avtomatsko uporabi zadnje izbrane podnapise",
|
||||
"disable-captions": "Avtomatsko brez podnapisov"
|
||||
},
|
||||
"name": "Izberi podnapise",
|
||||
@ -301,7 +349,7 @@
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"title": "Odpri izbir podnapisov"
|
||||
"title": "Odpri izbiro podnapisov"
|
||||
}
|
||||
},
|
||||
"compact-sidebar": {
|
||||
@ -320,15 +368,186 @@
|
||||
"fade-in-duration": "Čas zbledenja (v pesem) (ms)",
|
||||
"fade-out-duration": "Čas zbledenja (izven pesemi) (ms)",
|
||||
"fade-scaling": {
|
||||
"label": "Fade scaling",
|
||||
"label": "Zbledi skaliranje",
|
||||
"linear": "Linearno",
|
||||
"logarithmic": "Logaritmično"
|
||||
},
|
||||
"seconds-before-end": "Crossfade N seconds before end"
|
||||
"seconds-before-end": "Bledenje (crossfade) N sekund pred koncem"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,6 +207,10 @@
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"ad-speedup": {
|
||||
"description": "หากมีการเล่นโฆษณา เสียงจะถูกปิดและตั้งค่าความเร็วในการเล่นเป็น 16x",
|
||||
"name": "เพิ่มความเร็วโฆษณา"
|
||||
},
|
||||
"adblocker": {
|
||||
"description": "บล็อกโฆษณาและการติดตามทั้งหมดอย่างอัตโนมัติ",
|
||||
"menu": {
|
||||
@ -275,6 +279,18 @@
|
||||
},
|
||||
"name": "โหมดสภาพแวดล้อม"
|
||||
},
|
||||
"api-server": {
|
||||
"description": "เพิ่มเซิร์ฟเวอร์ API เพื่อควบคุมการเล่น",
|
||||
"dialog": {
|
||||
"request": {
|
||||
"buttons": {
|
||||
"allow": "อนุญาต",
|
||||
"deny": "ปฏิเสธ"
|
||||
},
|
||||
"message": "อนุญาตให้ {{ID}} ({{origin}}) เข้าถึง API หรือไม่?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"audio-compressor": {
|
||||
"description": "ใช้การบีบอัดเสียง (ลดระดับเสียงของส่วนที่ดังที่สุดของสัญญาณและเพิ่มระดับเสียงของส่วนที่เบาที่สุด)",
|
||||
"name": "เครื่องมือบีบอัดเสียง"
|
||||
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
|
||||
@ -209,7 +209,7 @@
|
||||
"plugins": {
|
||||
"ad-speedup": {
|
||||
"description": "При програванні реклами звук вимикається і встановлюється швидкість відтворення 16х",
|
||||
"name": "Прискорення реклами"
|
||||
"name": "Пришвидшення релками"
|
||||
},
|
||||
"adblocker": {
|
||||
"description": "Блокувати всю рекламу та відстеження з коробки",
|
||||
|
||||
134
src/i18n/resources/ur.json
Normal file
134
src/i18n/resources/ur.json
Normal 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": "زبان بدل گئی ہے"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
@ -671,7 +714,7 @@
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"menu": {
|
||||
@ -686,6 +729,10 @@
|
||||
"label": "Tập trung",
|
||||
"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": {
|
||||
"label": "Tỉ lệ",
|
||||
"tooltip": "Áp dụng tỉ lệ cho dòng hiện tại"
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
"clearing-cache-after-20s": "正在清理应用缓存"
|
||||
},
|
||||
"window": {
|
||||
"tried-to-render-offscreen": "窗口试图于屏幕外绘制, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
|
||||
"tried-to-render-offscreen": "窗口试图于屏幕外绘制,窗口大小={{windowSize}},显示尺寸={{displaySize}},位置={{position}}"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
@ -279,6 +279,49 @@
|
||||
},
|
||||
"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": {
|
||||
"description": "对音频应用压缩(压低响亮部分,提升柔和部分)",
|
||||
"name": "音频压缩器"
|
||||
@ -441,6 +484,18 @@
|
||||
"button": "下载"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "为播放器添加均衡器",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "预设",
|
||||
"list": {
|
||||
"bass-booster": "低音增强器"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "均衡器"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "让音量滑块指数化以便选择更低的音量。",
|
||||
"name": "指数化音量"
|
||||
@ -459,11 +514,11 @@
|
||||
"lyrics-genius": {
|
||||
"description": "为大多数歌曲添加歌词支持",
|
||||
"menu": {
|
||||
"romanized-lyrics": "罗马化字幕"
|
||||
"romanized-lyrics": "罗马化歌词"
|
||||
},
|
||||
"name": "Genius 歌词",
|
||||
"renderer": {
|
||||
"fetched-lyrics": "已从 Genius 获取字幕"
|
||||
"fetched-lyrics": "已从 Genius 获取歌词"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
@ -478,12 +533,12 @@
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "复制发起者 ID",
|
||||
"close": "关闭 Music Together",
|
||||
"close": "关闭一起听",
|
||||
"connected-users": "已连接用户",
|
||||
"disconnect": "断开 Music Together 连接",
|
||||
"disconnect": "断开一起听连接",
|
||||
"empty-user": "没有已连接的用户",
|
||||
"host": "Music Together 发起者",
|
||||
"join": "加入 Music Together",
|
||||
"host": "一起听发起者",
|
||||
"join": "加入一起听",
|
||||
"permission": {
|
||||
"all": "允许来宾控制播放列表与播放器",
|
||||
"host-only": "仅发起人可以控制播放列表与播放器",
|
||||
@ -496,20 +551,20 @@
|
||||
"host": "已作为发起人连接"
|
||||
}
|
||||
},
|
||||
"name": "Music Together [测试]",
|
||||
"name": "一起听 [测试]",
|
||||
"toast": {
|
||||
"add-song-failed": "添加歌曲失败",
|
||||
"closed": "Music Together 已关闭",
|
||||
"disconnected": "Music Together 已断开连接",
|
||||
"host-failed": "发起 Music Together 失败",
|
||||
"closed": "一起听已关闭",
|
||||
"disconnected": "一起听已断开连接",
|
||||
"host-failed": "发起一起听失败",
|
||||
"id-copied": "已将发起者 ID 复制到剪切板",
|
||||
"id-copy-failed": "复制发起者 ID 到剪贴板时失败",
|
||||
"join-failed": "加入 Music Together 失败",
|
||||
"joined": "已加入 Music Together",
|
||||
"permission-changed": "Music Together 权限已改为 \"{{permission}}\"",
|
||||
"join-failed": "加入一起听失败",
|
||||
"joined": "已加入一起听",
|
||||
"permission-changed": "一起听权限已改为 \"{{permission}}\"",
|
||||
"remove-song-failed": "移除歌曲失败",
|
||||
"user-connected": "{{name}} 加入了 Music Together",
|
||||
"user-disconnected": "{{name}} 离开了 Music Together"
|
||||
"user-connected": "{{name}} 加入了一起听",
|
||||
"user-disconnected": "{{name}} 离开了一起听"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
@ -631,7 +686,7 @@
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": {
|
||||
"label": "输入您的v ListenBrainz 用户令牌:",
|
||||
"label": "输入您的 ListenBrainz 用户令牌:",
|
||||
"title": "ListenBrainz 令牌"
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,6 +279,13 @@
|
||||
},
|
||||
"name": "微光效果"
|
||||
},
|
||||
"amuse": {
|
||||
"description": "加入支援 6K Labs 的 Amuse OBS 外掛以取得 Youtube Music 現正播放資訊",
|
||||
"name": "Amuse",
|
||||
"response": {
|
||||
"query": "Amuse API 伺服器正在運行中,使用 /query 以取得歌曲資訊。"
|
||||
}
|
||||
},
|
||||
"api-server": {
|
||||
"description": "新增伺服器以使用 API 控制播放器",
|
||||
"dialog": {
|
||||
@ -484,6 +491,18 @@
|
||||
"button": "下載"
|
||||
}
|
||||
},
|
||||
"equalizer": {
|
||||
"description": "為播放器加入等化器",
|
||||
"menu": {
|
||||
"presets": {
|
||||
"label": "預設格式",
|
||||
"list": {
|
||||
"bass-booster": "低音增強器"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "等化器"
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "使音量滑桿指數化,以便更容易選擇較低的音量。",
|
||||
"name": "指數化音量調整"
|
||||
@ -506,7 +525,7 @@
|
||||
},
|
||||
"name": "第三方字幕",
|
||||
"renderer": {
|
||||
"fetched-lyrics": "為Genius獲取字幕"
|
||||
"fetched-lyrics": "為 Genius 獲取字幕"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
@ -714,7 +733,7 @@
|
||||
"synced-lyrics": {
|
||||
"description": "使用 LRClib 等管道提供歌詞同步顯示。",
|
||||
"errors": {
|
||||
"fetch": "⚠️擷取歌詞時發生錯誤。請稍後再試。",
|
||||
"fetch": "⚠️\t擷取歌詞時發生錯誤\n請稍後再試。",
|
||||
"not-found": "⚠️未找到該首歌曲的歌詞。"
|
||||
},
|
||||
"menu": {
|
||||
@ -725,6 +744,10 @@
|
||||
"line-effect": {
|
||||
"label": "歌詞顯示效果",
|
||||
"submenu": {
|
||||
"fancy": {
|
||||
"label": "絢麗",
|
||||
"tooltip": "使用較為接近原生樣式並且放大當前該行歌詞"
|
||||
},
|
||||
"focus": {
|
||||
"label": "高亮",
|
||||
"tooltip": "高亮當前的歌詞"
|
||||
@ -761,7 +784,7 @@
|
||||
"warnings": {
|
||||
"duration-mismatch": "⚠️歌詞可能會出現不同步的情況。",
|
||||
"inexact": "⚠️該歌曲的歌詞可能並不精確",
|
||||
"instrumental": "⚠️該首歌曲並無人聲"
|
||||
"instrumental": "⚠️該首歌曲為純音樂"
|
||||
}
|
||||
},
|
||||
"taskbar-mediacontrol": {
|
||||
|
||||
43
src/index.ts
43
src/index.ts
@ -131,17 +131,21 @@ if (config.get('options.disableHardwareAcceleration')) {
|
||||
}
|
||||
|
||||
if (is.linux()) {
|
||||
const disabledFeatures = [
|
||||
// Workaround for issue #2248
|
||||
'UseMultiPlaneFormatForSoftwareVideo',
|
||||
];
|
||||
// Overrides WM_CLASS for X11 to correspond to icon filename
|
||||
app.setName('com.github.th_ch.youtube_music');
|
||||
|
||||
// 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
|
||||
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')) {
|
||||
@ -501,10 +505,11 @@ app.once('browser-window-created', (_event, win) => {
|
||||
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
|
||||
const originalUserAgent = win.webContents.userAgent;
|
||||
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:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||
linux: 'Mozilla/5.0 (Linux x86_64; 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 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.152 Safari/537.36',
|
||||
};
|
||||
|
||||
const updatedUserAgent = is.macOS()
|
||||
@ -900,9 +905,21 @@ function removeContentSecurityPolicy(
|
||||
betterSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
details.responseHeaders ??= {};
|
||||
|
||||
// Remove the content security policy
|
||||
delete details.responseHeaders['content-security-policy-report-only'];
|
||||
delete details.responseHeaders['content-security-policy'];
|
||||
// prettier-ignore
|
||||
if (new URL(details.url).protocol === 'https:') {
|
||||
// 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 });
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import path from 'node:path';
|
||||
import fs, { promises } from 'node:fs';
|
||||
|
||||
import { ElectronBlocker } from '@cliqz/adblocker-electron';
|
||||
import { ElectronBlocker } from '@ghostery/adblocker-electron';
|
||||
import { app, net } from 'electron';
|
||||
|
||||
const SOURCES = [
|
||||
@ -55,6 +55,7 @@ export const loadAdBlockerEngine = async (
|
||||
(url: string) => net.fetch(url),
|
||||
lists,
|
||||
{
|
||||
enableCompression: true,
|
||||
// When generating the engine for caching, do not load network filters
|
||||
// So that enhancing the session works as expected
|
||||
// Allowing to define multiple webRequest listeners
|
||||
@ -66,7 +67,7 @@ export const loadAdBlockerEngine = async (
|
||||
blocker.enableBlockingInSession(session);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error loading adBlocker engine', error);
|
||||
console.error('Error loading adBlocker engine', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
unloadAdBlockerEngine,
|
||||
} from './blocker';
|
||||
|
||||
import injectCliqzPreload from './injectors/inject-cliqz-preload';
|
||||
import { inject, isInjected } from './injectors/inject';
|
||||
import { loadAdSpeedup } from './adSpeedup';
|
||||
|
||||
@ -134,18 +133,13 @@ export default createPlugin({
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.blocker === blockers.WithBlocklists) {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
await injectCliqzPreload();
|
||||
} else if (config.blocker === blockers.InPlayer && !isInjected()) {
|
||||
if (config.blocker === blockers.InPlayer && !isInjected()) {
|
||||
inject(contextBridge);
|
||||
await webFrame.executeJavaScript(this.script);
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
if (newConfig.blocker === blockers.WithBlocklists) {
|
||||
await injectCliqzPreload();
|
||||
} else if (newConfig.blocker === blockers.InPlayer && !isInjected()) {
|
||||
if (newConfig.blocker === blockers.InPlayer && !isInjected()) {
|
||||
inject(contextBridge);
|
||||
await webFrame.executeJavaScript(this.script);
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export default async () => {
|
||||
await import('@cliqz/adblocker-electron-preload');
|
||||
await import('@ghostery/adblocker-electron-preload');
|
||||
};
|
||||
|
||||
@ -133,9 +133,11 @@ export default createPlugin<
|
||||
}
|
||||
},
|
||||
loadFullList(event: MouseEvent) {
|
||||
if (event.currentTarget instanceof Element) {
|
||||
if (event.target instanceof Element) {
|
||||
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')!;
|
||||
this.loadObserver = new MutationObserver(() => {
|
||||
this.applyToList(id, loader);
|
||||
|
||||
@ -34,3 +34,12 @@
|
||||
margin: 0 auto !important;
|
||||
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;
|
||||
}
|
||||
|
||||
71
src/plugins/amuse/backend.ts
Normal file
71
src/plugins/amuse/backend.ts
Normal 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();
|
||||
}
|
||||
},
|
||||
});
|
||||
20
src/plugins/amuse/index.ts
Normal file
20
src/plugins/amuse/index.ts
Normal 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,
|
||||
});
|
||||
20
src/plugins/amuse/types.ts
Normal file
20
src/plugins/amuse/types.ts
Normal 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;
|
||||
}
|
||||
@ -13,6 +13,7 @@ import { registerAuth, registerControl } from './routes';
|
||||
import { type APIServerConfig, AuthStrategy } from '../config';
|
||||
|
||||
import type { BackendType } from './types';
|
||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
||||
|
||||
export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
async start(ctx) {
|
||||
@ -23,6 +24,16 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
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);
|
||||
},
|
||||
stop() {
|
||||
@ -49,6 +60,12 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
|
||||
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
|
||||
this.app.use('/api/*', async (ctx, next) => {
|
||||
if (config.authStrategy !== AuthStrategy.NONE) {
|
||||
@ -73,7 +90,12 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
});
|
||||
|
||||
// routes
|
||||
registerControl(this.app, ctx, () => this.songInfo);
|
||||
registerControl(
|
||||
this.app,
|
||||
ctx,
|
||||
() => this.songInfo,
|
||||
() => this.currentRepeatMode,
|
||||
);
|
||||
registerAuth(this.app, ctx);
|
||||
|
||||
// swagger
|
||||
|
||||
@ -5,21 +5,28 @@ import { ipcMain } from 'electron';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
|
||||
import {
|
||||
AuthHeadersSchema,
|
||||
type ResponseSongInfo,
|
||||
SongInfoSchema,
|
||||
GoForwardScheme,
|
||||
AddSongToQueueSchema,
|
||||
GoBackSchema,
|
||||
SwitchRepeatSchema,
|
||||
SetVolumeSchema,
|
||||
GoForwardScheme,
|
||||
MoveSongInQueueSchema,
|
||||
QueueParamsSchema,
|
||||
SearchSchema,
|
||||
SeekSchema,
|
||||
SetFullscreenSchema,
|
||||
SetQueueIndexSchema,
|
||||
SetVolumeSchema,
|
||||
SongInfoSchema,
|
||||
SwitchRepeatSchema,
|
||||
type ResponseSongInfo,
|
||||
} from '../scheme';
|
||||
|
||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
import type { APIServerConfig } from '../../config';
|
||||
import type { HonoApp } from '../types';
|
||||
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
||||
import type { Context } from 'hono';
|
||||
|
||||
const API_VERSION = 'v1';
|
||||
|
||||
@ -102,14 +109,33 @@ const routes = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
seekTo: createRoute({
|
||||
method: 'post',
|
||||
path: `/api/${API_VERSION}/seek-to`,
|
||||
summary: 'seek',
|
||||
description: 'Seek to a specific time in the current song',
|
||||
request: {
|
||||
body: {
|
||||
description: 'seconds to seek to',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SeekSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
description: 'Success',
|
||||
},
|
||||
},
|
||||
}),
|
||||
goBack: createRoute({
|
||||
method: 'post',
|
||||
path: `/api/${API_VERSION}/go-back`,
|
||||
summary: 'go back',
|
||||
description: 'Move the current song back by a number of seconds',
|
||||
request: {
|
||||
headers: AuthHeadersSchema,
|
||||
body: {
|
||||
description: 'seconds to go back',
|
||||
content: {
|
||||
@ -132,7 +158,6 @@ const routes = {
|
||||
summary: 'go forward',
|
||||
description: 'Move the current song forward by a number of seconds',
|
||||
request: {
|
||||
headers: AuthHeadersSchema,
|
||||
body: {
|
||||
description: 'seconds to go forward',
|
||||
content: {
|
||||
@ -160,13 +185,30 @@ const routes = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
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({
|
||||
method: 'post',
|
||||
path: `/api/${API_VERSION}/switch-repeat`,
|
||||
summary: 'switch repeat',
|
||||
description: 'Switch the repeat mode',
|
||||
request: {
|
||||
headers: AuthHeadersSchema,
|
||||
body: {
|
||||
description: 'number of times to click the repeat button',
|
||||
content: {
|
||||
@ -188,7 +230,6 @@ const routes = {
|
||||
summary: 'set volume',
|
||||
description: 'Set the volume of the player',
|
||||
request: {
|
||||
headers: AuthHeadersSchema,
|
||||
body: {
|
||||
description: 'volume to set',
|
||||
content: {
|
||||
@ -210,7 +251,6 @@ const routes = {
|
||||
summary: 'set fullscreen',
|
||||
description: 'Set the fullscreen state of the player',
|
||||
request: {
|
||||
headers: AuthHeadersSchema,
|
||||
body: {
|
||||
description: 'fullscreen state',
|
||||
content: {
|
||||
@ -256,7 +296,8 @@ const routes = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
queueInfo: createRoute({
|
||||
oldQueueInfo: createRoute({
|
||||
deprecated: true,
|
||||
method: 'get',
|
||||
path: `/api/${API_VERSION}/queue-info`,
|
||||
summary: 'get current queue info',
|
||||
@ -275,7 +316,8 @@ const routes = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
songInfo: createRoute({
|
||||
oldSongInfo: createRoute({
|
||||
deprecated: true,
|
||||
method: 'get',
|
||||
path: `/api/${API_VERSION}/song-info`,
|
||||
summary: 'get current song info',
|
||||
@ -294,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 = (
|
||||
app: HonoApp,
|
||||
{ window }: BackendContext<APIServerConfig>,
|
||||
songInfoGetter: () => SongInfo | undefined,
|
||||
repeatModeGetter: () => RepeatMode | undefined,
|
||||
) => {
|
||||
const controller = getSongControls(window);
|
||||
|
||||
@ -345,6 +541,13 @@ export const register = (
|
||||
ctx.status(204);
|
||||
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) => {
|
||||
const { seconds } = ctx.req.valid('json');
|
||||
controller.goBack(seconds);
|
||||
@ -365,6 +568,11 @@ export const register = (
|
||||
ctx.status(204);
|
||||
return ctx.body(null);
|
||||
});
|
||||
|
||||
app.openapi(routes.repeatMode, (ctx) => {
|
||||
ctx.status(200);
|
||||
return ctx.json({ mode: repeatModeGetter() ?? null });
|
||||
});
|
||||
app.openapi(routes.switchRepeat, (ctx) => {
|
||||
const { iteration } = ctx.req.valid('json');
|
||||
controller.switchRepeat(iteration);
|
||||
@ -410,7 +618,26 @@ export const register = (
|
||||
ctx.status(200);
|
||||
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) => {
|
||||
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
|
||||
return resolve(queue);
|
||||
@ -428,19 +655,50 @@ export const register = (
|
||||
|
||||
ctx.status(200);
|
||||
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) => {
|
||||
const info = songInfoGetter();
|
||||
app.openapi(routes.moveSongInQueue, (ctx) => {
|
||||
const index = Number(ctx.req.param('index'));
|
||||
const { toIndex } = ctx.req.valid('json');
|
||||
controller.moveSongInQueue(index, toIndex);
|
||||
|
||||
if (!info) {
|
||||
ctx.status(204);
|
||||
return ctx.body(null);
|
||||
}
|
||||
ctx.status(204);
|
||||
return ctx.body(null);
|
||||
});
|
||||
app.openapi(routes.removeSongFromQueue, (ctx) => {
|
||||
const index = Number(ctx.req.param('index'));
|
||||
controller.removeSongFromQueue(index);
|
||||
|
||||
const body = { ...info };
|
||||
delete body.image;
|
||||
ctx.status(204);
|
||||
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);
|
||||
return ctx.json(body satisfies ResponseSongInfo);
|
||||
return ctx.json(response as object);
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
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 const JWTPayloadSchema = z.object({
|
||||
id: z.string(),
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
export * from './auth';
|
||||
export * from './song-info';
|
||||
export * from './seek';
|
||||
export * from './go-back';
|
||||
export * from './go-forward';
|
||||
export * from './switch-repeat';
|
||||
export * from './set-volume';
|
||||
export * from './set-fullscreen';
|
||||
export * from './queue';
|
||||
export * from './search';
|
||||
|
||||
15
src/plugins/api-server/backend/scheme/queue.ts
Normal file
15
src/plugins/api-server/backend/scheme/queue.ts
Normal 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(),
|
||||
});
|
||||
5
src/plugins/api-server/backend/scheme/search.ts
Normal file
5
src/plugins/api-server/backend/scheme/search.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { z } from '@hono/zod-openapi';
|
||||
|
||||
export const SearchSchema = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
5
src/plugins/api-server/backend/scheme/seek.ts
Normal file
5
src/plugins/api-server/backend/scheme/seek.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { z } from '@hono/zod-openapi';
|
||||
|
||||
export const SeekSchema = z.object({
|
||||
seconds: z.number(),
|
||||
});
|
||||
@ -3,6 +3,7 @@ import { serve } from '@hono/node-server';
|
||||
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
||||
import type { APIServerConfig } from '../config';
|
||||
|
||||
export type HonoApp = Hono;
|
||||
@ -11,6 +12,7 @@ export type BackendType = {
|
||||
server?: ReturnType<typeof serve>;
|
||||
oldConfig?: APIServerConfig;
|
||||
songInfo?: SongInfo;
|
||||
currentRepeatMode?: RepeatMode;
|
||||
|
||||
init: (ctx: BackendContext<APIServerConfig>) => Promise<void>;
|
||||
run: (hostname: string, port: number) => void;
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
}
|
||||
|
||||
ytmusic-tabs {
|
||||
top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px));
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { app, dialog, ipcMain } from 'electron';
|
||||
import { app, dialog } from 'electron';
|
||||
import { Client as DiscordClient } from '@xhayper/discord-rpc';
|
||||
import { dev } from 'electron-is';
|
||||
|
||||
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 { t } from '@/i18n';
|
||||
|
||||
@ -107,7 +110,7 @@ export const clear = () => {
|
||||
};
|
||||
|
||||
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
|
||||
export const isConnected = () => info.rpc !== null;
|
||||
export const isConnected = () => info.rpc?.isConnected;
|
||||
|
||||
export const backend = createBackend<
|
||||
{
|
||||
@ -243,25 +246,28 @@ export const backend = createBackend<
|
||||
|
||||
// If the page is ready, register the callback
|
||||
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();
|
||||
ipcMain.on('ytmd:time-changed', (_, t: number) => {
|
||||
const currentTime = Date.now();
|
||||
// if lastSent is more than 5 seconds ago, send the new time
|
||||
if (currentTime - lastSent > 5000) {
|
||||
lastSent = currentTime;
|
||||
if (lastSongInfo) {
|
||||
lastSongInfo.elapsedSeconds = t;
|
||||
if (this.config) this.updateActivity(lastSongInfo, this.config);
|
||||
registerCallback((songInfo, event) => {
|
||||
if (event !== SongInfoEvent.TimeChanged) {
|
||||
info.lastSongInfo = songInfo;
|
||||
if (this.config) this.updateActivity(songInfo, this.config);
|
||||
} else {
|
||||
const currentTime = Date.now();
|
||||
// if lastSent is more than 5 seconds ago, send the new time
|
||||
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);
|
||||
},
|
||||
stop() {
|
||||
|
||||
@ -30,12 +30,13 @@ import registerCallback, {
|
||||
getImage,
|
||||
MediaType,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
import { getNetFetchAsFetch } from '@/plugins/utils/main';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
|
||||
import { DefaultPresetList, type Preset, YoutubeFormatList } from '../types';
|
||||
|
||||
import type { DownloaderPluginConfig } from '../index';
|
||||
|
||||
@ -62,13 +63,23 @@ let yt: Innertube;
|
||||
let win: BrowserWindow;
|
||||
let playingUrl: string;
|
||||
|
||||
const isYouTubePremium = () =>
|
||||
win.webContents.executeJavaScript(
|
||||
'!document.querySelector(\'#endpoint[href="/music_premium"]\')',
|
||||
) as Promise<boolean>;
|
||||
|
||||
const sendError = (error: Error, source?: string) => {
|
||||
win.setProgressBar(-1); // Close progress bar
|
||||
setBadge(0); // Close badge
|
||||
sendFeedback_(win); // Reset feedback
|
||||
|
||||
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}`;
|
||||
|
||||
console.error(message);
|
||||
@ -174,7 +185,12 @@ function downloadSongOnFinishSetup({
|
||||
|
||||
const defaultDownloadFolder = app.getPath('downloads');
|
||||
|
||||
registerCallback((songInfo: SongInfo) => {
|
||||
registerCallback((songInfo: SongInfo, event) => {
|
||||
if (event === SongInfoEvent.TimeChanged) {
|
||||
const elapsedSeconds = songInfo.elapsedSeconds ?? 0;
|
||||
if (elapsedSeconds > time) time = elapsedSeconds;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!songInfo.isPaused &&
|
||||
songInfo.url !== currentUrl &&
|
||||
@ -213,10 +229,6 @@ function downloadSongOnFinishSetup({
|
||||
ipcMain.on('ytmd:player-api-loaded', () => {
|
||||
ipc.send('ytmd:setup-time-changed-listener');
|
||||
});
|
||||
|
||||
ipcMain.on('ytmd:time-changed', (_, t: number) => {
|
||||
if (t > time) time = t;
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadSongUnsafe(
|
||||
@ -306,7 +318,7 @@ async function downloadSongUnsafe(
|
||||
}
|
||||
|
||||
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.
|
||||
format: 'any', // Media container format
|
||||
};
|
||||
@ -578,20 +590,17 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!playlist ||
|
||||
!playlist.items ||
|
||||
playlist.items.length === 0 ||
|
||||
!playlist.header ||
|
||||
!('title' in playlist.header)
|
||||
) {
|
||||
if (!playlist || !playlist.items || playlist.items.length === 0) {
|
||||
sendError(
|
||||
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalPlaylistTitle = playlist.header?.title?.text;
|
||||
const normalPlaylistTitle =
|
||||
playlist.header && 'title' in playlist.header
|
||||
? playlist.header?.title?.text
|
||||
: undefined;
|
||||
const playlistTitle =
|
||||
normalPlaylistTitle ??
|
||||
playlist.page.contents_memo
|
||||
|
||||
@ -8,6 +8,8 @@ import { LoggerPrefix } from '@/utils';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
@ -108,7 +110,9 @@ export const onRendererLoad = ({
|
||||
ipc.on('downloader-feedback', (feedback: string) => {
|
||||
if (progress) {
|
||||
const targetHtml = feedback || t('plugins.downloader.templates.button');
|
||||
progress.innerHTML = window.trustedTypes?.defaultPolicy ? window.trustedTypes.defaultPolicy.createHTML(targetHtml) : targetHtml;
|
||||
(progress.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
||||
: targetHtml;
|
||||
} else {
|
||||
console.warn(
|
||||
LoggerPrefix,
|
||||
|
||||
81
src/plugins/equalizer/index.ts
Normal file
81
src/plugins/equalizer/index.ts
Normal 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 = [];
|
||||
},
|
||||
},
|
||||
});
|
||||
18
src/plugins/equalizer/presets.ts
Normal file
18
src/plugins/equalizer/presets.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
@ -5,9 +5,12 @@
|
||||
|
||||
/* youtube-music style */
|
||||
ytmusic-app-layout {
|
||||
overflow: scroll;
|
||||
overflow: auto scroll;
|
||||
height: calc(100vh - var(--menu-bar-height, 36px));
|
||||
margin-top: var(--menu-bar-height, 36px) !important;
|
||||
|
||||
/* fixes laggy list scrolling in large playlists */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
ytmusic-app-layout#layout {
|
||||
--ytmusic-nav-bar-offset: 0px;
|
||||
@ -72,3 +75,8 @@ ytmusic-app-layout ytmusic-player-page[is-mweb-modernization-enabled] .side-pane
|
||||
html {
|
||||
scrollbar-color: unset;
|
||||
}
|
||||
|
||||
/* fixes scrollbar lagging behind in large playlists */
|
||||
ytmusic-browse-response .ytmusic-responsive-list-item-renderer {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ export default createPlugin({
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
backend() {
|
||||
backend({ ipc }) {
|
||||
const secToMilisec = (t?: number) =>
|
||||
t ? Math.round(Number(t) * 1e3) : undefined;
|
||||
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) => {
|
||||
if (!songInfo.title && !songInfo.artist) {
|
||||
return;
|
||||
|
||||
@ -2,6 +2,8 @@ import { LoggerPrefix } from '@/utils';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
||||
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index';
|
||||
@ -20,7 +22,10 @@ export const onRendererLoad = ({
|
||||
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline">
|
||||
</yt-formatted-string>
|
||||
`;
|
||||
lyricsContainer.innerHTML = window.trustedTypes?.defaultPolicy ? window.trustedTypes.defaultPolicy.createHTML(targetHtml) : targetHtml;
|
||||
(lyricsContainer.innerHTML as string | TrustedHTML) =
|
||||
defaultTrustedTypePolicy
|
||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
||||
: targetHtml;
|
||||
|
||||
if (lyrics) {
|
||||
const footer = lyricsContainer.querySelector('.footer');
|
||||
|
||||
@ -18,8 +18,8 @@ export interface Section {
|
||||
|
||||
export interface Hit {
|
||||
highlights: Highlight[];
|
||||
index: Index;
|
||||
type: Index;
|
||||
index: ResultType;
|
||||
type: ResultType;
|
||||
result: Result;
|
||||
}
|
||||
|
||||
@ -35,14 +35,10 @@ export interface Range {
|
||||
end: number;
|
||||
}
|
||||
|
||||
export enum Index {
|
||||
Album = 'album',
|
||||
Lyric = 'lyric',
|
||||
Song = 'song',
|
||||
}
|
||||
export type ResultType = 'song' | 'album' | 'lyric';
|
||||
|
||||
export interface Result {
|
||||
_type: Index;
|
||||
_type: ResultType;
|
||||
annotation_count?: number;
|
||||
api_path: string;
|
||||
artist_names?: string;
|
||||
|
||||
@ -612,7 +612,9 @@ export default createPlugin<
|
||||
|
||||
const accountData = renderer.data as RawAccountData;
|
||||
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,
|
||||
thumbnail: accountData.accountPhoto.thumbnails[0].url,
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import { t } from '@/i18n';
|
||||
import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
|
||||
import type { Profile, VideoData } from '../types';
|
||||
import type { QueueItem } from '@/types/datahost-get-state';
|
||||
import type { QueueElement } from '@/types/queue';
|
||||
import type { QueueElement, Store } from '@/types/queue';
|
||||
|
||||
const getHeaderPayload = (() => {
|
||||
let payload: {
|
||||
@ -266,7 +266,8 @@ export class Queue {
|
||||
}
|
||||
|
||||
if (this.originalDispatch)
|
||||
this.queue.queue.store.store.dispatch = this.originalDispatch;
|
||||
this.queue.queue.store.store.dispatch = this
|
||||
.originalDispatch as Store['dispatch'];
|
||||
}
|
||||
|
||||
injection() {
|
||||
@ -295,7 +296,11 @@ export class Queue {
|
||||
videoId: it!.videoId,
|
||||
ownerId: this.owner!.id,
|
||||
}) satisfies VideoData,
|
||||
event.payload!.items!,
|
||||
(
|
||||
event.payload! as {
|
||||
items: QueueItem[];
|
||||
}
|
||||
).items,
|
||||
);
|
||||
const index = this._videoList.length + videoList.length - 1;
|
||||
|
||||
@ -334,7 +339,11 @@ export class Queue {
|
||||
videoId: it!.videoId,
|
||||
ownerId: this.owner!.id,
|
||||
}) 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;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,10 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac
|
||||
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
||||
|
||||
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 { setTrayOnClick, setTrayOnDoubleClick } from '@/tray';
|
||||
import { mediaIcons } from '@/types/media-icons';
|
||||
@ -258,15 +261,14 @@ export default (
|
||||
let currentSeconds = 0;
|
||||
on('ytmd:player-api-loaded', () => send('ytmd:setup-time-changed-listener'));
|
||||
|
||||
on('ytmd:time-changed', (t: number) => {
|
||||
currentSeconds = t;
|
||||
});
|
||||
|
||||
let savedSongInfo: SongInfo;
|
||||
let lastUrl: string | undefined;
|
||||
|
||||
// Register songInfoCallback
|
||||
registerCallback((songInfo) => {
|
||||
registerCallback((songInfo, event) => {
|
||||
if (event === SongInfoEvent.TimeChanged) {
|
||||
currentSeconds = songInfo.elapsedSeconds ?? 0;
|
||||
}
|
||||
if (!songInfo.artist && !songInfo.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@ import is from 'electron-is';
|
||||
import { notificationImage } from './utils';
|
||||
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 { BackendContext } from '@/types/contexts';
|
||||
@ -30,8 +33,9 @@ const setup = () => {
|
||||
let oldNotification: Notification;
|
||||
let currentUrl: string | undefined;
|
||||
|
||||
registerCallback((songInfo: SongInfo) => {
|
||||
registerCallback((songInfo: SongInfo, event) => {
|
||||
if (
|
||||
event !== SongInfoEvent.TimeChanged &&
|
||||
!songInfo.isPaused &&
|
||||
(songInfo.url !== currentUrl || config.unpauseNotification)
|
||||
) {
|
||||
|
||||
@ -3,6 +3,8 @@ import sliderHTML from './templates/slider.html?raw';
|
||||
import { getSongMenu } from '@/providers/dom-elements';
|
||||
import { singleton } from '@/providers/decorators';
|
||||
|
||||
import { defaultTrustedTypePolicy } from '@/utils/trusted-types';
|
||||
|
||||
import { ElementFromHtml } from '../utils/renderer';
|
||||
|
||||
const slider = ElementFromHtml(sliderHTML);
|
||||
@ -23,7 +25,10 @@ const updatePlayBackSpeed = () => {
|
||||
const playbackSpeedElement = document.querySelector('#playback-speed-value');
|
||||
if (playbackSpeedElement) {
|
||||
const targetHtml = String(playbackSpeed);
|
||||
playbackSpeedElement.innerHTML = window.trustedTypes?.defaultPolicy ? trustedTypes.defaultPolicy.createHTML(targetHtml) : targetHtml;
|
||||
(playbackSpeedElement.innerHTML as string | TrustedHTML) =
|
||||
defaultTrustedTypePolicy
|
||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
||||
: targetHtml;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron';
|
||||
import registerCallback, {
|
||||
MediaType,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
import { createBackend } from '@/utils';
|
||||
|
||||
@ -70,7 +71,8 @@ export const backend = createBackend<
|
||||
await this.createSessions(config, setConfig);
|
||||
this.setConfig = setConfig;
|
||||
|
||||
registerCallback((songInfo: SongInfo) => {
|
||||
registerCallback((songInfo: SongInfo, event) => {
|
||||
if (event === SongInfoEvent.TimeChanged) return;
|
||||
// Set remove the old scrobble timer
|
||||
clearTimeout(scrobbleTimer);
|
||||
if (!songInfo.isPaused) {
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import MprisPlayer, {
|
||||
Track,
|
||||
LoopStatus,
|
||||
type PlayBackStatus,
|
||||
type PlayerOptions,
|
||||
PLAYBACK_STATUS_STOPPED,
|
||||
PLAYBACK_STATUS_PAUSED,
|
||||
PLAYBACK_STATUS_PLAYING,
|
||||
LOOP_STATUS_NONE,
|
||||
LOOP_STATUS_PLAYLIST,
|
||||
LOOP_STATUS_TRACK,
|
||||
LoopStatus,
|
||||
PLAYBACK_STATUS_PAUSED,
|
||||
PLAYBACK_STATUS_PLAYING,
|
||||
PLAYBACK_STATUS_STOPPED,
|
||||
type PlayBackStatus,
|
||||
type PlayerOptions,
|
||||
type Position,
|
||||
Track,
|
||||
} 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 config from '@/config';
|
||||
import { LoggerPrefix } from '@/utils';
|
||||
@ -134,10 +137,6 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
player.seeked(secToMicro(t));
|
||||
});
|
||||
|
||||
ipcMain.on('ytmd:time-changed', (_, t: number) => {
|
||||
player.setPosition(secToMicro(t));
|
||||
});
|
||||
|
||||
ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
|
||||
switch (mode) {
|
||||
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) {
|
||||
const data: Track = {
|
||||
'mpris:length': secToMicro(songInfo.songDuration),
|
||||
|
||||
@ -16,7 +16,7 @@ export default createPlugin<
|
||||
restartNeeded: false,
|
||||
renderer: {
|
||||
start() {
|
||||
waitForElement<HTMLElement>('#dislike-button-renderer').then(
|
||||
waitForElement<HTMLElement>('#like-button-renderer').then(
|
||||
(dislikeBtn) => {
|
||||
this.observer = new MutationObserver(() => {
|
||||
if (dislikeBtn?.getAttribute('like-status') == 'DISLIKE') {
|
||||
|
||||
@ -10,7 +10,7 @@ import type { SyncedLyricsPluginConfig } from './types';
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.synced-lyrics.name'),
|
||||
description: () => t('plugins.synced-lyrics.description'),
|
||||
authors: ['Non0reo', 'ArjixWasTaken'],
|
||||
authors: ['Non0reo', 'ArjixWasTaken', 'KimJammer'],
|
||||
restartNeeded: true,
|
||||
addedVersion: '3.5.X',
|
||||
config: {
|
||||
@ -19,7 +19,7 @@ export default createPlugin({
|
||||
showLyricsEvenIfInexact: true,
|
||||
showTimeCodes: false,
|
||||
defaultTextString: '♪',
|
||||
lineEffect: 'scale',
|
||||
lineEffect: 'fancy',
|
||||
} satisfies SyncedLyricsPluginConfig,
|
||||
|
||||
menu,
|
||||
|
||||
@ -5,13 +5,10 @@ import { t } from '@/i18n';
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { SyncedLyricsPluginConfig } from './types';
|
||||
|
||||
export const menu = async ({
|
||||
getConfig,
|
||||
setConfig,
|
||||
}: MenuContext<SyncedLyricsPluginConfig>): Promise<
|
||||
MenuItemConstructorOptions[]
|
||||
> => {
|
||||
const config = await getConfig();
|
||||
export const menu = async (
|
||||
ctx: MenuContext<SyncedLyricsPluginConfig>,
|
||||
): Promise<MenuItemConstructorOptions[]> => {
|
||||
const config = await ctx.getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
@ -20,7 +17,7 @@ export const menu = async ({
|
||||
type: 'checkbox',
|
||||
checked: config.preciseTiming,
|
||||
click(item) {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
preciseTiming: item.checked,
|
||||
});
|
||||
},
|
||||
@ -30,6 +27,21 @@ export const menu = async ({
|
||||
toolTip: t('plugins.synced-lyrics.menu.line-effect.tooltip'),
|
||||
type: '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(
|
||||
'plugins.synced-lyrics.menu.line-effect.submenu.scale.label',
|
||||
@ -40,7 +52,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.lineEffect === 'scale',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
lineEffect: 'scale',
|
||||
});
|
||||
},
|
||||
@ -55,7 +67,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.lineEffect === 'offset',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
lineEffect: 'offset',
|
||||
});
|
||||
},
|
||||
@ -70,7 +82,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.lineEffect === 'focus',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
lineEffect: 'focus',
|
||||
});
|
||||
},
|
||||
@ -87,7 +99,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === '♪',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
defaultTextString: '♪',
|
||||
});
|
||||
},
|
||||
@ -97,7 +109,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === ' ',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
defaultTextString: ' ',
|
||||
});
|
||||
},
|
||||
@ -107,7 +119,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === '...',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
defaultTextString: '...',
|
||||
});
|
||||
},
|
||||
@ -117,7 +129,7 @@ export const menu = async ({
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === '———',
|
||||
click() {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
defaultTextString: '———',
|
||||
});
|
||||
},
|
||||
@ -130,7 +142,7 @@ export const menu = async ({
|
||||
type: 'checkbox',
|
||||
checked: config.showTimeCodes,
|
||||
click(item) {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
showTimeCodes: item.checked,
|
||||
});
|
||||
},
|
||||
@ -143,7 +155,7 @@ export const menu = async ({
|
||||
type: 'checkbox',
|
||||
checked: config.showLyricsEvenIfInexact,
|
||||
click(item) {
|
||||
setConfig({
|
||||
ctx.setConfig({
|
||||
showLyricsEvenIfInexact: item.checked,
|
||||
});
|
||||
},
|
||||
|
||||
89
src/plugins/synced-lyrics/parsers/lrc.ts
Normal file
89
src/plugins/synced-lyrics/parsers/lrc.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
137
src/plugins/synced-lyrics/providers/LRCLib.ts
Normal file
137
src/plugins/synced-lyrics/providers/LRCLib.ts
Normal 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;
|
||||
}[];
|
||||
132
src/plugins/synced-lyrics/providers/LyricsGenius.ts
Normal file
132
src/plugins/synced-lyrics/providers/LyricsGenius.ts
Normal 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;
|
||||
}
|
||||
110
src/plugins/synced-lyrics/providers/Megalobiz.ts
Normal file
110
src/plugins/synced-lyrics/providers/Megalobiz.ts
Normal 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;
|
||||
}
|
||||
10
src/plugins/synced-lyrics/providers/MusixMatch.ts
Normal file
10
src/plugins/synced-lyrics/providers/MusixMatch.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
200
src/plugins/synced-lyrics/providers/YTMusic.ts
Normal file
200
src/plugins/synced-lyrics/providers/YTMusic.ts
Normal 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;
|
||||
}[];
|
||||
}
|
||||
189
src/plugins/synced-lyrics/providers/index.ts
Normal file
189
src/plugins/synced-lyrics/providers/index.ts
Normal 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 },
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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] }],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -2,145 +2,54 @@ import { createSignal, For, Match, Show, Switch } from 'solid-js';
|
||||
|
||||
import { SyncedLine } from './SyncedLine';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
import { ErrorDisplay } from './ErrorDisplay';
|
||||
import { LoadingKaomoji } from './LoadingKaomoji';
|
||||
import { PlainLyrics } from './PlainLyrics';
|
||||
|
||||
import {
|
||||
differentDuration,
|
||||
hadSecondAttempt,
|
||||
isFetching,
|
||||
isInstrumental,
|
||||
makeLyricsRequest,
|
||||
} from '../lyrics/fetch';
|
||||
|
||||
import type { LineLyrics } from '../../types';
|
||||
import { currentLyrics, lyricsStore } from '../../providers';
|
||||
|
||||
export const [debugInfo, setDebugInfo] = createSignal<string>();
|
||||
export const [lineLyrics, setLineLyrics] = createSignal<LineLyrics[]>([]);
|
||||
export const [currentTime, setCurrentTime] = createSignal<number>(-1);
|
||||
|
||||
// prettier-ignore
|
||||
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 (
|
||||
<div class={'lyric-container'}>
|
||||
<div class="lyric-container">
|
||||
<Switch>
|
||||
<Match when={isFetching()}>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<tp-yt-paper-spinner-lite
|
||||
active
|
||||
class="loading-indicator style-scope"
|
||||
/>
|
||||
</div>
|
||||
<Match when={currentLyrics()?.state === 'fetching'}>
|
||||
<LoadingKaomoji />
|
||||
</Match>
|
||||
<Match when={error()}>
|
||||
<Match when={!currentLyrics().data?.lines && !currentLyrics().data?.lyrics}>
|
||||
<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={{
|
||||
runs: [
|
||||
{
|
||||
text: t('plugins.synced-lyrics.errors.fetch'),
|
||||
},
|
||||
],
|
||||
runs: [{ text: '\(〇_o)/' }],
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<Show when={lyricsStore.current.error}>
|
||||
<ErrorDisplay error={lyricsStore.current.error!} />
|
||||
</Show>
|
||||
|
||||
<Switch>
|
||||
<Match when={!lineLyrics().length}>
|
||||
<Show
|
||||
when={isInstrumental()}
|
||||
fallback={
|
||||
<>
|
||||
<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 when={currentLyrics().data?.lines}>
|
||||
<For each={currentLyrics().data?.lines}>
|
||||
{(item) => <SyncedLine line={item} />}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={lineLyrics().length && !hadSecondAttempt()}>
|
||||
<yt-formatted-string
|
||||
class="warning-lyrics description ytmusic-description-shelf-renderer"
|
||||
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 when={currentLyrics().data?.lyrics}>
|
||||
<PlainLyrics lyrics={currentLyrics().data?.lyrics!} />
|
||||
</Match>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
198
src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx
Normal file
198
src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { createEffect, createMemo } from 'solid-js';
|
||||
import { createEffect, createMemo, For } from 'solid-js';
|
||||
|
||||
import { currentTime } from './LyricsContainer';
|
||||
|
||||
@ -20,34 +20,63 @@ export const SyncedLine = ({ line }: SyncedLineProps) => {
|
||||
return 'current';
|
||||
});
|
||||
|
||||
let ref: HTMLDivElement;
|
||||
let ref: HTMLDivElement | undefined;
|
||||
createEffect(() => {
|
||||
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 (
|
||||
<div
|
||||
ref={ref!}
|
||||
ref={ref}
|
||||
class={`synced-line ${status()}`}
|
||||
onClick={() => {
|
||||
_ytAPI?.seekTo(line.timeInMs / 1000);
|
||||
}}
|
||||
>
|
||||
<yt-formatted-string
|
||||
class="text-lyrics description ytmusic-description-shelf-renderer"
|
||||
text={{
|
||||
runs: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
text: `${config()?.showTimeCodes ? `[${line.time}] ` : ''}${line.text}`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div class="text-lyrics description ytmusic-description-shelf-renderer">
|
||||
<yt-formatted-string
|
||||
text={{
|
||||
runs: [{ text: config()?.showTimeCodes ? `[${line.time}] ` : '' }],
|
||||
}}
|
||||
/>
|
||||
|
||||
<For each={text().split(' ')}>
|
||||
{(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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { createRenderer } from '@/utils';
|
||||
import { waitForElement } from '@/utils/wait-for-element';
|
||||
|
||||
import { makeLyricsRequest } from './lyrics';
|
||||
import { selectors, tabStates } from './utils';
|
||||
import { setConfig } from './renderer';
|
||||
import { setCurrentTime } from './components/LyricsContainer';
|
||||
|
||||
import { fetchLyrics } from '../providers';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
import type { SyncedLyricsPluginConfig } from '../types';
|
||||
|
||||
export let _ytAPI: YoutubePlayer | null = null;
|
||||
@ -36,9 +36,7 @@ export const renderer = createRenderer<
|
||||
header.removeAttribute('disabled');
|
||||
break;
|
||||
case 'aria-selected':
|
||||
tabStates[header.ariaSelected as 'true' | 'false']?.(
|
||||
_ytAPI?.getVideoData(),
|
||||
);
|
||||
tabStates[header.ariaSelected ?? 'false']();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -51,7 +49,6 @@ export const renderer = createRenderer<
|
||||
|
||||
await this.videoDataChange();
|
||||
},
|
||||
|
||||
async videoDataChange() {
|
||||
if (!this.updateTimestampInterval) {
|
||||
this.updateTimestampInterval = setInterval(
|
||||
@ -60,12 +57,17 @@ export const renderer = createRenderer<
|
||||
);
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
this.observer ??= new MutationObserver(this.observerCallback);
|
||||
|
||||
// Force the lyrics tab to be enabled at all times.
|
||||
this.observer.disconnect();
|
||||
|
||||
// Force the lyrics tab to be enabled at all times.
|
||||
const header = await waitForElement<HTMLElement>(selectors.head);
|
||||
{
|
||||
header.removeAttribute('disabled');
|
||||
tabStates[header.ariaSelected ?? 'false']();
|
||||
}
|
||||
|
||||
this.observer.observe(header, { attributes: true });
|
||||
header.removeAttribute('disabled');
|
||||
},
|
||||
@ -73,8 +75,8 @@ export const renderer = createRenderer<
|
||||
async start(ctx: RendererContext<SyncedLyricsPluginConfig>) {
|
||||
setConfig(await ctx.getConfig());
|
||||
|
||||
ctx.ipc.on('ytmd:update-song-info', async (info: SongInfo) => {
|
||||
await makeLyricsRequest(info);
|
||||
ctx.ipc.on('ytmd:update-song-info', (info: SongInfo) => {
|
||||
fetchLyrics(info);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -1,22 +1,143 @@
|
||||
import { createSignal, Show } from 'solid-js';
|
||||
import { createEffect, createSignal, onMount, Show } from 'solid-js';
|
||||
|
||||
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';
|
||||
|
||||
export const [isVisible, setIsVisible] = createSignal<boolean>(false);
|
||||
|
||||
export const [config, setConfig] =
|
||||
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 = () => {
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { waitForElement } from '@/utils/wait-for-element';
|
||||
|
||||
import { LyricsRenderer, setIsVisible, setPlayerState } from './renderer';
|
||||
|
||||
import type { VideoDetails } from '@/types/video-details';
|
||||
import { LyricsRenderer, setIsVisible } from './renderer';
|
||||
|
||||
export const selectors = {
|
||||
head: '#tabsContent > .tab-header:nth-of-type(2)',
|
||||
@ -14,10 +11,9 @@ export const selectors = {
|
||||
},
|
||||
};
|
||||
|
||||
export const tabStates = {
|
||||
true: async (data?: VideoDetails) => {
|
||||
export const tabStates: Record<string, () => void> = {
|
||||
true: async () => {
|
||||
setIsVisible(true);
|
||||
setPlayerState(data ?? null);
|
||||
|
||||
let container = document.querySelector('#synced-lyrics-container');
|
||||
if (container) return;
|
||||
|
||||
@ -3,22 +3,45 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#tab-renderer[page-type='MUSIC_PAGE_TYPE_TRACK_LYRICS'] > #synced-lyrics-container {
|
||||
#tab-renderer[page-type='MUSIC_PAGE_TYPE_TRACK_LYRICS']
|
||||
> #synced-lyrics-container {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* :root {
|
||||
--ytmusic-text-primary: #fff;
|
||||
--ytmusic-text-secondary: #aaa;
|
||||
} */
|
||||
|
||||
/* Variables are overridden by selected line effect */
|
||||
:root {
|
||||
/* Layout */
|
||||
--global-margin: 0.7rem;
|
||||
--previous-lyrics: var(--ytmusic-text-primary);
|
||||
--current-lyrics: var(--ytmusic-text-primary);
|
||||
--upcoming-lyrics: var(--ytmusic-text-secondary);
|
||||
--size-lyrics: 1.2em;
|
||||
--offset-lyrics: 1em;
|
||||
--lyrics-padding: 0;
|
||||
|
||||
/* Typography */
|
||||
--lyrics-font-family: Satoshi, Avenir, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
Open Sans, Helvetica Neue, sans-serif;
|
||||
--lyrics-font-size: 1.4rem;
|
||||
--lyrics-line-height: var(--ytmusic-body-line-height);
|
||||
--lyrics-width: 100%;
|
||||
|
||||
/* Inactive Lyrics */
|
||||
--lyrics-inactive-font-weight: 400;
|
||||
--lyrics-inactive-opacity: 0.33;
|
||||
--lyrics-inactive-scale: 1;
|
||||
--lyrics-inactive-offset: 0;
|
||||
|
||||
/* Active Lyrics */
|
||||
--lyrics-active-font-weight: 700;
|
||||
--lyrics-active-opacity: 1;
|
||||
--lyrics-active-scale: 1;
|
||||
--lyrics-active-offset: 0;
|
||||
|
||||
/* Animations */
|
||||
--lyrics-animations: lyrics-glow var(--lyrics-glow-duration) forwards, lyrics-wobble var(--lyrics-wobble-duration) forwards;
|
||||
--lyrics-scale-duration: 0.166s;
|
||||
--lyrics-opacity-transition: 0.33s;
|
||||
--lyrics-glow-duration: var(--lyrics-duration, 2s);
|
||||
--lyrics-wobble-duration: calc(var(--lyrics-duration, 2s) / 2);
|
||||
|
||||
/* Colors */
|
||||
--glow-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.lyric-container {
|
||||
@ -31,19 +54,18 @@
|
||||
}
|
||||
|
||||
.synced-line {
|
||||
width: var(--lyric-width, 100%);
|
||||
}
|
||||
width: var(--lyrics-width, 100%);
|
||||
|
||||
.synced-line > .text-lyrics {
|
||||
cursor: pointer;
|
||||
& > .text-lyrics {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.synced-lyrics {
|
||||
display: block;
|
||||
justify-content: left;
|
||||
text-align: left;
|
||||
margin: 0.5rem 0;
|
||||
margin-right: 20px;
|
||||
margin: 0.5rem 20px 0.5rem 0;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@ -53,26 +75,147 @@
|
||||
}
|
||||
|
||||
.text-lyrics {
|
||||
font-family: var(--lyrics-font-family) !important;
|
||||
font-size: var(--lyrics-font-size) !important;
|
||||
font-weight: var(--lyrics-inactive-font-weight) !important;
|
||||
line-height: var(--lyrics-line-height) !important;
|
||||
padding-top: var(--lyrics-padding);
|
||||
padding-bottom: var(--lyrics-padding);
|
||||
scale: var(--lyrics-inactive-scale);
|
||||
translate: var(--lyrics-inactive-offset);
|
||||
transition:
|
||||
scale var(--lyrics-scale-duration),
|
||||
translate 0.3s ease-in-out;
|
||||
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin: var(--global-margin) 0;
|
||||
transition: scale 0.3s ease-in-out, translate 0.3s ease-in-out, color 0.1s ease-in-out;
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
|
||||
.text-lyrics > span {
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
opacity: var(--lyrics-inactive-opacity);
|
||||
transition: opacity var(--lyrics-opacity-transition);
|
||||
}
|
||||
|
||||
.previous > .text-lyrics {
|
||||
color: var(--previous-lyrics);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.current > .text-lyrics {
|
||||
color: var(--current-lyrics);
|
||||
font-weight: bold;
|
||||
scale: var(--size-lyrics);
|
||||
translate: var(--offset-lyrics) 0;
|
||||
font-weight: var(--lyrics-active-font-weight) !important;
|
||||
scale: var(--lyrics-active-scale);
|
||||
translate: var(--lyrics-active-offset);
|
||||
}
|
||||
|
||||
.current > .text-lyrics > span {
|
||||
opacity: var(--lyrics-active-opacity);
|
||||
animation: var(--lyrics-animations);
|
||||
}
|
||||
|
||||
.upcoming > .text-lyrics {
|
||||
color: var(--upcoming-lyrics);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.lyrics-renderer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lyrics-picker {
|
||||
height: 5em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-block: 1em;
|
||||
}
|
||||
|
||||
.lyrics-picker-content {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lyrics-picker-content-label {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
/* padding-block: 5%; */
|
||||
}
|
||||
|
||||
.lyrics-picker-content-dots {
|
||||
display: block;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.lyrics-picker-item {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
transition: transform 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.lyrics-picker-dot {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: 0 4px 0;
|
||||
border-radius: 200px;
|
||||
border: 1px solid #6e7c7c7f;
|
||||
}
|
||||
|
||||
.lyrics-picker-left,
|
||||
.lyrics-picker-right {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s ease;
|
||||
border-radius: 25%;
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(0, 0%, 100%, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.lyrics-renderer-sticky {
|
||||
position: sticky;
|
||||
top: var(--top, 0);
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
transition: top 325ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes lyrics-wobble {
|
||||
from {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
33.33% {
|
||||
transform: translateY(1.75px);
|
||||
}
|
||||
66.66% {
|
||||
transform: translateY(-1.75px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lyrics-glow {
|
||||
0% {
|
||||
text-shadow: 0 0 1.5rem var(--glow-color);
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 0 var(--glow-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { SongInfo } from '@/providers/song-info';
|
||||
|
||||
export type SyncedLyricsPluginConfig = {
|
||||
enabled: boolean;
|
||||
preciseTiming: boolean;
|
||||
@ -10,29 +12,30 @@ export type SyncedLyricsPluginConfig = {
|
||||
export type LineLyricsStatus = 'previous' | 'current' | 'upcoming';
|
||||
|
||||
export type LineLyrics = {
|
||||
index: number;
|
||||
time: string;
|
||||
timeInMs: number;
|
||||
text: string;
|
||||
duration: number;
|
||||
|
||||
text: string;
|
||||
status: LineLyricsStatus;
|
||||
};
|
||||
|
||||
export type PlayPauseEvent = {
|
||||
isPaused: boolean;
|
||||
elapsedSeconds: number;
|
||||
};
|
||||
export type LineEffect = 'fancy' | 'scale' | 'offset' | 'focus';
|
||||
|
||||
export type LineEffect = 'scale' | 'offset' | 'focus';
|
||||
export interface LyricResult {
|
||||
title: string;
|
||||
artists: string[];
|
||||
|
||||
export type LRCLIBSearchResponse = {
|
||||
id: number;
|
||||
lyrics?: string;
|
||||
lines?: LineLyrics[];
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export type SearchSongInfo = Pick<SongInfo, 'title' | 'artist' | 'album' | 'songDuration' | 'videoId'>;
|
||||
|
||||
export interface LyricProvider {
|
||||
name: string;
|
||||
trackName: string;
|
||||
artistName: string;
|
||||
albumName: string;
|
||||
duration: number;
|
||||
instrumental: boolean;
|
||||
plainLyrics: string;
|
||||
syncedLyrics: string;
|
||||
}[];
|
||||
baseUrl: string;
|
||||
|
||||
search(songInfo: SearchSongInfo): Promise<LyricResult | null>;
|
||||
}
|
||||
|
||||
@ -8,7 +8,10 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||
import registerCallback, {
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
import { mediaIcons } from '@/types/media-icons';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
@ -47,7 +50,6 @@ export default createPlugin({
|
||||
const imagePath = getImagePath(kind);
|
||||
|
||||
if (imagePath) {
|
||||
console.log('imagePath', imagePath);
|
||||
const jimpImageBuffer = await Jimp.read(imagePath).then((img) => {
|
||||
if (imagePath && nativeTheme.shouldUseDarkColors) {
|
||||
return img.invert().getBuffer(JimpMime.png);
|
||||
@ -102,11 +104,13 @@ export default createPlugin({
|
||||
]);
|
||||
};
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
// Update currentsonginfo for win.on('show')
|
||||
currentSongInfo = songInfo;
|
||||
// Update thumbar
|
||||
setThumbar(songInfo);
|
||||
registerCallback((songInfo, event) => {
|
||||
if (event !== SongInfoEvent.TimeChanged) {
|
||||
// Update currentsonginfo for win.on('show')
|
||||
currentSongInfo = songInfo;
|
||||
// Update thumbar
|
||||
setThumbar(songInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// Need to set thumbar again after win.show
|
||||
|
||||
@ -2,7 +2,7 @@ import { nativeImage, type NativeImage, TouchBar } from 'electron';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
import registerCallback, { SongInfoEvent } from '@/providers/song-info';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
||||
@ -81,7 +81,8 @@ export default createPlugin({
|
||||
controls = [previous, playPause, next, dislike, like];
|
||||
|
||||
// Register the callback
|
||||
registerCallback((songInfo) => {
|
||||
registerCallback((songInfo, event) => {
|
||||
if (event === SongInfoEvent.TimeChanged) return;
|
||||
// Song information changed, so lets update the touchBar
|
||||
|
||||
// Set the song title
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user