feat(server/m7-cover-sources): artist art enricher (thumb + fanart)
Parallels EnrichAlbum but with no sidecar layer (artists have no per-artist directory in a music library). Iterates EnabledArtistProviders, writes thumb.jpg + fanart.jpg to <data_dir>/artist-art/<artist_id>/, stamps artist_art_source + artist_art_sources_version atomically. Atomicity: provider returns whichever images it has — partial success (thumb-only or fanart-only) is persisted with whichever paths landed, source still stamped. ErrNotFound from every enabled provider settles the row to 'none' with the current version stamp. Any ErrTransient leaves the row NULL for next-pass retry. CleanupArtistArt removes <data_dir>/artist-art/<artist_id>/ on artist delete; idempotent on missing dir. Hooked into the artist- delete cascade in tracks/service.go after the transaction commits; non-fatal on failure (logs but doesn't fail the delete). EnrichArtistBatch drains rows from ListArtistsMissingArt; the orchestrator's 4th stage (added in T10) calls this. Tests cover the eligibility table, atomicity (thumb-only / fanart-only), all-404-settles, ErrTransient-leaves-NULL, filesystem layout, and CleanupArtistArt idempotency. tracks.Service gains a dataDir field; tracks.NewService takes it as a new parameter. All three call sites updated (server/server.go, api/auth_test.go, api/admin_tracks_test.go). The dataDir flows from cfg.Storage.DataDir → server.New → s.DataDir → tracks.NewService without any change to cmd/minstrel/main.go.
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/coverart"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
)
|
||||
|
||||
@@ -48,18 +49,21 @@ type LidarrUnmonitorer interface {
|
||||
// the right fallback when Lidarr isn't configured: file + DB delete
|
||||
// still happen.
|
||||
type Service struct {
|
||||
pool *pgxpool.Pool
|
||||
logger *slog.Logger
|
||||
lidarr LidarrUnmonitorer
|
||||
pool *pgxpool.Pool
|
||||
logger *slog.Logger
|
||||
lidarr LidarrUnmonitorer
|
||||
dataDir string
|
||||
}
|
||||
|
||||
// NewService constructs a Service. logger may be nil (defaults to
|
||||
// slog.Default). lidarr may be nil to disable the unmonitor branch.
|
||||
func NewService(pool *pgxpool.Pool, logger *slog.Logger, lidarr LidarrUnmonitorer) *Service {
|
||||
// dataDir is the on-disk root for cached artifacts; used to clean up
|
||||
// artist-art on artist delete. Empty string disables the cleanup.
|
||||
func NewService(pool *pgxpool.Pool, logger *slog.Logger, lidarr LidarrUnmonitorer, dataDir string) *Service {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &Service{pool: pool, logger: logger, lidarr: lidarr}
|
||||
return &Service{pool: pool, logger: logger, lidarr: lidarr, dataDir: dataDir}
|
||||
}
|
||||
|
||||
// RemoveTrack deletes the file from disk and the DB rows, runs the
|
||||
@@ -159,6 +163,16 @@ func (s *Service) RemoveTrack(
|
||||
return nil, nil, false, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup artist-art filesystem cache if the delete cascade orphaned
|
||||
// an artist. Non-fatal — the destructive part is done; we just want
|
||||
// to keep the dataDir tidy.
|
||||
if deletedArtistID != nil && s.dataDir != "" {
|
||||
if err := coverart.CleanupArtistArt(s.dataDir, *deletedArtistID); err != nil {
|
||||
s.logger.Warn("track delete: artist-art cleanup failed",
|
||||
"artist_id", *deletedArtistID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Lidarr unmonitor — non-fatal. The destructive part is done; any
|
||||
// failure here is informational so the operator can retry manually.
|
||||
if unmonitor && track.Mbid != nil && *track.Mbid != "" && s.lidarr != nil {
|
||||
|
||||
Reference in New Issue
Block a user