From fa4423acfea166385156028fa5712c61f47274b6 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Thu, 4 Dec 2025 12:14:21 -0500 Subject: [PATCH] Enable TPDB UI, add settings page for API keys, and wire AE imports --- .env.example | 5 + .gitignore | 1 + README.md | 1 + cmd/goondex/main.go | 5 +- internal/config/keys.go | 96 ++++++ internal/web/server.go | 307 +++++++++++++++++- internal/web/static/css/style.css | 16 + .../web/static/img/logo/GOONDEX_Titty.svg | 91 ++++++ internal/web/static/img/logo/GOONDEX_logo.png | Bin 13303 -> 0 bytes internal/web/static/img/logo/GOONDEX_logo.svg | 59 +++- internal/web/static/js/app.js | 103 +++++- internal/web/templates/dashboard.html | 84 +++-- internal/web/templates/layout.html | 5 +- internal/web/templates/performers.html | 13 +- internal/web/templates/settings.html | 96 ++++++ scripts/load-env.sh | 22 ++ 16 files changed, 847 insertions(+), 57 deletions(-) create mode 100644 internal/config/keys.go create mode 100644 internal/web/static/img/logo/GOONDEX_Titty.svg delete mode 100644 internal/web/static/img/logo/GOONDEX_logo.png create mode 100644 internal/web/templates/settings.html create mode 100755 scripts/load-env.sh diff --git a/.env.example b/.env.example index 074bee9..1127dc5 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,8 @@ TPDB_API_KEY=your-api-key-here # Adult Empire API Key (if enabled) AE_API_KEY=your-api-key-here + +# StashDB / stash-box +STASHDB_API_KEY=your-api-key-here +# Optional custom endpoint (default stashdb.org GraphQL URL) +STASHDB_ENDPOINT=https://stashdb.org/graphql diff --git a/.gitignore b/.gitignore index a805100..2530041 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ # Cache directories /cache/ /tmp/ +/config/api_keys.json # Node modules (Bootstrap) node_modules/ diff --git a/README.md b/README.md index ce96dad..669a487 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ go run ./cmd/goondex performer-search "test" ### Scripts - `source scripts/env.sh` - Pin Go caches inside the repo (recommended before building) +- `source scripts/load-env.sh` - Load API keys from `.env.local` (or `.env`) without hardcoding them - `scripts/build.sh` - Build the CLI (`bin/goondex`) - `ADDR=localhost:8788 scripts/run.sh` - Build (if needed) and start the web UI - `scripts/test.sh` - Run `go test ./cmd/... ./internal/...` diff --git a/cmd/goondex/main.go b/cmd/goondex/main.go index b428193..f9c54ac 100644 --- a/cmd/goondex/main.go +++ b/cmd/goondex/main.go @@ -9,6 +9,7 @@ import ( "time" "git.leaktechnologies.dev/stu/Goondex/internal/db" + "git.leaktechnologies.dev/stu/Goondex/internal/config" "git.leaktechnologies.dev/stu/Goondex/internal/model" "git.leaktechnologies.dev/stu/Goondex/internal/scraper/adultemp" "git.leaktechnologies.dev/stu/Goondex/internal/scraper/merger" @@ -19,7 +20,7 @@ import ( ) const tpdbAPIKeyEnvVar = "TPDB_API_KEY" -const tpdbEnabled = false +const tpdbEnabled = true var ( dbPath string @@ -440,7 +441,7 @@ func getTPDBAPIKey() (string, error) { return "", fmt.Errorf("TPDB integration is disabled. Use Adult Empire commands (e.g., 'goondex adultemp search-performer' and 'goondex adultemp scrape-performer ') to import data instead.") } - apiKey := os.Getenv(tpdbAPIKeyEnvVar) + apiKey := config.GetAPIKeys().TPDBAPIKey if apiKey == "" { return "", fmt.Errorf("%s environment variable is not set.\n%s", tpdbAPIKeyEnvVar, apiKeySetupInstructions()) } diff --git a/internal/config/keys.go b/internal/config/keys.go new file mode 100644 index 0000000..44051a6 --- /dev/null +++ b/internal/config/keys.go @@ -0,0 +1,96 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" +) + +// APIKeys holds external service credentials. +type APIKeys struct { + TPDBAPIKey string `json:"tpdb_api_key"` + AEAPIKey string `json:"ae_api_key"` + StashDBAPIKey string `json:"stashdb_api_key"` + StashDBEndpoint string `json:"stashdb_endpoint"` +} + +const apiKeysFile = "config/api_keys.json" + +var ( + keysMu sync.RWMutex + cached *APIKeys +) + +// GetAPIKeys returns the configured API keys, preferring the persisted file, falling back to environment variables. +func GetAPIKeys() APIKeys { + keysMu.RLock() + if cached != nil { + defer keysMu.RUnlock() + return *cached + } + keysMu.RUnlock() + + keys := APIKeys{} + + // Try file first. + if b, err := os.ReadFile(apiKeysFile); err == nil { + _ = json.Unmarshal(b, &keys) + } + + // Fallback to env if fields are empty. + if keys.TPDBAPIKey == "" { + keys.TPDBAPIKey = os.Getenv("TPDB_API_KEY") + } + if keys.AEAPIKey == "" { + keys.AEAPIKey = os.Getenv("AE_API_KEY") + } + if keys.StashDBAPIKey == "" { + keys.StashDBAPIKey = os.Getenv("STASHDB_API_KEY") + } + if keys.StashDBEndpoint == "" { + keys.StashDBEndpoint = os.Getenv("STASHDB_ENDPOINT") + } + if keys.StashDBEndpoint == "" { + keys.StashDBEndpoint = "https://stashdb.org/graphql" + } + + keysMu.Lock() + cached = &keys + keysMu.Unlock() + + return keys +} + +// SaveAPIKeys persists API keys to disk (config/api_keys.json) and updates cache. +func SaveAPIKeys(keys APIKeys) error { + keysMu.Lock() + defer keysMu.Unlock() + + // Normalize whitespace. + keys.TPDBAPIKey = strings.TrimSpace(keys.TPDBAPIKey) + keys.AEAPIKey = strings.TrimSpace(keys.AEAPIKey) + keys.StashDBAPIKey = strings.TrimSpace(keys.StashDBAPIKey) + keys.StashDBEndpoint = strings.TrimSpace(keys.StashDBEndpoint) + if keys.StashDBEndpoint == "" { + keys.StashDBEndpoint = "https://stashdb.org/graphql" + } + + if err := os.MkdirAll(filepath.Dir(apiKeysFile), 0o755); err != nil { + return fmt.Errorf("create config dir: %w", err) + } + + data, err := json.MarshalIndent(keys, "", " ") + if err != nil { + return fmt.Errorf("marshal keys: %w", err) + } + + if err := os.WriteFile(apiKeysFile, data, 0o600); err != nil { + return fmt.Errorf("write keys file: %w", err) + } + + cached = &keys + return nil +} diff --git a/internal/web/server.go b/internal/web/server.go index 3ca805b..cc4ec19 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -7,14 +7,17 @@ import ( "fmt" "html/template" "io/fs" + "log" "net/http" - "os" "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" ) @@ -78,6 +81,16 @@ func (s *Server) Start() error { 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) @@ -106,6 +119,13 @@ func (s *Server) Start() error { 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 // ============================================================================ @@ -135,7 +155,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { "MovieCount": len(movies), } - s.templates.ExecuteTemplate(w, "dashboard.html", data) + s.render(w, "dashboard.html", data) } func (s *Server) handlePerformerList(w http.ResponseWriter, r *http.Request) { @@ -565,7 +585,7 @@ type APIResponse struct { Data interface{} `json:"data,omitempty"` } -const tpdbEnabled = false +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) { @@ -573,7 +593,7 @@ func tpdbAPIKey() (string, error) { return "", fmt.Errorf(tpdbDisabledMessage) } - apiKey := os.Getenv("TPDB_API_KEY") + apiKey := config.GetAPIKeys().TPDBAPIKey if apiKey == "" { return "", fmt.Errorf("TPDB_API_KEY not configured") } @@ -841,6 +861,213 @@ func (s *Server) handleAPISyncStatus(w http.ResponseWriter, r *http.Request) { }) } +// ============================================================================ +// 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 // ============================================================================ @@ -1128,3 +1355,75 @@ func (s *Server) handleAPIGlobalSearch(w http.ResponseWriter, r *http.Request) { 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) + } +} diff --git a/internal/web/static/css/style.css b/internal/web/static/css/style.css index 9b25d25..9d6c2fb 100644 --- a/internal/web/static/css/style.css +++ b/internal/web/static/css/style.css @@ -524,6 +524,22 @@ main.container { color: var(--color-text-secondary); } +.status-banner { + display: none; + padding: 0.75rem 1rem; + margin-top: 1rem; + border-radius: 8px; + border: 1px solid var(--color-border); + background: var(--color-bg-elevated); + color: var(--color-text-primary); + font-size: 0.95rem; +} + +.status-banner.error { + border-color: #ff8a8a; + color: #ff8a8a; +} + /* Detail views */ .breadcrumb { margin-bottom: 1.5rem; diff --git a/internal/web/static/img/logo/GOONDEX_Titty.svg b/internal/web/static/img/logo/GOONDEX_Titty.svg new file mode 100644 index 0000000..2120d1b --- /dev/null +++ b/internal/web/static/img/logo/GOONDEX_Titty.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + diff --git a/internal/web/static/img/logo/GOONDEX_logo.png b/internal/web/static/img/logo/GOONDEX_logo.png deleted file mode 100644 index c9b1a40b961d21f8c643e2d06668f0db1f03d07a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13303 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 NE2$(=D{dV0zW{RHi?;v( diff --git a/internal/web/static/img/logo/GOONDEX_logo.svg b/internal/web/static/img/logo/GOONDEX_logo.svg index be57e65..e494e72 100644 --- a/internal/web/static/img/logo/GOONDEX_logo.svg +++ b/internal/web/static/img/logo/GOONDEX_logo.svg @@ -23,16 +23,16 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:document-units="px" - inkscape:zoom="2.1896304" - inkscape:cx="884.39583" - inkscape:cy="34.252356" + inkscape:zoom="2.8284271" + inkscape:cx="1382.9241" + inkscape:cy="89.095455" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="g9" - showgrid="false"> + showgrid="true"> + bleed="0" + inkscape:export-filename="Goondex_LOGO2.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> + visible="true" /> + + @@ -139,14 +158,30 @@ id="path13" style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';stroke-width:7.85855;fill:#ff5fa2;fill-opacity:1" d="M 173.33789 50.472656 C 150.42237 50.472656 133.54297 67.165118 133.54297 89.986328 C 133.54297 112.80755 150.51808 129.49805 173.43359 129.49805 C 184.72527 129.49805 194.54994 125.44543 201.61328 118.60352 C 208.69985 125.44543 218.55013 129.49805 229.8418 129.49805 C 252.75733 129.49805 269.63672 112.80755 269.63672 89.986328 C 269.63672 67.165118 252.66358 50.472656 229.74805 50.472656 C 218.45638 50.472656 208.62975 54.525269 201.56641 61.367188 C 194.47983 54.525267 184.62956 50.472656 173.33789 50.472656 z M 173.33789 59.525391 C 182.39788 59.525391 190.21021 63.008763 195.58789 68.896484 C 198.21228 71.769779 200.25728 75.215888 201.58398 79.109375 C 202.9042 75.210676 204.94121 71.760371 207.55859 68.884766 C 212.91125 63.004031 220.69398 59.525391 229.74805 59.525391 C 247.00542 59.525391 259.73633 72.163146 259.73633 89.986328 C 259.73633 107.71522 247.00486 120.44531 229.8418 120.44531 C 220.78934 120.44531 212.98272 116.94263 207.60547 111.05273 C 204.97114 108.16726 202.91866 104.70856 201.58984 100.80859 C 200.2638 104.70381 198.21994 108.15962 195.5957 111.04297 C 190.22932 116.93923 182.44196 120.44531 173.43359 120.44531 C 156.17623 120.44531 143.44531 107.71522 143.44531 89.986328 C 143.44531 72.163146 156.08053 59.525391 173.33789 59.525391 z M 172.58594 100.67578 C 170.48224 100.6759 168.77728 102.38262 168.7793 104.48633 C 168.77939 106.58856 170.48372 108.2909 172.58594 108.29102 C 174.68815 108.2909 176.39244 106.58856 176.39258 104.48633 C 176.39458 102.38262 174.68964 100.6759 172.58594 100.67578 z M 229.05078 100.67578 C 226.94706 100.6759 225.24216 102.38262 225.24414 104.48633 C 225.24427 106.58856 226.94852 108.2909 229.05078 108.29102 C 231.153 108.2909 232.8573 106.58856 232.85742 104.48633 C 232.85942 102.38262 231.15448 100.6759 229.05078 100.67578 z " /> + + + + + + d="m 1463.5643,67.636337 c -2.6247,1.49e-4 -4.7519,2.129552 -4.7493,4.754267 10e-5,2.62286 2.1265,4.74679 4.7493,4.74695 2.623,-1.6e-4 4.7491,-2.12409 4.7495,-4.74695 0,-2.624715 -2.1248,-4.754118 -4.7495,-4.754267 z m 70.4489,0 c -2.6247,1.49e-4 -4.7518,2.129552 -4.7495,4.754267 2e-4,2.62286 2.1265,4.74679 4.7495,4.74695 2.6228,-1.6e-4 4.7492,-2.12409 4.7493,-4.74695 0,-2.624715 -2.1246,-4.754118 -4.7493,-4.754267 z" + style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#8a6f91;fill-opacity:1;stroke-width:9.80478" + id="path1-8" /> diff --git a/internal/web/static/js/app.js b/internal/web/static/js/app.js index 4f50e48..5501ea1 100644 --- a/internal/web/static/js/app.js +++ b/internal/web/static/js/app.js @@ -275,6 +275,106 @@ function copyToClipboard(text) { }); } +// ============================================================================ +// Adult Empire UI helpers +// ============================================================================ + +function setAEStatus(msg, isError = false) { + const el = document.getElementById('ae-status'); + if (!el) return; + el.textContent = msg; + el.classList.toggle('error', !!isError); + el.style.display = msg ? 'block' : 'none'; +} + +async function aeImportPerformerByName() { + const name = prompt('Import performer by name (Adult Empire):'); + if (!name) return; + setAEStatus(`Searching Adult Empire for "${name}"...`); + try { + const res = await fetch('/api/ae/import/performer', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: name }) + }); + const result = await res.json(); + if (result.success) { + setAEStatus(result.message); + setTimeout(() => location.reload(), 1500); + } else { + setAEStatus(result.message || 'Import failed', true); + } + } catch (err) { + setAEStatus(`Error: ${err.message}`, true); + } +} + +async function aeImportPerformerByURL() { + const url = prompt('Paste Adult Empire performer URL:'); + if (!url) return; + setAEStatus('Importing performer from Adult Empire URL...'); + try { + const res = await fetch('/api/ae/import/performer-by-url', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + const result = await res.json(); + if (result.success) { + setAEStatus(result.message); + setTimeout(() => location.reload(), 1500); + } else { + setAEStatus(result.message || 'Import failed', true); + } + } catch (err) { + setAEStatus(`Error: ${err.message}`, true); + } +} + +async function aeImportSceneByName() { + const title = prompt('Import scene by title (Adult Empire):'); + if (!title) return; + setAEStatus(`Searching Adult Empire for "${title}"...`); + try { + const res = await fetch('/api/ae/import/scene', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: title }) + }); + const result = await res.json(); + if (result.success) { + setAEStatus(result.message); + setTimeout(() => location.reload(), 1500); + } else { + setAEStatus(result.message || 'Import failed', true); + } + } catch (err) { + setAEStatus(`Error: ${err.message}`, true); + } +} + +async function aeImportSceneByURL() { + const url = prompt('Paste Adult Empire scene URL:'); + if (!url) return; + setAEStatus('Importing scene from Adult Empire URL...'); + try { + const res = await fetch('/api/ae/import/scene-by-url', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + const result = await res.json(); + if (result.success) { + setAEStatus(result.message); + setTimeout(() => location.reload(), 1500); + } else { + setAEStatus(result.message || 'Import failed', true); + } + } catch (err) { + setAEStatus(`Error: ${err.message}`, true); + } +} + function applyFilters() { // Hook for your search/filter logic console.log("Applying filters…"); @@ -396,7 +496,8 @@ async function importScene() { } async function syncAll() { - const force = document.getElementById('sync-force').checked; + const forceEl = document.getElementById('sync-force'); + const force = forceEl ? forceEl.checked : false; setImportStatus('sync', 'Syncing all data from TPDB...', false); try { diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index b743155..2d2d60e 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -93,16 +93,16 @@

Welcome to Goondex

-

Adult Empire-first indexer (TPDB temporarily disabled)

+

TPDB bulk imports with Adult Empire enrichment

- -
@@ -126,8 +126,8 @@
View all → -
@@ -142,10 +142,6 @@
View all → -
@@ -158,8 +154,8 @@
View all → -
@@ -178,39 +174,71 @@
- +
-

Import from Adult Empire (CLI)

+

TPDB Import & Sync

- TPDB bulk import is temporarily disabled. Use the Adult Empire CLI to seed your library with high-quality mainstream data: + Run bulk imports from TPDB, then enrich with AE/StashDB. Keep it running to build a complete base.

- - - - + +
+ +
+
+
+ + +
+

Adult Empire Imports

+

+ Import directly from Adult Empire via the UI with built-in progress feedback. +

+ +
+ + + +

- Movies: scraper not finished yet. Use scene/performer imports for now. + Movies: scraper not finished yet. Use performer/scene imports for now.

-

- Bulk “Import All” buttons will stay disabled until an Adult Empire bulk flow is available. -

+
diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index 3ac57a9..fd44c32 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -25,7 +25,7 @@