feat: autotag scoring overhaul, dump-based explore index, and lyrics search
Consolidates in-progress work across autotag, explore, and library: - autotag: beets/Picard-informed scoring engine — ID-first matching, VA handling, recommendation tiers, and a merged distance/rank cascade, with an eval harness for regression tracking. - explore: offline MusicBrainz dump import/incremental refresh replaces the legacy tier crawl; index-first local search with fuzzy matching and a dedicated ranker; disk-free guards for dump downloads. - library: artist-credit extraction and matching. - lyrics: owned-library lyric search (FTS) with LRCLIB backfill. Also: rewrite README to be user-focused, and migrate upstream to git.ljones.me/yonlu/yellowjacket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
d5140395da
commit
65048401e8
117 files changed
+17033
-4767
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"yellowjacket/backend/coverart"
|
||||
"yellowjacket/backend/database"
|
||||
"yellowjacket/backend/library"
|
||||
)
|
||||
@@ -54,6 +55,7 @@ var fieldMap = map[string]string{
|
||||
"album": "album",
|
||||
"genre": "genre",
|
||||
"year": "year",
|
||||
"release_year": "release_year",
|
||||
"composer": "composer",
|
||||
"file_type": "file_type",
|
||||
"duration": "length_milliseconds",
|
||||
@@ -72,6 +74,7 @@ var fieldMap = map[string]string{
|
||||
// numericFields identifies fields that accept numeric operators.
|
||||
var numericFields = map[string]bool{
|
||||
"year": true,
|
||||
"release_year": true,
|
||||
"duration": true,
|
||||
"sample_rate": true,
|
||||
"bit_depth": true,
|
||||
@@ -552,7 +555,11 @@ const leanTrackQuery = `SELECT
|
||||
af.bitrate,
|
||||
af.file_size,
|
||||
af.play_count,
|
||||
COALESCE(af.last_played, '') AS last_played
|
||||
COALESCE(af.last_played, '') AS last_played,
|
||||
af.cover_art_path,
|
||||
af.artist_mbid,
|
||||
af.release_group_mbid,
|
||||
af.recording_mbid
|
||||
FROM (
|
||||
SELECT
|
||||
af.id,
|
||||
@@ -564,7 +571,17 @@ FROM (
|
||||
r.track_number,
|
||||
r.disc_number,
|
||||
COALESCE(rg.name, '') AS album,
|
||||
COALESCE(r.year, 0) AS year,
|
||||
-- Two year fields, matching the canonical track_metadata view:
|
||||
-- year — the album's original (first-release) year,
|
||||
-- the default users filter on. A 1977 album owned
|
||||
-- as a 2010s reissue still filters as 1977.
|
||||
-- release_year — the year of the specific release in the library
|
||||
-- (the file/release-group tag), e.g. 2013 for that
|
||||
-- reissue.
|
||||
-- Both fall back through rg.year → r.year so a track without full
|
||||
-- MusicBrainz data still gets a sensible year.
|
||||
COALESCE(rg.original_year, rg.year, r.year, 0) AS year,
|
||||
COALESCE(rg.year, r.year, 0) AS release_year,
|
||||
COALESCE(r.composer, '') AS composer,
|
||||
COALESCE(ft.extension, '') AS file_type,
|
||||
af.sample_rate,
|
||||
@@ -574,7 +591,15 @@ FROM (
|
||||
af.file_size,
|
||||
af.library_id,
|
||||
af.play_count,
|
||||
af.last_played
|
||||
af.last_played,
|
||||
COALESCE(ca.file_path, '') AS cover_art_path,
|
||||
COALESCE((SELECT a.mbid
|
||||
FROM artist_credit_artist aca
|
||||
JOIN artists a ON a.id = aca.artist_id
|
||||
WHERE aca.credit_id = ac.id
|
||||
LIMIT 1), '') AS artist_mbid,
|
||||
COALESCE(rg.mbid, '') AS release_group_mbid,
|
||||
COALESCE(r.mbid, '') AS recording_mbid
|
||||
FROM audio_files af
|
||||
LEFT JOIN recordings r ON af.recording_id = r.id
|
||||
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
|
||||
@@ -585,6 +610,7 @@ FROM (
|
||||
GROUP BY recording_id
|
||||
) rgr ON r.id = rgr.recording_id
|
||||
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
|
||||
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
|
||||
LEFT JOIN file_types ft ON af.file_type_id = ft.id
|
||||
) af`
|
||||
|
||||
@@ -752,6 +778,11 @@ func scanTracks(rows *sql.Rows) ([]library.Track, []int64, error) {
|
||||
fileSize int64
|
||||
playCount int64
|
||||
lastPlayed string
|
||||
|
||||
coverArtPath string
|
||||
artistMBID string
|
||||
releaseGroupMBID string
|
||||
recordingMBID string
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
@@ -761,31 +792,46 @@ func scanTracks(rows *sql.Rows) ([]library.Track, []int64, error) {
|
||||
&sampleRate, &bitDepth, &channels,
|
||||
&bitrate, &fileSize,
|
||||
&playCount, &lastPlayed,
|
||||
&coverArtPath, &artistMBID,
|
||||
&releaseGroupMBID, &recordingMBID,
|
||||
); err != nil {
|
||||
return nil, nil, fmt.Errorf(
|
||||
"could not scan smart playlist row: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
tracks = append(tracks, library.Track{
|
||||
TrackName: title,
|
||||
ArtistName: artistName,
|
||||
TrackLength: strconv.FormatInt(lengthMs, 10),
|
||||
FilePath: filePath,
|
||||
TrackNumber: trackNumber.Int64,
|
||||
DiscNumber: discNumber.Int64,
|
||||
Album: album,
|
||||
Year: year,
|
||||
Composer: composer,
|
||||
FileType: fileType,
|
||||
SampleRate: sampleRate,
|
||||
BitDepth: bitDepth,
|
||||
Channels: channels,
|
||||
Bitrate: bitrate,
|
||||
FileSize: fileSize,
|
||||
PlayCount: playCount,
|
||||
LastPlayed: lastPlayed,
|
||||
})
|
||||
track := library.Track{
|
||||
TrackName: title,
|
||||
ArtistName: artistName,
|
||||
TrackLength: strconv.FormatInt(lengthMs, 10),
|
||||
FilePath: filePath,
|
||||
TrackNumber: trackNumber.Int64,
|
||||
DiscNumber: discNumber.Int64,
|
||||
Album: album,
|
||||
Year: year,
|
||||
Composer: composer,
|
||||
FileType: fileType,
|
||||
SampleRate: sampleRate,
|
||||
BitDepth: bitDepth,
|
||||
Channels: channels,
|
||||
Bitrate: bitrate,
|
||||
FileSize: fileSize,
|
||||
PlayCount: playCount,
|
||||
LastPlayed: lastPlayed,
|
||||
ArtistMBID: artistMBID,
|
||||
ReleaseGroupMBID: releaseGroupMBID,
|
||||
RecordingMBID: recordingMBID,
|
||||
}
|
||||
|
||||
if coverArtPath != "" {
|
||||
urls := coverart.ResolveURLs(coverArtPath)
|
||||
track.CoverArtPath = urls.Original
|
||||
track.CoverArtSmall = urls.Small
|
||||
track.CoverArtMedium = urls.Medium
|
||||
track.CoverArtLarge = urls.Large
|
||||
}
|
||||
|
||||
tracks = append(tracks, track)
|
||||
recordingIDs = append(recordingIDs, recordingID.Int64)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user