nsfwapp/backend/models_api.go
2026-06-26 21:25:45 +02:00

927 lines
25 KiB
Go

// backend\models_api.go
package main
import (
"database/sql"
"encoding/csv"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
modelStoreMu sync.RWMutex
modelStoreRef *ModelStore
)
func getModelStore() *ModelStore {
modelStoreMu.RLock()
defer modelStoreMu.RUnlock()
return modelStoreRef
}
func setModelStore(store *ModelStore) {
modelStoreMu.Lock()
defer modelStoreMu.Unlock()
modelStoreRef = store
setCoverModelStore(store)
setChaturbateOnlineModelStore(store)
}
// ✅ umbenannt, damit es nicht mit models.go kollidiert
func modelsWriteJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func modelsReadJSON(r *http.Request, v any) error {
if r.Body == nil {
return errors.New("missing body")
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(v)
}
type parseReq struct {
Input string `json:"input"`
}
func parseModelFromURL(raw string) (ParsedModelDTO, error) {
in := strings.TrimSpace(raw)
if in == "" {
return ParsedModelDTO{}, errors.New("Bitte eine URL eingeben.")
}
// scheme ergänzen, falls User "chaturbate.com/xyz" eingibt
if !strings.Contains(in, "://") {
in = "https://" + in
}
u, err := url.Parse(in)
if err != nil || u.Scheme == "" || u.Hostname() == "" {
return ParsedModelDTO{}, errors.New("Ungültige URL.")
}
host := strings.ToLower(u.Hostname())
host = strings.TrimPrefix(host, "www.")
// ModelKey aus Pfad/Fragment ableiten
path := strings.Trim(u.Path, "/")
segs := strings.Split(path, "/")
skip := map[string]bool{
"models": true, "model": true, "profile": true, "users": true, "user": true,
}
var key string
for _, s := range segs {
s = strings.TrimSpace(s)
if s == "" || skip[strings.ToLower(s)] {
continue
}
key = s
break
}
if key == "" && strings.TrimSpace(u.Fragment) != "" {
key = strings.Trim(strings.TrimSpace(u.Fragment), "/")
}
if key == "" {
return ParsedModelDTO{}, errors.New("Konnte keinen Modelnamen aus der URL ableiten.")
}
// URL-decode + kleines Sanitizing
if dec, err := url.PathUnescape(key); err == nil {
key = dec
}
key = strings.TrimPrefix(strings.TrimSpace(key), "@")
key = strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= 'A' && r <= 'Z':
return r
case r >= '0' && r <= '9':
return r
case r == '_' || r == '-' || r == '.':
return r
default:
return -1
}
}, key)
if key == "" {
return ParsedModelDTO{}, errors.New("Ungültiger Modelname in URL.")
}
return ParsedModelDTO{
Input: u.String(), // ✅ speicherst du als URL
IsURL: true,
Host: host,
Path: u.Path,
ModelKey: key, // ✅ kommt IMMER aus URL
}, nil
}
type importResult struct {
Processed int `json:"processed"`
Inserted int `json:"inserted"`
Updated int `json:"updated"`
Skipped int `json:"skipped"`
}
type modelsOverviewRequest struct {
Keys []string `json:"keys"`
IncludeWatched bool `json:"includeWatched"`
IncludeStats *bool `json:"includeStats,omitempty"`
IncludeModels *bool `json:"includeModels,omitempty"`
}
type modelsOverviewModel struct {
ID string `json:"id"`
Input string `json:"input"`
Host string `json:"host,omitempty"`
ModelKey string `json:"modelKey"`
Tags string `json:"tags,omitempty"`
Gender string `json:"gender,omitempty"`
Country string `json:"country,omitempty"`
Watching bool `json:"watching"`
Favorite bool `json:"favorite,omitempty"`
Liked *bool `json:"liked,omitempty"`
IsURL bool `json:"isUrl,omitempty"`
Path string `json:"path,omitempty"`
RoomStatus string `json:"roomStatus,omitempty"`
IsOnline bool `json:"isOnline,omitempty"`
ChatRoomURL string `json:"chatRoomUrl,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
LastOnlineAt string `json:"lastOnlineAt,omitempty"`
LastOfflineAt string `json:"lastOfflineAt,omitempty"`
LastRoomSyncAt string `json:"lastRoomSyncAt,omitempty"`
}
type modelsOverviewResponse struct {
TotalCount int `json:"totalCount"`
OnlineModelsCount int `json:"onlineModelsCount"`
OnlineWatchedModelsCount int `json:"onlineWatchedModelsCount"`
OnlineFavCount int `json:"onlineFavCount"`
OnlineLikedCount int `json:"onlineLikedCount"`
Models []modelsOverviewModel `json:"models"`
}
func normalizeModelKeySet(in []string) map[string]bool {
out := make(map[string]bool, len(in))
for _, s := range in {
k := strings.ToLower(strings.TrimSpace(s))
if k == "" {
continue
}
out[k] = true
}
return out
}
func overviewBool(v *bool, fallback bool) bool {
if v == nil {
return fallback
}
return *v
}
func isOnlineRoomStatus(status string, isOnline bool) bool {
s := strings.ToLower(strings.TrimSpace(status))
if s == "offline" {
return false
}
if isOnline {
return true
}
return s != ""
}
func buildModelsOverview(rawList any, req modelsOverviewRequest) (modelsOverviewResponse, error) {
var list []modelsOverviewModel
// absichtlich per JSON-Roundtrip:
// dadurch müssen wir den exakten Store-Typ hier nicht kennen
b, err := json.Marshal(rawList)
if err != nil {
return modelsOverviewResponse{}, err
}
if err := json.Unmarshal(b, &list); err != nil {
return modelsOverviewResponse{}, err
}
keySet := normalizeModelKeySet(req.Keys)
includeStats := overviewBool(req.IncludeStats, true)
includeModels := overviewBool(req.IncludeModels, true)
resp := modelsOverviewResponse{
Models: make([]modelsOverviewModel, 0, len(keySet)+32),
}
if includeStats {
resp.TotalCount = len(list)
}
for _, m := range list {
key := strings.ToLower(strings.TrimSpace(m.ModelKey))
if key == "" {
continue
}
if includeStats {
online := isOnlineRoomStatus(m.RoomStatus, m.IsOnline)
if strings.Contains(strings.ToLower(m.Host), "chaturbate") {
if online {
resp.OnlineModelsCount++
}
if m.Watching && online {
resp.OnlineWatchedModelsCount++
}
}
if m.Favorite && online {
resp.OnlineFavCount++
}
if m.Liked != nil && *m.Liked && online {
resp.OnlineLikedCount++
}
}
needModel :=
includeModels &&
(keySet[key] ||
(req.IncludeWatched && m.Watching))
if needModel {
resp.Models = append(resp.Models, m)
}
}
return resp, nil
}
func loadModelsOverview(store *ModelStore, req modelsOverviewRequest) (modelsOverviewResponse, error) {
if err := store.ensureInit(); err != nil {
return modelsOverviewResponse{}, err
}
includeStats := overviewBool(req.IncludeStats, true)
includeModels := overviewBool(req.IncludeModels, true)
var resp modelsOverviewResponse
resp.Models = make([]modelsOverviewModel, 0, len(req.Keys)+32)
if includeStats {
onlineExpr := `(lower(trim(COALESCE(room_status,''))) <> 'offline' AND (COALESCE(is_online,false) = true OR trim(COALESCE(room_status,'')) <> ''))`
var totalCount, onlineCount, onlineWatchedCount, onlineFavCount, onlineLikedCount int64
err := store.db.QueryRow(`
SELECT
COUNT(*),
COALESCE(SUM(CASE WHEN lower(COALESCE(host,'')) LIKE '%chaturbate%' AND `+onlineExpr+` THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN lower(COALESCE(host,'')) LIKE '%chaturbate%' AND COALESCE(watching,false) = true AND `+onlineExpr+` THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN COALESCE(favorite,false) = true AND `+onlineExpr+` THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN COALESCE(liked,false) = true AND `+onlineExpr+` THEN 1 ELSE 0 END), 0)
FROM models;
`).Scan(&totalCount, &onlineCount, &onlineWatchedCount, &onlineFavCount, &onlineLikedCount)
if err != nil {
return modelsOverviewResponse{}, err
}
resp.TotalCount = int(totalCount)
resp.OnlineModelsCount = int(onlineCount)
resp.OnlineWatchedModelsCount = int(onlineWatchedCount)
resp.OnlineFavCount = int(onlineFavCount)
resp.OnlineLikedCount = int(onlineLikedCount)
}
if !includeModels {
return resp, nil
}
keySet := normalizeModelKeySet(req.Keys)
keys := make([]string, 0, len(keySet))
for key := range keySet {
keys = append(keys, key)
}
sort.Strings(keys)
whereParts := make([]string, 0, 2)
args := make([]any, 0, len(keys))
if req.IncludeWatched {
whereParts = append(whereParts, "COALESCE(watching,false) = true")
}
if len(keys) > 0 {
placeholders := make([]string, 0, len(keys))
for _, key := range keys {
args = append(args, key)
placeholders = append(placeholders, "$"+strconv.Itoa(len(args)))
}
whereParts = append(whereParts, "lower(trim(model_key)) IN ("+strings.Join(placeholders, ",")+")")
}
if len(whereParts) == 0 {
return resp, nil
}
rows, err := store.db.Query(`
SELECT
id,
COALESCE(input,''),
COALESCE(host,''),
COALESCE(model_key,''),
COALESCE(tags,''),
COALESCE(cb_online_json,''),
COALESCE(watching,false),
COALESCE(favorite,false),
liked,
COALESCE(is_url,false),
COALESCE(path,''),
COALESCE(room_status,''),
COALESCE(is_online,false),
COALESCE(chat_room_url,''),
COALESCE(image_url,''),
last_online_at,
last_offline_at,
last_room_sync_at
FROM models
WHERE `+strings.Join(whereParts, " OR ")+`
ORDER BY updated_at DESC;
`, args...)
if err != nil {
return modelsOverviewResponse{}, err
}
defer rows.Close()
for rows.Next() {
var (
m modelsOverviewModel
cbOnlineJSON string
liked sql.NullBool
lastOnlineAt dbNullTime
lastOfflineAt dbNullTime
lastRoomSyncAt dbNullTime
)
if err := rows.Scan(
&m.ID,
&m.Input,
&m.Host,
&m.ModelKey,
&m.Tags,
&cbOnlineJSON,
&m.Watching,
&m.Favorite,
&liked,
&m.IsURL,
&m.Path,
&m.RoomStatus,
&m.IsOnline,
&m.ChatRoomURL,
&m.ImageURL,
&lastOnlineAt,
&lastOfflineAt,
&lastRoomSyncAt,
); err != nil {
return modelsOverviewResponse{}, err
}
if strings.TrimSpace(cbOnlineJSON) != "" {
var snap ChaturbateOnlineSnapshot
if err := json.Unmarshal([]byte(cbOnlineJSON), &snap); err == nil {
m.Gender = strings.TrimSpace(snap.Gender)
m.Country = strings.TrimSpace(snap.Country)
}
}
m.Liked = ptrLikedFromNullBool(liked)
m.LastOnlineAt = fmtNullTime(lastOnlineAt.NullTime)
m.LastOfflineAt = fmtNullTime(lastOfflineAt.NullTime)
m.LastRoomSyncAt = fmtNullTime(lastRoomSyncAt.NullTime)
resp.Models = append(resp.Models, m)
}
if err := rows.Err(); err != nil {
return modelsOverviewResponse{}, err
}
return resp, nil
}
func importModelsCSV(store *ModelStore, r io.Reader, kind string) (importResult, error) {
cr := csv.NewReader(r)
cr.Comma = ';'
cr.FieldsPerRecord = -1
cr.TrimLeadingSpace = true
header, err := cr.Read()
if err != nil {
return importResult{}, errors.New("CSV: header fehlt")
}
idx := map[string]int{}
for i, h := range header {
idx[strings.ToLower(strings.TrimSpace(h))] = i
}
need := []string{"url", "last_stream", "tags"}
for _, k := range need {
if _, ok := idx[k]; !ok {
return importResult{}, errors.New("CSV: Spalte fehlt: " + k)
}
}
// ✅ watch ODER watched akzeptieren
if _, ok := idx["watch"]; !ok {
if _, ok2 := idx["watched"]; !ok2 {
return importResult{}, errors.New("CSV: Spalte fehlt: watch oder watched")
}
}
seen := map[string]bool{}
out := importResult{}
for {
rec, err := cr.Read()
if err == io.EOF {
break
}
if err != nil {
return out, errors.New("CSV: ungültige Zeile")
}
get := func(key string) string {
i, ok := idx[key]
if !ok || i < 0 || i >= len(rec) {
return ""
}
return strings.TrimSpace(rec[i])
}
urlRaw := get("url")
if urlRaw == "" {
out.Skipped++
continue
}
dto, err := parseModelFromURL(urlRaw)
if err != nil {
out.Skipped++
continue
}
tags := get("tags")
lastStream := get("last_stream")
watchStr := get("watch")
if watchStr == "" {
watchStr = get("watched")
}
watch := false
if watchStr != "" {
if n, err := strconv.Atoi(watchStr); err == nil {
watch = n != 0
} else {
// "true"/"false" fallback
watch = strings.EqualFold(watchStr, "true") || strings.EqualFold(watchStr, "yes")
}
}
// dedupe innerhalb der Datei (host:modelKey)
key := strings.ToLower(dto.Host) + ":" + strings.ToLower(dto.ModelKey)
if seen[key] {
continue
}
seen[key] = true
_, inserted, err := store.UpsertFromImport(dto, tags, lastStream, watch, kind)
if err != nil {
out.Skipped++
continue
}
out.Processed++
if inserted {
out.Inserted++
} else {
out.Updated++
}
}
return out, nil
}
func RegisterModelAPI(mux *http.ServeMux, store *ModelStore) {
setModelStore(store)
mux.HandleFunc("/api/models/parse", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req parseReq
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
dto, err := parseModelFromURL(req.Input)
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, dto)
})
mux.HandleFunc("/api/models/meta", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
meta := store.Meta()
modelsWriteJSON(w, http.StatusOK, meta)
})
mux.HandleFunc("/api/models/watched", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
host := strings.TrimSpace(r.URL.Query().Get("host"))
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
modelsWriteJSON(w, http.StatusOK, store.ListWatchedLite(host))
})
mux.HandleFunc("/api/models/overview", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req modelsOverviewRequest
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
resp, err := loadModelsOverview(store, req)
if err != nil {
modelsWriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, resp)
})
mux.HandleFunc("/api/models/list", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
modelsWriteJSON(w, http.StatusOK, map[string]any{
"items": store.ListForModelTab(),
})
})
mux.HandleFunc("/api/models", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
// ✅ Wenn du List() als ([]T, error) hast -> Fehler sichtbar machen:
// Falls List() aktuell nur []T zurückgibt, siehe Schritt 2 unten.
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
list := store.List()
modelsWriteJSON(w, http.StatusOK, list)
})
// ✅ Profilbild-Blob aus DB ausliefern
mux.HandleFunc("/api/models/image", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
id := strings.TrimSpace(r.URL.Query().Get("id"))
if id == "" {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "id fehlt"})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
mime, data, ok, err := store.GetProfileImageByID(id)
if err != nil {
modelsWriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !ok || len(data) == 0 {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", mime)
w.Header().Set("Cache-Control", "public, max-age=86400")
_, _ = w.Write(data)
})
// ✅ Profilbild hochladen/ersetzen (Blob + URL speichern)
mux.HandleFunc("/api/models/profile-image", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart form"})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
host := strings.TrimSpace(r.FormValue("host"))
modelKey := strings.TrimSpace(r.FormValue("modelKey"))
sourceURL := strings.TrimSpace(r.FormValue("sourceUrl"))
if modelKey == "" {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "modelKey fehlt"})
return
}
f, _, err := r.FormFile("file")
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "missing file"})
return
}
defer f.Close()
data, err := io.ReadAll(io.LimitReader(f, 8<<20))
if err != nil || len(data) == 0 {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid image"})
return
}
mime := http.DetectContentType(data)
if !strings.HasPrefix(mime, "image/") {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "file is not an image"})
return
}
if err := store.SetProfileImage(host, modelKey, sourceURL, mime, data, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
modelsWriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
m, err := store.EnsureByHostModelKey(host, modelKey)
if err != nil {
modelsWriteJSON(w, http.StatusOK, map[string]any{"ok": true})
return
}
modelsWriteJSON(w, http.StatusOK, m)
})
mux.HandleFunc("/api/models/upsert", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req ParsedModelDTO
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
// ✅ Server-seitig: nur URL akzeptieren (wird zusätzlich im Store geprüft)
if !req.IsURL {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "Nur URL erlaubt."})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
m, err := store.UpsertFromParsed(req)
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, m)
})
// ✅ NEU: Ensure-Endpoint (für QuickActions aus FinishedDownloads)
mux.HandleFunc("/api/models/ensure", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req struct {
ModelKey string `json:"modelKey"`
Host string `json:"host,omitempty"`
}
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
key := strings.TrimSpace(req.ModelKey)
host := strings.ToLower(strings.TrimSpace(req.Host))
host = strings.TrimPrefix(host, "www.")
if key == "" {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "modelKey fehlt"})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
m, err := store.EnsureByHostModelKey(host, key)
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, m)
})
mux.HandleFunc("/api/models/import", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
if err := r.ParseMultipartForm(32 << 20); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart form"})
return
}
kind := strings.ToLower(strings.TrimSpace(r.FormValue("kind")))
if kind != "favorite" && kind != "liked" {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": `kind must be "favorite" or "liked"`})
return
}
f, _, err := r.FormFile("file")
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "missing file"})
return
}
defer f.Close()
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
res, err := importModelsCSV(store, f, kind)
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, res)
})
mux.HandleFunc("/api/models/flags", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req ModelFlagsPatch
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
req.ID = strings.TrimSpace(req.ID)
req.ModelKey = strings.TrimSpace(req.ModelKey)
req.Host = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(req.Host)), "www.")
// Nur wenn wirklich keine ID mitkommt -> Ensure
if req.ID == "" {
if req.ModelKey == "" {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": "id oder modelKey fehlt"})
return
}
ensured, err := store.EnsureByHostModelKey(req.Host, req.ModelKey)
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
req.ID = ensured.ID
}
m, err := store.PatchFlagsFast(req)
if err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
likedOn := (m.Liked != nil && *m.Liked)
if !m.Watching && !m.Favorite && !likedOn {
_ = store.Delete(m.ID)
w.WriteHeader(http.StatusNoContent)
return
}
modelsWriteJSON(w, http.StatusOK, m)
})
mux.HandleFunc("/api/models/delete", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req struct {
ID string `json:"id"`
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if err := store.Delete(req.ID); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, map[string]any{"ok": true})
})
}