wip on autotagging

This commit is contained in:
2026-05-01 11:52:50 -04:00
parent 5cf019a0ac
commit d5140395da
295 changed files with 11105 additions and 40714 deletions
+64 -124
View File
@@ -9,7 +9,6 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -34,24 +33,34 @@ const (
)
// CoverArtProxy fetches and caches cover art thumbnails locally.
// It checks three sources in order:
// 1. Local library cover art (instant, matched by album+artist name)
// 2. Disk cache from a previous CAA fetch (instant)
// 3. Cover Art Archive network fetch (slow, cached to disk)
// It checks two sources in order:
// 1. Disk cache from a previous CAA fetch (instant)
// 2. Cover Art Archive network fetch (slow, cached to disk)
//
// The proxy used to also consult the local library's cover_art
// table by album/artist name, but that path conflated externally-
// fetched art with audio-file embedded ID3 art (the cover_art
// table writes both with is_embedded=true, so they're
// indistinguishable downstream). For autotag review and explore
// browsing we want the canonical CAA cover, not whatever bytes
// happen to be tagged on a user's local file — so the library
// lookup was removed. The user's own library views (cover grid,
// album page) still display embedded art via a separate code path
// that reads cover_art.file_path directly, which is fine because
// that's their library, not Explore's view of MB.
type CoverArtProxy struct {
db *database.DB
cacheDir string
client *http.Client
limiter *RateLimiter
mu sync.Mutex // serializes disk writes
libOnce sync.Once
libIndex map[string]string // "album\x00artist" → cover art file path
mu sync.Mutex // serializes disk writes
}
// NewCoverArtProxy creates a proxy that checks the local library
// first and caches CAA thumbnails under the user data directory.
func NewCoverArtProxy(db *database.DB, limiter *RateLimiter) *CoverArtProxy {
// NewCoverArtProxy creates a proxy that caches CAA thumbnails
// under the user data directory. The db parameter is accepted
// for API stability but is no longer read; future cover-art
// logic that needs DB access can wire it back up.
func NewCoverArtProxy(_ *database.DB, limiter *RateLimiter) *CoverArtProxy {
dir := ""
dataDir, err := system.GetUserDataDirPath()
@@ -61,18 +70,18 @@ func NewCoverArtProxy(db *database.DB, limiter *RateLimiter) *CoverArtProxy {
}
return &CoverArtProxy{
db: db,
cacheDir: dir,
client: &http.Client{Timeout: thumbnailTimeout},
limiter: limiter,
}
}
// GetThumbnail returns a base64-encoded JPEG data URL for the given
// release group. Checks local library art first (by name match),
// GetThumbnail returns a base64 data URL for an album's cover art.
// Checks local library art first, then disk cache, then fetches from CAA.
// Returns "" on failure.
// GetThumbnail returns a base64-encoded JPEG data URL for the
// given release group. Checks the disk cache first; falls back
// to a CAA network fetch. Returns "" on failure. The albumName
// / artistName args are accepted for API stability and ignored
// (they used to drive a library-by-name lookup; see the proxy
// type comment for why that was removed).
//
// The mbid argument MUST be a release group MBID. Track-level cover
// art (where you only have a release MBID) should be resolved by
@@ -80,7 +89,6 @@ func NewCoverArtProxy(db *database.DB, limiter *RateLimiter) *CoverArtProxy {
func (p *CoverArtProxy) GetThumbnail(
releaseGroupMBID, albumName, artistName string,
) string {
// Source 1+2: local library art + disk cache (instant).
if cached := p.GetThumbnailCached(releaseGroupMBID, albumName, artistName); cached != "" {
return cached
}
@@ -106,67 +114,70 @@ func (p *CoverArtProxy) GetThumbnail(
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(data)
}
// GetThumbnailCached checks only local library art and disk cache.
// Returns "" if not cached — does NOT fetch from the network.
// GetThumbnailCached returns the disk-cached cover art for the
// release group, or "" if it isn't on disk. Does NOT fetch from
// the network. albumName/artistName accepted for API stability
// and ignored — see the proxy type comment.
func (p *CoverArtProxy) GetThumbnailCached(
releaseGroupMBID, albumName, artistName string,
releaseGroupMBID, _, _ string,
) string {
// Source 1: local library cover art (instant).
if albumName != "" {
if dataURL := p.libraryArt(albumName, artistName); dataURL != "" {
return dataURL
}
}
if p.cacheDir == "" || releaseGroupMBID == "" {
return ""
}
// Source 2: disk cache from previous CAA fetch (instant).
return p.readCache(releaseGroupMBID)
}
// GetTrackThumbnail returns cover art for a track. Tries, in order:
// 1. Local library art by album/artist name.
// 2. Disk cache for the release group MBID (shared with discography).
// 3. Disk cache for the release MBID (per-track fallback).
// 4. CAA network fetch on the release group (populates RG cache).
// 5. CAA network fetch on the release (populates release cache).
// 1. Disk cache for the release group MBID (shared with discography).
// 2. Disk cache for the release MBID (per-track fallback).
// 3. CAA network fetch on the release group (populates RG cache).
// 4. CAA network fetch on the release (populates release cache).
//
// Either or both MBIDs may be empty — whichever is present is tried.
// Release group is preferred because it shares the cache with the
// discography and top-releases sections; release is the fallback for
// tracks whose caa_release_mbid doesn't resolve to a known RG in the
// index (e.g. the track is on a release not fetched for that artist).
//
// The albumName/artistName args are accepted for API stability and
// ignored — see the proxy type comment for why the library-by-name
// step was removed.
func (p *CoverArtProxy) GetTrackThumbnail(
releaseMBID, releaseGroupMBID, albumName, artistName string,
releaseMBID, releaseGroupMBID, _, _ string,
) string {
// Source 1: local library art (instant).
if albumName != "" {
if dataURL := p.libraryArt(albumName, artistName); dataURL != "" {
return dataURL
}
}
return p.GetCandidateThumbnail(releaseMBID, releaseGroupMBID)
}
// GetCandidateThumbnail is the canonical CAA-only lookup: disk
// cache for the release group, disk cache for the release, then
// CAA network on each in turn. Used everywhere the user is
// browsing or reviewing albums that aren't *their* library copy
// (autotag review, explore) — for those views we want the
// canonical CAA art, not whatever ID3 bytes happen to be tagged
// on a local file. Returns "" when no art is available.
func (p *CoverArtProxy) GetCandidateThumbnail(
releaseMBID, releaseGroupMBID string,
) string {
if p.cacheDir == "" {
return ""
}
// Source 2: disk cache for release group (shared with discography).
// Disk cache for release group (shared with discography).
if releaseGroupMBID != "" {
if cached := p.readCache(releaseGroupMBID); cached != "" {
return cached
}
}
// Source 3: disk cache for release (per-track fallback).
// Disk cache for release (per-track fallback).
if releaseMBID != "" {
if cached := p.readCache(releaseMBID); cached != "" {
return cached
}
}
// Source 4: CAA network fetch on release group.
// Network fetch on release group.
if releaseGroupMBID != "" {
url := CoverArtGroupURL(releaseGroupMBID)
data, cacheable, err := p.fetch(url)
@@ -178,13 +189,11 @@ func (p *CoverArtProxy) GetTrackThumbnail(
}
if cacheable {
// Mark RG miss so we don't re-fetch it, but fall through
// to the release-level fallback.
p.writeCache(releaseGroupMBID, nil)
}
}
// Source 5: CAA network fetch on release (fallback).
// Network fetch on release (fallback).
if releaseMBID != "" {
url := CoverArtURL(releaseMBID)
data, cacheable, err := p.fetch(url)
@@ -205,18 +214,14 @@ func (p *CoverArtProxy) GetTrackThumbnail(
return ""
}
// GetTrackThumbnailCached returns a cached track thumbnail without
// hitting the network. Tries library art, then RG cache, then
// release cache. Returns "" if nothing is cached.
// GetTrackThumbnailCached returns the disk-cached track thumbnail
// (RG MBID first, then release MBID). Returns "" when nothing is
// cached. Does NOT fetch from the network. albumName/artistName
// accepted for API stability and ignored — see the proxy type
// comment.
func (p *CoverArtProxy) GetTrackThumbnailCached(
releaseMBID, releaseGroupMBID, albumName, artistName string,
releaseMBID, releaseGroupMBID, _, _ string,
) string {
if albumName != "" {
if dataURL := p.libraryArt(albumName, artistName); dataURL != "" {
return dataURL
}
}
if p.cacheDir == "" {
return ""
}
@@ -237,72 +242,7 @@ func (p *CoverArtProxy) GetTrackThumbnailCached(
}
// ---------------------------------------------------------------------------
// Source 1: local library art
// ---------------------------------------------------------------------------
// libraryArt returns a base64 data URL for the album if it exists
// in the local music library. Matched by lowercased album name +
// artist name.
func (p *CoverArtProxy) libraryArt(albumName, artistName string) string {
p.libOnce.Do(p.buildLibraryIndex)
key := libraryArtKey(albumName, artistName)
path, ok := p.libIndex[key]
if !ok || path == "" {
return ""
}
data, err := os.ReadFile(path)
if err != nil || len(data) == 0 {
return ""
}
mime := "image/jpeg"
if strings.HasSuffix(strings.ToLower(path), ".png") {
mime = "image/png"
}
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data)
}
func (p *CoverArtProxy) buildLibraryIndex() {
p.libIndex = make(map[string]string)
if p.db == nil {
return
}
rows, err := p.db.QueryContext(`
SELECT rg.name, a.name, ca.file_path
FROM release_groups rg
JOIN artist_credit ac ON ac.id = rg.album_artist_credit_id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
JOIN artists a ON a.id = aca.artist_id
LEFT JOIN cover_art ca ON ca.id = rg.cover_art_id
WHERE ca.file_path IS NOT NULL AND ca.file_path != ''
`)
if err != nil {
return
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var album, artist, path string
if err := rows.Scan(&album, &artist, &path); err == nil {
key := libraryArtKey(album, artist)
p.libIndex[key] = path
}
}
}
func libraryArtKey(album, artist string) string {
return strings.ToLower(album) + "\x00" + strings.ToLower(artist)
}
// ---------------------------------------------------------------------------
// Source 2+3: CAA disk cache and network fetch
// CAA disk cache and network fetch
// ---------------------------------------------------------------------------
func (p *CoverArtProxy) fetch(url string) ([]byte, bool, error) {