package main import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "io" "net/http" "os" "path/filepath" "regexp" "strings" "sync" "time" ) type Model struct { ID string `json:"id"` Name string `json:"name"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` // optional Flags (kannst du später im UI nutzen) Watching bool `json:"watching"` Favorite bool `json:"favorite"` Hot bool `json:"hot"` Liked *bool `json:"liked"` // nil = keine Angabe } type modelStore struct { mu sync.Mutex path string loaded bool items []Model } var models = &modelStore{ path: filepath.Join("data", "models.json"), } func modelsHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: list, err := modelsList() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, list) case http.MethodPost: var in Model if err := readJSON(r.Body, &in); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } in.Name = strings.TrimSpace(in.Name) if in.Name == "" { http.Error(w, "name required", http.StatusBadRequest) return } out, err := modelsUpsert(in) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, out) case http.MethodDelete: // /api/models?id=... id := strings.TrimSpace(r.URL.Query().Get("id")) if id == "" { http.Error(w, "id required", http.StatusBadRequest) return } if err := modelsDelete(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) default: w.Header().Set("Allow", "GET, POST, DELETE") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } // optional: /api/models/parse?file=ella_desire_12_18_2025__14-25-30.mp4 func modelsParseHandler(w http.ResponseWriter, r *http.Request) { file := strings.TrimSpace(r.URL.Query().Get("file")) if file == "" { http.Error(w, "file required", http.StatusBadRequest) return } name := modelNameFromFilename(file) writeJSON(w, http.StatusOK, map[string]string{"model": name}) } var reModel = regexp.MustCompile(`^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}`) func modelNameFromFilename(file string) string { file = strings.ReplaceAll(file, "\\", "/") base := file[strings.LastIndex(file, "/")+1:] base = strings.TrimSuffix(base, filepath.Ext(base)) // ✅ HOT Prefix beim Parsen ignorieren (case-insensitive) if strings.HasPrefix(strings.ToUpper(base), "HOT ") { base = strings.TrimSpace(base[4:]) } if m := reModel.FindStringSubmatch(base); len(m) == 2 && strings.TrimSpace(m[1]) != "" { return strings.TrimSpace(m[1]) } // fallback: bis zum letzten "_" (wie bisher) if i := strings.LastIndex(base, "_"); i > 0 { return strings.TrimSpace(base[:i]) } if base == "" { return "—" } return strings.TrimSpace(base) } func modelsEnsureLoaded() error { models.mu.Lock() defer models.mu.Unlock() if models.loaded { return nil } models.loaded = true b, err := os.ReadFile(models.path) if err != nil { if errors.Is(err, os.ErrNotExist) { models.items = []Model{} return nil } return err } if len(b) == 0 { models.items = []Model{} return nil } return json.Unmarshal(b, &models.items) } func modelsSaveLocked() error { if err := os.MkdirAll(filepath.Dir(models.path), 0o755); err != nil { return err } b, err := json.MarshalIndent(models.items, "", " ") if err != nil { return err } return os.WriteFile(models.path, b, 0o644) } func modelsList() ([]Model, error) { if err := modelsEnsureLoaded(); err != nil { return nil, err } models.mu.Lock() defer models.mu.Unlock() out := make([]Model, len(models.items)) copy(out, models.items) return out, nil } func modelsUpsert(in Model) (Model, error) { if err := modelsEnsureLoaded(); err != nil { return Model{}, err } now := time.Now() models.mu.Lock() defer models.mu.Unlock() // update by ID if provided if strings.TrimSpace(in.ID) != "" { for i := range models.items { if models.items[i].ID == in.ID { in.CreatedAt = models.items[i].CreatedAt in.UpdatedAt = now models.items[i] = in return in, modelsSaveLocked() } } } // otherwise: create new in.ID = newID() in.CreatedAt = now in.UpdatedAt = now models.items = append(models.items, in) return in, modelsSaveLocked() } func modelsDelete(id string) error { if err := modelsEnsureLoaded(); err != nil { return err } models.mu.Lock() defer models.mu.Unlock() out := models.items[:0] for _, m := range models.items { if m.ID != id { out = append(out, m) } } models.items = out return modelsSaveLocked() } func newID() string { var b [16]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) } func readJSON(r io.Reader, v any) error { dec := json.NewDecoder(r) dec.DisallowUnknownFields() return dec.Decode(v) } func writeJSON(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) }