Add database backups and two-digit disc numbers
Disc numbers: the organizer writes two-digit disc numbers by default
(Disc 01, 01-04 Title, {disc}), with an option for the plain style
(Disc 2, 2-04). Migration 8 adds the per-pair setting.
Backups (Settings -> Backups), similar to Sonarr:
- A backup is a zip with a consistent snapshot of the database (VACUUM
INTO, safe while running) and backup.json. Configurable folder,
schedule (off/1/3/7/14 days) and retention for scheduled backups;
manual backups are kept until deleted. List, download, delete.
- Restore from a listed backup or an uploaded zip: validated first
(integrity check, playlink tables, not a newer schema), staged next to
the live database, then the server restarts; at startup the staged
database is swapped in and the previous one kept as
playlink.db.pre-restore-<time>. Refused while a job runs. Backup names
are validated, so downloads/deletes cannot reach other files.
- Optional backup before every action in live mode: once per action the
user starts (link, promote, bulk, build/remove, tag edit, folder
renames, undo, purge) and before scans auto-link or auto-organize,
never per file and never in dry run. If the backup fails the action
does not run. The newest 10 action backups are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
96ea856dfd
commit
49fc32096a
18 files changed
+1235
-47
No files matched your search
@@ -150,8 +150,9 @@ type Pair struct {
|
||||
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
|
||||
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"`
|
||||
@@ -160,7 +161,7 @@ type Pair struct {
|
||||
|
||||
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, last_scan_at, last_scan_error,
|
||||
organize_multidisc, auto_organize, organize_disc_digits, last_scan_at, last_scan_error,
|
||||
last_scan_warnings, created_at`
|
||||
|
||||
type scanner interface{ Scan(...any) error }
|
||||
@@ -171,7 +172,7 @@ func scanPair(r scanner) (Pair, error) {
|
||||
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, &last, &p.LastScanError,
|
||||
&p.OrganizeMultiDisc, &p.AutoOrganize, &p.OrganizeDiscDigits, &last, &p.LastScanError,
|
||||
&warns, &p.CreatedAt)
|
||||
if err != nil {
|
||||
return p, err
|
||||
@@ -214,11 +215,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, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
organize_template, organize_multidisc, auto_organize, organize_disc_digits, 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, now())
|
||||
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -227,10 +228,11 @@ 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 = ? WHERE id = ?`,
|
||||
approx_matching = ?, approx_threshold = ?, organize_template = ?, organize_multidisc = ?, auto_organize = ?,
|
||||
organize_disc_digits = ? 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.ID)
|
||||
p.OrganizeTemplate, p.OrganizeMultiDisc, p.AutoOrganize, p.OrganizeDiscDigits, p.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -126,8 +126,14 @@ var migrations = []string{
|
||||
`ALTER TABLE pairs ADD COLUMN organize_template TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE pairs ADD COLUMN organize_multidisc TEXT NOT NULL DEFAULT 'prefix';
|
||||
ALTER TABLE pairs ADD COLUMN auto_organize TEXT NOT NULL DEFAULT 'off';`,
|
||||
|
||||
// 8: zero-padded disc numbers (Disc 01) by default.
|
||||
`ALTER TABLE pairs ADD COLUMN organize_disc_digits INTEGER NOT NULL DEFAULT 2;`,
|
||||
}
|
||||
|
||||
// SchemaVersion is the newest database schema this build knows.
|
||||
func SchemaVersion() int { return len(migrations) }
|
||||
|
||||
// Open opens (and migrates) the database in dataDir.
|
||||
func Open(dataDir string) (*Store, error) {
|
||||
dsn := "file:" + filepath.Join(dataDir, "playlink.db") +
|
||||
|
||||
Reference in new issue
Block a user