Add the step-by-step fixer and library types

Each pair is a Music (Artist/Album/01 Title) or Soundtracks (Album/01 Title)
library. The new Fixer tab takes the master, then the slave, through four
reviewed steps: folder structure, album tags (optional MusicBrainz lookup;
the slave reuses the fixed master's tags), folder names and file names,
then links the two libraries. Every fix is an undoable operation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-30 09:10:48 -07:00
1 parent b11d0806c1
commit 428935dc58
19 files changed
+3503 -23

No files matched your search

+28 -16
View File
@@ -154,6 +154,8 @@ type Pair struct {
AutoOrganize string `json:"auto_organize"` // off, slave, master or both
OrganizeDiscDigits int `json:"-"` // superseded by OrganizeConfig; read to convert old settings
OrganizeConfig json.RawMessage `json:"-"` // naming settings (engine.NamingConfig)
LibraryType string `json:"library_type"` // music or soundtracks
FixerState json.RawMessage `json:"-"` // step-by-step fixer progress (engine.FixerState)
LastScanAt *int64 `json:"last_scan_at"`
LastScanError string `json:"last_scan_error"`
LastScanWarnings []Warning `json:"last_scan_warnings"`
@@ -163,18 +165,18 @@ type Pair struct {
const pairCols = `id, name, master_root, slave_root, quarantine_dir, extensions, auto_link_identical,
scan_interval_minutes, retention_days, settle_minutes, approx_matching, approx_threshold, organize_template,
organize_multidisc, auto_organize, organize_disc_digits, organize_config, last_scan_at, last_scan_error,
last_scan_warnings, created_at`
last_scan_warnings, created_at, library_type, fixer_state`
type scanner interface{ Scan(...any) error }
func scanPair(r scanner) (Pair, error) {
var p Pair
var exts, warns, orgCfg string
var exts, warns, orgCfg, fixer string
var last sql.NullInt64
err := r.Scan(&p.ID, &p.Name, &p.MasterRoot, &p.SlaveRoot, &p.QuarantineDir, &exts, &p.AutoLinkIdentical,
&p.ScanIntervalMinutes, &p.RetentionDays, &p.SettleMinutes, &p.ApproxMatching, &p.ApproxThreshold, &p.OrganizeTemplate,
&p.OrganizeMultiDisc, &p.AutoOrganize, &p.OrganizeDiscDigits, &orgCfg, &last, &p.LastScanError,
&warns, &p.CreatedAt)
&warns, &p.CreatedAt, &p.LibraryType, &fixer)
if err != nil {
return p, err
}
@@ -186,6 +188,9 @@ func scanPair(r scanner) (Pair, error) {
if orgCfg != "" {
p.OrganizeConfig = json.RawMessage(orgCfg)
}
if fixer != "" {
p.FixerState = json.RawMessage(fixer)
}
return p, nil
}
@@ -219,11 +224,11 @@ func (s *Store) SavePair(ctx context.Context, p *Pair) error {
if p.ID == 0 {
res, err := s.DB.ExecContext(ctx, `INSERT INTO pairs (name, master_root, slave_root, quarantine_dir, extensions,
auto_link_identical, scan_interval_minutes, retention_days, settle_minutes, approx_matching, approx_threshold,
organize_template, organize_multidisc, auto_organize, organize_disc_digits, organize_config, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
organize_template, organize_multidisc, auto_organize, organize_disc_digits, organize_config, library_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
p.Name, p.MasterRoot, p.SlaveRoot, p.QuarantineDir, exts, p.AutoLinkIdentical,
p.ScanIntervalMinutes, p.RetentionDays, p.SettleMinutes, p.ApproxMatching, p.ApproxThreshold,
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, string(p.OrganizeConfig), now())
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, string(p.OrganizeConfig), p.LibraryType, now())
if err != nil {
return err
}
@@ -233,10 +238,16 @@ func (s *Store) SavePair(ctx context.Context, p *Pair) error {
_, err := s.DB.ExecContext(ctx, `UPDATE pairs SET name = ?, master_root = ?, slave_root = ?, quarantine_dir = ?,
extensions = ?, auto_link_identical = ?, scan_interval_minutes = ?, retention_days = ?, settle_minutes = ?,
approx_matching = ?, approx_threshold = ?, organize_template = ?, organize_multidisc = ?, auto_organize = ?,
organize_disc_digits = ?, organize_config = ? WHERE id = ?`,
organize_disc_digits = ?, organize_config = ?, library_type = ? WHERE id = ?`,
p.Name, p.MasterRoot, p.SlaveRoot, p.QuarantineDir, exts, p.AutoLinkIdentical,
p.ScanIntervalMinutes, p.RetentionDays, p.SettleMinutes, p.ApproxMatching, p.ApproxThreshold,
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, string(p.OrganizeConfig), p.ID)
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, string(p.OrganizeConfig), p.LibraryType, p.ID)
return err
}
// SetFixerState stores the fixer's progress for a pair.
func (s *Store) SetFixerState(ctx context.Context, id int64, raw []byte) error {
_, err := s.DB.ExecContext(ctx, `UPDATE pairs SET fixer_state = ? WHERE id = ?`, string(raw), id)
return err
}
@@ -664,14 +675,15 @@ type Operation struct {
// Operation actions.
const (
ActLink = "link" // slave replaced by hardlink of master
ActPromote = "promote" // master replaced by slave's file, then linked
ActBuild = "build" // master file linked into the slave (fresh library)
ActRemove = "remove" // slave hardlink removed (master copy kept)
ActRename = "rename" // slave folder renamed to the master's spelling
ActTags = "tags" // tags of a file edited
ActMove = "move" // file renamed/moved by the organizer
ActCover = "cover" // album folder image written
ActLink = "link" // slave replaced by hardlink of master
ActPromote = "promote" // master replaced by slave's file, then linked
ActBuild = "build" // master file linked into the slave (fresh library)
ActRemove = "remove" // slave hardlink removed (master copy kept)
ActRename = "rename" // slave folder renamed to the master's spelling
ActTags = "tags" // tags of a file edited
ActMove = "move" // file renamed/moved by the organizer
ActCover = "cover" // album folder image written
ActMoveDir = "move-dir" // folder moved by the fixer (structure or folder names)
)
// Operation statuses.
+4
View File
@@ -132,6 +132,10 @@ var migrations = []string{
// 9: Lidarr-style naming (artist/album folder and track formats) as JSON.
`ALTER TABLE pairs ADD COLUMN organize_config TEXT NOT NULL DEFAULT '';`,
// 10: library type (music or soundtracks) and the step-by-step fixer's progress.
`ALTER TABLE pairs ADD COLUMN library_type TEXT NOT NULL DEFAULT 'music';
ALTER TABLE pairs ADD COLUMN fixer_state TEXT NOT NULL DEFAULT '';`,
}
// SchemaVersion is the newest database schema this build knows.