Goondex/cmd/goondex/main.go

2820 lines
83 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 <url>') 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 \"<name>\"' 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 <url>' 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
},
}