- 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>
2834 lines
82 KiB
Go
2834 lines
82 KiB
Go
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
|
||
},
|
||
}
|