package main import ( "context" "fmt" "os" "strconv" "time" "git.leaktechnologies.dev/stu/Goondex/internal/db" "git.leaktechnologies.dev/stu/Goondex/internal/config" "git.leaktechnologies.dev/stu/Goondex/internal/model" "git.leaktechnologies.dev/stu/Goondex/internal/scraper/adultemp" "git.leaktechnologies.dev/stu/Goondex/internal/scraper/merger" "git.leaktechnologies.dev/stu/Goondex/internal/scraper/tpdb" "git.leaktechnologies.dev/stu/Goondex/internal/sync" "git.leaktechnologies.dev/stu/Goondex/internal/web" "github.com/spf13/cobra" ) const tpdbAPIKeyEnvVar = "TPDB_API_KEY" const tpdbEnabled = true var ( dbPath string rootCmd = &cobra.Command{ Use: "goondex", Short: "Goondex - Fast, local-first media indexer", Long: `Goondex is a fast, local-first media indexer for adult content that ingests metadata from external sources.`, } ) func init() { rootCmd.PersistentFlags().StringVar(&dbPath, "db", "./goondex.db", "Path to SQLite database") // Add subcommands rootCmd.AddCommand(performerSearchCmd) rootCmd.AddCommand(performerGetCmd) rootCmd.AddCommand(performerUpdateCmd) rootCmd.AddCommand(studioSearchCmd) rootCmd.AddCommand(studioGetCmd) rootCmd.AddCommand(sceneSearchCmd) rootCmd.AddCommand(sceneGetCmd) rootCmd.AddCommand(importCmd) rootCmd.AddCommand(syncCmd) rootCmd.AddCommand(adultempCmd) rootCmd.AddCommand(webCmd) rootCmd.AddCommand(enrichCmd) rootCmd.AddCommand(versionCmd) performerSearchCmd.Flags().Bool("show-bio", false, "Show performer bio in search results") performerGetCmd.Flags().Bool("show-bio", false, "Show performer bio") webCmd.Flags().String("addr", "localhost:8788", "Address to listen on") // Sync command flags syncCmd.PersistentFlags().Bool("force", false, "Force sync even if rate limit not met") syncCmd.PersistentFlags().Duration("interval", 1*time.Hour, "Minimum interval between syncs (default 1h, recommended 24h)") } // Import command with subcommands var importCmd = &cobra.Command{ Use: "import", Short: "Import data (Adult Empire available; TPDB disabled)", Long: `Adult Empire import commands are available. TPDB bulk import commands are temporarily disabled while we focus on Adult Empire data.`, } var importAllCmd = &cobra.Command{ Use: "all", Short: "Import all TPDB performers, studios, and scenes", Long: `Run the performer, studio, and scene bulk imports sequentially to ingest the entire TPDB catalog. Use the per-entity pagination flags to resume or limit parts of the import.`, RunE: func(cmd *cobra.Command, args []string) error { performerStart, _ := cmd.Flags().GetInt("performer-start-page") performerMax, _ := cmd.Flags().GetInt("performer-max-pages") studioStart, _ := cmd.Flags().GetInt("studio-start-page") studioMax, _ := cmd.Flags().GetInt("studio-max-pages") sceneStart, _ := cmd.Flags().GetInt("scene-start-page") sceneMax, _ := cmd.Flags().GetInt("scene-max-pages") // Propagate flag values to the existing per-entity commands if err := importAllPerformersCmd.Flags().Set("start-page", strconv.Itoa(performerStart)); err != nil { return fmt.Errorf("failed to set performer start page: %w", err) } if err := importAllPerformersCmd.Flags().Set("max-pages", strconv.Itoa(performerMax)); err != nil { return fmt.Errorf("failed to set performer max pages: %w", err) } if err := importAllStudiosCmd.Flags().Set("start-page", strconv.Itoa(studioStart)); err != nil { return fmt.Errorf("failed to set studio start page: %w", err) } if err := importAllStudiosCmd.Flags().Set("max-pages", strconv.Itoa(studioMax)); err != nil { return fmt.Errorf("failed to set studio max pages: %w", err) } if err := importAllScenesCmd.Flags().Set("start-page", strconv.Itoa(sceneStart)); err != nil { return fmt.Errorf("failed to set scene start page: %w", err) } if err := importAllScenesCmd.Flags().Set("max-pages", strconv.Itoa(sceneMax)); err != nil { return fmt.Errorf("failed to set scene max pages: %w", err) } fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ TPDB BULK IMPORT - ALL ENTITIES ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Println("\n➡ Importing performers...") if err := importAllPerformersCmd.RunE(importAllPerformersCmd, []string{}); err != nil { return fmt.Errorf("performer import failed: %w", err) } fmt.Println("\n➡ Importing studios...") if err := importAllStudiosCmd.RunE(importAllStudiosCmd, []string{}); err != nil { return fmt.Errorf("studio import failed: %w", err) } fmt.Println("\n➡ Importing scenes...") if err := importAllScenesCmd.RunE(importAllScenesCmd, []string{}); err != nil { return fmt.Errorf("scene import failed: %w", err) } fmt.Println("\n✓ Completed TPDB bulk import for performers, studios, and scenes") return nil }, } func init() { importCmd.AddCommand(importPerformerCmd) importCmd.AddCommand(importAllPerformersCmd) importCmd.AddCommand(importStudioCmd) importCmd.AddCommand(importAllStudiosCmd) importCmd.AddCommand(importSceneCmd) importCmd.AddCommand(importAllScenesCmd) importCmd.AddCommand(importMovieCmd) importCmd.AddCommand(importAllCmd) // Flags for bulk import importAllPerformersCmd.Flags().Int("start-page", 1, "Page to start from (for resuming)") importAllPerformersCmd.Flags().Int("max-pages", 0, "Maximum pages to import (0 = all)") importAllStudiosCmd.Flags().Int("start-page", 1, "Page to start from (for resuming)") importAllStudiosCmd.Flags().Int("max-pages", 0, "Maximum pages to import (0 = all)") importAllScenesCmd.Flags().Int("start-page", 1, "Page to start from (for resuming)") importAllScenesCmd.Flags().Int("max-pages", 0, "Maximum pages to import (0 = all)") importAllCmd.Flags().Int("performer-start-page", 1, "Performer import start page (for resuming)") importAllCmd.Flags().Int("performer-max-pages", 0, "Maximum performer pages to import (0 = all)") importAllCmd.Flags().Int("studio-start-page", 1, "Studio import start page (for resuming)") importAllCmd.Flags().Int("studio-max-pages", 0, "Maximum studio pages to import (0 = all)") importAllCmd.Flags().Int("scene-start-page", 1, "Scene import start page (for resuming)") importAllCmd.Flags().Int("scene-max-pages", 0, "Maximum scene pages to import (0 = all)") // Movie import flags importMovieCmd.Flags().String("source", "adultemp", "Source to import from (adultemp)") } // Sync command with subcommands var syncCmd = &cobra.Command{ Use: "sync", Short: "Sync and update existing data (TPDB disabled for now)", Long: `TPDB-based sync is temporarily disabled while we prioritize Adult Empire as the main source. Existing sync commands are left in place for when TPDB is re-enabled, but they will currently return a disabled message.`, } func init() { syncCmd.AddCommand(syncAllCmd) syncCmd.AddCommand(syncPerformersCmd) syncCmd.AddCommand(syncStudiosCmd) syncCmd.AddCommand(syncScenesCmd) syncCmd.AddCommand(syncStatusCmd) } var syncAllCmd = &cobra.Command{ Use: "all", Short: "Sync all entities (performers, studios, scenes)", RunE: func(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") interval, _ := cmd.Flags().GetDuration("interval") apiKey, err := getTPDBAPIKey() if err != nil { return err } database, err := getDB() if err != nil { return err } defer database.Close() scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) service := sync.NewService(database, scraper) opts := sync.SyncOptions{ Force: force, MinInterval: interval, } fmt.Printf("Starting sync with %s minimum interval...\n", interval) if force { fmt.Println("⚠ Force mode enabled - ignoring rate limits") } fmt.Println() results, err := service.SyncAll(context.Background(), opts) if err != nil { return err } // Display results for _, r := range results { fmt.Printf("═══ %s ═══\n", r.EntityType) if r.ErrorMessage != "" { fmt.Printf("⚠ %s\n", r.ErrorMessage) } else { fmt.Printf("✓ Updated: %d\n", r.Updated) if r.Failed > 0 { fmt.Printf("✗ Failed: %d\n", r.Failed) } if r.Skipped > 0 { fmt.Printf("- Skipped: %d\n", r.Skipped) } fmt.Printf("⏱ Duration: %s\n", r.Duration.Round(time.Second)) } fmt.Println() } return nil }, } var syncPerformersCmd = &cobra.Command{ Use: "performers", Short: "Sync performers only", RunE: func(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") interval, _ := cmd.Flags().GetDuration("interval") apiKey, err := getTPDBAPIKey() if err != nil { return err } database, err := getDB() if err != nil { return err } defer database.Close() scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) service := sync.NewService(database, scraper) opts := sync.SyncOptions{ Force: force, MinInterval: interval, } fmt.Printf("Syncing performers...\n") result, err := service.SyncPerformers(context.Background(), opts) if err != nil { return err } if result.ErrorMessage != "" { fmt.Printf("⚠ %s\n", result.ErrorMessage) } else { fmt.Printf("✓ Updated: %d\n", result.Updated) if result.Failed > 0 { fmt.Printf("✗ Failed: %d\n", result.Failed) } if result.Skipped > 0 { fmt.Printf("- Skipped: %d\n", result.Skipped) } fmt.Printf("⏱ Duration: %s\n", result.Duration.Round(time.Second)) } return nil }, } var syncStudiosCmd = &cobra.Command{ Use: "studios", Short: "Sync studios only", RunE: func(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") interval, _ := cmd.Flags().GetDuration("interval") apiKey, err := getTPDBAPIKey() if err != nil { return err } database, err := getDB() if err != nil { return err } defer database.Close() scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) service := sync.NewService(database, scraper) opts := sync.SyncOptions{ Force: force, MinInterval: interval, } fmt.Printf("Syncing studios...\n") result, err := service.SyncStudios(context.Background(), opts) if err != nil { return err } if result.ErrorMessage != "" { fmt.Printf("⚠ %s\n", result.ErrorMessage) } else { fmt.Printf("✓ Updated: %d\n", result.Updated) if result.Failed > 0 { fmt.Printf("✗ Failed: %d\n", result.Failed) } if result.Skipped > 0 { fmt.Printf("- Skipped: %d\n", result.Skipped) } fmt.Printf("⏱ Duration: %s\n", result.Duration.Round(time.Second)) } return nil }, } var syncScenesCmd = &cobra.Command{ Use: "scenes", Short: "Sync scenes only", RunE: func(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") interval, _ := cmd.Flags().GetDuration("interval") apiKey, err := getTPDBAPIKey() if err != nil { return err } database, err := getDB() if err != nil { return err } defer database.Close() scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) service := sync.NewService(database, scraper) opts := sync.SyncOptions{ Force: force, MinInterval: interval, } fmt.Printf("Syncing scenes...\n") result, err := service.SyncScenes(context.Background(), opts) if err != nil { return err } if result.ErrorMessage != "" { fmt.Printf("⚠ %s\n", result.ErrorMessage) } else { fmt.Printf("✓ Updated: %d\n", result.Updated) if result.Failed > 0 { fmt.Printf("✗ Failed: %d\n", result.Failed) } if result.Skipped > 0 { fmt.Printf("- Skipped: %d\n", result.Skipped) } fmt.Printf("⏱ Duration: %s\n", result.Duration.Round(time.Second)) } return nil }, } var syncStatusCmd = &cobra.Command{ Use: "status", Short: "View sync status and last sync times", RunE: func(cmd *cobra.Command, args []string) error { database, err := getDB() if err != nil { return err } defer database.Close() scraper := tpdb.NewScraper("https://api.theporndb.net", "") service := sync.NewService(database, scraper) statuses, err := service.GetSyncStatus() if err != nil { return err } if len(statuses) == 0 { fmt.Println("No sync history found.") fmt.Println("\nRun 'goondex sync all' to perform your first sync.") return nil } fmt.Println("Sync Status:") fmt.Println() for _, s := range statuses { fmt.Printf("═══ %s ═══\n", s.EntityType) fmt.Printf("Status: %s\n", s.Status) fmt.Printf("Last Sync: %s\n", s.LastSyncAt.Format("2006-01-02 15:04:05")) fmt.Printf("Updated: %d\n", s.RecordsUpdated) if s.RecordsFailed > 0 { fmt.Printf("Failed: %d\n", s.RecordsFailed) } if s.ErrorMessage != "" { fmt.Printf("Error: %s\n", s.ErrorMessage) } // Calculate time since last sync timeSince := time.Since(s.LastSyncAt) fmt.Printf("Time since last sync: %s\n", formatDuration(timeSince)) fmt.Println() } return nil }, } func formatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%d seconds", int(d.Seconds())) } if d < time.Hour { return fmt.Sprintf("%d minutes", int(d.Minutes())) } if d < 24*time.Hour { return fmt.Sprintf("%d hours", int(d.Hours())) } return fmt.Sprintf("%d days", int(d.Hours()/24)) } func getTPDBAPIKey() (string, error) { if !tpdbEnabled { return "", fmt.Errorf("TPDB integration is disabled. Use Adult Empire commands (e.g., 'goondex adultemp search-performer' and 'goondex adultemp scrape-performer ') to import data instead.") } apiKey := config.GetAPIKeys().TPDBAPIKey if apiKey == "" { return "", fmt.Errorf("%s environment variable is not set.\n%s", tpdbAPIKeyEnvVar, apiKeySetupInstructions()) } return apiKey, nil } func apiKeySetupInstructions() string { return fmt.Sprintf("Set it by running:\n\n export %s=\"your-api-key\"", tpdbAPIKeyEnvVar) } func main() { if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } func getDB() (*db.DB, error) { database, err := db.Open(dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } return database, nil } var webCmd = &cobra.Command{ Use: "web", Short: "Start the web UI server", Long: `Start a web server that provides a visual interface for browsing your Goondex database.`, RunE: func(cmd *cobra.Command, args []string) error { addr, _ := cmd.Flags().GetString("addr") database, err := getDB() if err != nil { return err } defer database.Close() server, err := web.NewServer(database, addr, dbPath) if err != nil { return fmt.Errorf("failed to create web server: %w", err) } return server.Start() }, } var versionCmd = &cobra.Command{ Use: "version", Short: "Print version information", Run: func(cmd *cobra.Command, args []string) { fmt.Println("Goondex v0.1.0-dev5") fmt.Println("Features:") fmt.Println(" • Adult Empire scraper (scenes & performers)") fmt.Println(" • TPDB commands temporarily disabled (Adult Empire-only mode)") fmt.Println(" • Multi-source data merging (when TPDB is re-enabled)") fmt.Println(" • Grid-based web UI with GX components") fmt.Println(" • Performer/studio/scene management") }, } var performerSearchCmd = &cobra.Command{ Use: "performer-search [query]", Short: "Search for performers (imports from Adult Empire if missing locally)", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] showBio, _ := cmd.Flags().GetBool("show-bio") database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewPerformerStore(database) performers, err := store.Search(query) if err != nil { return fmt.Errorf("search failed: %w", err) } // If no local results, pull the top Adult Empire match if len(performers) == 0 { fmt.Printf("No local results found. Searching Adult Empire for '%s'...\n", query) scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create Adult Empire scraper: %w", err) } results, err := scraper.SearchPerformersByName(context.Background(), query) if err != nil { fmt.Printf("⚠ Adult Empire search failed: %v\n", err) return nil } if len(results) == 0 { fmt.Println("No performers found on Adult Empire either.") return nil } fmt.Printf("Found %d performer(s) on Adult Empire. Importing the top match...\n\n", len(results)) // Import only the top result to avoid wrong matches; use search-performer for manual selection top := results[0] performerData, err := scraper.ScrapePerformerByURL(context.Background(), top.URL) if err != nil { fmt.Printf("⚠ Failed to fetch Adult Empire details: %v\n", err) return nil } performer := scraper.ConvertPerformerToModel(performerData) if err := store.Create(performer); err != nil { fmt.Printf("⚠ Failed to import %s: %v\n", performer.Name, err) } else { fmt.Printf("✓ Imported %s from Adult Empire\n", performer.Name) } // Search again to get the imported performer with their ID performers, err = store.Search(query) if err != nil { return fmt.Errorf("search failed after import: %w", err) } if len(performers) == 0 { fmt.Println("No performers matched locally even after Adult Empire import.") return nil } if len(results) > 1 { fmt.Println("Tip: run 'goondex adultemp search-performer \"\"' to pick a different match.") } imported := 1 fmt.Printf("\n✓ Imported %d performer(s)\n\n", imported) } fmt.Printf("Found %d performer(s):\n\n", len(performers)) for _, p := range performers { fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Name: %s\n", p.Name) if p.Aliases != "" { fmt.Printf("Aliases: %s\n", p.Aliases) } fmt.Printf("\n") // IDs fmt.Printf("Local ID: %d\n", p.ID) if p.Source != "" { fmt.Printf("Source: %s\n", p.Source) fmt.Printf("UUID: %s\n", p.SourceID) if p.SourceNumericID > 0 { fmt.Printf("TPDB ID: %d\n", p.SourceNumericID) } } fmt.Printf("\n") // Stats sceneCount, err := store.GetSceneCount(p.ID) if err != nil { fmt.Printf("⚠ Failed to get scene count: %v\n", err) } else { fmt.Printf("Scenes: %d\n", sceneCount) } fmt.Printf("\n") // Personal info if p.Gender != "" { fmt.Printf("Gender: %s\n", p.Gender) } if p.Birthday != "" { fmt.Printf("Birthday: %s\n", p.Birthday) } if p.Astrology != "" { fmt.Printf("Astrology: %s\n", p.Astrology) } if p.DateOfDeath != "" { fmt.Printf("Date of Death: %s\n", p.DateOfDeath) } if p.Career != "" { fmt.Printf("Career: %s\n", p.Career) } if p.Birthplace != "" { fmt.Printf("Birthplace: %s\n", p.Birthplace) } if p.Ethnicity != "" { fmt.Printf("Ethnicity: %s\n", p.Ethnicity) } if p.Nationality != "" { fmt.Printf("Nationality: %s\n", p.Nationality) } fmt.Printf("\n") // Physical attributes if p.CupSize != "" { fmt.Printf("Cup Size: %s\n", p.CupSize) } if p.HairColor != "" { fmt.Printf("Hair Colour: %s\n", p.HairColor) } if p.EyeColor != "" { fmt.Printf("Eye Colour: %s\n", p.EyeColor) } if p.Height > 0 { feet := float64(p.Height) / 30.48 inches := (float64(p.Height) / 2.54) - (feet * 12) fmt.Printf("Height: %dcm (%.0f'%.0f\")\n", p.Height, feet, inches) } if p.Weight > 0 { lbs := float64(p.Weight) * 2.20462 fmt.Printf("Weight: %dkg (%.0flb)\n", p.Weight, lbs) } if p.Measurements != "" { fmt.Printf("Measurements: %s\n", p.Measurements) } if p.TattooDescription != "" { fmt.Printf("Tattoos: %s\n", p.TattooDescription) } if p.PiercingDescription != "" { fmt.Printf("Piercings: %s\n", p.PiercingDescription) } if p.BoobJob != "" { fmt.Printf("Fake Boobs: %s\n", p.BoobJob) } fmt.Printf("\n") // Bio if showBio && p.Bio != "" { fmt.Printf("Bio:\n%s\n\n", p.Bio) } // Media if p.ImageURL != "" { fmt.Printf("Image: %s\n", p.ImageURL) } if p.PosterURL != "" { fmt.Printf("Poster: %s\n", p.PosterURL) } fmt.Printf("═══════════════════════════════════════════════════\n\n") } return nil }, } var performerUpdateCmd = &cobra.Command{ Use: "performer-update [id]", Short: "Update/refresh performer data from TPDB and Adult Empire", Long: `Update an existing performer with the latest data from TPDB and optionally merge with Adult Empire data.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // Parse ID var performerID int64 if _, err := fmt.Sscanf(args[0], "%d", &performerID); err != nil { return fmt.Errorf("invalid performer ID: %s", args[0]) } database, err := getDB() if err != nil { return err } defer database.Close() performerStore := db.NewPerformerStore(database) // Get existing performer performer, err := performerStore.GetByID(performerID) if err != nil { return fmt.Errorf("performer not found: %w", err) } fmt.Printf("Updating performer: %s (ID: %d)\n", performer.Name, performer.ID) // Update from TPDB if available if performer.Source == "tpdb" && performer.SourceID != "" { apiKey, err := getTPDBAPIKey() if err != nil { fmt.Printf("⚠ %s\n", err) } else { fmt.Println("📥 Fetching latest data from TPDB...") scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) updatedPerformer, err := scraper.GetPerformerByID(context.Background(), performer.SourceID) if err != nil { fmt.Printf("⚠ Failed to fetch from TPDB: %v\n", err) } else { // Preserve local ID updatedPerformer.ID = performer.ID if err := performerStore.Update(updatedPerformer); err != nil { fmt.Printf("⚠ Failed to update: %v\n", err) } else { fmt.Println("✓ Updated from TPDB") performer = updatedPerformer } } } } // Optionally search Adult Empire for additional data fmt.Printf("\n🔍 Searching Adult Empire for '%s'...\n", performer.Name) adultempScraper, err := adultemp.NewScraper() if err != nil { fmt.Printf("⚠ Failed to create Adult Empire scraper: %v\n", err) return nil } results, err := adultempScraper.SearchPerformersByName(context.Background(), performer.Name) if err != nil { fmt.Printf("⚠ Adult Empire search failed: %v\n", err) return nil } if len(results) == 0 { fmt.Println("No matches found on Adult Empire") return nil } fmt.Printf("\nFound %d potential match(es) on Adult Empire:\n", len(results)) for i, result := range results { fmt.Printf("%d. %s - %s\n", i+1, result.Title, result.URL) } fmt.Println("\n✓ Performer data refreshed successfully") fmt.Println("Note: Use 'adultemp scrape-performer ' to import specific Adult Empire data") return nil }, } var performerGetCmd = &cobra.Command{ Use: "performer-get [id]", Short: "Get detailed information about a performer by ID", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // Parse ID var performerID int64 if _, err := fmt.Sscanf(args[0], "%d", &performerID); err != nil { return fmt.Errorf("invalid performer ID: %s", args[0]) } showBio, _ := cmd.Flags().GetBool("show-bio") database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewPerformerStore(database) // Get performer performer, err := store.GetByID(performerID) if err != nil { return fmt.Errorf("performer not found: %w", err) } // Get scene count sceneCount, err := store.GetSceneCount(performerID) if err != nil { fmt.Printf("⚠ Failed to get scene count: %v\n", err) sceneCount = 0 } // Display performer information (reusing the format from performer-search) fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Name: %s\n", performer.Name) if performer.Aliases != "" { fmt.Printf("Aliases: %s\n", performer.Aliases) } fmt.Printf("\n") // IDs fmt.Printf("Local ID: %d\n", performer.ID) if performer.Source != "" { fmt.Printf("Source: %s\n", performer.Source) fmt.Printf("UUID: %s\n", performer.SourceID) if performer.SourceNumericID > 0 { fmt.Printf("TPDB ID: %d\n", performer.SourceNumericID) } } fmt.Printf("\n") // Stats fmt.Printf("Scenes: %d\n", sceneCount) fmt.Printf("\n") // Personal info if performer.Gender != "" { fmt.Printf("Gender: %s\n", performer.Gender) } if performer.Birthday != "" { fmt.Printf("Birthday: %s\n", performer.Birthday) } if performer.Astrology != "" { fmt.Printf("Astrology: %s\n", performer.Astrology) } if performer.DateOfDeath != "" { fmt.Printf("Date of Death: %s\n", performer.DateOfDeath) } if performer.Career != "" { fmt.Printf("Career: %s\n", performer.Career) } if performer.Birthplace != "" { fmt.Printf("Birthplace: %s\n", performer.Birthplace) } if performer.Ethnicity != "" { fmt.Printf("Ethnicity: %s\n", performer.Ethnicity) } if performer.Nationality != "" { fmt.Printf("Nationality: %s\n", performer.Nationality) } fmt.Printf("\n") // Physical attributes if performer.CupSize != "" { fmt.Printf("Cup Size: %s\n", performer.CupSize) } if performer.HairColor != "" { fmt.Printf("Hair Colour: %s\n", performer.HairColor) } if performer.EyeColor != "" { fmt.Printf("Eye Colour: %s\n", performer.EyeColor) } if performer.Height > 0 { feet := float64(performer.Height) / 30.48 inches := (float64(performer.Height) / 2.54) - (feet * 12) fmt.Printf("Height: %dcm (%.0f'%.0f\")\n", performer.Height, feet, inches) } if performer.Weight > 0 { lbs := float64(performer.Weight) * 2.20462 fmt.Printf("Weight: %dkg (%.0flb)\n", performer.Weight, lbs) } if performer.Measurements != "" { fmt.Printf("Measurements: %s\n", performer.Measurements) } if performer.TattooDescription != "" { fmt.Printf("Tattoos: %s\n", performer.TattooDescription) } if performer.PiercingDescription != "" { fmt.Printf("Piercings: %s\n", performer.PiercingDescription) } if performer.BoobJob != "" { fmt.Printf("Fake Boobs: %s\n", performer.BoobJob) } fmt.Printf("\n") // Bio if showBio && performer.Bio != "" { fmt.Printf("Bio:\n%s\n\n", performer.Bio) } // Media if performer.ImageURL != "" { fmt.Printf("Image: %s\n", performer.ImageURL) } if performer.PosterURL != "" { fmt.Printf("Poster: %s\n", performer.PosterURL) } // Timestamps fmt.Printf("\n") fmt.Printf("Created: %s\n", performer.CreatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("Updated: %s\n", performer.UpdatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("═══════════════════════════════════════════════════\n\n") return nil }, } var studioSearchCmd = &cobra.Command{ Use: "studio-search [query]", Short: "Search for studios (local database only; TPDB disabled)", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewStudioStore(database) studios, err := store.Search(query) if err != nil { return fmt.Errorf("search failed: %w", err) } fmt.Printf("Found %d studio(s):\n\n", len(studios)) for _, s := range studios { fmt.Printf("ID: %d\n", s.ID) fmt.Printf("Name: %s\n", s.Name) if s.Description != "" { fmt.Printf("Description: %s\n", s.Description) } if s.Source != "" { fmt.Printf("Source: %s (ID: %s)\n", s.Source, s.SourceID) } fmt.Println("---") } return nil }, } var studioGetCmd = &cobra.Command{ Use: "studio-get [id]", Short: "Get detailed information about a studio by ID", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // Parse ID var studioID int64 if _, err := fmt.Sscanf(args[0], "%d", &studioID); err != nil { return fmt.Errorf("invalid studio ID: %s", args[0]) } database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewStudioStore(database) // Get studio studio, err := store.GetByID(studioID) if err != nil { return fmt.Errorf("studio not found: %w", err) } // Get scene count sceneCount, err := store.GetSceneCount(studioID) if err != nil { fmt.Printf("⚠ Failed to get scene count: %v\n", err) sceneCount = 0 } // Display studio information fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Studio Details\n") fmt.Printf("═══════════════════════════════════════════════════\n\n") fmt.Printf("ID: %d\n", studio.ID) fmt.Printf("Name: %s\n", studio.Name) if studio.Description != "" { fmt.Printf("\nDescription:\n%s\n", studio.Description) } fmt.Printf("\n") fmt.Printf("Scenes: %d\n", sceneCount) // Parent studio if studio.ParentID != nil && *studio.ParentID > 0 { parentStudio, err := store.GetByID(*studio.ParentID) if err == nil { fmt.Printf("Parent Studio: %s (ID: %d)\n", parentStudio.Name, parentStudio.ID) } else { fmt.Printf("Parent Studio ID: %d\n", *studio.ParentID) } } // Source information if studio.Source != "" { fmt.Printf("\n") fmt.Printf("Source: %s\n", studio.Source) fmt.Printf("Source ID: %s\n", studio.SourceID) } // Media if studio.ImageURL != "" { fmt.Printf("\n") fmt.Printf("Image: %s\n", studio.ImageURL) } if studio.ImagePath != "" { fmt.Printf("Image Path: %s\n", studio.ImagePath) } // Timestamps fmt.Printf("\n") fmt.Printf("Created: %s\n", studio.CreatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("Updated: %s\n", studio.UpdatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("\n═══════════════════════════════════════════════════\n") return nil }, } var sceneSearchCmd = &cobra.Command{ Use: "scene-search [query]", Short: "Search for scenes (local database only; TPDB disabled)", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewSceneStore(database) scenes, err := store.Search(query) if err != nil { return fmt.Errorf("search failed: %w", err) } fmt.Printf("Found %d scene(s):\n\n", len(scenes)) for _, sc := range scenes { fmt.Printf("ID: %d\n", sc.ID) fmt.Printf("Title: %s\n", sc.Title) if sc.Code != "" { fmt.Printf("Code: %s\n", sc.Code) } if sc.Date != "" { fmt.Printf("Date: %s\n", sc.Date) } if sc.Description != "" { fmt.Printf("Description: %s\n", sc.Description) } if sc.Source != "" { fmt.Printf("Source: %s (ID: %s)\n", sc.Source, sc.SourceID) } fmt.Println("---") } return nil }, } var sceneGetCmd = &cobra.Command{ Use: "scene-get [id]", Short: "Get detailed information about a scene by ID", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // Parse ID var sceneID int64 if _, err := fmt.Sscanf(args[0], "%d", &sceneID); err != nil { return fmt.Errorf("invalid scene ID: %s", args[0]) } database, err := getDB() if err != nil { return err } defer database.Close() sceneStore := db.NewSceneStore(database) studioStore := db.NewStudioStore(database) // Get scene scene, err := sceneStore.GetByID(sceneID) if err != nil { return fmt.Errorf("scene not found: %w", err) } // Get performers performers, err := sceneStore.GetPerformers(sceneID) if err != nil { fmt.Printf("⚠ Failed to get performers: %v\n", err) } // Get tags tags, err := sceneStore.GetTags(sceneID) if err != nil { fmt.Printf("⚠ Failed to get tags: %v\n", err) } // Get studio if present var studio *model.Studio if scene.StudioID != nil && *scene.StudioID > 0 { studio, err = studioStore.GetByID(*scene.StudioID) if err != nil { fmt.Printf("⚠ Failed to get studio: %v\n", err) } } // Display scene information fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Scene Details\n") fmt.Printf("═══════════════════════════════════════════════════\n\n") fmt.Printf("ID: %d\n", scene.ID) fmt.Printf("Title: %s\n", scene.Title) if scene.Code != "" { fmt.Printf("Code: %s\n", scene.Code) } if scene.Date != "" { fmt.Printf("Date: %s\n", scene.Date) } if scene.Director != "" { fmt.Printf("Director: %s\n", scene.Director) } fmt.Printf("\n") // Studio if studio != nil { fmt.Printf("Studio: %s (ID: %d)\n", studio.Name, studio.ID) } // Performers if len(performers) > 0 { fmt.Printf("\nPerformers (%d):\n", len(performers)) for _, p := range performers { fmt.Printf(" - %s (ID: %d)\n", p.Name, p.ID) } } // Tags if len(tags) > 0 { fmt.Printf("\nTags (%d):\n", len(tags)) for _, t := range tags { fmt.Printf(" - %s\n", t.Name) } } // Description if scene.Description != "" { fmt.Printf("\nDescription:\n%s\n", scene.Description) } // Source information if scene.Source != "" { fmt.Printf("\n") fmt.Printf("Source: %s\n", scene.Source) fmt.Printf("Source ID: %s\n", scene.SourceID) } // Media if scene.URL != "" { fmt.Printf("\nURL: %s\n", scene.URL) } if scene.ImageURL != "" { fmt.Printf("Image: %s\n", scene.ImageURL) } if scene.ImagePath != "" { fmt.Printf("Image Path: %s\n", scene.ImagePath) } // Timestamps fmt.Printf("\n") fmt.Printf("Created: %s\n", scene.CreatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("Updated: %s\n", scene.UpdatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("\n═══════════════════════════════════════════════════\n") return nil }, } var importAllPerformersCmd = &cobra.Command{ Use: "all-performers", Short: "Import ALL performers from TPDB (paginated)", Long: `Import all 10,000+ performers from ThePornDB by paginating through all pages. This may take a while.`, RunE: func(cmd *cobra.Command, args []string) error { startPage, _ := cmd.Flags().GetInt("start-page") maxPages, _ := cmd.Flags().GetInt("max-pages") // Get API key from environment apiKey, err := getTPDBAPIKey() if err != nil { return err } // Create TPDB scraper scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) // Open database database, err := getDB() if err != nil { return err } defer database.Close() performerStore := db.NewPerformerStore(database) fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ TPDB BULK PERFORMER IMPORT ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Println() totalImported := 0 totalFailed := 0 totalSkipped := 0 currentPage := startPage for { fmt.Printf("📥 Fetching page %d...\n", currentPage) performers, meta, err := scraper.ListPerformers(context.Background(), currentPage) if err != nil { fmt.Printf("⚠ Failed to fetch page %d: %v\n", currentPage, err) totalFailed++ currentPage++ continue } if meta == nil { fmt.Println("⚠ No metadata returned") break } fmt.Printf(" Page %d/%d | Total: %d performers | Per page: %d\n", meta.CurrentPage, meta.LastPage, meta.Total, meta.PerPage) // Import performers from this page pageImported := 0 pageFailed := 0 pageSkipped := 0 for _, p := range performers { // Check if already exists existing, _ := performerStore.GetBySourceID("tpdb", p.SourceID) if existing != nil { pageSkipped++ totalSkipped++ continue } if err := performerStore.Create(&p); err != nil { fmt.Printf(" ⚠ Failed to import %s: %v\n", p.Name, err) pageFailed++ totalFailed++ continue } pageImported++ totalImported++ } fmt.Printf(" ✓ Imported: %d | ⚠ Failed: %d | ⊘ Skipped: %d\n\n", pageImported, pageFailed, pageSkipped) // Check if we should stop if maxPages > 0 && (currentPage-startPage+1) >= maxPages { fmt.Printf("⏹ Reached maximum pages limit (%d)\n", maxPages) break } if currentPage >= meta.LastPage { fmt.Println("✓ Reached last page") break } currentPage++ // Small delay to be nice to the API time.Sleep(500 * time.Millisecond) } fmt.Println() fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ IMPORT COMPLETE ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Printf("✓ Total Imported: %d\n", totalImported) fmt.Printf("⚠ Total Failed: %d\n", totalFailed) fmt.Printf("⊘ Total Skipped (already exist): %d\n", totalSkipped) fmt.Printf("\n💡 Tip: Run 'sqlite3 goondex.db \"SELECT COUNT(*) FROM performers\"' to check total count\n") return nil }, } var importAllStudiosCmd = &cobra.Command{ Use: "all-studios", Short: "Import ALL studios from TPDB (paginated)", Long: `Import all 60,000+ studios from ThePornDB by paginating through all pages. This may take a while.`, RunE: func(cmd *cobra.Command, args []string) error { startPage, _ := cmd.Flags().GetInt("start-page") maxPages, _ := cmd.Flags().GetInt("max-pages") // Get API key from environment apiKey, err := getTPDBAPIKey() if err != nil { return err } // Create TPDB scraper scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) // Get database database, err := getDB() if err != nil { return err } defer database.Close() studioStore := db.NewStudioStore(database) fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ TPDB BULK STUDIO IMPORT - ALL PAGES ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Printf("Starting from page: %d\n", startPage) if maxPages > 0 { fmt.Printf("Max pages: %d\n", maxPages) } else { fmt.Println("Max pages: ALL (no limit)") } fmt.Println() totalImported := 0 totalFailed := 0 totalSkipped := 0 currentPage := startPage for { fmt.Printf("📥 Fetching page %d...\n", currentPage) studios, meta, err := scraper.ListStudios(context.Background(), currentPage) if err != nil { fmt.Printf("⚠ Failed to fetch page %d: %v\n", currentPage, err) totalFailed++ currentPage++ continue } if meta == nil { fmt.Println("⚠ No metadata returned") break } fmt.Printf(" Page %d/%d | Total: %d studios | Per page: %d\n", meta.CurrentPage, meta.LastPage, meta.Total, meta.PerPage) // Import studios from this page pageImported := 0 pageFailed := 0 pageSkipped := 0 for _, st := range studios { // Check if already exists existing, _ := studioStore.GetBySourceID("tpdb", st.SourceID) if existing != nil { pageSkipped++ totalSkipped++ continue } if err := studioStore.Create(&st); err != nil { fmt.Printf(" ⚠ Failed to import %s: %v\n", st.Name, err) pageFailed++ totalFailed++ continue } pageImported++ totalImported++ } fmt.Printf(" ✓ Imported: %d | ⚠ Failed: %d | ⊘ Skipped: %d\n\n", pageImported, pageFailed, pageSkipped) // Check if we should stop if maxPages > 0 && (currentPage-startPage+1) >= maxPages { fmt.Printf("⏹ Reached maximum pages limit (%d)\n", maxPages) break } if currentPage >= meta.LastPage { fmt.Println("✓ Reached last page") break } currentPage++ // Small delay to be nice to the API time.Sleep(500 * time.Millisecond) } fmt.Println() fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ IMPORT COMPLETE ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Printf("✓ Total Imported: %d\n", totalImported) fmt.Printf("⚠ Total Failed: %d\n", totalFailed) fmt.Printf("⊘ Total Skipped (already exist): %d\n", totalSkipped) fmt.Printf("\n💡 Tip: Run 'sqlite3 goondex.db \"SELECT COUNT(*) FROM studios\"' to check total count\n") return nil }, } var importAllScenesCmd = &cobra.Command{ Use: "all-scenes", Short: "Import ALL scenes from TPDB (paginated)", Long: `Import all scenes from ThePornDB by paginating through all pages. This may take a while.`, RunE: func(cmd *cobra.Command, args []string) error { startPage, _ := cmd.Flags().GetInt("start-page") maxPages, _ := cmd.Flags().GetInt("max-pages") // Get API key from environment apiKey, err := getTPDBAPIKey() if err != nil { return err } // Create TPDB scraper scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) // Get database database, err := getDB() if err != nil { return err } defer database.Close() sceneStore := db.NewSceneStore(database) performerStore := db.NewPerformerStore(database) studioStore := db.NewStudioStore(database) tagStore := db.NewTagStore(database) fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ TPDB BULK SCENE IMPORT - ALL PAGES ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Printf("Starting from page: %d\n", startPage) if maxPages > 0 { fmt.Printf("Max pages: %d\n", maxPages) } else { fmt.Println("Max pages: ALL (no limit)") } fmt.Println() totalImported := 0 totalFailed := 0 totalSkipped := 0 currentPage := startPage for { fmt.Printf("📥 Fetching page %d...\n", currentPage) scenes, meta, err := scraper.ListScenes(context.Background(), currentPage) if err != nil { fmt.Printf("⚠ Failed to fetch page %d: %v\n", currentPage, err) totalFailed++ currentPage++ continue } if meta == nil { fmt.Println("⚠ No metadata returned") break } fmt.Printf(" Page %d/%d | Total: %d scenes | Per page: %d\n", meta.CurrentPage, meta.LastPage, meta.Total, meta.PerPage) // Import scenes from this page pageImported := 0 pageFailed := 0 pageSkipped := 0 for _, sc := range scenes { // Check if already exists existing, _ := sceneStore.GetBySourceID("tpdb", sc.SourceID) if existing != nil { pageSkipped++ totalSkipped++ continue } // Import studio if not exists if sc.Studio != nil { existingStudio, _ := studioStore.GetBySourceID(sc.Studio.Source, sc.Studio.SourceID) if existingStudio != nil { sc.StudioID = &existingStudio.ID } else { if err := studioStore.Create(sc.Studio); err == nil { sc.StudioID = &sc.Studio.ID } } } // Create scene if err := sceneStore.Create(&sc); err != nil { fmt.Printf(" ⚠ Failed to import %s: %v\n", sc.Title, err) pageFailed++ totalFailed++ continue } // Import performers and link them for _, p := range sc.Performers { existingPerformer, _ := performerStore.GetBySourceID(p.Source, p.SourceID) if existingPerformer != nil { sceneStore.AddPerformer(sc.ID, existingPerformer.ID) } else { if err := performerStore.Create(&p); err == nil { sceneStore.AddPerformer(sc.ID, p.ID) } } } // Import tags and link them for _, t := range sc.Tags { existingTag, _ := tagStore.GetByName(t.Name) if existingTag != nil { sceneStore.AddTag(sc.ID, existingTag.ID) } else { if err := tagStore.Create(&t); err == nil { sceneStore.AddTag(sc.ID, t.ID) } } } pageImported++ totalImported++ } fmt.Printf(" ✓ Imported: %d | ⚠ Failed: %d | ⊘ Skipped: %d\n\n", pageImported, pageFailed, pageSkipped) // Check if we should stop if maxPages > 0 && (currentPage-startPage+1) >= maxPages { fmt.Printf("⏹ Reached maximum pages limit (%d)\n", maxPages) break } if currentPage >= meta.LastPage { fmt.Println("✓ Reached last page") break } currentPage++ // Small delay to be nice to the API time.Sleep(500 * time.Millisecond) } fmt.Println() fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ IMPORT COMPLETE ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Printf("✓ Total Imported: %d\n", totalImported) fmt.Printf("⚠ Total Failed: %d\n", totalFailed) fmt.Printf("⊘ Total Skipped (already exist): %d\n", totalSkipped) fmt.Printf("\n💡 Tip: Run 'sqlite3 goondex.db \"SELECT COUNT(*) FROM scenes\"' to check total count\n") return nil }, } var importPerformerCmd = &cobra.Command{ Use: "performer [query]", Short: "Search TPDB for performers and import them to local database", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] // Get API key from environment apiKey, err := getTPDBAPIKey() if err != nil { return err } // Create TPDB scraper scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) // Search TPDB fmt.Printf("Searching TPDB for performers matching '%s'...\n", query) performers, err := scraper.SearchPerformers(context.Background(), query) if err != nil { return fmt.Errorf("failed to search TPDB: %w", err) } if len(performers) == 0 { fmt.Println("No performers found on TPDB") return nil } fmt.Printf("Found %d performer(s) on TPDB\n\n", len(performers)) // Open database database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewPerformerStore(database) // Import each performer imported := 0 for _, p := range performers { fmt.Printf("Importing: %s (TPDB ID: %s)\n", p.Name, p.SourceID) if err := store.Create(&p); err != nil { fmt.Printf(" ⚠ Warning: Failed to import: %v\n", err) continue } fmt.Printf(" ✓ Imported with local ID: %d\n", p.ID) imported++ } fmt.Printf("\n✓ Successfully imported %d/%d performers\n", imported, len(performers)) return nil }, } var importStudioCmd = &cobra.Command{ Use: "studio [query]", Short: "Search TPDB for studios and import them to local database", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] // Get API key from environment apiKey, err := getTPDBAPIKey() if err != nil { return err } // Create TPDB scraper scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) // Search TPDB fmt.Printf("Searching TPDB for studios matching '%s'...\n", query) studios, err := scraper.SearchStudios(context.Background(), query) if err != nil { return fmt.Errorf("failed to search TPDB: %w", err) } if len(studios) == 0 { fmt.Println("No studios found on TPDB") return nil } fmt.Printf("Found %d studio(s) on TPDB\n\n", len(studios)) // Open database database, err := getDB() if err != nil { return err } defer database.Close() store := db.NewStudioStore(database) // Import each studio imported := 0 for _, s := range studios { fmt.Printf("Importing: %s (TPDB ID: %s)\n", s.Name, s.SourceID) if err := store.Create(&s); err != nil { fmt.Printf(" ⚠ Warning: Failed to import: %v\n", err) continue } fmt.Printf(" ✓ Imported with local ID: %d\n", s.ID) imported++ } fmt.Printf("\n✓ Successfully imported %d/%d studios\n", imported, len(studios)) return nil }, } // Adult Empire command with subcommands var adultempCmd = &cobra.Command{ Use: "adultemp", Short: "Scrape data from Adult Empire", Long: `Search and scrape performers, scenes, and metadata from Adult Empire (adultdvdempire.com).`, } func init() { adultempCmd.AddCommand(adultempSearchSceneCmd) adultempCmd.AddCommand(adultempSearchPerformerCmd) adultempCmd.AddCommand(adultempSearchMovieCmd) adultempCmd.AddCommand(adultempScrapeSceneCmd) adultempCmd.AddCommand(adultempScrapePerformerCmd) adultempCmd.AddCommand(adultempScrapeMovieCmd) adultempCmd.AddCommand(adultempMergePerformerCmd) adultempCmd.PersistentFlags().String("etoken", "", "Adult Empire authentication token (etoken cookie)") } var adultempSearchSceneCmd = &cobra.Command{ Use: "search-scene [query]", Short: "Search for scenes on Adult Empire", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Set auth token if provided if flag := cmd.Flag("etoken"); flag != nil { if etoken := flag.Value.String(); etoken != "" { if err := scraper.SetAuthToken(etoken); err != nil { return fmt.Errorf("failed to set auth token: %w", err) } } } fmt.Printf("Searching Adult Empire for scenes matching '%s'...\n\n", query) results, err := scraper.SearchScenesByName(context.Background(), query) if err != nil { return fmt.Errorf("search failed: %w", err) } if len(results) == 0 { fmt.Println("No scenes found on Adult Empire") return nil } fmt.Printf("Found %d scene(s):\n\n", len(results)) for i, result := range results { fmt.Printf("%d. %s\n", i+1, result.Title) fmt.Printf(" URL: %s\n", result.URL) if result.Image != "" { fmt.Printf(" Image: %s\n", result.Image) } fmt.Println() } return nil }, } var adultempSearchPerformerCmd = &cobra.Command{ Use: "search-performer [name]", Short: "Search for performers on Adult Empire", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Set auth token if provided if flag := cmd.Flag("etoken"); flag != nil { if etoken := flag.Value.String(); etoken != "" { if err := scraper.SetAuthToken(etoken); err != nil { return fmt.Errorf("failed to set auth token: %w", err) } } } fmt.Printf("Searching Adult Empire for performers matching '%s'...\n\n", name) results, err := scraper.SearchPerformersByName(context.Background(), name) if err != nil { return fmt.Errorf("search failed: %w", err) } if len(results) == 0 { fmt.Println("No performers found on Adult Empire") return nil } fmt.Printf("Found %d performer(s):\n\n", len(results)) for i, result := range results { fmt.Printf("%d. %s\n", i+1, result.Title) fmt.Printf(" URL: %s\n", result.URL) if result.Image != "" { fmt.Printf(" Image: %s\n", result.Image) } fmt.Println() } return nil }, } var adultempScrapeSceneCmd = &cobra.Command{ Use: "scrape-scene [url]", Short: "Scrape a scene from Adult Empire by URL and import to database", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { url := args[0] scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Set auth token if provided if flag := cmd.Flag("etoken"); flag != nil { if etoken := flag.Value.String(); etoken != "" { if err := scraper.SetAuthToken(etoken); err != nil { return fmt.Errorf("failed to set auth token: %w", err) } } } fmt.Printf("Scraping scene from %s...\n", url) sceneData, err := scraper.ScrapeSceneByURL(context.Background(), url) if err != nil { return fmt.Errorf("scrape failed: %w", err) } // Display scraped data fmt.Printf("\n✓ Scraped scene data:\n") fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Title: %s\n", sceneData.Title) if sceneData.Date != "" { fmt.Printf("Date: %s\n", sceneData.Date) } if sceneData.Studio != "" { fmt.Printf("Studio: %s\n", sceneData.Studio) } if sceneData.Director != "" { fmt.Printf("Director: %s\n", sceneData.Director) } if sceneData.Code != "" { fmt.Printf("Code: %s\n", sceneData.Code) } if len(sceneData.Performers) > 0 { fmt.Printf("Performers: %s\n", sceneData.Performers) } if len(sceneData.Tags) > 0 { fmt.Printf("Tags: %s\n", sceneData.Tags) } if sceneData.Description != "" { fmt.Printf("\nDescription:\n%s\n", sceneData.Description) } fmt.Printf("═══════════════════════════════════════════════════\n\n") // Convert to model and save to database scene := scraper.ConvertSceneToModel(sceneData) database, err := getDB() if err != nil { return err } defer database.Close() sceneStore := db.NewSceneStore(database) // Import studio if present if sceneData.Studio != "" { studioStore := db.NewStudioStore(database) studios, _ := studioStore.Search(sceneData.Studio) if len(studios) > 0 { scene.StudioID = &studios[0].ID } else { // Create new studio newStudio := &model.Studio{ Name: sceneData.Studio, Source: "adultemp", } if err := studioStore.Create(newStudio); err == nil { scene.StudioID = &newStudio.ID } } } if err := sceneStore.Create(scene); err != nil { return fmt.Errorf("failed to save scene: %w", err) } fmt.Printf("✓ Scene imported to database with ID: %d\n", scene.ID) return nil }, } var adultempScrapePerformerCmd = &cobra.Command{ Use: "scrape-performer [url]", Short: "Scrape a performer from Adult Empire by URL and import to database", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { url := args[0] scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Set auth token if provided if flag := cmd.Flag("etoken"); flag != nil { if etoken := flag.Value.String(); etoken != "" { if err := scraper.SetAuthToken(etoken); err != nil { return fmt.Errorf("failed to set auth token: %w", err) } } } fmt.Printf("Scraping performer from %s...\n", url) performerData, err := scraper.ScrapePerformerByURL(context.Background(), url) if err != nil { return fmt.Errorf("scrape failed: %w", err) } // Display scraped data fmt.Printf("\n✓ Scraped performer data:\n") fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Name: %s\n", performerData.Name) if len(performerData.Aliases) > 0 { fmt.Printf("Aliases: %v\n", performerData.Aliases) } if performerData.Birthdate != "" { fmt.Printf("Birthday: %s\n", performerData.Birthdate) } if performerData.Ethnicity != "" { fmt.Printf("Ethnicity: %s\n", performerData.Ethnicity) } if performerData.Country != "" { fmt.Printf("Country: %s\n", performerData.Country) } if performerData.Height != "" { fmt.Printf("Height: %s\n", performerData.Height) } if performerData.HairColor != "" { fmt.Printf("Hair Color: %s\n", performerData.HairColor) } if performerData.EyeColor != "" { fmt.Printf("Eye Color: %s\n", performerData.EyeColor) } if performerData.Measurements != "" { fmt.Printf("Measurements: %s\n", performerData.Measurements) } if performerData.Biography != "" { fmt.Printf("\nBio:\n%s\n", performerData.Biography) } fmt.Printf("═══════════════════════════════════════════════════\n\n") // Convert to model and save to database performer := scraper.ConvertPerformerToModel(performerData) database, err := getDB() if err != nil { return err } defer database.Close() performerStore := db.NewPerformerStore(database) if err := performerStore.Create(performer); err != nil { return fmt.Errorf("failed to save performer: %w", err) } fmt.Printf("✓ Performer imported to database with ID: %d\n", performer.ID) return nil }, } var adultempSearchMovieCmd = &cobra.Command{ Use: "search-movie [query]", Short: "Search for movies on Adult Empire", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] fmt.Println("🔍 Searching Adult Empire for movies...") fmt.Printf("Query: \"%s\"\n\n", query) fmt.Println("⚠️ Movie search is not yet implemented.") fmt.Println() fmt.Println("For now, please:") fmt.Println("1. Go to https://www.adultdvdempire.com") fmt.Printf("2. Search for: \"%s\"\n", query) fmt.Println("3. Copy the URL of the movie you want") fmt.Println("4. Import it with: ./goondex adultemp scrape-movie [url]") return nil }, } var adultempScrapeMovieCmd = &cobra.Command{ Use: "scrape-movie [url]", Short: "Scrape and import a movie from Adult Empire by URL", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { movieURL := args[0] fmt.Println("📥 Scraping movie from Adult Empire...") fmt.Printf("URL: %s\n\n", movieURL) fmt.Println("⚠️ Movie scraping is not yet fully implemented.") fmt.Println() fmt.Println("The movie database schema is ready, but the Adult Empire") fmt.Println("movie scraper needs to be completed.") fmt.Println() fmt.Println("📌 What's ready:") fmt.Println(" • Movie database table and schema") fmt.Println(" • Movie web UI for browsing") fmt.Println(" • Movie-Scene relationships") fmt.Println() fmt.Println("📌 What's needed:") fmt.Println(" • Adult Empire HTML parser for movies") fmt.Println(" • Movie data extraction (title, cast, scenes, etc.)") fmt.Println() fmt.Println("💡 This feature is planned for the next release.") return nil }, } var adultempMergePerformerCmd = &cobra.Command{ Use: "merge-performer [id] [adultemp-url]", Short: "Merge Adult Empire data into an existing performer", Long: `Fetch performer data from Adult Empire and intelligently merge it with existing TPDB data.`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { // Parse ID var performerID int64 if _, err := fmt.Sscanf(args[0], "%d", &performerID); err != nil { return fmt.Errorf("invalid performer ID: %s", args[0]) } adultempURL := args[1] database, err := getDB() if err != nil { return err } defer database.Close() performerStore := db.NewPerformerStore(database) // Get existing performer performer, err := performerStore.GetByID(performerID) if err != nil { return fmt.Errorf("performer not found: %w", err) } fmt.Printf("Merging Adult Empire data into: %s (ID: %d)\n", performer.Name, performer.ID) // Scrape Adult Empire data scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Set auth token if provided if flag := cmd.Flag("etoken"); flag != nil { if etoken := flag.Value.String(); etoken != "" { if err := scraper.SetAuthToken(etoken); err != nil { return fmt.Errorf("failed to set auth token: %w", err) } } } fmt.Printf("Scraping Adult Empire data from: %s\n", adultempURL) adultempData, err := scraper.ScrapePerformerByURL(context.Background(), adultempURL) if err != nil { return fmt.Errorf("failed to scrape Adult Empire: %w", err) } // Check if names match if !merger.ShouldMerge(performer.Name, adultempData.Name) { fmt.Printf("\n⚠ Warning: Names don't match closely:\n") fmt.Printf(" Database: %s\n", performer.Name) fmt.Printf(" Adult Empire: %s\n", adultempData.Name) fmt.Printf("\nProceed anyway? This may merge data for different performers.\n") fmt.Printf("Type 'yes' to continue: ") var response string fmt.Scanln(&response) if response != "yes" { fmt.Println("Merge cancelled.") return nil } } // Merge the data fmt.Println("\n🔄 Merging data...") mergedPerformer := merger.MergePerformerData(performer, adultempData) // Update the database if err := performerStore.Update(mergedPerformer); err != nil { return fmt.Errorf("failed to update performer: %w", err) } fmt.Println("✓ Successfully merged Adult Empire data!") fmt.Printf("\nUpdated fields:\n") if mergedPerformer.Birthday != performer.Birthday && mergedPerformer.Birthday != "" { fmt.Printf(" - Birthday: %s\n", mergedPerformer.Birthday) } if mergedPerformer.Ethnicity != performer.Ethnicity && mergedPerformer.Ethnicity != "" { fmt.Printf(" - Ethnicity: %s\n", mergedPerformer.Ethnicity) } if mergedPerformer.Country != performer.Country && mergedPerformer.Country != "" { fmt.Printf(" - Country: %s\n", mergedPerformer.Country) } if mergedPerformer.Height != performer.Height && mergedPerformer.Height > 0 { fmt.Printf(" - Height: %d cm\n", mergedPerformer.Height) } if mergedPerformer.HairColor != performer.HairColor && mergedPerformer.HairColor != "" { fmt.Printf(" - Hair Color: %s\n", mergedPerformer.HairColor) } if mergedPerformer.EyeColor != performer.EyeColor && mergedPerformer.EyeColor != "" { fmt.Printf(" - Eye Color: %s\n", mergedPerformer.EyeColor) } if mergedPerformer.Aliases != performer.Aliases && mergedPerformer.Aliases != "" { fmt.Printf(" - Aliases: %s\n", mergedPerformer.Aliases) } return nil }, } var importSceneCmd = &cobra.Command{ Use: "scene [query]", Short: "Search TPDB for scenes and import them to local database", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { query := args[0] // Get API key from environment apiKey, err := getTPDBAPIKey() if err != nil { return err } // Create TPDB scraper scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey) // Search TPDB fmt.Printf("Searching TPDB for scenes matching '%s'...\n", query) scenes, err := scraper.SearchScenes(context.Background(), query) if err != nil { return fmt.Errorf("failed to search TPDB: %w", err) } if len(scenes) == 0 { fmt.Println("No scenes found on TPDB") return nil } fmt.Printf("Found %d scene(s) on TPDB\n\n", len(scenes)) // Open database database, err := getDB() if err != nil { return err } defer database.Close() sceneStore := db.NewSceneStore(database) performerStore := db.NewPerformerStore(database) studioStore := db.NewStudioStore(database) tagStore := db.NewTagStore(database) // Import each scene imported := 0 for _, sc := range scenes { fmt.Printf("Importing: %s (TPDB ID: %s)\n", sc.Title, sc.SourceID) // Import studio if present if sc.Studio != nil { if err := studioStore.Create(sc.Studio); err != nil { // Studio might already exist, try to fetch it studios, _ := studioStore.Search(sc.Studio.Name) if len(studios) > 0 { sc.StudioID = &studios[0].ID } } else { sc.StudioID = &sc.Studio.ID } } // Create scene if err := sceneStore.Create(&sc); err != nil { fmt.Printf(" ⚠ Warning: Failed to import scene: %v\n", err) continue } // Import and link performers for _, p := range sc.Performers { if err := performerStore.Create(&p); err != nil { // Performer might already exist performers, _ := performerStore.Search(p.Name) if len(performers) > 0 { p.ID = performers[0].ID } } if p.ID > 0 { sceneStore.AddPerformer(sc.ID, p.ID) } } // Import and link tags for _, t := range sc.Tags { existing, _ := tagStore.GetByName(t.Name) if existing != nil { t.ID = existing.ID } else { if err := tagStore.Create(&t); err != nil { continue } } if t.ID > 0 { sceneStore.AddTag(sc.ID, t.ID) } } fmt.Printf(" ✓ Imported with local ID: %d\n", sc.ID) imported++ } fmt.Printf("\n✓ Successfully imported %d/%d scenes\n", imported, len(scenes)) return nil }, } var importMovieCmd = &cobra.Command{ Use: "movie [title or url]", Short: "Import movies from Adult Empire", Long: `Import movies from Adult Empire. TPDB does not have a movies database. Movies can be imported by: 1. Searching by title: ./goondex import movie "Movie Title" 2. Direct URL: ./goondex import movie https://www.adultdvdempire.com/... Note: For bulk movie import, movies are best imported through Adult Empire's catalog.`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { input := args[0] fmt.Println("╔═══════════════════════════════════════════════════════════════╗") fmt.Println("║ MOVIE IMPORT - ADULT EMPIRE ║") fmt.Println("╚═══════════════════════════════════════════════════════════════╝") fmt.Println() fmt.Println("📌 Note: TPDB does not have movies. Movies are imported from Adult Empire.") fmt.Println() // Check if input is a URL isURL := len(input) > 4 && (input[:4] == "http" || input[:3] == "www") if isURL { fmt.Printf("🔗 Importing movie from URL: %s\n", input) fmt.Println() fmt.Println("ℹ️ To import movies, use the Adult Empire scraper:") fmt.Printf(" ./goondex adultemp scrape-movie %s\n", input) } else { fmt.Printf("🔍 Searching Adult Empire for: \"%s\"\n", input) fmt.Println() fmt.Println("ℹ️ To search and import movies, use:") fmt.Printf(" 1. Search: ./goondex adultemp search-movie \"%s\"\n", input) fmt.Println(" 2. Copy the URL of the movie you want") fmt.Println(" 3. Import: ./goondex adultemp scrape-movie [url]") } fmt.Println() fmt.Println("💡 Tip: Movies imported from Adult Empire will include:") fmt.Println(" • Movie title, date, studio, director") fmt.Println(" • Front and back cover images") fmt.Println(" • Full description") fmt.Println(" • Cast (performers)") fmt.Println(" • Individual scenes within the movie") fmt.Println() fmt.Println("For now, movies need to be imported manually. Bulk import") fmt.Println("from Adult Empire is planned for a future release.") return nil }, } // Enrich command with subcommands var enrichCmd = &cobra.Command{ Use: "enrich", Short: "Enrich existing data with Adult Empire metadata", Long: `Automatically enrich performers and scenes with additional metadata from Adult Empire. This command searches Adult Empire for matching entities and merges the data: - TPDB data is primary (never overwritten) - Adult Empire fills in missing fields (bio, ethnicity, measurements, etc.) - Fuzzy name matching ensures accurate matches (70% threshold) - Rate limited to prevent API overload Examples: goondex enrich performer 123 # Enrich single performer goondex enrich all-performers # Enrich all performers goondex enrich scene 456 # Enrich single scene goondex enrich all-scenes # Enrich all scenes`, } func init() { enrichCmd.AddCommand(enrichPerformerCmd) enrichCmd.AddCommand(enrichAllPerformersCmd) enrichCmd.AddCommand(enrichSceneCmd) enrichCmd.AddCommand(enrichAllScenesCmd) // Flags for bulk enrich enrichAllPerformersCmd.Flags().Int("start-id", 1, "Performer ID to start from (for resuming)") enrichAllPerformersCmd.Flags().Int("limit", 0, "Maximum performers to enrich (0 = all)") enrichAllPerformersCmd.Flags().Duration("rate-limit", 500*time.Millisecond, "Delay between searches") enrichAllScenesCmd.Flags().Int("start-id", 1, "Scene ID to start from (for resuming)") enrichAllScenesCmd.Flags().Int("limit", 0, "Maximum scenes to enrich (0 = all)") enrichAllScenesCmd.Flags().Duration("rate-limit", 500*time.Millisecond, "Delay between searches") } var enrichPerformerCmd = &cobra.Command{ Use: "performer [id]", Short: "Enrich a single performer with Adult Empire data", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { id := args[0] database, err := getDB() if err != nil { return err } defer database.Close() performerStore := db.NewPerformerStore(database) // Get performer from DB var performerID int64 if _, err := fmt.Sscanf(id, "%d", &performerID); err != nil { return fmt.Errorf("invalid performer ID: %w", err) } performer, err := performerStore.GetByID(performerID) if err != nil { return fmt.Errorf("performer not found: %w", err) } fmt.Printf("🔍 Enriching performer: %s (ID: %d)\n", performer.Name, performer.ID) // Search Adult Empire scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } fmt.Printf(" Searching Adult Empire for '%s'...\n", performer.Name) results, err := scraper.SearchPerformersByName(context.Background(), performer.Name) if err != nil { return fmt.Errorf("search failed: %w", err) } if len(results) == 0 { fmt.Println(" ⚠ No matches found on Adult Empire") return nil } // Find best match using fuzzy matching var bestMatch *adultemp.SearchResult for i := range results { if merger.ShouldMerge(performer.Name, results[i].Title) { bestMatch = &results[i] break } } if bestMatch == nil { fmt.Printf(" ⚠ No confident matches found (searched %d results)\n", len(results)) fmt.Println(" 💡 Tip: First result was:", results[0].Title) return nil } fmt.Printf(" ✓ Found match: %s\n", bestMatch.Title) fmt.Printf(" 📄 Scraping: %s\n", bestMatch.URL) // Scrape full data adultempData, err := scraper.ScrapePerformerByURL(context.Background(), bestMatch.URL) if err != nil { return fmt.Errorf("scraping failed: %w", err) } // Merge data merged := merger.MergePerformerData(performer, adultempData) // Update in database if err := performerStore.Update(merged); err != nil { return fmt.Errorf("failed to update performer: %w", err) } fmt.Printf(" ✓ Successfully enriched %s\n", performer.Name) fmt.Println() fmt.Println("📊 Enriched fields:") if adultempData.Birthdate != "" { fmt.Printf(" • Birthday: %s\n", adultempData.Birthdate) } if adultempData.Ethnicity != "" { fmt.Printf(" • Ethnicity: %s\n", adultempData.Ethnicity) } if adultempData.HairColor != "" { fmt.Printf(" • Hair Color: %s\n", adultempData.HairColor) } if adultempData.EyeColor != "" { fmt.Printf(" • Eye Color: %s\n", adultempData.EyeColor) } if adultempData.Measurements != "" { fmt.Printf(" • Measurements: %s\n", adultempData.Measurements) } return nil }, } var enrichAllPerformersCmd = &cobra.Command{ Use: "all-performers", Short: "Enrich all performers with Adult Empire data", Long: `Automatically enrich all performers in the database with Adult Empire metadata. This process: - Searches Adult Empire for each performer by name - Uses fuzzy matching to find confident matches - Merges data only for high-confidence matches (70% name similarity) - Rate limited to prevent API overload (default 500ms between searches) - Tracks progress and can be resumed Examples: goondex enrich all-performers # Enrich all performers goondex enrich all-performers --start-id 100 # Resume from ID 100 goondex enrich all-performers --limit 50 # Enrich first 50 performers goondex enrich all-performers --rate-limit 1s # Slower rate limit`, RunE: func(cmd *cobra.Command, args []string) error { startID, _ := cmd.Flags().GetInt("start-id") limit, _ := cmd.Flags().GetInt("limit") rateLimit, _ := cmd.Flags().GetDuration("rate-limit") database, err := getDB() if err != nil { return err } defer database.Close() performerStore := db.NewPerformerStore(database) // Get all performers performers, err := performerStore.Search("") if err != nil { return fmt.Errorf("failed to search performers: %w", err) } fmt.Printf("🔍 Enriching performers from Adult Empire\n") fmt.Printf(" Total performers: %d\n", len(performers)) fmt.Printf(" Start ID: %d\n", startID) if limit > 0 { fmt.Printf(" Limit: %d\n", limit) } fmt.Printf(" Rate limit: %v\n", rateLimit) fmt.Println() // Create scraper scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Track stats totalProcessed := 0 totalEnriched := 0 totalSkipped := 0 totalFailed := 0 for _, performer := range performers { // Skip if before start ID if performer.ID < int64(startID) { continue } // Check limit if limit > 0 && totalProcessed >= limit { break } totalProcessed++ fmt.Printf("[%d/%d] Processing: %s (ID: %d)\n", totalProcessed, len(performers), performer.Name, performer.ID) // Search Adult Empire results, err := scraper.SearchPerformersByName(context.Background(), performer.Name) if err != nil { fmt.Printf(" ⚠ Search failed: %v\n", err) totalFailed++ time.Sleep(rateLimit) continue } if len(results) == 0 { fmt.Println(" ⚠ No matches found") totalSkipped++ time.Sleep(rateLimit) continue } // Find best match using fuzzy matching var bestMatch *adultemp.SearchResult for i := range results { if merger.ShouldMerge(performer.Name, results[i].Title) { bestMatch = &results[i] break } } if bestMatch == nil { fmt.Printf(" ⚠ No confident match (closest: %s)\n", results[0].Title) totalSkipped++ time.Sleep(rateLimit) continue } fmt.Printf(" ✓ Found: %s\n", bestMatch.Title) // Scrape full data adultempData, err := scraper.ScrapePerformerByURL(context.Background(), bestMatch.URL) if err != nil { fmt.Printf(" ⚠ Scraping failed: %v\n", err) totalFailed++ time.Sleep(rateLimit) continue } // Merge data merged := merger.MergePerformerData(&performer, adultempData) // Update in database if err := performerStore.Update(merged); err != nil { fmt.Printf(" ⚠ Update failed: %v\n", err) totalFailed++ time.Sleep(rateLimit) continue } fmt.Printf(" ✓ Enriched successfully\n") totalEnriched++ // Rate limiting time.Sleep(rateLimit) } fmt.Println() fmt.Println("═══════════════════════════════════════") fmt.Printf("✓ Enrichment Complete\n") fmt.Printf(" Processed: %d\n", totalProcessed) fmt.Printf(" Enriched: %d\n", totalEnriched) fmt.Printf(" Skipped: %d\n", totalSkipped) fmt.Printf(" Failed: %d\n", totalFailed) fmt.Println("═══════════════════════════════════════") return nil }, } var enrichSceneCmd = &cobra.Command{ Use: "scene [id]", Short: "Enrich a single scene with Adult Empire data", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { id := args[0] database, err := getDB() if err != nil { return err } defer database.Close() sceneStore := db.NewSceneStore(database) // Get scene from DB var sceneID int64 if _, err := fmt.Sscanf(id, "%d", &sceneID); err != nil { return fmt.Errorf("invalid scene ID: %w", err) } scene, err := sceneStore.GetByID(sceneID) if err != nil { return fmt.Errorf("scene not found: %w", err) } fmt.Printf("🔍 Enriching scene: %s (ID: %d)\n", scene.Title, scene.ID) // Search Adult Empire scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } fmt.Printf(" Searching Adult Empire for '%s'...\n", scene.Title) results, err := scraper.SearchScenesByName(context.Background(), scene.Title) if err != nil { return fmt.Errorf("search failed: %w", err) } if len(results) == 0 { fmt.Println(" ⚠ No matches found on Adult Empire") return nil } // Find best match using fuzzy matching var bestMatch *adultemp.SearchResult for i := range results { if merger.ShouldMerge(scene.Title, results[i].Title) { bestMatch = &results[i] break } } if bestMatch == nil { fmt.Printf(" ⚠ No confident matches found (searched %d results)\n", len(results)) fmt.Println(" 💡 Tip: First result was:", results[0].Title) return nil } fmt.Printf(" ✓ Found match: %s\n", bestMatch.Title) fmt.Printf(" 📄 Scraping: %s\n", bestMatch.URL) // Scrape full data adultempData, err := scraper.ScrapeSceneByURL(context.Background(), bestMatch.URL) if err != nil { return fmt.Errorf("scraping failed: %w", err) } // Merge scene data (fill in missing fields) if scene.Description == "" && adultempData.Description != "" { scene.Description = adultempData.Description } if scene.Director == "" && adultempData.Director != "" { scene.Director = adultempData.Director } if scene.ImageURL == "" && adultempData.Image != "" { scene.ImageURL = adultempData.Image } // Update in database if err := sceneStore.Update(scene); err != nil { return fmt.Errorf("failed to update scene: %w", err) } fmt.Printf(" ✓ Successfully enriched %s\n", scene.Title) return nil }, } var enrichAllScenesCmd = &cobra.Command{ Use: "all-scenes", Short: "Enrich all scenes with Adult Empire data", Long: `Automatically enrich all scenes in the database with Adult Empire metadata. This process: - Searches Adult Empire for each scene by title - Uses fuzzy matching to find confident matches - Merges data only for high-confidence matches (70% title similarity) - Rate limited to prevent API overload (default 500ms between searches) - Tracks progress and can be resumed`, RunE: func(cmd *cobra.Command, args []string) error { startID, _ := cmd.Flags().GetInt("start-id") limit, _ := cmd.Flags().GetInt("limit") rateLimit, _ := cmd.Flags().GetDuration("rate-limit") database, err := getDB() if err != nil { return err } defer database.Close() sceneStore := db.NewSceneStore(database) // Get all scenes scenes, err := sceneStore.Search("") if err != nil { return fmt.Errorf("failed to search scenes: %w", err) } fmt.Printf("🔍 Enriching scenes from Adult Empire\n") fmt.Printf(" Total scenes: %d\n", len(scenes)) fmt.Printf(" Start ID: %d\n", startID) if limit > 0 { fmt.Printf(" Limit: %d\n", limit) } fmt.Printf(" Rate limit: %v\n", rateLimit) fmt.Println() // Create scraper scraper, err := adultemp.NewScraper() if err != nil { return fmt.Errorf("failed to create scraper: %w", err) } // Track stats totalProcessed := 0 totalEnriched := 0 totalSkipped := 0 totalFailed := 0 for _, scene := range scenes { // Skip if before start ID if scene.ID < int64(startID) { continue } // Check limit if limit > 0 && totalProcessed >= limit { break } totalProcessed++ fmt.Printf("[%d/%d] Processing: %s (ID: %d)\n", totalProcessed, len(scenes), scene.Title, scene.ID) // Search Adult Empire results, err := scraper.SearchScenesByName(context.Background(), scene.Title) if err != nil { fmt.Printf(" ⚠ Search failed: %v\n", err) totalFailed++ time.Sleep(rateLimit) continue } if len(results) == 0 { fmt.Println(" ⚠ No matches found") totalSkipped++ time.Sleep(rateLimit) continue } // Find best match using fuzzy matching var bestMatch *adultemp.SearchResult for i := range results { if merger.ShouldMerge(scene.Title, results[i].Title) { bestMatch = &results[i] break } } if bestMatch == nil { fmt.Printf(" ⚠ No confident match (closest: %s)\n", results[0].Title) totalSkipped++ time.Sleep(rateLimit) continue } fmt.Printf(" ✓ Found: %s\n", bestMatch.Title) // Scrape full data adultempData, err := scraper.ScrapeSceneByURL(context.Background(), bestMatch.URL) if err != nil { fmt.Printf(" ⚠ Scraping failed: %v\n", err) totalFailed++ time.Sleep(rateLimit) continue } // Merge scene data (fill in missing fields) if scene.Description == "" && adultempData.Description != "" { scene.Description = adultempData.Description } if scene.Director == "" && adultempData.Director != "" { scene.Director = adultempData.Director } if scene.ImageURL == "" && adultempData.Image != "" { scene.ImageURL = adultempData.Image } // Update in database if err := sceneStore.Update(&scene); err != nil { fmt.Printf(" ⚠ Update failed: %v\n", err) totalFailed++ time.Sleep(rateLimit) continue } fmt.Printf(" ✓ Enriched successfully\n") totalEnriched++ // Rate limiting time.Sleep(rateLimit) } fmt.Println() fmt.Println("═══════════════════════════════════════") fmt.Printf("✓ Enrichment Complete\n") fmt.Printf(" Processed: %d\n", totalProcessed) fmt.Printf(" Enriched: %d\n", totalEnriched) fmt.Printf(" Skipped: %d\n", totalSkipped) fmt.Printf(" Failed: %d\n", totalFailed) fmt.Println("═══════════════════════════════════════") return nil }, }