This release adds comprehensive metadata support and fixes the duplicate performer issue. MAJOR FIXES: ✅ Duplicate Prevention - Added UNIQUE(source, source_id) constraint to performers table - ON CONFLICT DO UPDATE in performer store - No more duplicate Riley Reid entries! ✅ Comprehensive TPDB Metadata - Extended Performer model with ALL TPDB fields - Physical: height, weight, measurements, cup size, eye/hair color - Personal: birthday, astrology, birthplace, ethnicity, nationality - Body: tattoos, piercings, boob job status - Career: start/end years, active status - Added PerformerExtras nested struct for TPDB "extras" object - Parse weight/height strings ("49kg" -> 49, "160cm" -> 160) - Handle British spelling (hair_colour, eye_colour) ✅ Enriched Import - Auto-fetch full performer details via GetPerformerByID - Search results now enriched with complete metadata - UUID + numeric TPDB ID both stored ✅ Enhanced CLI Output - Formatted display with all available stats - Height shown in cm and feet/inches - Weight shown in kg and lbs - Organized sections (IDs, Personal, Physical, Bio, Media) - Beautiful separator bars TECHNICAL DETAILS: - Schema: 25+ new performer fields with proper types - Types: PerformerExtras struct for nested TPDB response - Mapper: String parsing for "160cm", "49kg" format - Store: Full field support in Create/Search/GetByID - Display: Conditional rendering of all available data TESTING: ✅ Riley Reid import: All 25+ fields populated correctly ✅ Duplicate prevention: Second import updates existing record ✅ Broad search ("riley"): Only 2 unique performers ✅ Data accuracy: Matches theporndb.net/performers/riley-reid Database now captures: - UUID: 26d101c0-1e23-4e1f-ac12-8c30e0e2f451 - TPDB ID: 83047 - Birthday: 1991-07-09 - Height: 160cm (5'3") - Weight: 49kg (108lb) - Measurements: 32A-24-34 - All tattoos, piercings, career info, and full bio 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
239 lines
4.9 KiB
Go
239 lines
4.9 KiB
Go
package tpdb
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"git.leaktechnologies.dev/stu/Goondex/internal/model"
|
||
)
|
||
|
||
// 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,
|
||
SourceNumericID: p.NumericID,
|
||
}
|
||
|
||
// Aliases
|
||
if len(p.Aliases) > 0 {
|
||
performer.Aliases = strings.Join(p.Aliases, ", ")
|
||
}
|
||
|
||
// Bio
|
||
if p.Bio != nil {
|
||
performer.Bio = *p.Bio
|
||
}
|
||
|
||
// Media
|
||
if p.Image != nil {
|
||
performer.ImageURL = *p.Image
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
return performer
|
||
}
|
||
|
||
// mapStudio converts a TPDB studio to our internal model
|
||
func mapStudio(s StudioResponse) model.Studio {
|
||
studio := model.Studio{
|
||
Name: s.Name,
|
||
Source: "tpdb",
|
||
SourceID: strconv.Itoa(s.ID),
|
||
}
|
||
|
||
if s.Description != nil {
|
||
studio.Description = *s.Description
|
||
}
|
||
|
||
if s.Logo != nil {
|
||
studio.ImageURL = *s.Logo
|
||
}
|
||
|
||
// Handle parent studio
|
||
if s.Parent != nil {
|
||
// We'll need to look up or create the parent studio separately
|
||
// For now, we'll store the parent ID as a string that needs to be resolved
|
||
// This is a limitation that should be handled by the import logic
|
||
}
|
||
|
||
return studio
|
||
}
|
||
|
||
// mapScene converts a TPDB scene to our internal model
|
||
func mapScene(s SceneResponse) model.Scene {
|
||
scene := model.Scene{
|
||
Title: s.Title,
|
||
Source: "tpdb",
|
||
SourceID: s.ID,
|
||
}
|
||
|
||
if s.Description != nil {
|
||
scene.Description = *s.Description
|
||
}
|
||
|
||
if s.URL != nil {
|
||
scene.URL = *s.URL
|
||
}
|
||
|
||
if s.Date != nil {
|
||
scene.Date = *s.Date
|
||
}
|
||
|
||
if s.Image != nil {
|
||
scene.ImageURL = *s.Image
|
||
}
|
||
|
||
if s.Director != nil {
|
||
scene.Director = *s.Director
|
||
}
|
||
|
||
if s.Code != nil {
|
||
scene.Code = *s.Code
|
||
}
|
||
|
||
// Map performers
|
||
if len(s.Performers) > 0 {
|
||
performers := make([]model.Performer, 0, len(s.Performers))
|
||
for _, p := range s.Performers {
|
||
performer := model.Performer{
|
||
Name: p.Name,
|
||
Source: "tpdb",
|
||
SourceID: p.ID,
|
||
}
|
||
if p.Gender != nil {
|
||
performer.Gender = *p.Gender
|
||
}
|
||
performers = append(performers, performer)
|
||
}
|
||
scene.Performers = performers
|
||
}
|
||
|
||
// Map tags
|
||
if len(s.Tags) > 0 {
|
||
tags := make([]model.Tag, 0, len(s.Tags))
|
||
for _, t := range s.Tags {
|
||
tag := model.Tag{
|
||
Name: t.Name,
|
||
Source: "tpdb",
|
||
SourceID: t.ID,
|
||
}
|
||
tags = append(tags, tag)
|
||
}
|
||
scene.Tags = tags
|
||
}
|
||
|
||
// Map studio
|
||
if s.Site != nil {
|
||
studio := model.Studio{
|
||
Name: s.Site.Name,
|
||
Source: "tpdb",
|
||
SourceID: strconv.Itoa(s.Site.ID),
|
||
}
|
||
if s.Site.URL != nil {
|
||
studio.Description = fmt.Sprintf("URL: %s", *s.Site.URL)
|
||
}
|
||
scene.Studio = &studio
|
||
}
|
||
|
||
return scene
|
||
}
|
||
|
||
// stringToInt64 safely converts a string to int64
|
||
func stringToInt64(s string) (int64, error) {
|
||
return strconv.ParseInt(s, 10, 64)
|
||
}
|