Add organizing files into Artist/Album/01 Title from their tags
A new Organize tab renames audio files from their tags into a per-pair
naming pattern, by default {artist}/{album}/{track} {title} (album
artist, falling back to artist). Multi-disc albums get a disc prefix
(2-04) or a Disc N folder; names are made safe for Windows/SMB shares.
- Preview first: every rename, files already organized, and files
skipped with the reason (missing tags, with a button into the tag
editor; two files resolving to the same name; an existing file).
- Applying re-checks each file against the preview, moves it within its
library (hardlinks and contents untouched, case-only renames via a
temporary name, completed on startup after a crash), carries .lrc
lyrics and folder cover art along, and removes emptied folders. Each
move is an operation that can be undone; the library is rescanned.
- Optional automatic organizing of the slave, master or both during
scans (off by default). Dry run applies.
- Scans now recognise a slave file that is a hardlink of a master file
at a different path as linked ("same file, other path") instead of
unmatched.
Migration 7 adds the per-pair naming settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3eb02e2c8d
commit
dbcf562f7e
16 files changed
+1480
-11
No files matched your search
@@ -287,6 +287,11 @@ func (e *Engine) scan(ctx context.Context, j *Job, pairID int64, trigger string)
|
||||
_ = e.st.FinishScan(ctx, pairID, started.Unix(), err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
if l.pair.AutoOrganize != "" && l.pair.AutoOrganize != "off" {
|
||||
if err := e.autoOrganize(ctx, j, l); err != nil {
|
||||
e.log.Warn("auto-organize failed", "pair", l.pair.Name, "err", err)
|
||||
}
|
||||
}
|
||||
warns := layoutWarnings(l)
|
||||
|
||||
j.phase("Indexing master library", 0)
|
||||
@@ -304,6 +309,15 @@ func (e *Engine) scan(ctx context.Context, j *Job, pairID int64, trigger string)
|
||||
return err
|
||||
}
|
||||
ix := buildIndex(masterFiles, l.pair)
|
||||
// The same file (hardlink) can sit at another path, e.g. after one library
|
||||
// was organized; it is recognised as linked rather than unmatched.
|
||||
byInode := map[[2]uint64]string{}
|
||||
for _, rel := range sortedKeys(masterFiles) {
|
||||
in := masterFiles[rel]
|
||||
if _, dup := byInode[[2]uint64{in.Dev, in.Ino}]; !dup {
|
||||
byInode[[2]uint64{in.Dev, in.Ino}] = rel
|
||||
}
|
||||
}
|
||||
if ix.approx != nil {
|
||||
ix.verify = e.artistVerifier(ctx, l)
|
||||
}
|
||||
@@ -322,6 +336,11 @@ func (e *Engine) scan(ctx context.Context, j *Job, pairID int64, trigger string)
|
||||
si := slaveFiles[rel]
|
||||
m := store.Match{PairID: pairID, RelSlave: rel}
|
||||
m.RelMaster, m.Kind, m.Reason = ix.find(rel)
|
||||
if m.Kind != store.KindPath {
|
||||
if mrel, ok := byInode[[2]uint64{si.Dev, si.Ino}]; ok {
|
||||
m.RelMaster, m.Kind, m.Reason = mrel, store.KindInode, ""
|
||||
}
|
||||
}
|
||||
var pm *store.Match
|
||||
if p, ok := prev[rel]; ok {
|
||||
pm = &p
|
||||
|
||||
Reference in new issue
Block a user