Compare commits

...

43 Commits

Author SHA1 Message Date
34cb79eeaf Bump version to 3.2.2 2024-01-05 23:33:42 +09:00
1c30a07031 chore(deps): update dependency playwright to v1.41.0-alpha-jan-5-2024 2024-01-05 23:30:16 +09:00
5dd5f41ef5 chore(deps): update dependency electron to v29.0.0-alpha.7
Change to electron v29 to use node.js v20.
2024-01-05 23:28:24 +09:00
45a3c11d51 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (331 of 331 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-01-05 14:24:44 +00:00
8bd3b4d3f0 fix(visualizer): fixed an issue with audio getting unusually loud 2024-01-05 23:23:39 +09:00
4e3cb5806d fix(music-together): modernize code 2024-01-05 23:11:26 +09:00
6563eb4ddd chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (330 of 330 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-01-05 14:02:12 +00:00
895386f6f8 fix(music-together): typing 2024-01-05 23:01:55 +09:00
3810955e56 fix(skip-silences): fix audio distorted
fix #1141
2024-01-05 21:58:36 +09:00
59c521e53f fix: download button not working 2024-01-05 21:04:52 +09:00
25d266f8f9 feat(tray): Add song info and paused icon (#1592) 2024-01-05 20:56:47 +09:00
0c3c380591 chore(deps): update dependency rollup to v4.9.3 2024-01-05 20:56:17 +09:00
a20cfa30a1 chore(deps): update dependency vite to v5.0.11 2024-01-05 20:43:13 +09:00
fefe899393 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-01-05 11:12:50 +01:00
55759e8d7a chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-01-05 11:12:50 +01:00
ddb561937f fix(deps): update dependency i18next to v23.7.16 2024-01-04 22:41:16 +09:00
198cb71a4c chore(deps): update dependency electron to v28.1.1 2024-01-04 22:38:45 +09:00
c34b880752 chore(deps): update dependency electron-vite to v2.0.0-beta.3 2024-01-04 22:38:30 +09:00
76944e3e41 fix(deps): update dependency i18next to v23.7.14 2024-01-03 17:35:30 +09:00
68cd76f2af chore(deps): update pnpm to v8.14.0 2024-01-03 17:35:23 +09:00
81145b52b7 fix(#1580): fix NEW badge doesn't show 2024-01-03 15:02:29 +09:00
2a19dab061 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 3.3% (11 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-01-02 10:52:22 +01:00
6958d59d4f chore(i18n): Translated using Weblate (Lithuanian)
Currently translated at 89.6% (294 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2024-01-02 09:49:04 +01:00
8a51dfad87 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 89.6% (294 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-01-02 09:49:04 +01:00
5bb4d9efbe chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-01-02 09:49:04 +01:00
927aa5f24b chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 89.6% (294 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2024-01-02 09:49:03 +01:00
d695bc93a1 chore(i18n): Translated using Weblate (Polish)
Currently translated at 89.3% (293 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-01-02 09:49:03 +01:00
b05fb4ccbe chore(i18n): Translated using Weblate (Norwegian Bokmål)
Currently translated at 67.9% (223 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nb_NO/
2024-01-02 09:49:03 +01:00
299eb7e7d6 chore(i18n): Translated using Weblate (Italian)
Currently translated at 89.3% (293 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-01-02 09:49:03 +01:00
ae26333224 chore(i18n): Translated using Weblate (French)
Currently translated at 85.9% (282 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-01-02 09:49:03 +01:00
35176469b0 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-01-02 09:49:03 +01:00
4e74f9cbc5 chore(i18n): Translated using Weblate (Czech)
Currently translated at 94.8% (311 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-02 09:49:02 +01:00
4091b36f36 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-01-02 09:49:02 +01:00
b3f805fce6 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 2.4% (8 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-01-02 09:43:14 +01:00
b129a3e8d8 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 89.9% (295 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-01-02 09:43:13 +01:00
64ea1fdb58 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-01-02 09:43:13 +01:00
8fcf59ed0a chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-01-02 09:43:12 +01:00
9811ca63de chore(i18n): Translated using Weblate (Czech)
Currently translated at 94.8% (311 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-02 09:43:12 +01:00
9028f88299 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-01-02 09:43:12 +01:00
fd47766d93 chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.17.0 2024-01-02 17:34:04 +09:00
26b12c7208 chore(i18n): Added translation using Weblate (Vietnamese) 2024-01-02 07:19:07 +01:00
8da9b3454d chore(deps): update dependency esbuild to v0.19.11 2024-01-01 20:32:13 +09:00
205cbefc83 Update changelog for v3.2.1 2024-01-01 00:32:24 +00:00
34 changed files with 1207 additions and 687 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -2,8 +2,18 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v3.2.1](https://github.com/th-ch/youtube-music/compare/v3.2.0...v3.2.1)
- fix: fix #1574 [`#1574`](https://github.com/th-ch/youtube-music/issues/1574)
- fix: fix #1575 [`#1575`](https://github.com/th-ch/youtube-music/issues/1575)
- chore(i18n): Translated using Weblate [`f5aa179`](https://github.com/th-ch/youtube-music/commit/f5aa179cd639eb4b8f70f1264b5b459ebcc16695)
- chore(i18n): Translated using Weblate (English) [`e409165`](https://github.com/th-ch/youtube-music/commit/e409165e1bed85f3d1aea3a565e7b9e462b1e05b)
- chore(i18n): Translated using Weblate (Czech) [`0ca4e34`](https://github.com/th-ch/youtube-music/commit/0ca4e34efd86e877314e5a245f266065b4cf0013)
#### [v3.2.0](https://github.com/th-ch/youtube-music/compare/v3.1.1...v3.2.0) #### [v3.2.0](https://github.com/th-ch/youtube-music/compare/v3.1.1...v3.2.0)
> 1 January 2024
- feat(album-color-theme): improve `Album Color Theme` style [`#1571`](https://github.com/th-ch/youtube-music/pull/1571) - feat(album-color-theme): improve `Album Color Theme` style [`#1571`](https://github.com/th-ch/youtube-music/pull/1571)
- feat(menu): add more detail in Menu [`#1570`](https://github.com/th-ch/youtube-music/pull/1570) - feat(menu): add more detail in Menu [`#1570`](https://github.com/th-ch/youtube-music/pull/1570)
- feat(music-together): Add new plugin `Music Together` [`#1562`](https://github.com/th-ch/youtube-music/pull/1562) - feat(music-together): Add new plugin `Music Together` [`#1562`](https://github.com/th-ch/youtube-music/pull/1562)

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.2.1", "version": "3.2.2",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js", "main": "./dist/main/index.js",
"license": "MIT", "license": "MIT",
@ -123,14 +123,17 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"esbuild": "0.18.20", "esbuild": "0.19.11",
"usocket": "1.0.1", "usocket": "1.0.1",
"rollup": "4.9.2", "rollup": "4.9.3",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"@electron/universal": "2.0.1", "@electron/universal": "2.0.1",
"@babel/runtime": "7.23.7" "@babel/runtime": "7.23.7"
},
"patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch"
} }
}, },
"dependencies": { "dependencies": {
@ -163,7 +166,7 @@
"filenamify": "6.0.0", "filenamify": "6.0.0",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.7.13", "i18next": "23.7.16",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.12", "node-html-parser": "6.1.12",
@ -178,23 +181,23 @@
"youtubei.js": "8.1.0" "youtubei.js": "8.1.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.41.0-alpha-dec-18-2023", "@playwright/test": "1.41.0-alpha-jan-5-2024",
"@total-typescript/ts-reset": "0.5.1", "@total-typescript/ts-reset": "0.5.1",
"@types/color": "3.0.6", "@types/color": "3.0.6",
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.11", "@types/howler": "2.2.11",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@types/semver": "7.5.6", "@types/semver": "7.5.6",
"@typescript-eslint/eslint-plugin": "6.16.0", "@typescript-eslint/eslint-plugin": "6.17.0",
"bufferutil": "4.0.8", "bufferutil": "4.0.8",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"del-cli": "5.1.0", "del-cli": "5.1.0",
"electron": "28.1.0", "electron": "29.0.0-alpha.7",
"electron-builder": "24.9.1", "electron-builder": "24.9.1",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "2.0.0-beta.2", "electron-vite": "2.0.0-beta.3",
"esbuild": "0.18.20", "esbuild": "0.19.11",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-import-resolver-exports": "1.0.0-beta.5", "eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",
@ -202,11 +205,11 @@
"eslint-plugin-prettier": "5.1.2", "eslint-plugin-prettier": "5.1.2",
"glob": "10.3.10", "glob": "10.3.10",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"playwright": "1.41.0-alpha-dec-18-2023", "playwright": "1.41.0-alpha-jan-5-2024",
"rollup": "4.9.2", "rollup": "4.9.3",
"typescript": "5.3.3", "typescript": "5.3.3",
"utf-8-validate": "6.0.3", "utf-8-validate": "6.0.3",
"vite": "5.0.10", "vite": "5.0.11",
"vite-plugin-inspect": "0.8.1", "vite-plugin-inspect": "0.8.1",
"vite-plugin-resolve": "2.5.1", "vite-plugin-resolve": "2.5.1",
"ws": "8.16.0" "ws": "8.16.0"
@ -217,5 +220,5 @@
"unreleased": true, "unreleased": true,
"output": "changelog.md" "output": "changelog.md"
}, },
"packageManager": "pnpm@8.13.1" "packageManager": "pnpm@8.14.0"
} }

20
patches/vudio@2.1.1.patch Normal file
View File

@ -0,0 +1,20 @@
diff --git a/umd/vudio.js b/umd/vudio.js
index d0d1127e57125ad4e77442af2db4a26998c7b385..c0b66bd4327c65c31dc6e588bfa4ae6ec70bd3b8 100644
--- a/umd/vudio.js
+++ b/umd/vudio.js
@@ -147,7 +147,6 @@
source.connect(this.analyser);
this.analyser.fftSize = this.option.accuracy * 2;
- this.analyser.connect(audioContext.destination);
this.freqByteData = new Uint8Array(this.analyser.frequencyBinCount);
@@ -207,7 +206,6 @@
source.connect(this.analyser);
this.analyser.fftSize = this.option.accuracy * 2;
- this.analyser.connect(audioContext.destination);
},
__rebuildData : function (freqByteData, horizontalAlign) {

746
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -293,7 +293,7 @@
"menu": { "menu": {
"advanced": "Pokročilý" "advanced": "Pokročilý"
}, },
"name": "Prolínání [beta]", "name": "Prolínání [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -413,7 +413,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Přidává Lumia Stream podporu", "description": "Přidává Lumia Stream podporu",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Přidává lyrics podporu pro většinu písniček", "description": "Přidává lyrics podporu pro většinu písniček",
@ -422,6 +422,7 @@
} }
}, },
"music-together": { "music-together": {
"description": "Sdílejte seznam písniček s ostatními. Když the host hraje písničku, uslyší jí i všichni ostatní.",
"dialog": { "dialog": {
"enter-host": "Zadejte Host ID" "enter-host": "Zadejte Host ID"
}, },
@ -430,6 +431,7 @@
"unknown-user": "Neznámý uživatel" "unknown-user": "Neznámý uživatel"
}, },
"menu": { "menu": {
"click-to-copy-id": "Zkopírovat Host ID",
"close": "Zavřít Hudba Spolu", "close": "Zavřít Hudba Spolu",
"connected-users": "Připojení uživatelé", "connected-users": "Připojení uživatelé",
"disconnect": "Odpojit od Hudby Spolu", "disconnect": "Odpojit od Hudby Spolu",
@ -437,9 +439,11 @@
"host": "Hudba Spolu Host", "host": "Hudba Spolu Host",
"join": "Připojit se k Hudbě Spolu", "join": "Připojit se k Hudbě Spolu",
"permission": { "permission": {
"playlist": "Seznam písniček Control" "all": "Povolit hostům ovládat seznam písniček a přehrávač",
"host-only": "Jenom host může ovládat seznam písniček a přehrávač",
"playlist": "Povolit hostům ovládat seznam písniček"
}, },
"set-permission": "Změnit Control oprávnění", "set-permission": "Změnit ovládací oprávnění",
"status": { "status": {
"disconnected": "Odpojen", "disconnected": "Odpojen",
"guest": "Připojený/á jako Guest", "guest": "Připojený/á jako Guest",
@ -583,7 +587,7 @@
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Ovládejte přehrávání z vašeho hlavního panelu Windows", "description": "Ovládejte přehrávání z vašeho Windows hlavního panelu",
"name": "Hlavní panel Media Control" "name": "Hlavní panel Media Control"
}, },
"touchbar": { "touchbar": {

View File

@ -186,6 +186,10 @@
} }
}, },
"tray": { "tray": {
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
},
"next": "Next", "next": "Next",
"play-pause": "Play/Pause", "play-pause": "Play/Pause",
"previous": "Previous", "previous": "Previous",
@ -294,7 +298,7 @@
"menu": { "menu": {
"advanced": "Advanced" "advanced": "Advanced"
}, },
"name": "Crossfade [beta]", "name": "Crossfade [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -419,7 +423,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Adds Lumia Stream support", "description": "Adds Lumia Stream support",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Adds lyrics support for most songs", "description": "Adds lyrics support for most songs",
@ -468,6 +472,7 @@
"disconnected": "Music Together disconnected", "disconnected": "Music Together disconnected",
"host-failed": "Failed to host Music Together", "host-failed": "Failed to host Music Together",
"id-copied": "Host ID copied to clipboard", "id-copied": "Host ID copied to clipboard",
"id-copy-failed": "Failed to copy Host ID to clipboard",
"join-failed": "Failed to join Music Together", "join-failed": "Failed to join Music Together",
"joined": "Joined Music Together", "joined": "Joined Music Together",
"permission-changed": "Music Together permission changed to \"{{permission}}\"", "permission-changed": "Music Together permission changed to \"{{permission}}\"",

View File

@ -191,7 +191,11 @@
"previous": "Anterior", "previous": "Anterior",
"quit": "Salir", "quit": "Salir",
"restart": "Reiniciar la aplicación", "restart": "Reiniciar la aplicación",
"show": "Mostrar ventana" "show": "Mostrar ventana",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
} }
}, },
"plugins": { "plugins": {
@ -294,7 +298,7 @@
"menu": { "menu": {
"advanced": "Avanzado" "advanced": "Avanzado"
}, },
"name": "Crossfade [beta]", "name": "Crossfade [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -419,7 +423,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Agrega soporte para Lumia Stream", "description": "Agrega soporte para Lumia Stream",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Añade el soporte para las letras para la mayoría de las canciones", "description": "Añade el soporte para las letras para la mayoría de las canciones",
@ -450,9 +454,9 @@
"host": "Host de Music Together", "host": "Host de Music Together",
"join": "Únase a Music Together", "join": "Únase a Music Together",
"permission": { "permission": {
"all": "Todo el control", "all": "Permite a los invitados controlar la lista de reproducción y el reproductor",
"host-only": "Solo anfitrión", "host-only": "Sólo el anfitrión puede controlar la lista de reproducción y el reproductor",
"playlist": "Control de las listas de reproducción" "playlist": "Permita que los invitados controlen la lista de reproducción"
}, },
"set-permission": "Permiso de control de cambios", "set-permission": "Permiso de control de cambios",
"status": { "status": {

View File

@ -289,7 +289,7 @@
"menu": { "menu": {
"advanced": "Avancé" "advanced": "Avancé"
}, },
"name": "Fondu enchaîné [bêta]", "name": "Fondu enchaîné [Bêta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -414,7 +414,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Ajoute la prise en charge de Lumia Stream", "description": "Ajoute la prise en charge de Lumia Stream",
"name": "Lumia Stream [bêta]" "name": "Lumia Stream [Bêta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Ajoute la prise en charge des paroles pour la plupart des chansons", "description": "Ajoute la prise en charge des paroles pour la plupart des chansons",

View File

@ -170,7 +170,8 @@
}, },
"plugins": { "plugins": {
"enabled": "Attivato", "enabled": "Attivato",
"label": "Plugin" "label": "Plugin",
"new": "NUOVO"
}, },
"view": { "view": {
"label": "Visualizzazione", "label": "Visualizzazione",
@ -201,6 +202,10 @@
}, },
"name": "Adblocker" "name": "Adblocker"
}, },
"album-actions": {
"description": "Aggiunge i pulsanti Undislike, Dislike, Like e Unlike a tutti i brani di una playlist o di un album.",
"name": "Azioni album"
},
"album-color-theme": { "album-color-theme": {
"description": "Applica un tema dinamico e degli effetti visivi basandosi sul colore dell'album", "description": "Applica un tema dinamico e degli effetti visivi basandosi sul colore dell'album",
"name": "Tema abbinato a colore album" "name": "Tema abbinato a colore album"
@ -414,7 +419,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Aggiungi supporto per Lumia Stream", "description": "Aggiungi supporto per Lumia Stream",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Aggiunge il supporto dei testi per la maggior parte delle canzoni", "description": "Aggiunge il supporto dei testi per la maggior parte delle canzoni",
@ -426,6 +431,51 @@
"fetched-lyrics": "Testi recuperati per Genius" "fetched-lyrics": "Testi recuperati per Genius"
} }
}, },
"music-together": {
"description": "Condividi una playlist con altri. Quando l'Host riproduce un brano, tutti gli altri ascolteranno lo stesso brano",
"dialog": {
"enter-host": "Inserisci l'ID dell'Host"
},
"internal": {
"save": "Salva",
"track-source": "Traccia sorgente",
"unknown-user": "Utente sconosciuto"
},
"menu": {
"click-to-copy-id": "Copia l'ID dell'Host",
"close": "Chiudi Music Together",
"connected-users": "Utenti connessi",
"disconnect": "Disconetti Music Together",
"empty-user": "Utenti non connessi",
"host": "Music Together Host",
"join": "Unisciti a Music Together",
"permission": {
"all": "Consenti ai Guest di controllare la playlist e il player",
"host-only": "Solo l'Host può controllare la playlist e il player",
"playlist": "Consenti ai Guest di controllare la playlist"
},
"set-permission": "Cambia autorizzazione di controllo",
"status": {
"disconnected": "Disconnesso",
"guest": "Connesso come Guest",
"host": "Connesso come Host"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Impossibile aggiungere il brano",
"closed": "Music Together chiuso",
"disconnected": "Music Together disconnesso",
"host-failed": "Impossibile ospitare Music Together",
"id-copied": "L'ID dell Host è stato copiato negli appunti",
"join-failed": "Impossibile unirsi a Music Together",
"joined": "Unito a Music Together",
"permission-changed": "L'autorizzazione di Music Together è cambiata in {{permission}}",
"remove-song-failed": "Impossibile rimuovere il brano",
"user-connected": "{{name}} si è unito a Music Together",
"user-disconnected": "{{name}} ha lasciato Music Together"
}
},
"navigation": { "navigation": {
"description": "Frecce di navigazione Avanti/Indietro integrate direttamente nell'interfaccia, come nel tuo browser preferito", "description": "Frecce di navigazione Avanti/Indietro integrate direttamente nell'interfaccia, come nel tuo browser preferito",
"name": "Navigazione" "name": "Navigazione"

View File

@ -191,7 +191,11 @@
"previous": "이전", "previous": "이전",
"quit": "종료", "quit": "종료",
"restart": "앱 재시작", "restart": "앱 재시작",
"show": "창 표시" "show": "창 표시",
"tooltip": {
"default": "유튜브 뮤직",
"with-song-info": "유튜브 뮤직: {{artist}} - {{title}}"
}
} }
}, },
"plugins": { "plugins": {
@ -468,6 +472,7 @@
"disconnected": "Music Together 연결이 끊어졌습니다", "disconnected": "Music Together 연결이 끊어졌습니다",
"host-failed": "Music Together를 열 수 없습니다", "host-failed": "Music Together를 열 수 없습니다",
"id-copied": "호스트 아이디가 클립보드에 복사되었습니다", "id-copied": "호스트 아이디가 클립보드에 복사되었습니다",
"id-copy-failed": "호스트 ID를 클립보드에 복사하지 못했습니다",
"join-failed": "Music Together에 참여할 수 없습니다", "join-failed": "Music Together에 참여할 수 없습니다",
"joined": "Music Together에 참여했습니다", "joined": "Music Together에 참여했습니다",
"permission-changed": "Music Together 제어 권한이 \"{{permission}}\"(으)로 변경되었습니다", "permission-changed": "Music Together 제어 권한이 \"{{permission}}\"(으)로 변경되었습니다",

View File

@ -289,7 +289,7 @@
"menu": { "menu": {
"advanced": "Išplėstinė" "advanced": "Išplėstinė"
}, },
"name": "Perliejimas [beta]", "name": "Perliejimas [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -414,7 +414,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Prideda \"Lumia Stream\" palaikymą", "description": "Prideda \"Lumia Stream\" palaikymą",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Prideda daugumai dainių žodžių tekstus", "description": "Prideda daugumai dainių žodžių tekstus",

View File

@ -287,7 +287,7 @@
"menu": { "menu": {
"advanced": "Avansert" "advanced": "Avansert"
}, },
"name": "Overgang [beta]", "name": "Overgang [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -412,7 +412,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Legger til Lumia Stream-støtte", "description": "Legger til Lumia Stream-støtte",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Gir sangtekststøtte for de fleste spor", "description": "Gir sangtekststøtte for de fleste spor",

View File

@ -414,7 +414,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Dodaje obsługę Lumia Stream", "description": "Dodaje obsługę Lumia Stream",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Dodaje obsługę tekstów dla większości piosenek", "description": "Dodaje obsługę tekstów dla większości piosenek",

View File

@ -289,7 +289,7 @@
"menu": { "menu": {
"advanced": "Avançado" "advanced": "Avançado"
}, },
"name": "Transição entre músicas [beta]", "name": "Transição entre músicas [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -414,7 +414,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Adiciona suporte Lumia Stream", "description": "Adiciona suporte Lumia Stream",
"name": "Lumia Stream [beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Adiciona suporte a letras para a maioria das músicas", "description": "Adiciona suporte a letras para a maioria das músicas",

View File

@ -8,7 +8,7 @@
"load-all": "Tüm eklentiler yükleniyor", "load-all": "Tüm eklentiler yükleniyor",
"load-failed": "\"{{pluginName}}\" eklentisi yüklenemedi", "load-failed": "\"{{pluginName}}\" eklentisi yüklenemedi",
"loaded": "\"{{pluginName}}\" eklentisi yüklendi", "loaded": "\"{{pluginName}}\" eklentisi yüklendi",
"unload-failed": "\"{{pluginName}}\" eklentisi kaldırılamadı.", "unload-failed": "\"{{pluginName}}\" eklentisi çıkartılamadı",
"unloaded": "\"{{pluginName}}\" eklentisi kaldırıldı" "unloaded": "\"{{pluginName}}\" eklentisi kaldırıldı"
} }
} }
@ -50,53 +50,117 @@
}, },
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
"later": "Daha Sonra",
"restart-now": "Şimdi yeniden başlat" "restart-now": "Şimdi yeniden başlat"
} },
"detail": "\"{{pluginName}}\" eklentisinin çalışabilmesi için yeniden başlatman gerekiyor",
"message": "\"{{pluginName}}\" için yeniden başlatman gerekiyor",
"title": "Uygulamayı yeniden başlatman gerekiyor"
},
"unresponsive": {
"buttons": {
"quit": ıkış",
"relaunch": "Yeniden Başlat",
"wait": "Bekle"
},
"detail": "Rahatsızlık için özür dileriz! Lütfen ne yapacağınızı seçin:",
"message": "Uygulama yanıt vermiyor",
"title": "Pencere yanıt vermiyor"
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {
"disable": "Güncellemeleri devre dışı bırak",
"download": "İndir", "download": "İndir",
"ok": "Tamam" "ok": "Tamam"
} },
"detail": "Yeni bir sürüm mevcut. {{downloadLink}} adresi üzerinden indirebilirsin",
"message": "Yeni bir sürüm mevcut",
"title": "Güncelleme Mevcut"
} }
}, },
"menu": { "menu": {
"about": "Hakkında",
"navigation": { "navigation": {
"label": "Navigasyon" "label": "Navigasyon",
"submenu": {
"copy-current-url": "Geçerli Url'yi kopyala",
"go-back": "Geri dön",
"go-forward": "İlerle",
"quit": ıkış",
"restart": "Uygulamayı Yeniden Başlat"
}
}, },
"options": { "options": {
"label": "Seçenekler", "label": "Seçenekler",
"submenu": { "submenu": {
"advanced-options": { "advanced-options": {
"label": "Gelişmiş Seçenekler",
"submenu": { "submenu": {
"auto-reset-app-cache": "Uygulama başlatıldığında uygulama önbelleğini sıfırla",
"disable-hardware-acceleration": "Donanım hızlandırmayı devre dışı bırak",
"edit-config-json": "Düzenle config.json",
"override-user-agent": "\"User-Agent \"ı geçersiz kıl",
"restart-on-config-changes": "Yapılandırma değişikliğinde yeniden başlat",
"set-proxy": { "set-proxy": {
"label": "Proxy ayarla", "label": "Proxy ayarla",
"prompt": { "prompt": {
"label": "Proxy Adresini Gir: (devre dışı bırakmak için boş bırakın)",
"placeholder": "Örnek: SOCKS5://127.0.0.1:9999",
"title": "Proxy ayarla" "title": "Proxy ayarla"
} }
} },
"toggle-dev-tools": "DevTools'u Aç / Kapat"
} }
}, },
"always-on-top": "Her zaman üstte",
"auto-update": "Otomatik Güncelleme", "auto-update": "Otomatik Güncelleme",
"language": { "hide-menu": {
"label": "Dil" "dialog": {
"message": "Menü bir sonraki açılışta gizlenecektir, göstermek için [Alt] tuşunu kullanın (veya uygulama-içi-menü kullanıyorsanız [`] tuşuna geri basın)",
"title": "Gizli Menü Aktif"
},
"label": "Gizli Menü"
}, },
"language": {
"dialog": {
"message": "Dil değişikliği yeniden başlattıktan sonra etkinleşecektir",
"title": "Dil değiştirildi"
},
"label": "Dil",
"submenu": {
"to-help-translate": "Çeviriye yardım etmek ister misiniz? Buraya tıklayın"
}
},
"resume-on-start": "Uygulama başlatıldığında son şarkıyı devam ettir",
"single-instance-lock": "Tek Örnek Kilidi",
"start-at-login": "Başlangıçta çalıştır", "start-at-login": "Başlangıçta çalıştır",
"starting-page": {
"label": "Başlangıç sayfası",
"unset": "Ayarlanmadı"
},
"tray": { "tray": {
"label": "Tepsi", "label": "Tepsi",
"submenu": { "submenu": {
"enabled-and-hide-app": "Uygulamayı etkinleştirin gizleyin.", "disabled": "Devre Dışı",
"play-pause-on-click": "Tıklaynınca Oynat-Duraklat" "enabled-and-hide-app": "Uygulamayı etkinleştirin gizleyin",
"enabled-and-show-app": "Etkinleştir ve uygulamayı göster",
"play-pause-on-click": "Tıklandığında Oynat/Duraklat"
} }
}, },
"visual-tweaks": { "visual-tweaks": {
"label": "Görsel İnce Ayarlar",
"submenu": { "submenu": {
"like-buttons": { "like-buttons": {
"default": "Varsayılan" "default": "Varsayılan",
"force-show": "Zorla göster",
"hide": "Gizle",
"label": "Beğenme düğmeleri"
}, },
"remove-upgrade-button": "Yükseltme düğmesini kaldır",
"theme": { "theme": {
"label": "Tema", "label": "Tema",
"submenu": { "submenu": {
"import-css-file": "Özel CSS dosyanı içeri aktar",
"no-theme": "Tema Yok" "no-theme": "Tema Yok"
} }
} }
@ -105,31 +169,54 @@
} }
}, },
"plugins": { "plugins": {
"label": "Eklentiler" "enabled": "Aktif",
"label": "Eklentiler",
"new": "YENİ"
}, },
"view": { "view": {
"label": "Görüntü" "label": "Görüntü",
"submenu": {
"force-reload": "Zorla yeniden başlat",
"reload": "Yeniden Başlat",
"reset-zoom": "Asıl Boyut",
"toggle-fullscreen": "Tam Ekran'a Geçiş",
"zoom-in": "Yakınlaştır",
"zoom-out": "Uzaklaştır"
}
} }
},
"tray": {
"next": "Sonraki",
"play-pause": "Oynat/Durdur",
"previous": "Önceki",
"quit": ıkış",
"restart": "Yeniden başlat",
"show": "Pencereyi görüntüle"
} }
}, },
"plugins": { "plugins": {
"adblocker": { "adblocker": {
"description": "Tüm reklamları ve izleyicileri engelle", "description": "Tüm reklamları ve izleyicileri engelle",
"menu": { "menu": {
"blocker": "Engelleme Yöntemi" "blocker": "Engelleyici"
}, },
"name": "Adblocker" "name": "Reklam Engelleyici"
},
"album-actions": {
"description": "Çalma listesindeki veya albümdeki tüm şarkılara Beğendim ve Beğenmedim düğmeleri ekler.",
"name": "Albüm Eylemleri"
}, },
"album-color-theme": { "album-color-theme": {
"description": "Albümün renk paletine dayalı dinamik bir tema ve efektler uygular.", "description": "Albümün renk paletine dayalı dinamik bir tema ve efektler uygular",
"name": "Albüm Renk Teması" "name": "Albüm Renk Teması"
}, },
"ambient-mode": { "ambient-mode": {
"description": "Videodaki yumuşak renkleri ekranınızın arka planına yansıtarak bir ışık efekti uygular..", "description": "Videodaki yumuşak renkleri ekranınızın arka planına yansıtarak bir ışık efekti uygular..",
"menu": { "menu": {
"blur-amount": { "blur-amount": {
"label": "Bulanıklık miktarı",
"submenu": { "submenu": {
"pixels": "{{blurAmount}} pixels" "pixels": "{{blurAmount}} piksel"
} }
}, },
"buffer": { "buffer": {
@ -145,53 +232,82 @@
} }
}, },
"quality": { "quality": {
"label": "Kalite",
"submenu": { "submenu": {
"pixels": "{{quality}} pixels" "pixels": "{{quality}} piksel"
} }
}, },
"size": { "size": {
"label": "Boyut",
"submenu": { "submenu": {
"percent": "{{size}}%" "percent": "{{size}}%"
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"label": "Yumuşak Geçiş",
"submenu": { "submenu": {
"during": "{{interpolationTime}} saniye içinde" "during": "{{interpolationTime}} saniye boyunca"
} }
},
"use-fullscreen": {
"label": "Tam ekran kullanılıyor"
} }
} },
"name": "Ambiyans Modu"
}, },
"audio-compressor": { "audio-compressor": {
"description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)" "description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)",
"name": "Ses Sıkıştırma"
}, },
"blur-nav-bar": { "blur-nav-bar": {
"description": "Gezinme çubuğunu şeffaf ve bulanık yapar" "description": "Gezinme çubuğunu şeffaf ve bulanık yapar",
"name": "Navigasyon barını bulanıklaştır"
}, },
"bypass-age-restrictions": { "bypass-age-restrictions": {
"description": "YouTube yaş doğrulamasını atla" "description": "YouTube yaş doğrulamasını atla",
"name": "Yaş doğrulamasını atla"
}, },
"captions-selector": { "captions-selector": {
"description": "YouTube Music için altyazı seçici",
"menu": {
"autoload": "Son kullanılan altyazıyı otomatik olarak seç",
"disable-captions": "Varsayılan olarak altyazı yok"
},
"name": "Altyazı Seçici",
"prompt": { "prompt": {
"selector": { "selector": {
"none": "Hiçbiri" "label": "Geçerli altyazı dili: {{language}}",
"none": "Hiçbiri",
"title": "Altyazı dilini seç"
} }
},
"templates": {
"title": "Altyazı seçiciyi aç"
} }
}, },
"compact-sidebar": { "compact-sidebar": {
"description": "Her zaman kompakt kenar çubugu" "description": "Her zaman kompakt kenar çubugu",
"name": "Kompakt Kenar Çubuğu"
}, },
"crossfade": { "crossfade": {
"description": "Şarkılar arasında Çapraz Geçiş",
"menu": { "menu": {
"advanced": "İleri düzey için" "advanced": "Gelişmiş"
}, },
"name": "Çapraz Geçiş [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
"fade-in-duration": "Güçlenme süresi (ms)",
"fade-out-duration": "Zayıflama süresi (ms)",
"fade-scaling": { "fade-scaling": {
"label": "Zayıflama Ölçeği",
"linear": "Doğrusal", "linear": "Doğrusal",
"logarithmic": "Logaritmik" "logarithmic": "Logaritmik"
} },
} "seconds-before-end": "Bitişten N saniye önce çapraz geçiş"
},
"title": "Çapraz Geçiş ayarları"
} }
} }
}, },
@ -199,14 +315,33 @@
"description": "Şarkıların otomatik olarak duraklatılmasını sağlar", "description": "Şarkıların otomatik olarak duraklatılmasını sağlar",
"menu": { "menu": {
"apply-once": "Yalnızca ilk şarkı için geçerlidir" "apply-once": "Yalnızca ilk şarkı için geçerlidir"
} },
"name": "Otomatik oynatmayı devre dışı bırak"
}, },
"discord": { "discord": {
"description": "Rich Presence ile Discord'da ne dinlediğinizi gösterin.", "backend": {
"already-connected": "Aktif bağlantı olduğu halde bağlantı kurulmaya çalışıldı",
"connected": "Discord'a bağlandı",
"disconnected": "Discord ile bağlantı kesildi"
},
"description": "Rich Presence ile Discord'da ne dinlediğinizi gösterin",
"menu": { "menu": {
"auto-reconnect": "Otomatik yeniden bağlan",
"clear-activity": "Etkinliği temizle",
"clear-activity-after-timeout": "Zaman aşımından sonra etkinliği temizle",
"connected": "Bağlı",
"disconnected": "Bağlantı kesildi",
"hide-duration-left": "Kalan süreyi gizle", "hide-duration-left": "Kalan süreyi gizle",
"hide-github-button": "GitHub bağlantısını gizle", "hide-github-button": "GitHub bağlantısını gizle",
"play-on-youtube-music": "YouTube Music de oynat",
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla" "set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Hareketsizlik zaman aşımını saniye cinsinden girin:",
"title": "Hareketsizlik zaman aşımını ayarla"
}
} }
}, },
"downloader": { "downloader": {
@ -216,71 +351,298 @@
"buttons": { "buttons": {
"ok": "Tamam" "ok": "Tamam"
}, },
"title": "İndirmede hata meydana geldi!" "message": "Argh! Özür dilerim, indirme başarısız oldu…",
"title": "İndirme sırasında bir hata meydana geldi!"
}, },
"start-download-playlist": { "start-download-playlist": {
"buttons": { "buttons": {
"ok": "Tamam" "ok": "Tamam"
}, },
"message": "Çalma listesini indir : {{playlistTitle}}", "detail": "({{playlistSize}} şarkı)",
"message": "Oynatma listesini indir : {{playlistTitle}}",
"title": "İndirme Başladı" "title": "İndirme Başladı"
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "Dönüştürme : {{percent}}%", "conversion-progress": "Dönüştürme : {{percent}}%",
"converting": "Dönüştürülüyor…",
"done": "Tamamlandı: {{filePath}}",
"download-info": "{{artist}} - {{title}} [{{videoId}} indiriliyor",
"download-progress": "İndirme : {{percent}}%", "download-progress": "İndirme : {{percent}}%",
"preparing-file": "Dosya Hazırlanıyor…" "downloading": "İndiriliyor…",
"downloading-counter": "İndiriliyor {{current}}/{{total}}…",
"downloading-playlist": "\"{{playlistTitle}}\" şarkı listesi indiriliyor - {{playlistSize}} şarkı ({{playlistId}})",
"error-while-downloading": "\"{{author}} - {{title}}\" indirilirken hata oluştu: {{error}}",
"folder-already-exists": "{{playlistFolder}} klasörü zaten mevcut",
"getting-playlist-info": "Oynatma listesi bilgisi alınıyor…",
"loading": "Yükleniyor…",
"playlist-has-only-one-song": "Oynatma listesinde yalnızca bir şarkı var, doğrudan indiriliyor",
"playlist-id-not-found": "Oynatma listesi ID'si bulunamadı",
"playlist-is-empty": "Oynatma listesi boş",
"playlist-is-mix-or-private": "Çalma listesi bilgisi alınırken hata oluştu: özel veya \"Size özel karışık\" bir çalma listesi olmadığından emin olun\n\n{{error}}",
"preparing-file": "Dosya Hazırlanıyor…",
"saving": "Kaydediliyor…",
"trying-to-get-playlist-id": "Çalma listesi ID'si alınmaya çalışılıyor: {{playlistId}}",
"video-id-not-found": "Video bulunamadı",
"writing-id3": "ID3 etiketleri yazılıyor…"
} }
}, },
"description": "MP3 / kaynak sesini doğrudan arayüzden indir",
"menu": {
"choose-download-folder": "İndirme klasörünü seç",
"download-playlist": "Oynatma listesini indir",
"presets": "Hazır Ayarlar",
"skip-existing": "Mevcut dosyaları atla"
},
"name": "İndirici",
"renderer": {
"can-not-update-progress": "İlerleme güncellenemiyor"
},
"templates": { "templates": {
"button": "İndir" "button": "İndir"
} }
}, },
"exponential-volume": {
"description": "Ses seviyesi kaydırıcısını üstel hale getirir, böylece daha düşük ses seviyelerini seçmek daha kolay olur.",
"name": "Üstel Ses Seviyesi"
},
"in-app-menu": {
"description": "Menü çubuklarına süslü, koyu veya albüm renginde bir görünüm verir",
"menu": {
"hide-dom-window-controls": "DOM penceresi kontrollerini gizle"
},
"name": "Uygulama İçi Menü"
},
"last-fm": { "last-fm": {
"description": "Last.fm için scrobbling desteği ekler",
"name": "Last.fm" "name": "Last.fm"
}, },
"lumiastream": {
"description": "Lumia Stream desteği ekler",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Çoğu şarkı için şarkı sözü desteği ekler",
"menu": {
"romanized-lyrics": "Romanlaştırılmış Şarkı Sözleri"
},
"name": "Genius Şarkı Sözleri",
"renderer": {
"fetched-lyrics": "Şarkı sözleri genius tarafından alındı"
}
},
"music-together": {
"description": "Oynatma listesini başkalarıyla paylaşın. Sunucu sahibi bir şarkı çaldığında, diğer herkes aynı şarkıyı duyacak",
"dialog": {
"enter-host": "Sunucu ID'si Girin"
},
"internal": {
"save": "Kaydet",
"track-source": "Parça Kaynağı",
"unknown-user": "Bilinmeyen Kullanıcı"
},
"menu": {
"click-to-copy-id": "Sunucu ID'sini kopyala",
"close": "Birlikte Müziği Kapat",
"connected-users": "Bağlanan Kullanıcılar",
"disconnect": "Birlikte Müzik Bağlantısını Kesin",
"empty-user": "Bağlı kullanıcı bulunmuyor",
"host": "Birlikte Müzik Sunucusu",
"join": "Birlikte Müziğe Katıl",
"permission": {
"all": "Konukların oynatma listesini ve oynatıcıyı kontrol etmesine izin verin",
"host-only": "Çalma listesini ve oynatıcıyı yalnızca yönetici kontrol edebilir",
"playlist": "Konukların oynatma listesini kontrol etmesine izin ver"
},
"set-permission": "Kontrol İznini Değiştir",
"status": {
"disconnected": "Bağlantı kesildi",
"guest": "Misafir olarak bağlandı",
"host": "Ev Sahibi olarak bağlandı"
}
},
"name": "Birlikte Müzik [Beta]",
"toast": {
"add-song-failed": "Şarkı eklenirken bir hata meydana geldi",
"closed": "Birlikte Müzik kapatıldı",
"disconnected": "Birlikte Müzik bağlantı kesildi",
"host-failed": "Birlikte Müzik sunucusu kurulamadı",
"id-copied": "Sunucu ID'si kopyalandı",
"join-failed": "Birlikte Müziğe katılırken bir hata meydana geldi",
"joined": "Birlikte Müziğe Katıldı",
"permission-changed": "Birlikte Müzik yetkisi \"{{permission}}\" olarak değiştirildi",
"remove-song-failed": "Şarkı kaldırılırken bir hata meydana geldi",
"user-connected": "{{name}} Birlikte Müziğe Katıldı",
"user-disconnected": "{{name}} Birlikte Müzik'ten ayrıldı"
}
},
"navigation": { "navigation": {
"description": "Favori tarayıcınızdaki gibi doğrudan arayüze entegre edilmiş İleri/Geri gezinme okları",
"name": "Navigasyon" "name": "Navigasyon"
}, },
"no-google-login": { "no-google-login": {
"description": "Google giriş düğmelerini ve bağlantılarını arayüzden kaldır",
"name": "Google Girişini Kaldır" "name": "Google Girişini Kaldır"
}, },
"notifications": { "notifications": {
"description": "Bir şarkı çalmaya başladığında bir bildirim görüntüler (etkileşimli bildirimler Windows'ta mevcuttur)",
"menu": {
"interactive": "İnteraktif Bildirimler",
"interactive-settings": {
"label": "İnteraktif Ayarlar",
"submenu": {
"hide-button-text": "Buton metnini gizle",
"refresh-on-play-pause": "Oynat/Duraklat'ta Yenile",
"tray-controls": "Tepsi tıklamasıyla Aç/Kapat"
}
},
"priority": "Bildirim Önceliği",
"toast-style": "Bildirim Tarzı",
"unpause-notification": "Şarkı tekrar oynatılınca bildirim göster"
},
"name": "Bildirimler" "name": "Bildirimler"
}, },
"shortcuts": { "picture-in-picture": {
"prompt": { "description": "Uygulamayı resim-içinde-resim moduna geçirmeye izin verir",
"keybind": { "menu": {
"keybind-options": { "always-on-top": "Her zaman üstte",
"next": "İler" "hotkey": {
"label": "Kısayol",
"prompt": {
"keybind-options": {
"hotkey": "Kısayol"
},
"label": "Resim-içinde-resim arasında geçiş yapmak için bir kısayol tuşu seçin",
"title": "Resim-içinde-resim Kısayol Tuşu"
} }
},
"save-window-position": "Pencere konumunu kaydet",
"save-window-size": "Pencere boyutunu kaydet",
"use-native-pip": "Tarayıcı yerel PiP'sini kullan"
},
"name": "Resim-içinde-resim",
"templates": {
"button": "Resim-içinde-resim"
}
},
"playback-speed": {
"description": "Hızlı dinle, yavaş dinle! Şarkı hızını kontrol eden bir kaydırıcı ekler",
"name": "Oynatma Hızı",
"templates": {
"button": "Hız"
}
},
"precise-volume": {
"description": "Özel bir HUD ve özelleştirilebilir ses seviyesi adımları ile fare tekerleği / kısayol tuşlarını kullanarak ses seviyesini hassas bir şekilde kontrol edin",
"menu": {
"arrows-shortcuts": "Yerel Ok Tuşu Kontrolleri",
"custom-volume-steps": "Özel Ses Seviyesi Adımlarını Ayarlama",
"global-shortcuts": "Genel Kısayol Tuşları"
},
"name": "Hassas Ses Seviyesi",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Ses Seviyesi Azaltma",
"increase": "Ses Seviyesi Yükseltme"
},
"label": "Genel Ses Tuş Atamalarını seçin:",
"title": "Genel Ses Tuş Atamaları"
},
"volume-steps": {
"label": "Ses Artırma/Azaltma Kademelerini Seçin",
"title": "Ses Kademeleri"
} }
} }
}, },
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Mevcut Kalite: {{quality}}",
"message": "Video Kalitesini Seçin:",
"title": "Video Kalitesini Seçin"
}
}
},
"description": "Video katmanı üzerindeki bir düğme ile video kalitesinin değiştirilmesine izin verir",
"name": "Video Kalitesi Değiştirici"
},
"shortcuts": {
"description": "Oynatma için global kısayol tuşları (oynat/duraklat/sonraki/önceki) ayarlamaya ve medya tuşlarını geçersiz kılarak medya OSD'sini kapatmaya, arama yapmak için Ctrl/CMD + F tuşlarını açmaya, medya tuşları için Linux MPRIS desteğini açmaya ve ileri düzey kullanıcılar için özel kısayol tuşlarına izin verir.",
"menu": {
"override-media-keys": "Medya Tuşlarını Geçersiz Kıl",
"set-keybinds": "Global Şarkı Kontrollerini Ayarla"
},
"name": "Kısayollar (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "İler",
"play-pause": "Oynat / Durdur",
"previous": "Önceki"
},
"label": "Şarkı Kontrolü için Genel Tuş Atamaları'nı seçin:",
"title": "Genel Tuş Atamaları"
}
}
},
"skip-disliked-songs": {
"description": "Beğenmediğin şarkıları atlar",
"name": "Beğenmediklerini Atla"
},
"skip-silences": {
"description": "Şarkılardaki sessiz bölümleri otomatik olarak atlar",
"name": "Sessizlikleri Atla"
},
"sponsorblock": { "sponsorblock": {
"description": "Giriş/Çıkış gibi müzik olmayan kısımları veya müzik videolarında şarkının çalmadığı kısımları otomatik olarak atlar",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"taskbar-mediacontrol": {
"description": "Windows görev çubuğu üzerinden oynatmayı kontrol edebilmenize olanak sağlar",
"name": "Görev Çubuğu Medya Kontrolü"
},
"touchbar": { "touchbar": {
"description": "macOS kullanıcıları için bir TouchBar widget'ı ekler",
"name": "TouchBar" "name": "TouchBar"
}, },
"tuna-obs": { "tuna-obs": {
"description": "OBS eklentisi Tuna ile entegrasyon sağlar",
"name": "Tuna OBS" "name": "Tuna OBS"
}, },
"video-toggle": { "video-toggle": {
"description": "Video/Şarkı modu arasında geçiş yapmak için bir düğme ekler. ayrıca isteğe bağlı olarak tüm video sekmesini kaldırabilir",
"menu": { "menu": {
"align": { "align": {
"label": "Hizalama",
"submenu": { "submenu": {
"left": "Sol",
"middle": "Orta", "middle": "Orta",
"right": "Sağ" "right": "Sağ"
} }
}, },
"force-hide": "Video sekmesini kaldırmaya zorla",
"mode": { "mode": {
"label": "Mod" "label": "Mod",
"submenu": {
"custom": "Özel Ayar",
"disabled": "Devre dışı",
"native": "Yerel geçiş"
}
} }
}, },
"name": "Video Geçiş",
"templates": { "templates": {
"button": "Şarkı" "button": "Şarkı"
} }
},
"visualizer": {
"description": "Oynatıcıya bir görselleştirici ekler",
"menu": {
"visualizer-type": "Görselleştirici Tipi"
},
"name": "Görselleştirici"
} }
} }
} }

View File

@ -0,0 +1,21 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Lỗi khi bắt đầu phần mở rộng {{pluginName}}::{{contextName}}",
"executed-at-ms": "Phần mở rộng {{pluginName}}::{{contextName}} đã bắt đầu trong {{ms}}ms",
"initialize-failed": "Lỗi khi khởi động phần mở rộng \"{{pluginName}}\"",
"load-all": "Đang tải tất cả phần mở rộng",
"load-failed": "Lỗi khi tải phần mở rộng\"{{pluginName}}\"",
"loaded": "Đã tải phần mở rộng \"{{pluginName}}\"",
"unload-failed": "Lỗi khi hủy tải phần mở rộng \"{{pluginName}}\"",
"unloaded": "Đã hủy tải phần mở rộng \"{{pluginName}}\""
}
}
},
"language": {
"code": "vi",
"local-name": "Tiếng Việt",
"name": "Vietnamese"
}
}

View File

@ -170,7 +170,8 @@
}, },
"plugins": { "plugins": {
"enabled": "已启用", "enabled": "已启用",
"label": "插件" "label": "插件",
"new": "新建"
}, },
"view": { "view": {
"label": "视图", "label": "视图",
@ -201,6 +202,10 @@
}, },
"name": "广告屏蔽器" "name": "广告屏蔽器"
}, },
"album-actions": {
"description": "添加作用于播放列表或专辑中所有歌曲的全局“点赞/取消点赞”与“喜欢/取消喜欢”按钮。",
"name": "专辑操作"
},
"album-color-theme": { "album-color-theme": {
"description": "根据专辑封面配色动态改变主题与视觉效果", "description": "根据专辑封面配色动态改变主题与视觉效果",
"name": "专辑配色主题" "name": "专辑配色主题"
@ -289,7 +294,7 @@
"menu": { "menu": {
"advanced": "高级" "advanced": "高级"
}, },
"name": "交叉淡化 [beta]", "name": "交叉淡化 [Beta]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
@ -426,6 +431,51 @@
"fetched-lyrics": "已从 Genius 获取字幕" "fetched-lyrics": "已从 Genius 获取字幕"
} }
}, },
"music-together": {
"description": "与他人共享播放列表。当发起人播放歌曲时,其他人也会听到相同歌曲",
"dialog": {
"enter-host": "输入发起人 ID"
},
"internal": {
"save": "保存",
"track-source": "追踪来源",
"unknown-user": "未知用户"
},
"menu": {
"click-to-copy-id": "复制发起者 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 复制到剪切板",
"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": { "navigation": {
"description": "如同浏览器般,在应用界面内直接显示前进/后退导航按钮", "description": "如同浏览器般,在应用界面内直接显示前进/后退导航按钮",
"name": "导航" "name": "导航"

View File

@ -687,13 +687,15 @@ app.whenReady().then(async () => {
const dialogOptions: Electron.MessageBoxOptions = { const dialogOptions: Electron.MessageBoxOptions = {
type: 'info', type: 'info',
buttons: [ buttons: [
t('main.dialog.update-available.buttons.download'),
t('main.dialog.update-available.buttons.ok'), t('main.dialog.update-available.buttons.ok'),
t('main.dialog.update-available.buttons.download'),
t('main.dialog.update-available.buttons.disable'), t('main.dialog.update-available.buttons.disable'),
], ],
title: t('main.dialog.update-available.title'), title: t('main.dialog.update-available.title'),
message: t('main.dialog.update-available.message'), message: t('main.dialog.update-available.message'),
detail: t('main.dialog.update-available.detail', { downloadLink }), detail: t('main.dialog.update-available.detail', { downloadLink }),
defaultId: 1,
cancelId: 0,
}; };
let dialogPromise: Promise<Electron.MessageBoxReturnValue>; let dialogPromise: Promise<Electron.MessageBoxReturnValue>;
@ -717,7 +719,7 @@ app.whenReady().then(async () => {
break; break;
} }
default: { case 0: {
break; break;
} }
} }

View File

@ -11,7 +11,7 @@ export default createPlugin({
name: () => t('plugins.album-actions.name'), name: () => t('plugins.album-actions.name'),
description: () => t('plugins.album-actions.description'), description: () => t('plugins.album-actions.description'),
restartNeeded: false, restartNeeded: false,
addedVersion: '3.2.0', addedVersion: '3.2.X',
config: { config: {
enabled: false, enabled: false,
}, },

View File

@ -1,5 +1,7 @@
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import { DataConnection } from 'peerjs';
import { t } from '@/i18n'; import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '@/providers/prompt-options';
@ -17,7 +19,6 @@ import style from './style.css?inline';
import type { YoutubePlayer } from '@/types/youtube-player'; import type { YoutubePlayer } from '@/types/youtube-player';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { VideoDataChanged } from '@/types/video-data-changed'; import type { VideoDataChanged } from '@/types/video-data-changed';
import { DataConnection } from 'peerjs';
type RawAccountData = { type RawAccountData = {
accountName: { accountName: {
@ -34,59 +35,84 @@ type RawAccountData = {
}; };
}; };
export default createPlugin({ export default createPlugin<
unknown,
unknown,
{
connection?: Connection;
ipc?: RendererContext<never>['ipc'];
api: HTMLElement & AppAPI | null;
queue?: Queue;
playerApi?: YoutubePlayer;
showPrompt: (title: string, label: string) => Promise<string>;
popups: {
host: ReturnType<typeof createHostPopup>;
guest: ReturnType<typeof createGuestPopup>;
setting: ReturnType<typeof createSettingPopup>;
};
elements: {
setting: HTMLElement;
icon: SVGElement;
spinner: HTMLElement;
};
stateInterval?: number;
updateNext: boolean;
ignoreChange: boolean;
rollbackInjector?: (() => void);
me?: Omit<Profile, 'id'>;
profiles: Record<string, Profile>;
permission: Permission;
videoChangeListener: (event: CustomEvent<VideoDataChanged>) => void;
videoStateChangeListener: () => void;
onHost: () => Promise<boolean>;
onJoin: () => Promise<boolean>;
onStop: () => void;
putProfile: (id: string, profile?: Profile) => void;
showSpinner: () => void;
hideSpinner: () => void;
initMyProfile: () => void;
}
>({
name: () => t('plugins.music-together.name'), name: () => t('plugins.music-together.name'),
description: () => t('plugins.music-together.description'), description: () => t('plugins.music-together.description'),
restartNeeded: false, restartNeeded: false,
addedVersion: '3.2.0', addedVersion: '3.2.X',
config: { config: {
enabled: false enabled: false
}, },
stylesheets: [style], stylesheets: [style],
backend: { backend({ ipc }) {
async start({ ipc }) { ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({
ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({ title,
title, label,
label, type: 'input',
type: 'input', ...promptOptions()
...promptOptions() }));
}));
}
}, },
renderer: { renderer: {
connection: null as Connection | null, updateNext: false,
ipc: null as RendererContext<never>['ipc'] | null, ignoreChange: false,
permission: 'playlist',
api: null as (HTMLElement & AppAPI) | null,
queue: null as Queue | null,
playerApi: null as YoutubePlayer | null,
showPrompt: (async () => null) as ((title: string, label: string) => Promise<string | null>),
elements: {} as {
setting: HTMLElement;
icon: SVGElement;
spinner: HTMLElement;
},
popups: {} as { popups: {} as {
host: ReturnType<typeof createHostPopup>; host: ReturnType<typeof createHostPopup>;
guest: ReturnType<typeof createGuestPopup>; guest: ReturnType<typeof createGuestPopup>;
setting: ReturnType<typeof createSettingPopup>; setting: ReturnType<typeof createSettingPopup>;
}, },
stateInterval: null as number | null, elements: {} as {
updateNext: false, setting: HTMLElement;
ignoreChange: false, icon: SVGElement;
rollbackInjector: null as (() => void) | null, spinner: HTMLElement;
},
me: null as Omit<Profile, 'id'> | null, profiles: {},
profiles: {} as Record<string, Profile>, showPrompt: () => Promise.resolve(''),
permission: 'playlist' as Permission, api: null,
/* events */ /* events */
videoChangeListener(event: CustomEvent<VideoDataChanged>) { videoChangeListener(event: CustomEvent<VideoDataChanged>) {
if (event.detail.name === 'dataloaded' || this.updateNext) { if (event.detail.name === 'dataloaded' || this.updateNext) {
if (this.connection?.mode === 'host') { if (this.connection?.mode === 'host') {
const videoList: VideoData[] = this.queue?.flatItems.map((it: any) => ({ const videoList: VideoData[] = this.queue?.flatItems.map((it) => ({
videoId: it.videoId, videoId: it!.videoId,
ownerId: this.connection!.id ownerId: this.connection!.id
} satisfies VideoData)) ?? []; } satisfies VideoData)) ?? [];
@ -123,8 +149,8 @@ export default createPlugin({
if (!wait) return false; if (!wait) return false;
if (!this.me) this.me = getDefaultProfile(this.connection.id); if (!this.me) this.me = getDefaultProfile(this.connection.id);
const rawItems = this.queue?.flatItems?.map((it: any) => ({ const rawItems = this.queue?.flatItems?.map((it) => ({
videoId: it.videoId, videoId: it!.videoId,
ownerId: this.connection!.id ownerId: this.connection!.id
} satisfies VideoData)) ?? []; } satisfies VideoData)) ?? [];
this.queue?.setOwner({ this.queue?.setOwner({
@ -170,7 +196,7 @@ export default createPlugin({
case 'REMOVE_SONG': { case 'REMOVE_SONG': {
if (conn && this.permission === 'host-only') return; if (conn && this.permission === 'host-only') return;
await this.queue?.removeVideo(event.payload.index); this.queue?.removeVideo(event.payload.index);
await this.connection?.broadcast('REMOVE_SONG', event.payload); await this.connection?.broadcast('REMOVE_SONG', event.payload);
break; break;
} }
@ -295,11 +321,11 @@ export default createPlugin({
break; break;
} }
case 'REMOVE_SONG': { case 'REMOVE_SONG': {
await this.queue?.removeVideo(event.payload.index); this.queue?.removeVideo(event.payload.index);
break; break;
} }
case 'MOVE_SONG': { case 'MOVE_SONG': {
await this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex); this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
break; break;
} }
case 'IDENTIFY': { case 'IDENTIFY': {
@ -461,7 +487,7 @@ export default createPlugin({
this.queue?.removeQueueOwner(); this.queue?.removeQueueOwner();
if (this.rollbackInjector) { if (this.rollbackInjector) {
this.rollbackInjector(); this.rollbackInjector();
this.rollbackInjector = null; this.rollbackInjector = undefined;
} }
this.profiles = {}; this.profiles = {};
@ -530,7 +556,7 @@ export default createPlugin({
start({ ipc }) { start({ ipc }) {
this.ipc = ipc; this.ipc = ipc;
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label); this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>;
this.api = document.querySelector<HTMLElement & AppAPI>('ytmusic-app'); this.api = document.querySelector<HTMLElement & AppAPI>('ytmusic-app');
/* setup */ /* setup */
@ -571,10 +597,15 @@ export default createPlugin({
} }
if (id === 'music-together-copy-id') { if (id === 'music-together-copy-id') {
navigator.clipboard.writeText(this.connection?.id ?? ''); navigator.clipboard.writeText(this.connection?.id ?? '')
.then(() => {
this.api?.openToast(t('plugins.music-together.toast.id-copied')); this.api?.openToast(t('plugins.music-together.toast.id-copied'));
hostPopup.dismiss(); hostPopup.dismiss();
})
.catch(() => {
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed'));
hostPopup.dismiss();
});
} }
if (id === 'music-together-permission') { if (id === 'music-together-permission') {
@ -614,9 +645,14 @@ export default createPlugin({
this.hideSpinner(); this.hideSpinner();
if (result) { if (result) {
navigator.clipboard.writeText(this.connection?.id ?? ''); navigator.clipboard.writeText(this.connection?.id ?? '')
this.api?.openToast(t('plugins.music-together.toast.id-copied')); .then(() => {
hostPopup.showAtAnchor(setting); this.api?.openToast(t('plugins.music-together.toast.id-copied'));
hostPopup.showAtAnchor(setting);
}).catch(() => {
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed'));
hostPopup.showAtAnchor(setting);
});
} else { } else {
this.api?.openToast(t('plugins.music-together.toast.host-failed')); this.api?.openToast(t('plugins.music-together.toast.host-failed'));
} }
@ -642,7 +678,7 @@ export default createPlugin({
guest: guestPopup, guest: guestPopup,
setting: settingPopup setting: settingPopup
}; };
setting.addEventListener('click', async () => { setting.addEventListener('click', () => {
let popup = settingPopup; let popup = settingPopup;
if (this.connection?.mode === 'host') popup = hostPopup; if (this.connection?.mode === 'host') popup = hostPopup;
if (this.connection?.mode === 'guest') popup = guestPopup; if (this.connection?.mode === 'guest') popup = guestPopup;

View File

@ -1,15 +1,12 @@
import { SHA1Hash } from './sha1hash'; import { SHA1Hash } from './sha1hash';
export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure\-3PAPISID=([^;]+);/)?.[1]; export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
export const getHash = (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => { export const getHash = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') =>
const hash = SHA1Hash(); (await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
hash.update(`${millis} ${papisid} ${origin}`);
return hash.digestString().toLowerCase();
};
export const getAuthorizationHeader = (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => { export const getAuthorizationHeader = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => {
return `SAPISIDHASH ${millis}_${getHash(papisid, millis, origin)}`; return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
}; };
export const getClient = () => { export const getClient = () => {

View File

@ -1,12 +1,51 @@
import { getMusicQueueRenderer } from './song'; import { getMusicQueueRenderer } from './song';
import { mapQueueItem } from './utils'; import { mapQueueItem } from './utils';
import type { Profile, QueueAPI, VideoData } from '../types';
import { ConnectionEventUnion } from '@/plugins/music-together/connection'; import { ConnectionEventUnion } from '@/plugins/music-together/connection';
import { t } from '@/i18n'; import { t } from '@/i18n';
import type { Profile, QueueAPI, VideoData } from '../types';
import type { QueueItem } from '@/types/datahost-get-state';
const getHeaderPayload = (() => { const getHeaderPayload = (() => {
let payload: unknown = null; let payload: {
items?: QueueItem[] | undefined;
title: {
runs: {
text: string;
}[];
};
subtitle: {
runs: {
text: string;
}[];
};
buttons: {
chipCloudChipRenderer: {
style: {
styleType: string;
};
text: {
runs: {
text: string;
}[];
};
navigationEndpoint: {
saveQueueToPlaylistCommand: unknown;
};
icon: {
iconType: string;
};
accessibilityData: {
accessibilityData: {
label: string;
};
};
isSelected: boolean;
uniqueId: string;
};
}[];
} | null = null;
return () => { return () => {
if (!payload) { if (!payload) {
@ -58,7 +97,7 @@ const getHeaderPayload = (() => {
} }
return payload; return payload;
} };
})(); })();
export type QueueOptions = { export type QueueOptions = {
@ -70,11 +109,11 @@ export type QueueOptions = {
export type QueueEventListener = (event: ConnectionEventUnion) => void; export type QueueEventListener = (event: ConnectionEventUnion) => void;
export class Queue { export class Queue {
private queue: (HTMLElement & QueueAPI) | null = null; private queue: (HTMLElement & QueueAPI);
private originalDispatch: ((obj: { private originalDispatch?: (obj: {
type: string; type: string;
payload?: unknown; payload?: { items?: QueueItem[] | undefined; };
}) => void) | null = null; }) => void;
private internalDispatch = false; private internalDispatch = false;
private ignoreFlag = false; private ignoreFlag = false;
private listeners: QueueEventListener[] = []; private listeners: QueueEventListener[] = [];
@ -83,7 +122,7 @@ export class Queue {
constructor(options: QueueOptions) { constructor(options: QueueOptions) {
this.getProfile = options.getProfile; this.getProfile = options.getProfile;
this.queue = options.queue ?? document.querySelector<HTMLElement & QueueAPI>('#queue'); this.queue = options.queue ?? document.querySelector<HTMLElement & QueueAPI>('#queue')!;
this.owner = options.owner ?? null; this.owner = options.owner ?? null;
this._videoList = options.videoList ?? []; this._videoList = options.videoList ?? [];
} }
@ -96,7 +135,7 @@ export class Queue {
} }
get selectedIndex() { get selectedIndex() {
return mapQueueItem((it) => it?.selected, this.queue?.store.getState().queue.items).findIndex(Boolean) ?? 0; return mapQueueItem((it) => it?.selected, this.queue.store.getState().queue.items).findIndex(Boolean) ?? 0;
} }
get rawItems() { get rawItems() {
@ -146,7 +185,7 @@ export class Queue {
return true; return true;
} }
async removeVideo(index: number) { removeVideo(index: number) {
this.internalDispatch = true; this.internalDispatch = true;
this._videoList.splice(index, 1); this._videoList.splice(index, 1);
this.queue?.dispatch({ this.queue?.dispatch({
@ -233,10 +272,10 @@ export class Queue {
if (event.type === 'ADD_ITEMS') { if (event.type === 'ADD_ITEMS') {
if (this.ignoreFlag) { if (this.ignoreFlag) {
this.ignoreFlag = false; this.ignoreFlag = false;
const videoList = mapQueueItem((it: any) => ({ const videoList = mapQueueItem((it) => ({
videoId: it.videoId, videoId: it!.videoId,
ownerId: this.owner!.id ownerId: this.owner!.id
} satisfies VideoData), (event.payload as any).items); } satisfies VideoData), event.payload!.items!);
const index = this._videoList.length + videoList.length - 1; const index = this._videoList.length + videoList.length - 1;
if (videoList.length > 0) { if (videoList.length > 0) {
@ -255,15 +294,17 @@ export class Queue {
] ]
}); });
} }
} else if ((event.payload as any).items.length === 1) { } else if ((event.payload as {
items: unknown[];
}).items.length === 1) {
this.broadcast({ // add playlist this.broadcast({ // add playlist
type: 'ADD_SONGS', type: 'ADD_SONGS',
payload: { payload: {
// index: (event.payload as any).index, // index: (event.payload as any).index,
videoList: mapQueueItem((it: any) => ({ videoList: mapQueueItem((it) => ({
videoId: it.videoId, videoId: it!.videoId,
ownerId: this.owner!.id ownerId: this.owner!.id
} satisfies VideoData), (event.payload as any).items) } satisfies VideoData), event.payload!.items!)
} }
}); });
} }
@ -275,8 +316,12 @@ export class Queue {
this.broadcast({ this.broadcast({
type: 'MOVE_SONG', type: 'MOVE_SONG',
payload: { payload: {
fromIndex: (event.payload as any).fromIndex, fromIndex: (event.payload as {
toIndex: (event.payload as any).toIndex fromIndex: number;
}).fromIndex,
toIndex: (event.payload as {
toIndex: number;
}).toIndex
} }
}); });
return; return;
@ -306,7 +351,7 @@ export class Queue {
event.payload = undefined; event.payload = undefined;
} }
if (event.type === 'SET_PLAYER_UI_STATE') { if (event.type === 'SET_PLAYER_UI_STATE') {
if (event.payload === 'INACTIVE' && this.videoList.length > 0) { if (event.payload as string === 'INACTIVE' && this.videoList.length > 0) {
return; return;
} }
} }
@ -321,12 +366,12 @@ export class Queue {
dispatch: this.originalDispatch dispatch: this.originalDispatch
} }
}; };
this.originalDispatch!.call(fakeContext, event); this.originalDispatch?.call(fakeContext, event);
}; };
} }
/* sync */ /* sync */
async initQueue() { initQueue() {
if (!this.queue) return; if (!this.queue) return;
this.internalDispatch = true; this.internalDispatch = true;
@ -369,13 +414,13 @@ export class Queue {
return true; return true;
} }
async syncQueueOwner() { syncQueueOwner() {
const allQueue = document.querySelectorAll('#queue'); const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => { allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []); const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
list.forEach((item, index) => { list.forEach((item, index: number | undefined) => {
if (typeof index !== 'number') return; if (typeof index !== 'number') return;
const id = this._videoList[index]?.ownerId; const id = this._videoList[index]?.ownerId;

View File

@ -1,117 +1,7 @@
export function SHA1Hash(): { export const SHA1Hash = async (str: string) => {
reset: () => void, const enc = new TextEncoder();
update: (message: string | number[], length?: number) => void, const hash = await crypto.subtle.digest('SHA-1', enc.encode(str));
digest: () => number[], return Array.from(new Uint8Array(hash))
digestString: () => string .map((v) => v.toString(16).padStart(2, '0'))
} { .join('');
let hash: number[]; };
function initialize(): void {
hash = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
totalLength = currentLength = 0;
}
function processBlock(block: number[]): void {
let words: number[] = [];
for (let i = 0; i < 64; i += 4) {
words[i / 4] = block[i] << 24 | block[i + 1] << 16 | block[i + 2] << 8 | block[i + 3];
}
for (let i = 16; i < 80; i++) {
let temp = words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16];
words[i] = (temp << 1 | temp >>> 31) & 4294967295;
}
let a = hash[0],
b = hash[1],
c = hash[2],
d = hash[3],
e = hash[4];
for (let i = 0; i < 80; i++) {
let f, k;
if (i < 20) {
f = d ^ b & (c ^ d);
k = 1518500249;
} else if (i < 40) {
f = b ^ c ^ d;
k = 1859775393;
} else if (i < 60) {
f = b & c | d & (b | c);
k = 2400959708;
} else {
f = b ^ c ^ d;
k = 3395469782;
}
let temp = ((a << 5 | a >>> 27) & 4294967295) + f + e + k + words[i] & 4294967295;
e = d;
d = c;
c = (b << 30 | b >>> 2) & 4294967295;
b = a;
a = temp;
}
hash[0] = hash[0] + a & 4294967295;
hash[1] = hash[1] + b & 4294967295;
hash[2] = hash[2] + c & 4294967295;
hash[3] = hash[3] + d & 4294967295;
hash[4] = hash[4] + e & 4294967295;
}
function update(message: string | number[], length?: number): void {
if ('string' === typeof message) {
message = unescape(encodeURIComponent(message));
let bytes: number[] = [];
for (let i = 0, len = message.length; i < len; ++i)
bytes.push(message.charCodeAt(i));
message = bytes;
}
length || (length = message.length);
let i = 0;
if (0 == currentLength)
for (; i + 64 < length;)
processBlock(message.slice(i, i + 64)),
i += 64,
totalLength += 64;
for (; i < length;)
if (buffer[currentLength++] = message[i++],
totalLength++,
64 == currentLength)
for (currentLength = 0,
processBlock(buffer); i + 64 < length;)
processBlock(message.slice(i, i + 64)),
i += 64,
totalLength += 64;
}
function finalize(): number[] {
let result: number[] = []
, bits = 8 * totalLength;
if (currentLength < 56)
update(padding, 56 - currentLength);
else
update(padding, 64 - (currentLength - 56));
for (let i = 63; i >= 56; i--)
buffer[i] = bits & 255,
bits >>>= 8;
processBlock(buffer);
for (let i = 0; i < 5; i++)
for (let j = 24; j >= 0; j -= 8)
result.push(hash[i] >> j & 255);
return result;
}
let buffer: number[] = [], padding: number[] = [128], totalLength: number, currentLength: number;
for (let i = 1; i < 64; ++i)
padding[i] = 0;
initialize();
return {
reset: initialize,
update: update,
digest: finalize,
digestString: function(): string {
let hash = finalize(), hex = '';
for (let i = 0; i < hash.length; i++)
hex += '0123456789ABCDEF'.charAt(Math.floor(hash[i] / 16)) + '0123456789ABCDEF'.charAt(hash[i] % 16);
return hex;
}
};
}

View File

@ -33,8 +33,8 @@ export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRe
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Origin: 'https://music.youtube.com', 'Origin': 'https://music.youtube.com',
Authorization: getAuthorizationHeader(token) 'Authorization': await getAuthorizationHeader(token),
} }
} }
); );

View File

@ -1,15 +1,21 @@
export const mapQueueItem = <T>(map: (item: any | null) => T, array: any[]): T[] => array import {
ItemPlaylistPanelVideoRenderer,
PlaylistPanelVideoWrapperRenderer,
QueueItem
} from '@/types/datahost-get-state';
export const mapQueueItem = <T>(map: (item?: ItemPlaylistPanelVideoRenderer) => T, array: QueueItem[]): T[] => array
.map((item) => { .map((item) => {
if ('playlistPanelVideoWrapperRenderer' in item) { if ('playlistPanelVideoWrapperRenderer' in item) {
const keys = Object.keys(item.playlistPanelVideoWrapperRenderer.primaryRenderer); const keys = Object.keys(item.playlistPanelVideoWrapperRenderer!.primaryRenderer) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
return item.playlistPanelVideoWrapperRenderer.primaryRenderer[keys[0]]; return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
} }
if ('playlistPanelVideoRenderer' in item) { if ('playlistPanelVideoRenderer' in item) {
return item.playlistPanelVideoRenderer; return item.playlistPanelVideoRenderer;
} }
console.error('Music Together: Unknown item', item); console.error('Music Together: Unknown item', item);
return null; return undefined;
}) })
.map(map); .map(map);

View File

@ -1,14 +1,14 @@
<div class="music-together-status"> <div class="music-together-status">
<div class="music-together-status-container"> <div class="music-together-status-container">
<img class="music-together-profile big"> <img class="music-together-profile big" alt="Profile Image">
<div class="music-together-status-item"> <div class="music-together-status-item">
<ytmd-trans key="plugins.music-together.name"></ytmd-trans> <ytmd-trans key="plugins.music-together.name"></ytmd-trans>
<span id="music-together-status-label"> <span id="music-together-status-label">
<ytmd-trans key="plugins.music-together.menu.status.disconnected"></ytmd-trans> <ytmd-trans key="plugins.music-together.menu.status.disconnected"></ytmd-trans>
</span> </span>
<span id="music-together-permission-label"> <marquee id="music-together-permission-label">
<ytmd-trans key="plugins.music-together.menu.permission.playlist" style="color: rgba(255, 255, 255, 0.75)"></ytmd-trans> <ytmd-trans key="plugins.music-together.menu.permission.playlist" style="color: rgba(255, 255, 255, 0.75)"></ytmd-trans>
</span> </marquee>
</div> </div>
</div> </div>
<div class="music-together-divider horizontal" style="margin: 16px 0;"></div> <div class="music-together-divider horizontal" style="margin: 16px 0;"></div>

View File

@ -1,15 +1,19 @@
import { YoutubePlayer } from '@/types/youtube-player'; import { YoutubePlayer } from '@/types/youtube-player';
type StoreState = any; import { GetState, QueueItem } from '@/types/datahost-get-state';
type StoreState = GetState;
type Store = { type Store = {
dispatch: (obj: { dispatch: (obj: {
type: string; type: string;
payload?: unknown; payload?: {
items?: QueueItem[];
};
}) => void; }) => void;
getState: () => StoreState; getState: () => StoreState;
replaceReducer: (param1: unknown) => unknown; replaceReducer: (param1: unknown) => unknown;
subscribe: (callback: () => void) => unknown; subscribe: (callback: () => void) => unknown;
}; }
export type QueueAPI = { export type QueueAPI = {
dispatch(obj: { dispatch(obj: {
type: string; type: string;
@ -28,8 +32,6 @@ export type AppAPI = {
// TODO: Add more // TODO: Add more
}; };
export type Profile = { export type Profile = {
id: string; id: string;
handleId: string; handleId: string;

View File

@ -1,6 +1,8 @@
import { ElementFromHtml } from '@/plugins/utils/renderer'; import { ElementFromHtml } from '@/plugins/utils/renderer';
import statusHTML from '../templates/status.html?raw';
import { t } from '@/i18n'; import { t } from '@/i18n';
import statusHTML from '../templates/status.html?raw';
import type { Permission, Profile } from '../types'; import type { Permission, Profile } from '../types';
export const createStatus = () => { export const createStatus = () => {
@ -9,7 +11,7 @@ export const createStatus = () => {
const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!; const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!;
const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!; const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!;
const permissionLabel = element.querySelector<HTMLSpanElement>('#music-together-permission-label')!; const permissionLabel = element.querySelector<HTMLMarqueeElement>('#music-together-permission-label')!;
profile.src = icon?.src ?? ''; profile.src = icon?.src ?? '';

View File

@ -40,7 +40,6 @@ const audioCanPlayListener = (e: CustomEvent<Compressor>) => {
const fftBins = new Float32Array(analyser.frequencyBinCount); const fftBins = new Float32Array(analyser.frequencyBinCount);
sourceNode.connect(analyser); sourceNode.connect(analyser);
analyser.connect(audioContext.destination);
const looper = () => { const looper = () => {
setTimeout(() => { setTimeout(() => {

View File

@ -127,7 +127,7 @@ export default (api: YoutubePlayer) => {
const waitingEvent = new Set<string>(); const waitingEvent = new Set<string>();
// Name = "dataloaded" and abit later "dataupdated" // Name = "dataloaded" and abit later "dataupdated"
api.addEventListener('videodatachange', (name: string, videoData) => { api.addEventListener('videodatachange', (name, videoData) => {
videoEventDispatcher(name, videoData); videoEventDispatcher(name, videoData);
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) { if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {

View File

@ -1,10 +1,12 @@
import { Menu, nativeImage, Tray } from 'electron'; import { Menu, nativeImage, Tray } from 'electron';
import youtubeMusicTrayIcon from '@assets/youtube-music-tray.png?asset&asarUnpack'; import defaultTrayIconAsset from '@assets/youtube-music-tray.png?asset&asarUnpack';
import pausedTrayIconAsset from '@assets/youtube-music-tray-paused.png?asset&asarUnpack';
import config from './config'; import config from './config';
import { restart } from './providers/app-controls'; import { restart } from './providers/app-controls';
import registerCallback from './providers/song-info';
import getSongControls from './providers/song-controls'; import getSongControls from './providers/song-controls';
import { t } from '@/i18n'; import { t } from '@/i18n';
@ -46,14 +48,18 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
const { playPause, next, previous } = getSongControls(win); const { playPause, next, previous } = getSongControls(win);
const trayIcon = nativeImage.createFromPath(youtubeMusicTrayIcon).resize({ const defaultTrayIcon = nativeImage.createFromPath(defaultTrayIconAsset).resize({
width: 16,
height: 16,
});
const pausedTrayIcon = nativeImage.createFromPath(pausedTrayIconAsset).resize({
width: 16, width: 16,
height: 16, height: 16,
}); });
tray = new Tray(trayIcon); tray = new Tray(defaultTrayIcon);
tray.setToolTip('YouTube Music'); tray.setToolTip(t('main.tray.tooltip.default'));
// MacOS only // MacOS only
tray.setIgnoreDoubleClickEvents(true); tray.setIgnoreDoubleClickEvents(true);
@ -110,4 +116,18 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
const trayMenu = Menu.buildFromTemplate(template); const trayMenu = Menu.buildFromTemplate(template);
tray.setContextMenu(trayMenu); tray.setContextMenu(trayMenu);
registerCallback(songInfo => {
if (typeof songInfo.isPaused === 'undefined') {
tray.setImage(defaultTrayIcon);
return;
}
tray.setToolTip(t('main.tray.tooltip.with-song-info', {
artist: songInfo.artist,
title: songInfo.title,
}));
tray.setImage(songInfo.isPaused ? pausedTrayIcon : defaultTrayIcon);
})
}; };

View File

@ -256,6 +256,7 @@ export type VideoDataChangeValue = Record<string, unknown> & {
export interface PlayerAPIEvents { export interface PlayerAPIEvents {
videodatachange: { videodatachange: {
value: VideoDataChangeValue; value: VideoDataChangeValue;
} & ({ name: 'dataloaded' } | { name: 'dataupdated ' }); name: 'dataloaded' | 'dataupdated';
};
onStateChange: number; onStateChange: number;
} }

View File

@ -357,8 +357,8 @@ export interface YoutubePlayer {
type: K, type: K,
listener: ( listener: (
this: Document, this: Document,
name: PlayerAPIEvents[K]['name'], name: K extends 'videodatachange' ? PlayerAPIEvents[K]['name'] : never,
data: PlayerAPIEvents[K]['value'], data: K extends 'videodatachange' ? PlayerAPIEvents[K]['value'] : never,
) => void, ) => void,
options?: boolean | AddEventListenerOptions | undefined, options?: boolean | AddEventListenerOptions | undefined,
) => void; ) => void;
@ -366,8 +366,8 @@ export interface YoutubePlayer {
type: K, type: K,
listener: ( listener: (
this: Document, this: Document,
name: PlayerAPIEvents[K]['name'], name: K extends 'videodatachange' ? PlayerAPIEvents[K]['name'] : never,
data: PlayerAPIEvents[K]['value'], data: K extends 'videodatachange' ? PlayerAPIEvents[K]['value'] : never,
) => void, ) => void,
options?: boolean | EventListenerOptions | undefined, options?: boolean | EventListenerOptions | undefined,
) => void; ) => void;