package sync import ( "context" "fmt" "time" "git.leaktechnologies.dev/stu/Goondex/internal/db" "git.leaktechnologies.dev/stu/Goondex/internal/scraper/tpdb" ) // Service handles synchronization operations type Service struct { db *db.DB scraper *tpdb.Scraper } // NewService creates a new sync service func NewService(database *db.DB, scraper *tpdb.Scraper) *Service { return &Service{ db: database, scraper: scraper, } } // SyncOptions configures sync behavior type SyncOptions struct { Force bool // Force sync even if rate limit not met MinInterval time.Duration // Minimum time between syncs } // DefaultSyncOptions returns default sync options (1 hour minimum) func DefaultSyncOptions() SyncOptions { return SyncOptions{ Force: false, MinInterval: 1 * time.Hour, } } // SyncResult contains the results of a sync operation type SyncResult struct { EntityType string Updated int Failed int Skipped int Duration time.Duration ErrorMessage string } // SyncAll syncs all entity types (performers, studios, scenes) func (s *Service) SyncAll(ctx context.Context, opts SyncOptions) ([]SyncResult, error) { var results []SyncResult // Sync performers performerResult, err := s.SyncPerformers(ctx, opts) if err != nil { return results, fmt.Errorf("failed to sync performers: %w", err) } results = append(results, performerResult) // Sync studios studioResult, err := s.SyncStudios(ctx, opts) if err != nil { return results, fmt.Errorf("failed to sync studios: %w", err) } results = append(results, studioResult) // Sync scenes sceneResult, err := s.SyncScenes(ctx, opts) if err != nil { return results, fmt.Errorf("failed to sync scenes: %w", err) } results = append(results, sceneResult) return results, nil } // SyncPerformers syncs all performers from TPDB func (s *Service) SyncPerformers(ctx context.Context, opts SyncOptions) (SyncResult, error) { result := SyncResult{EntityType: "performers"} start := time.Now() defer func() { result.Duration = time.Since(start) }() syncStore := db.NewSyncStore(s.db) // Check rate limiting if !opts.Force { canSync, nextAllowed, err := syncStore.CanSync("performers", opts.MinInterval) if err != nil { return result, err } if !canSync { result.Skipped = 1 result.ErrorMessage = fmt.Sprintf("Rate limit: next sync allowed at %s", nextAllowed.Format(time.RFC3339)) return result, nil } } // Record sync start if err := syncStore.RecordSyncStart("performers"); err != nil { return result, err } performerStore := db.NewPerformerStore(s.db) // Get all performers with TPDB source performers, err := performerStore.Search("") if err != nil { syncStore.RecordSyncError("performers", err.Error()) return result, err } // Update each performer for _, p := range performers { if p.Source != "tpdb" || p.SourceID == "" { result.Skipped++ continue } // Fetch updated data from TPDB updated, err := s.scraper.GetPerformerByID(ctx, p.SourceID) if err != nil { fmt.Printf("⚠ Failed to fetch performer %s (ID: %d): %v\n", p.Name, p.ID, err) result.Failed++ continue } // Preserve local ID updated.ID = p.ID // Update in database if err := performerStore.Update(updated); err != nil { fmt.Printf("⚠ Failed to update performer %s (ID: %d): %v\n", p.Name, p.ID, err) result.Failed++ continue } result.Updated++ } // Record completion if err := syncStore.RecordSyncComplete("performers", result.Updated, result.Failed, result.ErrorMessage); err != nil { return result, err } return result, nil } // SyncStudios syncs all studios from TPDB func (s *Service) SyncStudios(ctx context.Context, opts SyncOptions) (SyncResult, error) { result := SyncResult{EntityType: "studios"} start := time.Now() defer func() { result.Duration = time.Since(start) }() syncStore := db.NewSyncStore(s.db) // Check rate limiting if !opts.Force { canSync, nextAllowed, err := syncStore.CanSync("studios", opts.MinInterval) if err != nil { return result, err } if !canSync { result.Skipped = 1 result.ErrorMessage = fmt.Sprintf("Rate limit: next sync allowed at %s", nextAllowed.Format(time.RFC3339)) return result, nil } } // Record sync start if err := syncStore.RecordSyncStart("studios"); err != nil { return result, err } studioStore := db.NewStudioStore(s.db) // Get all studios with TPDB source studios, err := studioStore.Search("") if err != nil { syncStore.RecordSyncError("studios", err.Error()) return result, err } // Update each studio for _, st := range studios { if st.Source != "tpdb" || st.SourceID == "" { result.Skipped++ continue } // Fetch updated data from TPDB updated, err := s.scraper.GetStudioByID(ctx, st.SourceID) if err != nil { fmt.Printf("⚠ Failed to fetch studio %s (ID: %d): %v\n", st.Name, st.ID, err) result.Failed++ continue } // Preserve local ID updated.ID = st.ID // Update in database if err := studioStore.Update(updated); err != nil { fmt.Printf("⚠ Failed to update studio %s (ID: %d): %v\n", st.Name, st.ID, err) result.Failed++ continue } result.Updated++ } // Record completion if err := syncStore.RecordSyncComplete("studios", result.Updated, result.Failed, result.ErrorMessage); err != nil { return result, err } return result, nil } // SyncScenes syncs all scenes from TPDB func (s *Service) SyncScenes(ctx context.Context, opts SyncOptions) (SyncResult, error) { result := SyncResult{EntityType: "scenes"} start := time.Now() defer func() { result.Duration = time.Since(start) }() syncStore := db.NewSyncStore(s.db) // Check rate limiting if !opts.Force { canSync, nextAllowed, err := syncStore.CanSync("scenes", opts.MinInterval) if err != nil { return result, err } if !canSync { result.Skipped = 1 result.ErrorMessage = fmt.Sprintf("Rate limit: next sync allowed at %s", nextAllowed.Format(time.RFC3339)) return result, nil } } // Record sync start if err := syncStore.RecordSyncStart("scenes"); err != nil { return result, err } sceneStore := db.NewSceneStore(s.db) // Get all scenes with TPDB source scenes, err := sceneStore.Search("") if err != nil { syncStore.RecordSyncError("scenes", err.Error()) return result, err } // Update each scene for _, sc := range scenes { if sc.Source != "tpdb" || sc.SourceID == "" { result.Skipped++ continue } // Fetch updated data from TPDB updated, err := s.scraper.GetSceneByID(ctx, sc.SourceID) if err != nil { fmt.Printf("⚠ Failed to fetch scene %s (ID: %d): %v\n", sc.Title, sc.ID, err) result.Failed++ continue } // Preserve local ID updated.ID = sc.ID // Update in database if err := sceneStore.Update(updated); err != nil { fmt.Printf("⚠ Failed to update scene %s (ID: %d): %v\n", sc.Title, sc.ID, err) result.Failed++ continue } result.Updated++ } // Record completion if err := syncStore.RecordSyncComplete("scenes", result.Updated, result.Failed, result.ErrorMessage); err != nil { return result, err } return result, nil } // GetSyncStatus returns the current sync status for all entity types func (s *Service) GetSyncStatus() ([]db.SyncMetadata, error) { syncStore := db.NewSyncStore(s.db) return syncStore.GetAllSyncStatus() }