synced-lyrics: make the lyrics search more reliable (#2343)

* fix: comparing multiple artists on a single track

* fix: get song info on startup

* chore: also split on commas

* chore: re-apply .toLowerCase() on the artist names

* chore: remove redundant code

* chore: attempt at improving the initial videodata

* oops

* eureka!

* stuff
This commit is contained in:
Angelos Bouklis
2024-08-28 06:21:58 +03:00
committed by GitHub
parent 9317e99f43
commit 6b1995145a
4 changed files with 78 additions and 22 deletions

View File

@ -17,7 +17,6 @@ export const [hadSecondAttempt, setHadSecondAttempt] = createSignal(false);
// prettier-ignore // prettier-ignore
export const [differentDuration, setDifferentDuration] = createSignal(false); export const [differentDuration, setDifferentDuration] = createSignal(false);
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
export let foundPlainTextLyrics = false;
export const extractTimeAndText = ( export const extractTimeAndText = (
line: string, line: string,
@ -47,7 +46,9 @@ export const extractTimeAndText = (
}; };
export const makeLyricsRequest = async (extractedSongInfo: SongInfo) => { export const makeLyricsRequest = async (extractedSongInfo: SongInfo) => {
setIsFetching(true);
setLineLyrics([]); setLineLyrics([]);
const songData: Parameters<typeof getLyricsList>[0] = { const songData: Parameters<typeof getLyricsList>[0] = {
title: `${extractedSongInfo.title}`, title: `${extractedSongInfo.title}`,
artist: `${extractedSongInfo.artist}`, artist: `${extractedSongInfo.artist}`,
@ -55,14 +56,18 @@ export const makeLyricsRequest = async (extractedSongInfo: SongInfo) => {
songDuration: extractedSongInfo.songDuration, songDuration: extractedSongInfo.songDuration,
}; };
const lyrics = await getLyricsList(songData); let lyrics;
try {
lyrics = await getLyricsList(songData);
} catch {}
setLineLyrics(lyrics ?? []); setLineLyrics(lyrics ?? []);
setIsFetching(false);
}; };
export const getLyricsList = async ( export const getLyricsList = async (
songData: Pick<SongInfo, 'title' | 'artist' | 'album' | 'songDuration'>, songData: Pick<SongInfo, 'title' | 'artist' | 'album' | 'songDuration'>,
): Promise<LineLyrics[] | null> => { ): Promise<LineLyrics[] | null> => {
setIsFetching(true);
setIsInstrumental(false); setIsInstrumental(false);
setHadSecondAttempt(false); setHadSecondAttempt(false);
setDifferentDuration(false); setDifferentDuration(false);
@ -73,26 +78,21 @@ export const getLyricsList = async (
track_name: songData.title, track_name: songData.title,
}); });
if (songData.album) { query.set('album_name', songData.album!);
query.set('album_name', songData.album); if (query.get('album_name') === 'undefined') {
query.delete('album_name');
} }
let url = `https://lrclib.net/api/search?${query.toString()}`; let url = `https://lrclib.net/api/search?${query.toString()}`;
let response = await fetch(url); let response = await fetch(url);
if (!response.ok) { if (!response.ok) {
setIsFetching(false);
setDebugInfo('Got non-OK response from server.'); setDebugInfo('Got non-OK response from server.');
return null; return null;
} }
let data = (await response.json().catch((e: Error) => { let data = await response.json() as LRCLIBSearchResponse;
setDebugInfo(`Error: ${e.message}\n\n${e.stack}`);
return null;
})) as LRCLIBSearchResponse | null;
if (!data || !Array.isArray(data)) { if (!data || !Array.isArray(data)) {
setIsFetching(false);
setDebugInfo('Unexpected server response.'); setDebugInfo('Unexpected server response.');
return null; return null;
} }
@ -108,14 +108,12 @@ export const getLyricsList = async (
response = await fetch(url); response = await fetch(url);
if (!response.ok) { if (!response.ok) {
setIsFetching(false);
setDebugInfo('Got non-OK response from server. (2)'); setDebugInfo('Got non-OK response from server. (2)');
return null; return null;
} }
data = (await response.json()) as LRCLIBSearchResponse; data = (await response.json()) as LRCLIBSearchResponse;
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
setIsFetching(false);
setDebugInfo('Unexpected server response. (2)'); setDebugInfo('Unexpected server response. (2)');
return null; return null;
} }
@ -125,12 +123,26 @@ export const getLyricsList = async (
const filteredResults = []; const filteredResults = [];
for (const item of data) { for (const item of data) {
if (!item.syncedLyrics) continue;
const { artist } = songData; const { artist } = songData;
const { artistName } = item; const { artistName } = item;
const ratio = jaroWinkler(artist.toLowerCase(), artistName.toLowerCase()); const artists = artist.split(/&,/).map((i) => i.trim());
const itemArtists = artistName.split(/&,/).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; if (ratio <= 0.9) continue;
filteredResults.push(item); filteredResults.push(item);
@ -146,23 +158,31 @@ export const getLyricsList = async (
const closestResult = filteredResults[0]; const closestResult = filteredResults[0];
if (!closestResult) { if (!closestResult) {
setIsFetching(false);
setDebugInfo('No search result matched the criteria.'); setDebugInfo('No search result matched the criteria.');
return null; return null;
} }
// setDebugInfo(JSON.stringify(closestResult, null, 4)); setDebugInfo(JSON.stringify(closestResult, null, 4));
if (Math.abs(closestResult.duration - duration) > 15) {
return null;
}
if (Math.abs(closestResult.duration - duration) > 15) return null;
if (Math.abs(closestResult.duration - duration) > 5) { if (Math.abs(closestResult.duration - duration) > 5) {
// show message that the timings may be wrong // show message that the timings may be wrong
setDifferentDuration(true); setDifferentDuration(true);
} }
setIsInstrumental(closestResult.instrumental); setIsInstrumental(closestResult.instrumental);
if (closestResult.instrumental) {
return null;
}
// Separate the lyrics into lines // Separate the lyrics into lines
const raw = closestResult.syncedLyrics.split('\n'); const raw = closestResult.syncedLyrics?.split('\n') ?? [];
if (!raw.length) {
return null;
}
// Add a blank line at the beginning // Add a blank line at the beginning
raw.unshift('[0:0.0] '); raw.unshift('[0:0.0] ');
@ -186,6 +206,5 @@ export const getLyricsList = async (
line.duration = next.timeInMs - line.timeInMs; line.duration = next.timeInMs - line.timeInMs;
} }
setIsFetching(false);
return syncedLyricList; return syncedLyricList;
}; };

View File

@ -200,6 +200,26 @@ export default (api: YoutubePlayer) => {
for (const status of ['playing', 'pause'] as const) { for (const status of ['playing', 'pause'] as const) {
video.addEventListener(status, playPausedHandlers[status]); video.addEventListener(status, playPausedHandlers[status]);
} }
if (!isNaN(video.duration)) {
const {
title, author,
video_id: videoId,
list: playlistId
} = api.getVideoData();
const { playerOverlays } = api.getWatchNextResponse();
sendSongInfo(<VideoDataChangeValue>{
title, author, videoId, playlistId,
isUpcoming: false,
lengthSeconds: video.duration,
loading: true,
uhhh: { playerOverlays }
});
}
} }
function sendSongInfo(videoData: VideoDataChangeValue) { function sendSongInfo(videoData: VideoDataChangeValue) {

View File

@ -5,3 +5,17 @@ export interface QueueResponse {
autoPlaying?: boolean; autoPlaying?: boolean;
continuation?: string; continuation?: string;
} }
export interface WatchNextResponse {
playerOverlays: {
playerOverlayRenderer: {
browserMediaSession: {
browserMediaSessionRenderer: {
album: {
runs: { text: string; }[]
}
}
}
}
};
}

View File

@ -3,6 +3,7 @@
import { VideoDetails } from './video-details'; import { VideoDetails } from './video-details';
import { GetPlayerResponse } from './get-player-response'; import { GetPlayerResponse } from './get-player-response';
import { PlayerAPIEvents } from './player-api-events'; import { PlayerAPIEvents } from './player-api-events';
import { WatchNextResponse } from '@/types/youtube-music-desktop-internal';
export interface YoutubePlayer { export interface YoutubePlayer {
getInternalApiInterface: <Parameters extends unknown[], Return>( getInternalApiInterface: <Parameters extends unknown[], Return>(
@ -427,4 +428,6 @@ export interface YoutubePlayer {
addEmbedsConversionTrackingParams: <Parameters extends unknown[], Return>( addEmbedsConversionTrackingParams: <Parameters extends unknown[], Return>(
...params: Parameters ...params: Parameters
) => Return; ) => Return;
getWatchNextResponse(): WatchNextResponse;
} }