229 lines
4.9 KiB
Go
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)
|
|
}
|