Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 151 additions & 112 deletions src/plugins/synced-lyrics/providers/LRCLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ export class LRCLib implements LyricProvider {
name = 'LRCLib';
baseUrl = 'https://lrclib.net';

async searchLyrics(query: URLSearchParams): Promise<LRCLIBSearchResponse> {
const response = await fetch(
`${this.baseUrl}/api/search?${query.toString()}`,
);

if (!response.ok) {
throw new Error(`bad HTTPStatus(${response.statusText})`);
}

const data = (await response.json()) as LRCLIBSearchResponse;
if (!data || !Array.isArray(data)) {
throw new Error(`Expected an array, instead got ${typeof data}`);
}
return data;
}

async search({
title,
alternativeTitle,
Expand All @@ -26,152 +42,174 @@ export class LRCLib implements LyricProvider {
if (query.get('album_name') === 'undefined') {
query.delete('album_name');
}
const queries = [query];

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;
}
const trackName = alternativeTitle || title;
let alternativeArtist = trackName.split(' - ')[0];
alternativeArtist =
alternativeArtist !== trackName ? alternativeArtist : '';

if (config()?.showLyricsEvenIfInexact) {
// Try to search with the alternative title (original language)
const trackName = alternativeTitle || title;
query = new URLSearchParams({ q: `${trackName}` });
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}`);
}
queries.push(query);

// If still no results, try with the original title as fallback
if (data.length === 0 && alternativeTitle) {
if (alternativeTitle) {
// If still no results, try with the original title as fallback
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}`);
}
queries.push(query);
}
}

const filteredResults = [];
for (const item of data) {
const { artistName } = item;
let filteredResults: LRCLIBSearchResponse = [];
const artists = artist.split(/[&,]/g).map((i) => i.trim());
if (alternativeArtist !== '') {
artists.push(alternativeArtist);
}

const artists = artist.split(/[&,]/g).map((i) => i.trim());
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
for (const query of queries) {
const data = await this.searchLyrics(query);
if (data.length == 0) continue;

// Try to match using artist name first
const permutations = [];
for (const artistA of artists) {
for (const artistB of itemArtists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}
for (const item of data) {
const { artistName } = item;
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());

for (const artistA of itemArtists) {
for (const artistB of artists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
// Try to match using artist name first
const permutations = [];
for (const artistA of artists) {
for (const artistB of itemArtists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}
}

let ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y)));
for (const artistA of itemArtists) {
for (const artistB of artists) {
permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]);
}
}

// If direct artist match is below threshold and we have tags, try matching with tags
if (ratio <= 0.9 && tags && tags.length > 0) {
// Filter out the artist from tags to avoid duplicate comparisons
const filteredTags = tags.filter(
(tag) => tag.toLowerCase() !== artist.toLowerCase(),
let ratio = Math.max(
...permutations.map(([x, y]) => jaroWinkler(x, y)),
);

const tagPermutations = [];
// Compare each tag with each item artist
for (const tag of filteredTags) {
// If direct artist match is below threshold, and we have tags, try matching with tags
if (ratio <= 0.9 && tags && tags.length > 0) {
// Filter out the artist from tags to avoid duplicate comparisons
const filteredTags = tags.filter(
(tag) => tag.toLowerCase() !== artist.toLowerCase(),
);

const tagPermutations = [];
// Compare each tag with each item artist
for (const tag of filteredTags) {
for (const itemArtist of itemArtists) {
tagPermutations.push([
tag.toLowerCase(),
itemArtist.toLowerCase(),
]);
}
}

// Compare each item artist with each tag
for (const itemArtist of itemArtists) {
tagPermutations.push([tag.toLowerCase(), itemArtist.toLowerCase()]);
for (const tag of filteredTags) {
tagPermutations.push([
itemArtist.toLowerCase(),
tag.toLowerCase(),
]);
}
}
}

// Compare each item artist with each tag
for (const itemArtist of itemArtists) {
for (const tag of filteredTags) {
tagPermutations.push([itemArtist.toLowerCase(), tag.toLowerCase()]);
if (tagPermutations.length > 0) {
const tagRatio = Math.max(
...tagPermutations.map(([x, y]) => jaroWinkler(x, y)),
);

// Use the best match ratio between direct artist match and tag match
ratio = Math.max(ratio, tagRatio);
}
}

if (tagPermutations.length > 0) {
const tagRatio = Math.max(
...tagPermutations.map(([x, y]) => jaroWinkler(x, y)),
);
if (ratio <= 0.9) continue;
filteredResults.push(item);
}

// Use the best match ratio between direct artist match and tag match
ratio = Math.max(ratio, tagRatio);
filteredResults = filteredResults.map((lrc) => {
if (album) {
lrc.albumRatio = jaroWinkler(
lrc.albumName.toLowerCase(),
album.toLowerCase(),
);
} else {
lrc.albumRatio = 0;
}
}

if (ratio <= 0.9) continue;
filteredResults.push(item);
}
return lrc;
});

filteredResults.sort(({ duration: durationA }, { duration: durationB }) => {
const left = Math.abs(durationA - songDuration);
const right = Math.abs(durationB - songDuration);
filteredResults = filteredResults.filter((lrc) => {
return Math.abs(lrc.duration - songDuration) < 15;
});

return left - right;
});
if (filteredResults.length == 0) continue;

const closestResult = filteredResults[0];
if (!closestResult) {
return null;
}
filteredResults.sort(
(
{ duration: durationA, syncedLyrics: lyricsA, albumRatio: arA },
{ duration: durationB, syncedLyrics: lyricsB, albumRatio: arB },
) => {
const hasLyricsA = lyricsA != null && lyricsA !== '';
const hasLyricsB = lyricsB != null && lyricsB !== '';

if (Math.abs(closestResult.duration - songDuration) > 15) {
return null;
}
if (hasLyricsA !== hasLyricsB) {
return hasLyricsB ? 1 : -1;
}
const durationDiffA = Math.abs(durationA - songDuration);
const durationDiffB = Math.abs(durationB - songDuration);

if (closestResult.instrumental) {
return null;
}
const normalizedDurationA = durationDiffA / songDuration;
const normalizedDurationB = durationDiffB / songDuration;

const weightAlbumRatio = 0.7;
const weightDuration = 0.3;

const scoreA =
weightAlbumRatio * arA! - weightDuration * normalizedDurationA;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '*' and '-'. Use parentheses to clarify the intended order of operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '*' and '-'. Use parentheses to clarify the intended order of operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '-' and '*'. Use parentheses to clarify the intended order of operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '-' and '*'. Use parentheses to clarify the intended order of operations.

const scoreB =
weightAlbumRatio * arB! - weightDuration * normalizedDurationB;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '*' and '-'. Use parentheses to clarify the intended order of operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '*' and '-'. Use parentheses to clarify the intended order of operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '-' and '*'. Use parentheses to clarify the intended order of operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <stylistic/no-mixed-operators> reported by reviewdog 🐶
Unexpected mix of '-' and '*'. Use parentheses to clarify the intended order of operations.


// Mayor score es mejor
return scoreB - scoreA;
},
);

const closestResult = filteredResults[0];

if (closestResult.instrumental) {
return null;
}

const raw = closestResult.syncedLyrics;
const plain = closestResult.plainLyrics;
if (!raw && !plain) {
continue;
}

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

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,
};
return null;
}
}

Expand All @@ -185,4 +223,5 @@ type LRCLIBSearchResponse = {
instrumental: boolean;
plainLyrics: string;
syncedLyrics: string;
albumRatio?: number;
}[];
Loading