Compare commits

..

5 Commits

Author SHA1 Message Date
d06896450c chore(i18n): Translated using Weblate (Chinese (Traditional 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_Hant/
2025-12-08 06:00:18 +01:00
4a59afc505 chore(i18n): Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
2025-12-08 06:00:18 +01:00
7b0d63b6cf chore(i18n): Translated using Weblate (Georgian)
Currently translated at 31.0% (142 of 457 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ka/
2025-12-06 19:00:19 +01:00
c734ffe70f chore(i18n): Translated using Weblate (Spanish)
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/es/
2025-12-04 23:00:20 +01:00
3d0ad69ddb chore(i18n): Translated using Weblate (Albanian)
Currently translated at 5.0% (23 of 457 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/sq/
2025-12-04 23:00:18 +01:00
11 changed files with 151 additions and 380 deletions

View File

@ -90,7 +90,6 @@
"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",
@ -145,7 +144,6 @@
"@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",

16
pnpm-lock.yaml generated
View File

@ -117,9 +117,6 @@ 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)
@ -274,9 +271,6 @@ 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
@ -1304,9 +1298,6 @@ 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==}
@ -2063,9 +2054,6 @@ 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==}
@ -5930,8 +5918,6 @@ 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
@ -6809,8 +6795,6 @@ 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

View File

@ -3,11 +3,11 @@
"console": {
"plugins": {
"execute-failed": "Error al ejecutar el complemento {{pluginName}}::{{contextName}}",
"executed-at-ms": "Complemento {{pluginName}}::{{contextName}} Ejecutó en {{ms}}ms",
"executed-at-ms": "Complemento {{pluginName}}::{{contextName}} se ejecutó en {{ms}}ms",
"initialize-failed": "Error al inicializar el complemento \"{{pluginName}}\"",
"load-all": "Cargando todos los complementos",
"load-failed": "Error al cargar el complemento \"{{pluginName}}\"",
"loaded": "Complementos \"{{pluginName}}\" cargado",
"loaded": "Complementos \"{{pluginName}}\" cargados",
"unload-failed": "No se ha podido descargar el complemento \"{{pluginName}}\"",
"unloaded": "Complemento \"{{pluginName}}\" descargado"
}
@ -237,7 +237,8 @@
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"enable-seekbar": "Habilitar temas a la barra de búsqueda"
},
"name": "Tema de color del álbum"
},
@ -462,8 +463,8 @@
"label": "Texto de estado",
"submenu": {
"artist": "Escuchando a {artist}",
"title": "Escuchando {song title}",
"pear-desktop": "Escuchando Pear Desktop"
"pear-desktop": "Escuchando Pear Desktop",
"title": "Escuchando {song title}"
}
}
},

View File

@ -44,25 +44,38 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "მენიუ დამალულია, გამოიყენეთ 'Alt', რათა გამოაჩინოთ ის (ან 'Escape' თუ იყენებთ აპლიკაციის შიგნითა მენიუს)"
"detail": "მენიუ დამალულია, გამოიყენეთ 'Alt', რათა გამოაჩინოთ ის (ან 'Escape' თუ იყენებთ აპლიკაციის შიგნითა მენიუს)",
"message": "დამალული მენიუ ჩართულია",
"title": "დამალული მენიუ ჩართულია"
},
"need-to-restart": {
"buttons": {
"later": "მოგვიანებით"
}
"later": "მოგვიანებით",
"restart-now": "გადატვირთვა ახლავე"
},
"detail": "„{{pluginName}}“ დანამატის ძალაში შესასვლელად გადატვირთვა საჭიროა",
"message": "\"{{pluginName}}\" საჭიროებს გადატვირთვას",
"title": "საჭიროებს გადატვირთვას"
},
"unresponsive": {
"buttons": {
"quit": "გასვლა",
"relaunch": "თავიდან გაშვება",
"wait": "მოცდა"
}
},
"detail": "ბოდიშს გიხდით მოუხერხელობისათვის! გთხოვთ აირჩიეთ რა უნდა გაკეთდეს:",
"message": "აპლიკაცია არ პასუხობს",
"title": "ფანჯარა არ პასუხობს"
},
"update-available": {
"buttons": {
"disable": "განახლებების გამორთვა",
"download": "გადმოწერა",
"ok": "დიახ"
}
},
"detail": "ახალი ვერსიაა ხელმისაწვდომი, მისი ჩამოტვირთვა შესაძლებელია {{downloadLink}}-დან",
"message": "ახალი ვერსია ხელმისაწვდომია",
"title": "განახლება ხელმისაწვდომია"
}
},
"menu": {
@ -70,19 +83,63 @@
"navigation": {
"label": "ნავიგაცია",
"submenu": {
"quit": "გასვლა"
"copy-current-url": "მიმდინარე URL-ის დაკოპირება",
"go-back": "უკან დაბრუნება",
"go-forward": "წინ გადასვლა",
"quit": "გასვლა",
"restart": "აპლიკაციის გადატვირთვა"
}
},
"options": {
"label": "მორგება",
"submenu": {
"language": {
"label": "ენა"
"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": "გსურთ დაგვეხმაროთ თარგმნაში? დააწკაპუნეთ აქ"
}
},
"resume-on-start": "აპლიკაციის თავიდან გაშვებისას ბოლო სიმღერა დაუკრას",
"single-instance-lock": "ერთჯერადი ინსტანციის საკეტი",
"start-at-login": "შესვლაზე დაწყება",
"starting-page": {
"label": "საწყისი გვერდი",
"unset": "მოხსნა"
},
"tray": {
"label": "უჯრა",
"submenu": {
"disabled": "გამორთულია"
}
@ -166,7 +223,7 @@
}
},
"amuse": {
"name": "Amuse"
"name": "გაკვირვება"
},
"api-server": {
"dialog": {
@ -309,6 +366,30 @@
}
}
}
},
"video-toggle": {
"menu": {
"mode": {
"label": "რეჟიმი",
"submenu": {
"custom": "მორგებული გადამრთველი",
"disabled": "გამორთულია",
"native": "ადგილობრივი გადართვა"
}
}
},
"name": "ვიდეოს გადართვა",
"templates": {
"button-song": "სიმღერა",
"button-video": "ვიდეო"
}
},
"visualizer": {
"description": "პლეიერს ვიზუალიზატორს უმატებს",
"menu": {
"visualizer-type": "ვიზუალიზატორის ტიპი"
},
"name": "ვიზუალიზატორი"
}
}
}

View File

@ -237,7 +237,8 @@
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"enable-seekbar": "Ativar personalização da barra de progresso"
},
"name": "Tema da cor do álbum"
},

View File

@ -3,7 +3,56 @@
"console": {
"plugins": {
"execute-failed": "Dështoi në ekzekutimin e plugin-it {{pluginName}}::{{contextName}}",
"executed-at-ms": "Shtojca {{pluginName}}::{{contextName}} u ekzekutua në {{ms}}ms"
"executed-at-ms": "Shtojca {{pluginName}}::{{contextName}} u ekzekutua në {{ms}}ms",
"initialize-failed": "Dështoi ekzekutimi i plugin-it \"{{pluginName}}\"",
"load-all": "Duke ngarkuar të gjithe plugins",
"load-failed": "Dështoi në ngarkimin e plugin-it \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" u ngarkua",
"unload-failed": "Shkarkimi i plugin-it \"{{pluginName}}\" dështoi",
"unloaded": "Plugin \"{{pluginName}}\" u shkarkua"
}
}
},
"language": {
"name": "Emri i gjuhës në anglisht. p.sh. japonisht, koreanisht, anglisht, rusisht"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Ngarkimi përfundoi. DevTools u hap."
},
"i18n": {
"loaded": "i18n i ngarkuar"
},
"second-instance": {
"receive-command": "U mor komanda mbi protokollin: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Skedari CSS \"{{cssFile}}\" nuk ekziston, duke u injoruar"
},
"unresponsive": {
"details": "Gabim i Papërgjigjes!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Duke pastruar memorien e përkohshme të aplikacionit"
},
"window": {
"tried-to-render-offscreen": "Dritarja u përpoq të renderohej jashtë ekranit, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menuja është e fshehur, përdorni 'Alt' për ta shfaqur (ose 'Escape' nëse përdorni Menunë brenda aplikacionit)",
"message": "Fshehja e menusë është aktivizuar",
"title": "Fshih Menunë Aktivizuar"
},
"need-to-restart": {
"buttons": {
"later": "Më vonë",
"restart-now": "Rinisni Tani"
},
"detail": "\"{{pluginName}}\" kërkon një restart që të hyjë në fuqi",
"message": "\"{{pluginName}}\" kerkon restart"
}
}
}

View File

@ -237,7 +237,8 @@
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"enable-seekbar": "啟用進度條主題樣式"
},
"name": "隨歌曲色調變更主題"
},

View File

@ -77,11 +77,10 @@ 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

View File

@ -1,340 +0,0 @@
// Code adapted from https://greasyfork.org/en/scripts/548724-youtube-music-spotify-%E7%BD%91%E6%98%93%E4%BA%91%E6%AD%8C%E8%AF%8D%E6%98%BE%E7%A4%BA
// which is licenced under the MIT licence
import CryptoJS from 'crypto-js';
import { jaroWinkler } from '@skyra/jaro-winkler';
import { z } from 'zod';
import { LRC } from '../parsers/lrc';
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
const EAPI_AES_KEY = 'e82ckenh8dichen8';
const EAPI_ENCODE_KEY = '3go8&$8*3*3h0k(2)2';
const EAPI_CHECK_TOKEN =
'9ca17ae2e6ffcda170e2e6ee8ad85dba908ca4d74da9ac8ea2d44e938f9eadc66da5a8979af572a5a9b68ac12af0feaec3b92aa69af9b1d372f6b8adccb35e968b9bb6c14f908d0099fb6ff48efdacd361f5b6ee9e';
const EAPI_BASE_HEADERS = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) NeteaseMusicDesktop/3.0.14.2534',
};
const EAPI_BASE_COOKIES = {
os: 'osx',
appver: '3.0.14',
requestId: 0,
osver: '15.6.1',
};
const artistSchema = z.object({ id: z.number(), name: z.string() });
const songSchema = z.object({
resourceId: z.coerce.number(),
baseInfo: z.object({
simpleSongData: z.object({
name: z.string(),
ar: z.array(artistSchema).optional(),
dt: z.number(),
}),
}),
});
const searchResponseDataSchema = z.object({
resources: z.array(songSchema).default([]),
});
const searchResponseSchema = z.object({
code: z.number(),
message: z.string(),
data: searchResponseDataSchema,
});
type Song = z.infer<typeof songSchema>;
const lyricPartSchema = z.object({ lyric: z.string().nullable() });
const lyricResponseSchema = z.object({
lrc: lyricPartSchema.optional(),
tlyric: lyricPartSchema.optional(),
romalrc: lyricPartSchema.optional(),
});
export class Netease implements LyricProvider {
name = 'Netease';
baseUrl = 'https://interface.music.163.com';
cookies: Record<string, string> = {};
initialized = false;
private encode(id: string): string {
// XOR step (unchanged)
let xoredString = '';
for (let i = 0; i < id.length; i++) {
const charCode =
id.charCodeAt(i) ^
EAPI_ENCODE_KEY.charCodeAt(i % EAPI_ENCODE_KEY.length);
xoredString += String.fromCharCode(charCode);
}
// MD5 -> Base64 using crypto-js
const hash = CryptoJS.MD5(CryptoJS.enc.Latin1.parse(xoredString)).toString(
CryptoJS.enc.Base64,
);
// Build a binary WordArray for "id hash"
const combinedWordArray = CryptoJS.enc.Latin1.parse(id + ' ' + hash);
// Convert to Base64 (replaces Buffer.from(...).toString("base64"))
return CryptoJS.enc.Base64.stringify(combinedWordArray);
}
private async register() {
const deviceId = '7B79802670C7A45DB9091976D71E0AE829E28926C6C34A1B8644';
const username = this.encode(deviceId);
try {
await this.eapi('/register/anonimous', { username }, { _nmclfl: '1' });
this.initialized = true;
} catch (e) {
throw new Error(`Registration failed: ${e}`);
}
}
private async eapi(
path: string,
data: Record<string, unknown> = {},
params: Record<string, string> = {},
) {
const header = { ...EAPI_BASE_COOKIES };
const bodyData = { ...data, header: JSON.stringify(header) };
const body = JSON.stringify(bodyData);
const sign = CryptoJS.MD5(
`nobody/api${path}use${body}md5forencrypt`,
).toString();
const payload = `/api${path}-36cd479b6b5-${body}-36cd479b6b5-${sign}`;
const key = CryptoJS.enc.Utf8.parse(EAPI_AES_KEY);
const encrypted = CryptoJS.AES.encrypt(payload, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}).ciphertext.toString(CryptoJS.enc.Hex);
const cookieString = Object.entries({ ...this.cookies })
.map(([k, v]) => `${k}=${v}`)
.join('; ');
const queryStr = new URLSearchParams(params).toString();
const url = `${this.baseUrl}/eapi${path}${queryStr ? `?${queryStr}` : ''}`;
const response = await fetch(url, {
method: 'POST',
headers: {
...EAPI_BASE_HEADERS,
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': cookieString,
},
body: `params=${encodeURIComponent(encrypted.toUpperCase())}`,
});
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
const cookieStrings = setCookieHeader.split(/,(?=\s*[^=;\s]+=)/);
for (const cookieStr of cookieStrings) {
const parts = cookieStr.split(';')[0].split('=');
if (parts.length === 2) {
this.cookies[parts[0].trim()] = parts[1].trim();
}
}
}
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
const json = await response.json();
z.object({ code: z.literal(200) }).parse(json);
return json;
}
private async searchSongs(keyword: string, limit = 10): Promise<Song[]> {
const response = await this.eapi(
'/search/song/list/page',
{
offset: '0',
scene: 'NORMAL',
needCorrect: 'true',
checkToken: EAPI_CHECK_TOKEN,
keyword,
limit: limit.toString(),
verifyId: 1,
},
{
_nmclfl: '1',
},
);
const parsed = searchResponseSchema.parse(response);
return parsed.data?.resources || [];
}
private async getLyric(id: number) {
const response = await this.eapi(
'/song/lyric/v1',
{
id,
tv: '-1',
yv: '-1',
rv: '-1',
lv: '-1',
verifyId: 1,
},
{
_nmclfl: '1',
},
);
return lyricResponseSchema.parse(response);
}
private splitTitle(title: string): string[] {
const masterPattern =
/(?:[「『](?<content>.+?)[」』])|(?:【.*?】|〖.*?〗|\(.*?\)|.*?)|(?<delimiter>\s+-\s+|\s*[/|:|│]\s*)/i;
const noiseWords = /\b(MV|PV)\b|\b(?:covered by|feat?|ft?)\b.+/gi;
const parse = (str: string): string[] => {
if (!str?.trim()) return [];
const match = str.match(masterPattern);
if (!match || match.index === undefined) return [str];
const before = str.substring(0, match.index);
const after = str.substring(match.index + match[0].length);
const { delimiter, content } = match.groups || {};
if (delimiter && (before.trim().length < 2 || after.trim().length < 2)) {
const remaining = parse(after);
return [
before + match[0] + (remaining[0] || ''),
...remaining.slice(1),
];
}
return [...parse(before), ...(content ? [content] : []), ...parse(after)];
};
return [
...new Set(
parse(title)
.map((p) => p.replace(noiseWords, '').trim())
.filter((p) => p.length > 0),
),
];
}
async search({
title,
artist,
songDuration,
}: SearchSongInfo): Promise<LyricResult | null> {
if (!this.initialized) {
await this.register();
}
const parts = this.splitTitle(title);
if (parts.length === 0) {
parts.push(title);
}
const keywords = [...parts];
if (parts[0] !== artist) keywords.push(`${parts[0]} ${artist}`);
const results = await Promise.all(
keywords.map((kw) => this.searchSongs(kw, 10)),
);
const calcTitleScore = (searchTitle: string) => {
let avgScore = 0;
parts.forEach((part, idx) => {
let weight = 1 / (idx * 2 + 1); // Earlier parts have higher weight
if (searchTitle.startsWith(part)) weight *= 2;
// Bonus for prefix match
else if (searchTitle.includes(part)) weight *= 1.5; // Bonus for substring match
avgScore += (jaroWinkler(part, searchTitle) * weight) / parts.length;
});
const score = Math.max(jaroWinkler(title, searchTitle), avgScore);
return score;
};
const artists = artist.split(/[&,]/g).map((i) => i.trim());
const filteredResults = [];
for (const result of results.flat()) {
const {
baseInfo: {
simpleSongData: { name, ar: itemArtists },
},
} = result;
const permutations = [];
for (const artistA of artists) {
for (const artistB of itemArtists ?? []) {
permutations.push([
artistA.toLowerCase(),
artistB.name.toLowerCase(),
]);
}
}
for (const artistA of itemArtists ?? []) {
for (const artistB of artists) {
permutations.push([
artistA.name.toLowerCase(),
artistB.toLowerCase(),
]);
}
}
const ratio =
calcTitleScore(name) +
Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y)));
if (ratio < 1.8) continue;
filteredResults.push(result);
}
const closestResult = filteredResults[0];
if (!closestResult) {
return null;
}
if (
Math.abs(closestResult.baseInfo.simpleSongData.dt / 1000 - songDuration) >
15
) {
return null;
}
const lyric = await this.getLyric(closestResult.resourceId);
if (!lyric || !lyric.lrc?.lyric) return null;
const lyrics = stripMetadata(lyric.lrc.lyric);
const lines = LRC.parse(lyrics).lines.map((l) => ({
...l,
status: 'upcoming' as const,
}));
if (lines.length === 0 && !lyrics.trim()) return null;
return {
title: closestResult.baseInfo.simpleSongData.name,
artists:
closestResult.baseInfo.simpleSongData.ar?.map((a) => a.name) ?? [],
lines,
lyrics: lyrics,
};
}
}
const stripMetadata = (lyrics: string) => {
return lyrics
.split('\n')
.filter((line) => {
if (!line.includes('{')) return true;
try {
JSON.parse(line);
return false;
} catch {}
return true;
})
.join('\n');
};

View File

@ -7,7 +7,6 @@ export enum ProviderNames {
LRCLib = 'LRCLib',
MusixMatch = 'MusixMatch',
LyricsGenius = 'LyricsGenius',
NetEase = 'NetEase',
// Megalobiz = 'Megalobiz',
}

View File

@ -3,13 +3,11 @@ 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;