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

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

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

2834 lines
82 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"
"time"
"github.com/spf13/cobra"
"git.leaktechnologies.dev/stu/Goondex/internal/db"
"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"
)
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:8080", "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 from external sources (TPDB)",
Long: `Import performers, studios, and scenes from ThePornDB into your local database.`,
}
func init() {
importCmd.AddCommand(importPerformerCmd)
importCmd.AddCommand(importAllPerformersCmd)
importCmd.AddCommand(importStudioCmd)
importCmd.AddCommand(importAllStudiosCmd)
importCmd.AddCommand(importSceneCmd)
importCmd.AddCommand(importAllScenesCmd)
importCmd.AddCommand(importMovieCmd)
// 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)")
// 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 from TPDB",
Long: `Update existing performers, studios, and scenes with latest data from ThePornDB.
Rate limiting is enforced to prevent excessive API calls:
- Default minimum interval: 1 hour
- Recommended interval: 24 hours
- Use --force to override rate limiting
Examples:
goondex sync all # Sync all entities (with 1h rate limit)
goondex sync all --interval 24h # Sync all entities (24h rate limit)
goondex sync all --force # Force sync, ignore rate limit
goondex sync performers # Sync only performers
goondex sync status # View last sync times`,
}
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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
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 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)
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-dev4")
fmt.Println("Features:")
fmt.Println(" • TPDB integration with auto-import")
fmt.Println(" • Adult Empire scraper (scenes & performers)")
fmt.Println(" • Multi-source data merging")
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 (auto-fetches from TPDB if not in local database)",
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, try fetching from TPDB
if len(performers) == 0 {
fmt.Printf("No local results found. Searching TPDB for '%s'...\n", query)
apiKey := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
fmt.Println("⚠ TPDB_API_KEY not set. Cannot fetch from TPDB.")
fmt.Println("Set it with: export TPDB_API_KEY=\"your-key\"")
return nil
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
tpdbPerformers, err := scraper.SearchPerformers(context.Background(), query)
if err != nil {
fmt.Printf("⚠ TPDB search failed: %v\n", err)
return nil
}
if len(tpdbPerformers) == 0 {
fmt.Println("No performers found on TPDB either.")
return nil
}
fmt.Printf("Found %d performer(s) on TPDB. Fetching full details...\n\n", len(tpdbPerformers))
// Import from TPDB with enriched data
imported := 0
for _, p := range tpdbPerformers {
// Fetch full details for this performer
fullPerformer, err := scraper.GetPerformerByID(context.Background(), p.SourceID)
if err != nil {
fmt.Printf("⚠ Failed to fetch details for %s: %v\n", p.Name, err)
// Fall back to search result
fullPerformer = &p
}
if err := store.Create(fullPerformer); err != nil {
fmt.Printf("⚠ Failed to import %s: %v\n", fullPerformer.Name, err)
continue
}
imported++
}
// Search again to get the imported performers with their IDs
performers, err = store.Search(query)
if err != nil {
return fmt.Errorf("search failed after import: %w", err)
}
fmt.Printf("✓ 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 := os.Getenv("TPDB_API_KEY")
if apiKey != "" {
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
}
}
} else {
fmt.Println("⚠ TPDB_API_KEY not set, skipping TPDB update")
}
}
// 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 (auto-fetches from TPDB if not in local database)",
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)
}
// If no local results, try fetching from TPDB
if len(studios) == 0 {
fmt.Printf("No local results found. Searching TPDB for '%s'...\n", query)
apiKey := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
fmt.Println("⚠ TPDB_API_KEY not set. Cannot fetch from TPDB.")
fmt.Println("Set it with: export TPDB_API_KEY=\"your-key\"")
return nil
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
tpdbStudios, err := scraper.SearchStudios(context.Background(), query)
if err != nil {
fmt.Printf("⚠ TPDB search failed: %v\n", err)
return nil
}
if len(tpdbStudios) == 0 {
fmt.Println("No studios found on TPDB either.")
return nil
}
fmt.Printf("Found %d studio(s) on TPDB. Importing...\n\n", len(tpdbStudios))
// Import from TPDB
imported := 0
for _, s := range tpdbStudios {
if err := store.Create(&s); err != nil {
fmt.Printf("⚠ Failed to import %s: %v\n", s.Name, err)
continue
}
imported++
}
// Search again to get the imported studios with their IDs
studios, err = store.Search(query)
if err != nil {
return fmt.Errorf("search failed after import: %w", err)
}
fmt.Printf("✓ Imported %d studio(s)\n\n", imported)
}
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 (auto-fetches from TPDB if not in local database)",
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)
}
// If no local results, try fetching from TPDB
if len(scenes) == 0 {
fmt.Printf("No local results found. Searching TPDB for '%s'...\n", query)
apiKey := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
fmt.Println("⚠ TPDB_API_KEY not set. Cannot fetch from TPDB.")
fmt.Println("Set it with: export TPDB_API_KEY=\"your-key\"")
return nil
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
tpdbScenes, err := scraper.SearchScenes(context.Background(), query)
if err != nil {
fmt.Printf("⚠ TPDB search failed: %v\n", err)
return nil
}
if len(tpdbScenes) == 0 {
fmt.Println("No scenes found on TPDB either.")
return nil
}
fmt.Printf("Found %d scene(s) on TPDB. Importing (basic metadata only)...\n\n", len(tpdbScenes))
// Import scenes (simplified - just scene metadata, no relationships)
imported := 0
for _, sc := range tpdbScenes {
// Clear relationships to avoid complexity in auto-import
sc.Performers = nil
sc.Tags = nil
sc.Studio = nil
sc.StudioID = nil
if err := store.Create(&sc); err != nil {
fmt.Printf("⚠ Failed to import %s: %v\n", sc.Title, err)
continue
}
imported++
}
// Search again to get the imported scenes with their IDs
scenes, err = store.Search(query)
if err != nil {
return fmt.Errorf("search failed after import: %w", err)
}
fmt.Printf("✓ Imported %d scene(s) (use 'import scene' for full metadata with relationships)\n\n", imported)
}
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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
// 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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
// 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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
// 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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
// 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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
// 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 := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return fmt.Errorf("TPDB_API_KEY environment variable is not set")
}
// 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
},
}