Redo file naming in the style of Lidarr and add "Rename now"
Naming: separate artist folder, album folder, track and multi-disc track
formats with presets, Lidarr-style tokens ({Artist Name}, {Artist
NameThe}, {Artist CleanName}, {Artist NameFirstCharacter}, {Album
Title}, {Album TitleThe}, {Album CleanTitle}, {Release Year},
{track:00}, {medium:00}, {Medium Format}, {Track Title}, {Track
CleanTitle}, {Track ArtistName}, {Genre}) with Lidarr's word-separator
and case variants, colon replacement modes and replace/remove for other
illegal characters. Empty brackets and separators left by missing values
are cleaned up. Defaults to Artist/Album/01 Title with Disc 01 folders
for multi-disc albums; earlier pattern/disc settings are converted.
Examples are now rendered by the server with the same code as real
renames, fixing the settings page showing paths that differed from what
would happen (e.g. disc 1 of a multi-disc album outside its Disc folder).
"Rename now..." checks one or both libraries, then lists every change
grouped by destination folder with per-file and per-folder selection,
skipped files with reasons and a Fix tags button, and warnings; applying
re-checks each file, renames, and rescans. The pre-action backup runs
first when enabled. Migration 9 stores the naming as JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49fc32096a
commit
5fb195f0d6
12 files changed
+1297
-355
No files matched your search
+32
-28
@@ -137,42 +137,43 @@ func decodeWarnings(raw string) []Warning {
|
||||
// ---- pairs ----
|
||||
|
||||
type Pair struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MasterRoot string `json:"master_root"`
|
||||
SlaveRoot string `json:"slave_root"`
|
||||
QuarantineDir string `json:"quarantine_dir"`
|
||||
Extensions []string `json:"extensions"`
|
||||
AutoLinkIdentical bool `json:"auto_link_identical"`
|
||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||
RetentionDays int `json:"retention_days"`
|
||||
SettleMinutes int `json:"settle_minutes"`
|
||||
ApproxMatching bool `json:"approx_matching"`
|
||||
ApproxThreshold int `json:"approx_threshold"` // percent
|
||||
OrganizeTemplate string `json:"organize_template"`
|
||||
OrganizeMultiDisc string `json:"organize_multidisc"` // prefix, folder or none
|
||||
AutoOrganize string `json:"auto_organize"` // off, slave, master or both
|
||||
OrganizeDiscDigits int `json:"organize_disc_digits"` // 2: Disc 01, 1: Disc 1
|
||||
LastScanAt *int64 `json:"last_scan_at"`
|
||||
LastScanError string `json:"last_scan_error"`
|
||||
LastScanWarnings []Warning `json:"last_scan_warnings"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MasterRoot string `json:"master_root"`
|
||||
SlaveRoot string `json:"slave_root"`
|
||||
QuarantineDir string `json:"quarantine_dir"`
|
||||
Extensions []string `json:"extensions"`
|
||||
AutoLinkIdentical bool `json:"auto_link_identical"`
|
||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||
RetentionDays int `json:"retention_days"`
|
||||
SettleMinutes int `json:"settle_minutes"`
|
||||
ApproxMatching bool `json:"approx_matching"`
|
||||
ApproxThreshold int `json:"approx_threshold"` // percent
|
||||
OrganizeTemplate string `json:"organize_template"`
|
||||
OrganizeMultiDisc string `json:"organize_multidisc"` // prefix, folder or none
|
||||
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)
|
||||
LastScanAt *int64 `json:"last_scan_at"`
|
||||
LastScanError string `json:"last_scan_error"`
|
||||
LastScanWarnings []Warning `json:"last_scan_warnings"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
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, last_scan_at, last_scan_error,
|
||||
organize_multidisc, auto_organize, organize_disc_digits, organize_config, last_scan_at, last_scan_error,
|
||||
last_scan_warnings, created_at`
|
||||
|
||||
type scanner interface{ Scan(...any) error }
|
||||
|
||||
func scanPair(r scanner) (Pair, error) {
|
||||
var p Pair
|
||||
var exts, warns string
|
||||
var exts, warns, orgCfg 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, &last, &p.LastScanError,
|
||||
&p.OrganizeMultiDisc, &p.AutoOrganize, &p.OrganizeDiscDigits, &orgCfg, &last, &p.LastScanError,
|
||||
&warns, &p.CreatedAt)
|
||||
if err != nil {
|
||||
return p, err
|
||||
@@ -182,6 +183,9 @@ func scanPair(r scanner) (Pair, error) {
|
||||
}
|
||||
p.Extensions = strings.Split(exts, ",")
|
||||
p.LastScanWarnings = decodeWarnings(warns)
|
||||
if orgCfg != "" {
|
||||
p.OrganizeConfig = json.RawMessage(orgCfg)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -215,11 +219,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, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
organize_template, organize_multidisc, auto_organize, organize_disc_digits, organize_config, 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, now())
|
||||
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, string(p.OrganizeConfig), now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -229,10 +233,10 @@ 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 = ? WHERE id = ?`,
|
||||
organize_disc_digits = ?, organize_config = ? 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, p.ID)
|
||||
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, string(p.OrganizeConfig), p.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user