Goondex/internal/web/server.go

1131 lines
30 KiB
Go

package web
import (
"context"
"embed"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"strconv"
"time"
"git.leaktechnologies.dev/stu/Goondex/internal/db"
import_service "git.leaktechnologies.dev/stu/Goondex/internal/import"
"git.leaktechnologies.dev/stu/Goondex/internal/model"
"git.leaktechnologies.dev/stu/Goondex/internal/scraper/tpdb"
"git.leaktechnologies.dev/stu/Goondex/internal/sync"
)
// ============================================================================
// EMBED STATIC + TEMPLATES
// ============================================================================
//go:embed templates/* static/**/*
var content embed.FS
type Server struct {
db *db.DB
templates *template.Template
addr string
}
func NewServer(database *db.DB, addr string) (*Server, error) {
tmpl, err := template.ParseFS(content, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("failed to parse templates: %w", err)
}
return &Server{
db: database,
templates: tmpl,
addr: addr,
}, nil
}
func (s *Server) Start() error {
mux := http.NewServeMux()
// ============================================================================
// FIXED STATIC SERVER — THIS IS THE CORRECT WAY
// ============================================================================
staticFS, err := fs.Sub(content, "static")
if err != nil {
return fmt.Errorf("failed to load embedded static directory: %w", err)
}
mux.Handle(
"/static/",
http.StripPrefix(
"/static/",
http.FileServer(http.FS(staticFS)),
),
)
// ============================================================================
// ROUTES
// ============================================================================
mux.HandleFunc("/", s.handleDashboard)
mux.HandleFunc("/performers", s.handlePerformerList)
mux.HandleFunc("/performers/", s.handlePerformerDetail)
mux.HandleFunc("/studios", s.handleStudioList)
mux.HandleFunc("/studios/", s.handleStudioDetail)
mux.HandleFunc("/scenes", s.handleSceneList)
mux.HandleFunc("/scenes/", s.handleSceneDetail)
mux.HandleFunc("/movies", s.handleMovieList)
mux.HandleFunc("/movies/", s.handleMovieDetail)
// API
mux.HandleFunc("/api/import/performer", s.handleAPIImportPerformer)
mux.HandleFunc("/api/import/studio", s.handleAPIImportStudio)
mux.HandleFunc("/api/import/scene", s.handleAPIImportScene)
mux.HandleFunc("/api/import/all", s.handleAPIBulkImportAll)
mux.HandleFunc("/api/import/all-performers", s.handleAPIBulkImportPerformers)
mux.HandleFunc("/api/import/all-studios", s.handleAPIBulkImportStudios)
mux.HandleFunc("/api/import/all-scenes", s.handleAPIBulkImportScenes)
mux.HandleFunc("/api/import/all-performers/progress", s.handleAPIBulkImportPerformersProgress)
mux.HandleFunc("/api/import/all-studios/progress", s.handleAPIBulkImportStudiosProgress)
mux.HandleFunc("/api/import/all-scenes/progress", s.handleAPIBulkImportScenesProgress)
mux.HandleFunc("/api/sync", s.handleAPISync)
mux.HandleFunc("/api/sync/status", s.handleAPISyncStatus)
mux.HandleFunc("/api/search", s.handleAPIGlobalSearch)
// ============================================================================
// START SERVER
// ============================================================================
fmt.Printf("Starting Goondex Web Server at http://%s\n", s.addr)
return http.ListenAndServe(s.addr, mux)
}
// ============================================================================
// PAGE HANDLERS
// ============================================================================
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
performerStore := db.NewPerformerStore(s.db)
studioStore := db.NewStudioStore(s.db)
sceneStore := db.NewSceneStore(s.db)
movieStore := db.NewMovieStore(s.db)
performers, _ := performerStore.Search("")
studios, _ := studioStore.Search("")
scenes, _ := sceneStore.Search("")
movies, _ := movieStore.Search("")
data := map[string]interface{}{
"PageTitle": "Dashboard",
"ActivePage": "dashboard",
"PerformerCount": len(performers),
"StudioCount": len(studios),
"SceneCount": len(scenes),
"MovieCount": len(movies),
}
s.templates.ExecuteTemplate(w, "dashboard.html", data)
}
func (s *Server) handlePerformerList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
nationalityFilter := r.URL.Query().Get("nationality")
genderFilter := r.URL.Query().Get("gender")
store := db.NewPerformerStore(s.db)
performers, err := store.Search(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Apply filters
var filteredPerformers []interface{}
for _, p := range performers {
// Filter by nationality if specified
if nationalityFilter != "" && nationalityFilter != "all" {
if p.Nationality != nationalityFilter {
continue
}
}
// Filter by gender if specified
if genderFilter != "" && genderFilter != "all" {
if p.Gender != genderFilter {
continue
}
}
filteredPerformers = append(filteredPerformers, p)
}
type PerformerWithCount struct {
Performer interface{}
SceneCount int
Age int
CountryFlag string
}
var performersWithCounts []PerformerWithCount
for _, p := range filteredPerformers {
performer := p.(model.Performer)
count, _ := store.GetSceneCount(performer.ID)
// Calculate age from birthday
age := calculateAge(performer.Birthday)
// Get country flag emoji
countryFlag := getCountryFlag(performer.Nationality)
performersWithCounts = append(performersWithCounts,
PerformerWithCount{
Performer: performer,
SceneCount: count,
Age: age,
CountryFlag: countryFlag,
})
}
// Get unique nationalities and genders for filter dropdowns
nationalitiesMap := make(map[string]bool)
gendersMap := make(map[string]bool)
for _, p := range performers {
if p.Nationality != "" {
nationalitiesMap[p.Nationality] = true
}
if p.Gender != "" {
gendersMap[p.Gender] = true
}
}
var nationalities []string
for nat := range nationalitiesMap {
nationalities = append(nationalities, nat)
}
var genders []string
for gender := range gendersMap {
genders = append(genders, gender)
}
data := map[string]interface{}{
"PageTitle": "Performers",
"ActivePage": "performers",
"Performers": performersWithCounts,
"Query": query,
"Nationalities": nationalities,
"Genders": genders,
"SelectedNationality": nationalityFilter,
"SelectedGender": genderFilter,
}
s.templates.ExecuteTemplate(w, "performers.html", data)
}
// calculateAge calculates age from birthday string (YYYY-MM-DD)
func calculateAge(birthday string) int {
if birthday == "" {
return 0
}
birthDate, err := time.Parse("2006-01-02", birthday)
if err != nil {
return 0
}
now := time.Now()
age := now.Year() - birthDate.Year()
// Adjust if birthday hasn't occurred yet this year
if now.Month() < birthDate.Month() ||
(now.Month() == birthDate.Month() && now.Day() < birthDate.Day()) {
age--
}
return age
}
// getCountryFlag converts ISO country code to flag emoji
func getCountryFlag(countryCode string) string {
if countryCode == "" {
return ""
}
// Map of common ISO country codes to flag emojis
// Unicode flags are formed by regional indicator symbols
countryFlags := map[string]string{
"US": "🇺🇸", "GB": "🇬🇧", "CA": "🇨🇦", "AU": "🇦🇺",
"FR": "🇫🇷", "DE": "🇩🇪", "IT": "🇮🇹", "ES": "🇪🇸",
"RU": "🇷🇺", "JP": "🇯🇵", "CN": "🇨🇳", "BR": "🇧🇷",
"MX": "🇲🇽", "AR": "🇦🇷", "CL": "🇨🇱", "CO": "🇨🇴",
"CZ": "🇨🇿", "HU": "🇭🇺", "PL": "🇵🇱", "RO": "🇷🇴",
"SK": "🇸🇰", "UA": "🇺🇦", "SE": "🇸🇪", "NO": "🇳🇴",
"FI": "🇫🇮", "DK": "🇩🇰", "NL": "🇳🇱", "BE": "🇧🇪",
"CH": "🇨🇭", "AT": "🇦🇹", "PT": "🇵🇹", "GR": "🇬🇷",
"IE": "🇮🇪", "NZ": "🇳🇿", "ZA": "🇿🇦", "IN": "🇮🇳",
"TH": "🇹🇭", "VN": "🇻🇳", "PH": "🇵🇭", "ID": "🇮🇩",
"MY": "🇲🇾", "SG": "🇸🇬", "KR": "🇰🇷", "TW": "🇹🇼",
"HK": "🇭🇰", "TR": "🇹🇷", "IL": "🇮🇱", "EG": "🇪🇬",
}
if flag, ok := countryFlags[countryCode]; ok {
return flag
}
// For codes not in our map, try to convert programmatically
// This works for any valid ISO 3166-1 alpha-2 code
if len(countryCode) == 2 {
code := []rune(countryCode)
if code[0] >= 'A' && code[0] <= 'Z' && code[1] >= 'A' && code[1] <= 'Z' {
// Regional indicator symbol base
const regionalBase = 0x1F1E6 - 'A'
return string([]rune{
rune(regionalBase + code[0]),
rune(regionalBase + code[1]),
})
}
}
return "🌍" // Default globe emoji
}
func (s *Server) handlePerformerDetail(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/performers/"):]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid performer ID", http.StatusBadRequest)
return
}
performerStore := db.NewPerformerStore(s.db)
sceneStore := db.NewSceneStore(s.db)
performer, err := performerStore.GetByID(id)
if err != nil {
http.NotFound(w, r)
return
}
sceneCount, _ := performerStore.GetSceneCount(id)
scenes, _ := sceneStore.GetByPerformer(id)
data := map[string]interface{}{
"PageTitle": fmt.Sprintf("Performer: %s", performer.Name),
"ActivePage": "performers",
"Stylesheets": []string{"/static/css/style.css", "/static/css/goondex.css"},
"Performer": performer,
"SceneCount": sceneCount,
"Scenes": scenes,
}
s.templates.ExecuteTemplate(w, "performer_detail.html", data)
}
func (s *Server) handleStudioList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
store := db.NewStudioStore(s.db)
studios, err := store.Search(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type StudioWithCount struct {
Studio interface{}
SceneCount int
}
var studiosWithCounts []StudioWithCount
for _, st := range studios {
count, _ := store.GetSceneCount(st.ID)
studiosWithCounts = append(studiosWithCounts,
StudioWithCount{Studio: st, SceneCount: count})
}
data := map[string]interface{}{
"PageTitle": "Studios",
"ActivePage": "studios",
"Studios": studiosWithCounts,
"Query": query,
}
s.templates.ExecuteTemplate(w, "studios.html", data)
}
func (s *Server) handleStudioDetail(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/studios/"):]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid studio ID", http.StatusBadRequest)
return
}
store := db.NewStudioStore(s.db)
studio, err := store.GetByID(id)
if err != nil {
http.NotFound(w, r)
return
}
sceneCount, _ := store.GetSceneCount(id)
data := map[string]interface{}{
"PageTitle": fmt.Sprintf("Studio: %s", studio.Name),
"ActivePage": "studios",
"Stylesheets": []string{"/static/css/style.css", "/static/css/goondex.css"},
"Studio": studio,
"SceneCount": sceneCount,
}
s.templates.ExecuteTemplate(w, "studio_detail.html", data)
}
func (s *Server) handleSceneList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
sceneStore := db.NewSceneStore(s.db)
studioStore := db.NewStudioStore(s.db)
scenes, err := sceneStore.Search(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type SceneWithStudio struct {
Scene interface{}
StudioName string
}
var scenesWithStudios []SceneWithStudio
for _, sc := range scenes {
studioName := ""
if sc.StudioID != nil && *sc.StudioID > 0 {
if studio, err := studioStore.GetByID(*sc.StudioID); err == nil {
studioName = studio.Name
}
}
scenesWithStudios = append(scenesWithStudios,
SceneWithStudio{Scene: sc, StudioName: studioName})
}
data := map[string]interface{}{
"PageTitle": "Scenes",
"ActivePage": "scenes",
"Scenes": scenesWithStudios,
"Query": query,
}
s.templates.ExecuteTemplate(w, "scenes.html", data)
}
func (s *Server) handleSceneDetail(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/scenes/"):]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid scene ID", http.StatusBadRequest)
return
}
sceneStore := db.NewSceneStore(s.db)
studioStore := db.NewStudioStore(s.db)
scene, err := sceneStore.GetByID(id)
if err != nil {
http.NotFound(w, r)
return
}
performers, _ := sceneStore.GetPerformers(id)
tags, _ := sceneStore.GetTags(id)
movies, _ := sceneStore.GetMovies(id)
studioName := ""
if scene.StudioID != nil && *scene.StudioID > 0 {
if studio, err := studioStore.GetByID(*scene.StudioID); err == nil {
studioName = studio.Name
}
}
data := map[string]interface{}{
"PageTitle": fmt.Sprintf("Scene: %s", scene.Title),
"ActivePage": "scenes",
"Stylesheets": []string{"/static/css/style.css", "/static/css/goondex.css"},
"Scene": scene,
"Performers": performers,
"Tags": tags,
"Movies": movies,
"StudioName": studioName,
}
s.templates.ExecuteTemplate(w, "scene_detail.html", data)
}
func (s *Server) handleMovieList(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
movieStore := db.NewMovieStore(s.db)
studioStore := db.NewStudioStore(s.db)
movies, err := movieStore.Search(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type MovieWithDetails struct {
Movie interface{}
StudioName string
SceneCount int
}
var moviesWithDetails []MovieWithDetails
for _, m := range movies {
studioName := ""
if m.StudioID != nil && *m.StudioID > 0 {
if studio, err := studioStore.GetByID(*m.StudioID); err == nil {
studioName = studio.Name
}
}
sceneCount, _ := movieStore.GetSceneCount(m.ID)
moviesWithDetails = append(moviesWithDetails,
MovieWithDetails{Movie: m, StudioName: studioName, SceneCount: sceneCount})
}
data := map[string]interface{}{
"PageTitle": "Movies",
"ActivePage": "movies",
"Movies": moviesWithDetails,
"Query": query,
}
s.templates.ExecuteTemplate(w, "movies.html", data)
}
func (s *Server) handleMovieDetail(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/movies/"):]
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid movie ID", http.StatusBadRequest)
return
}
movieStore := db.NewMovieStore(s.db)
studioStore := db.NewStudioStore(s.db)
movie, err := movieStore.GetByID(id)
if err != nil {
http.NotFound(w, r)
return
}
scenes, _ := movieStore.GetScenes(id)
studioName := ""
if movie.StudioID != nil && *movie.StudioID > 0 {
if studio, err := studioStore.GetByID(*movie.StudioID); err == nil {
studioName = studio.Name
}
}
data := map[string]interface{}{
"PageTitle": fmt.Sprintf("Movie: %s", movie.Title),
"ActivePage": "movies",
"Movie": movie,
"Scenes": scenes,
"StudioName": studioName,
}
s.templates.ExecuteTemplate(w, "movie_detail.html", data)
}
// ============================================================================
// API HANDLERS (NO LOGIC CHANGES — ONLY STATIC FIXES ABOVE)
// ============================================================================
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
const tpdbEnabled = false
const tpdbDisabledMessage = "TPDB integration is disabled. Use the Adult Empire CLI commands to import and enrich data for now."
func tpdbAPIKey() (string, error) {
if !tpdbEnabled {
return "", fmt.Errorf(tpdbDisabledMessage)
}
apiKey := os.Getenv("TPDB_API_KEY")
if apiKey == "" {
return "", fmt.Errorf("TPDB_API_KEY not configured")
}
return apiKey, nil
}
func writeTPDBError(w http.ResponseWriter, err error) bool {
if err == nil {
return false
}
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: err.Error()})
return true
}
func (s *Server) handleAPIImportPerformer(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Invalid request"})
return
}
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
performers, err := scraper.SearchPerformers(context.Background(), req.Query)
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Search failed: %v", err)})
return
}
if len(performers) == 0 {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "No performers found"})
return
}
store := db.NewPerformerStore(s.db)
imported := 0
for _, p := range performers {
fullPerformer, err := scraper.GetPerformerByID(context.Background(), p.SourceID)
if err != nil {
fullPerformer = &p
}
if err := store.Create(fullPerformer); err != nil {
continue
}
imported++
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Imported %d performer(s)", imported),
Data: map[string]int{"imported": imported, "found": len(performers)},
})
}
func (s *Server) handleAPIImportStudio(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Invalid request"})
return
}
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
studios, err := scraper.SearchStudios(context.Background(), req.Query)
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Search failed: %v", err)})
return
}
if len(studios) == 0 {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "No studios found"})
return
}
store := db.NewStudioStore(s.db)
imported := 0
for _, st := range studios {
if err := store.Create(&st); err != nil {
continue
}
imported++
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Imported %d studio(s)", imported),
Data: map[string]int{"imported": imported, "found": len(studios)},
})
}
func (s *Server) handleAPIImportScene(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Invalid request"})
return
}
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
scenes, err := scraper.SearchScenes(context.Background(), req.Query)
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Search failed: %v", err)})
return
}
if len(scenes) == 0 {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "No scenes found"})
return
}
sceneStore := db.NewSceneStore(s.db)
performerStore := db.NewPerformerStore(s.db)
studioStore := db.NewStudioStore(s.db)
tagStore := db.NewTagStore(s.db)
imported := 0
for _, sc := range scenes {
if sc.Studio != nil {
if err := studioStore.Create(sc.Studio); err != nil {
studios, _ := studioStore.Search(sc.Studio.Name)
if len(studios) > 0 {
sc.StudioID = &studios[0].ID
}
} else {
sc.StudioID = &sc.Studio.ID
}
}
if err := sceneStore.Create(&sc); err != nil {
continue
}
for _, p := range sc.Performers {
if err := performerStore.Create(&p); err != nil {
performers, _ := performerStore.Search(p.Name)
if len(performers) > 0 {
p.ID = performers[0].ID
}
}
if p.ID > 0 {
sceneStore.AddPerformer(sc.ID, p.ID)
}
}
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)
}
}
imported++
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Imported %d scene(s)", imported),
Data: map[string]int{"imported": imported, "found": len(scenes)},
})
}
func (s *Server) handleAPISync(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Force bool `json:"force"`
}
json.NewDecoder(r.Body).Decode(&req)
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := sync.NewService(s.db, scraper)
opts := sync.SyncOptions{
Force: req.Force,
MinInterval: 24 * time.Hour,
}
results, err := service.SyncAll(context.Background(), opts)
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Sync failed: %v", err)})
return
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: "Sync completed",
Data: results,
})
}
func (s *Server) handleAPISyncStatus(w http.ResponseWriter, r *http.Request) {
if writeTPDBError(w, fmt.Errorf(tpdbDisabledMessage)) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", "")
service := sync.NewService(s.db, scraper)
statuses, err := service.GetSyncStatus()
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Failed to get status: %v", err)})
return
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: "Status retrieved",
Data: statuses,
})
}
// ============================================================================
// BULK IMPORT ENDPOINTS + SSE PROGRESS
// ============================================================================
func (s *Server) handleAPIBulkImportAll(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
results, err := service.BulkImportAll(context.Background())
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Bulk import failed: %v", err)})
return
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: "Bulk import completed successfully",
Data: results,
})
}
func (s *Server) handleAPIBulkImportPerformers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
result, err := service.BulkImportAllPerformers(context.Background())
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Import failed: %v", err)})
return
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Imported %d/%d performers", result.Imported, result.Total),
Data: result,
})
}
func (s *Server) handleAPIBulkImportStudios(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
result, err := service.BulkImportAllStudios(context.Background())
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Import failed: %v", err)})
return
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Imported %d/%d studios", result.Imported, result.Total),
Data: result,
})
}
func (s *Server) handleAPIBulkImportScenes(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
apiKey, err := tpdbAPIKey()
if writeTPDBError(w, err) {
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
result, err := service.BulkImportAllScenes(context.Background())
if err != nil {
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Import failed: %v", err)})
return
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Imported %d/%d scenes", result.Imported, result.Total),
Data: result,
})
}
func (s *Server) handleAPIBulkImportPerformersProgress(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
apiKey, err := tpdbAPIKey()
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
progressCallback := func(update import_service.ProgressUpdate) {
data, _ := json.Marshal(update)
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
result, err := service.BulkImportAllPerformersWithProgress(
context.Background(), progressCallback)
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
} else {
data, _ := json.Marshal(map[string]interface{}{
"complete": true,
"result": result,
})
fmt.Fprintf(w, "data: %s\n\n", data)
}
flusher.Flush()
}
func (s *Server) handleAPIBulkImportStudiosProgress(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
apiKey, err := tpdbAPIKey()
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
progressCallback := func(update import_service.ProgressUpdate) {
data, _ := json.Marshal(update)
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
result, err := service.BulkImportAllStudiosWithProgress(
context.Background(), progressCallback)
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
} else {
data, _ := json.Marshal(map[string]interface{}{
"complete": true,
"result": result,
})
fmt.Fprintf(w, "data: %s\n\n", data)
}
flusher.Flush()
}
func (s *Server) handleAPIBulkImportScenesProgress(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
apiKey, err := tpdbAPIKey()
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
return
}
scraper := tpdb.NewScraper("https://api.theporndb.net", apiKey)
service := import_service.NewService(s.db, scraper)
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
progressCallback := func(update import_service.ProgressUpdate) {
data, _ := json.Marshal(update)
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
result, err := service.BulkImportAllScenesWithProgress(
context.Background(), progressCallback)
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
} else {
data, _ := json.Marshal(map[string]interface{}{
"complete": true,
"result": result,
})
fmt.Fprintf(w, "data: %s\n\n", data)
}
flusher.Flush()
}
// ============================================================================
// GLOBAL SEARCH
// ============================================================================
func (s *Server) handleAPIGlobalSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
json.NewEncoder(w).Encode(APIResponse{
Success: false,
Message: "Query parameter required",
})
return
}
performerStore := db.NewPerformerStore(s.db)
studioStore := db.NewStudioStore(s.db)
sceneStore := db.NewSceneStore(s.db)
tagStore := db.NewTagStore(s.db)
performers, _ := performerStore.Search(query)
studios, _ := studioStore.Search(query)
scenes, _ := sceneStore.Search(query)
tags, _ := tagStore.Search(query)
results := map[string]interface{}{
"performers": performers,
"studios": studios,
"scenes": scenes,
"tags": tags,
"total": len(performers) + len(studios) + len(scenes) + len(tags),
}
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Message: fmt.Sprintf("Found %d results", results["total"]),
Data: results,
})
}