Compare commits

..

9 Commits

Author SHA1 Message Date
1e9f32e248 Merge branch 'master' into synced-lyrics/netease 2025-12-04 09:26:15 +02:00
6c4ae0dbfa chore(i18n): Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (457 of 457 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2025-12-03 01:00:18 +01:00
f7aaa3377a chore(i18n): Translated using Weblate (Slovak)
Currently translated at 88.3% (403 of 456 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/sk/
2025-12-01 22:00:36 +01:00
ab1a0478cf chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 100.0% (456 of 456 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2025-12-01 22:00:35 +01:00
127f56905c fix(album-color-theme): improve theming consistency across UI elements (#4109)
Co-authored-by: 최민우 <chaeminu0711@gmail.com>
2025-12-01 16:25:14 +09:00
8d448c667f chore(i18n): Translated using Weblate (Urdu)
Currently translated at 25.0% (114 of 456 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ur/
2025-11-29 08:01:53 +01:00
e00f33eb2d chore(i18n): Translated using Weblate (Slovak)
Currently translated at 84.4% (385 of 456 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/sk/
2025-11-29 08:01:52 +01:00
8100804ced chore(i18n): Translated using Weblate (Hebrew)
Currently translated at 46.9% (214 of 456 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/he/
2025-11-28 06:02:01 +01:00
dbbdb63aa8 feat(synced-lyrics): NetEase provider 2025-11-26 23:23:00 +02:00
15 changed files with 581 additions and 114 deletions

View File

@ -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",
@ -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",
@ -166,7 +168,7 @@
"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.0", "playwright": "1.55.0",
"ts-morph": "27.0.0", "ts-morph": "27.0.0",

84
pnpm-lock.yaml generated
View File

@ -117,6 +117,9 @@ importers:
conf: conf:
specifier: 14.0.0 specifier: 14.0.0
version: 14.0.0 version: 14.0.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
custom-electron-prompt: custom-electron-prompt:
specifier: 1.5.8 specifier: 1.5.8
version: 1.5.8(electron@38.2.0) version: 1.5.8(electron@38.2.0)
@ -271,6 +274,9 @@ importers:
'@total-typescript/ts-reset': '@total-typescript/ts-reset':
specifier: 0.6.1 specifier: 0.6.1
version: 0.6.1 version: 0.6.1
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/electron-localshortcut': '@types/electron-localshortcut':
specifier: 3.1.3 specifier: 3.1.3
version: 3.1.3 version: 3.1.3
@ -338,8 +344,8 @@ importers:
specifier: 0.14.5 specifier: 0.14.5
version: 0.14.5(eslint@9.35.0)(typescript@5.9.2) version: 0.14.5(eslint@9.35.0)(typescript@5.9.2)
glob: glob:
specifier: 11.1.0 specifier: 11.0.3
version: 11.1.0 version: 11.0.3
node-gyp: node-gyp:
specifier: 11.4.2 specifier: 11.4.2
version: 11.4.2 version: 11.4.2
@ -1298,6 +1304,9 @@ packages:
'@types/cacheable-request@6.0.3': '@types/cacheable-request@6.0.3':
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} 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': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@ -1640,10 +1649,6 @@ packages:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'} engines: {node: '>=12'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0: ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2058,6 +2063,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
css-select@5.2.2: css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
@ -2785,12 +2793,12 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
glob@10.5.0: glob@10.4.5:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true hasBin: true
glob@11.1.0: glob@11.0.3:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
hasBin: true hasBin: true
@ -3426,8 +3434,8 @@ packages:
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.2: lru-cache@11.1.0:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
lru-cache@5.1.1: lru-cache@5.1.1:
@ -3538,8 +3546,8 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
minimatch@10.1.1: minimatch@10.0.3:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@3.1.2: minimatch@3.1.2:
@ -3845,8 +3853,8 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'} engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.1: path-scurry@2.0.0:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
path-to-regexp@3.3.0: path-to-regexp@3.3.0:
@ -4417,10 +4425,6 @@ packages:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
strip-bom@3.0.0: strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -5077,8 +5081,8 @@ snapshots:
'@electron/asar@4.0.1': '@electron/asar@4.0.1':
dependencies: dependencies:
commander: 13.1.0 commander: 13.1.0
glob: 11.1.0 glob: 11.0.3
minimatch: 10.1.1 minimatch: 10.0.3
'@electron/fuses@1.8.0': '@electron/fuses@1.8.0':
dependencies: dependencies:
@ -5429,7 +5433,7 @@ snapshots:
dependencies: dependencies:
string-width: 5.1.2 string-width: 5.1.2
string-width-cjs: string-width@4.2.3 string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.2 strip-ansi: 7.1.0
strip-ansi-cjs: strip-ansi@6.0.1 strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0 wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0 wrap-ansi-cjs: wrap-ansi@7.0.0
@ -5889,7 +5893,7 @@ snapshots:
'@ts-morph/common@0.28.0': '@ts-morph/common@0.28.0':
dependencies: dependencies:
minimatch: 10.1.1 minimatch: 10.0.3
path-browserify: 1.0.1 path-browserify: 1.0.1
tinyglobby: 0.2.14 tinyglobby: 0.2.14
@ -5926,6 +5930,8 @@ snapshots:
'@types/node': 24.3.0 '@types/node': 24.3.0
'@types/responselike': 1.0.3 '@types/responselike': 1.0.3
'@types/crypto-js@4.2.2': {}
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
@ -6284,8 +6290,6 @@ snapshots:
ansi-regex@6.1.0: {} ansi-regex@6.1.0: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0: ansi-styles@4.3.0:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
@ -6328,7 +6332,7 @@ snapshots:
js-yaml: 4.1.0 js-yaml: 4.1.0
json5: 2.2.3 json5: 2.2.3
lazy-val: 1.0.5 lazy-val: 1.0.5
minimatch: 10.1.1 minimatch: 10.0.3
plist: 3.1.0 plist: 3.1.0
resedit: 1.7.2 resedit: 1.7.2
semver: 7.7.2 semver: 7.7.2
@ -6599,7 +6603,7 @@ snapshots:
dependencies: dependencies:
'@npmcli/fs': 4.0.0 '@npmcli/fs': 4.0.0
fs-minipass: 3.0.3 fs-minipass: 3.0.3
glob: 10.5.0 glob: 10.4.5
lru-cache: 10.4.3 lru-cache: 10.4.3
minipass: 7.1.2 minipass: 7.1.2
minipass-collect: 2.0.1 minipass-collect: 2.0.1
@ -6774,7 +6778,7 @@ snapshots:
config-file-ts@0.2.8-rc1: config-file-ts@0.2.8-rc1:
dependencies: dependencies:
glob: 10.5.0 glob: 10.4.5
typescript: 5.9.2 typescript: 5.9.2
content-disposition@0.5.2: {} content-disposition@0.5.2: {}
@ -6805,6 +6809,8 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
crypto-js@4.2.0: {}
css-select@5.2.2: css-select@5.2.2:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
@ -7748,7 +7754,7 @@ snapshots:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
glob@10.5.0: glob@10.4.5:
dependencies: dependencies:
foreground-child: 3.3.1 foreground-child: 3.3.1
jackspeak: 3.4.3 jackspeak: 3.4.3
@ -7757,14 +7763,14 @@ snapshots:
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 1.11.1 path-scurry: 1.11.1
glob@11.1.0: glob@11.0.3:
dependencies: dependencies:
foreground-child: 3.3.1 foreground-child: 3.3.1
jackspeak: 4.1.1 jackspeak: 4.1.1
minimatch: 10.1.1 minimatch: 10.0.3
minipass: 7.1.2 minipass: 7.1.2
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 2.0.1 path-scurry: 2.0.0
glob@7.2.3: glob@7.2.3:
dependencies: dependencies:
@ -8393,7 +8399,7 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@11.2.2: {} lru-cache@11.1.0: {}
lru-cache@5.1.1: lru-cache@5.1.1:
dependencies: dependencies:
@ -8515,7 +8521,7 @@ snapshots:
mimic-response@3.1.0: {} mimic-response@3.1.0: {}
minimatch@10.1.1: minimatch@10.0.3:
dependencies: dependencies:
'@isaacs/brace-expansion': 5.0.0 '@isaacs/brace-expansion': 5.0.0
@ -8826,9 +8832,9 @@ snapshots:
lru-cache: 10.4.3 lru-cache: 10.4.3
minipass: 7.1.2 minipass: 7.1.2
path-scurry@2.0.1: path-scurry@2.0.0:
dependencies: dependencies:
lru-cache: 11.2.2 lru-cache: 11.1.0
minipass: 7.1.2 minipass: 7.1.2
path-to-regexp@3.3.0: {} path-to-regexp@3.3.0: {}
@ -9427,10 +9433,6 @@ snapshots:
dependencies: dependencies:
ansi-regex: 6.1.0 ansi-regex: 6.1.0
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.2
strip-bom@3.0.0: {} strip-bom@3.0.0: {}
strip-final-newline@2.0.0: {} strip-final-newline@2.0.0: {}

View File

@ -237,7 +237,8 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
} },
"enable-seekbar": "Enable seekbar theming"
}, },
"name": "Album Color Theme" "name": "Album Color Theme"
}, },

View File

@ -150,6 +150,11 @@
"visual-tweaks": { "visual-tweaks": {
"label": "תיקונים חזותיים", "label": "תיקונים חזותיים",
"submenu": { "submenu": {
"custom-window-title": {
"prompt": {
"placeholder": "לדוגמה: שולחן כתיבה אגסי"
}
},
"like-buttons": { "like-buttons": {
"default": "ברירת מחדל", "default": "ברירת מחדל",
"force-show": "הפעל בכוח", "force-show": "הפעל בכוח",
@ -201,8 +206,8 @@
"restart": "הפעל מחדש", "restart": "הפעל מחדש",
"show": "הראה חלון", "show": "הראה חלון",
"tooltip": { "tooltip": {
"default": "יוטיוב מיוזיק", "default": "שולחן כתיבה אגסי",
"with-song-info": "יוטיוב מיוזיק: {{artist}} - {{title}}" "with-song-info": "שולחן כתיב אגסי: {{יוצר}} - {{כותרת}}"
} }
} }
}, },
@ -212,7 +217,7 @@
"name": "הגבר מהירות פרסומת" "name": "הגבר מהירות פרסומת"
}, },
"adblocker": { "adblocker": {
"description": "חסום את כל המודעות והמעקב מחוץ לקופסה", "description": "חסום את כל המודעות והמעקבים",
"menu": { "menu": {
"blocker": "חוסם" "blocker": "חוסם"
}, },
@ -244,6 +249,7 @@
} }
}, },
"buffer": { "buffer": {
"label": "חוצץ",
"submenu": { "submenu": {
"buffer": "{{buffer}}" "buffer": "{{buffer}}"
} }
@ -380,6 +386,11 @@
}, },
"templates": { "templates": {
"title": "פתח בחירת כתוביות" "title": "פתח בחירת כתוביות"
},
"toast": {
"caption-changed": "תרגום שונה ל {{שפה}}",
"caption-disabled": "תרגום בוטל",
"no-captions": "אין תרגום זמין לשיר הזה"
} }
}, },
"compact-sidebar": { "compact-sidebar": {
@ -391,9 +402,11 @@
"menu": { "menu": {
"advanced": "מתקדם" "advanced": "מתקדם"
}, },
"name": "התפיידות צלב[בית]",
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
"fade-in-duration": "תתפייד בזמן[מילישניות]",
"fade-scaling": { "fade-scaling": {
"linear": "לינארי", "linear": "לינארי",
"logarithmic": "לוגריתמי" "logarithmic": "לוגריתמי"

View File

@ -237,7 +237,8 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
} },
"enable-seekbar": "재생바 색조 변경 활성화"
}, },
"name": "앨범 컬러 기반 테마" "name": "앨범 컬러 기반 테마"
}, },

View File

@ -462,7 +462,7 @@
"label": "Text stavu", "label": "Text stavu",
"submenu": { "submenu": {
"artist": "Aktuálne si prehráva {artist}", "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}" "title": "Aktuálne si prehráva {song title}"
} }
} }
@ -731,6 +731,13 @@
"title": "Výber kvality videa" "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": { "scrobbler": {
@ -771,12 +778,38 @@
"shortcuts": { "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.", "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": { "menu": {
"override-media-keys": "Prepísať Media Kľúče" "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": { "synced-lyrics": {
"description": "Poskytuje synchronizované texty k skladbám, pričom používa poskytovateľov ako LRClib.", "description": "Poskytuje synchronizované texty k skladbám, pričom používa poskytovateľov ako LRClib.",
"errors": { "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." "not-found": "⚠Pre túto skladbu nebol nájdený žiadny text."
}, },
"menu": { "menu": {

View File

@ -461,7 +461,9 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Статус", "label": "Статус",
"submenu": { "submenu": {
"pear-desktop": "Відтворення з Pear Desktop" "artist": "Ви слухаєте {artist}",
"pear-desktop": "Відтворення з Pear Desktop",
"title": "Ви слухаєте {song title}"
} }
} }
}, },
@ -731,7 +733,12 @@
} }
}, },
"description": "Дозволяє змінювати якість відео за допомогою кнопки на відео оверлеї", "description": "Дозволяє змінювати якість відео за допомогою кнопки на відео оверлеї",
"name": "Зміна якості відео" "name": "Зміна якості відео",
"renderer": {
"quality-settings-button": {
"label": "Відкрити налаштування якості плеєру"
}
}
}, },
"scrobbler": { "scrobbler": {
"description": "Додає підтримку скроблінгу (last.fm, Listenbrainz тощо)", "description": "Додає підтримку скроблінгу (last.fm, Listenbrainz тощо)",
@ -750,6 +757,7 @@
"listenbrainz": { "listenbrainz": {
"token": "Ввести токен користувача ListenBrainz" "token": "Ввести токен користувача ListenBrainz"
}, },
"scrobble-alternative-artist": "Використати іншого виконавця",
"scrobble-alternative-title": "Використовувати альтернативні назви", "scrobble-alternative-title": "Використовувати альтернативні назви",
"scrobble-other-media": "Скробилити інші медіа" "scrobble-other-media": "Скробилити інші медіа"
}, },
@ -835,6 +843,14 @@
"label": "Зробити текст пісні ідеально синхронізованим", "label": "Зробити текст пісні ідеально синхронізованим",
"tooltip": "Обчисли до мілісекунд відображення наступного рядка (може мати невеликий вплив на продуктивність)" "tooltip": "Обчисли до мілісекунд відображення наступного рядка (може мати невеликий вплив на продуктивність)"
}, },
"preferred-provider": {
"label": "Пріорітетний Провайдер",
"none": {
"label": "Жоден",
"tooltip": "Нема провайдера за замовчуванням"
},
"tooltip": "Оберіть якого провайдера використовувати за замовчуванням"
},
"romanization": { "romanization": {
"label": "Транслітерувати текст пісень", "label": "Транслітерувати текст пісень",
"tooltip": "Якщо текст пісні іншою мовою, спробувати його відобразити латинською версією." "tooltip": "Якщо текст пісні іншою мовою, спробувати його відобразити латинською версією."
@ -867,6 +883,27 @@
"description": "Додає віджет TouchBar для користувачів macOS", "description": "Додає віджет TouchBar для користувачів macOS",
"name": "TouchBar" "name": "TouchBar"
}, },
"transparent-player": {
"description": "Зробити вікно програми прозорим",
"menu": {
"opacity": {
"label": "Прозорість",
"submenu": {
"percent": "{{opacity}}%"
}
},
"type": {
"label": "Тип",
"submenu": {
"acrylic": "Акриловий",
"mica": "Міка",
"none": "Жоден",
"tabbed": "З роздільниками"
}
}
},
"name": "Прозорий Плеєр"
},
"tuna-obs": { "tuna-obs": {
"description": "Інтеграція з плагіном Tuna для OBS", "description": "Інтеграція з плагіном Tuna для OBS",
"name": "Tuna OBS" "name": "Tuna OBS"
@ -898,7 +935,8 @@
}, },
"name": "Перемикач відео", "name": "Перемикач відео",
"templates": { "templates": {
"button-song": "Пісня" "button-song": "Пісня",
"button-video": "Відео"
} }
}, },
"visualizer": { "visualizer": {

View File

@ -151,7 +151,9 @@
"label": "بصری تبدیلیاں", "label": "بصری تبدیلیاں",
"submenu": { "submenu": {
"custom-window-title": { "custom-window-title": {
"label": "اپنی مرضی کا ونڈو عنوان",
"prompt": { "prompt": {
"label": "اپنی مرضی کا ونڈو عنوان درج کریں: (بند کرنے کے لیے خالی چھوڑ دیں)",
"placeholder": "مثال: پیئر ڈیسک ٹاپ" "placeholder": "مثال: پیئر ڈیسک ٹاپ"
} }
}, },
@ -219,7 +221,8 @@
"description": "شروغ سے تمام اشتہارات اور ٹریکنگ بلاک کردیں", "description": "شروغ سے تمام اشتہارات اور ٹریکنگ بلاک کردیں",
"menu": { "menu": {
"blocker": "بلاکر" "blocker": "بلاکر"
} },
"name": "ایڈ بلاکر"
} }
} }
} }

View File

@ -237,7 +237,8 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
} },
"enable-seekbar": "启用进度条主题"
}, },
"name": "专辑配色主题" "name": "专辑配色主题"
}, },
@ -462,8 +463,8 @@
"label": "状态文本", "label": "状态文本",
"submenu": { "submenu": {
"artist": "在听 {artist}", "artist": "在听 {artist}",
"title": "在听 {song title}", "pear-desktop": "在听 Pear Desktop",
"pear-desktop": "在听 Pear Desktop" "title": "在听 {song title}"
} }
} }
}, },

View File

@ -10,41 +10,32 @@ const COLOR_KEY = '--ytmusic-album-color';
const DARK_COLOR_KEY = '--ytmusic-album-color-dark'; const DARK_COLOR_KEY = '--ytmusic-album-color-dark';
const RATIO_KEY = '--ytmusic-album-color-ratio'; const RATIO_KEY = '--ytmusic-album-color-ratio';
export default createPlugin< type Config = {
unknown, enabled: boolean;
unknown, ratio: number;
{ enableSeekbar: boolean;
color?: ColorInstance; };
darkColor?: ColorInstance;
playerPage: HTMLElement | null; type Renderer = {
navBarBackground: HTMLElement | null; getMixedColor(
ytmusicPlayerBar: HTMLElement | null; color: string,
playerBarBackground: HTMLElement | null; key: string,
sidebarBig: HTMLElement | null; alpha?: number,
sidebarSmall: HTMLElement | null; ratioMultiply?: number,
ytmusicAppLayout: HTMLElement | null; ): string;
updateColor(alpha: number): void;
onConfigChange(newConfig: Config): void;
};
getMixedColor( export default createPlugin({
color: string,
key: string,
alpha?: number,
ratioMultiply?: number,
): string;
updateColor(alpha: number): void;
},
{
enabled: boolean;
ratio: number;
}
>({
name: () => t('plugins.album-color-theme.name'), name: () => t('plugins.album-color-theme.name'),
description: () => t('plugins.album-color-theme.description'), description: () => t('plugins.album-color-theme.description'),
restartNeeded: false, restartNeeded: false,
config: { config: {
enabled: false, enabled: false,
ratio: 0.5, ratio: 0.5,
}, enableSeekbar: true,
} satisfies Config as Config,
stylesheets: [style], stylesheets: [style],
menu: async ({ getConfig, setConfig }) => { 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]; 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: { renderer: {
playerPage: null, playerPage: null as HTMLElement | null,
navBarBackground: null, navBarBackground: null as HTMLElement | null,
ytmusicPlayerBar: null, ytmusicPlayerBar: null as HTMLElement | null,
playerBarBackground: null, playerBarBackground: null as HTMLElement | null,
sidebarBig: null, sidebarBig: null as HTMLElement | null,
sidebarSmall: null, sidebarSmall: null as HTMLElement | null,
ytmusicAppLayout: 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.playerPage = document.querySelector<HTMLElement>('#player-page');
this.navBarBackground = document.querySelector<HTMLElement>( this.navBarBackground = document.querySelector<HTMLElement>(
'#nav-bar-background', '#nav-bar-background',
@ -94,14 +95,11 @@ export default createPlugin<
'#mini-guide-background', '#mini-guide-background',
); );
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); 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(); const fastAverageColor = new FastAverageColor();
document.addEventListener('videodatachange', async (event) => { document.addEventListener('videodatachange', async (event) => {
@ -152,7 +150,7 @@ export default createPlugin<
alpha = value; alpha = value;
} }
} }
this.updateColor(alpha ?? 1); (this as Renderer).updateColor(alpha ?? 1);
}); });
}, },
onConfigChange(config) { onConfigChange(config) {
@ -160,8 +158,15 @@ export default createPlugin<
RATIO_KEY, RATIO_KEY,
`${~~(config.ratio * 100)}%`, `${~~(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})`; const keyColor = `rgba(var(${key}), ${alpha})`;
let colorRatio = `var(${RATIO_KEY}, 50%)`; 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-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-98': 'rgba(40,40,40,0.98)',
'--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)', '--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]) => { Object.entries(variableMap).map(([variable, color]) => {
const key = colorKeyMap[variable] ?? COLOR_KEY;
const ratio = ratioMap[variable] ?? undefined;
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
variable, variable,
this.getMixedColor(color, COLOR_KEY, alpha), getMixedColor(color, key, alpha, ratio),
'important', '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',
);
}, },
}, },
}); });

View File

@ -81,3 +81,14 @@ ytmusic-browse-response[has-background]:not([disable-gradient]) .background-grad
#background.immersive-background.style-scope.ytmusic-browse-response { #background.immersive-background.style-scope.ytmusic-browse-response {
opacity: 0.6; 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;
}

View File

@ -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

View 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');
};

View File

@ -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',
} }

View File

@ -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;