Skip to content

Commit

Permalink
synced-lyrics(provider): YTMusic
Browse files Browse the repository at this point in the history
+ some bug fixes
  • Loading branch information
ArjixWasTaken committed Nov 1, 2024
1 parent 006e38f commit a93b318
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 57 deletions.
2 changes: 1 addition & 1 deletion src/plugins/synced-lyrics/parsers/lrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const LRC = {
const currentLine: LRCLine = {
time: `${minutes}:${seconds}:${milliseconds}`,
timeInMs,
text,
text: text.trim(),
duration: Infinity,
};

Expand Down
21 changes: 13 additions & 8 deletions src/plugins/synced-lyrics/providers/LRCLib.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { jaroWinkler } from '@skyra/jaro-winkler';

import type { LyricProvider } from '../types';
import type { LyricProvider, LyricResult, SearchSongInfo } from '../types';
import { config } from '../renderer/renderer';
import { LRC } from '../parsers/lrc';

export const LRCLib: LyricProvider = {
name: 'LRCLib',
baseUrl: 'https://lrclib.net',
export class LRCLib implements LyricProvider {
name = 'LRCLib';
baseUrl = 'https://lrclib.net';

async search({ title, artist, album, songDuration }) {
async search({
title,
artist,
album,
songDuration,
}: SearchSongInfo): Promise<LyricResult | null> {
let query = new URLSearchParams({
artist_name: artist,
track_name: title,
Expand Down Expand Up @@ -105,10 +110,10 @@ export const LRCLib: LyricProvider = {
return {
title: closestResult.trackName,
artists: closestResult.artistName.split(/[&,]/g),
lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' })),
lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' as const })),
};
},
} as const;
}
}

type LRCLIBSearchResponse = {
id: number;
Expand Down
19 changes: 10 additions & 9 deletions src/plugins/synced-lyrics/providers/LyricsGenius.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { LyricProvider } from '@/plugins/synced-lyrics/types';
import { LyricProvider, LyricResult, SearchSongInfo } from '../types';

const preloadedStateRegex = /__PRELOADED_STATE__ = JSON\.parse\('(.*?)'\);/;
const preloadHtmlRegex = /body":{"html":"(.*?)","children"/;

export const LyricsGenius: LyricProvider & { domParser: DOMParser } = {
name: 'Genius',
baseUrl: 'https://genius.com',

domParser: new DOMParser(),
export class LyricsGenius implements LyricProvider {
public name = 'Genius';
public baseUrl = 'https://genius.com';
private domParser = new DOMParser();

// prettier-ignore
async search({ title, artist }) {
async search({ title, artist }: SearchSongInfo): Promise<LyricResult | null> {
const query = new URLSearchParams({
q: `${artist} ${title}`,
page: '1',
Expand Down Expand Up @@ -73,13 +72,15 @@ export const LyricsGenius: LyricProvider & { domParser: DOMParser } = {
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;
Expand Down
20 changes: 11 additions & 9 deletions src/plugins/synced-lyrics/providers/Megalobiz.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LyricProvider } from '@/plugins/synced-lyrics/types';
import { LyricProvider, LyricResult, SearchSongInfo } from '../types';
import { jaroWinkler } from '@skyra/jaro-winkler';
import { LRC } from '../parsers/lrc';

Expand All @@ -12,18 +12,20 @@ const removeNoise = (text: string) => {
.replace(/\s+by$/, '');
};

export const Megalobiz: LyricProvider & { domParser: DOMParser } = {
name: 'Megalobiz',
baseUrl: 'https://www.megalobiz.com',
domParser: new DOMParser(),
export class Megalobiz implements LyricProvider {
public name = 'Megalobiz';
public baseUrl = 'https://www.megalobiz.com';
private domParser = new DOMParser();

// prettier-ignore
async search({ title, artist, songDuration }) {
async search({ title, artist, songDuration }: SearchSongInfo): Promise<LyricResult | null> {
const query = new URLSearchParams({
qry: `${artist} ${title}`,
});

const response = await fetch(`${this.baseUrl}/search/all?${query}`);
const response = await fetch(`${this.baseUrl}/search/all?${query}`, {
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}
Expand Down Expand Up @@ -95,8 +97,8 @@ export const Megalobiz: LyricProvider & { domParser: DOMParser } = {
artists: closestResult.artists,
lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' })),
};
},
};
}
}

interface MegalobizSearchResult {
title: string;
Expand Down
14 changes: 7 additions & 7 deletions src/plugins/synced-lyrics/providers/MusixMatch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { LyricProvider } from '@/plugins/synced-lyrics/types';
import { LyricProvider, LyricResult, SearchSongInfo } from '../types';

export const MusixMatch: LyricProvider = {
name: 'MusixMatch',
baseUrl: 'https://www.musixmatch.com/',
export class MusixMatch implements LyricProvider {
name = 'MusixMatch';
baseUrl = 'https://www.musixmatch.com/';

async search() {
async search({}: SearchSongInfo): Promise<LyricResult | null> {
throw new Error('Not implemented');
return null;
},
} as const;
}
}
179 changes: 170 additions & 9 deletions src/plugins/synced-lyrics/providers/YTMusic.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,172 @@
import { LyricProvider } from '@/plugins/synced-lyrics/types';
import { LyricProvider, LyricResult, SearchSongInfo } from '../types';

export const YTMusic: LyricProvider = {
name: 'YTMusic',
baseUrl: 'https://music.youtube.com/',
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};

async search() {
throw new Error('Not implemented');
return null;
},
} as const;
const client = {
clientName: '26',
clientVersion: '6.48.2',
};

export class YTMusic implements LyricProvider {
public name = 'YTMusic';
public baseUrl = 'https://music.youtube.com/';

// prettier-ignore
public async search({ videoId, title, artist }: SearchSongInfo): Promise<LyricResult | null> {
const data = await this.fetchNext(videoId);

const { tabs } = data?.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer ?? {};
if (!Array.isArray(tabs)) return null;

const lyricsTab = tabs.find((it) => {
const pageType = it?.tabRenderer?.endpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType;
return pageType === "MUSIC_PAGE_TYPE_TRACK_LYRICS";
});

if (!lyricsTab) return null;

const { browseId } = lyricsTab?.tabRenderer?.endpoint?.browseEndpoint ?? {};
if (!browseId) return null;

const { contents } = await this.fetchBrowse(browseId);
if (!contents) return null;

const synced = "elementRenderer" in contents
? (contents?.elementRenderer?.newElement?.type?.componentType?.model?.timedLyricsModel?.lyricsData?.timedLyricsData as SyncedLyricLine[])
?.map((it) => ({
time: this.millisToTime(parseInt(it.cueRange.startTimeMilliseconds)),
timeInMs: parseInt(it.cueRange.startTimeMilliseconds),
duration: parseInt(it.cueRange.endTimeMilliseconds) - parseInt(it.cueRange.startTimeMilliseconds),
text: it.lyricLine.trim() === '♪' ? '' : it.lyricLine.trim(),
status: 'upcoming' as const,
}))
: undefined;

const plain = !synced
? (contents?.messageRenderer?.text as PlainLyricsTextRenderer
)?.runs.map((it) => it.text).join('\n')
: undefined;

if (typeof plain === 'string' && plain === 'Lyrics not available') {
return null;
}

if (synced?.length && synced[0].timeInMs > 300) {
synced.unshift({
duration: 0,
text: '',
time: '00:00.00',
timeInMs: 0,
status: 'upcoming' as const,
});
}

return {
title,
artists: [artist],

lyrics: plain,
lines: synced,
}
}

private millisToTime(millis: number) {
const minutes = Math.floor(millis / 60000);
const seconds = Math.floor((millis - minutes * 60 * 1000) / 1000);
const remaining = (millis - minutes * 60 * 1000 - seconds * 1000) / 10;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${remaining.toString().padStart(2, '0')}`;
}

private ENDPOINT = 'https://youtubei.googleapis.com/youtubei/v1/';

private fetchNext(videoId: string) {
return fetch(this.ENDPOINT + 'next', {
headers,
method: 'POST',
body: JSON.stringify({
videoId,
context: { client },
}),
}).then((res) => res.json()) as Promise<NextData>;
}

private fetchBrowse(browseId: string) {
return fetch(this.ENDPOINT + 'browse', {
headers,
method: 'POST',
body: JSON.stringify({
browseId,
context: { client },
}),
}).then((res) => res.json()) as Promise<BrowseData>;
}
}

interface NextData {
contents: {
singleColumnMusicWatchNextResultsRenderer: {
tabbedRenderer: {
watchNextTabbedResultsRenderer: {
tabs: {
tabRenderer: {
endpoint: {
browseEndpoint: {
browseId: string;
browseEndpointContextSupportedConfigs: {
browseEndpointContextMusicConfig: {
pageType: string;
};
};
};
};
};
}[];
};
};
};
};
}

interface BrowseData {
contents: {
elementRenderer: {
newElement: {
type: {
componentType: {
model: {
timedLyricsModel: {
lyricsData: {
timedLyricsData: any;
};
};
};
};
};
};
};
messageRenderer: {
text: {
runs: any[];
};
};
};
}

interface SyncedLyricLine {
lyricLine: string;
cueRange: CueRange;
}

interface CueRange {
startTimeMilliseconds: string;
endTimeMilliseconds: string;
}

interface PlainLyricsTextRenderer {
runs: {
text: string;
}[];
}
32 changes: 23 additions & 9 deletions src/plugins/synced-lyrics/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { createMemo } from 'solid-js';
import { getSongInfo } from '@/providers/song-info-front';

export const providers = {
LRCLib,
LyricsGenius,
Megalobiz,
MusixMatch,
YTMusic,
YTMusic: new YTMusic(),
LRCLib: new LRCLib(),
LyricsGenius: new LyricsGenius(),
MusixMatch: new MusixMatch(),
Megalobiz: new Megalobiz(),
} as const;

export type ProviderName = keyof typeof providers;
Expand Down Expand Up @@ -68,10 +68,22 @@ interface SearchCache {
const searchCache = new Map<VideoId, SearchCache>();
export const fetchLyrics = (info: SongInfo) => {
if (searchCache.has(info.videoId)) {
const cache = searchCache.get(info.videoId);
if (cache && getSongInfo().videoId === info.videoId) {
const cache = searchCache.get(info.videoId)!;

if (cache.state === 'loading') {
setTimeout(() => {
fetchLyrics(info);
});
return;
}

console.log('Cache hit', cache?.state);

if (getSongInfo().videoId === info.videoId) {
// @ts-expect-error
setLyricsStore('lyrics', () => {
return { ...cache.data };
// weird bug with solid-js
return JSON.parse(JSON.stringify(cache.data));
});
}

Expand All @@ -85,8 +97,10 @@ export const fetchLyrics = (info: SongInfo) => {

searchCache.set(info.videoId, cache);
if (getSongInfo().videoId === info.videoId) {
// @ts-expect-error
setLyricsStore('lyrics', () => {
return { ...cache.data };
// weird bug with solid-js
return JSON.parse(JSON.stringify(cache.data));
});
}

Expand Down
Loading

0 comments on commit a93b318

Please sign in to comment.