feat: autotag scoring overhaul, dump-based explore index, and lyrics search

Consolidates in-progress work across autotag, explore, and library:

- autotag: beets/Picard-informed scoring engine — ID-first matching, VA
  handling, recommendation tiers, and a merged distance/rank cascade, with
  an eval harness for regression tracking.
- explore: offline MusicBrainz dump import/incremental refresh replaces the
  legacy tier crawl; index-first local search with fuzzy matching and a
  dedicated ranker; disk-free guards for dump downloads.
- library: artist-credit extraction and matching.
- lyrics: owned-library lyric search (FTS) with LRCLIB backfill.

Also: rewrite README to be user-focused, and migrate upstream to
git.ljones.me/yonlu/yellowjacket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 12:14:20 -04:00
co-authored by Claude Opus 4.8
parent d5140395da
commit 65048401e8
117 changed files with 17033 additions and 4767 deletions
+39 -2
View File
@@ -174,9 +174,46 @@ func (c *Config) Save() error {
return fmt.Errorf("could not marshal config struct: %w", err)
}
err = os.WriteFile(c.filePath, confFileData, 0o644)
// Write atomically: marshal into a temp file in the same directory,
// then rename it over the target. os.WriteFile truncates the file
// in place before writing, so a crash or kill mid-write (common
// during dev restarts) can leave a truncated — often empty — config.
// An empty TOML file loads "successfully" as all-defaults and then
// gets re-saved as defaults, silently wiping the user's settings.
// A temp-file + rename makes the replacement atomic: a reader always
// sees either the previous file or the complete new one.
tmp, err := os.CreateTemp(path.Dir(c.filePath), "config-*.toml.tmp")
if err != nil {
return fmt.Errorf("could not write config file (%s): %w", c.filePath, err)
return fmt.Errorf("could not create temp config file: %w", err)
}
tmpName := tmp.Name()
// Best-effort cleanup if we bail before the rename succeeds.
defer func() { _ = os.Remove(tmpName) }()
if _, err := tmp.Write(confFileData); err != nil {
_ = tmp.Close()
return fmt.Errorf("could not write temp config file: %w", err)
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return fmt.Errorf("could not sync temp config file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("could not close temp config file: %w", err)
}
if err := os.Chmod(tmpName, 0o644); err != nil {
return fmt.Errorf("could not set config file permissions: %w", err)
}
if err := os.Rename(tmpName, c.filePath); err != nil {
return fmt.Errorf("could not replace config file (%s): %w", c.filePath, err)
}
c.logger.Debug("saved config to file", "file", c.filePath)
+14 -4
View File
@@ -1,10 +1,20 @@
package config
const (
// DefaultWidth is the default window width in pixels.
DefaultWidth = 512
// DefaultHeight is the default window height in pixels.
DefaultHeight = 384
// DefaultWidth is the default window width in pixels for a fresh
// config. Kept comfortably above the minimum so a first launch
// (or a config with no saved size) opens at a usable size rather
// than the cramped minimum.
DefaultWidth = 1100
// DefaultHeight is the default window height in pixels for a fresh config.
DefaultHeight = 720
// MinWidth is the smallest allowed window width in pixels. Wails
// enforces this at runtime; it is also the floor below which a
// reported size is treated as bogus and not persisted.
MinWidth = 512
// MinHeight is the smallest allowed window height in pixels.
MinHeight = 384
)
// WindowConfig holds window size preferences.