feat: add migrate command, media server detection, and debrid auto-config
- Migration wizard from Sonarr/Radarr/Prowlarr (unarr migrate) [pre-beta] - Auto-detect instances via Docker, config files, port scan, Prowlarr - Import wanted list (monitored+missing movies/series) - Import download history and blocklist to avoid re-downloading - Extract debrid tokens from *arr download clients - Quality profile mapping to preferred_quality config - DISTINCT ON PostgreSQL query for optimal torrent selection - JSON export with --dry-run --json (text to stderr, JSON to stdout) - Media server detection (Plex/Jellyfin/Emby) in unarr init - Detects library paths and offers them as download directory options - Debrid auto-configuration in unarr init - Scans *arr instances for debrid tokens - Validates and saves via API if user confirms - New preferred_quality setting in config (2160p/1080p/720p) - Library scan command (unarr scan) with ffprobe metadata extraction
This commit is contained in:
parent
0b6c6849b1
commit
677a8fe083
34 changed files with 4766 additions and 22 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -136,6 +137,53 @@ func cleanTitle(title string) string {
|
|||
return t
|
||||
}
|
||||
|
||||
// replaceFile moves the old file to a backup dir, then moves the new file to the old path.
|
||||
// Used by upgrade downloads to replace an existing file with a better version.
|
||||
func replaceFile(oldPath, newPath, backupDir string) error {
|
||||
if _, err := os.Stat(oldPath); err != nil {
|
||||
return fmt.Errorf("original file not found: %w", err)
|
||||
}
|
||||
|
||||
if backupDir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
backupDir = filepath.Join(home, ".local", "share", "unarr", "replaced")
|
||||
}
|
||||
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create backup dir: %w", err)
|
||||
}
|
||||
|
||||
// Move old file to backup (with timestamp to avoid collisions)
|
||||
base := filepath.Base(oldPath)
|
||||
ext := filepath.Ext(base)
|
||||
nameNoExt := strings.TrimSuffix(base, ext)
|
||||
backupName := fmt.Sprintf("%s.%d%s", nameNoExt, time.Now().Unix(), ext)
|
||||
backupPath := filepath.Join(backupDir, backupName)
|
||||
|
||||
if err := os.Rename(oldPath, backupPath); err != nil {
|
||||
// Cross-device: copy + delete
|
||||
if err := copyFile(oldPath, backupPath); err != nil {
|
||||
return fmt.Errorf("backup failed: %w", err)
|
||||
}
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Move new file to old path
|
||||
if err := os.MkdirAll(filepath.Dir(oldPath), 0o755); err != nil {
|
||||
return fmt.Errorf("create target dir: %w", err)
|
||||
}
|
||||
if err := os.Rename(newPath, oldPath); err != nil {
|
||||
// Cross-device: copy + delete
|
||||
if err := copyFile(newPath, oldPath); err != nil {
|
||||
// Rollback: restore backup
|
||||
os.Rename(backupPath, oldPath)
|
||||
return fmt.Errorf("replace failed: %w", err)
|
||||
}
|
||||
os.Remove(newPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
s, err := os.Open(src)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue