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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user