mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 19:31:46 +00:00
Compare commits
12 Commits
b7e43e3125
...
synced-lyr
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e9f32e248 | |||
| 6c4ae0dbfa | |||
| f7aaa3377a | |||
| ab1a0478cf | |||
| 127f56905c | |||
| 8d448c667f | |||
| e00f33eb2d | |||
| 8100804ced | |||
| dbbdb63aa8 | |||
| 4fba3ffd92 | |||
| bef8252314 | |||
| a96cc5aa8a |
@ -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",
|
||||
@ -134,7 +135,7 @@
|
||||
"virtua": "0.42.3",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "15.0.1",
|
||||
"youtubei.js": "^16.0.1",
|
||||
"zod": "4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -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",
|
||||
|
||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@ -117,6 +117,9 @@ importers:
|
||||
conf:
|
||||
specifier: 14.0.0
|
||||
version: 14.0.0
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
custom-electron-prompt:
|
||||
specifier: 1.5.8
|
||||
version: 1.5.8(electron@38.2.0)
|
||||
@ -250,8 +253,8 @@ importers:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
youtubei.js:
|
||||
specifier: 15.0.1
|
||||
version: 15.0.1
|
||||
specifier: ^16.0.1
|
||||
version: 16.0.1
|
||||
zod:
|
||||
specifier: 4.1.5
|
||||
version: 4.1.5
|
||||
@ -271,6 +274,9 @@ importers:
|
||||
'@total-typescript/ts-reset':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/electron-localshortcut':
|
||||
specifier: 3.1.3
|
||||
version: 3.1.3
|
||||
@ -482,8 +488,8 @@ packages:
|
||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bufbuild/protobuf@2.6.3':
|
||||
resolution: {integrity: sha512-w/gJKME9mYN7ZoUAmSMAWXk4hkVpxRKvEJCb3dV5g9wwWdxTJJ0ayOJAVcNxtdqaxDyFuC0uz4RSGVacJ030PQ==}
|
||||
'@bufbuild/protobuf@2.10.0':
|
||||
resolution: {integrity: sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==}
|
||||
|
||||
'@dehoist/romanize-thai@1.0.0':
|
||||
resolution: {integrity: sha512-6SqD4vyZ48otnypLXMh901CeQetoP5ptYOaIr58N6zDqjjoN0bHszMb5d/6AXJJQf8kIvbmSWBeuDrbAWLajPQ==}
|
||||
@ -1298,6 +1304,9 @@ packages:
|
||||
'@types/cacheable-request@6.0.3':
|
||||
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
@ -2054,6 +2063,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
css-select@5.2.2:
|
||||
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||
|
||||
@ -2571,6 +2583,9 @@ packages:
|
||||
exponential-backoff@3.1.2:
|
||||
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
|
||||
|
||||
exponential-backoff@3.1.3:
|
||||
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||
engines: {node: '>= 10.17.0'}
|
||||
@ -3214,9 +3229,6 @@ packages:
|
||||
resolution: {integrity: sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
jintr@3.3.1:
|
||||
resolution: {integrity: sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA==}
|
||||
|
||||
jpeg-js@0.4.4:
|
||||
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
|
||||
|
||||
@ -3480,6 +3492,10 @@ packages:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
meriyah@6.1.4:
|
||||
resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
@ -4875,8 +4891,8 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
youtubei.js@15.0.1:
|
||||
resolution: {integrity: sha512-2slapqJS5NuXKHvcACEknyVz0AjH/TrXaOhDM0q2twQKa54kCmfj+7B/2nGfd20uzAe29zW1ejk2qOc4ABuGkg==}
|
||||
youtubei.js@16.0.1:
|
||||
resolution: {integrity: sha512-3802bCAGkBc2/G5WUTc0l/bO5mPYJbQAHL04d9hE9PnrDHoBUT8MN721Yqt4RCNncAXdHcfee9VdJy3Fhq1r5g==}
|
||||
|
||||
zlibjs@0.3.1:
|
||||
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
|
||||
@ -5021,7 +5037,7 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@bufbuild/protobuf@2.6.3': {}
|
||||
'@bufbuild/protobuf@2.10.0': {}
|
||||
|
||||
'@dehoist/romanize-thai@1.0.0': {}
|
||||
|
||||
@ -5091,7 +5107,7 @@ snapshots:
|
||||
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
exponential-backoff: 3.1.2
|
||||
exponential-backoff: 3.1.3
|
||||
glob: 8.1.0
|
||||
graceful-fs: 4.2.11
|
||||
make-fetch-happen: 10.2.1
|
||||
@ -5914,6 +5930,8 @@ snapshots:
|
||||
'@types/node': 24.3.0
|
||||
'@types/responselike': 1.0.3
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@ -6791,6 +6809,8 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
css-select@5.2.2:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
@ -7505,6 +7525,8 @@ snapshots:
|
||||
|
||||
exponential-backoff@3.1.2: {}
|
||||
|
||||
exponential-backoff@3.1.3: {}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
@ -8200,10 +8222,6 @@ snapshots:
|
||||
'@jimp/types': 1.6.0
|
||||
'@jimp/utils': 1.6.0
|
||||
|
||||
jintr@3.3.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
jpeg-js@0.4.4: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@ -8470,6 +8488,8 @@ snapshots:
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
meriyah@6.1.4: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
@ -9904,11 +9924,10 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
youtubei.js@15.0.1:
|
||||
youtubei.js@16.0.1:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.6.3
|
||||
jintr: 3.3.1
|
||||
undici: 6.21.3
|
||||
'@bufbuild/protobuf': 2.10.0
|
||||
meriyah: 6.1.4
|
||||
|
||||
zlibjs@0.3.1: {}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"console": {
|
||||
"plugins": {
|
||||
"execute-failed": "Erweiterung {{pluginName}}::{{contextName}} konnte nicht ausgeführt werden",
|
||||
"executed-at-ms": "Erweiterung {{pluginName}}::{{contextName}} ausgeführt in {{ms}}ms",
|
||||
"executed-at-ms": "Erweiterung {{pluginName}}::{{contextName}} in {{ms}}ms ausgeführt",
|
||||
"initialize-failed": "Initialisierung der Erweiterung \"{{pluginName}}\" fehlgeschlagen",
|
||||
"load-all": "Lade alle Erweiterungen",
|
||||
"load-failed": "Laden der Erweiterung \"{{pluginName}}\" fehlgeschlagen",
|
||||
|
||||
@ -237,7 +237,8 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "Enable seekbar theming"
|
||||
},
|
||||
"name": "Album Color Theme"
|
||||
},
|
||||
|
||||
@ -150,6 +150,11 @@
|
||||
"visual-tweaks": {
|
||||
"label": "תיקונים חזותיים",
|
||||
"submenu": {
|
||||
"custom-window-title": {
|
||||
"prompt": {
|
||||
"placeholder": "לדוגמה: שולחן כתיבה אגסי"
|
||||
}
|
||||
},
|
||||
"like-buttons": {
|
||||
"default": "ברירת מחדל",
|
||||
"force-show": "הפעל בכוח",
|
||||
@ -201,8 +206,8 @@
|
||||
"restart": "הפעל מחדש",
|
||||
"show": "הראה חלון",
|
||||
"tooltip": {
|
||||
"default": "יוטיוב מיוזיק",
|
||||
"with-song-info": "יוטיוב מיוזיק: {{artist}} - {{title}}"
|
||||
"default": "שולחן כתיבה אגסי",
|
||||
"with-song-info": "שולחן כתיב אגסי: {{יוצר}} - {{כותרת}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -212,7 +217,7 @@
|
||||
"name": "הגבר מהירות פרסומת"
|
||||
},
|
||||
"adblocker": {
|
||||
"description": "חסום את כל המודעות והמעקב מחוץ לקופסה",
|
||||
"description": "חסום את כל המודעות והמעקבים",
|
||||
"menu": {
|
||||
"blocker": "חוסם"
|
||||
},
|
||||
@ -244,6 +249,7 @@
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"label": "חוצץ",
|
||||
"submenu": {
|
||||
"buffer": "{{buffer}}"
|
||||
}
|
||||
@ -380,6 +386,11 @@
|
||||
},
|
||||
"templates": {
|
||||
"title": "פתח בחירת כתוביות"
|
||||
},
|
||||
"toast": {
|
||||
"caption-changed": "תרגום שונה ל {{שפה}}",
|
||||
"caption-disabled": "תרגום בוטל",
|
||||
"no-captions": "אין תרגום זמין לשיר הזה"
|
||||
}
|
||||
},
|
||||
"compact-sidebar": {
|
||||
@ -391,9 +402,11 @@
|
||||
"menu": {
|
||||
"advanced": "מתקדם"
|
||||
},
|
||||
"name": "התפיידות צלב[בית]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
"fade-in-duration": "תתפייד בזמן[מילישניות]",
|
||||
"fade-scaling": {
|
||||
"linear": "לינארי",
|
||||
"logarithmic": "לוגריתמי"
|
||||
|
||||
@ -237,7 +237,8 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "재생바 색조 변경 활성화"
|
||||
},
|
||||
"name": "앨범 컬러 기반 테마"
|
||||
},
|
||||
|
||||
@ -462,7 +462,7 @@
|
||||
"label": "Text stavu",
|
||||
"submenu": {
|
||||
"artist": "Aktuálne si prehráva {artist}",
|
||||
"pear-desktop": "Aktuálne prehráva Pear Desktop",
|
||||
"pear-desktop": "Počúvať Pear Desktop",
|
||||
"title": "Aktuálne si prehráva {song title}"
|
||||
}
|
||||
}
|
||||
@ -731,11 +731,85 @@
|
||||
"title": "Výber kvality videa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Umožňuje zmeniť kvalitu videa pomocou tlačidla v prekrytí videa",
|
||||
"name": "Zmena kvality videa",
|
||||
"renderer": {
|
||||
"quality-settings-button": {
|
||||
"label": "Otvoriť nastavenia kvality prehrávača"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrobbler": {
|
||||
"description": "Pridať podporu scrobbling (napr. last.fm, Listenbrainz)",
|
||||
"dialog": {
|
||||
"lastfm": {
|
||||
"auth-failed": {
|
||||
"message": "Nepodarilo sa autentifikovať s Last.fm\nSkryť vyskakovacie okno do ďalšieho reštartu.",
|
||||
"title": "Autentifikácia zlyhala"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"lastfm": {
|
||||
"api-settings": "Last.fm API Nastavenia"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": "Vložiť ListenBrainz používateľský token"
|
||||
},
|
||||
"scrobble-alternative-artist": "Použiť alternatívnych umelcov",
|
||||
"scrobble-alternative-title": "Použiť alternatívne názvy",
|
||||
"scrobble-other-media": "Scrobble iných médií"
|
||||
},
|
||||
"name": "Scrobbler",
|
||||
"prompt": {
|
||||
"lastfm": {
|
||||
"api-key": "Last.fm API kľúč",
|
||||
"api-secret": "Last.fm API tajomstvo"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": {
|
||||
"label": "Vlož svoj ListenBrainz používateľský token:",
|
||||
"title": "ListenBrainz token"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"description": "Povoľuje nastaviť globálne klávesové skratky pre prehrávanie (Prehrať/Pozastaviť/Ďalšie/Predošlé) a vypínať media OSD prepisovaním media kľúčov, zapne Ctrl/CMD + F pre vyhľadávanie, zapne Linux MPRIS podporu pre media kľúče a vlastné klávesové skratky pre pokročilých používateľov.",
|
||||
"menu": {
|
||||
"override-media-keys": "Prepísať Media Kľúče",
|
||||
"set-keybinds": "Globálne ovládanie skladieb"
|
||||
},
|
||||
"name": "Skratky (& MPRIS)",
|
||||
"prompt": {
|
||||
"keybind": {
|
||||
"keybind-options": {
|
||||
"next": "Ďalšia",
|
||||
"play-pause": "Prehrať / Pauza",
|
||||
"previous": "Predošlá"
|
||||
},
|
||||
"label": "Zvoliť globálne klávesové skratky na ovládanie skladieb:",
|
||||
"title": "Globálne klávesové skratky"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skip-disliked-songs": {
|
||||
"description": "Preskakuje skladby označené Nepáči sa",
|
||||
"name": "Preskakovať skladby označené Nepáči sa"
|
||||
},
|
||||
"skip-silences": {
|
||||
"description": "Automaticky preskakovať tiché časti v hudbe",
|
||||
"name": "Preskakuj tiché časti"
|
||||
},
|
||||
"sponsorblock": {
|
||||
"description": "Automaticky preskakuje nehudbné časti ako intro/outro, alebo tie časti videoklipov v ktorých nehrá hudba",
|
||||
"name": "Sponzorský blok"
|
||||
},
|
||||
"synced-lyrics": {
|
||||
"description": "Poskytuje synchronizované texty k skladbám, pričom používa poskytovateľov ako LRClib.",
|
||||
"errors": {
|
||||
"fetch": "⚠️\t\tPri získavaní textu sa vyskytla chyba. \n\tSkúste znova neskôr.",
|
||||
"not-found": "⚠️Pre túto skladbu nebol nájdený žiadny text."
|
||||
},
|
||||
"menu": {
|
||||
|
||||
@ -461,7 +461,9 @@
|
||||
"set-status-display-type": {
|
||||
"label": "Статус",
|
||||
"submenu": {
|
||||
"pear-desktop": "Відтворення з Pear Desktop"
|
||||
"artist": "Ви слухаєте {artist}",
|
||||
"pear-desktop": "Відтворення з Pear Desktop",
|
||||
"title": "Ви слухаєте {song title}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -731,7 +733,12 @@
|
||||
}
|
||||
},
|
||||
"description": "Дозволяє змінювати якість відео за допомогою кнопки на відео оверлеї",
|
||||
"name": "Зміна якості відео"
|
||||
"name": "Зміна якості відео",
|
||||
"renderer": {
|
||||
"quality-settings-button": {
|
||||
"label": "Відкрити налаштування якості плеєру"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrobbler": {
|
||||
"description": "Додає підтримку скроблінгу (last.fm, Listenbrainz тощо)",
|
||||
@ -750,6 +757,7 @@
|
||||
"listenbrainz": {
|
||||
"token": "Ввести токен користувача ListenBrainz"
|
||||
},
|
||||
"scrobble-alternative-artist": "Використати іншого виконавця",
|
||||
"scrobble-alternative-title": "Використовувати альтернативні назви",
|
||||
"scrobble-other-media": "Скробилити інші медіа"
|
||||
},
|
||||
@ -835,6 +843,14 @@
|
||||
"label": "Зробити текст пісні ідеально синхронізованим",
|
||||
"tooltip": "Обчисли до мілісекунд відображення наступного рядка (може мати невеликий вплив на продуктивність)"
|
||||
},
|
||||
"preferred-provider": {
|
||||
"label": "Пріорітетний Провайдер",
|
||||
"none": {
|
||||
"label": "Жоден",
|
||||
"tooltip": "Нема провайдера за замовчуванням"
|
||||
},
|
||||
"tooltip": "Оберіть якого провайдера використовувати за замовчуванням"
|
||||
},
|
||||
"romanization": {
|
||||
"label": "Транслітерувати текст пісень",
|
||||
"tooltip": "Якщо текст пісні іншою мовою, спробувати його відобразити латинською версією."
|
||||
@ -867,6 +883,27 @@
|
||||
"description": "Додає віджет TouchBar для користувачів macOS",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "Зробити вікно програми прозорим",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "Прозорість",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип",
|
||||
"submenu": {
|
||||
"acrylic": "Акриловий",
|
||||
"mica": "Міка",
|
||||
"none": "Жоден",
|
||||
"tabbed": "З роздільниками"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Прозорий Плеєр"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Інтеграція з плагіном Tuna для OBS",
|
||||
"name": "Tuna OBS"
|
||||
@ -898,7 +935,8 @@
|
||||
},
|
||||
"name": "Перемикач відео",
|
||||
"templates": {
|
||||
"button-song": "Пісня"
|
||||
"button-song": "Пісня",
|
||||
"button-video": "Відео"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
|
||||
@ -151,7 +151,9 @@
|
||||
"label": "بصری تبدیلیاں",
|
||||
"submenu": {
|
||||
"custom-window-title": {
|
||||
"label": "اپنی مرضی کا ونڈو عنوان",
|
||||
"prompt": {
|
||||
"label": "اپنی مرضی کا ونڈو عنوان درج کریں: (بند کرنے کے لیے خالی چھوڑ دیں)",
|
||||
"placeholder": "مثال: پیئر ڈیسک ٹاپ"
|
||||
}
|
||||
},
|
||||
@ -219,7 +221,8 @@
|
||||
"description": "شروغ سے تمام اشتہارات اور ٹریکنگ بلاک کردیں",
|
||||
"menu": {
|
||||
"blocker": "بلاکر"
|
||||
}
|
||||
},
|
||||
"name": "ایڈ بلاکر"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,7 +237,8 @@
|
||||
"submenu": {
|
||||
"percent": "{{ratio}}%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enable-seekbar": "启用进度条主题"
|
||||
},
|
||||
"name": "专辑配色主题"
|
||||
},
|
||||
@ -462,8 +463,8 @@
|
||||
"label": "状态文本",
|
||||
"submenu": {
|
||||
"artist": "在听 {artist}",
|
||||
"title": "在听 {song title}",
|
||||
"pear-desktop": "在听 Pear Desktop"
|
||||
"pear-desktop": "在听 Pear Desktop",
|
||||
"title": "在听 {song title}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -10,41 +10,32 @@ const COLOR_KEY = '--ytmusic-album-color';
|
||||
const DARK_COLOR_KEY = '--ytmusic-album-color-dark';
|
||||
const RATIO_KEY = '--ytmusic-album-color-ratio';
|
||||
|
||||
export default createPlugin<
|
||||
unknown,
|
||||
unknown,
|
||||
{
|
||||
color?: ColorInstance;
|
||||
darkColor?: ColorInstance;
|
||||
type Config = {
|
||||
enabled: boolean;
|
||||
ratio: number;
|
||||
enableSeekbar: boolean;
|
||||
};
|
||||
|
||||
playerPage: HTMLElement | null;
|
||||
navBarBackground: HTMLElement | null;
|
||||
ytmusicPlayerBar: HTMLElement | null;
|
||||
playerBarBackground: HTMLElement | null;
|
||||
sidebarBig: HTMLElement | null;
|
||||
sidebarSmall: HTMLElement | null;
|
||||
ytmusicAppLayout: HTMLElement | null;
|
||||
type Renderer = {
|
||||
getMixedColor(
|
||||
color: string,
|
||||
key: string,
|
||||
alpha?: number,
|
||||
ratioMultiply?: number,
|
||||
): string;
|
||||
updateColor(alpha: number): void;
|
||||
onConfigChange(newConfig: Config): void;
|
||||
};
|
||||
|
||||
getMixedColor(
|
||||
color: string,
|
||||
key: string,
|
||||
alpha?: number,
|
||||
ratioMultiply?: number,
|
||||
): string;
|
||||
updateColor(alpha: number): void;
|
||||
},
|
||||
{
|
||||
enabled: boolean;
|
||||
ratio: number;
|
||||
}
|
||||
>({
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.album-color-theme.name'),
|
||||
description: () => t('plugins.album-color-theme.description'),
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
ratio: 0.5,
|
||||
},
|
||||
enableSeekbar: true,
|
||||
} satisfies Config as Config,
|
||||
stylesheets: [style],
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
const ratioList = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
|
||||
@ -68,18 +59,28 @@ export default createPlugin<
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: t('plugins.album-color-theme.menu.enable-seekbar'),
|
||||
type: 'checkbox',
|
||||
checked: config.enableSeekbar,
|
||||
click(item) {
|
||||
setConfig({ enableSeekbar: item.checked });
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
renderer: {
|
||||
playerPage: null,
|
||||
navBarBackground: null,
|
||||
ytmusicPlayerBar: null,
|
||||
playerBarBackground: null,
|
||||
sidebarBig: null,
|
||||
sidebarSmall: null,
|
||||
ytmusicAppLayout: null,
|
||||
playerPage: null as HTMLElement | null,
|
||||
navBarBackground: null as HTMLElement | null,
|
||||
ytmusicPlayerBar: null as HTMLElement | null,
|
||||
playerBarBackground: null as HTMLElement | null,
|
||||
sidebarBig: null as HTMLElement | null,
|
||||
sidebarSmall: null as HTMLElement | null,
|
||||
ytmusicAppLayout: null as HTMLElement | null,
|
||||
color: null as ColorInstance | null,
|
||||
darkColor: null as ColorInstance | null,
|
||||
|
||||
async start({ getConfig }) {
|
||||
start() {
|
||||
this.playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
this.navBarBackground = document.querySelector<HTMLElement>(
|
||||
'#nav-bar-background',
|
||||
@ -94,14 +95,11 @@ export default createPlugin<
|
||||
'#mini-guide-background',
|
||||
);
|
||||
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const config = await getConfig();
|
||||
document.documentElement.style.setProperty(
|
||||
RATIO_KEY,
|
||||
`${~~(config.ratio * 100)}%`,
|
||||
);
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
async onPlayerApiReady(playerApi, { getConfig }) {
|
||||
const config = await getConfig();
|
||||
(this as Renderer).onConfigChange(config);
|
||||
|
||||
const fastAverageColor = new FastAverageColor();
|
||||
|
||||
document.addEventListener('videodatachange', async (event) => {
|
||||
@ -152,7 +150,7 @@ export default createPlugin<
|
||||
alpha = value;
|
||||
}
|
||||
}
|
||||
this.updateColor(alpha ?? 1);
|
||||
(this as Renderer).updateColor(alpha ?? 1);
|
||||
});
|
||||
},
|
||||
onConfigChange(config) {
|
||||
@ -160,8 +158,15 @@ export default createPlugin<
|
||||
RATIO_KEY,
|
||||
`${~~(config.ratio * 100)}%`,
|
||||
);
|
||||
if (config.enableSeekbar) document.body.classList.add('seekbar-theme');
|
||||
else document.body.classList.remove('seekbar-theme');
|
||||
},
|
||||
getMixedColor(color: string, key: string, alpha = 1, ratioMultiply) {
|
||||
getMixedColor(
|
||||
color: string,
|
||||
key: string,
|
||||
alpha = 1,
|
||||
ratioMultiply?: number,
|
||||
) {
|
||||
const keyColor = `rgba(var(${key}), ${alpha})`;
|
||||
|
||||
let colorRatio = `var(${RATIO_KEY}, 50%)`;
|
||||
@ -207,26 +212,39 @@ export default createPlugin<
|
||||
'--yt-spec-black-pure-alpha-80': 'rgba(0,0,0,0.8)',
|
||||
'--yt-spec-black-1-alpha-98': 'rgba(40,40,40,0.98)',
|
||||
'--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)',
|
||||
'--paper-toast-background-color': '#323232',
|
||||
'--ytmusic-search-background': '#030303',
|
||||
'--paper-slider-knob-color': '#f03',
|
||||
'--paper-dialog-background-color': '#212121',
|
||||
'--paper-progress-active-color-1': '#f03',
|
||||
'--paper-progress-active-color-2': '#ff2791',
|
||||
'--yt-spec-inverted-background': '#f3f3f3',
|
||||
'background': 'rgba(3, 3, 3)',
|
||||
'--ytmusic-background': 'rgba(3, 3, 3)',
|
||||
};
|
||||
|
||||
const colorKeyMap: Record<string, string> = {
|
||||
'background': DARK_COLOR_KEY,
|
||||
'--ytmusic-background': DARK_COLOR_KEY,
|
||||
};
|
||||
|
||||
const ratioMap: Record<string, number> = {
|
||||
'--paper-progress-active-color-1': 1.75,
|
||||
'--paper-progress-active-color-2': 1.75,
|
||||
'--yt-spec-inverted-background': 1.75,
|
||||
};
|
||||
|
||||
const getMixedColor = (this as Renderer).getMixedColor.bind(this);
|
||||
Object.entries(variableMap).map(([variable, color]) => {
|
||||
const key = colorKeyMap[variable] ?? COLOR_KEY;
|
||||
const ratio = ratioMap[variable] ?? undefined;
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
variable,
|
||||
this.getMixedColor(color, COLOR_KEY, alpha),
|
||||
getMixedColor(color, key, alpha, ratio),
|
||||
'important',
|
||||
);
|
||||
});
|
||||
|
||||
document.body.style.setProperty(
|
||||
'background',
|
||||
this.getMixedColor('rgba(3, 3, 3)', DARK_COLOR_KEY, alpha),
|
||||
'important',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--ytmusic-background',
|
||||
// #030303
|
||||
this.getMixedColor('rgba(3, 3, 3)', DARK_COLOR_KEY, alpha),
|
||||
'important',
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -81,3 +81,14 @@ ytmusic-browse-response[has-background]:not([disable-gradient]) .background-grad
|
||||
#background.immersive-background.style-scope.ytmusic-browse-response {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
ytmusic-search-box[is-bauhaus-sidenav-enabled] {
|
||||
--ytmusic-search-background: var(--ytmusic-color-black3) !important;
|
||||
}
|
||||
|
||||
.seekbar-theme #progress-bar.ytmusic-player-bar {
|
||||
--paper-slider-active-color: linear-gradient(to right, var(--paper-progress-active-color-1) 80%, var(--paper-progress-active-color-2) 100%) !important;
|
||||
--paper-slider-knob-color: var(--paper-progress-active-color-1) !important;
|
||||
--paper-slider-knob-start-color: var(--paper-progress-active-color-2) !important;
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
UniversalCache,
|
||||
Utils,
|
||||
YTNodes,
|
||||
Platform,
|
||||
} from '\u0079\u006f\u0075\u0074\u0075\u0062\u0065i.js';
|
||||
import is from 'electron-is';
|
||||
import filenamify from 'filenamify';
|
||||
@ -57,6 +58,22 @@ const ffmpeg = lazy(async () =>
|
||||
);
|
||||
const ffmpegMutex = new Mutex();
|
||||
|
||||
Platform.shim.eval = async (data: Types.BuildScriptResult, env: Record<string, Types.VMPrimative>) => {
|
||||
const properties = [];
|
||||
|
||||
if(env.n) {
|
||||
properties.push(`n: exportedVars.nFunction("${env.n}")`)
|
||||
}
|
||||
|
||||
if (env.sig) {
|
||||
properties.push(`sig: exportedVars.sigFunction("${env.sig}")`)
|
||||
}
|
||||
|
||||
const code = `${data.output}\nreturn { ${properties.join(', ')} }`;
|
||||
|
||||
return new Function(code)();
|
||||
}
|
||||
|
||||
let yt: Innertube;
|
||||
let win: BrowserWindow;
|
||||
let playingUrl: string;
|
||||
@ -131,7 +148,6 @@ export const onMainLoad = async ({
|
||||
|
||||
yt = await Innertube.create({
|
||||
cache: new UniversalCache(false),
|
||||
player_id: '0004de42',
|
||||
cookie: await getCookieFromWindow(win),
|
||||
generate_session_locally: true,
|
||||
fetch: getNetFetchAsFetch(),
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user