Files
silo-server/internal/api/handlers/admin_subtitles.go
T

195 lines
5.8 KiB
Go

// internal/api/handlers/admin_subtitles.go
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/Silo-Server/silo-server/internal/subtitles"
"github.com/Silo-Server/silo-server/internal/subtitles/opensubtitles"
"github.com/Silo-Server/silo-server/internal/subtitles/subdl"
"github.com/Silo-Server/silo-server/internal/subtitles/subsource"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// SubtitleProviderFactory creates a Provider from a config. Allows testing without real providers.
type SubtitleProviderFactory func(cfg *subtitles.ProviderConfig) (subtitles.Provider, error)
// AdminSubtitleHandler handles admin operations for subtitle provider management.
type AdminSubtitleHandler struct {
repo subtitles.Repository
manager *subtitles.Manager
pool *pgxpool.Pool
providerFactory SubtitleProviderFactory
}
// NewAdminSubtitleHandler creates a new AdminSubtitleHandler.
func NewAdminSubtitleHandler(repo subtitles.Repository) *AdminSubtitleHandler {
return &AdminSubtitleHandler{
repo: repo,
providerFactory: defaultProviderFactory,
}
}
type updateSubtitleProviderRequest struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
Username string `json:"username"`
Password string `json:"password"`
}
var builtinSubtitleProviders = []string{"opensubtitles", "subdl", "subsource"}
// HandleListProviders handles GET /api/v1/admin/subtitle-providers
func (h *AdminSubtitleHandler) HandleListProviders(w http.ResponseWriter, r *http.Request) {
configs, err := h.repo.ListProviderConfigs(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "list_error", "Failed to list providers")
return
}
configs = mergeSubtitleProviderConfigs(configs)
writeJSON(w, http.StatusOK, map[string]interface{}{"providers": configs})
}
// HandleUpdateProvider handles PUT /api/v1/admin/subtitle-providers/{provider}
func (h *AdminSubtitleHandler) HandleUpdateProvider(w http.ResponseWriter, r *http.Request) {
providerName := chi.URLParam(r, "provider")
var req updateSubtitleProviderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "Invalid request body")
return
}
cfg := &subtitles.ProviderConfig{
ProviderName: providerName,
Enabled: req.Enabled,
APIKey: req.APIKey,
Username: req.Username,
Password: req.Password,
}
if err := h.repo.UpsertProviderConfig(r.Context(), cfg); err != nil {
writeError(w, http.StatusInternalServerError, "update_error", "Failed to update provider config")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "ok"})
}
// HandleTestProvider handles POST /api/v1/admin/subtitle-providers/{provider}/test
func (h *AdminSubtitleHandler) HandleTestProvider(w http.ResponseWriter, r *http.Request) {
providerName := chi.URLParam(r, "provider")
cfg, err := h.repo.GetProviderConfig(r.Context(), providerName)
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": false, "error": fmt.Sprintf("Failed to load config: %v", err),
})
return
}
if cfg == nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": false, "error": "Provider not found",
})
return
}
provider, err := h.providerFactory(cfg)
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": false, "error": err.Error(),
})
return
}
// Do a test search with a well-known title.
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
results, err := provider.Search(ctx, subtitles.SearchRequest{
Title: "The Matrix",
Year: 1999,
Languages: []string{"en"},
})
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": false, "error": err.Error(),
})
return
}
if len(results) == 0 {
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": false, "error": "Search returned no results (credentials may be invalid)",
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
})
}
func defaultProviderFactory(cfg *subtitles.ProviderConfig) (subtitles.Provider, error) {
switch cfg.ProviderName {
case "opensubtitles":
if cfg.Username == "" || cfg.Password == "" {
return nil, fmt.Errorf("Username and password are required")
}
return opensubtitles.New(opensubtitles.Config{
Username: cfg.Username,
Password: cfg.Password,
}), nil
case "subdl":
if cfg.APIKey == "" {
return nil, fmt.Errorf("API key is required")
}
return subdl.New(subdl.Config{APIKey: cfg.APIKey}), nil
case "subsource":
if cfg.APIKey == "" {
return nil, fmt.Errorf("API key is required")
}
return subsource.New(subsource.Config{APIKey: cfg.APIKey}), nil
default:
return nil, fmt.Errorf("Unknown provider: %s", cfg.ProviderName)
}
}
func mergeSubtitleProviderConfigs(configs []subtitles.ProviderConfig) []subtitles.ProviderConfig {
if len(configs) == 0 {
merged := make([]subtitles.ProviderConfig, 0, len(builtinSubtitleProviders))
for _, providerName := range builtinSubtitleProviders {
merged = append(merged, subtitles.ProviderConfig{ProviderName: providerName})
}
return merged
}
byName := make(map[string]subtitles.ProviderConfig, len(configs))
for _, cfg := range configs {
byName[cfg.ProviderName] = cfg
}
merged := make([]subtitles.ProviderConfig, 0, len(builtinSubtitleProviders))
for _, providerName := range builtinSubtitleProviders {
if cfg, ok := byName[providerName]; ok {
merged = append(merged, cfg)
delete(byName, providerName)
continue
}
merged = append(merged, subtitles.ProviderConfig{ProviderName: providerName})
}
for _, cfg := range configs {
if _, ok := byName[cfg.ProviderName]; ok {
merged = append(merged, cfg)
}
}
return merged
}