// backend/internal/refresh.go package internal import ( "io" "log" "net/http" "os" "strings" "sync" "time" ) // Debounce refresh calls so a burst of uploads/renames/deletes only triggers one scan. var ( refreshMu sync.Mutex refreshTimer *time.Timer ) // RefreshLibrary triggers a Jellyfin library scan. // - Prefers JELLYFIN_API_KEY (admin key). Falls back to userToken if no key. // - Debounced: calls within 2s are coalesced to a single scan. func (j *Jellyfin) RefreshLibrary(userToken string) { refreshMu.Lock() defer refreshMu.Unlock() if refreshTimer != nil { // push the timer out a bit if more events arrive refreshTimer.Reset(2 * time.Second) return } refreshTimer = time.AfterFunc(2*time.Second, func() { base := strings.TrimRight(os.Getenv("JELLYFIN_URL"), "/") if base == "" { log.Printf("jellyfin refresh: JELLYFIN_URL missing; skipping") safeClearTimer() return } token := os.Getenv("JELLYFIN_API_KEY") if token == "" { token = userToken // last resort (may fail if user is not admin) } if token == "" { log.Printf("jellyfin refresh: no token; skipping") safeClearTimer() return } req, _ := http.NewRequest(http.MethodPost, base+"/Library/Refresh", nil) // Jellyfin/Emby accept API key via header or query; header is cleanest. // Either X-Emby-Token or X-MediaBrowser-Token are accepted; we use X-Emby-Token. req.Header.Set("X-Emby-Token", token) client := j.Client if client == nil { client = &http.Client{Timeout: 20 * time.Second} } resp, err := client.Do(req) if err != nil { log.Printf("jellyfin refresh: request error: %v", err) safeClearTimer() return } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() if resp.StatusCode >= 300 { log.Printf("jellyfin refresh: HTTP %s", resp.Status) } else { log.Printf("jellyfin refresh: triggered") } safeClearTimer() }) } func safeClearTimer() { refreshMu.Lock() defer refreshMu.Unlock() refreshTimer = nil }