Files
youtube-music/src/plugins/synced-lyrics/providers/LRCLib.ts
2024-12-25 07:44:29 +09:00

138 lines
3.5 KiB
TypeScript

import { jaroWinkler } from '@skyra/jaro-winkler';
import { config } from '../renderer/renderer';
import { LRC } from '../parsers/lrc';
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
export class LRCLib implements LyricProvider {
name = 'LRCLib';
baseUrl = 'https://lrclib.net';
async search({
title,
artist,
album,
songDuration,
}: SearchSongInfo): Promise<LyricResult | null> {
let query = new URLSearchParams({
artist_name: artist,
track_name: title,
});
query.set('album_name', album!);
if (query.get('album_name') === 'undefined') {
query.delete('album_name');
}
let url = `${this.baseUrl}/api/search?${query.toString()}`;
let response = await fetch(url);
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
let data = (await response.json()) as LRCLIBSearchResponse;
if (!data || !Array.isArray(data)) {
throw new Error(`Expected an array, instead got ${typeof data}`);
}
if (data.length === 0) {
if (!config()?.showLyricsEvenIfInexact) {
return null;
}
query = new URLSearchParams({ q: title });
url = `${this.baseUrl}/api/search?${query.toString()}`;
response = await fetch(url);
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
data = (await response.json()) as LRCLIBSearchResponse;
if (!Array.isArray(data)) {
throw new Error(`Expected an array, instead got ${typeof data}`);
}
}
const filteredResults = [];
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());
const permutations = [];
for (const artistA of artists) {
for (const artistB of itemArtists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}
for (const artistA of itemArtists) {
for (const artistB of artists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}
const ratio = Math.max(
...permutations.map(([x, y]) => jaroWinkler(x, y)),
);
if (ratio <= 0.9) continue;
filteredResults.push(item);
}
filteredResults.sort(({ duration: durationA }, { duration: durationB }) => {
const left = Math.abs(durationA - songDuration);
const right = Math.abs(durationB - songDuration);
return left - right;
});
const closestResult = filteredResults[0];
if (!closestResult) {
return null;
}
if (Math.abs(closestResult.duration - songDuration) > 15) {
return null;
}
if (closestResult.instrumental) {
return null;
}
const raw = closestResult.syncedLyrics;
const plain = closestResult.plainLyrics;
if (!raw && !plain) {
return null;
}
return {
title: closestResult.trackName,
artists: closestResult.artistName.split(/[&,]/g),
lines: raw
? LRC.parse(raw).lines.map((l) => ({
...l,
status: 'upcoming' as const,
}))
: undefined,
lyrics: plain,
};
}
}
type LRCLIBSearchResponse = {
id: number;
name: string;
trackName: string;
artistName: string;
albumName: string;
duration: number;
instrumental: boolean;
plainLyrics: string;
syncedLyrics: string;
}[];