mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 04:11:47 +00:00
feat(synced-lyrics): multiple lyric sources (#2383)
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
132
src/plugins/synced-lyrics/providers/LyricsGenius.ts
Normal file
132
src/plugins/synced-lyrics/providers/LyricsGenius.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
|
||||
|
||||
const preloadedStateRegex = /__PRELOADED_STATE__ = JSON\.parse\('(.*?)'\);/;
|
||||
const preloadHtmlRegex = /body":{"html":"(.*?)","children"/;
|
||||
|
||||
export class LyricsGenius implements LyricProvider {
|
||||
public name = 'Genius';
|
||||
public baseUrl = 'https://genius.com';
|
||||
private domParser = new DOMParser();
|
||||
|
||||
// prettier-ignore
|
||||
async search({ title, artist }: SearchSongInfo): Promise<LyricResult | null> {
|
||||
const query = new URLSearchParams({
|
||||
q: `${artist} ${title}`,
|
||||
page: '1',
|
||||
per_page: '10',
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/api/search/song?${query}`);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as LyricsGeniusSearch;
|
||||
const hits = data.response.sections[0].hits;
|
||||
|
||||
hits.sort(
|
||||
({
|
||||
result: {
|
||||
title: titleA,
|
||||
primary_artist: { name: artistA },
|
||||
},
|
||||
},
|
||||
{
|
||||
result: {
|
||||
title: titleB,
|
||||
primary_artist: { name: artistB },
|
||||
},
|
||||
}) => {
|
||||
const pointsA = (titleA === title ? 1 : 0) + (artistA.includes(artist) ? 1 : 0);
|
||||
const pointsB = (titleB === title ? 1 : 0) + (artistB.includes(artist) ? 1 : 0);
|
||||
|
||||
return pointsB - pointsA;
|
||||
},
|
||||
);
|
||||
|
||||
const closestHit = hits.at(0);
|
||||
if (!closestHit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { result: { path } } = closestHit;
|
||||
|
||||
const html = await fetch(`${this.baseUrl}${path}`).then((res) => res.text());
|
||||
const doc = this.domParser.parseFromString(html, 'text/html');
|
||||
|
||||
const preloadedStateScript = Array.prototype.find.call(doc.querySelectorAll('script'), (script: HTMLScriptElement) => {
|
||||
return script.textContent?.includes('window.__PRELOADED_STATE__');
|
||||
}) as HTMLScriptElement;
|
||||
|
||||
const preloadedState = preloadedStateScript.textContent?.match(preloadedStateRegex)?.[1]?.replace(/\\"/g, '"');
|
||||
|
||||
const lyricsHtml = preloadedState?.match(preloadHtmlRegex)?.[1]
|
||||
?.replace(/\\\//g, '/')
|
||||
?.replace(/\\\\/g, '\\')
|
||||
?.replace(/\\n/g, '\n')
|
||||
?.replace(/\\'/g, "'")
|
||||
?.replace(/\\"/g, '"');
|
||||
|
||||
if (!lyricsHtml) throw new Error('Failed to extract lyrics from preloaded state.');
|
||||
|
||||
const lyricsDoc = this.domParser.parseFromString(lyricsHtml, 'text/html');
|
||||
const lyrics = lyricsDoc.body.innerText;
|
||||
|
||||
if (lyrics.trim().toLowerCase().replace(/[[\]]/g, '') === 'instrumental') return null;
|
||||
|
||||
return {
|
||||
title: closestHit.result.title,
|
||||
artists: closestHit.result.primary_artists.map(({ name }) => name),
|
||||
lyrics,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface LyricsGeniusSearch {
|
||||
response: Response;
|
||||
}
|
||||
|
||||
interface Response {
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
interface Section {
|
||||
hits: {
|
||||
highlights: unknown[];
|
||||
index: string;
|
||||
type: string;
|
||||
result: Result;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Result {
|
||||
api_path: string;
|
||||
artist_names: string;
|
||||
full_title: string;
|
||||
id: number;
|
||||
instrumental: boolean;
|
||||
path: string;
|
||||
release_date_components: ReleaseDateComponents;
|
||||
title: string;
|
||||
title_with_featured: string;
|
||||
updated_by_human_at: number;
|
||||
url: string;
|
||||
featured_artists: Artist[];
|
||||
primary_artist: Artist;
|
||||
primary_artists: Artist[];
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
api_path: string;
|
||||
id: number;
|
||||
image_url: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ReleaseDateComponents {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}
|
||||
Reference in New Issue
Block a user