unarr/internal/arr/discover_e2e_test.go
Deivid Soto 677a8fe083 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
2026-03-29 16:54:32 +02:00

180 lines
5.2 KiB
Go

package arr
import (
"fmt"
"net"
"os"
"testing"
"time"
)
// TestDiscoverE2E is an integration test that requires real *arr instances running.
// Skip if ports 8989/7878 are not reachable.
func TestDiscoverE2E(t *testing.T) {
if os.Getenv("ARR_E2E") == "" {
t.Skip("Set ARR_E2E=1 to run integration tests")
}
// Check ports are reachable
for _, port := range []string{"8989", "7878"} {
conn, err := net.DialTimeout("tcp", "localhost:"+port, 2*time.Second)
if err != nil {
t.Skipf("Port %s not reachable, skipping", port)
}
conn.Close()
}
t.Run("Discover", func(t *testing.T) {
instances := Discover()
if len(instances) == 0 {
t.Fatal("Discover() returned 0 instances")
}
for _, inst := range instances {
t.Logf("Found: %s at %s (source=%s, version=%s, hasKey=%v)",
inst.App, inst.URL, inst.Source, inst.Version, inst.APIKey != "")
}
})
t.Run("Radarr_Movies", func(t *testing.T) {
radarrKey := os.Getenv("RADARR_KEY")
if radarrKey == "" {
t.Skip("RADARR_KEY not set")
}
client := NewClient("http://localhost:7878", radarrKey)
status, err := client.SystemStatus()
if err != nil {
t.Fatalf("SystemStatus: %v", err)
}
t.Logf("Radarr %s", status.Version)
movies, err := client.Movies()
if err != nil {
t.Fatalf("Movies: %v", err)
}
t.Logf("Found %d movies", len(movies))
for _, m := range movies {
t.Logf(" %s (tmdb=%d, imdb=%s) monitored=%v hasFile=%v profile=%d",
m.Title, m.TmdbID, m.ImdbID, m.Monitored, m.HasFile, m.QualityProfileID)
}
if len(movies) < 3 {
t.Errorf("Expected at least 3 movies, got %d", len(movies))
}
profiles, err := client.QualityProfiles()
if err != nil {
t.Fatalf("QualityProfiles: %v", err)
}
t.Logf("Found %d quality profiles", len(profiles))
for _, p := range profiles {
mapped := MapQualityProfile(p)
t.Logf(" %s (id=%d) → %s", p.Name, p.ID, mapped)
}
folders, err := client.RootFolders()
if err != nil {
t.Fatalf("RootFolders: %v", err)
}
t.Logf("Found %d root folders", len(folders))
for _, f := range folders {
t.Logf(" %s", f.Path)
}
dcs, err := client.DownloadClients()
if err != nil {
t.Fatalf("DownloadClients: %v", err)
}
t.Logf("Found %d download clients", len(dcs))
// Test wanted extraction
wanted := ExtractWantedMovies(movies)
t.Logf("Wanted movies: %d", len(wanted))
for _, w := range wanted {
t.Logf(" %s (tmdb=%d)", w.Title, w.TmdbID)
}
if len(wanted) != 2 {
t.Errorf("Expected 2 wanted movies, got %d", len(wanted))
}
})
t.Run("Sonarr_Series", func(t *testing.T) {
sonarrKey := os.Getenv("SONARR_KEY")
if sonarrKey == "" {
t.Skip("SONARR_KEY not set")
}
client := NewClient("http://localhost:8989", sonarrKey)
status, err := client.SystemStatus()
if err != nil {
t.Fatalf("SystemStatus: %v", err)
}
t.Logf("Sonarr %s", status.Version)
series, err := client.Series()
if err != nil {
t.Fatalf("Series: %v", err)
}
t.Logf("Found %d series", len(series))
for _, s := range series {
t.Logf(" %s (tvdb=%d, imdb=%s) monitored=%v eps=%d/%d",
s.Title, s.TvdbID, s.ImdbID, s.Monitored,
s.Statistics.EpisodeFileCount, s.Statistics.EpisodeCount)
}
wanted := ExtractWantedSeries(series)
t.Logf("Wanted series: %d", len(wanted))
for _, w := range wanted {
t.Logf(" %s (imdb=%s)", w.Title, w.ImdbID)
}
if len(wanted) != 2 {
t.Errorf("Expected 2 wanted series, got %d", len(wanted))
}
})
t.Run("BuildMigrationResult", func(t *testing.T) {
radarrKey := os.Getenv("RADARR_KEY")
sonarrKey := os.Getenv("SONARR_KEY")
if radarrKey == "" || sonarrKey == "" {
t.Skip("RADARR_KEY and SONARR_KEY required")
}
rc := NewClient("http://localhost:7878", radarrKey)
sc := NewClient("http://localhost:8989", sonarrKey)
movies, _ := rc.Movies()
series, _ := sc.Series()
rp, _ := rc.QualityProfiles()
sp, _ := sc.QualityProfiles()
rf, _ := rc.RootFolders()
sf, _ := sc.RootFolders()
dcs, _ := rc.DownloadClients()
result := BuildMigrationResult(movies, series, rp, sp, rf, sf, nil, dcs)
fmt.Printf("\n=== Migration Result ===\n")
fmt.Printf(" MoviesDir: %s\n", result.MoviesDir)
fmt.Printf(" TVShowsDir: %s\n", result.TVShowsDir)
fmt.Printf(" Quality: %s (from %q)\n", result.Quality, result.QualitySource)
fmt.Printf(" Organize: %v\n", result.OrganizeEnabled)
fmt.Printf(" Movies: %d total, %d with files\n", result.TotalMovies, result.MoviesWithFiles)
fmt.Printf(" Series: %d total, %d complete\n", result.TotalSeries, result.SeriesComplete)
fmt.Printf(" Wanted movies: %d\n", len(result.WantedMovies))
fmt.Printf(" Wanted series: %d\n", len(result.WantedSeries))
if result.MoviesDir != "/data/media/movies" {
t.Errorf("MoviesDir = %q, want /data/media/movies", result.MoviesDir)
}
if result.TVShowsDir != "/data/media/tv" {
t.Errorf("TVShowsDir = %q, want /data/media/tv", result.TVShowsDir)
}
if len(result.WantedMovies) != 2 {
t.Errorf("WantedMovies = %d, want 2", len(result.WantedMovies))
}
if len(result.WantedSeries) != 2 {
t.Errorf("WantedSeries = %d, want 2", len(result.WantedSeries))
}
if !result.OrganizeEnabled {
t.Error("OrganizeEnabled should be true")
}
})
}