- 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>
296 lines
7.2 KiB
Go
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()
|
|
}
|