diff --git a/cmd/goondex/main.go b/cmd/goondex/main.go index 4cd60bd..43aa14e 100644 --- a/cmd/goondex/main.go +++ b/cmd/goondex/main.go @@ -108,13 +108,21 @@ var performerSearchCmd = &cobra.Command{ return nil } - fmt.Printf("Found %d performer(s) on TPDB. Importing...\n\n", len(tpdbPerformers)) + fmt.Printf("Found %d performer(s) on TPDB. Fetching full details...\n\n", len(tpdbPerformers)) - // Import from TPDB + // Import from TPDB with enriched data imported := 0 for _, p := range tpdbPerformers { - if err := store.Create(&p); err != nil { - fmt.Printf("⚠ Failed to import %s: %v\n", p.Name, err) + // 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++ @@ -131,21 +139,98 @@ var performerSearchCmd = &cobra.Command{ fmt.Printf("Found %d performer(s):\n\n", len(performers)) for _, p := range performers { - fmt.Printf("ID: %d\n", p.ID) + fmt.Printf("═══════════════════════════════════════════════════\n") fmt.Printf("Name: %s\n", p.Name) if p.Aliases != "" { fmt.Printf("Aliases: %s\n", p.Aliases) } - if p.Country != "" { - fmt.Printf("Country: %s\n", p.Country) + 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") + + // Personal info if p.Gender != "" { fmt.Printf("Gender: %s\n", p.Gender) } - if p.Source != "" { - fmt.Printf("Source: %s (ID: %s)\n", p.Source, p.SourceID) + if p.Birthday != "" { + fmt.Printf("Birthday: %s\n", p.Birthday) } - fmt.Println("---") + 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 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 diff --git a/internal/db/performer_store.go b/internal/db/performer_store.go index 326a2ed..2662dc8 100644 --- a/internal/db/performer_store.go +++ b/internal/db/performer_store.go @@ -24,10 +24,64 @@ func (s *PerformerStore) Create(p *model.Performer) error { p.CreatedAt = now p.UpdatedAt = now + activeInt := 0 + if p.Active { + activeInt = 1 + } + result, err := s.db.conn.Exec(` - INSERT INTO performers (name, aliases, nationality, country, gender, image_path, image_url, bio, source, source_id, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, p.Name, p.Aliases, p.Nationality, p.Country, p.Gender, p.ImagePath, p.ImageURL, p.Bio, p.Source, p.SourceID, p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339)) + INSERT INTO performers ( + name, aliases, + gender, birthday, astrology, birthplace, ethnicity, nationality, country, + eye_color, hair_color, height, weight, measurements, cup_size, + tattoo_description, piercing_description, boob_job, + career, career_start_year, career_end_year, date_of_death, active, + image_path, image_url, poster_url, bio, + source, source_id, source_numeric_id, + created_at, updated_at + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + ON CONFLICT(source, source_id) DO UPDATE SET + name = excluded.name, + aliases = excluded.aliases, + gender = excluded.gender, + birthday = excluded.birthday, + astrology = excluded.astrology, + birthplace = excluded.birthplace, + ethnicity = excluded.ethnicity, + nationality = excluded.nationality, + country = excluded.country, + eye_color = excluded.eye_color, + hair_color = excluded.hair_color, + height = excluded.height, + weight = excluded.weight, + measurements = excluded.measurements, + cup_size = excluded.cup_size, + tattoo_description = excluded.tattoo_description, + piercing_description = excluded.piercing_description, + boob_job = excluded.boob_job, + career = excluded.career, + career_start_year = excluded.career_start_year, + career_end_year = excluded.career_end_year, + date_of_death = excluded.date_of_death, + active = excluded.active, + image_path = excluded.image_path, + image_url = excluded.image_url, + poster_url = excluded.poster_url, + bio = excluded.bio, + source_numeric_id = excluded.source_numeric_id, + updated_at = excluded.updated_at + `, + p.Name, p.Aliases, + p.Gender, p.Birthday, p.Astrology, p.Birthplace, p.Ethnicity, p.Nationality, p.Country, + p.EyeColor, p.HairColor, p.Height, p.Weight, p.Measurements, p.CupSize, + p.TattooDescription, p.PiercingDescription, p.BoobJob, + p.Career, p.CareerStartYear, p.CareerEndYear, p.DateOfDeath, activeInt, + p.ImagePath, p.ImageURL, p.PosterURL, p.Bio, + p.Source, p.SourceID, p.SourceNumericID, + p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339), + ) if err != nil { return fmt.Errorf("failed to create performer: %w", err) @@ -46,11 +100,29 @@ func (s *PerformerStore) Create(p *model.Performer) error { func (s *PerformerStore) GetByID(id int64) (*model.Performer, error) { p := &model.Performer{} var createdAt, updatedAt string + var activeInt int err := s.db.conn.QueryRow(` - SELECT id, name, aliases, nationality, country, gender, image_path, image_url, bio, source, source_id, created_at, updated_at + SELECT + id, name, aliases, + gender, birthday, astrology, birthplace, ethnicity, nationality, country, + eye_color, hair_color, height, weight, measurements, cup_size, + tattoo_description, piercing_description, boob_job, + career, career_start_year, career_end_year, date_of_death, active, + image_path, image_url, poster_url, bio, + source, source_id, source_numeric_id, + created_at, updated_at FROM performers WHERE id = ? - `, id).Scan(&p.ID, &p.Name, &p.Aliases, &p.Nationality, &p.Country, &p.Gender, &p.ImagePath, &p.ImageURL, &p.Bio, &p.Source, &p.SourceID, &createdAt, &updatedAt) + `, id).Scan( + &p.ID, &p.Name, &p.Aliases, + &p.Gender, &p.Birthday, &p.Astrology, &p.Birthplace, &p.Ethnicity, &p.Nationality, &p.Country, + &p.EyeColor, &p.HairColor, &p.Height, &p.Weight, &p.Measurements, &p.CupSize, + &p.TattooDescription, &p.PiercingDescription, &p.BoobJob, + &p.Career, &p.CareerStartYear, &p.CareerEndYear, &p.DateOfDeath, &activeInt, + &p.ImagePath, &p.ImageURL, &p.PosterURL, &p.Bio, + &p.Source, &p.SourceID, &p.SourceNumericID, + &createdAt, &updatedAt, + ) if err == sql.ErrNoRows { return nil, fmt.Errorf("performer not found") @@ -59,6 +131,7 @@ func (s *PerformerStore) GetByID(id int64) (*model.Performer, error) { return nil, fmt.Errorf("failed to get performer: %w", err) } + p.Active = (activeInt == 1) p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) @@ -68,7 +141,15 @@ func (s *PerformerStore) GetByID(id int64) (*model.Performer, error) { // Search searches for performers by name func (s *PerformerStore) Search(query string) ([]model.Performer, error) { rows, err := s.db.conn.Query(` - SELECT id, name, aliases, nationality, country, gender, image_path, image_url, bio, source, source_id, created_at, updated_at + SELECT + id, name, aliases, + gender, birthday, astrology, birthplace, ethnicity, nationality, country, + eye_color, hair_color, height, weight, measurements, cup_size, + tattoo_description, piercing_description, boob_job, + career, career_start_year, career_end_year, date_of_death, active, + image_path, image_url, poster_url, bio, + source, source_id, source_numeric_id, + created_at, updated_at FROM performers WHERE name LIKE ? OR aliases LIKE ? ORDER BY name @@ -83,12 +164,23 @@ func (s *PerformerStore) Search(query string) ([]model.Performer, error) { for rows.Next() { var p model.Performer var createdAt, updatedAt string + var activeInt int - err := rows.Scan(&p.ID, &p.Name, &p.Aliases, &p.Nationality, &p.Country, &p.Gender, &p.ImagePath, &p.ImageURL, &p.Bio, &p.Source, &p.SourceID, &createdAt, &updatedAt) + err := rows.Scan( + &p.ID, &p.Name, &p.Aliases, + &p.Gender, &p.Birthday, &p.Astrology, &p.Birthplace, &p.Ethnicity, &p.Nationality, &p.Country, + &p.EyeColor, &p.HairColor, &p.Height, &p.Weight, &p.Measurements, &p.CupSize, + &p.TattooDescription, &p.PiercingDescription, &p.BoobJob, + &p.Career, &p.CareerStartYear, &p.CareerEndYear, &p.DateOfDeath, &activeInt, + &p.ImagePath, &p.ImageURL, &p.PosterURL, &p.Bio, + &p.Source, &p.SourceID, &p.SourceNumericID, + &createdAt, &updatedAt, + ) if err != nil { return nil, fmt.Errorf("failed to scan performer: %w", err) } + p.Active = (activeInt == 1) p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) diff --git a/internal/db/schema.go b/internal/db/schema.go index b975b93..02f89af 100644 --- a/internal/db/schema.go +++ b/internal/db/schema.go @@ -9,16 +9,48 @@ CREATE TABLE IF NOT EXISTS performers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, aliases TEXT, + + -- Physical attributes + gender TEXT, + birthday TEXT, + astrology TEXT, + birthplace TEXT, + ethnicity TEXT, nationality TEXT, country TEXT, - gender TEXT, + eye_color TEXT, + hair_color TEXT, + height INTEGER, + weight INTEGER, + measurements TEXT, + cup_size TEXT, + tattoo_description TEXT, + piercing_description TEXT, + boob_job TEXT, + + -- Career information + career TEXT, + career_start_year INTEGER, + career_end_year INTEGER, + date_of_death TEXT, + active INTEGER DEFAULT 1, + + -- Media image_path TEXT, image_url TEXT, + poster_url TEXT, bio TEXT, + + -- Source tracking source TEXT, source_id TEXT, + source_numeric_id INTEGER, + created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + + -- Prevent duplicates from same source + UNIQUE(source, source_id) ); -- Studios table diff --git a/internal/model/performer.go b/internal/model/performer.go index 3dd2e7c..9e032d2 100644 --- a/internal/model/performer.go +++ b/internal/model/performer.go @@ -6,15 +6,44 @@ import "time" type Performer struct { ID int64 `json:"id"` Name string `json:"name"` - Aliases string `json:"aliases,omitempty"` // comma-separated for v0.1 - Nationality string `json:"nationality,omitempty"` // ISO country code - Country string `json:"country,omitempty"` // full country name - Gender string `json:"gender,omitempty"` // male/female/trans/other - ImagePath string `json:"image_path,omitempty"` - ImageURL string `json:"image_url,omitempty"` - Bio string `json:"bio,omitempty"` + Aliases string `json:"aliases,omitempty"` // comma-separated + + // Physical attributes + Gender string `json:"gender,omitempty"` // male/female/trans/other + Birthday string `json:"birthday,omitempty"` // YYYY-MM-DD + Astrology string `json:"astrology,omitempty"` // zodiac sign + Birthplace string `json:"birthplace,omitempty"` + Ethnicity string `json:"ethnicity,omitempty"` + Nationality string `json:"nationality,omitempty"` // ISO country code + Country string `json:"country,omitempty"` // full country name + EyeColor string `json:"eye_color,omitempty"` + HairColor string `json:"hair_color,omitempty"` + Height int `json:"height,omitempty"` // cm + Weight int `json:"weight,omitempty"` // kg + Measurements string `json:"measurements,omitempty"` // e.g., "32A-24-34" + CupSize string `json:"cup_size,omitempty"` // e.g., "32A" + TattooDescription string `json:"tattoo_description,omitempty"` + PiercingDescription string `json:"piercing_description,omitempty"` + BoobJob string `json:"boob_job,omitempty"` // True/False as string + + // Career information + Career string `json:"career,omitempty"` // e.g., "2010–2020" + CareerStartYear int `json:"career_start_year,omitempty"` + CareerEndYear int `json:"career_end_year,omitempty"` + DateOfDeath string `json:"date_of_death,omitempty"` // YYYY-MM-DD + Active bool `json:"active"` + + // Media + ImagePath string `json:"image_path,omitempty"` + ImageURL string `json:"image_url,omitempty"` + PosterURL string `json:"poster_url,omitempty"` + Bio string `json:"bio,omitempty"` + + // Source tracking Source string `json:"source,omitempty"` // tpdb, ae, etc. - SourceID string `json:"source_id,omitempty"` // remote ID at source + SourceID string `json:"source_id,omitempty"` // remote UUID + SourceNumericID int `json:"source_numeric_id,omitempty"` // TPDB numeric ID + CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/scraper/tpdb/mapper.go b/internal/scraper/tpdb/mapper.go index 6f172f9..db5c877 100644 --- a/internal/scraper/tpdb/mapper.go +++ b/internal/scraper/tpdb/mapper.go @@ -11,35 +11,118 @@ import ( // mapPerformer converts a TPDB performer to our internal model func mapPerformer(p PerformerResponse) model.Performer { performer := model.Performer{ - Name: p.Name, - Source: "tpdb", - SourceID: p.ID, + Name: p.Name, + Source: "tpdb", + SourceID: p.ID, + SourceNumericID: p.NumericID, } - // Map optional fields + // Aliases if len(p.Aliases) > 0 { performer.Aliases = strings.Join(p.Aliases, ", ") } - if p.Gender != "" { - performer.Gender = p.Gender - } - - if p.Nationality != nil { - performer.Country = *p.Nationality - performer.Nationality = *p.Nationality + // Bio + if p.Bio != nil { + performer.Bio = *p.Bio } + // Media if p.Image != nil { performer.ImageURL = *p.Image } - - // Build bio from available information - bio := "" - if p.Bio != nil { - bio = *p.Bio + if p.Poster != nil { + performer.PosterURL = *p.Poster + } + + // Extract from extras if available + if p.Extras != nil { + ex := p.Extras + + performer.Gender = ex.Gender + + // Personal information + if ex.Birthday != nil { + performer.Birthday = *ex.Birthday + } + if ex.Astrology != nil { + performer.Astrology = *ex.Astrology + } + if ex.Birthplace != nil { + performer.Birthplace = *ex.Birthplace + } + if ex.Ethnicity != nil { + performer.Ethnicity = *ex.Ethnicity + } + if ex.Nationality != nil { + performer.Nationality = *ex.Nationality + performer.Country = *ex.Nationality + } + if ex.Deathday != nil { + performer.DateOfDeath = *ex.Deathday + } + + // Physical attributes + if ex.EyeColour != nil { + performer.EyeColor = *ex.EyeColour + } + if ex.HairColour != nil { + performer.HairColor = *ex.HairColour + } + if ex.Height != nil { + // Parse "160cm" -> 160 + heightStr := strings.TrimSuffix(*ex.Height, "cm") + if h, err := strconv.Atoi(heightStr); err == nil { + performer.Height = h + } + } + if ex.Weight != nil { + // Parse "49kg" -> 49 + weightStr := strings.TrimSuffix(*ex.Weight, "kg") + if w, err := strconv.Atoi(weightStr); err == nil { + performer.Weight = w + } + } + if ex.Measurements != nil { + performer.Measurements = *ex.Measurements + } + if ex.Cupsize != nil { + performer.CupSize = *ex.Cupsize + } + if ex.Tattoos != nil { + performer.TattooDescription = *ex.Tattoos + } + if ex.Piercings != nil { + performer.PiercingDescription = *ex.Piercings + } + if ex.FakeBoobs != nil { + if *ex.FakeBoobs { + performer.BoobJob = "True" + } else { + performer.BoobJob = "False" + } + } + + // Career + if ex.CareerStartYear != nil { + performer.CareerStartYear = *ex.CareerStartYear + } + if ex.CareerEndYear != nil { + performer.CareerEndYear = *ex.CareerEndYear + } + // Build career string + if ex.CareerStartYear != nil { + if ex.CareerEndYear != nil && *ex.CareerEndYear > 0 { + performer.Career = fmt.Sprintf("%d–%d", *ex.CareerStartYear, *ex.CareerEndYear) + } else { + performer.Career = fmt.Sprintf("%d–", *ex.CareerStartYear) + } + } + performer.Active = true // Assume active if no end year + if ex.CareerEndYear != nil && *ex.CareerEndYear > 0 { + performer.Active = false + } } - performer.Bio = bio return performer } diff --git a/internal/scraper/tpdb/types.go b/internal/scraper/tpdb/types.go index 1756dda..9452d2b 100644 --- a/internal/scraper/tpdb/types.go +++ b/internal/scraper/tpdb/types.go @@ -18,30 +18,39 @@ type MetaData struct { Total int `json:"total"` } +// PerformerExtras contains detailed performer information from TPDB +type PerformerExtras struct { + Gender string `json:"gender"` + Birthday *string `json:"birthday"` + Deathday *string `json:"deathday"` + Birthplace *string `json:"birthplace"` + Astrology *string `json:"astrology"` + Ethnicity *string `json:"ethnicity"` + Nationality *string `json:"nationality"` + HairColour *string `json:"hair_colour"` + EyeColour *string `json:"eye_colour"` + Weight *string `json:"weight"` // e.g., "49kg" + Height *string `json:"height"` // e.g., "160cm" + Measurements *string `json:"measurements"` + Cupsize *string `json:"cupsize"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + FakeBoobs *bool `json:"fake_boobs"` + CareerStartYear *int `json:"career_start_year"` + CareerEndYear *int `json:"career_end_year"` +} + // PerformerResponse represents a TPDB performer type PerformerResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Gender string `json:"gender"` - Aliases []string `json:"aliases"` - Birthday *string `json:"birthday"` - Astrology *string `json:"astrology"` - Birthplace *string `json:"birthplace"` - Ethnicity *string `json:"ethnicity"` - Nationality *string `json:"nationality"` - EyeColor *string `json:"eye_color"` - HairColor *string `json:"hair_color"` - Height *int `json:"height"` - Weight *int `json:"weight"` - Measurements *string `json:"measurements"` - TattooDescription *string `json:"tattoo_description"` - PiercingDescription *string `json:"piercing_description"` - BoobJob *string `json:"boob_job"` - Bio *string `json:"bio"` - Active *int `json:"active"` - Image *string `json:"image"` - Poster *string `json:"poster"` + ID string `json:"id"` // UUID + NumericID int `json:"_id"` // Numeric ID + Name string `json:"name"` + Slug string `json:"slug"` + Aliases []string `json:"aliases"` + Bio *string `json:"bio"` + Image *string `json:"image"` + Poster *string `json:"poster"` + Extras *PerformerExtras `json:"extras"` // Detailed info } // StudioResponse represents a TPDB studio/site