Enable TPDB UI, add settings page for API keys, and wire AE imports
This commit is contained in:
parent
e3d253ba92
commit
fa4423acfe
|
|
@ -3,3 +3,8 @@ TPDB_API_KEY=your-api-key-here
|
||||||
|
|
||||||
# Adult Empire API Key (if enabled)
|
# Adult Empire API Key (if enabled)
|
||||||
AE_API_KEY=your-api-key-here
|
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
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -20,6 +20,7 @@
|
||||||
# Cache directories
|
# Cache directories
|
||||||
/cache/
|
/cache/
|
||||||
/tmp/
|
/tmp/
|
||||||
|
/config/api_keys.json
|
||||||
|
|
||||||
# Node modules (Bootstrap)
|
# Node modules (Bootstrap)
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ go run ./cmd/goondex performer-search "test"
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
- `source scripts/env.sh` - Pin Go caches inside the repo (recommended before building)
|
- `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`)
|
- `scripts/build.sh` - Build the CLI (`bin/goondex`)
|
||||||
- `ADDR=localhost:8788 scripts/run.sh` - Build (if needed) and start the web UI
|
- `ADDR=localhost:8788 scripts/run.sh` - Build (if needed) and start the web UI
|
||||||
- `scripts/test.sh` - Run `go test ./cmd/... ./internal/...`
|
- `scripts/test.sh` - Run `go test ./cmd/... ./internal/...`
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.leaktechnologies.dev/stu/Goondex/internal/db"
|
"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/model"
|
||||||
"git.leaktechnologies.dev/stu/Goondex/internal/scraper/adultemp"
|
"git.leaktechnologies.dev/stu/Goondex/internal/scraper/adultemp"
|
||||||
"git.leaktechnologies.dev/stu/Goondex/internal/scraper/merger"
|
"git.leaktechnologies.dev/stu/Goondex/internal/scraper/merger"
|
||||||
|
|
@ -19,7 +20,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const tpdbAPIKeyEnvVar = "TPDB_API_KEY"
|
const tpdbAPIKeyEnvVar = "TPDB_API_KEY"
|
||||||
const tpdbEnabled = false
|
const tpdbEnabled = true
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dbPath string
|
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 <url>') to import data instead.")
|
return "", fmt.Errorf("TPDB integration is disabled. Use Adult Empire commands (e.g., 'goondex adultemp search-performer' and 'goondex adultemp scrape-performer <url>') to import data instead.")
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := os.Getenv(tpdbAPIKeyEnvVar)
|
apiKey := config.GetAPIKeys().TPDBAPIKey
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
return "", fmt.Errorf("%s environment variable is not set.\n%s", tpdbAPIKeyEnvVar, apiKeySetupInstructions())
|
return "", fmt.Errorf("%s environment variable is not set.\n%s", tpdbAPIKeyEnvVar, apiKeySetupInstructions())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
internal/config/keys.go
Normal file
96
internal/config/keys.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -7,14 +7,17 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.leaktechnologies.dev/stu/Goondex/internal/db"
|
"git.leaktechnologies.dev/stu/Goondex/internal/db"
|
||||||
import_service "git.leaktechnologies.dev/stu/Goondex/internal/import"
|
import_service "git.leaktechnologies.dev/stu/Goondex/internal/import"
|
||||||
"git.leaktechnologies.dev/stu/Goondex/internal/model"
|
"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/scraper/tpdb"
|
||||||
"git.leaktechnologies.dev/stu/Goondex/internal/sync"
|
"git.leaktechnologies.dev/stu/Goondex/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
@ -78,6 +81,16 @@ func (s *Server) Start() error {
|
||||||
mux.HandleFunc("/scenes/", s.handleSceneDetail)
|
mux.HandleFunc("/scenes/", s.handleSceneDetail)
|
||||||
mux.HandleFunc("/movies", s.handleMovieList)
|
mux.HandleFunc("/movies", s.handleMovieList)
|
||||||
mux.HandleFunc("/movies/", s.handleMovieDetail)
|
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
|
// API
|
||||||
mux.HandleFunc("/api/import/performer", s.handleAPIImportPerformer)
|
mux.HandleFunc("/api/import/performer", s.handleAPIImportPerformer)
|
||||||
|
|
@ -106,6 +119,13 @@ func (s *Server) Start() error {
|
||||||
return http.ListenAndServe(s.addr, mux)
|
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
|
// PAGE HANDLERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -135,7 +155,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
"MovieCount": len(movies),
|
"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) {
|
func (s *Server) handlePerformerList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -565,7 +585,7 @@ type APIResponse struct {
|
||||||
Data interface{} `json:"data,omitempty"`
|
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."
|
const tpdbDisabledMessage = "TPDB integration is disabled. Use the Adult Empire CLI commands to import and enrich data for now."
|
||||||
|
|
||||||
func tpdbAPIKey() (string, error) {
|
func tpdbAPIKey() (string, error) {
|
||||||
|
|
@ -573,7 +593,7 @@ func tpdbAPIKey() (string, error) {
|
||||||
return "", fmt.Errorf(tpdbDisabledMessage)
|
return "", fmt.Errorf(tpdbDisabledMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := os.Getenv("TPDB_API_KEY")
|
apiKey := config.GetAPIKeys().TPDBAPIKey
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
return "", fmt.Errorf("TPDB_API_KEY not configured")
|
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
|
// BULK IMPORT ENDPOINTS + SSE PROGRESS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -1128,3 +1355,75 @@ func (s *Server) handleAPIGlobalSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
Data: results,
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -524,6 +524,22 @@ main.container {
|
||||||
color: var(--color-text-secondary);
|
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 */
|
/* Detail views */
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
|
||||||
91
internal/web/static/img/logo/GOONDEX_Titty.svg
Normal file
91
internal/web/static/img/logo/GOONDEX_Titty.svg
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="180"
|
||||||
|
height="110"
|
||||||
|
viewBox="0 0 180 110"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="GOONDEX_Titty.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2.8507295"
|
||||||
|
inkscape:cx="236.07992"
|
||||||
|
inkscape:cy="43.497638"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid1"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="1"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="5"
|
||||||
|
enabled="true"
|
||||||
|
visible="true" />
|
||||||
|
<inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="110"
|
||||||
|
id="page1"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" />
|
||||||
|
<inkscape:page
|
||||||
|
x="190"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="110"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
id="path13-4"
|
||||||
|
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#ff5fa2;fill-opacity:1;stroke-width:9.80478"
|
||||||
|
d="M 54.6505,5 C 26.0598,5 5,25.826486 5,54.299551 c 0,28.473073 21.1791,49.297109 49.7699,49.297109 14.0881,0 26.3458,-5.056266 35.1585,-13.592646 8.8417,8.53638 21.1312,13.592646 35.2194,13.592646 28.5909,0 49.6506,-20.824036 49.6506,-49.297109 C 174.7984,25.826486 153.6218,5 125.0311,5 110.9429,5 98.6826,10.056275 89.8699,18.592651 81.0283,10.056273 68.7387,5 54.6505,5 Z m 0,11.294718 c 11.3038,0 21.0508,4.346057 27.7603,11.69192 3.2744,3.584891 5.8258,7.884457 7.4811,12.742197 1.6471,-4.864244 4.1887,-9.169044 7.4542,-12.756817 6.6784,-7.337146 16.3885,-11.6773 27.685,-11.6773 21.5312,0 37.415,15.767597 37.415,38.004833 0,22.119593 -15.8844,38.002393 -37.2982,38.002393 -11.2943,0 -21.0343,-4.37015 -27.7432,-11.71873 -3.2869,-3.60007 -5.8476,-7.915358 -7.5056,-12.781188 -1.6544,4.859897 -4.2044,9.171568 -7.4786,12.769018 -6.6954,7.35651 -16.4112,11.7309 -27.6506,11.7309 -21.5314,0 -37.4152,-15.88281 -37.4152,-38.002393 0,-22.237236 15.7644,-38.004833 37.2958,-38.004833 z m -0.9383,51.341619 c -2.6247,1.49e-4 -4.7519,2.129552 -4.7493,4.754267 1e-4,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" />
|
||||||
|
<g
|
||||||
|
id="g22"
|
||||||
|
transform="translate(-1219.8521)">
|
||||||
|
<path
|
||||||
|
id="path22"
|
||||||
|
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#483737;fill-opacity:1;stroke-width:9.80478"
|
||||||
|
d="m 1464.5026,5 c -28.5907,0 -49.6505,20.826486 -49.6505,49.299551 0,28.473073 21.1791,49.297109 49.7699,49.297109 14.0881,0 26.3458,-5.056266 35.1585,-13.592646 8.8417,8.53638 21.1312,13.592646 35.2194,13.592646 28.5909,0 49.6506,-20.824036 49.6506,-49.297109 C 1584.6505,25.826486 1563.4739,5 1534.8832,5 1520.795,5 1508.5347,10.056275 1499.722,18.592651 1490.8804,10.056273 1478.5908,5 1464.5026,5 Z m 0,11.294718 c 11.3038,0 21.0508,4.346057 27.7603,11.69192 3.2744,3.584891 5.8258,7.884457 7.4811,12.742197 1.6471,-4.864244 4.1887,-9.169044 7.4542,-12.756817 6.6784,-7.337146 16.3885,-11.6773 27.685,-11.6773 21.5312,0 37.415,15.767597 37.415,38.004833 0,22.119593 -15.8844,38.002393 -37.2982,38.002393 -11.2943,0 -21.0343,-4.37015 -27.7432,-11.71873 -3.2869,-3.60007 -5.8476,-7.915358 -7.5056,-12.781188 -1.6544,4.859897 -4.2044,9.171568 -7.4786,12.769018 -6.6954,7.35651 -16.4112,11.7309 -27.6506,11.7309 -21.5314,0 -37.4152,-15.88281 -37.4152,-38.002393 0,-22.237236 15.7644,-38.004833 37.2958,-38.004833 z"
|
||||||
|
sodipodi:nodetypes="ssscssscssscssssscssss" />
|
||||||
|
<path
|
||||||
|
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" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -23,16 +23,16 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="2.1896304"
|
inkscape:zoom="2.8284271"
|
||||||
inkscape:cx="884.39583"
|
inkscape:cx="1382.9241"
|
||||||
inkscape:cy="34.252356"
|
inkscape:cy="89.095455"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="1011"
|
inkscape:window-height="1011"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="0"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="g9"
|
inkscape:current-layer="g9"
|
||||||
showgrid="false">
|
showgrid="true">
|
||||||
<inkscape:page
|
<inkscape:page
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
|
|
@ -48,7 +48,10 @@
|
||||||
height="180"
|
height="180"
|
||||||
id="page2"
|
id="page2"
|
||||||
margin="0"
|
margin="0"
|
||||||
bleed="0" />
|
bleed="0"
|
||||||
|
inkscape:export-filename="Goondex_LOGO2.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96" />
|
||||||
<inkscape:grid
|
<inkscape:grid
|
||||||
id="grid3"
|
id="grid3"
|
||||||
units="px"
|
units="px"
|
||||||
|
|
@ -62,7 +65,23 @@
|
||||||
opacity="0.14901961"
|
opacity="0.14901961"
|
||||||
empspacing="5"
|
empspacing="5"
|
||||||
enabled="true"
|
enabled="true"
|
||||||
visible="false" />
|
visible="true" />
|
||||||
|
<inkscape:page
|
||||||
|
x="1220"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="110"
|
||||||
|
id="page19"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" />
|
||||||
|
<inkscape:page
|
||||||
|
x="1410"
|
||||||
|
y="0"
|
||||||
|
width="180"
|
||||||
|
height="110"
|
||||||
|
id="page20"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" />
|
||||||
</sodipodi:namedview>
|
</sodipodi:namedview>
|
||||||
<defs
|
<defs
|
||||||
id="defs1" />
|
id="defs1" />
|
||||||
|
|
@ -139,14 +158,30 @@
|
||||||
id="path13"
|
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"
|
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 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 " />
|
||||||
|
<g
|
||||||
|
id="g20">
|
||||||
|
<path
|
||||||
|
id="path19"
|
||||||
|
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#483737;fill-opacity:1;stroke-width:7.85855"
|
||||||
|
d="m 783.33779,50.472656 c -22.91552,0 -39.79492,16.692462 -39.79492,39.513672 0,22.821222 16.97511,39.511722 39.89062,39.511722 11.29168,0 21.11635,-4.05262 28.17969,-10.89453 7.08657,6.84191 16.93685,10.89453 28.22852,10.89453 22.91553,0 39.79492,-16.6905 39.79492,-39.511722 0,-22.82121 -16.97314,-39.513672 -39.88867,-39.513672 -11.29167,0 -21.1183,4.052613 -28.18164,10.894532 -7.08658,-6.841921 -16.93685,-10.894532 -28.22852,-10.894532 z m 0,9.052735 c 9.05999,0 16.87232,3.483372 22.25,9.371093 2.62439,2.873295 4.66939,6.319404 5.99609,10.212891 1.32022,-3.898699 3.35723,-7.349004 5.97461,-10.224609 5.35266,-5.880735 13.13539,-9.359375 22.18946,-9.359375 17.25737,0 29.98828,12.637755 29.98828,30.460937 0,17.728892 -12.73147,30.458982 -29.89453,30.458982 -9.05246,0 -16.85908,-3.50268 -22.23633,-9.39258 -2.63433,-2.88547 -4.68681,-6.34417 -6.01563,-10.24414 -1.32604,3.89522 -3.3699,7.35103 -5.99414,10.23438 -5.36638,5.89626 -13.15374,9.40234 -22.16211,9.40234 -17.25736,0 -29.98828,-12.73009 -29.98828,-30.458982 0,-17.823182 12.63522,-30.460937 29.89258,-30.460937 z"
|
||||||
|
sodipodi:nodetypes="ssscssscssscssssscssss" />
|
||||||
|
<path
|
||||||
|
d="m 782.58584,100.67578 c -2.1037,1.2e-4 -3.80866,1.70684 -3.80664,3.81055 9e-5,2.10223 1.70442,3.80457 3.80664,3.80469 2.10221,-1.2e-4 3.8065,-1.70246 3.80664,-3.80469 0.002,-2.10371 -1.70294,-3.81043 -3.80664,-3.81055 z m 56.46484,0 c -2.10372,1.2e-4 -3.80862,1.70684 -3.80664,3.81055 1.3e-4,2.10223 1.70438,3.80457 3.80664,3.80469 2.10222,-1.2e-4 3.80652,-1.70246 3.80664,-3.80469 0.002,-2.10371 -1.70294,-3.81043 -3.80664,-3.81055 z"
|
||||||
|
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#8a6f91;fill-opacity:1;stroke-width:7.85855"
|
||||||
|
id="path1-52" />
|
||||||
|
</g>
|
||||||
<path
|
<path
|
||||||
id="path19"
|
id="path13-4"
|
||||||
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#483737;fill-opacity:1;stroke-width:7.85855"
|
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#ff5fa2;fill-opacity:1;stroke-width:9.80478"
|
||||||
d="m 783.33779,50.472656 c -22.91552,0 -39.79492,16.692462 -39.79492,39.513672 0,22.821222 16.97511,39.511722 39.89062,39.511722 11.29168,0 21.11635,-4.05262 28.17969,-10.89453 7.08657,6.84191 16.93685,10.89453 28.22852,10.89453 22.91553,0 39.79492,-16.6905 39.79492,-39.511722 0,-22.82121 -16.97314,-39.513672 -39.88867,-39.513672 -11.29167,0 -21.1183,4.052613 -28.18164,10.894532 -7.08658,-6.841921 -16.93685,-10.894532 -28.22852,-10.894532 z m 0,9.052735 c 9.05999,0 16.87232,3.483372 22.25,9.371093 2.62439,2.873295 4.66939,6.319404 5.99609,10.212891 1.32022,-3.898699 3.35723,-7.349004 5.97461,-10.224609 5.35266,-5.880735 13.13539,-9.359375 22.18946,-9.359375 17.25737,0 29.98828,12.637755 29.98828,30.460937 0,17.728892 -12.73147,30.458982 -29.89453,30.458982 -9.05246,0 -16.85908,-3.50268 -22.23633,-9.39258 -2.63433,-2.88547 -4.68681,-6.34417 -6.01563,-10.24414 -1.32604,3.89522 -3.3699,7.35103 -5.99414,10.23438 -5.36638,5.89626 -13.15374,9.40234 -22.16211,9.40234 -17.25736,0 -29.98828,-12.73009 -29.98828,-30.458982 0,-17.823182 12.63522,-30.460937 29.89258,-30.460937 z"
|
d="M 1274.6505,5 C 1246.0598,5 1225,25.826486 1225,54.299551 c 0,28.473073 21.1791,49.297109 49.7699,49.297109 14.0881,0 26.3458,-5.056266 35.1585,-13.592646 8.8417,8.53638 21.1312,13.592646 35.2194,13.592646 28.5909,0 49.6506,-20.824036 49.6506,-49.297109 C 1394.7984,25.826486 1373.6218,5 1345.0311,5 1330.9429,5 1318.6826,10.056275 1309.8699,18.592651 1301.0283,10.056273 1288.7387,5 1274.6505,5 Z m 0,11.294718 c 11.3038,0 21.0508,4.346057 27.7603,11.69192 3.2744,3.584891 5.8258,7.884457 7.4811,12.742197 1.6471,-4.864244 4.1887,-9.169044 7.4542,-12.756817 6.6784,-7.337146 16.3885,-11.6773 27.685,-11.6773 21.5312,0 37.415,15.767597 37.415,38.004833 0,22.119593 -15.8844,38.002393 -37.2982,38.002393 -11.2943,0 -21.0343,-4.37015 -27.7432,-11.71873 -3.2869,-3.60007 -5.8476,-7.915358 -7.5056,-12.781188 -1.6544,4.859897 -4.2044,9.171568 -7.4786,12.769018 -6.6954,7.35651 -16.4112,11.7309 -27.6506,11.7309 -21.5314,0 -37.4152,-15.88281 -37.4152,-38.002393 0,-22.237236 15.7644,-38.004833 37.2958,-38.004833 z m -0.9383,51.341619 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" />
|
||||||
|
<path
|
||||||
|
id="path22"
|
||||||
|
style="font-size:86.3973px;font-family:'Gmarket Sans';-inkscape-font-specification:'Gmarket Sans, Normal';fill:#483737;fill-opacity:1;stroke-width:9.80478"
|
||||||
|
d="m 1464.5026,5 c -28.5907,0 -49.6505,20.826486 -49.6505,49.299551 0,28.473073 21.1791,49.297109 49.7699,49.297109 14.0881,0 26.3458,-5.056266 35.1585,-13.592646 8.8417,8.53638 21.1312,13.592646 35.2194,13.592646 28.5909,0 49.6506,-20.824036 49.6506,-49.297109 C 1584.6505,25.826486 1563.4739,5 1534.8832,5 1520.795,5 1508.5347,10.056275 1499.722,18.592651 1490.8804,10.056273 1478.5908,5 1464.5026,5 Z m 0,11.294718 c 11.3038,0 21.0508,4.346057 27.7603,11.69192 3.2744,3.584891 5.8258,7.884457 7.4811,12.742197 1.6471,-4.864244 4.1887,-9.169044 7.4542,-12.756817 6.6784,-7.337146 16.3885,-11.6773 27.685,-11.6773 21.5312,0 37.415,15.767597 37.415,38.004833 0,22.119593 -15.8844,38.002393 -37.2982,38.002393 -11.2943,0 -21.0343,-4.37015 -27.7432,-11.71873 -3.2869,-3.60007 -5.8476,-7.915358 -7.5056,-12.781188 -1.6544,4.859897 -4.2044,9.171568 -7.4786,12.769018 -6.6954,7.35651 -16.4112,11.7309 -27.6506,11.7309 -21.5314,0 -37.4152,-15.88281 -37.4152,-38.002393 0,-22.237236 15.7644,-38.004833 37.2958,-38.004833 z"
|
||||||
sodipodi:nodetypes="ssscssscssscssssscssss" />
|
sodipodi:nodetypes="ssscssscssscssssscssss" />
|
||||||
<path
|
<path
|
||||||
d="m 782.58584,100.67578 c -2.1037,1.2e-4 -3.80866,1.70684 -3.80664,3.81055 9e-5,2.10223 1.70442,3.80457 3.80664,3.80469 2.10221,-1.2e-4 3.8065,-1.70246 3.80664,-3.80469 0.002,-2.10371 -1.70294,-3.81043 -3.80664,-3.81055 z m 56.46484,0 c -2.10372,1.2e-4 -3.80862,1.70684 -3.80664,3.81055 1.3e-4,2.10223 1.70438,3.80457 3.80664,3.80469 2.10222,-1.2e-4 3.80652,-1.70246 3.80664,-3.80469 0.002,-2.10371 -1.70294,-3.81043 -3.80664,-3.81055 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:7.85855"
|
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-52" />
|
id="path1-8" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -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() {
|
function applyFilters() {
|
||||||
// Hook for your search/filter logic
|
// Hook for your search/filter logic
|
||||||
console.log("Applying filters…");
|
console.log("Applying filters…");
|
||||||
|
|
@ -396,7 +496,8 @@ async function importScene() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncAll() {
|
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);
|
setImportStatus('sync', 'Syncing all data from TPDB...', false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -93,16 +93,16 @@
|
||||||
<!-- HERO -->
|
<!-- HERO -->
|
||||||
<section class="hero-section">
|
<section class="hero-section">
|
||||||
<h1 class="hero-title">Welcome to Goondex</h1>
|
<h1 class="hero-title">Welcome to Goondex</h1>
|
||||||
<p class="hero-subtitle">Adult Empire-first indexer (TPDB temporarily disabled)</p>
|
<p class="hero-subtitle">TPDB bulk imports with Adult Empire enrichment</p>
|
||||||
|
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<button class="btn" onclick="scrollToAEImport()">
|
<button class="btn" onclick="bulkImportAll()">
|
||||||
Get Started (Adult Empire)
|
TPDB Bulk Import
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-secondary" onclick="showTPDBDisabled()">
|
<button class="btn-secondary" onclick="syncAll()">
|
||||||
TPDB Sync (Disabled)
|
Sync Data
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,8 +126,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-actions">
|
<div class="stat-actions">
|
||||||
<a href="/performers" class="stat-link">View all →</a>
|
<a href="/performers" class="stat-link">View all →</a>
|
||||||
<button class="btn-small" onclick="scrollToAEImport()">
|
<button class="btn-small" onclick="aeImportPerformerByName()">
|
||||||
Import via CLI
|
Quick import
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -142,10 +142,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-actions">
|
<div class="stat-actions">
|
||||||
<a href="/studios" class="stat-link">View all →</a>
|
<a href="/studios" class="stat-link">View all →</a>
|
||||||
<button class="btn-small" onclick="scrollToAEImport()">
|
|
||||||
Import via CLI
|
|
||||||
<div class="hoverEffect"><div></div></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -158,8 +154,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-actions">
|
<div class="stat-actions">
|
||||||
<a href="/scenes" class="stat-link">View all →</a>
|
<a href="/scenes" class="stat-link">View all →</a>
|
||||||
<button class="btn-small" onclick="scrollToAEImport()">
|
<button class="btn-small" onclick="aeImportSceneByName()">
|
||||||
Import via CLI
|
Quick import
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -178,39 +174,71 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- IMPORT SECTION -->
|
<!-- TPDB IMPORT/SYNC -->
|
||||||
<section class="import-section">
|
<section class="import-section">
|
||||||
<h3 id="ae-import">Import from Adult Empire (CLI)</h3>
|
<h3 id="ae-import">TPDB Import & Sync</h3>
|
||||||
<p class="help-text">
|
<p class="help-text">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="import-buttons">
|
<div class="import-buttons">
|
||||||
<button class="btn-secondary" onclick="copyToClipboard('./goondex adultemp search-performer \"Name\"')">
|
<button class="btn" onclick="bulkImportAll()">
|
||||||
Copy: Search Performer
|
Import Everything (TPDB)
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-secondary" onclick="copyToClipboard('./goondex adultemp scrape-performer <url>')">
|
<button class="btn-secondary" onclick="bulkImportPerformers()">
|
||||||
Copy: Import Performer
|
Import All Performers
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-secondary" onclick="copyToClipboard('./goondex adultemp search-scene \"Title\"')">
|
<button class="btn-secondary" onclick="bulkImportStudios()">
|
||||||
Copy: Search Scene
|
Import All Studios
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-secondary" onclick="copyToClipboard('./goondex adultemp scrape-scene <url>')">
|
<button class="btn-secondary" onclick="bulkImportScenes()">
|
||||||
Copy: Import Scene
|
Import All Scenes
|
||||||
|
<div class="hoverEffect"><div></div></div>
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" onclick="syncAll()">
|
||||||
|
Sync All
|
||||||
|
<div class="hoverEffect"><div></div></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="import-all-import-status" class="status-banner" style="margin-top: 0.75rem;"></div>
|
||||||
|
<div id="sync-import-status" class="status-banner" style="margin-top: 0.75rem;"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- AE IMPORT SECTION -->
|
||||||
|
<section class="import-section">
|
||||||
|
<h3>Adult Empire Imports</h3>
|
||||||
|
<p class="help-text">
|
||||||
|
Import directly from Adult Empire via the UI with built-in progress feedback.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="import-buttons">
|
||||||
|
<button class="btn-secondary" onclick="aeImportPerformerByName()">
|
||||||
|
Import Performer by Name
|
||||||
|
<div class="hoverEffect"><div></div></div>
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" onclick="aeImportPerformerByURL()">
|
||||||
|
Import Performer by URL
|
||||||
|
<div class="hoverEffect"><div></div></div>
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" onclick="aeImportSceneByName()">
|
||||||
|
Import Scene by Title
|
||||||
|
<div class="hoverEffect"><div></div></div>
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" onclick="aeImportSceneByURL()">
|
||||||
|
Import Scene by URL
|
||||||
<div class="hoverEffect"><div></div></div>
|
<div class="hoverEffect"><div></div></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="help-text">
|
<p class="help-text">
|
||||||
Movies: scraper not finished yet. Use scene/performer imports for now.
|
Movies: scraper not finished yet. Use performer/scene imports for now.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="help-text">
|
<div id="ae-status" class="status-banner"></div>
|
||||||
Bulk “Import All” buttons will stay disabled until an Adult Empire bulk flow is available.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
<div class="container nav-inner">
|
<div class="container nav-inner">
|
||||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||||
<img src="/static/img/logo/GOONDEX_logo.png" class="logo-img" alt="Goondex logo">
|
<img src="/static/img/logo/Goondex_LOGO.png" class="logo-img" alt="Goondex logo">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav"
|
||||||
|
|
@ -50,6 +50,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="/movies" class="nav-link {{if eq .ActivePage "movies"}}active{{end}}">Movies</a>
|
<a href="/movies" class="nav-link {{if eq .ActivePage "movies"}}active{{end}}">Movies</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/settings" class="nav-link {{if eq .ActivePage "settings"}}active{{end}}">Settings</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -66,17 +66,12 @@
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>No performers found.</p>
|
<p>No performers found.</p>
|
||||||
<div class="empty-import-actions">
|
<div class="empty-import-actions">
|
||||||
<p class="hint">Kick off a bulk import to seed your library.</p>
|
<p class="hint">Import performers from Adult Empire without the CLI.</p>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button type="button" class="btn" onclick="showTPDBDisabled()">Import all performers</button>
|
<button type="button" class="btn" onclick="aeImportPerformerByName()">Import performer by name</button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="showTPDBDisabled()">Import all studios</button>
|
<button type="button" class="btn btn-secondary" onclick="aeImportPerformerByURL()">Import performer by URL</button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="showTPDBDisabled()">Import all scenes</button>
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="bulkImportMovies()">Import all movies</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">
|
<div id="ae-status" class="status-banner"></div>
|
||||||
Bulk import currently targets TPDB endpoints (disabled). Adult Empire bulk import will be added soon.
|
|
||||||
For Adult Empire now, use CLI to pick targets: <code>./goondex adultemp search-performer "Name"</code> then <code>./goondex adultemp scrape-performer <url></code>.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
96
internal/web/templates/settings.html
Normal file
96
internal/web/templates/settings.html
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "html-head" .}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<p class="help-text">Manage API keys locally. Keys are stored in <code>config/api_keys.json</code> (gitignored).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gx-card" style="margin-top: 1.5rem; padding: 1.5rem;">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="form-label">TPDB API Key</label>
|
||||||
|
<input type="password" id="tpdb-key" class="input" placeholder="TPDB API key">
|
||||||
|
|
||||||
|
<label class="form-label">Adult Empire API Key</label>
|
||||||
|
<input type="password" id="ae-key" class="input" placeholder="Adult Empire API key (optional)">
|
||||||
|
|
||||||
|
<label class="form-label">StashDB API Key</label>
|
||||||
|
<input type="password" id="stashdb-key" class="input" placeholder="StashDB API key">
|
||||||
|
|
||||||
|
<label class="form-label">StashDB Endpoint</label>
|
||||||
|
<input type="text" id="stashdb-endpoint" class="input" placeholder="https://stashdb.org/graphql">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons" style="margin-top: 1rem;">
|
||||||
|
<button class="btn" onclick="saveApiKeys()">Save Keys<div class="hoverEffect"><div></div></div></button>
|
||||||
|
<button class="btn-secondary" onclick="loadApiKeys()">Reload<div class="hoverEffect"><div></div></div></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings-status" class="status-banner" style="margin-top: 1rem;"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{template "html-scripts" .}}
|
||||||
|
<script>
|
||||||
|
async function loadApiKeys() {
|
||||||
|
setSettingsStatus('Loading...');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/settings/api-keys');
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const d = result.data;
|
||||||
|
document.getElementById('tpdb-key').value = d.tpdb_api_key || '';
|
||||||
|
document.getElementById('ae-key').value = d.ae_api_key || '';
|
||||||
|
document.getElementById('stashdb-key').value = d.stashdb_api_key || '';
|
||||||
|
document.getElementById('stashdb-endpoint').value = d.stashdb_endpoint || 'https://stashdb.org/graphql';
|
||||||
|
setSettingsStatus('Loaded');
|
||||||
|
} else {
|
||||||
|
setSettingsStatus(result.message || 'Failed to load', true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setSettingsStatus('Error: ' + err.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveApiKeys() {
|
||||||
|
const payload = {
|
||||||
|
tpdb_api_key: document.getElementById('tpdb-key').value,
|
||||||
|
ae_api_key: document.getElementById('ae-key').value,
|
||||||
|
stashdb_api_key: document.getElementById('stashdb-key').value,
|
||||||
|
stashdb_endpoint: document.getElementById('stashdb-endpoint').value,
|
||||||
|
};
|
||||||
|
setSettingsStatus('Saving...');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/settings/api-keys', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
setSettingsStatus('Saved. Restart not required; new keys are active.');
|
||||||
|
} else {
|
||||||
|
setSettingsStatus(result.message || 'Failed to save', true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setSettingsStatus('Error: ' + err.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSettingsStatus(msg, isError) {
|
||||||
|
const el = document.getElementById('settings-status');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.classList.toggle('error', !!isError);
|
||||||
|
el.style.display = msg ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadApiKeys);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
scripts/load-env.sh
Executable file
22
scripts/load-env.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Usage: source scripts/load-env.sh
|
||||||
|
# Loads API keys from .env.local (or .env if you prefer) without hardcoding them in scripts.
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||||
|
echo "Source this script: source scripts/load-env.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
if [[ -f "$ROOT/.env.local" ]]; then
|
||||||
|
set -a
|
||||||
|
source "$ROOT/.env.local"
|
||||||
|
set +a
|
||||||
|
elif [[ -f "$ROOT/.env" ]]; then
|
||||||
|
set -a
|
||||||
|
source "$ROOT/.env"
|
||||||
|
set +a
|
||||||
|
else
|
||||||
|
echo "No .env.local or .env found at $ROOT. Create one with your API keys." >&2
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue
Block a user