173 lines
5.5 KiB
Go
173 lines
5.5 KiB
Go
package migrate
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/TopherMayor/unified-media-manager/internal/db"
|
|
)
|
|
|
|
// ArrSources holds paths to arr SQLite databases.
|
|
type ArrSources struct {
|
|
Sonarr string
|
|
Radarr string
|
|
SonarrAnime string
|
|
RadarrAnime string
|
|
Lidarr string
|
|
Readarr string
|
|
Prowlarr string
|
|
}
|
|
|
|
// ImportCount tracks imported and skipped counts for a category.
|
|
type ImportCount struct {
|
|
Imported int
|
|
Skipped int
|
|
}
|
|
|
|
// Report holds the migration results.
|
|
type Report struct {
|
|
Indexers ImportCount
|
|
Series ImportCount
|
|
Movies ImportCount
|
|
Albums ImportCount
|
|
Books ImportCount
|
|
Files ImportCount
|
|
Profiles ImportCount
|
|
RootFolders ImportCount
|
|
Tags ImportCount
|
|
Blocklist ImportCount
|
|
Errors int
|
|
}
|
|
|
|
func (r *Report) String() string {
|
|
var b strings.Builder
|
|
b.WriteString("Migration Report:\n")
|
|
fmt.Fprintf(&b, " Indexers: %d imported, %d skipped\n", r.Indexers.Imported, r.Indexers.Skipped)
|
|
fmt.Fprintf(&b, " Series: %d imported, %d deduplicated\n", r.Series.Imported, r.Series.Skipped)
|
|
fmt.Fprintf(&b, " Movies: %d imported, %d deduplicated\n", r.Movies.Imported, r.Movies.Skipped)
|
|
fmt.Fprintf(&b, " Albums: %d imported, %d deduplicated\n", r.Albums.Imported, r.Albums.Skipped)
|
|
fmt.Fprintf(&b, " Books: %d imported, %d deduplicated\n", r.Books.Imported, r.Books.Skipped)
|
|
fmt.Fprintf(&b, " Files: %d imported\n", r.Files.Imported)
|
|
fmt.Fprintf(&b, " Profiles: %d imported\n", r.Profiles.Imported)
|
|
fmt.Fprintf(&b, " Root Folders: %d imported\n", r.RootFolders.Imported)
|
|
fmt.Fprintf(&b, " Tags: %d imported\n", r.Tags.Imported)
|
|
fmt.Fprintf(&b, " Blocklist: %d imported\n", r.Blocklist.Imported)
|
|
fmt.Fprintf(&b, " Errors: %d\n", r.Errors)
|
|
return b.String()
|
|
}
|
|
|
|
// Migrator orchestrates the data migration from arr SQLite databases to UMM PostgreSQL.
|
|
type Migrator struct {
|
|
db *db.DB
|
|
sources ArrSources
|
|
report Report
|
|
|
|
// Maps arr entity IDs to UMM media IDs, keyed by arr instance
|
|
sonarrSeriesMap map[int64]int64 // sonarr series ID → UMM media ID
|
|
sonarrAnimeSeriesMap map[int64]int64 // sonarr-anime series ID → UMM media ID
|
|
radarrMovieMap map[int64]int64 // radarr movie ID → UMM media ID
|
|
radarrAnimeMovieMap map[int64]int64 // radarr-anime movie ID → UMM media ID
|
|
lidarrAlbumMap map[int64]int64 // lidarr album ID → UMM media ID
|
|
readarrBookMap map[int64]int64 // readarr book ID → UMM media ID
|
|
}
|
|
|
|
// NewMigrator creates a new Migrator instance.
|
|
func NewMigrator(database *db.DB, sources ArrSources) *Migrator {
|
|
return &Migrator{
|
|
db: database,
|
|
sources: sources,
|
|
sonarrSeriesMap: make(map[int64]int64),
|
|
sonarrAnimeSeriesMap: make(map[int64]int64),
|
|
radarrMovieMap: make(map[int64]int64),
|
|
radarrAnimeMovieMap: make(map[int64]int64),
|
|
lidarrAlbumMap: make(map[int64]int64),
|
|
readarrBookMap: make(map[int64]int64),
|
|
}
|
|
}
|
|
|
|
// Run executes the full migration pipeline.
|
|
func (m *Migrator) Run(ctx context.Context) (*Report, error) {
|
|
slog.Info("starting arr data migration")
|
|
|
|
// Step 1: Import Prowlarr indexers
|
|
if m.sources.Prowlarr != "" {
|
|
slog.Info("importing prowlarr indexers", "path", m.sources.Prowlarr)
|
|
if err := m.importProwlarr(ctx); err != nil {
|
|
slog.Error("failed to import prowlarr", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 2: Import Sonarr series
|
|
if m.sources.Sonarr != "" {
|
|
slog.Info("importing sonarr series", "path", m.sources.Sonarr)
|
|
if err := m.importSonarr(ctx, m.sources.Sonarr, false); err != nil {
|
|
slog.Error("failed to import sonarr", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 3: Import Sonarr-anime (deduplicate by TVDB ID)
|
|
if m.sources.SonarrAnime != "" {
|
|
slog.Info("importing sonarr-anime series", "path", m.sources.SonarrAnime)
|
|
if err := m.importSonarr(ctx, m.sources.SonarrAnime, true); err != nil {
|
|
slog.Error("failed to import sonarr-anime", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 4: Import Radarr movies
|
|
if m.sources.Radarr != "" {
|
|
slog.Info("importing radarr movies", "path", m.sources.Radarr)
|
|
if err := m.importRadarr(ctx, m.sources.Radarr, false); err != nil {
|
|
slog.Error("failed to import radarr", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 5: Import Radarr-anime (deduplicate by TMDB ID)
|
|
if m.sources.RadarrAnime != "" {
|
|
slog.Info("importing radarr-anime movies", "path", m.sources.RadarrAnime)
|
|
if err := m.importRadarr(ctx, m.sources.RadarrAnime, true); err != nil {
|
|
slog.Error("failed to import radarr-anime", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 6: Import Lidarr artists + albums
|
|
if m.sources.Lidarr != "" {
|
|
slog.Info("importing lidarr", "path", m.sources.Lidarr)
|
|
if err := m.importLidarr(ctx); err != nil {
|
|
slog.Error("failed to import lidarr", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 7: Import Readarr books
|
|
if m.sources.Readarr != "" {
|
|
slog.Info("importing readarr", "path", m.sources.Readarr)
|
|
if err := m.importReadarr(ctx); err != nil {
|
|
slog.Error("failed to import readarr", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
}
|
|
|
|
// Step 8: Reset PostgreSQL sequences
|
|
if err := m.resetSequences(ctx); err != nil {
|
|
slog.Error("failed to reset sequences", "error", err)
|
|
m.report.Errors++
|
|
}
|
|
|
|
slog.Info("migration complete",
|
|
"series", m.report.Series.Imported,
|
|
"movies", m.report.Movies.Imported,
|
|
"albums", m.report.Albums.Imported,
|
|
"books", m.report.Books.Imported,
|
|
"files", m.report.Files.Imported,
|
|
"errors", m.report.Errors,
|
|
)
|
|
return &m.report, nil
|
|
}
|