1430 lines
39 KiB
Go
1430 lines
39 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"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/config"
|
|
"git.leaktechnologies.dev/stu/Goondex/internal/scraper/adultemp"
|
|
"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)
|
|
mux.HandleFunc("/settings", s.handleSettingsPage)
|
|
|
|
// Adult Empire endpoints
|
|
mux.HandleFunc("/api/ae/import/performer", s.handleAEImportPerformer)
|
|
mux.HandleFunc("/api/ae/import/performer-by-url", s.handleAEImportPerformerByURL)
|
|
mux.HandleFunc("/api/ae/import/scene", s.handleAEImportScene)
|
|
mux.HandleFunc("/api/ae/import/scene-by-url", s.handleAEImportSceneByURL)
|
|
|
|
// Settings endpoints
|
|
mux.HandleFunc("/api/settings/api-keys", s.handleAPISettingsKeys)
|
|
|
|
// 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)
|
|
}
|
|
|
|
func (s *Server) render(w http.ResponseWriter, name string, data interface{}) {
|
|
if err := s.templates.ExecuteTemplate(w, name, data); err != nil {
|
|
log.Printf("template render error (%s): %v", name, err)
|
|
http.Error(w, fmt.Sprintf("template render error: %v", err), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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.render(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 = true
|
|
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 := config.GetAPIKeys().TPDBAPIKey
|
|
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,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Adult Empire API (search + scrape)
|
|
// ============================================================================
|
|
|
|
func (s *Server) handleAEImportPerformer(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 || strings.TrimSpace(req.Query) == "" {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "query is required"})
|
|
return
|
|
}
|
|
|
|
scraper, err := adultemp.NewScraper()
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scraper init failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
results, err := scraper.SearchPerformersByName(r.Context(), req.Query)
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("search failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "No performers found on Adult Empire"})
|
|
return
|
|
}
|
|
|
|
top := results[0]
|
|
data, err := scraper.ScrapePerformerByURL(r.Context(), top.URL)
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scrape failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
performer := scraper.ConvertPerformerToModel(data)
|
|
|
|
store := db.NewPerformerStore(s.db)
|
|
if err := store.Create(performer); err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("save failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(APIResponse{
|
|
Success: true,
|
|
Message: fmt.Sprintf("Imported %s from Adult Empire", performer.Name),
|
|
Data: map[string]interface{}{
|
|
"name": performer.Name,
|
|
"url": top.URL,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAEImportPerformerByURL(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.URL) == "" {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "url is required"})
|
|
return
|
|
}
|
|
|
|
scraper, err := adultemp.NewScraper()
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scraper init failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
data, err := scraper.ScrapePerformerByURL(r.Context(), req.URL)
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scrape failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
performer := scraper.ConvertPerformerToModel(data)
|
|
store := db.NewPerformerStore(s.db)
|
|
if err := store.Create(performer); err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("save failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(APIResponse{
|
|
Success: true,
|
|
Message: fmt.Sprintf("Imported %s from Adult Empire", performer.Name),
|
|
Data: map[string]interface{}{
|
|
"name": performer.Name,
|
|
"url": req.URL,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAEImportScene(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 || strings.TrimSpace(req.Query) == "" {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "query is required"})
|
|
return
|
|
}
|
|
|
|
scraper, err := adultemp.NewScraper()
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scraper init failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
results, err := scraper.SearchScenesByName(r.Context(), req.Query)
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("search failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "No scenes found on Adult Empire"})
|
|
return
|
|
}
|
|
|
|
top := results[0]
|
|
data, err := scraper.ScrapeSceneByURL(r.Context(), top.URL)
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scrape failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
scene := scraper.ConvertSceneToModel(data)
|
|
sceneStore := db.NewSceneStore(s.db)
|
|
|
|
if err := sceneStore.Create(scene); err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("save failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(APIResponse{
|
|
Success: true,
|
|
Message: fmt.Sprintf("Imported scene: %s", scene.Title),
|
|
Data: map[string]interface{}{
|
|
"title": scene.Title,
|
|
"url": top.URL,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAEImportSceneByURL(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.URL) == "" {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "url is required"})
|
|
return
|
|
}
|
|
|
|
scraper, err := adultemp.NewScraper()
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scraper init failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
data, err := scraper.ScrapeSceneByURL(r.Context(), req.URL)
|
|
if err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("scrape failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
scene := scraper.ConvertSceneToModel(data)
|
|
sceneStore := db.NewSceneStore(s.db)
|
|
|
|
if err := sceneStore.Create(scene); err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("save failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(APIResponse{
|
|
Success: true,
|
|
Message: fmt.Sprintf("Imported scene: %s", scene.Title),
|
|
Data: map[string]interface{}{
|
|
"title": scene.Title,
|
|
"url": req.URL,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// SETTINGS
|
|
// ============================================================================
|
|
|
|
func (s *Server) handleSettingsPage(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/settings" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"PageTitle": "Settings",
|
|
"ActivePage": "settings",
|
|
}
|
|
|
|
s.render(w, "settings.html", data)
|
|
}
|
|
|
|
type apiKeysPayload struct {
|
|
TPDBAPIKey string `json:"tpdb_api_key"`
|
|
AEAPIKey string `json:"ae_api_key"`
|
|
StashDBAPIKey string `json:"stashdb_api_key"`
|
|
StashDBEndpoint string `json:"stashdb_endpoint"`
|
|
}
|
|
|
|
func (s *Server) handleAPISettingsKeys(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
keys := config.GetAPIKeys()
|
|
resp := map[string]interface{}{
|
|
"tpdbConfigured": keys.TPDBAPIKey != "",
|
|
"aeConfigured": keys.AEAPIKey != "",
|
|
"stashdbConfigured": keys.StashDBAPIKey != "",
|
|
"stashdbEndpoint": keys.StashDBEndpoint,
|
|
"tpdb_api_key": keys.TPDBAPIKey, // local-only UI; if you prefer, mask these
|
|
"ae_api_key": keys.AEAPIKey,
|
|
"stashdb_api_key": keys.StashDBAPIKey,
|
|
"stashdb_endpoint": keys.StashDBEndpoint, // duplicate for UI convenience
|
|
}
|
|
json.NewEncoder(w).Encode(APIResponse{
|
|
Success: true,
|
|
Message: "OK",
|
|
Data: resp,
|
|
})
|
|
case http.MethodPost:
|
|
var payload apiKeysPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Invalid JSON"})
|
|
return
|
|
}
|
|
|
|
keys := config.APIKeys{
|
|
TPDBAPIKey: payload.TPDBAPIKey,
|
|
AEAPIKey: payload.AEAPIKey,
|
|
StashDBAPIKey: payload.StashDBAPIKey,
|
|
StashDBEndpoint: payload.StashDBEndpoint,
|
|
}
|
|
|
|
if err := config.SaveAPIKeys(keys); err != nil {
|
|
json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Failed to save keys: %v", err)})
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(APIResponse{
|
|
Success: true,
|
|
Message: "API keys saved",
|
|
})
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|