Sync from /srv/compose/unified-media-manager
This commit is contained in:
172
internal/migrate/migrator.go
Normal file
172
internal/migrate/migrator.go
Normal file
@@ -0,0 +1,172 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user