-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Prioritize synced lyrics and improved artist value for lyrics on videos #3254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
72f9f70
6dd5ec0
0609628
4a0b493
b2412bb
505ce91
73260db
a4230c5
f98ce97
ebc9e61
11e2869
590a6b6
b7ad8e6
99adecf
4fa14a6
9cbcd19
c6528a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const scoreB = | ||
weightAlbumRatio * arB! - weightDuration * normalizedDurationB; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
// 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; | ||
} | ||
} | ||
|
||
|
@@ -185,4 +223,5 @@ type LRCLIBSearchResponse = { | |
instrumental: boolean; | ||
plainLyrics: string; | ||
syncedLyrics: string; | ||
albumRatio?: number; | ||
}[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unexpected mix of '*' and '-'. Use parentheses to clarify the intended order of operations.