nsfwapp/backend/models.go
2025-12-19 17:52:14 +01:00

229 lines
4.9 KiB
Go

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))
if m := reModel.FindStringSubmatch(base); len(m) == 2 && strings.TrimSpace(m[1]) != "" {
return m[1]
}
// fallback: bis zum letzten "_" (wie bisher)
if i := strings.LastIndex(base, "_"); i > 0 {
return base[:i]
}
if base == "" {
return "—"
}
return 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)
}