Goondex/internal/scraper/tpdb/mapper.go
Team Goon d9048db660 v0.1.0-dev3: Complete TPDB metadata with duplicate prevention
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>
2025-11-15 11:50:40 -05:00

239 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package 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)
}