Go back to the hardlink deduplicator (1da0132), keeping backups
Restores the code of 1da0132 ("Add a setting to hide the quarantine from
Plex") and removes everything added after it: the tag editor, album art,
organizing/naming, the step-by-step tune-up and library types.
Kept:
- Database backups (Settings -> Backups): scheduled, manual, download,
restore, and the optional backup before every action.
- The .env-driven docker-compose.yml for Arcane.
Databases that already ran the later migrations keep working: migrations
6-10 stay in the list (they only add columns) so numbering never goes
backwards, and Undo refuses History entries of removed features instead
of treating them as links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b180e26db
commit
e0617047f7
44 files changed
+103
-10456
No files matched your search
+57
-110
@@ -137,46 +137,37 @@ 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:"-"` // 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"`
|
||||
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
|
||||
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, organize_config, last_scan_at, last_scan_error,
|
||||
last_scan_warnings, created_at, library_type, fixer_state`
|
||||
scan_interval_minutes, retention_days, settle_minutes, approx_matching, approx_threshold, 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, orgCfg, fixer string
|
||||
var exts, warns 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, &p.LibraryType, &fixer)
|
||||
&p.ScanIntervalMinutes, &p.RetentionDays, &p.SettleMinutes, &p.ApproxMatching, &p.ApproxThreshold, &last, &p.LastScanError,
|
||||
&warns, &p.CreatedAt)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
@@ -185,12 +176,6 @@ func scanPair(r scanner) (Pair, error) {
|
||||
}
|
||||
p.Extensions = strings.Split(exts, ",")
|
||||
p.LastScanWarnings = decodeWarnings(warns)
|
||||
if orgCfg != "" {
|
||||
p.OrganizeConfig = json.RawMessage(orgCfg)
|
||||
}
|
||||
if fixer != "" {
|
||||
p.FixerState = json.RawMessage(fixer)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -223,12 +208,10 @@ func (s *Store) SavePair(ctx context.Context, p *Pair) error {
|
||||
exts := strings.Join(p.Extensions, ",")
|
||||
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, library_type, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
auto_link_identical, scan_interval_minutes, retention_days, settle_minutes, approx_matching, approx_threshold, 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), p.LibraryType, now())
|
||||
p.ScanIntervalMinutes, p.RetentionDays, p.SettleMinutes, p.ApproxMatching, p.ApproxThreshold, now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -237,17 +220,9 @@ 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 = ?, library_type = ? WHERE id = ?`,
|
||||
approx_matching = ?, approx_threshold = ? 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.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)
|
||||
p.ScanIntervalMinutes, p.RetentionDays, p.SettleMinutes, p.ApproxMatching, p.ApproxThreshold, p.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -291,7 +266,6 @@ const (
|
||||
KindFuzzy = "fuzzy" // same directory, normalized title matches
|
||||
KindElsewhere = "elsewhere" // same top-level folder and title, different subfolder
|
||||
KindApprox = "approximate" // opt-in: similar title, disc folders ignored
|
||||
KindInode = "inode" // the same file (hardlink) at another path
|
||||
KindNone = "none"
|
||||
)
|
||||
|
||||
@@ -649,41 +623,36 @@ func (s *Store) Summary(ctx context.Context, pairID int64) (Summary, error) {
|
||||
// ---- operations ----
|
||||
|
||||
type Operation struct {
|
||||
ID int64 `json:"id"`
|
||||
PairID int64 `json:"pair_id"`
|
||||
Action string `json:"action"`
|
||||
RelMaster string `json:"rel_master"`
|
||||
RelSlave string `json:"rel_slave"`
|
||||
RelNew string `json:"rel_new"`
|
||||
MatchKind string `json:"match_kind"`
|
||||
MatchReason string `json:"match_reason"`
|
||||
LinkPreexisted bool `json:"link_preexisted"`
|
||||
Detail json.RawMessage `json:"detail,omitempty"`
|
||||
QuarantinePath string `json:"quarantine_path"`
|
||||
QuarantineSide string `json:"quarantine_side"`
|
||||
TmpPath string `json:"-"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Trigger string `json:"trigger"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
FinishedAt *int64 `json:"finished_at"`
|
||||
UndoneAt *int64 `json:"undone_at"`
|
||||
PurgedAt *int64 `json:"purged_at"`
|
||||
ID int64 `json:"id"`
|
||||
PairID int64 `json:"pair_id"`
|
||||
Action string `json:"action"`
|
||||
RelMaster string `json:"rel_master"`
|
||||
RelSlave string `json:"rel_slave"`
|
||||
RelNew string `json:"rel_new"`
|
||||
MatchKind string `json:"match_kind"`
|
||||
MatchReason string `json:"match_reason"`
|
||||
LinkPreexisted bool `json:"link_preexisted"`
|
||||
QuarantinePath string `json:"quarantine_path"`
|
||||
QuarantineSide string `json:"quarantine_side"`
|
||||
TmpPath string `json:"-"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Trigger string `json:"trigger"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
FinishedAt *int64 `json:"finished_at"`
|
||||
UndoneAt *int64 `json:"undone_at"`
|
||||
PurgedAt *int64 `json:"purged_at"`
|
||||
}
|
||||
|
||||
// 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
|
||||
ActMoveDir = "move-dir" // folder moved by the fixer (structure or folder names)
|
||||
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
|
||||
)
|
||||
|
||||
// Operation statuses.
|
||||
@@ -696,17 +665,13 @@ const (
|
||||
)
|
||||
|
||||
const opCols = `id, pair_id, action, rel_master, rel_slave, rel_new, quarantine_path, quarantine_side, tmp_path, bytes,
|
||||
dry_run, trigger, status, error, created_at, finished_at, undone_at, purged_at, match_kind, match_reason, link_preexisted, detail`
|
||||
dry_run, trigger, status, error, created_at, finished_at, undone_at, purged_at, match_kind, match_reason, link_preexisted`
|
||||
|
||||
func scanOp(r scanner) (Operation, error) {
|
||||
var o Operation
|
||||
var fin, und, pur sql.NullInt64
|
||||
var detail string
|
||||
err := r.Scan(&o.ID, &o.PairID, &o.Action, &o.RelMaster, &o.RelSlave, &o.RelNew, &o.QuarantinePath, &o.QuarantineSide,
|
||||
&o.TmpPath, &o.Bytes, &o.DryRun, &o.Trigger, &o.Status, &o.Error, &o.CreatedAt, &fin, &und, &pur, &o.MatchKind, &o.MatchReason, &o.LinkPreexisted, &detail)
|
||||
if detail != "" {
|
||||
o.Detail = json.RawMessage(detail)
|
||||
}
|
||||
&o.TmpPath, &o.Bytes, &o.DryRun, &o.Trigger, &o.Status, &o.Error, &o.CreatedAt, &fin, &und, &pur, &o.MatchKind, &o.MatchReason, &o.LinkPreexisted)
|
||||
if fin.Valid {
|
||||
o.FinishedAt = &fin.Int64
|
||||
}
|
||||
@@ -722,10 +687,10 @@ func scanOp(r scanner) (Operation, error) {
|
||||
func (s *Store) CreateOp(ctx context.Context, o *Operation) error {
|
||||
o.CreatedAt = now()
|
||||
res, err := s.DB.ExecContext(ctx, `INSERT INTO operations (pair_id, action, rel_master, rel_slave, rel_new, quarantine_path,
|
||||
quarantine_side, tmp_path, bytes, dry_run, trigger, status, error, created_at, match_kind, match_reason, link_preexisted, detail)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
quarantine_side, tmp_path, bytes, dry_run, trigger, status, error, created_at, match_kind, match_reason, link_preexisted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
o.PairID, o.Action, o.RelMaster, o.RelSlave, o.RelNew, o.QuarantinePath, o.QuarantineSide, o.TmpPath, o.Bytes,
|
||||
o.DryRun, o.Trigger, o.Status, o.Error, o.CreatedAt, o.MatchKind, o.MatchReason, o.LinkPreexisted, string(o.Detail))
|
||||
o.DryRun, o.Trigger, o.Status, o.Error, o.CreatedAt, o.MatchKind, o.MatchReason, o.LinkPreexisted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -835,21 +800,3 @@ func (s *Store) ExpiredQuarantine(ctx context.Context, pairID, cutoff int64) ([]
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// MatchesByMaster lists the matches of a pair that point at a master file.
|
||||
func (s *Store) MatchesByMaster(ctx context.Context, pairID int64, relMaster string) ([]Match, error) {
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT `+matchCols+` FROM matches WHERE pair_id = ? AND rel_master = ?`, pairID, relMaster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Match
|
||||
for rows.Next() {
|
||||
m, err := scanMatch(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
@@ -119,6 +119,10 @@ var migrations = []string{
|
||||
// undo must then leave that target alone.
|
||||
`ALTER TABLE operations ADD COLUMN link_preexisted INTEGER NOT NULL DEFAULT 0;`,
|
||||
|
||||
// 6-10 belong to features that were removed again (tag editing,
|
||||
// organizing, the step-by-step fixer). They only add columns, which are
|
||||
// unused now, and stay so databases that already ran them keep the same
|
||||
// numbering and future migrations are never skipped.
|
||||
// 6: operation-specific details as JSON (e.g. which tags an edit changed).
|
||||
`ALTER TABLE operations ADD COLUMN detail TEXT NOT NULL DEFAULT '';`,
|
||||
|
||||
|
||||
Reference in new issue
Block a user