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:
Deivid Soto 2026-03-29 16:54:32 +02:00
parent 0b6c6849b1
commit 677a8fe083
34 changed files with 4766 additions and 22 deletions

View file

@ -153,6 +153,33 @@ func (c *Client) GetUsenetUsage(ctx context.Context) (*UsenetUsageResponse, erro
return &resp, nil
}
// ConfigureDebrid saves a debrid provider token for the user (used by unarr init/migrate).
func (c *Client) ConfigureDebrid(ctx context.Context, req ConfigureDebridRequest) (*ConfigureDebridResponse, error) {
var resp ConfigureDebridResponse
if err := c.doPost(ctx, "/api/internal/agent/debrid-config", req, &resp); err != nil {
return nil, fmt.Errorf("configure debrid: %w", err)
}
return &resp, nil
}
// BatchDownload queues multiple items for download (used by unarr migrate).
func (c *Client) BatchDownload(ctx context.Context, req BatchDownloadRequest) (*BatchDownloadResponse, error) {
var resp BatchDownloadResponse
if err := c.doPost(ctx, "/api/internal/agent/batch-download", req, &resp); err != nil {
return nil, fmt.Errorf("batch download: %w", err)
}
return &resp, nil
}
// SyncLibrary sends scanned library items to the server for matching and upgrade discovery.
func (c *Client) SyncLibrary(ctx context.Context, req LibrarySyncRequest) (*LibrarySyncResponse, error) {
var resp LibrarySyncResponse
if err := c.doPost(ctx, "/api/internal/agent/library-sync", req, &resp); err != nil {
return nil, fmt.Errorf("library sync: %w", err)
}
return &resp, nil
}
// doPost sends a JSON POST request and decodes the response.
func (c *Client) doPost(ctx context.Context, path string, body any, dst any) error {
jsonBody, err := json.Marshal(body)

View file

@ -68,6 +68,8 @@ type Task struct {
DirectFileName string `json:"directFileName,omitempty"` // Original filename from direct URL
NzbID string `json:"nzbId,omitempty"` // Pre-resolved NZB ID from server
NzbPassword string `json:"nzbPassword,omitempty"` // Password for encrypted NZB archives
ReplacePath string `json:"replacePath,omitempty"` // File to replace after download (upgrade mode)
LibraryItemID int `json:"libraryItemId,omitempty"` // Library item being upgraded
}
// TasksResponse wraps the array of tasks returned by the server.
@ -197,3 +199,102 @@ type UsenetUsageResponse struct {
RemainingBytes int64 `json:"remainingBytes"`
QuotaResetDate string `json:"quotaResetDate"`
}
// ---------------------------------------------------------------------------
// Batch download types (used by unarr migrate)
// ---------------------------------------------------------------------------
// BatchDownloadRequest sends a list of wanted items to queue for download.
type BatchDownloadRequest struct {
Items []WantedItem `json:"items"`
ExcludeHashes []string `json:"excludeHashes,omitempty"` // blocklisted + already-downloaded hashes
}
// WantedItem represents a movie or series the user wants.
type WantedItem struct {
TmdbID int `json:"tmdbId,omitempty"`
ImdbID string `json:"imdbId,omitempty"`
Title string `json:"title"`
Year int `json:"year,omitempty"`
Type string `json:"type"` // "movie" or "show"
}
// BatchDownloadResponse reports the outcome of a batch download request.
type BatchDownloadResponse struct {
Queued int `json:"queued"`
NotFound int `json:"notFound"`
AlreadyActive int `json:"alreadyActive"`
Items []BatchItem `json:"items"`
}
// BatchItem is the per-item result of a batch download.
type BatchItem struct {
Title string `json:"title"`
Status string `json:"status"` // "queued", "not_found", "already_active"
}
// ---------------------------------------------------------------------------
// Debrid config types (used by unarr init/migrate)
// ---------------------------------------------------------------------------
// ConfigureDebridRequest configures a debrid provider.
type ConfigureDebridRequest struct {
Provider string `json:"provider"` // "real-debrid", "alldebrid", "torbox", "premiumize"
Token string `json:"token"`
}
// ConfigureDebridResponse is returned after configuring a debrid provider.
type ConfigureDebridResponse struct {
Success bool `json:"success"`
Account DebridAccount `json:"account"`
Error string `json:"error,omitempty"`
}
// DebridAccount holds verified debrid account info.
type DebridAccount struct {
Valid bool `json:"valid"`
Premium bool `json:"premium"`
Username string `json:"username"`
ExpiresAt string `json:"expiresAt,omitempty"`
}
// ---------------------------------------------------------------------------
// Library sync types (used by unarr scan)
// ---------------------------------------------------------------------------
// LibrarySyncRequest sends scanned media items to the server.
type LibrarySyncRequest struct {
Items []LibrarySyncItem `json:"items"`
ScanPath string `json:"scanPath"`
IsLastBatch bool `json:"isLastBatch"`
}
// LibrarySyncItem is a single scanned media file with ffprobe metadata.
type LibrarySyncItem struct {
FilePath string `json:"filePath"`
FileName string `json:"fileName"`
FileSize int64 `json:"fileSize,omitempty"`
Title string `json:"title"`
Year string `json:"year,omitempty"`
Season int `json:"season,omitempty"`
Episode int `json:"episode,omitempty"`
ContentType string `json:"contentType"`
Resolution string `json:"resolution,omitempty"`
VideoCodec string `json:"videoCodec,omitempty"`
HDR string `json:"hdr,omitempty"`
BitDepth int `json:"bitDepth,omitempty"`
AudioCodec string `json:"audioCodec,omitempty"`
AudioChannels int `json:"audioChannels,omitempty"`
AudioLanguages []string `json:"audioLanguages,omitempty"`
SubtitleLanguages []string `json:"subtitleLanguages,omitempty"`
AudioTracks any `json:"audioTracks,omitempty"`
SubtitleTracks any `json:"subtitleTracks,omitempty"`
VideoInfo any `json:"videoInfo,omitempty"`
}
// LibrarySyncResponse is returned after syncing library items.
type LibrarySyncResponse struct {
Synced int `json:"synced"`
Matched int `json:"matched"`
Removed int `json:"removed"`
}