wip on autotagging
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user