Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ type Flags struct {
ExcludeLocal bool
Persist bool
PersistSet bool
SearchMBID string
RefreshOnly bool
SearchMBID string
RefreshOnly bool
CleanDownloads bool
}

type ServerConfig struct {
Expand Down
3 changes: 3 additions & 0 deletions src/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func (cfg *Config) GetFlags() error {
var showVersion bool
var searchMBID string
var refreshOnly bool
var cleanDownloads bool
// Long flags
flag.StringVarP(&configPath, "config", "c", ".env", "Path of the configuration file")
flag.StringVarP(&playlist, "playlist", "p", "weekly-exploration", "Playlist where to get tracks. Supported: weekly-exploration, weekly-jams, daily-jams, on-repeat")
Expand All @@ -33,6 +34,7 @@ func (cfg *Config) GetFlags() error {
flag.BoolVarP(&showVersion, "version", "v", false, "Print version and exit")
flag.StringVar(&searchMBID, "search-mbid", "", "Test Plex search for a single recording MBID (resolves via ListenBrainz, then searches your library)")
flag.BoolVar(&refreshOnly, "refresh-only", false, "Trigger alibrary rescan and exit; skips discovery and downloads")
flag.BoolVar(&cleanDownloads, "clean-downloads", false, "Delete previously downloaded tracks before downloading new ones (requires USE_SUBDIRECTORY)")

flag.Parse()

Expand Down Expand Up @@ -64,6 +66,7 @@ func (cfg *Config) GetFlags() error {
cfg.Flags.Persist = persist
cfg.Flags.SearchMBID = searchMBID
cfg.Flags.RefreshOnly = refreshOnly
cfg.Flags.CleanDownloads = cleanDownloads

// for deprecation purposes (can be removed at a later date)
cfg.Flags.PersistSet = persistSet
Expand Down
6 changes: 3 additions & 3 deletions src/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ func main() {
if err != nil {
slog.Warn(err.Error(), "notify", true)
}
if cfg.DownloadCfg.UseSubDir {
downloader.DeleteSongs()
}
}
if cfg.Flags.CleanDownloads && cfg.DownloadCfg.UseSubDir {
downloader.DeleteSongs()
}
if cfg.Flags.DownloadMode != "force" {
if err := client.CheckTracks(tracks); err != nil { // Check if tracks exist on system before downloading
Expand Down
101 changes: 101 additions & 0 deletions src/web/backend/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ func (s *Server) registerRoutes() {
s.mux.Handle("/api/ui/config/schedules", s.authStore.RequireAuth(http.HandlerFunc(s.handleSaveSchedule)))
s.mux.Handle("/api/ui/config/path-template", s.authStore.RequireAuth(http.HandlerFunc(s.handleSavePathTemplate)))
s.mux.Handle("/api/ui/config/enrich-metadata", s.authStore.RequireAuth(http.HandlerFunc(s.handleSaveEnrichMetadata)))
s.mux.Handle("/api/ui/config/persist", s.authStore.RequireAuth(http.HandlerFunc(s.handleSavePersist)))
s.mux.Handle("/api/ui/config/clean-downloads", s.authStore.RequireAuth(http.HandlerFunc(s.handleSaveCleanDownloads)))

// Path template presets: GET list, POST add; DELETE per name under prefix
s.mux.HandleFunc("/api/ui/path-templates", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -555,6 +557,20 @@ func (s *Server) handleSaveSchedule(w http.ResponseWriter, r *http.Request) {
return
}

// Carry over --persist=false / --clean-downloads if globally set
data, _ := os.ReadFile(s.cfg.WebEnvPath)
for k, v := range parseEnvText(string(data)) {
if strings.HasSuffix(k, "_FLAGS") && v != "" {
if strings.Contains(v, "--persist=false") {
defaultFlags = addFlag(defaultFlags, "--persist=false")
}
if strings.Contains(v, "--clean-downloads") {
defaultFlags = addFlag(defaultFlags, "--clean-downloads")
}
break
}
}

updates := map[string]string{}
if !body.Enabled {
// Toggle off — truly disable, regardless of day value carried over from state
Expand Down Expand Up @@ -627,6 +643,91 @@ func (s *Server) handleSaveEnrichMetadata(w http.ResponseWriter, r *http.Request
w.WriteHeader(http.StatusOK)
}

// handleSavePersist toggles persist by injecting/removing --persist=false
// from every active *_FLAGS entry, which is what start.sh feeds to the CLI.
func (s *Server) handleSavePersist(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}

if err := s.toggleFlagInEnv(!body.Enabled, "--persist=false"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Clean up the deprecated PERSIST env var if present
data, _ := os.ReadFile(s.cfg.WebEnvPath)
if _, ok := parseEnvText(string(data))["PERSIST"]; ok {
_ = updateEnvKeys(s.cfg.WebEnvPath, map[string]string{"PERSIST": ""}, web.SampleEnv)
}
w.WriteHeader(http.StatusOK)
}

// handleSaveCleanDownloads toggles --clean-downloads in every active *_FLAGS entry.
func (s *Server) handleSaveCleanDownloads(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
if err := s.toggleFlagInEnv(body.Enabled, "--clean-downloads"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

// toggleFlagInEnv adds or removes a CLI flag from every active *_FLAGS entry.
func (s *Server) toggleFlagInEnv(add bool, flag string) error {
data, _ := os.ReadFile(s.cfg.WebEnvPath)
env := parseEnvText(string(data))
updates := map[string]string{}
for k, v := range env {
if !strings.HasSuffix(k, "_FLAGS") || v == "" {
continue
}
var updated string
if add {
updated = addFlag(v, flag)
} else {
updated = removeFlag(v, flag)
}
if updated != v {
updates[k] = updated
}
}
return updateEnvKeys(s.cfg.WebEnvPath, updates, web.SampleEnv)
}

func addFlag(flags, flag string) string {
if strings.Contains(flags, flag) {
return flags
}
return strings.TrimSpace(flags + " " + flag)
}

func removeFlag(flags, flag string) string {
f := strings.ReplaceAll(flags, flag, "")
for strings.Contains(f, " ") {
f = strings.ReplaceAll(f, " ", " ")
}
return strings.TrimSpace(f)
}

// updateEnvKeys reads the env file (falling back to fallback if missing), updates the
// given key=value pairs in-place preserving comments, and writes the result back.
func updateEnvKeys(path string, updates map[string]string, fallback []byte) error {
Expand Down
51 changes: 51 additions & 0 deletions src/web/frontend/src/components/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
fetchConfig, fetchConfigRaw, saveConfig, resetConfig,
saveSchedule, startRun, stopRun, fetchRunStatus, fetchLogs,
fetchCustomPlaylists, deleteCustomPlaylist, savePathTemplate, saveEnrichMetadata,
savePersist, saveCleanDownloads,
fetchPathTemplatePresets, addPathTemplatePreset, deletePathTemplatePreset,
} from '../lib/api'
import { parseSlogLine, cronToFields, highlightEnv } from '../lib/utils'
Expand Down Expand Up @@ -223,6 +224,7 @@ function HomeSection() {
]).then(([{ values, sources }, customList]) => {
setEnvSources(sources || {})
setLbUser(values.LISTENBRAINZ_USER || '')
setNoPersist((values.WEEKLY_EXPLORATION_FLAGS || values.WEEKLY_JAMS_FLAGS || values.DAILY_JAMS_FLAGS || values.ON_REPEAT_FLAGS || '').includes('--persist=false'))
setCustomPlaylists(customList)

const s = {}
Expand Down Expand Up @@ -512,6 +514,8 @@ function DownloadPathSection() {
const [showModal, setShowModal] = useState(false)
const [openMenuIdx, setOpenMenuIdx] = useState(null)
const [enrichEnabled, setEnrichEnabled] = useState(false)
const [replacePlaylist, setReplacePlaylist] = useState(true)
const [cleanDownloads, setCleanDownloads] = useState(false)
const [templateEnabled, setTemplateEnabled] = useState(false)

useEffect(() => {
Expand All @@ -524,6 +528,9 @@ function DownloadPathSection() {
...jsonPresets,
]
setEnrichEnabled(values.ENRICH_TRACK_METADATA === 'true')
const anyFlags = values.WEEKLY_EXPLORATION_FLAGS || values.WEEKLY_JAMS_FLAGS || values.DAILY_JAMS_FLAGS || values.ON_REPEAT_FLAGS || ''
setReplacePlaylist(anyFlags.includes('--persist=false'))
setCleanDownloads(anyFlags.includes('--clean-downloads'))
const t = values.PATH_TEMPLATE || ''
if (t) {
setTemplateEnabled(true)
Expand Down Expand Up @@ -551,6 +558,18 @@ function DownloadPathSection() {
try { await saveEnrichMetadata(next) } catch { setEnrichEnabled(!next) }
}

const handleReplaceToggle = async () => {
const next = !replacePlaylist
setReplacePlaylist(next)
try { await savePersist(!next) } catch { setReplacePlaylist(!next) }
}

const handleCleanToggle = async () => {
const next = !cleanDownloads
setCleanDownloads(next)
try { await saveCleanDownloads(next) } catch { setCleanDownloads(!next) }
}

const handleTemplateToggle = async () => {
if (templateEnabled) {
setTemplateEnabled(false)
Expand Down Expand Up @@ -636,6 +655,38 @@ function DownloadPathSection() {
</button>
</div>

{/* Replace playlist toggle */}
<div className="flex items-start justify-between mt-3 mb-1 gap-4">
<div className="flex flex-col gap-0.5">
<span className="text-[13px] text-white">Update playlist in place</span>
<span className="text-[11px] text-muted">Keep a single playlist per type and refresh it with new recommendations each run. When off, a new playlist is created every time and previous ones are kept.</span>
</div>
<button
role="switch"
aria-checked={replacePlaylist}
onClick={handleReplaceToggle}
className={`relative inline-flex h-[22px] w-10 shrink-0 cursor-pointer rounded-full transition-colors duration-200 ${replacePlaylist ? 'bg-accent' : 'bg-[#383838]'}`}
>
<span className={`inline-block h-[18px] w-[18px] my-[2px] rounded-full bg-white shadow transition-transform duration-200 ${replacePlaylist ? 'translate-x-[20px]' : 'translate-x-[2px]'}`} />
</button>
</div>

{/* Clean old downloads toggle */}
<div className="flex items-start justify-between mt-3 mb-1 gap-4">
<div className="flex flex-col gap-0.5">
<span className="text-[13px] text-white">Clean old downloads</span>
<span className="text-[11px] text-muted">Remove previously downloaded tracks before each run. Only affects Explo's download folder, not your main library.</span>
</div>
<button
role="switch"
aria-checked={cleanDownloads}
onClick={handleCleanToggle}
className={`relative inline-flex h-[22px] w-10 shrink-0 cursor-pointer rounded-full transition-colors duration-200 ${cleanDownloads ? 'bg-accent' : 'bg-[#383838]'}`}
>
<span className={`inline-block h-[18px] w-[18px] my-[2px] rounded-full bg-white shadow transition-transform duration-200 ${cleanDownloads ? 'translate-x-[20px]' : 'translate-x-[2px]'}`} />
</button>
</div>

{/* Organize into folders toggle */}
<div className="flex items-start justify-between mt-3 mb-1 gap-4">
<div className="flex flex-col gap-0.5">
Expand Down
18 changes: 18 additions & 0 deletions src/web/frontend/src/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,24 @@ export async function saveEnrichMetadata(enabled) {
if (!res.ok) throw new Error(await res.text())
}

export async function savePersist(enabled) {
const res = await apiFetch('/api/ui/config/persist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
})
if (!res.ok) throw new Error(await res.text())
}

export async function saveCleanDownloads(enabled) {
const res = await apiFetch('/api/ui/config/clean-downloads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
})
if (!res.ok) throw new Error(await res.text())
}

export async function fetchBackgroundArt() {
try {
const res = await fetch('/api/ui/background-art')
Expand Down
Loading