// 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"` } type PendingAutoStartResponse struct { Items []PendingAutoStartItem `json:"items"` } type pendingAutoStartFile struct { Items []PendingAutoStartItem `json:"items"` } var pendingAutoStartMu sync.Mutex 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), }) } return out, nil } 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 := loadPendingAutoStartItems(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) 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 } } }