mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 02:31:45 +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": {
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@7.3.0",
|
||||
"vite": "npm:rolldown-vite@7.1.8",
|
||||
"node-gyp": "11.4.2",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "3.0.2",
|
||||
"@electron/universal": "3.0.1",
|
||||
"@babel/runtime": "7.28.4"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
@ -67,15 +67,15 @@
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@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",
|
||||
"@foobar404/wave": "2.0.5",
|
||||
"@ghostery/adblocker-electron": "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/swagger-ui": "0.5.2",
|
||||
"@hono/zod-openapi": "1.2.0",
|
||||
"@hono/zod-openapi": "1.1.0",
|
||||
"@hono/zod-validator": "0.7.2",
|
||||
"@jellybrick/dbus-next": "0.10.3",
|
||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||
@ -90,6 +90,7 @@
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"color": "5.0.0",
|
||||
"conf": "14.0.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"custom-electron-prompt": "1.5.8",
|
||||
"deepmerge-ts": "7.1.5",
|
||||
"delay": "6.0.0",
|
||||
@ -105,8 +106,8 @@
|
||||
"fflate": "0.8.2",
|
||||
"filenamify": "6.0.0",
|
||||
"hanja": "1.1.5",
|
||||
"happy-dom": "20.0.2",
|
||||
"hono": "4.10.3",
|
||||
"happy-dom": "18.0.1",
|
||||
"hono": "4.9.6",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "25.5.2",
|
||||
@ -131,11 +132,11 @@
|
||||
"solid-transition-group": "0.3.0",
|
||||
"tiny-pinyin": "1.3.2",
|
||||
"tinyld": "1.3.4",
|
||||
"virtua": "0.48.2",
|
||||
"virtua": "0.42.3",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "^16.0.1",
|
||||
"zod": "4.2.1"
|
||||
"zod": "4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
@ -144,6 +145,7 @@
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.3.1",
|
||||
"@total-typescript/ts-reset": "0.6.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/electron-localshortcut": "3.1.3",
|
||||
"@types/howler": "2.2.12",
|
||||
"@types/html-to-text": "9.0.4",
|
||||
@ -153,12 +155,12 @@
|
||||
"builtin-modules": "5.0.0",
|
||||
"cross-env": "10.0.0",
|
||||
"del-cli": "6.0.0",
|
||||
"discord-api-types": "0.38.37",
|
||||
"electron": "38.7.2",
|
||||
"discord-api-types": "0.38.23",
|
||||
"electron": "38.2.0",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "26.0.12",
|
||||
"electron-devtools-installer": "4.0.0",
|
||||
"electron-vite": "4.0.1",
|
||||
"electron-vite": "4.0.0",
|
||||
"eslint": "9.35.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||
@ -166,14 +168,14 @@
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-solid": "0.14.5",
|
||||
"glob": "11.1.0",
|
||||
"glob": "11.0.3",
|
||||
"node-gyp": "11.4.2",
|
||||
"playwright": "1.55.1",
|
||||
"ts-morph": "27.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"playwright": "1.55.0",
|
||||
"ts-morph": "27.0.0",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.43.0",
|
||||
"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-resolve": "2.5.2",
|
||||
"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": {
|
||||
"console": {
|
||||
"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",
|
||||
"initialize-failed": "Ha fallat la inicialització de l'extensió \"{{pluginName}}\"",
|
||||
"load-all": "Carregant totes les extensions",
|
||||
@ -237,8 +237,7 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Activar el tema en la barra de progrés"
|
||||
}
|
||||
},
|
||||
"name": "Tema de color de l'àlbum"
|
||||
},
|
||||
@ -463,8 +462,8 @@
|
||||
"label": "Text d'estat",
|
||||
"submenu": {
|
||||
"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": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Suchleisten-Design aktivieren"
|
||||
}
|
||||
},
|
||||
"name": "Thema aus Albumfarbe"
|
||||
},
|
||||
@ -607,7 +606,7 @@
|
||||
"permission": {
|
||||
"all": "Gästen erlauben, Wiederhabeliste und Player zu bedienen",
|
||||
"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",
|
||||
"status": {
|
||||
|
||||
@ -323,22 +323,6 @@
|
||||
},
|
||||
"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]",
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
"console": {
|
||||
"plugins": {
|
||||
"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}}\"",
|
||||
"load-all": "Cargando todos los complementos",
|
||||
"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}}\"",
|
||||
"unloaded": "Complemento \"{{pluginName}}\" descargado"
|
||||
}
|
||||
@ -237,8 +237,7 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Habilitar temas a la barra de búsqueda"
|
||||
}
|
||||
},
|
||||
"name": "Tema de color del álbum"
|
||||
},
|
||||
@ -463,8 +462,8 @@
|
||||
"label": "Texto de estado",
|
||||
"submenu": {
|
||||
"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": {
|
||||
"label": "Titre de fenêtre personnalisé",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@ -237,8 +237,7 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Activer le thème sur la barre de progression"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"name": "Amuse",
|
||||
"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": {
|
||||
@ -303,7 +302,7 @@
|
||||
"deny": "Interdire"
|
||||
},
|
||||
"message": "Autoriser {{ID}} ({{origin}}) à accéder à l'API ?",
|
||||
"title": "Demande d'autorisation à l'API"
|
||||
"title": "Requête d'autorisation d'API"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@ -311,7 +310,7 @@
|
||||
"label": "Plan d'autorisation",
|
||||
"submenu": {
|
||||
"auth-at-first": {
|
||||
"label": "Autoriser lors de la première requête"
|
||||
"label": "Autoriser à la première requête"
|
||||
},
|
||||
"none": {
|
||||
"label": "Pas d'autorisation"
|
||||
@ -332,7 +331,7 @@
|
||||
"title": "Nom d'hôte"
|
||||
},
|
||||
"port": {
|
||||
"label": "Entrez le port du serveur API :",
|
||||
"label": "Entrez le port du serveur de l'API :",
|
||||
"title": "Port"
|
||||
}
|
||||
}
|
||||
@ -393,7 +392,7 @@
|
||||
"toast": {
|
||||
"caption-changed": "Sous-titres changés en {{language}}",
|
||||
"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": {
|
||||
@ -463,8 +462,8 @@
|
||||
"label": "Texte d'état",
|
||||
"submenu": {
|
||||
"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",
|
||||
"templates": {
|
||||
"back": {
|
||||
"title": "Aller à la page précédente"
|
||||
"title": "Revenir à la page précédente"
|
||||
},
|
||||
"forward": {
|
||||
"title": "Aller à la page suivante"
|
||||
@ -910,7 +909,7 @@
|
||||
"name": "Tuna OBS"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"video-toggle": {
|
||||
|
||||
@ -44,38 +44,25 @@
|
||||
},
|
||||
"dialog": {
|
||||
"hide-menu-enabled": {
|
||||
"detail": "მენიუ დამალულია, გამოიყენეთ 'Alt', რათა გამოაჩინოთ ის (ან 'Escape' თუ იყენებთ აპლიკაციის შიგნითა მენიუს)",
|
||||
"message": "მენიუს დამალვა ჩართულია",
|
||||
"title": "მენიუს დამალვა ჩართულია"
|
||||
"detail": "მენიუ დამალულია, გამოიყენეთ 'Alt', რათა გამოაჩინოთ ის (ან 'Escape' თუ იყენებთ აპლიკაციის შიგნითა მენიუს)"
|
||||
},
|
||||
"need-to-restart": {
|
||||
"buttons": {
|
||||
"later": "მოგვიანებით",
|
||||
"restart-now": "გადატვირთვა ახლავე"
|
||||
},
|
||||
"detail": "„{{pluginName}}“ დანამატის ძალაში შესასვლელად გადატვირთვა საჭიროა",
|
||||
"message": "\"{{pluginName}}\" საჭიროებს გადატვირთვას",
|
||||
"title": "საჭიროებს გადატვირთვას"
|
||||
"later": "მოგვიანებით"
|
||||
}
|
||||
},
|
||||
"unresponsive": {
|
||||
"buttons": {
|
||||
"quit": "გასვლა",
|
||||
"relaunch": "თავიდან გაშვება",
|
||||
"wait": "მოცდა"
|
||||
},
|
||||
"detail": "ბოდიშს გიხდით მოუხერხელობისათვის! გთხოვთ აირჩიეთ რა უნდა გაკეთდეს:",
|
||||
"message": "აპლიკაცია არ პასუხობს",
|
||||
"title": "ფანჯარა არ პასუხობს"
|
||||
}
|
||||
},
|
||||
"update-available": {
|
||||
"buttons": {
|
||||
"disable": "განახლებების გამორთვა",
|
||||
"download": "გადმოწერა",
|
||||
"ok": "დიახ"
|
||||
},
|
||||
"detail": "ახალი ვერსიაა ხელმისაწვდომი, მისი ჩამოტვირთვა შესაძლებელია {{downloadLink}}-დან",
|
||||
"message": "ახალი ვერსია ხელმისაწვდომია",
|
||||
"title": "განახლება ხელმისაწვდომია"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@ -83,63 +70,19 @@
|
||||
"navigation": {
|
||||
"label": "ნავიგაცია",
|
||||
"submenu": {
|
||||
"copy-current-url": "მიმდინარე URL-ის დაკოპირება",
|
||||
"go-back": "უკან დაბრუნება",
|
||||
"go-forward": "წინ გადასვლა",
|
||||
"quit": "გასვლა",
|
||||
"restart": "აპლიკაციის გადატვირთვა"
|
||||
"quit": "გასვლა"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"label": "მორგება",
|
||||
"submenu": {
|
||||
"advanced-options": {
|
||||
"label": "გაფართოებული პარამეტრები",
|
||||
"submenu": {
|
||||
"auto-reset-app-cache": "აპლიკაციის ქეშის გადატვირთვა როცა აპლიკაცია დაიწყება",
|
||||
"disable-hardware-acceleration": "აპარატურული აჩქარების გამორთვა",
|
||||
"edit-config-json": "config.json-ის რედაქტირება",
|
||||
"override-user-agent": "მომხმარებლის აგენტის შეცვლა",
|
||||
"restart-on-config-changes": "გადატვირთვა კონფიგურაციის ცვლილებების დროს",
|
||||
"set-proxy": {
|
||||
"label": "პროქსის დაყენება",
|
||||
"prompt": {
|
||||
"label": "შეიყვანეთ პროქსის მისამართი: (გამორთვისთვის დატოვეთ ცარიელი)",
|
||||
"placeholder": "მაგალითი: SOCKS5://127.0.0.1:9999",
|
||||
"title": "პროქსის დაყენება"
|
||||
}
|
||||
},
|
||||
"toggle-dev-tools": "DevTools-ის გადართვა"
|
||||
}
|
||||
},
|
||||
"always-on-top": "მუდამ ზემოთ",
|
||||
"auto-update": "ავტომატური განახლება",
|
||||
"hide-menu": {
|
||||
"dialog": {
|
||||
"message": "მენიუ შემდეგი გაშვებისას დაიმალება, მის საჩვენებლად გამოიყენეთ [Alt] (ან თუ აპლიკაციის მენიუს იყენებთ, უკან დააწკაპუნეთ [`])",
|
||||
"title": "მენიუს დამალვა ჩართულია"
|
||||
},
|
||||
"label": "მენიუს დამალვა"
|
||||
},
|
||||
"language": {
|
||||
"dialog": {
|
||||
"message": "გადატვირთვის შემდეგ ენა შეიცვლება",
|
||||
"title": "ენა შეიცვალა"
|
||||
},
|
||||
"label": "ენა",
|
||||
"submenu": {
|
||||
"to-help-translate": "გსურთ დაგვეხმაროთ თარგმნაში? დააწკაპუნეთ აქ"
|
||||
}
|
||||
"label": "ენა"
|
||||
},
|
||||
"resume-on-start": "აპლიკაციის თავიდან გაშვებისას ბოლო სიმღერა დაუკრას",
|
||||
"single-instance-lock": "ერთჯერადი ინსტანციის საკეტი",
|
||||
"start-at-login": "შესვლაზე დაწყება",
|
||||
"starting-page": {
|
||||
"label": "საწყისი გვერდი",
|
||||
"unset": "მოხსნა"
|
||||
},
|
||||
"tray": {
|
||||
"label": "უჯრა",
|
||||
"submenu": {
|
||||
"disabled": "გამორთულია"
|
||||
}
|
||||
@ -219,15 +162,11 @@
|
||||
"submenu": {
|
||||
"percent": "{{size}}%"
|
||||
}
|
||||
},
|
||||
"use-fullscreen": {
|
||||
"label": "სრული ეკრანის გამოყენება"
|
||||
}
|
||||
},
|
||||
"name": "გარემოს რეჟიმი"
|
||||
}
|
||||
},
|
||||
"amuse": {
|
||||
"name": "გაკვირვება"
|
||||
"name": "Amuse"
|
||||
},
|
||||
"api-server": {
|
||||
"dialog": {
|
||||
@ -328,13 +267,6 @@
|
||||
"status": {
|
||||
"disconnected": "გათიშული"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"closed": "Music Together-ის ორგანიზატორი დაიხურა",
|
||||
"disconnected": "Music Together-ის კავშირი გათიშულია",
|
||||
"host-failed": "Music Together-ის გამოცხადება ვერ მოხერხდა",
|
||||
"id-copied": "გამოსაცხადებელი ID დაკოპირებულია ბუფერში",
|
||||
"id-copy-failed": "გამოსაცხადებელი ID-ის ვერ დაკოპირდა ბუფერში"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"label": "भिजुअल ट्वीक्स",
|
||||
"submenu": {
|
||||
"custom-window-title": {
|
||||
"label": "अनुकूलन विन्डो शीर्षक",
|
||||
"prompt": {
|
||||
"label": "अनुकूलन विन्डो शीर्षक प्रविष्ट गर्नुहोस्: (असक्षम पार्न खाली छोड्नुहोस्)",
|
||||
"placeholder": "उदाहरण: पियर डेस्कटप"
|
||||
}
|
||||
},
|
||||
"like-buttons": {
|
||||
"default": "पूर्वनिर्धारित",
|
||||
"force-show": "देखाउनुहोस",
|
||||
@ -186,7 +179,7 @@
|
||||
"plugins": {
|
||||
"enabled": "सक्षम गरियो",
|
||||
"label": "प्लगइनहरू",
|
||||
"new": "नयाँ"
|
||||
"new": "NEW"
|
||||
},
|
||||
"view": {
|
||||
"label": "हेर्नुहोस्",
|
||||
|
||||
@ -237,8 +237,7 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Ativar personalização da barra de progresso"
|
||||
}
|
||||
},
|
||||
"name": "Tema da cor do álbum"
|
||||
},
|
||||
|
||||
@ -237,8 +237,7 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Ativar temas na barra de reprodução"
|
||||
}
|
||||
},
|
||||
"name": "Tema de cores do álbum"
|
||||
},
|
||||
@ -463,8 +462,8 @@
|
||||
"label": "Texto de estado",
|
||||
"submenu": {
|
||||
"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": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Povoliť farebnú tému aj v seekbare"
|
||||
}
|
||||
},
|
||||
"name": "Farebná téma albumu"
|
||||
},
|
||||
|
||||
@ -3,56 +3,7 @@
|
||||
"console": {
|
||||
"plugins": {
|
||||
"execute-failed": "Dështoi në ekzekutimin e plugin-it {{pluginName}}::{{contextName}}",
|
||||
"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"
|
||||
"executed-at-ms": "Shtojca {{pluginName}}::{{contextName}} u ekzekutua në {{ms}}ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
"copy-current-url": "Kopiera nuvarande länk",
|
||||
"go-back": "Gå tillbaka",
|
||||
"go-forward": "Gå framåt",
|
||||
"quit": "Stäng",
|
||||
"quit": "Avsluta",
|
||||
"restart": "Starta om appen"
|
||||
}
|
||||
},
|
||||
@ -237,8 +237,7 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}} %"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Aktivera temaanpassning av uppspelningsreglaget"
|
||||
}
|
||||
},
|
||||
"name": "Albumfärgtema"
|
||||
},
|
||||
@ -463,8 +462,8 @@
|
||||
"label": "Statusmeddelande",
|
||||
"submenu": {
|
||||
"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": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Arama çubuğu temalarını etkinleştir"
|
||||
}
|
||||
},
|
||||
"name": "Albüm Renk Teması"
|
||||
},
|
||||
@ -463,8 +462,8 @@
|
||||
"label": "Durum metni",
|
||||
"submenu": {
|
||||
"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": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "啟用進度條主題樣式"
|
||||
}
|
||||
},
|
||||
"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 { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
||||
import { cors } from 'hono/cors';
|
||||
@ -52,26 +48,22 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
||||
);
|
||||
|
||||
this.run(config);
|
||||
this.run(config.hostname, config.port);
|
||||
},
|
||||
stop() {
|
||||
this.end();
|
||||
},
|
||||
onConfigChange(config) {
|
||||
const old = this.oldConfig;
|
||||
if (
|
||||
old?.hostname === config.hostname &&
|
||||
old?.port === config.port &&
|
||||
old?.useHttps === config.useHttps &&
|
||||
old?.certPath === config.certPath &&
|
||||
old?.keyPath === config.keyPath
|
||||
this.oldConfig?.hostname === config.hostname &&
|
||||
this.oldConfig?.port === config.port
|
||||
) {
|
||||
this.oldConfig = config;
|
||||
return;
|
||||
}
|
||||
|
||||
this.end();
|
||||
this.run(config);
|
||||
this.run(config.hostname, config.port);
|
||||
this.oldConfig = config;
|
||||
},
|
||||
|
||||
@ -161,30 +153,15 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
|
||||
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
||||
},
|
||||
run(config) {
|
||||
run(hostname, port) {
|
||||
if (!this.app) return;
|
||||
|
||||
try {
|
||||
const serveOptions =
|
||||
config.useHttps && config.certPath && config.keyPath
|
||||
? {
|
||||
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);
|
||||
this.server = serve({
|
||||
fetch: this.app.fetch.bind(this.app),
|
||||
port,
|
||||
hostname,
|
||||
});
|
||||
|
||||
if (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({
|
||||
method: 'get',
|
||||
path: `/api/${API_VERSION}/queue`,
|
||||
@ -768,63 +748,6 @@ export const register = (
|
||||
app.openapi(routes.oldQueueInfo, 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) => {
|
||||
const { videoId, insertPosition } = ctx.req.valid('json');
|
||||
controller.addSongToQueue(videoId, insertPosition);
|
||||
|
||||
@ -17,6 +17,6 @@ export type BackendType = {
|
||||
injectWebSocket?: (server: ReturnType<typeof serve>) => void;
|
||||
|
||||
init: (ctx: BackendContext<APIServerConfig>) => void;
|
||||
run: (config: APIServerConfig) => void;
|
||||
run: (hostname: string, port: number) => void;
|
||||
end: () => void;
|
||||
};
|
||||
|
||||
@ -11,9 +11,6 @@ export interface APIServerConfig {
|
||||
secret: string;
|
||||
|
||||
authorizedClients: string[];
|
||||
useHttps: boolean;
|
||||
certPath: string;
|
||||
keyPath: string;
|
||||
}
|
||||
|
||||
export const defaultAPIServerConfig: APIServerConfig = {
|
||||
@ -24,7 +21,4 @@ export const defaultAPIServerConfig: APIServerConfig = {
|
||||
secret: Date.now().toString(36),
|
||||
|
||||
authorizedClients: [],
|
||||
useHttps: false,
|
||||
certPath: '',
|
||||
keyPath: '',
|
||||
};
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { dialog } from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
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';
|
||||
|
||||
export type ConnectionEventMap = {
|
||||
CLEAR_QUEUE: {};
|
||||
ADD_SONGS: { videoList: VideoData[]; index?: number };
|
||||
REMOVE_SONG: { index: number };
|
||||
MOVE_SONG: { fromIndex: number; toIndex: number };
|
||||
SET_INDEX: { index: number };
|
||||
IDENTIFY: { profile: Profile } | undefined;
|
||||
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
|
||||
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
||||
@ -173,10 +171,9 @@ export class Connection {
|
||||
public async broadcast<Event extends keyof ConnectionEventMap>(
|
||||
type: Event,
|
||||
payload: ConnectionEventMap[Event],
|
||||
after?: ConnectionEventUnion[],
|
||||
) {
|
||||
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;
|
||||
|
||||
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': {
|
||||
if (conn && this.permission === 'host-only') {
|
||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||
@ -253,15 +234,7 @@ export default createPlugin<
|
||||
await this.connection?.broadcast('ADD_SONGS', {
|
||||
...event.payload,
|
||||
videoList,
|
||||
},
|
||||
event.after,
|
||||
);
|
||||
|
||||
const afterevent = event.after?.at(0);
|
||||
if (afterevent?.type === 'SET_INDEX') {
|
||||
this.queue?.setIndex(afterevent.payload.index);
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'REMOVE_SONG': {
|
||||
@ -412,16 +385,6 @@ export default createPlugin<
|
||||
const queueListener = async (event: ConnectionEventUnion) => {
|
||||
this.ignoreChange = true;
|
||||
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': {
|
||||
await this.connection?.broadcast('ADD_SONGS', {
|
||||
...event.payload,
|
||||
@ -429,9 +392,7 @@ export default createPlugin<
|
||||
...it,
|
||||
ownerId: it.ownerId ?? this.connection!.id,
|
||||
})),
|
||||
},
|
||||
event.after,
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'REMOVE_SONG': {
|
||||
@ -459,14 +420,6 @@ export default createPlugin<
|
||||
const listener = async (event: ConnectionEventUnion) => {
|
||||
this.ignoreChange = true;
|
||||
switch (event.type) {
|
||||
case 'CLEAR_QUEUE': {
|
||||
this.queue?.clear();
|
||||
break;
|
||||
}
|
||||
case 'SET_INDEX': {
|
||||
this.queue?.setIndex(event.payload.index);
|
||||
break;
|
||||
}
|
||||
case 'ADD_SONGS': {
|
||||
const videoList: VideoData[] = event.payload.videoList.map(
|
||||
(it) => ({
|
||||
@ -476,13 +429,6 @@ export default createPlugin<
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
case 'REMOVE_SONG': {
|
||||
|
||||
@ -314,11 +314,6 @@ export class Queue {
|
||||
if (!this.internalDispatch) {
|
||||
if (event.type === 'CLEAR') {
|
||||
this.ignoreFlag = true;
|
||||
this.broadcast({
|
||||
type: 'CLEAR_QUEUE',
|
||||
payload: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === 'ADD_ITEMS') {
|
||||
if (this.ignoreFlag) {
|
||||
@ -352,7 +347,7 @@ export class Queue {
|
||||
},
|
||||
after: [
|
||||
{
|
||||
type: 'SET_INDEX',
|
||||
type: 'SYNC_PROGRESS',
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
|
||||
@ -77,10 +77,11 @@ export class LRCLib implements LyricProvider {
|
||||
}
|
||||
|
||||
const filteredResults = [];
|
||||
const artists = artist.split(/[&,]/g).map((i) => i.trim());
|
||||
|
||||
for (const item of data) {
|
||||
const { artistName } = item;
|
||||
|
||||
const artists = artist.split(/[&,]/g).map((i) => i.trim());
|
||||
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
|
||||
|
||||
// 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',
|
||||
MusixMatch = 'MusixMatch',
|
||||
LyricsGenius = 'LyricsGenius',
|
||||
NetEase = 'NetEase',
|
||||
// Megalobiz = 'Megalobiz',
|
||||
}
|
||||
|
||||
|
||||
@ -3,11 +3,13 @@ import { YTMusic } from './YTMusic';
|
||||
import { LRCLib } from './LRCLib';
|
||||
import { MusixMatch } from './MusixMatch';
|
||||
import { LyricsGenius } from './LyricsGenius';
|
||||
import { Netease } from './NetEase';
|
||||
|
||||
export const providers = {
|
||||
[ProviderNames.YTMusic]: new YTMusic(),
|
||||
[ProviderNames.LRCLib]: new LRCLib(),
|
||||
[ProviderNames.MusixMatch]: new MusixMatch(),
|
||||
[ProviderNames.LyricsGenius]: new LyricsGenius(),
|
||||
[ProviderNames.NetEase]: new Netease(),
|
||||
// [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow
|
||||
} as const;
|
||||
|
||||
@ -16,5 +16,4 @@ export const startingPages: Record<string, string> = {
|
||||
'Uploaded Songs': 'FEmusic_library_privately_owned_tracks',
|
||||
'Uploaded Albums': 'FEmusic_library_privately_owned_releases',
|
||||
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
||||
'Mixed for you': 'FEmusic_mixed_for_you',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user