mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Compare commits
42 Commits
synced-lyr
...
62ef1734e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 62ef1734e4 | |||
| 99be7c2629 | |||
| 200da8dfaa | |||
| 9a218a6516 | |||
| 933ee0ef75 | |||
| 8dd7bcdf97 | |||
| d5c7e0475b | |||
| a5af233683 | |||
| 27e3796622 | |||
| 5843e85c4d | |||
| 92a943c755 | |||
| 58a19cdaa2 | |||
| b1d2112bfc | |||
| d229bc7f00 | |||
| 033a4d3122 | |||
| 8d252b6375 | |||
| 9453c0ca8f | |||
| ce073b30d9 | |||
| 8f63e5e3a3 | |||
| 23853b66c6 | |||
| 62a322f10d | |||
| 9627dd2202 | |||
| 3383926faa | |||
| 8179664064 | |||
| 1a5e417f4f | |||
| ceb6da9bc9 | |||
| fdafb2dd07 | |||
| c3700e0e59 | |||
| 3f1c26f82d | |||
| bb7816815c | |||
| a1773fd992 | |||
| 1671bea942 | |||
| 141ae03208 | |||
| 55c1012cda | |||
| 612c5c89c9 | |||
| 01bbf7e3f7 | |||
| 6a7b7d88de | |||
| d06896450c | |||
| 4a59afc505 | |||
| 7b0d63b6cf | |||
| c734ffe70f | |||
| 3d0ad69ddb |
36
package.json
36
package.json
@ -45,11 +45,11 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "npm:rolldown-vite@7.1.8",
|
"vite": "npm:rolldown-vite@7.3.0",
|
||||||
"node-gyp": "11.4.2",
|
"node-gyp": "11.4.2",
|
||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"@electron/universal": "3.0.1",
|
"@electron/universal": "3.0.2",
|
||||||
"@babel/runtime": "7.28.4"
|
"@babel/runtime": "7.28.4"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
@ -67,15 +67,15 @@
|
|||||||
"@electron-toolkit/tsconfig": "1.0.1",
|
"@electron-toolkit/tsconfig": "1.0.1",
|
||||||
"@electron/remote": "2.1.3",
|
"@electron/remote": "2.1.3",
|
||||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||||
"@ffmpeg.wasm/main": "0.12.0",
|
"@ffmpeg.wasm/main": "0.13.1",
|
||||||
"@floating-ui/dom": "1.7.4",
|
"@floating-ui/dom": "1.7.4",
|
||||||
"@foobar404/wave": "2.0.5",
|
"@foobar404/wave": "2.0.5",
|
||||||
"@ghostery/adblocker-electron": "2.11.6",
|
"@ghostery/adblocker-electron": "2.11.6",
|
||||||
"@ghostery/adblocker-electron-preload": "2.11.6",
|
"@ghostery/adblocker-electron-preload": "2.11.6",
|
||||||
"@hono/node-server": "1.19.1",
|
"@hono/node-server": "1.19.7",
|
||||||
"@hono/node-ws": "1.2.0",
|
"@hono/node-ws": "1.2.0",
|
||||||
"@hono/swagger-ui": "0.5.2",
|
"@hono/swagger-ui": "0.5.2",
|
||||||
"@hono/zod-openapi": "1.1.0",
|
"@hono/zod-openapi": "1.2.0",
|
||||||
"@hono/zod-validator": "0.7.2",
|
"@hono/zod-validator": "0.7.2",
|
||||||
"@jellybrick/dbus-next": "0.10.3",
|
"@jellybrick/dbus-next": "0.10.3",
|
||||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||||
@ -90,7 +90,6 @@
|
|||||||
"butterchurn-presets": "3.0.0-beta.4",
|
"butterchurn-presets": "3.0.0-beta.4",
|
||||||
"color": "5.0.0",
|
"color": "5.0.0",
|
||||||
"conf": "14.0.0",
|
"conf": "14.0.0",
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"custom-electron-prompt": "1.5.8",
|
"custom-electron-prompt": "1.5.8",
|
||||||
"deepmerge-ts": "7.1.5",
|
"deepmerge-ts": "7.1.5",
|
||||||
"delay": "6.0.0",
|
"delay": "6.0.0",
|
||||||
@ -106,8 +105,8 @@
|
|||||||
"fflate": "0.8.2",
|
"fflate": "0.8.2",
|
||||||
"filenamify": "6.0.0",
|
"filenamify": "6.0.0",
|
||||||
"hanja": "1.1.5",
|
"hanja": "1.1.5",
|
||||||
"happy-dom": "18.0.1",
|
"happy-dom": "20.0.2",
|
||||||
"hono": "4.9.6",
|
"hono": "4.10.3",
|
||||||
"howler": "2.2.4",
|
"howler": "2.2.4",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"i18next": "25.5.2",
|
"i18next": "25.5.2",
|
||||||
@ -132,11 +131,11 @@
|
|||||||
"solid-transition-group": "0.3.0",
|
"solid-transition-group": "0.3.0",
|
||||||
"tiny-pinyin": "1.3.2",
|
"tiny-pinyin": "1.3.2",
|
||||||
"tinyld": "1.3.4",
|
"tinyld": "1.3.4",
|
||||||
"virtua": "0.42.3",
|
"virtua": "0.48.2",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"x11": "2.3.0",
|
"x11": "2.3.0",
|
||||||
"youtubei.js": "^16.0.1",
|
"youtubei.js": "^16.0.1",
|
||||||
"zod": "4.1.5"
|
"zod": "4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/tsconfig": "1.0.1",
|
"@electron-toolkit/tsconfig": "1.0.1",
|
||||||
@ -145,7 +144,6 @@
|
|||||||
"@playwright/test": "1.55.0",
|
"@playwright/test": "1.55.0",
|
||||||
"@stylistic/eslint-plugin": "5.3.1",
|
"@stylistic/eslint-plugin": "5.3.1",
|
||||||
"@total-typescript/ts-reset": "0.6.1",
|
"@total-typescript/ts-reset": "0.6.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
|
||||||
"@types/electron-localshortcut": "3.1.3",
|
"@types/electron-localshortcut": "3.1.3",
|
||||||
"@types/howler": "2.2.12",
|
"@types/howler": "2.2.12",
|
||||||
"@types/html-to-text": "9.0.4",
|
"@types/html-to-text": "9.0.4",
|
||||||
@ -155,12 +153,12 @@
|
|||||||
"builtin-modules": "5.0.0",
|
"builtin-modules": "5.0.0",
|
||||||
"cross-env": "10.0.0",
|
"cross-env": "10.0.0",
|
||||||
"del-cli": "6.0.0",
|
"del-cli": "6.0.0",
|
||||||
"discord-api-types": "0.38.23",
|
"discord-api-types": "0.38.37",
|
||||||
"electron": "38.2.0",
|
"electron": "38.7.2",
|
||||||
"electron-builder": "26.0.12",
|
"electron-builder": "26.0.12",
|
||||||
"electron-builder-squirrel-windows": "26.0.12",
|
"electron-builder-squirrel-windows": "26.0.12",
|
||||||
"electron-devtools-installer": "4.0.0",
|
"electron-devtools-installer": "4.0.0",
|
||||||
"electron-vite": "4.0.0",
|
"electron-vite": "4.0.1",
|
||||||
"eslint": "9.35.0",
|
"eslint": "9.35.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||||
@ -168,14 +166,14 @@
|
|||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-prettier": "5.5.4",
|
"eslint-plugin-prettier": "5.5.4",
|
||||||
"eslint-plugin-solid": "0.14.5",
|
"eslint-plugin-solid": "0.14.5",
|
||||||
"glob": "11.0.3",
|
"glob": "11.1.0",
|
||||||
"node-gyp": "11.4.2",
|
"node-gyp": "11.4.2",
|
||||||
"playwright": "1.55.0",
|
"playwright": "1.55.1",
|
||||||
"ts-morph": "27.0.0",
|
"ts-morph": "27.0.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.43.0",
|
"typescript-eslint": "8.43.0",
|
||||||
"utf-8-validate": "6.0.5",
|
"utf-8-validate": "6.0.5",
|
||||||
"vite": "npm:rolldown-vite@7.1.8",
|
"vite": "npm:rolldown-vite@7.3.0",
|
||||||
"vite-plugin-inspect": "11.3.3",
|
"vite-plugin-inspect": "11.3.3",
|
||||||
"vite-plugin-resolve": "2.5.2",
|
"vite-plugin-resolve": "2.5.2",
|
||||||
"vite-plugin-solid": "2.11.8",
|
"vite-plugin-solid": "2.11.8",
|
||||||
|
|||||||
946
pnpm-lock.yaml
generated
946
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"console": {
|
"console": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"execute-failed": "Ha fallat l'execució de l'extensió {{pluginName}}::{{contextName}}",
|
"execute-failed": "Error en l'execució de l'extensió {{pluginName}}::{{contextName}}",
|
||||||
"executed-at-ms": "L'extensió {{pluginName}}::{{contextName}} s'ha executat als {{ms}}ms",
|
"executed-at-ms": "L'extensió {{pluginName}}::{{contextName}} s'ha executat als {{ms}}ms",
|
||||||
"initialize-failed": "Ha fallat la inicialització de l'extensió \"{{pluginName}}\"",
|
"initialize-failed": "Ha fallat la inicialització de l'extensió \"{{pluginName}}\"",
|
||||||
"load-all": "Carregant totes les extensions",
|
"load-all": "Carregant totes les extensions",
|
||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Activar el tema en la barra de progrés"
|
||||||
},
|
},
|
||||||
"name": "Tema de color de l'àlbum"
|
"name": "Tema de color de l'àlbum"
|
||||||
},
|
},
|
||||||
@ -462,8 +463,8 @@
|
|||||||
"label": "Text d'estat",
|
"label": "Text d'estat",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Escoltant {artist}",
|
"artist": "Escoltant {artist}",
|
||||||
"title": "Escoltant {song title}",
|
"pear-desktop": "Escoltant Pear Desktop",
|
||||||
"pear-desktop": "Escoltant Pear Desktop"
|
"title": "Escoltant {song title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Suchleisten-Design aktivieren"
|
||||||
},
|
},
|
||||||
"name": "Thema aus Albumfarbe"
|
"name": "Thema aus Albumfarbe"
|
||||||
},
|
},
|
||||||
@ -606,7 +607,7 @@
|
|||||||
"permission": {
|
"permission": {
|
||||||
"all": "Gästen erlauben, Wiederhabeliste und Player zu bedienen",
|
"all": "Gästen erlauben, Wiederhabeliste und Player zu bedienen",
|
||||||
"host-only": "Nur der Host kann die Playlist und den Player kontrollieren",
|
"host-only": "Nur der Host kann die Playlist und den Player kontrollieren",
|
||||||
"playlist": "Gäste das Kontrollieren der Playlist erlauben"
|
"playlist": "Gästen das Kontrollieren der Playlist erlauben"
|
||||||
},
|
},
|
||||||
"set-permission": "Kontrollberechtigung ändern",
|
"set-permission": "Kontrollberechtigung ändern",
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
@ -323,6 +323,22 @@
|
|||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"label": "Port"
|
"label": "Port"
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"label": "HTTPS & Certificates",
|
||||||
|
"submenu": {
|
||||||
|
"enable-https": {
|
||||||
|
"label": "Enable HTTPS"
|
||||||
|
},
|
||||||
|
"cert": {
|
||||||
|
"label": "Certificate file (.crt/.pem)",
|
||||||
|
"dialogTitle": "Select HTTPS certificate file"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"label": "Private key file (.key/.pem)",
|
||||||
|
"dialogTitle": "Select HTTPS private key file"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "API Server [Beta]",
|
"name": "API Server [Beta]",
|
||||||
|
|||||||
@ -3,11 +3,11 @@
|
|||||||
"console": {
|
"console": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"execute-failed": "Error al ejecutar el complemento {{pluginName}}::{{contextName}}",
|
"execute-failed": "Error al ejecutar el complemento {{pluginName}}::{{contextName}}",
|
||||||
"executed-at-ms": "Complemento {{pluginName}}::{{contextName}} Ejecutó en {{ms}}ms",
|
"executed-at-ms": "Complemento {{pluginName}}::{{contextName}} se ejecutó en {{ms}}ms",
|
||||||
"initialize-failed": "Error al inicializar el complemento \"{{pluginName}}\"",
|
"initialize-failed": "Error al inicializar el complemento \"{{pluginName}}\"",
|
||||||
"load-all": "Cargando todos los complementos",
|
"load-all": "Cargando todos los complementos",
|
||||||
"load-failed": "Error al cargar el complemento \"{{pluginName}}\"",
|
"load-failed": "Error al cargar el complemento \"{{pluginName}}\"",
|
||||||
"loaded": "Complementos \"{{pluginName}}\" cargado",
|
"loaded": "Complementos \"{{pluginName}}\" cargados",
|
||||||
"unload-failed": "No se ha podido descargar el complemento \"{{pluginName}}\"",
|
"unload-failed": "No se ha podido descargar el complemento \"{{pluginName}}\"",
|
||||||
"unloaded": "Complemento \"{{pluginName}}\" descargado"
|
"unloaded": "Complemento \"{{pluginName}}\" descargado"
|
||||||
}
|
}
|
||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Habilitar temas a la barra de búsqueda"
|
||||||
},
|
},
|
||||||
"name": "Tema de color del álbum"
|
"name": "Tema de color del álbum"
|
||||||
},
|
},
|
||||||
@ -462,8 +463,8 @@
|
|||||||
"label": "Texto de estado",
|
"label": "Texto de estado",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Escuchando a {artist}",
|
"artist": "Escuchando a {artist}",
|
||||||
"title": "Escuchando {song title}",
|
"pear-desktop": "Escuchando Pear Desktop",
|
||||||
"pear-desktop": "Escuchando Pear Desktop"
|
"title": "Escuchando {song title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -153,7 +153,7 @@
|
|||||||
"custom-window-title": {
|
"custom-window-title": {
|
||||||
"label": "Titre de fenêtre personnalisé",
|
"label": "Titre de fenêtre personnalisé",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"label": "Entrés un titre de fenêtre : (Laissé vide pour déactiver)",
|
"label": "Entrer un titre de fenêtre : (Laissé vide pour désactiver)",
|
||||||
"placeholder": "Exemple : Pear Desktop"
|
"placeholder": "Exemple : Pear Desktop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Activer le thème sur la barre de progression"
|
||||||
},
|
},
|
||||||
"name": "Thème de couleur d'album"
|
"name": "Thème de couleur d'album"
|
||||||
},
|
},
|
||||||
@ -290,7 +291,7 @@
|
|||||||
"description": "Ajout de la prise en charge de Pear Desktop pour le widget Amuse now playing de 6K Labs",
|
"description": "Ajout de la prise en charge de Pear Desktop pour le widget Amuse now playing de 6K Labs",
|
||||||
"name": "Amuse",
|
"name": "Amuse",
|
||||||
"response": {
|
"response": {
|
||||||
"query": "Le serveur API Amuse est en cours d'exécution. Envoyez une requête GET /query pour obtenir des informations sur la chanson."
|
"query": "Le serveur API d'Amuse est en cours d'exécution. GET /query pour obtenir des informations sur les chansons."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
@ -302,7 +303,7 @@
|
|||||||
"deny": "Interdire"
|
"deny": "Interdire"
|
||||||
},
|
},
|
||||||
"message": "Autoriser {{ID}} ({{origin}}) à accéder à l'API ?",
|
"message": "Autoriser {{ID}} ({{origin}}) à accéder à l'API ?",
|
||||||
"title": "Requête d'autorisation d'API"
|
"title": "Demande d'autorisation à l'API"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -310,7 +311,7 @@
|
|||||||
"label": "Plan d'autorisation",
|
"label": "Plan d'autorisation",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"auth-at-first": {
|
"auth-at-first": {
|
||||||
"label": "Autoriser à la première requête"
|
"label": "Autoriser lors de la première requête"
|
||||||
},
|
},
|
||||||
"none": {
|
"none": {
|
||||||
"label": "Pas d'autorisation"
|
"label": "Pas d'autorisation"
|
||||||
@ -331,7 +332,7 @@
|
|||||||
"title": "Nom d'hôte"
|
"title": "Nom d'hôte"
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"label": "Entrez le port du serveur de l'API :",
|
"label": "Entrez le port du serveur API :",
|
||||||
"title": "Port"
|
"title": "Port"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -392,7 +393,7 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"caption-changed": "Sous-titres changés en {{language}}",
|
"caption-changed": "Sous-titres changés en {{language}}",
|
||||||
"caption-disabled": "Sous-titres désactivés",
|
"caption-disabled": "Sous-titres désactivés",
|
||||||
"no-captions": "Aucun sous-titre disponible pour cette chanson"
|
"no-captions": "Aucun sous-titres disponibles pour cette chanson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compact-sidebar": {
|
"compact-sidebar": {
|
||||||
@ -462,8 +463,8 @@
|
|||||||
"label": "Texte d'état",
|
"label": "Texte d'état",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Écoute {artiste}",
|
"artist": "Écoute {artiste}",
|
||||||
"title": "Écoute {titre de la chanson}",
|
"pear-desktop": "Écoute Pear Desktop",
|
||||||
"pear-desktop": "Écoute Pear Desktop"
|
"title": "Écoute {titre de la chanson}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -636,7 +637,7 @@
|
|||||||
"name": "Navigation",
|
"name": "Navigation",
|
||||||
"templates": {
|
"templates": {
|
||||||
"back": {
|
"back": {
|
||||||
"title": "Revenir à la page précédente"
|
"title": "Aller à la page précédente"
|
||||||
},
|
},
|
||||||
"forward": {
|
"forward": {
|
||||||
"title": "Aller à la page suivante"
|
"title": "Aller à la page suivante"
|
||||||
@ -909,7 +910,7 @@
|
|||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
},
|
},
|
||||||
"unobtrusive-player": {
|
"unobtrusive-player": {
|
||||||
"description": "Empêche le lecteur de s'afficher quand un chanson est en lecture",
|
"description": "Empêche le lecteur de s'afficher quand une chanson est en cours de lecture",
|
||||||
"name": "Lecteur Non-Intrusif"
|
"name": "Lecteur Non-Intrusif"
|
||||||
},
|
},
|
||||||
"video-toggle": {
|
"video-toggle": {
|
||||||
|
|||||||
@ -44,25 +44,38 @@
|
|||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"hide-menu-enabled": {
|
"hide-menu-enabled": {
|
||||||
"detail": "მენიუ დამალულია, გამოიყენეთ 'Alt', რათა გამოაჩინოთ ის (ან 'Escape' თუ იყენებთ აპლიკაციის შიგნითა მენიუს)"
|
"detail": "მენიუ დამალულია, გამოიყენეთ 'Alt', რათა გამოაჩინოთ ის (ან 'Escape' თუ იყენებთ აპლიკაციის შიგნითა მენიუს)",
|
||||||
|
"message": "მენიუს დამალვა ჩართულია",
|
||||||
|
"title": "მენიუს დამალვა ჩართულია"
|
||||||
},
|
},
|
||||||
"need-to-restart": {
|
"need-to-restart": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"later": "მოგვიანებით"
|
"later": "მოგვიანებით",
|
||||||
}
|
"restart-now": "გადატვირთვა ახლავე"
|
||||||
|
},
|
||||||
|
"detail": "„{{pluginName}}“ დანამატის ძალაში შესასვლელად გადატვირთვა საჭიროა",
|
||||||
|
"message": "\"{{pluginName}}\" საჭიროებს გადატვირთვას",
|
||||||
|
"title": "საჭიროებს გადატვირთვას"
|
||||||
},
|
},
|
||||||
"unresponsive": {
|
"unresponsive": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"quit": "გასვლა",
|
"quit": "გასვლა",
|
||||||
"relaunch": "თავიდან გაშვება",
|
"relaunch": "თავიდან გაშვება",
|
||||||
"wait": "მოცდა"
|
"wait": "მოცდა"
|
||||||
}
|
},
|
||||||
|
"detail": "ბოდიშს გიხდით მოუხერხელობისათვის! გთხოვთ აირჩიეთ რა უნდა გაკეთდეს:",
|
||||||
|
"message": "აპლიკაცია არ პასუხობს",
|
||||||
|
"title": "ფანჯარა არ პასუხობს"
|
||||||
},
|
},
|
||||||
"update-available": {
|
"update-available": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
|
"disable": "განახლებების გამორთვა",
|
||||||
"download": "გადმოწერა",
|
"download": "გადმოწერა",
|
||||||
"ok": "დიახ"
|
"ok": "დიახ"
|
||||||
}
|
},
|
||||||
|
"detail": "ახალი ვერსიაა ხელმისაწვდომი, მისი ჩამოტვირთვა შესაძლებელია {{downloadLink}}-დან",
|
||||||
|
"message": "ახალი ვერსია ხელმისაწვდომია",
|
||||||
|
"title": "განახლება ხელმისაწვდომია"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -70,19 +83,63 @@
|
|||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "ნავიგაცია",
|
"label": "ნავიგაცია",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"quit": "გასვლა"
|
"copy-current-url": "მიმდინარე URL-ის დაკოპირება",
|
||||||
|
"go-back": "უკან დაბრუნება",
|
||||||
|
"go-forward": "წინ გადასვლა",
|
||||||
|
"quit": "გასვლა",
|
||||||
|
"restart": "აპლიკაციის გადატვირთვა"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"label": "მორგება",
|
"label": "მორგება",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"language": {
|
"advanced-options": {
|
||||||
"label": "ენა"
|
"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] (ან თუ აპლიკაციის მენიუს იყენებთ, უკან დააწკაპუნეთ [`])",
|
||||||
|
"title": "მენიუს დამალვა ჩართულია"
|
||||||
|
},
|
||||||
|
"label": "მენიუს დამალვა"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"dialog": {
|
||||||
|
"message": "გადატვირთვის შემდეგ ენა შეიცვლება",
|
||||||
|
"title": "ენა შეიცვალა"
|
||||||
|
},
|
||||||
|
"label": "ენა",
|
||||||
|
"submenu": {
|
||||||
|
"to-help-translate": "გსურთ დაგვეხმაროთ თარგმნაში? დააწკაპუნეთ აქ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resume-on-start": "აპლიკაციის თავიდან გაშვებისას ბოლო სიმღერა დაუკრას",
|
||||||
|
"single-instance-lock": "ერთჯერადი ინსტანციის საკეტი",
|
||||||
|
"start-at-login": "შესვლაზე დაწყება",
|
||||||
"starting-page": {
|
"starting-page": {
|
||||||
|
"label": "საწყისი გვერდი",
|
||||||
"unset": "მოხსნა"
|
"unset": "მოხსნა"
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
|
"label": "უჯრა",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"disabled": "გამორთულია"
|
"disabled": "გამორთულია"
|
||||||
}
|
}
|
||||||
@ -162,11 +219,15 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{size}}%"
|
"percent": "{{size}}%"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"use-fullscreen": {
|
||||||
|
"label": "სრული ეკრანის გამოყენება"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"name": "გარემოს რეჟიმი"
|
||||||
},
|
},
|
||||||
"amuse": {
|
"amuse": {
|
||||||
"name": "Amuse"
|
"name": "გაკვირვება"
|
||||||
},
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@ -267,6 +328,13 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"disconnected": "გათიშული"
|
"disconnected": "გათიშული"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"closed": "Music Together-ის ორგანიზატორი დაიხურა",
|
||||||
|
"disconnected": "Music Together-ის კავშირი გათიშულია",
|
||||||
|
"host-failed": "Music Together-ის გამოცხადება ვერ მოხერხდა",
|
||||||
|
"id-copied": "გამოსაცხადებელი ID დაკოპირებულია ბუფერში",
|
||||||
|
"id-copy-failed": "გამოსაცხადებელი ID-ის ვერ დაკოპირდა ბუფერში"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
@ -309,6 +377,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"video-toggle": {
|
||||||
|
"menu": {
|
||||||
|
"mode": {
|
||||||
|
"label": "რეჟიმი",
|
||||||
|
"submenu": {
|
||||||
|
"custom": "მორგებული გადამრთველი",
|
||||||
|
"disabled": "გამორთულია",
|
||||||
|
"native": "ადგილობრივი გადართვა"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "ვიდეოს გადართვა",
|
||||||
|
"templates": {
|
||||||
|
"button-song": "სიმღერა",
|
||||||
|
"button-video": "ვიდეო"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"description": "პლეიერს ვიზუალიზატორს უმატებს",
|
||||||
|
"menu": {
|
||||||
|
"visualizer-type": "ვიზუალიზატორის ტიპი"
|
||||||
|
},
|
||||||
|
"name": "ვიზუალიზატორი"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/i18n/resources/kmr.json
Normal file
1
src/i18n/resources/kmr.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -150,6 +150,13 @@
|
|||||||
"visual-tweaks": {
|
"visual-tweaks": {
|
||||||
"label": "भिजुअल ट्वीक्स",
|
"label": "भिजुअल ट्वीक्स",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
|
"custom-window-title": {
|
||||||
|
"label": "अनुकूलन विन्डो शीर्षक",
|
||||||
|
"prompt": {
|
||||||
|
"label": "अनुकूलन विन्डो शीर्षक प्रविष्ट गर्नुहोस्: (असक्षम पार्न खाली छोड्नुहोस्)",
|
||||||
|
"placeholder": "उदाहरण: पियर डेस्कटप"
|
||||||
|
}
|
||||||
|
},
|
||||||
"like-buttons": {
|
"like-buttons": {
|
||||||
"default": "पूर्वनिर्धारित",
|
"default": "पूर्वनिर्धारित",
|
||||||
"force-show": "देखाउनुहोस",
|
"force-show": "देखाउनुहोस",
|
||||||
@ -179,7 +186,7 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"enabled": "सक्षम गरियो",
|
"enabled": "सक्षम गरियो",
|
||||||
"label": "प्लगइनहरू",
|
"label": "प्लगइनहरू",
|
||||||
"new": "NEW"
|
"new": "नयाँ"
|
||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"label": "हेर्नुहोस्",
|
"label": "हेर्नुहोस्",
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Ativar personalização da barra de progresso"
|
||||||
},
|
},
|
||||||
"name": "Tema da cor do álbum"
|
"name": "Tema da cor do álbum"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Ativar temas na barra de reprodução"
|
||||||
},
|
},
|
||||||
"name": "Tema de cores do álbum"
|
"name": "Tema de cores do álbum"
|
||||||
},
|
},
|
||||||
@ -462,8 +463,8 @@
|
|||||||
"label": "Texto de estado",
|
"label": "Texto de estado",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "A ouvir {artist}",
|
"artist": "A ouvir {artist}",
|
||||||
"title": "A ouvir {song title}",
|
"pear-desktop": "A reproduzir Pear Desktop",
|
||||||
"pear-desktop": "A reproduzir Pear Desktop"
|
"title": "A ouvir {song title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
15
src/i18n/resources/qu.json
Normal file
15
src/i18n/resources/qu.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"console": {
|
||||||
|
"plugins": {
|
||||||
|
"execute-failed": "Pluginta mana ruwayta atirqanchu {{pluginName}}::{{contextName}}",
|
||||||
|
"executed-at-ms": "Plugin nisqa {{pluginName}}::{{contextName}} ejecutado en {{ms}}ms",
|
||||||
|
"initialize-failed": "Plugin qallariyta mana atirqanchu \"{{pluginName}}\"",
|
||||||
|
"load-all": "Llapanta cargaspa",
|
||||||
|
"load-failed": "Pluginta mana kargayta atirqanchu \"{{pluginName}}\"",
|
||||||
|
"loaded": "Plugin nisqa \"{{pluginName}}\" cargado",
|
||||||
|
"unload-failed": "Pluginta mana uraykachiyta atirqanchu \"{{pluginName}}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Povoliť farebnú tému aj v seekbare"
|
||||||
},
|
},
|
||||||
"name": "Farebná téma albumu"
|
"name": "Farebná téma albumu"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,56 @@
|
|||||||
"console": {
|
"console": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"execute-failed": "Dështoi në ekzekutimin e plugin-it {{pluginName}}::{{contextName}}",
|
"execute-failed": "Dështoi në ekzekutimin e plugin-it {{pluginName}}::{{contextName}}",
|
||||||
"executed-at-ms": "Shtojca {{pluginName}}::{{contextName}} u ekzekutua në {{ms}}ms"
|
"executed-at-ms": "Shtojca {{pluginName}}::{{contextName}} u ekzekutua në {{ms}}ms",
|
||||||
|
"initialize-failed": "Dështoi ekzekutimi i plugin-it \"{{pluginName}}\"",
|
||||||
|
"load-all": "Duke ngarkuar të gjithe plugins",
|
||||||
|
"load-failed": "Dështoi në ngarkimin e plugin-it \"{{pluginName}}\"",
|
||||||
|
"loaded": "Plugin \"{{pluginName}}\" u ngarkua",
|
||||||
|
"unload-failed": "Shkarkimi i plugin-it \"{{pluginName}}\" dështoi",
|
||||||
|
"unloaded": "Plugin \"{{pluginName}}\" u shkarkua"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "Emri i gjuhës në anglisht. p.sh. japonisht, koreanisht, anglisht, rusisht"
|
||||||
|
},
|
||||||
|
"main": {
|
||||||
|
"console": {
|
||||||
|
"did-finish-load": {
|
||||||
|
"dev-tools": "Ngarkimi përfundoi. DevTools u hap."
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"loaded": "i18n i ngarkuar"
|
||||||
|
},
|
||||||
|
"second-instance": {
|
||||||
|
"receive-command": "U mor komanda mbi protokollin: \"{{command}}\""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"css-file-not-found": "Skedari CSS \"{{cssFile}}\" nuk ekziston, duke u injoruar"
|
||||||
|
},
|
||||||
|
"unresponsive": {
|
||||||
|
"details": "Gabim i Papërgjigjes!\n{{error}}"
|
||||||
|
},
|
||||||
|
"when-ready": {
|
||||||
|
"clearing-cache-after-20s": "Duke pastruar memorien e përkohshme të aplikacionit"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"tried-to-render-offscreen": "Dritarja u përpoq të renderohej jashtë ekranit, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"hide-menu-enabled": {
|
||||||
|
"detail": "Menuja është e fshehur, përdorni 'Alt' për ta shfaqur (ose 'Escape' nëse përdorni Menunë brenda aplikacionit)",
|
||||||
|
"message": "Fshehja e menusë është aktivizuar",
|
||||||
|
"title": "Fshih Menunë Aktivizuar"
|
||||||
|
},
|
||||||
|
"need-to-restart": {
|
||||||
|
"buttons": {
|
||||||
|
"later": "Më vonë",
|
||||||
|
"restart-now": "Rinisni Tani"
|
||||||
|
},
|
||||||
|
"detail": "\"{{pluginName}}\" kërkon një restart që të hyjë në fuqi",
|
||||||
|
"message": "\"{{pluginName}}\" kerkon restart"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,7 +86,7 @@
|
|||||||
"copy-current-url": "Kopiera nuvarande länk",
|
"copy-current-url": "Kopiera nuvarande länk",
|
||||||
"go-back": "Gå tillbaka",
|
"go-back": "Gå tillbaka",
|
||||||
"go-forward": "Gå framåt",
|
"go-forward": "Gå framåt",
|
||||||
"quit": "Avsluta",
|
"quit": "Stäng",
|
||||||
"restart": "Starta om appen"
|
"restart": "Starta om appen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}} %"
|
"percent": "{{ratio}} %"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Aktivera temaanpassning av uppspelningsreglaget"
|
||||||
},
|
},
|
||||||
"name": "Albumfärgtema"
|
"name": "Albumfärgtema"
|
||||||
},
|
},
|
||||||
@ -462,8 +463,8 @@
|
|||||||
"label": "Statusmeddelande",
|
"label": "Statusmeddelande",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Lyssnar på {artist}",
|
"artist": "Lyssnar på {artist}",
|
||||||
"title": "Lyssnar på {song title}",
|
"pear-desktop": "Lyssnar på Pear Desktop",
|
||||||
"pear-desktop": "Lyssnar på Pear Desktop"
|
"title": "Lyssnar på {song title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "Arama çubuğu temalarını etkinleştir"
|
||||||
},
|
},
|
||||||
"name": "Albüm Renk Teması"
|
"name": "Albüm Renk Teması"
|
||||||
},
|
},
|
||||||
@ -462,8 +463,8 @@
|
|||||||
"label": "Durum metni",
|
"label": "Durum metni",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "{artist} Dinleniyor",
|
"artist": "{artist} Dinleniyor",
|
||||||
"title": "{song title} Dinleniyor",
|
"pear-desktop": "Pear Desktop Dinleniyor",
|
||||||
"pear-desktop": "Pear Desktop Dinleniyor"
|
"title": "{song title} Dinleniyor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"enable-seekbar": "啟用進度條主題樣式"
|
||||||
},
|
},
|
||||||
"name": "隨歌曲色調變更主題"
|
"name": "隨歌曲色調變更主題"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
import { createServer as createHttpServer } from 'node:http';
|
||||||
|
import { createServer as createHttpsServer } from 'node:https';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
import { jwt } from 'hono/jwt';
|
import { jwt } from 'hono/jwt';
|
||||||
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
@ -48,22 +52,26 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.run(config.hostname, config.port);
|
this.run(config);
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
this.end();
|
this.end();
|
||||||
},
|
},
|
||||||
onConfigChange(config) {
|
onConfigChange(config) {
|
||||||
|
const old = this.oldConfig;
|
||||||
if (
|
if (
|
||||||
this.oldConfig?.hostname === config.hostname &&
|
old?.hostname === config.hostname &&
|
||||||
this.oldConfig?.port === config.port
|
old?.port === config.port &&
|
||||||
|
old?.useHttps === config.useHttps &&
|
||||||
|
old?.certPath === config.certPath &&
|
||||||
|
old?.keyPath === config.keyPath
|
||||||
) {
|
) {
|
||||||
this.oldConfig = config;
|
this.oldConfig = config;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.end();
|
this.end();
|
||||||
this.run(config.hostname, config.port);
|
this.run(config);
|
||||||
this.oldConfig = config;
|
this.oldConfig = config;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -153,15 +161,30 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
|
|
||||||
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
||||||
},
|
},
|
||||||
run(hostname, port) {
|
run(config) {
|
||||||
if (!this.app) return;
|
if (!this.app) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.server = serve({
|
const serveOptions =
|
||||||
fetch: this.app.fetch.bind(this.app),
|
config.useHttps && config.certPath && config.keyPath
|
||||||
port,
|
? {
|
||||||
hostname,
|
fetch: this.app.fetch.bind(this.app),
|
||||||
});
|
port: config.port,
|
||||||
|
hostname: config.hostname,
|
||||||
|
createServer: createHttpsServer,
|
||||||
|
serverOptions: {
|
||||||
|
key: readFileSync(config.keyPath),
|
||||||
|
cert: readFileSync(config.certPath),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fetch: this.app.fetch.bind(this.app),
|
||||||
|
port: config.port,
|
||||||
|
hostname: config.hostname,
|
||||||
|
createServer: createHttpServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.server = serve(serveOptions);
|
||||||
|
|
||||||
if (this.injectWebSocket && this.server) {
|
if (this.injectWebSocket && this.server) {
|
||||||
this.injectWebSocket(this.server);
|
this.injectWebSocket(this.server);
|
||||||
|
|||||||
@ -411,6 +411,26 @@ const routes = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
nextSongInfo: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/queue/next`,
|
||||||
|
summary: 'get next song info',
|
||||||
|
description:
|
||||||
|
'Get information about the next song in the queue (relative index +1)',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SongInfoSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
204: {
|
||||||
|
description: 'No next song in queue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
queueInfo: createRoute({
|
queueInfo: createRoute({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: `/api/${API_VERSION}/queue`,
|
path: `/api/${API_VERSION}/queue`,
|
||||||
@ -748,6 +768,63 @@ export const register = (
|
|||||||
app.openapi(routes.oldQueueInfo, queueInfo);
|
app.openapi(routes.oldQueueInfo, queueInfo);
|
||||||
app.openapi(routes.queueInfo, queueInfo);
|
app.openapi(routes.queueInfo, queueInfo);
|
||||||
|
|
||||||
|
app.openapi(routes.nextSongInfo, async (ctx) => {
|
||||||
|
const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
|
||||||
|
ipcMain.once('peard:get-queue-response', (_, queue: QueueResponse) => {
|
||||||
|
return resolve(queue);
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.requestQueueInformation();
|
||||||
|
});
|
||||||
|
|
||||||
|
const queue = await queueResponsePromise;
|
||||||
|
|
||||||
|
if (!queue?.items || queue.items.length === 0) {
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the currently selected song
|
||||||
|
const currentIndex = queue.items.findIndex((item) => {
|
||||||
|
const renderer =
|
||||||
|
item.playlistPanelVideoRenderer ||
|
||||||
|
item.playlistPanelVideoWrapperRenderer?.primaryRenderer
|
||||||
|
?.playlistPanelVideoRenderer;
|
||||||
|
return renderer?.selected === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the next song (currentIndex + 1)
|
||||||
|
const nextIndex = currentIndex + 1;
|
||||||
|
if (nextIndex >= queue.items.length) {
|
||||||
|
// No next song available
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItem = queue.items[nextIndex];
|
||||||
|
const nextRenderer =
|
||||||
|
nextItem.playlistPanelVideoRenderer ||
|
||||||
|
nextItem.playlistPanelVideoWrapperRenderer?.primaryRenderer
|
||||||
|
?.playlistPanelVideoRenderer;
|
||||||
|
|
||||||
|
if (!nextRenderer) {
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract relevant information similar to SongInfo format
|
||||||
|
const nextSongInfo = {
|
||||||
|
title: nextRenderer.title?.runs?.[0]?.text,
|
||||||
|
videoId: nextRenderer.videoId,
|
||||||
|
thumbnail: nextRenderer.thumbnail,
|
||||||
|
lengthText: nextRenderer.lengthText,
|
||||||
|
shortBylineText: nextRenderer.shortBylineText,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json(nextSongInfo);
|
||||||
|
});
|
||||||
|
|
||||||
app.openapi(routes.addSongToQueue, (ctx) => {
|
app.openapi(routes.addSongToQueue, (ctx) => {
|
||||||
const { videoId, insertPosition } = ctx.req.valid('json');
|
const { videoId, insertPosition } = ctx.req.valid('json');
|
||||||
controller.addSongToQueue(videoId, insertPosition);
|
controller.addSongToQueue(videoId, insertPosition);
|
||||||
|
|||||||
@ -17,6 +17,6 @@ export type BackendType = {
|
|||||||
injectWebSocket?: (server: ReturnType<typeof serve>) => void;
|
injectWebSocket?: (server: ReturnType<typeof serve>) => void;
|
||||||
|
|
||||||
init: (ctx: BackendContext<APIServerConfig>) => void;
|
init: (ctx: BackendContext<APIServerConfig>) => void;
|
||||||
run: (hostname: string, port: number) => void;
|
run: (config: APIServerConfig) => void;
|
||||||
end: () => void;
|
end: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,9 @@ export interface APIServerConfig {
|
|||||||
secret: string;
|
secret: string;
|
||||||
|
|
||||||
authorizedClients: string[];
|
authorizedClients: string[];
|
||||||
|
useHttps: boolean;
|
||||||
|
certPath: string;
|
||||||
|
keyPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAPIServerConfig: APIServerConfig = {
|
export const defaultAPIServerConfig: APIServerConfig = {
|
||||||
@ -21,4 +24,7 @@ export const defaultAPIServerConfig: APIServerConfig = {
|
|||||||
secret: Date.now().toString(36),
|
secret: Date.now().toString(36),
|
||||||
|
|
||||||
authorizedClients: [],
|
authorizedClients: [],
|
||||||
|
useHttps: false,
|
||||||
|
certPath: '',
|
||||||
|
keyPath: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { dialog } from 'electron';
|
||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
@ -93,5 +94,51 @@ export const onMenu = async ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.https.label'),
|
||||||
|
type: 'submenu',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.https.submenu.enable-https.label'),
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: config.useHttps,
|
||||||
|
click(menuItem) {
|
||||||
|
setConfig({ ...config, useHttps: menuItem.checked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.https.submenu.cert.label'),
|
||||||
|
type: 'normal',
|
||||||
|
async click() {
|
||||||
|
const config = await getConfig();
|
||||||
|
const result = await dialog.showOpenDialog(window, {
|
||||||
|
title: t(
|
||||||
|
'plugins.api-server.menu.https.submenu.cert.dialogTitle',
|
||||||
|
),
|
||||||
|
filters: [{ name: 'Certificate', extensions: ['crt', 'pem'] }],
|
||||||
|
properties: ['openFile'],
|
||||||
|
});
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
setConfig({ ...config, certPath: result.filePaths[0] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.https.submenu.key.label'),
|
||||||
|
type: 'normal',
|
||||||
|
async click() {
|
||||||
|
const config = await getConfig();
|
||||||
|
const result = await dialog.showOpenDialog(window, {
|
||||||
|
title: t('plugins.api-server.menu.https.submenu.key.dialogTitle'),
|
||||||
|
filters: [{ name: 'Private Key', extensions: ['key', 'pem'] }],
|
||||||
|
properties: ['openFile'],
|
||||||
|
});
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
setConfig({ ...config, keyPath: result.filePaths[0] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import delay from 'delay';
|
|||||||
import type { Permission, Profile, VideoData } from './types';
|
import type { Permission, Profile, VideoData } from './types';
|
||||||
|
|
||||||
export type ConnectionEventMap = {
|
export type ConnectionEventMap = {
|
||||||
|
CLEAR_QUEUE: {};
|
||||||
ADD_SONGS: { videoList: VideoData[]; index?: number };
|
ADD_SONGS: { videoList: VideoData[]; index?: number };
|
||||||
REMOVE_SONG: { index: number };
|
REMOVE_SONG: { index: number };
|
||||||
MOVE_SONG: { fromIndex: number; toIndex: number };
|
MOVE_SONG: { fromIndex: number; toIndex: number };
|
||||||
|
SET_INDEX: { index: number };
|
||||||
IDENTIFY: { profile: Profile } | undefined;
|
IDENTIFY: { profile: Profile } | undefined;
|
||||||
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
|
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
|
||||||
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
||||||
@ -171,9 +173,10 @@ export class Connection {
|
|||||||
public async broadcast<Event extends keyof ConnectionEventMap>(
|
public async broadcast<Event extends keyof ConnectionEventMap>(
|
||||||
type: Event,
|
type: Event,
|
||||||
payload: ConnectionEventMap[Event],
|
payload: ConnectionEventMap[Event],
|
||||||
|
after?: ConnectionEventUnion[],
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.getConnections().map((conn) => conn.send({ type, payload })),
|
this.getConnections().map((conn) => conn.send({ type, payload, after })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -215,6 +215,25 @@ export default createPlugin<
|
|||||||
this.ignoreChange = true;
|
this.ignoreChange = true;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case 'CLEAR_QUEUE': {
|
||||||
|
if (conn && this.permission === 'host-only') {
|
||||||
|
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||||
|
videoList: this.queue?.videoList ?? [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queue?.clear();
|
||||||
|
await this.connection?.broadcast('CLEAR_QUEUE', {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SET_INDEX': {
|
||||||
|
this.queue?.setIndex(event.payload.index);
|
||||||
|
await this.connection?.broadcast('SET_INDEX', {
|
||||||
|
index: event.payload.index,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'ADD_SONGS': {
|
case 'ADD_SONGS': {
|
||||||
if (conn && this.permission === 'host-only') {
|
if (conn && this.permission === 'host-only') {
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||||
@ -234,7 +253,15 @@ export default createPlugin<
|
|||||||
await this.connection?.broadcast('ADD_SONGS', {
|
await this.connection?.broadcast('ADD_SONGS', {
|
||||||
...event.payload,
|
...event.payload,
|
||||||
videoList,
|
videoList,
|
||||||
});
|
},
|
||||||
|
event.after,
|
||||||
|
);
|
||||||
|
|
||||||
|
const afterevent = event.after?.at(0);
|
||||||
|
if (afterevent?.type === 'SET_INDEX') {
|
||||||
|
this.queue?.setIndex(afterevent.payload.index);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'REMOVE_SONG': {
|
case 'REMOVE_SONG': {
|
||||||
@ -385,6 +412,16 @@ export default createPlugin<
|
|||||||
const queueListener = async (event: ConnectionEventUnion) => {
|
const queueListener = async (event: ConnectionEventUnion) => {
|
||||||
this.ignoreChange = true;
|
this.ignoreChange = true;
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case 'CLEAR_QUEUE': {
|
||||||
|
await this.connection?.broadcast('CLEAR_QUEUE', {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SET_INDEX': {
|
||||||
|
await this.connection?.broadcast('SET_INDEX', {
|
||||||
|
index: event.payload.index,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'ADD_SONGS': {
|
case 'ADD_SONGS': {
|
||||||
await this.connection?.broadcast('ADD_SONGS', {
|
await this.connection?.broadcast('ADD_SONGS', {
|
||||||
...event.payload,
|
...event.payload,
|
||||||
@ -392,7 +429,9 @@ export default createPlugin<
|
|||||||
...it,
|
...it,
|
||||||
ownerId: it.ownerId ?? this.connection!.id,
|
ownerId: it.ownerId ?? this.connection!.id,
|
||||||
})),
|
})),
|
||||||
});
|
},
|
||||||
|
event.after,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'REMOVE_SONG': {
|
case 'REMOVE_SONG': {
|
||||||
@ -420,6 +459,14 @@ export default createPlugin<
|
|||||||
const listener = async (event: ConnectionEventUnion) => {
|
const listener = async (event: ConnectionEventUnion) => {
|
||||||
this.ignoreChange = true;
|
this.ignoreChange = true;
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case 'CLEAR_QUEUE': {
|
||||||
|
this.queue?.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SET_INDEX': {
|
||||||
|
this.queue?.setIndex(event.payload.index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'ADD_SONGS': {
|
case 'ADD_SONGS': {
|
||||||
const videoList: VideoData[] = event.payload.videoList.map(
|
const videoList: VideoData[] = event.payload.videoList.map(
|
||||||
(it) => ({
|
(it) => ({
|
||||||
@ -429,6 +476,13 @@ export default createPlugin<
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.queue?.addVideos(videoList, event.payload.index);
|
await this.queue?.addVideos(videoList, event.payload.index);
|
||||||
|
|
||||||
|
const afterevent = event.after?.at(0);
|
||||||
|
if (afterevent?.type === 'SET_INDEX') {
|
||||||
|
this.queue?.setIndex(afterevent.payload.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'REMOVE_SONG': {
|
case 'REMOVE_SONG': {
|
||||||
|
|||||||
@ -314,6 +314,11 @@ export class Queue {
|
|||||||
if (!this.internalDispatch) {
|
if (!this.internalDispatch) {
|
||||||
if (event.type === 'CLEAR') {
|
if (event.type === 'CLEAR') {
|
||||||
this.ignoreFlag = true;
|
this.ignoreFlag = true;
|
||||||
|
this.broadcast({
|
||||||
|
type: 'CLEAR_QUEUE',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (event.type === 'ADD_ITEMS') {
|
if (event.type === 'ADD_ITEMS') {
|
||||||
if (this.ignoreFlag) {
|
if (this.ignoreFlag) {
|
||||||
@ -347,7 +352,7 @@ export class Queue {
|
|||||||
},
|
},
|
||||||
after: [
|
after: [
|
||||||
{
|
{
|
||||||
type: 'SYNC_PROGRESS',
|
type: 'SET_INDEX',
|
||||||
payload: {
|
payload: {
|
||||||
index,
|
index,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -77,11 +77,10 @@ export class LRCLib implements LyricProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredResults = [];
|
const filteredResults = [];
|
||||||
const artists = artist.split(/[&,]/g).map((i) => i.trim());
|
|
||||||
|
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
const { artistName } = item;
|
const { artistName } = item;
|
||||||
|
|
||||||
|
const artists = artist.split(/[&,]/g).map((i) => i.trim());
|
||||||
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
|
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
|
||||||
|
|
||||||
// Try to match using artist name first
|
// Try to match using artist name first
|
||||||
|
|||||||
@ -1,340 +0,0 @@
|
|||||||
// Code adapted from https://greasyfork.org/en/scripts/548724-youtube-music-spotify-%E7%BD%91%E6%98%93%E4%BA%91%E6%AD%8C%E8%AF%8D%E6%98%BE%E7%A4%BA
|
|
||||||
// which is licenced under the MIT licence
|
|
||||||
|
|
||||||
import CryptoJS from 'crypto-js';
|
|
||||||
import { jaroWinkler } from '@skyra/jaro-winkler';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { LRC } from '../parsers/lrc';
|
|
||||||
|
|
||||||
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
|
|
||||||
|
|
||||||
const EAPI_AES_KEY = 'e82ckenh8dichen8';
|
|
||||||
const EAPI_ENCODE_KEY = '3go8&$8*3*3h0k(2)2';
|
|
||||||
const EAPI_CHECK_TOKEN =
|
|
||||||
'9ca17ae2e6ffcda170e2e6ee8ad85dba908ca4d74da9ac8ea2d44e938f9eadc66da5a8979af572a5a9b68ac12af0feaec3b92aa69af9b1d372f6b8adccb35e968b9bb6c14f908d0099fb6ff48efdacd361f5b6ee9e';
|
|
||||||
const EAPI_BASE_HEADERS = {
|
|
||||||
'User-Agent':
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) NeteaseMusicDesktop/3.0.14.2534',
|
|
||||||
};
|
|
||||||
const EAPI_BASE_COOKIES = {
|
|
||||||
os: 'osx',
|
|
||||||
appver: '3.0.14',
|
|
||||||
requestId: 0,
|
|
||||||
osver: '15.6.1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const artistSchema = z.object({ id: z.number(), name: z.string() });
|
|
||||||
const songSchema = z.object({
|
|
||||||
resourceId: z.coerce.number(),
|
|
||||||
baseInfo: z.object({
|
|
||||||
simpleSongData: z.object({
|
|
||||||
name: z.string(),
|
|
||||||
ar: z.array(artistSchema).optional(),
|
|
||||||
dt: z.number(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const searchResponseDataSchema = z.object({
|
|
||||||
resources: z.array(songSchema).default([]),
|
|
||||||
});
|
|
||||||
const searchResponseSchema = z.object({
|
|
||||||
code: z.number(),
|
|
||||||
message: z.string(),
|
|
||||||
data: searchResponseDataSchema,
|
|
||||||
});
|
|
||||||
type Song = z.infer<typeof songSchema>;
|
|
||||||
|
|
||||||
const lyricPartSchema = z.object({ lyric: z.string().nullable() });
|
|
||||||
const lyricResponseSchema = z.object({
|
|
||||||
lrc: lyricPartSchema.optional(),
|
|
||||||
tlyric: lyricPartSchema.optional(),
|
|
||||||
romalrc: lyricPartSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export class Netease implements LyricProvider {
|
|
||||||
name = 'Netease';
|
|
||||||
baseUrl = 'https://interface.music.163.com';
|
|
||||||
cookies: Record<string, string> = {};
|
|
||||||
initialized = false;
|
|
||||||
|
|
||||||
private encode(id: string): string {
|
|
||||||
// XOR step (unchanged)
|
|
||||||
let xoredString = '';
|
|
||||||
for (let i = 0; i < id.length; i++) {
|
|
||||||
const charCode =
|
|
||||||
id.charCodeAt(i) ^
|
|
||||||
EAPI_ENCODE_KEY.charCodeAt(i % EAPI_ENCODE_KEY.length);
|
|
||||||
xoredString += String.fromCharCode(charCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MD5 -> Base64 using crypto-js
|
|
||||||
const hash = CryptoJS.MD5(CryptoJS.enc.Latin1.parse(xoredString)).toString(
|
|
||||||
CryptoJS.enc.Base64,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build a binary WordArray for "id hash"
|
|
||||||
const combinedWordArray = CryptoJS.enc.Latin1.parse(id + ' ' + hash);
|
|
||||||
|
|
||||||
// Convert to Base64 (replaces Buffer.from(...).toString("base64"))
|
|
||||||
return CryptoJS.enc.Base64.stringify(combinedWordArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async register() {
|
|
||||||
const deviceId = '7B79802670C7A45DB9091976D71E0AE829E28926C6C34A1B8644';
|
|
||||||
const username = this.encode(deviceId);
|
|
||||||
try {
|
|
||||||
await this.eapi('/register/anonimous', { username }, { _nmclfl: '1' });
|
|
||||||
this.initialized = true;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Registration failed: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async eapi(
|
|
||||||
path: string,
|
|
||||||
data: Record<string, unknown> = {},
|
|
||||||
params: Record<string, string> = {},
|
|
||||||
) {
|
|
||||||
const header = { ...EAPI_BASE_COOKIES };
|
|
||||||
const bodyData = { ...data, header: JSON.stringify(header) };
|
|
||||||
const body = JSON.stringify(bodyData);
|
|
||||||
const sign = CryptoJS.MD5(
|
|
||||||
`nobody/api${path}use${body}md5forencrypt`,
|
|
||||||
).toString();
|
|
||||||
const payload = `/api${path}-36cd479b6b5-${body}-36cd479b6b5-${sign}`;
|
|
||||||
|
|
||||||
const key = CryptoJS.enc.Utf8.parse(EAPI_AES_KEY);
|
|
||||||
|
|
||||||
const encrypted = CryptoJS.AES.encrypt(payload, key, {
|
|
||||||
mode: CryptoJS.mode.ECB,
|
|
||||||
padding: CryptoJS.pad.Pkcs7,
|
|
||||||
}).ciphertext.toString(CryptoJS.enc.Hex);
|
|
||||||
|
|
||||||
const cookieString = Object.entries({ ...this.cookies })
|
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
|
||||||
.join('; ');
|
|
||||||
|
|
||||||
const queryStr = new URLSearchParams(params).toString();
|
|
||||||
const url = `${this.baseUrl}/eapi${path}${queryStr ? `?${queryStr}` : ''}`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
...EAPI_BASE_HEADERS,
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Cookie': cookieString,
|
|
||||||
},
|
|
||||||
body: `params=${encodeURIComponent(encrypted.toUpperCase())}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setCookieHeader = response.headers.get('set-cookie');
|
|
||||||
if (setCookieHeader) {
|
|
||||||
const cookieStrings = setCookieHeader.split(/,(?=\s*[^=;\s]+=)/);
|
|
||||||
for (const cookieStr of cookieStrings) {
|
|
||||||
const parts = cookieStr.split(';')[0].split('=');
|
|
||||||
if (parts.length === 2) {
|
|
||||||
this.cookies[parts[0].trim()] = parts[1].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`bad HTTPStatus(${response.statusText})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await response.json();
|
|
||||||
z.object({ code: z.literal(200) }).parse(json);
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async searchSongs(keyword: string, limit = 10): Promise<Song[]> {
|
|
||||||
const response = await this.eapi(
|
|
||||||
'/search/song/list/page',
|
|
||||||
{
|
|
||||||
offset: '0',
|
|
||||||
scene: 'NORMAL',
|
|
||||||
needCorrect: 'true',
|
|
||||||
checkToken: EAPI_CHECK_TOKEN,
|
|
||||||
keyword,
|
|
||||||
limit: limit.toString(),
|
|
||||||
verifyId: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_nmclfl: '1',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const parsed = searchResponseSchema.parse(response);
|
|
||||||
return parsed.data?.resources || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getLyric(id: number) {
|
|
||||||
const response = await this.eapi(
|
|
||||||
'/song/lyric/v1',
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
tv: '-1',
|
|
||||||
yv: '-1',
|
|
||||||
rv: '-1',
|
|
||||||
lv: '-1',
|
|
||||||
verifyId: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_nmclfl: '1',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return lyricResponseSchema.parse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private splitTitle(title: string): string[] {
|
|
||||||
const masterPattern =
|
|
||||||
/(?:[「『](?<content>.+?)[」』])|(?:【.*?】|〖.*?〗|\(.*?\)|(.*?))|(?<delimiter>\s+-\s+|\s*[//|:|│]\s*)/i;
|
|
||||||
const noiseWords = /\b(MV|PV)\b|\b(?:covered by|feat?|ft?)\b.+/gi;
|
|
||||||
|
|
||||||
const parse = (str: string): string[] => {
|
|
||||||
if (!str?.trim()) return [];
|
|
||||||
|
|
||||||
const match = str.match(masterPattern);
|
|
||||||
if (!match || match.index === undefined) return [str];
|
|
||||||
|
|
||||||
const before = str.substring(0, match.index);
|
|
||||||
const after = str.substring(match.index + match[0].length);
|
|
||||||
const { delimiter, content } = match.groups || {};
|
|
||||||
|
|
||||||
if (delimiter && (before.trim().length < 2 || after.trim().length < 2)) {
|
|
||||||
const remaining = parse(after);
|
|
||||||
return [
|
|
||||||
before + match[0] + (remaining[0] || ''),
|
|
||||||
...remaining.slice(1),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...parse(before), ...(content ? [content] : []), ...parse(after)];
|
|
||||||
};
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
parse(title)
|
|
||||||
.map((p) => p.replace(noiseWords, '').trim())
|
|
||||||
.filter((p) => p.length > 0),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async search({
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
songDuration,
|
|
||||||
}: SearchSongInfo): Promise<LyricResult | null> {
|
|
||||||
if (!this.initialized) {
|
|
||||||
await this.register();
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = this.splitTitle(title);
|
|
||||||
if (parts.length === 0) {
|
|
||||||
parts.push(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keywords = [...parts];
|
|
||||||
if (parts[0] !== artist) keywords.push(`${parts[0]} ${artist}`);
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
keywords.map((kw) => this.searchSongs(kw, 10)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const calcTitleScore = (searchTitle: string) => {
|
|
||||||
let avgScore = 0;
|
|
||||||
parts.forEach((part, idx) => {
|
|
||||||
let weight = 1 / (idx * 2 + 1); // Earlier parts have higher weight
|
|
||||||
if (searchTitle.startsWith(part)) weight *= 2;
|
|
||||||
// Bonus for prefix match
|
|
||||||
else if (searchTitle.includes(part)) weight *= 1.5; // Bonus for substring match
|
|
||||||
avgScore += (jaroWinkler(part, searchTitle) * weight) / parts.length;
|
|
||||||
});
|
|
||||||
const score = Math.max(jaroWinkler(title, searchTitle), avgScore);
|
|
||||||
return score;
|
|
||||||
};
|
|
||||||
|
|
||||||
const artists = artist.split(/[&,]/g).map((i) => i.trim());
|
|
||||||
const filteredResults = [];
|
|
||||||
for (const result of results.flat()) {
|
|
||||||
const {
|
|
||||||
baseInfo: {
|
|
||||||
simpleSongData: { name, ar: itemArtists },
|
|
||||||
},
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
const permutations = [];
|
|
||||||
for (const artistA of artists) {
|
|
||||||
for (const artistB of itemArtists ?? []) {
|
|
||||||
permutations.push([
|
|
||||||
artistA.toLowerCase(),
|
|
||||||
artistB.name.toLowerCase(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const artistA of itemArtists ?? []) {
|
|
||||||
for (const artistB of artists) {
|
|
||||||
permutations.push([
|
|
||||||
artistA.name.toLowerCase(),
|
|
||||||
artistB.toLowerCase(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ratio =
|
|
||||||
calcTitleScore(name) +
|
|
||||||
Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y)));
|
|
||||||
|
|
||||||
if (ratio < 1.8) continue;
|
|
||||||
filteredResults.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closestResult = filteredResults[0];
|
|
||||||
if (!closestResult) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Math.abs(closestResult.baseInfo.simpleSongData.dt / 1000 - songDuration) >
|
|
||||||
15
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lyric = await this.getLyric(closestResult.resourceId);
|
|
||||||
if (!lyric || !lyric.lrc?.lyric) return null;
|
|
||||||
|
|
||||||
const lyrics = stripMetadata(lyric.lrc.lyric);
|
|
||||||
|
|
||||||
const lines = LRC.parse(lyrics).lines.map((l) => ({
|
|
||||||
...l,
|
|
||||||
status: 'upcoming' as const,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (lines.length === 0 && !lyrics.trim()) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: closestResult.baseInfo.simpleSongData.name,
|
|
||||||
artists:
|
|
||||||
closestResult.baseInfo.simpleSongData.ar?.map((a) => a.name) ?? [],
|
|
||||||
lines,
|
|
||||||
lyrics: lyrics,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stripMetadata = (lyrics: string) => {
|
|
||||||
return lyrics
|
|
||||||
.split('\n')
|
|
||||||
.filter((line) => {
|
|
||||||
if (!line.includes('{')) return true;
|
|
||||||
try {
|
|
||||||
JSON.parse(line);
|
|
||||||
return false;
|
|
||||||
} catch {}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
};
|
|
||||||
@ -7,7 +7,6 @@ export enum ProviderNames {
|
|||||||
LRCLib = 'LRCLib',
|
LRCLib = 'LRCLib',
|
||||||
MusixMatch = 'MusixMatch',
|
MusixMatch = 'MusixMatch',
|
||||||
LyricsGenius = 'LyricsGenius',
|
LyricsGenius = 'LyricsGenius',
|
||||||
NetEase = 'NetEase',
|
|
||||||
// Megalobiz = 'Megalobiz',
|
// Megalobiz = 'Megalobiz',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,11 @@ import { YTMusic } from './YTMusic';
|
|||||||
import { LRCLib } from './LRCLib';
|
import { LRCLib } from './LRCLib';
|
||||||
import { MusixMatch } from './MusixMatch';
|
import { MusixMatch } from './MusixMatch';
|
||||||
import { LyricsGenius } from './LyricsGenius';
|
import { LyricsGenius } from './LyricsGenius';
|
||||||
import { Netease } from './NetEase';
|
|
||||||
|
|
||||||
export const providers = {
|
export const providers = {
|
||||||
[ProviderNames.YTMusic]: new YTMusic(),
|
[ProviderNames.YTMusic]: new YTMusic(),
|
||||||
[ProviderNames.LRCLib]: new LRCLib(),
|
[ProviderNames.LRCLib]: new LRCLib(),
|
||||||
[ProviderNames.MusixMatch]: new MusixMatch(),
|
[ProviderNames.MusixMatch]: new MusixMatch(),
|
||||||
[ProviderNames.LyricsGenius]: new LyricsGenius(),
|
[ProviderNames.LyricsGenius]: new LyricsGenius(),
|
||||||
[ProviderNames.NetEase]: new Netease(),
|
|
||||||
// [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow
|
// [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -16,4 +16,5 @@ export const startingPages: Record<string, string> = {
|
|||||||
'Uploaded Songs': 'FEmusic_library_privately_owned_tracks',
|
'Uploaded Songs': 'FEmusic_library_privately_owned_tracks',
|
||||||
'Uploaded Albums': 'FEmusic_library_privately_owned_releases',
|
'Uploaded Albums': 'FEmusic_library_privately_owned_releases',
|
||||||
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
||||||
|
'Mixed for you': 'FEmusic_mixed_for_you',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user