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

133 lines
3.4 KiB
TypeScript

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