Goondex/internal/sync/service.go
Stu Leak 16fb407a3c v0.1.0-dev4: Add web frontend with UI component library
- Implement full web interface with Go html/template server
- Add GX component library (buttons, dialogs, tables, forms, etc.)
- Create scene/performer/studio/movie detail and listing pages
- Add Adult Empire scraper for additional metadata sources
- Implement movie support with database schema
- Add import and sync services for data management
- Include comprehensive API and frontend documentation
- Add custom color scheme and responsive layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 10:47:30 -05:00

296 lines
7.2 KiB
Go

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()
}