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
|
|
@ -280,6 +280,19 @@ func (m *Manager) processTask(ctx context.Context, task *Task) {
|
|||
task.FilePath = finalPath
|
||||
task.mu.Unlock()
|
||||
|
||||
// 4b. Handle upgrade replacement (mode = "upgrade")
|
||||
if task.ReplacePath != "" {
|
||||
backupDir := "" // uses default ~/.local/share/unarr/replaced/
|
||||
if err := replaceFile(task.ReplacePath, finalPath, backupDir); err != nil {
|
||||
log.Printf("[%s] replace warning: %v (keeping new file at %s)", task.ID[:8], err, finalPath)
|
||||
} else {
|
||||
task.mu.Lock()
|
||||
task.FilePath = task.ReplacePath
|
||||
task.mu.Unlock()
|
||||
log.Printf("[%s] upgraded: replaced %s", task.ID[:8], task.ReplacePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Complete
|
||||
if method == MethodTorrent && m.cfg.Organize.Enabled {
|
||||
// Could add seeding here in the future
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -135,15 +135,17 @@ func (ss *StreamServer) Shutdown(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (ss *StreamServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
// CORS headers — allow web player from any origin (HTTPS site → localhost)
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Range")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges")
|
||||
// CORS headers — only when browser sends Origin (HTTPS site → localhost)
|
||||
if origin := r.Header.Get("Origin"); origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Range")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reader := ss.provider.NewFileReader(r.Context())
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ type Task struct {
|
|||
DirectFileName string // Original filename from direct URL
|
||||
NzbID string // Pre-resolved NZB ID (usenet)
|
||||
NzbPassword string // Password for encrypted NZB archives
|
||||
ReplacePath string // File to replace after download (upgrade mode)
|
||||
LibraryItemID int // Library item being upgraded
|
||||
|
||||
// Runtime state
|
||||
Status TaskStatus
|
||||
|
|
@ -88,6 +90,8 @@ func NewTaskFromAgent(at agent.Task) *Task {
|
|||
DirectFileName: at.DirectFileName,
|
||||
NzbID: at.NzbID,
|
||||
NzbPassword: at.NzbPassword,
|
||||
ReplacePath: at.ReplacePath,
|
||||
LibraryItemID: at.LibraryItemID,
|
||||
Mode: mode,
|
||||
Status: StatusClaimed,
|
||||
ClaimedAt: time.Now(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue