Add album art search, auto-match and a whole-album tag editor

Search iTunes, Deezer, MusicBrainz, DuckDuckGo and Google for square covers,
embed them and/or save cover.jpg, auto-match covers for albums, and edit an
album's shared tags and track list in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-29 15:30:19 -07:00
1 parent 5fb195f0d6
commit d8ac25a55d
21 files changed
+2719 -38

No files matched your search

+14
View File
@@ -13,6 +13,7 @@ import (
"sync"
"time"
"github.com/cdnutter/playlink/internal/art"
"github.com/cdnutter/playlink/internal/fsops"
"github.com/cdnutter/playlink/internal/probe"
"github.com/cdnutter/playlink/internal/store"
@@ -45,6 +46,8 @@ type Engine struct {
tagTools tags.Tools
beforeAction func(ctx context.Context, reason string) error
tagCache tagCache
artClient *art.Client
artStore *art.Store
plansMu sync.Mutex
plans map[int64]*organizePlan // latest "rename now" preview per pair
@@ -74,6 +77,7 @@ type Job struct {
Error string `json:"error"`
Results map[string]int `json:"results"`
Skipped []string `json:"skipped"`
Notes []string `json:"notes"`
StartedAt int64 `json:"started_at"`
FinishedAt int64 `json:"finished_at,omitempty"`
@@ -95,6 +99,15 @@ func (j *Job) tick() { j.update(func(j *Job) { j.Done++ }) }
func (j *Job) count(key string) { j.update(func(j *Job) { j.Results[key]++ }) }
// note records an informational line (e.g. which cover was chosen).
func (j *Job) note(msg string) {
j.update(func(j *Job) {
if len(j.Notes) < 300 {
j.Notes = append(j.Notes, msg)
}
})
}
func (j *Job) skip(msg string) {
j.update(func(j *Job) {
j.Results["skipped"]++
@@ -118,6 +131,7 @@ func (e *Engine) Job(pairID int64) *Job {
cp.Results[k] = v
}
cp.Skipped = append([]string{}, j.Skipped...) // never nil: the UI expects an array
cp.Notes = append([]string{}, j.Notes...)
return &cp
}