diff --git a/src/web/backend/custom_playlists.go b/src/web/backend/custom_playlists.go index 205f2df..f9c8a4d 100644 --- a/src/web/backend/custom_playlists.go +++ b/src/web/backend/custom_playlists.go @@ -96,12 +96,25 @@ func customPlaylistsPath(cfgDir string) string { return filepath.Join(cfgDir, "custom-playlists.json") } -// customEnvPrefix converts a custom playlist ID like "custom-a1b2c3d4" -// to an env-var prefix like "CUSTOM_A1B2C3D4". -func customEnvPrefix(id string) string { - return strings.ToUpper(strings.ReplaceAll(id, "-", "_")) +// customEnvPrefix converts a playlist name like "Today's Hits" +// to an env-var prefix like "CUSTOM_TODAYS_HITS". +// Non-alphanumeric characters are collapsed into underscores. +func customEnvPrefix(name string) string { + var b strings.Builder + prevUnderscore := true // start true so leading separators are skipped + for _, r := range strings.ToUpper(name) { + if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { + b.WriteRune(r) + prevUnderscore = false + } else if !prevUnderscore { + b.WriteRune('_') + prevUnderscore = true + } + } + return "CUSTOM_" + strings.TrimRight(b.String(), "_") } + func loadCustomPlaylists(cfgDir string) []CustomPlaylist { data, err := os.ReadFile(customPlaylistsPath(cfgDir)) if err != nil { @@ -216,7 +229,7 @@ func (s *Server) handleGetCustomPlaylists(w http.ResponseWriter, r *http.Request items := make([]respItem, 0, len(playlists)) for _, p := range playlists { count := customPlaylistTrackCount(s.cfg.WebDataDir, p.ID) - prefix := customEnvPrefix(p.ID) + prefix := customEnvPrefix(p.Name) sched := envValues[prefix+"_SCHEDULE"] flags := envValues[prefix+"_FLAGS"] items = append(items, respItem{CustomPlaylist: p, TrackCount: count, Schedule: sched, Flags: flags}) @@ -334,7 +347,7 @@ func (s *Server) handleImportCustomPlaylist(w http.ResponseWriter, r *http.Reque // a daily poll SCHEDULE — RefreshDays in the JSON gates the actual refresh interval // inside the cron task body. "Never" imports get FLAGS only so the card is usable // for manual runs while the schedule editor pre-selects "Never". - prefix := customEnvPrefix(id) + prefix := customEnvPrefix(name) envUpdates := map[string]string{ prefix + "_FLAGS": "--playlist " + id, } @@ -438,9 +451,11 @@ func (s *Server) handleDeleteCustomPlaylist(w http.ResponseWriter, r *http.Reque existing := loadCustomPlaylists(s.cfg.WebDataDir) filtered := existing[:0] found := false + var deletedName string for _, p := range existing { if p.ID == id { found = true + deletedName = p.Name } else { filtered = append(filtered, p) } @@ -460,7 +475,7 @@ func (s *Server) handleDeleteCustomPlaylist(w http.ResponseWriter, r *http.Reque _ = os.Remove(cachePath) // Remove schedule env vars from .env - prefix := customEnvPrefix(id) + prefix := customEnvPrefix(deletedName) _ = updateEnvKeys(s.cfg.WebEnvPath, map[string]string{ prefix + "_SCHEDULE": "", prefix + "_FLAGS": "", diff --git a/src/web/backend/jobs.go b/src/web/backend/jobs.go index 33c0795..01901d4 100644 --- a/src/web/backend/jobs.go +++ b/src/web/backend/jobs.go @@ -66,7 +66,7 @@ func (j *Jobs) RegisterCustomPlaylistRefresh(cfgDir, envPath string) error { for _, p := range playlists { p := p - prefix := customEnvPrefix(p.ID) + prefix := customEnvPrefix(p.Name) flags := envValues[prefix+"_FLAGS"] if flags == "" { continue // disabled diff --git a/src/web/frontend/src/components/Settings.jsx b/src/web/frontend/src/components/Settings.jsx index 61c1757..5c9d166 100644 --- a/src/web/frontend/src/components/Settings.jsx +++ b/src/web/frontend/src/components/Settings.jsx @@ -151,28 +151,25 @@ function CustomPlaylistsSection({ No custom playlists yet. Import one from ListenBrainz or Apple Music.

) : ( -
+
{customPlaylists.map((cp, i) => { if (!schedules[cp.id]) return null return ( -
- d.value === schedules[cp.id].day)?.summary ?? 'Every day' - : 'Disabled'} - tracklistOpen={openTracklist === cp.id} - onTracklistToggle={() => setOpenTracklist(v => v === cp.id ? null : cp.id)} - onDelete={(opts) => onDelete(cp.id, opts)} - /> -
+ playlist={{ value: `custom-${cp.color_index ?? i}`, name: cp.name }} + trackId={cp.id} + artworkUrl={cp.artwork_url || undefined} + {...scheduleProps(cp.id)} + index={i} + nextRunText={schedules[cp.id]?.enabled + ? SCHEDULE_DAYS.find(d => d.value === schedules[cp.id].day)?.summary ?? 'Every day' + : 'Disabled'} + tracklistOpen={openTracklist === cp.id} + onTracklistToggle={() => setOpenTracklist(v => v === cp.id ? null : cp.id)} + sourceUrl={cp.source_url || undefined} + onDelete={(opts) => onDelete(cp.id, opts)} + /> ) })}
@@ -367,7 +364,17 @@ function HomeSection() { ))}
p.value === openTracklist)} slideKey={openTracklist}> - + { + await startRun(openTracklist, 'normal', true, false) + setRunning(true) + setStatus('running…') + setLogEntries([]) + connect() + }} + />

Schedule changes take effect after restarting the container.

@@ -617,7 +624,7 @@ function DownloadPathSection() {
Auto-tag songs - Looks up track numbers, year, genre & more from MusicBrainz and writes them to downloaded files. + Looks up track numbers, year, genre & more from MusicBrainz and writes them to downloaded files. Applies to scheduled playlists only — not custom imports.
+ )} {canEdit && (