mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Compare commits
2 Commits
62ef1734e4
...
synced-lyr
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e9f32e248 | |||
| dbbdb63aa8 |
36
package.json
36
package.json
@ -45,11 +45,11 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "npm:rolldown-vite@7.3.0",
|
"vite": "npm:rolldown-vite@7.1.8",
|
||||||
"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.2",
|
"@electron/universal": "3.0.1",
|
||||||
"@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.13.1",
|
"@ffmpeg.wasm/main": "0.12.0",
|
||||||
"@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.7",
|
"@hono/node-server": "1.19.1",
|
||||||
"@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.2.0",
|
"@hono/zod-openapi": "1.1.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,6 +90,7 @@
|
|||||||
"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",
|
||||||
@ -105,8 +106,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": "20.0.2",
|
"happy-dom": "18.0.1",
|
||||||
"hono": "4.10.3",
|
"hono": "4.9.6",
|
||||||
"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",
|
||||||
@ -131,11 +132,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.48.2",
|
"virtua": "0.42.3",
|
||||||
"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.2.1"
|
"zod": "4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/tsconfig": "1.0.1",
|
"@electron-toolkit/tsconfig": "1.0.1",
|
||||||
@ -144,6 +145,7 @@
|
|||||||
"@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",
|
||||||
@ -153,12 +155,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.37",
|
"discord-api-types": "0.38.23",
|
||||||
"electron": "38.7.2",
|
"electron": "38.2.0",
|
||||||
"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.1",
|
"electron-vite": "4.0.0",
|
||||||
"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",
|
||||||
@ -166,14 +168,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.1.0",
|
"glob": "11.0.3",
|
||||||
"node-gyp": "11.4.2",
|
"node-gyp": "11.4.2",
|
||||||
"playwright": "1.55.1",
|
"playwright": "1.55.0",
|
||||||
"ts-morph": "27.0.2",
|
"ts-morph": "27.0.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.2",
|
||||||
"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.3.0",
|
"vite": "npm:rolldown-vite@7.1.8",
|
||||||
"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",
|
||||||
|
|||||||
948
pnpm-lock.yaml
generated
948
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": "Error en l'execució de l'extensió {{pluginName}}::{{contextName}}",
|
"execute-failed": "Ha fallat 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,8 +237,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -463,8 +462,8 @@
|
|||||||
"label": "Text d'estat",
|
"label": "Text d'estat",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Escoltant {artist}",
|
"artist": "Escoltant {artist}",
|
||||||
"pear-desktop": "Escoltant Pear Desktop",
|
"title": "Escoltant {song title}",
|
||||||
"title": "Escoltant {song title}"
|
"pear-desktop": "Escoltant Pear Desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,8 +237,7 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"enable-seekbar": "Suchleisten-Design aktivieren"
|
|
||||||
},
|
},
|
||||||
"name": "Thema aus Albumfarbe"
|
"name": "Thema aus Albumfarbe"
|
||||||
},
|
},
|
||||||
@ -607,7 +606,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ästen das Kontrollieren der Playlist erlauben"
|
"playlist": "Gäste das Kontrollieren der Playlist erlauben"
|
||||||
},
|
},
|
||||||
"set-permission": "Kontrollberechtigung ändern",
|
"set-permission": "Kontrollberechtigung ändern",
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
@ -323,22 +323,6 @@
|
|||||||
},
|
},
|
||||||
"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}} se ejecutó en {{ms}}ms",
|
"executed-at-ms": "Complemento {{pluginName}}::{{contextName}} 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}}\" cargados",
|
"loaded": "Complementos \"{{pluginName}}\" cargado",
|
||||||
"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,8 +237,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -463,8 +462,8 @@
|
|||||||
"label": "Texto de estado",
|
"label": "Texto de estado",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Escuchando a {artist}",
|
"artist": "Escuchando a {artist}",
|
||||||
"pear-desktop": "Escuchando Pear Desktop",
|
"title": "Escuchando {song title}",
|
||||||
"title": "Escuchando {song title}"
|
"pear-desktop": "Escuchando Pear Desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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": "Entrer un titre de fenêtre : (Laissé vide pour désactiver)",
|
"label": "Entrés un titre de fenêtre : (Laissé vide pour déactiver)",
|
||||||
"placeholder": "Exemple : Pear Desktop"
|
"placeholder": "Exemple : Pear Desktop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,8 +237,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -291,7 +290,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 d'Amuse est en cours d'exécution. GET /query pour obtenir des informations sur les chansons."
|
"query": "Le serveur API Amuse est en cours d'exécution. Envoyez une requête GET /query pour obtenir des informations sur la chanson."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
@ -303,7 +302,7 @@
|
|||||||
"deny": "Interdire"
|
"deny": "Interdire"
|
||||||
},
|
},
|
||||||
"message": "Autoriser {{ID}} ({{origin}}) à accéder à l'API ?",
|
"message": "Autoriser {{ID}} ({{origin}}) à accéder à l'API ?",
|
||||||
"title": "Demande d'autorisation à l'API"
|
"title": "Requête d'autorisation d'API"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -311,7 +310,7 @@
|
|||||||
"label": "Plan d'autorisation",
|
"label": "Plan d'autorisation",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"auth-at-first": {
|
"auth-at-first": {
|
||||||
"label": "Autoriser lors de la première requête"
|
"label": "Autoriser à la première requête"
|
||||||
},
|
},
|
||||||
"none": {
|
"none": {
|
||||||
"label": "Pas d'autorisation"
|
"label": "Pas d'autorisation"
|
||||||
@ -332,7 +331,7 @@
|
|||||||
"title": "Nom d'hôte"
|
"title": "Nom d'hôte"
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"label": "Entrez le port du serveur API :",
|
"label": "Entrez le port du serveur de l'API :",
|
||||||
"title": "Port"
|
"title": "Port"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -393,7 +392,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-titres disponibles pour cette chanson"
|
"no-captions": "Aucun sous-titre disponible pour cette chanson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compact-sidebar": {
|
"compact-sidebar": {
|
||||||
@ -463,8 +462,8 @@
|
|||||||
"label": "Texte d'état",
|
"label": "Texte d'état",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Écoute {artiste}",
|
"artist": "Écoute {artiste}",
|
||||||
"pear-desktop": "Écoute Pear Desktop",
|
"title": "Écoute {titre de la chanson}",
|
||||||
"title": "Écoute {titre de la chanson}"
|
"pear-desktop": "Écoute Pear Desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -637,7 +636,7 @@
|
|||||||
"name": "Navigation",
|
"name": "Navigation",
|
||||||
"templates": {
|
"templates": {
|
||||||
"back": {
|
"back": {
|
||||||
"title": "Aller à la page précédente"
|
"title": "Revenir à la page précédente"
|
||||||
},
|
},
|
||||||
"forward": {
|
"forward": {
|
||||||
"title": "Aller à la page suivante"
|
"title": "Aller à la page suivante"
|
||||||
@ -910,7 +909,7 @@
|
|||||||
"name": "Tuna OBS"
|
"name": "Tuna OBS"
|
||||||
},
|
},
|
||||||
"unobtrusive-player": {
|
"unobtrusive-player": {
|
||||||
"description": "Empêche le lecteur de s'afficher quand une chanson est en cours de lecture",
|
"description": "Empêche le lecteur de s'afficher quand un chanson est en lecture",
|
||||||
"name": "Lecteur Non-Intrusif"
|
"name": "Lecteur Non-Intrusif"
|
||||||
},
|
},
|
||||||
"video-toggle": {
|
"video-toggle": {
|
||||||
|
|||||||
@ -44,38 +44,25 @@
|
|||||||
},
|
},
|
||||||
"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": {
|
||||||
@ -83,63 +70,19 @@
|
|||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "ნავიგაცია",
|
"label": "ნავიგაცია",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"copy-current-url": "მიმდინარე URL-ის დაკოპირება",
|
"quit": "გასვლა"
|
||||||
"go-back": "უკან დაბრუნება",
|
|
||||||
"go-forward": "წინ გადასვლა",
|
|
||||||
"quit": "გასვლა",
|
|
||||||
"restart": "აპლიკაციის გადატვირთვა"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"label": "მორგება",
|
"label": "მორგება",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"advanced-options": {
|
|
||||||
"label": "გაფართოებული პარამეტრები",
|
|
||||||
"submenu": {
|
|
||||||
"auto-reset-app-cache": "აპლიკაციის ქეშის გადატვირთვა როცა აპლიკაცია დაიწყება",
|
|
||||||
"disable-hardware-acceleration": "აპარატურული აჩქარების გამორთვა",
|
|
||||||
"edit-config-json": "config.json-ის რედაქტირება",
|
|
||||||
"override-user-agent": "მომხმარებლის აგენტის შეცვლა",
|
|
||||||
"restart-on-config-changes": "გადატვირთვა კონფიგურაციის ცვლილებების დროს",
|
|
||||||
"set-proxy": {
|
|
||||||
"label": "პროქსის დაყენება",
|
|
||||||
"prompt": {
|
|
||||||
"label": "შეიყვანეთ პროქსის მისამართი: (გამორთვისთვის დატოვეთ ცარიელი)",
|
|
||||||
"placeholder": "მაგალითი: SOCKS5://127.0.0.1:9999",
|
|
||||||
"title": "პროქსის დაყენება"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"toggle-dev-tools": "DevTools-ის გადართვა"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"always-on-top": "მუდამ ზემოთ",
|
|
||||||
"auto-update": "ავტომატური განახლება",
|
|
||||||
"hide-menu": {
|
|
||||||
"dialog": {
|
|
||||||
"message": "მენიუ შემდეგი გაშვებისას დაიმალება, მის საჩვენებლად გამოიყენეთ [Alt] (ან თუ აპლიკაციის მენიუს იყენებთ, უკან დააწკაპუნეთ [`])",
|
|
||||||
"title": "მენიუს დამალვა ჩართულია"
|
|
||||||
},
|
|
||||||
"label": "მენიუს დამალვა"
|
|
||||||
},
|
|
||||||
"language": {
|
"language": {
|
||||||
"dialog": {
|
"label": "ენა"
|
||||||
"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": "გამორთულია"
|
||||||
}
|
}
|
||||||
@ -219,15 +162,11 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{size}}%"
|
"percent": "{{size}}%"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"use-fullscreen": {
|
|
||||||
"label": "სრული ეკრანის გამოყენება"
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"name": "გარემოს რეჟიმი"
|
|
||||||
},
|
},
|
||||||
"amuse": {
|
"amuse": {
|
||||||
"name": "გაკვირვება"
|
"name": "Amuse"
|
||||||
},
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@ -328,13 +267,6 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"disconnected": "გათიშული"
|
"disconnected": "გათიშული"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"toast": {
|
|
||||||
"closed": "Music Together-ის ორგანიზატორი დაიხურა",
|
|
||||||
"disconnected": "Music Together-ის კავშირი გათიშულია",
|
|
||||||
"host-failed": "Music Together-ის გამოცხადება ვერ მოხერხდა",
|
|
||||||
"id-copied": "გამოსაცხადებელი ID დაკოპირებულია ბუფერში",
|
|
||||||
"id-copy-failed": "გამოსაცხადებელი ID-ის ვერ დაკოპირდა ბუფერში"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
@ -377,30 +309,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"video-toggle": {
|
|
||||||
"menu": {
|
|
||||||
"mode": {
|
|
||||||
"label": "რეჟიმი",
|
|
||||||
"submenu": {
|
|
||||||
"custom": "მორგებული გადამრთველი",
|
|
||||||
"disabled": "გამორთულია",
|
|
||||||
"native": "ადგილობრივი გადართვა"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "ვიდეოს გადართვა",
|
|
||||||
"templates": {
|
|
||||||
"button-song": "სიმღერა",
|
|
||||||
"button-video": "ვიდეო"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"visualizer": {
|
|
||||||
"description": "პლეიერს ვიზუალიზატორს უმატებს",
|
|
||||||
"menu": {
|
|
||||||
"visualizer-type": "ვიზუალიზატორის ტიპი"
|
|
||||||
},
|
|
||||||
"name": "ვიზუალიზატორი"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -150,13 +150,6 @@
|
|||||||
"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": "देखाउनुहोस",
|
||||||
@ -186,7 +179,7 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"enabled": "सक्षम गरियो",
|
"enabled": "सक्षम गरियो",
|
||||||
"label": "प्लगइनहरू",
|
"label": "प्लगइनहरू",
|
||||||
"new": "नयाँ"
|
"new": "NEW"
|
||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"label": "हेर्नुहोस्",
|
"label": "हेर्नुहोस्",
|
||||||
|
|||||||
@ -237,8 +237,7 @@
|
|||||||
"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,8 +237,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -463,8 +462,8 @@
|
|||||||
"label": "Texto de estado",
|
"label": "Texto de estado",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "A ouvir {artist}",
|
"artist": "A ouvir {artist}",
|
||||||
"pear-desktop": "A reproduzir Pear Desktop",
|
"title": "A ouvir {song title}",
|
||||||
"title": "A ouvir {song title}"
|
"pear-desktop": "A reproduzir Pear Desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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,8 +237,7 @@
|
|||||||
"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,56 +3,7 @@
|
|||||||
"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": "Stäng",
|
"quit": "Avsluta",
|
||||||
"restart": "Starta om appen"
|
"restart": "Starta om appen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,8 +237,7 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}} %"
|
"percent": "{{ratio}} %"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"enable-seekbar": "Aktivera temaanpassning av uppspelningsreglaget"
|
|
||||||
},
|
},
|
||||||
"name": "Albumfärgtema"
|
"name": "Albumfärgtema"
|
||||||
},
|
},
|
||||||
@ -463,8 +462,8 @@
|
|||||||
"label": "Statusmeddelande",
|
"label": "Statusmeddelande",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "Lyssnar på {artist}",
|
"artist": "Lyssnar på {artist}",
|
||||||
"pear-desktop": "Lyssnar på Pear Desktop",
|
"title": "Lyssnar på {song title}",
|
||||||
"title": "Lyssnar på {song title}"
|
"pear-desktop": "Lyssnar på Pear Desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,8 +237,7 @@
|
|||||||
"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ı"
|
||||||
},
|
},
|
||||||
@ -463,8 +462,8 @@
|
|||||||
"label": "Durum metni",
|
"label": "Durum metni",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"artist": "{artist} Dinleniyor",
|
"artist": "{artist} Dinleniyor",
|
||||||
"pear-desktop": "Pear Desktop Dinleniyor",
|
"title": "{song title} Dinleniyor",
|
||||||
"title": "{song title} Dinleniyor"
|
"pear-desktop": "Pear Desktop Dinleniyor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -237,8 +237,7 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"enable-seekbar": "啟用進度條主題樣式"
|
|
||||||
},
|
},
|
||||||
"name": "隨歌曲色調變更主題"
|
"name": "隨歌曲色調變更主題"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,3 @@
|
|||||||
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';
|
||||||
@ -52,26 +48,22 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.run(config);
|
this.run(config.hostname, config.port);
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
this.end();
|
this.end();
|
||||||
},
|
},
|
||||||
onConfigChange(config) {
|
onConfigChange(config) {
|
||||||
const old = this.oldConfig;
|
|
||||||
if (
|
if (
|
||||||
old?.hostname === config.hostname &&
|
this.oldConfig?.hostname === config.hostname &&
|
||||||
old?.port === config.port &&
|
this.oldConfig?.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);
|
this.run(config.hostname, config.port);
|
||||||
this.oldConfig = config;
|
this.oldConfig = config;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -161,30 +153,15 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
|
|
||||||
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
||||||
},
|
},
|
||||||
run(config) {
|
run(hostname, port) {
|
||||||
if (!this.app) return;
|
if (!this.app) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serveOptions =
|
this.server = serve({
|
||||||
config.useHttps && config.certPath && config.keyPath
|
fetch: this.app.fetch.bind(this.app),
|
||||||
? {
|
port,
|
||||||
fetch: this.app.fetch.bind(this.app),
|
hostname,
|
||||||
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,26 +411,6 @@ 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`,
|
||||||
@ -768,63 +748,6 @@ 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: (config: APIServerConfig) => void;
|
run: (hostname: string, port: number) => void;
|
||||||
end: () => void;
|
end: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,9 +11,6 @@ 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 = {
|
||||||
@ -24,7 +21,4 @@ export const defaultAPIServerConfig: APIServerConfig = {
|
|||||||
secret: Date.now().toString(36),
|
secret: Date.now().toString(36),
|
||||||
|
|
||||||
authorizedClients: [],
|
authorizedClients: [],
|
||||||
useHttps: false,
|
|
||||||
certPath: '',
|
|
||||||
keyPath: '',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { dialog } from 'electron';
|
|
||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
@ -94,51 +93,5 @@ 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,11 +9,9 @@ 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;
|
||||||
@ -173,10 +171,9 @@ 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, after })),
|
this.getConnections().map((conn) => conn.send({ type, payload })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -215,25 +215,6 @@ 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', {
|
||||||
@ -253,15 +234,7 @@ 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': {
|
||||||
@ -412,16 +385,6 @@ 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,
|
||||||
@ -429,9 +392,7 @@ 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': {
|
||||||
@ -459,14 +420,6 @@ 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) => ({
|
||||||
@ -476,13 +429,6 @@ 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,11 +314,6 @@ 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) {
|
||||||
@ -352,7 +347,7 @@ export class Queue {
|
|||||||
},
|
},
|
||||||
after: [
|
after: [
|
||||||
{
|
{
|
||||||
type: 'SET_INDEX',
|
type: 'SYNC_PROGRESS',
|
||||||
payload: {
|
payload: {
|
||||||
index,
|
index,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -77,10 +77,11 @@ 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
|
||||||
|
|||||||
340
src/plugins/synced-lyrics/providers/NetEase.ts
Normal file
340
src/plugins/synced-lyrics/providers/NetEase.ts
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
// 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,6 +7,7 @@ export enum ProviderNames {
|
|||||||
LRCLib = 'LRCLib',
|
LRCLib = 'LRCLib',
|
||||||
MusixMatch = 'MusixMatch',
|
MusixMatch = 'MusixMatch',
|
||||||
LyricsGenius = 'LyricsGenius',
|
LyricsGenius = 'LyricsGenius',
|
||||||
|
NetEase = 'NetEase',
|
||||||
// Megalobiz = 'Megalobiz',
|
// Megalobiz = 'Megalobiz',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,13 @@ 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,5 +16,4 @@ 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