diff --git a/src/web/backend/playlists.go b/src/web/backend/playlists.go index 8bd1e0f..bea5280 100644 --- a/src/web/backend/playlists.go +++ b/src/web/backend/playlists.go @@ -3,6 +3,7 @@ package backend import ( "bytes" "encoding/json" + "explo/src/discovery" "explo/src/models" "fmt" "image" @@ -95,6 +96,26 @@ type lbPlaylistResp struct { } `json:"playlist"` } +func fetchOnRepeatTracks(username string) ([][4]string, error) { + body, err := lbGet(fmt.Sprintf("%s/stats/user/%s/recordings?count=30&range=month", lbAPIBase, username)) + if err != nil { + return nil, fmt.Errorf("on-repeat stats fetch: %w", err) + } + var resp discovery.TopRecordings + if err := json.Unmarshal(body, &resp); err != nil { + return nil, fmt.Errorf("on-repeat stats parse: %w", err) + } + out := make([][4]string, 0, len(resp.Payload.Recordings)) + for _, rec := range resp.Payload.Recordings { + var cover string + if rec.ReleaseMbid != "" { + cover = fmt.Sprintf("https://coverartarchive.org/release/%s/front-250", rec.ReleaseMbid) + } + out = append(out, [4]string{rec.TrackName, rec.ArtistName, rec.ReleaseName, cover}) + } + return out, nil +} + func fetchMostRecentLBPlaylist(username, playlistType string) ([][4]string, time.Time, error) { var offset int var bestDate time.Time @@ -287,7 +308,13 @@ func (s *Server) handlePrefetchCovers(w http.ResponseWriter, r *http.Request) { slog.Info("prefetch: cache already exists, skipping", "playlist", pt) continue } - tracks, _, err := fetchMostRecentLBPlaylist(body.User, pt) + var tracks [][4]string + var err error + if pt == "on-repeat" { + tracks, err = fetchOnRepeatTracks(body.User) + } else { + tracks, _, err = fetchMostRecentLBPlaylist(body.User, pt) + } if err != nil { slog.Warn("prefetch: failed to fetch LB playlist", "type", pt, "err", err) continue diff --git a/src/web/backend/server.go b/src/web/backend/server.go index 1495ec1..cd09b8c 100644 --- a/src/web/backend/server.go +++ b/src/web/backend/server.go @@ -115,7 +115,10 @@ func NewServer(cfg config.ServerConfig) *Server { func (s *Server) Start() error { s.initServerLog() s.startJobs() - s.PrefetchCovers() + coversDir := filepath.Join(s.cfg.WebDataDir, "cache", "covers") + if _, err := os.Stat(coversDir); os.IsNotExist(err) { + s.PrefetchCovers() + } slog.Info("Explo web UI started", "addr", s.server.Addr) return s.server.ListenAndServe() } @@ -333,7 +336,9 @@ func parseEnvText(text string) map[string]string { } // handleGetConfig returns resolved config as JSON: { values, sources }. -// Sources are "env" when set via os.Environ (takes precedence), "file" otherwise. +// File keys are checked first because cleanenv sets them as OS env vars on startup, +// so checking os.LookupEnv first would misclassify all file keys as "env". +// Only keys present in the OS environment but absent from the file are marked "env". func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { data, err := os.ReadFile(s.cfg.WebEnvPath) var fileValues map[string]string @@ -346,12 +351,12 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { values := make(map[string]string, len(allConfigKeys)) sources := make(map[string]string, len(allConfigKeys)) for _, key := range allConfigKeys { - if v, ok := os.LookupEnv(key); ok && v != "" { - values[key] = v - sources[key] = "env" - } else if v, ok := fileValues[key]; ok { + if v, ok := fileValues[key]; ok && v != "" { values[key] = v sources[key] = "file" + } else if v, ok := os.LookupEnv(key); ok && v != "" { + values[key] = v + sources[key] = "env" } } diff --git a/src/web/frontend/src/components/Settings.jsx b/src/web/frontend/src/components/Settings.jsx index 7860f27..8e42768 100644 --- a/src/web/frontend/src/components/Settings.jsx +++ b/src/web/frontend/src/components/Settings.jsx @@ -450,6 +450,7 @@ function ConfigSection({ onWizard }) { function LogsSection() { const [logFileEntries, setLogFileEntries] = useState([]) + const panelRef = useRef(null) const loadLog = () => { fetchLogs().then(text => { @@ -459,6 +460,10 @@ function LogsSection() { useEffect(() => { loadLog() }, []) + useEffect(() => { + if (panelRef.current) panelRef.current.scrollTop = panelRef.current.scrollHeight + }, [logFileEntries]) + return (
No log output yet.
) : ( -