nsfwapp/backend/pending_autostart.go
2026-04-08 13:04:01 +02:00

349 lines
8.4 KiB
Go

// backend\pending_autostart.go
package main
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
)
type PendingAutoStartItem struct {
ModelKey string `json:"modelKey"`
URL string `json:"url"`
Mode string `json:"mode,omitempty"` // wait_public | probe_retry
NextProbeAtMs int64 `json:"nextProbeAtMs,omitempty"` // nur bei probe_retry
CurrentShow string `json:"currentShow,omitempty"` // public/private/hidden/away/offline/unknown
ImageURL string `json:"imageUrl,omitempty"`
Source string `json:"source,omitempty"` // manual | watched
}
type PendingAutoStartResponse struct {
Items []PendingAutoStartItem `json:"items"`
}
type pendingAutoStartFile struct {
Items []PendingAutoStartItem `json:"items"`
}
var pendingAutoStartMu sync.Mutex
const pendingAutoStartGlobalUserKey = "__global__"
func normalizePendingSourceServer(v string) string {
switch strings.TrimSpace(strings.ToLower(v)) {
case "watched":
return "watched"
default:
return "manual"
}
}
func normalizePendingModeServer(v string) string {
if strings.TrimSpace(strings.ToLower(v)) == "probe_retry" {
return "probe_retry"
}
return "wait_public"
}
func normalizePendingShowServer(v string) string {
switch strings.TrimSpace(strings.ToLower(v)) {
case "public":
return "public"
case "private":
return "private"
case "hidden":
return "hidden"
case "away":
return "away"
case "offline":
return "offline"
default:
return "unknown"
}
}
func safeUserKeyForFile(v string) string {
re := regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
return re.ReplaceAllString(v, "_")
}
func pendingAutoStartFilePath(userKey string) string {
return filepath.Join("data", "pending-autostart", safeUserKeyForFile(userKey)+".json")
}
func loadPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
path := pendingAutoStartFilePath(userKey)
raw, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return []PendingAutoStartItem{}, nil
}
return nil, err
}
var f pendingAutoStartFile
if err := json.Unmarshal(raw, &f); err != nil {
// kaputte Datei => lieber leer zurückgeben statt alles zu crashen
return []PendingAutoStartItem{}, nil
}
if f.Items == nil {
f.Items = []PendingAutoStartItem{}
}
// noch einmal sauber normalisieren
out := make([]PendingAutoStartItem, 0, len(f.Items))
for _, it := range f.Items {
key := strings.ToLower(strings.TrimSpace(it.ModelKey))
u := strings.TrimSpace(it.URL)
if key == "" || u == "" {
continue
}
out = append(out, PendingAutoStartItem{
ModelKey: key,
URL: u,
Mode: normalizePendingModeServer(it.Mode),
NextProbeAtMs: it.NextProbeAtMs,
CurrentShow: normalizePendingShowServer(it.CurrentShow),
ImageURL: strings.TrimSpace(it.ImageURL),
Source: normalizePendingSourceServer(it.Source),
})
}
return out, nil
}
func loadMergedPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
globalItems, err := loadPendingAutoStartItems(pendingAutoStartGlobalUserKey)
if err != nil {
return nil, err
}
userItems, err := loadPendingAutoStartItems(userKey)
if err != nil {
return nil, err
}
byKey := make(map[string]PendingAutoStartItem)
for _, it := range globalItems {
k := strings.ToLower(strings.TrimSpace(it.ModelKey))
if k == "" {
continue
}
byKey[k] = it
}
// user-spezifisch überschreibt global
for _, it := range userItems {
k := strings.ToLower(strings.TrimSpace(it.ModelKey))
if k == "" {
continue
}
byKey[k] = it
}
out := make([]PendingAutoStartItem, 0, len(byKey))
for _, it := range byKey {
out = append(out, it)
}
return out, nil
}
func saveWatchedPendingAutoStartItems(items []PendingAutoStartItem) error {
next := make([]PendingAutoStartItem, 0, len(items))
for _, it := range items {
it.ModelKey = strings.ToLower(strings.TrimSpace(it.ModelKey))
it.URL = strings.TrimSpace(it.URL)
it.Mode = normalizePendingModeServer(it.Mode)
it.CurrentShow = normalizePendingShowServer(it.CurrentShow)
it.ImageURL = strings.TrimSpace(it.ImageURL)
it.Source = "watched"
if it.ModelKey == "" || it.URL == "" {
continue
}
next = append(next, it)
}
return savePendingAutoStartItems(pendingAutoStartGlobalUserKey, next)
}
func removeWatchedPendingAutoStartItem(modelKey string) error {
modelKey = strings.ToLower(strings.TrimSpace(modelKey))
if modelKey == "" {
return nil
}
items, err := loadPendingAutoStartItems(pendingAutoStartGlobalUserKey)
if err != nil {
return err
}
next := make([]PendingAutoStartItem, 0, len(items))
for _, it := range items {
if strings.ToLower(strings.TrimSpace(it.ModelKey)) == modelKey {
continue
}
next = append(next, it)
}
return savePendingAutoStartItems(pendingAutoStartGlobalUserKey, next)
}
func savePendingAutoStartItems(userKey string, items []PendingAutoStartItem) error {
path := pendingAutoStartFilePath(userKey)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
payload := pendingAutoStartFile{
Items: items,
}
raw, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return err
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, raw, 0o644); err != nil {
return err
}
return os.Rename(tmp, path)
}
func handlePendingAutoStart(auth *AuthManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sess, _ := auth.getSession(r)
if sess == nil || !sess.Authed || strings.TrimSpace(sess.User) == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
userKey := strings.ToLower(strings.TrimSpace(sess.User))
pendingAutoStartMu.Lock()
defer pendingAutoStartMu.Unlock()
switch r.Method {
case http.MethodGet:
items, err := loadMergedPendingAutoStartItems(userKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(PendingAutoStartResponse{
Items: items,
})
return
case http.MethodPost:
var in PendingAutoStartItem
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json body", http.StatusBadRequest)
return
}
in.ModelKey = strings.ToLower(strings.TrimSpace(in.ModelKey))
in.URL = strings.TrimSpace(in.URL)
in.Mode = normalizePendingModeServer(in.Mode)
in.CurrentShow = normalizePendingShowServer(in.CurrentShow)
in.ImageURL = strings.TrimSpace(in.ImageURL)
in.Source = normalizePendingSourceServer(in.Source)
if in.ModelKey == "" {
http.Error(w, "missing modelKey", http.StatusBadRequest)
return
}
if in.URL == "" {
http.Error(w, "missing url", http.StatusBadRequest)
return
}
u, err := url.Parse(in.URL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
http.Error(w, "invalid url", http.StatusBadRequest)
return
}
items, err := loadPendingAutoStartItems(userKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
replaced := false
for i := range items {
if strings.ToLower(strings.TrimSpace(items[i].ModelKey)) == in.ModelKey {
items[i] = in
replaced = true
break
}
}
if !replaced {
items = append(items, in)
}
if err := savePendingAutoStartItems(userKey, items); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
return
case http.MethodDelete:
modelKey := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("modelKey")))
if modelKey == "" {
http.Error(w, "missing modelKey", http.StatusBadRequest)
return
}
items, err := loadPendingAutoStartItems(userKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
next := make([]PendingAutoStartItem, 0, len(items))
for _, item := range items {
if strings.ToLower(strings.TrimSpace(item.ModelKey)) == modelKey {
continue
}
next = append(next, item)
}
if err := savePendingAutoStartItems(userKey, next); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
return
default:
w.Header().Set("Allow", "GET, POST, DELETE")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
}
}