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
86
internal/library/cache.go
Normal file
86
internal/library/cache.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package library
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/torrentclaw/torrentclaw-cli/internal/config"
|
||||
)
|
||||
|
||||
// CachePath returns the default library cache file path.
|
||||
func CachePath() string {
|
||||
return filepath.Join(config.DataDir(), "library.json")
|
||||
}
|
||||
|
||||
// LoadCache reads the library cache from disk. Returns nil if file doesn't exist.
|
||||
func LoadCache() (*LibraryCache, error) {
|
||||
return LoadCacheFrom(CachePath())
|
||||
}
|
||||
|
||||
// LoadCacheFrom reads the library cache from a specific path.
|
||||
func LoadCacheFrom(path string) (*LibraryCache, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read cache: %w", err)
|
||||
}
|
||||
|
||||
var cache LibraryCache
|
||||
if err := json.Unmarshal(data, &cache); err != nil {
|
||||
return nil, fmt.Errorf("parse cache: %w", err)
|
||||
}
|
||||
|
||||
if cache.Version != cacheVersion {
|
||||
return nil, nil // incompatible version, treat as missing
|
||||
}
|
||||
|
||||
return &cache, nil
|
||||
}
|
||||
|
||||
// SaveCache writes the library cache to disk atomically.
|
||||
func SaveCache(cache *LibraryCache) error {
|
||||
return SaveCacheTo(cache, CachePath())
|
||||
}
|
||||
|
||||
// SaveCacheTo writes the library cache to a specific path atomically.
|
||||
func SaveCacheTo(cache *LibraryCache, path string) error {
|
||||
cache.Version = cacheVersion
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("create cache dir: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cache, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode cache: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("write temp cache: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("rename cache: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildCacheIndex creates a lookup map from filePath → index for incremental scanning.
|
||||
func BuildCacheIndex(cache *LibraryCache) map[string]int {
|
||||
if cache == nil {
|
||||
return nil
|
||||
}
|
||||
idx := make(map[string]int, len(cache.Items))
|
||||
for i, item := range cache.Items {
|
||||
idx[item.FilePath] = i
|
||||
}
|
||||
return idx
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue