diff --git a/src/client/client.go b/src/client/client.go
index 2deae0b8..a38c57e4 100644
--- a/src/client/client.go
+++ b/src/client/client.go
@@ -162,7 +162,6 @@ func (c *Client) systemSetup() error {
if err := c.API.GetAuth(); err != nil {
return err
}
-
}
if err := c.API.AddHeader(); err != nil {
diff --git a/src/client/plex.go b/src/client/plex.go
index 6f0880c8..39a4ce69 100644
--- a/src/client/plex.go
+++ b/src/client/plex.go
@@ -283,7 +283,7 @@ func (c *Plex) GetAuth() error { // Get user token from plex
return nil
}
func (c *Plex) GetLibrary() error {
- if c.Cfg.AdminCreds.User != "" && c.Cfg.AdminCreds.Password != "" {
+ if (c.Cfg.AdminCreds.User != "" && c.Cfg.AdminCreds.Password != "") {
adminCfg := c.Cfg
adminCfg.Creds = config.Credentials{
User: c.Cfg.AdminCreds.User,
@@ -304,6 +304,23 @@ func (c *Plex) GetLibrary() error {
}
c.LibraryID = c.AdminClient.LibraryID
+ return err
+ } else if (c.Cfg.AdminCreds.APIKey != "") {
+ adminCfg := c.Cfg
+ adminCfg.Creds = config.Credentials{
+ APIKey: c.Cfg.AdminCreds.APIKey,
+ }
+
+ c.AdminClient = NewPlex(adminCfg, c.HttpClient)
+ if err := c.AdminClient.AddHeader(); err != nil {
+ return err
+ }
+ err := c.AdminClient.getLibraryRequest()
+ if err != nil {
+ return err
+ }
+ c.LibraryID = c.AdminClient.LibraryID
+
return err
}
return c.getLibraryRequest()
diff --git a/src/config/config.go b/src/config/config.go
index f70a7ce2..7ecc1aca 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -81,6 +81,7 @@ type Credentials struct {
}
type AdminCredentials struct {
+ APIKey string `env:"ADMIN_API_KEY"`
User string `env:"ADMIN_SYSTEM_USERNAME"`
Password string `env:"ADMIN_SYSTEM_PASSWORD"`
}
diff --git a/src/web/backend/server.go b/src/web/backend/server.go
index 5b808e8c..27ef6db0 100644
--- a/src/web/backend/server.go
+++ b/src/web/backend/server.go
@@ -692,12 +692,18 @@ func (s *Server) handleWizardStep2(w http.ResponseWriter, r *http.Request) {
Password string `json:"password"`
PlaylistDir string `json:"playlist_dir"`
Sleep string `json:"sleep"`
+ AdminAPIKey string `json:"admin_api_key"`
+ AdminSystemUsername string `json:"admin_system_username"`
+ AdminSystemPassword string `json:"admin_system_password"`
+
PublicPlaylist bool `json:"public_playlist"`
}
+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
+
if body.System == "" {
http.Error(w, "system is required", http.StatusBadRequest)
return
@@ -717,6 +723,9 @@ func (s *Server) handleWizardStep2(w http.ResponseWriter, r *http.Request) {
"PLAYLIST_DIR": body.PlaylistDir,
"SLEEP": body.Sleep,
"PUBLIC_PLAYLIST": publicPlaylist,
+ "ADMIN_SYSTEM_USERNAME": body.AdminSystemUsername,
+ "ADMIN_SYSTEM_PASSWORD": body.AdminSystemPassword,
+ "ADMIN_SYSTEM_APIKEY": body.AdminAPIKey,
}
if err := updateEnvKeys(s.cfg.WebEnvPath, updates, web.SampleEnv); err != nil {
diff --git a/src/web/frontend/src/components/Wizard.jsx b/src/web/frontend/src/components/Wizard.jsx
index c5b4a7d8..eb9a7c31 100644
--- a/src/web/frontend/src/components/Wizard.jsx
+++ b/src/web/frontend/src/components/Wizard.jsx
@@ -22,9 +22,9 @@ const NextBtn = ({ onClick, disabled, saving, label = 'Next →' }) => (
disabled={disabled || saving}
className="bg-accent text-white rounded-[6px] px-6 py-2.5 text-[14px] border-none cursor-pointer hover:opacity-85 disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
>
- {saving ? 'Saving…' : label}
+ {saving ? "Saving…" : label}
-)
+);
const BackBtn = ({ onClick }) => (
(
>
← Back
-)
+);
// ── Step 1: Discovery ─────────────────────────────────────────────────────────
// Collects the ListenBrainz username, discovery mode (playlist vs API), and
@@ -43,7 +43,7 @@ const PLAYLISTS = [
{ value: 'weekly-exploration', name: 'Weekly Exploration', desc: '~50 tracks · refreshes every Tuesday' },
{ value: 'weekly-jams', name: 'Weekly Jams', desc: '~25 tracks · refreshes every Monday' },
{ value: 'daily-jams', name: 'Daily Jams', desc: '~25 tracks · refreshes daily' },
-]
+];
function Step1({ fields, setField, envSources, onNext, saving }) {
const { user, discoveryMode, checked } = fields
@@ -54,7 +54,8 @@ function Step1({ fields, setField, envSources, onNext, saving }) {
Step 1 of 3 — Discovery
- Explo uses your ListenBrainz listening history to find music recommendations.
+ Explo uses your ListenBrainz listening history to find music
+ recommendations.
@@ -63,6 +64,33 @@ function Step1({ fields, setField, envSources, onNext, saving }) {
setField('user', e.target.value)}
disabled={isLocked('LISTENBRAINZ_USER')} />
+ label="ListenBrainz username"
+ labelFor="lb-user"
+ hint={
+ <>
+ Don't have an account?{" "}
+
+ Sign up free.
+
+ >
+ }
+ >
+
setField("user", e.target.value)}
+ disabled={isLocked("LISTENBRAINZ_USER")}
+ />
@@ -74,9 +102,9 @@ function Step1({ fields, setField, envSources, onNext, saving }) {
].map(m => (
setField('discoveryMode', m.value)}
+ onClick={() => setField("discoveryMode", m.value)}
className={`text-left flex flex-col gap-[5px] px-4 py-3.5 bg-surface border rounded-[6px] cursor-pointer transition-colors
- ${discoveryMode === m.value ? 'border-accent' : 'border-ui-border hover:border-[#404040]'}`}
+ ${discoveryMode === m.value ? "border-accent" : "border-ui-border hover:border-[#404040]"}`}
>
{m.name}
{m.desc}
@@ -85,11 +113,11 @@ function Step1({ fields, setField, envSources, onNext, saving }) {
- {discoveryMode === 'playlist' && (
+ {discoveryMode === "playlist" && (
Which playlists should run on a schedule?
- {PLAYLISTS.map(p => (
+ {PLAYLISTS.map((p) => (
- )
+ );
}
// ── Step 2: Media System ──────────────────────────────────────────────────────
// Collects the media server type and its credentials. Fields shown/hidden
// conditionally based on which system is selected.
+function SegmentedControl({ value, onChange, options }) {
+ return (
+
+ {options.map((opt) => (
+ onChange(opt.value)}
+ className={`flex-1 px-4 py-2.5 border text-[13px] rounded-[6px] font-medium transition-colors
+ ${value === opt.value ? "border-accent text-accent" : "border-ui-border text-white hover:border-[#404040]"}`}
+ >
+ {opt.label}
+
+ ))}
+
+ );
+}
const SYSTEMS = [
- { value: 'jellyfin', name: 'Jellyfin' },
- { value: 'emby', name: 'Emby' },
- { value: 'plex', name: 'Plex' },
- { value: 'subsonic', name: 'Subsonic' },
- { value: 'mpd', name: 'MPD' },
-]
+ { value: "jellyfin", name: "Jellyfin" },
+ { value: "emby", name: "Emby" },
+ { value: "plex", name: "Plex" },
+ { value: "subsonic", name: "Subsonic" },
+ { value: "mpd", name: "MPD" },
+];
-const API_KEY_SYSTEMS = ['jellyfin', 'emby', 'plex']
+const API_KEY_SYSTEMS = ["emby", "plex"];
+const ADMIN_SYSTEMS = ["plex", "subsonic"];
function Step2({ fields, setField, envSources, onBack, onNext, saving }) {
- const { system, systemUrl, apiKey, libraryName, systemUsername, systemPassword,
- playlistDir, sleepMinutes, publicPlaylist } = fields
- const isLocked = key => envSources[key] === 'env'
+ const {
+ system,
+ systemUrl,
+ authMethod,
+ apiKey,
+ libraryName,
+ systemUsername,
+ systemPassword,
+ adminAuthMethod,
+ adminApiKey,
+ adminCredentials,
+ adminSystemUsername,
+ adminSystemPassword,
+ playlistDir,
+ sleepMinutes,
+ publicPlaylist,
+ } = fields;
+ const isLocked = (key) => envSources[key] === ".env";
const urlPlaceholder = () => {
const ports = { jellyfin: '8096', emby: '8096', plex: '32400', subsonic: '4533' }
@@ -135,31 +196,42 @@ function Step2({ fields, setField, envSources, onBack, onNext, saving }) {
}
const valid = () => {
- if (!system) return false
- if (system === 'mpd') return playlistDir.trim() !== ''
- if (!systemUrl) return false
- if (API_KEY_SYSTEMS.includes(system) && !apiKey) return false
- if (system === 'subsonic' && (!systemUsername || !systemPassword)) return false
- return true
- }
+ if (!system) return false;
+ if (system === "mpd") return playlistDir.trim() !== "";
+ if (!systemUrl.trim()) return false;
+ if (API_KEY_SYSTEMS.includes(system)) {
+ if (authMethod === "apikey" && !apiKey.trim()) return false;
+ if (authMethod === "password" && (!systemUsername.trim() || !systemPassword.trim())) return false;
+ }
+ if (system === "jellyfin" && ( !systemUsername.trim() || !apiKey.trim() ) ) return false;
+ if (system === "subsonic" && (!systemUsername.trim() || !systemPassword.trim())) return false;
+ if (adminCredentials) {
+ if (adminAuthMethod == "apikey" && !adminApiKey?.trim()) return false;
+ if (adminAuthMethod == "password" && (!adminSystemUsername?.trim() || !adminSystemPassword?.trim())) return false;
+ }
+
+ return true;
+ };
return (
Step 2 of 3 — Media System
- Explo will add discovered tracks to your library and create playlists automatically. It needs access to your media server to do this.
+ Explo will add discovered tracks to your library and create playlists
+ automatically. It needs access to your media server to do this.
Which media system do you use?
- {SYSTEMS.map(s => (
+ {SYSTEMS.map((s) => (
setField('system', s.value)}
- className={`text-[14px] font-medium px-3 py-[18px] text-center bg-surface border rounded-[6px] ${isLocked('EXPLO_SYSTEM') ? 'cursor-not-allowed' : 'cursor-pointer'} transition-colors
- ${system === s.value ? 'border-accent text-accent' : 'border-ui-border text-white hover:border-[#404040]'}`} disabled={isLocked('EXPLO_SYSTEM')}
+ onClick={() => setField("system", s.value)}
+ className={`text-[14px] font-medium px-3 py-[18px] text-center bg-surface border rounded-[6px] ${isLocked("EXPLO_SYSTEM") ? "cursor-not-allowed" : "cursor-pointer"} transition-colors
+ ${system === s.value ? "border-accent text-accent" : "border-ui-border text-white hover:border-[#404040]"}`}
+ disabled={isLocked("EXPLO_SYSTEM")}
>
{s.name}
@@ -167,31 +239,194 @@ function Step2({ fields, setField, envSources, onBack, onNext, saving }) {
- {system && system !== 'mpd' && (
+ {system && system !== "mpd" && (
setField('systemUrl', e.target.value)}
placeholder={urlPlaceholder()} disabled={isLocked('SYSTEM_URL')} />
)}
-
- {API_KEY_SYSTEMS.includes(system) && (
-
- setField('apiKey', e.target.value)}
- autoComplete="off" spellCheck={false} disabled={isLocked('API_KEY')} />
-
+ {system && system == "jellyfin" && (
+ <>
+
+ setField("apiKey", e.target.value)}
+ autoComplete="off"
+ spellCheck={false}
+ disabled={isLocked("API_KEY")}
+ />
+
+
+ setField("systemUsername", e.target.value)}
+ autoComplete="off"
+ spellCheck={false}
+ disabled={isLocked("SYSTEM_USERNAME")}
+ />
+
+
+ setField("libraryName", e.target.value)}
+ placeholder="e.g. Music"
+ disabled={isLocked("LIBRARY_NAME")}
+ />
+
+ >
)}
-
{API_KEY_SYSTEMS.includes(system) && (
-
- setField('libraryName', e.target.value)}
- placeholder="e.g. Music" disabled={isLocked('LIBRARY_NAME')} />
-
+ <>
+
+ setField("authMethod", v)}
+ options={[
+ {
+ value: "password",
+ label: "Username & Password",
+ },
+ {
+ value: "apikey",
+ label: "API Key",
+ },
+ ]}
+ />
+
+
+ {authMethod === "apikey" ? (
+
+ setField("apiKey", e.target.value)}
+ autoComplete="off"
+ spellCheck={false}
+ disabled={isLocked("API_KEY")}
+ />
+
+ ) : (
+ <>
+
+ setField("systemUsername", e.target.value)}
+ disabled={isLocked("SYSTEM_USERNAME")}
+ />
+
+
+
+ setField("systemPassword", e.target.value)}
+ disabled={isLocked("SYSTEM_PASSWORD")}
+ />
+
+ >
+ )}
+
+ setField("libraryName", e.target.value)}
+ placeholder="e.g. Music"
+ disabled={isLocked("LIBRARY_NAME")}
+ />
+
+
+
+ setField("adminCredentials", e.target.checked)}
+ className="mt-[2px] accent-accent"
+ />
+
+
+
+ Add Administrator Credentials
+
+
+
+ Optional. Used for administrative actions such as creating
+ playlists for other users.
+
+
+
+
+
+ setField("adminAuthMethod", v)}
+ options={[
+ {
+ value: "password",
+ label: "Username & Password",
+ },
+ {
+ value: "apikey",
+ label: "API Key",
+ },
+ ]}
+ />
+ {adminAuthMethod === "apikey" ? (
+
+ setField("adminApiKey", e.target.value)}
+ />
+
+ ) : (
+ <>
+
+
+ setField("adminSystemUsername", e.target.value)
+ }
+ />
+
+
+
+
+ setField("adminSystemPassword", e.target.value)
+ }
+ />
+
+ >
+ )}
+
+
+ >
)}
- {system === 'subsonic' && (
+ {system === "subsonic" && (
<>
- setField('systemUsername', e.target.value)}
+ setField('systemUsername', e.target.value)}
autoComplete="off" disabled={isLocked('SYSTEM_USERNAME')} />
@@ -217,11 +452,11 @@ function Step2({ fields, setField, envSources, onBack, onNext, saving }) {
)}
- {system === 'subsonic' && (
+ {system === "subsonic" && (
setField('publicPlaylist', v)}
- disabled={isLocked('PUBLIC_PLAYLIST')}
+ onChange={(v) => setField("publicPlaylist", v)}
+ disabled={isLocked("PUBLIC_PLAYLIST")}
name="Public playlists"
desc="Make playlists visible to all users on the server"
/>
@@ -233,7 +468,7 @@ function Step2({ fields, setField, envSources, onBack, onNext, saving }) {
- )
+ );
}
function Collapse({ open, children }) {
@@ -247,7 +482,7 @@ function Collapse({ open, children }) {
{children}
- )
+ );
}
// ── Step 3: Downloader ────────────────────────────────────────────────────────
@@ -255,25 +490,36 @@ function Collapse({ open, children }) {
// credentials, download directory, and file format preferences.
function Step3({ fields, setField, envSources, onBack, onFinish, saving }) {
- const { downloadDir, useSubdirectory, migrateDownloads, dlServices,
- youtubeApiKey, trackExtension, filterList, slskdUrl, slskdApiKey, extensions } = fields
- const isLocked = key => envSources[key] === 'env'
+ const {
+ downloadDir,
+ useSubdirectory,
+ migrateDownloads,
+ dlServices,
+ youtubeApiKey,
+ trackExtension,
+ filterList,
+ slskdUrl,
+ slskdApiKey,
+ extensions,
+ } = fields;
+ const isLocked = (key) => envSources[key] === "env";
const valid = () => {
- if (!Object.values(dlServices).some(Boolean)) return false
- if (dlServices.slskd && (!slskdUrl.trim() || !slskdApiKey.trim())) return false
- return true
- }
+ if (!Object.values(dlServices).some(Boolean)) return false;
+ if (dlServices.slskd && (!slskdUrl.trim() || !slskdApiKey.trim()))
+ return false;
+ return true;
+ };
return (
Step 3 of 3 — Downloader
- Explo downloads tracks using one or both services. Enable what you have access to — if both are enabled, YouTube is tried first.
+ Explo downloads tracks using one or both services. Enable what you have
+ access to — if both are enabled, YouTube is tried first.
-
{/* YouTube section */}
setField('useSubdirectory', v)}
- disabled={isLocked('USE_SUBDIRECTORY')}
+ onChange={(v) => setField("useSubdirectory", v)}
+ disabled={isLocked("USE_SUBDIRECTORY")}
name="Use playlist subfolders"
desc="Create a subfolder per playlist inside the download directory"
/>
@@ -349,12 +595,13 @@ function Step3({ fields, setField, envSources, onBack, onFinish, saving }) {
- By default, slskd saves tracks to whichever download path is configured in your slskd instance.
+ By default, slskd saves tracks to whichever download path is
+ configured in your slskd instance.
setField('migrateDownloads', v)}
- disabled={isLocked('MIGRATE_DOWNLOADS')}
+ onChange={(v) => setField("migrateDownloads", v)}
+ disabled={isLocked("MIGRATE_DOWNLOADS")}
desc="Move completed downloads to a separate directory after transfer"
/>
@@ -368,8 +615,8 @@ function Step3({ fields, setField, envSources, onBack, onFinish, saving }) {
setField('useSubdirectory', v)}
- disabled={isLocked('USE_SUBDIRECTORY')}
+ onChange={(v) => setField("useSubdirectory", v)}
+ disabled={isLocked("USE_SUBDIRECTORY")}
name="Use playlist subfolders"
desc="Create a subfolder per playlist inside the download directory"
/>
@@ -385,127 +632,181 @@ function Step3({ fields, setField, envSources, onBack, onFinish, saving }) {
- )
+ );
}
// ── Wizard ────────────────────────────────────────────────────────────────────
// Owns all wizard state and calls wizardStep1/2/3 APIs to save each step.
// Receives existing config/envSources from App to pre-populate fields.
-export default function Wizard({ config, envSources, bgUrl, bgLoaded, onBgLoad, onComplete }) {
- const [step, setStep] = useState(1)
- const [saving, setSaving] = useState(false)
+export default function Wizard({
+ config,
+ envSources,
+ bgUrl,
+ bgLoaded,
+ onBgLoad,
+ onComplete,
+}) {
+ const [step, setStep] = useState(1);
+ const [saving, setSaving] = useState(false);
const [fields, setFields] = useState(() => {
- const s = (config.DOWNLOAD_SERVICES || '').split(',')
+ const s = (config.DOWNLOAD_SERVICES || "").split(",");
return {
// Step 1
- user: config.LISTENBRAINZ_USER || '',
- discoveryMode: config.LISTENBRAINZ_DISCOVERY || 'playlist',
+ user: config.LISTENBRAINZ_USER || "",
+ discoveryMode: config.LISTENBRAINZ_DISCOVERY || "playlist",
checked: {
- 'weekly-exploration': !!config.WEEKLY_EXPLORATION_SCHEDULE,
- 'weekly-jams': !!config.WEEKLY_JAMS_SCHEDULE,
- 'daily-jams': !!config.DAILY_JAMS_SCHEDULE,
+ "weekly-exploration": !!config.WEEKLY_EXPLORATION_SCHEDULE,
+ "weekly-jams": !!config.WEEKLY_JAMS_SCHEDULE,
+ "daily-jams": !!config.DAILY_JAMS_SCHEDULE,
},
// Step 2
- system: config.EXPLO_SYSTEM || '',
- systemUrl: config.SYSTEM_URL || '',
- apiKey: config.API_KEY || '',
- libraryName: config.LIBRARY_NAME || '',
- systemUsername: config.SYSTEM_USERNAME || '',
- systemPassword: config.SYSTEM_PASSWORD || '',
- playlistDir: config.PLAYLIST_DIR || '',
- sleepMinutes: config.SLEEP || '',
- publicPlaylist: config.PUBLIC_PLAYLIST === 'true',
+ system: config.EXPLO_SYSTEM || "",
+ systemUrl: config.SYSTEM_URL || "",
+ apiKey: config.API_KEY || "",
+ libraryName: config.LIBRARY_NAME || "",
+ systemUsername: config.SYSTEM_USERNAME || "",
+ systemPassword: config.SYSTEM_PASSWORD || "",
+ playlistDir: config.PLAYLIST_DIR || "",
+ sleepMinutes: config.SLEEP || "",
+ publicPlaylist: config.PUBLIC_PLAYLIST === "true",
// Step 3
- downloadDir: config.DOWNLOAD_DIR || '',
- useSubdirectory: config.USE_SUBDIRECTORY !== 'false',
- migrateDownloads: config.MIGRATE_DOWNLOADS === 'true',
- dlServices: { youtube: s.includes('youtube'), slskd: s.includes('slskd') },
- youtubeApiKey: config.YOUTUBE_API_KEY || '',
- trackExtension: config.TRACK_EXTENSION || '',
- filterList: config.FILTER_LIST || '',
- slskdUrl: config.SLSKD_URL || '',
- slskdApiKey: config.SLSKD_API_KEY || '',
- extensions: config.EXTENSIONS || '',
- }
- })
-
- const setField = (key, val) => setFields(prev => ({ ...prev, [key]: val }))
+ downloadDir: config.DOWNLOAD_DIR || "",
+ useSubdirectory: config.USE_SUBDIRECTORY !== "false",
+ migrateDownloads: config.MIGRATE_DOWNLOADS === "true",
+ dlServices: {
+ youtube: s.includes("youtube"),
+ slskd: s.includes("slskd"),
+ },
+ youtubeApiKey: config.YOUTUBE_API_KEY || "",
+ trackExtension: config.TRACK_EXTENSION || "",
+ filterList: config.FILTER_LIST || "",
+ slskdUrl: config.SLSKD_URL || "",
+ slskdApiKey: config.SLSKD_API_KEY || "",
+ extensions: config.EXTENSIONS || "",
+ adminAuthMethod: config.ADMIN_AUTH_METHOD || "password",
+ adminApiKey: config.ADMIN_API_KEY || "",
+ adminSystemUsername: config.ADMIN_SYSTEM_USERNAME || "",
+ adminSystemPassword: config.ADMIN_SYSTEM_PASSWORD || "",
+ };
+ });
+
+ const setField = (key, val) => setFields((prev) => ({ ...prev, [key]: val }));
const lockedKeys = Object.entries(envSources)
- .filter(([k, s]) => s === 'env' && !k.endsWith('_SCHEDULE') && !k.endsWith('_FLAGS'))
- .map(([k]) => k)
+ .filter(
+ ([k, s]) =>
+ s === "env" && !k.endsWith("_SCHEDULE") && !k.endsWith("_FLAGS"),
+ )
+ .map(([k]) => k);
async function handleStep1() {
- setSaving(true)
+ setSaving(true);
try {
- const playlists = Object.entries(fields.checked).filter(([, v]) => v).map(([k]) => k)
- await wizardStep1(fields.user.trim(), playlists, fields.discoveryMode)
+ const playlists = Object.entries(fields.checked)
+ .filter(([, v]) => v)
+ .map(([k]) => k);
+ await wizardStep1(fields.user.trim(), playlists, fields.discoveryMode);
if (playlists.length > 0) {
- prefetchPlaylists(fields.user.trim(), playlists, { source: 'wizard' }).catch(() => {})
+ prefetchPlaylists(fields.user.trim(), playlists, {
+ source: "wizard",
+ }).catch(() => {});
}
- setStep(2)
+ setStep(2);
} catch (e) {
- alert('Error saving: ' + e.message)
+ alert("Error saving: " + e.message);
} finally {
- setSaving(false)
+ setSaving(false);
}
}
async function handleStep2() {
- setSaving(true)
+ setSaving(true);
try {
await wizardStep2({
- system: fields.system, url: fields.systemUrl, api_key: fields.apiKey,
- library_name: fields.libraryName, username: fields.systemUsername,
- password: fields.systemPassword, playlist_dir: fields.playlistDir,
- sleep: fields.sleepMinutes, public_playlist: fields.publicPlaylist,
- })
- setStep(3)
+ system: fields.system,
+ url: fields.systemUrl,
+
+ auth_method: fields.authMethod,
+
+ api_key: fields.apiKey,
+ username: fields.systemUsername,
+ password: fields.systemPassword,
+
+ admin_credentials: fields.adminCredentials,
+ admin_auth_method: fields.adminAuthMethod,
+ admin_api_key: fields.adminApiKey,
+ admin_system_username: fields.adminSystemUsername,
+ admin_system_password: fields.adminSystemPassword,
+
+ library_name: fields.libraryName,
+ playlist_dir: fields.playlistDir,
+ sleep: fields.sleepMinutes,
+ public_playlist: fields.publicPlaylist,
+ });
+ setStep(3);
} catch (e) {
- alert('Error saving: ' + e.message)
+ alert("Error saving: " + e.message);
} finally {
- setSaving(false)
+ setSaving(false);
}
}
async function handleStep3() {
- setSaving(true)
+ setSaving(true);
try {
- const services = Object.entries(fields.dlServices).filter(([, v]) => v).map(([k]) => k)
+ const services = Object.entries(fields.dlServices)
+ .filter(([, v]) => v)
+ .map(([k]) => k);
await wizardStep3({
- download_dir: fields.downloadDir, use_subdirectory: fields.useSubdirectory,
- migrate_downloads: fields.migrateDownloads, download_services: services,
- youtube_api_key: fields.youtubeApiKey, track_extension: fields.trackExtension,
- filter_list: fields.filterList, slskd_url: fields.slskdUrl, slskd_api_key: fields.slskdApiKey,
+ download_dir: fields.downloadDir,
+ use_subdirectory: fields.useSubdirectory,
+ migrate_downloads: fields.migrateDownloads,
+ download_services: services,
+ youtube_api_key: fields.youtubeApiKey,
+ track_extension: fields.trackExtension,
+ filter_list: fields.filterList,
+ slskd_url: fields.slskdUrl,
+ slskd_api_key: fields.slskdApiKey,
extensions: fields.extensions,
- })
- onComplete()
+ });
+ onComplete();
} catch (e) {
- alert('Error saving: ' + e.message)
+ alert("Error saving: " + e.message);
} finally {
- setSaving(false)
+ setSaving(false);
}
}
return (
-
-
+
{/* Artwork backdrop — blurred ambient glow, matches the Settings page treatment */}
-
+
{bgUrl && (
@@ -513,39 +814,49 @@ export default function Wizard({ config, envSources, bgUrl, bgLoaded, onBgLoad,
-
Explo
+
+ Explo
+
{lockedKeys.length > 0 && (
- You've set the following in your Docker environment, so they can't be changed here:{' '}
- {lockedKeys.join(', ')}
+ You've set the following in your Docker environment, so they can't
+ be changed here: {lockedKeys.join(", ")}
)}
{step === 1 && (
)}
{step === 2 && (
setStep(1)} onNext={handleStep2} saving={saving}
+ onBack={() => setStep(1)}
+ onNext={handleStep2}
+ saving={saving}
/>
)}
{step === 3 && (
setStep(2)} onFinish={handleStep3} saving={saving}
+ onBack={() => setStep(2)}
+ onFinish={handleStep3}
+ saving={saving}
/>
)}
- )
+ );
}