// 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}) }) }