Release v1.3.0: Multi-source downloads, audio analyzer resilience, mobile improvements

Major Features:
- Multi-source download system (Soulseek/Lidarr with fallback)
- Configurable enrichment speed control (1-5x)
- Mobile touch drag support for seek sliders
- iOS PWA media controls (Control Center, Lock Screen)
- Artist name alias resolution via Last.fm
- Circuit breaker pattern for audio analysis

Critical Fixes:
- Audio analyzer stability (non-ASCII, BrokenProcessPool, OOM)
- Discovery system race conditions and import failures
- Radio decade categorization using originalYear
- LastFM API response normalization
- Mood bucket infinite loop prevention

Security:
- Bull Board admin authentication
- Lidarr webhook signature verification
- JWT token expiration and refresh
- Encryption key validation on startup

Closes #2, #6, #9, #13, #21, #26, #31, #34, #35, #37, #40, #43
This commit is contained in:
Your Name
2026-01-06 20:07:33 -06:00
parent 8fe151a0d1
commit cc8d0f6969
242 changed files with 20562 additions and 7725 deletions

View File

@@ -12,6 +12,34 @@ export function DownloadPreferencesSection({
settings,
onUpdate,
}: DownloadPreferencesSectionProps) {
// Service configuration detection
const isLidarrConfigured =
settings.lidarrEnabled === true &&
settings.lidarrUrl.trim() !== "" &&
settings.lidarrApiKey.trim() !== "";
const isSoulseekConfigured =
settings.soulseekUsername.trim() !== "" &&
settings.soulseekPassword.trim() !== "";
const areBothServicesConfigured = isLidarrConfigured && isSoulseekConfigured;
const isDisabled = !areBothServicesConfigured;
// Dynamic fallback options based on primary source
const getFallbackOptions = () => {
if (settings.downloadSource === "soulseek") {
return [
{ value: "none", label: "Skip track" },
{ value: "lidarr", label: "Download full album via Lidarr" },
];
} else {
return [
{ value: "none", label: "Skip album" },
{ value: "soulseek", label: "Try Soulseek for individual tracks" },
];
}
};
return (
<SettingsSection
id="download-preferences"
@@ -20,42 +48,80 @@ export function DownloadPreferencesSection({
>
<SettingsRow
label="Primary Download Source"
description="Choose how to download music for imported playlists"
description={
isDisabled
? "Requires both Soulseek and Lidarr to be configured"
: "Choose how to download music for imported playlists"
}
>
<SettingsSelect
value={settings.downloadSource || "soulseek"}
onChange={(v) =>
onUpdate({ downloadSource: v as "soulseek" | "lidarr" })
onUpdate({
downloadSource: v as "soulseek" | "lidarr",
primaryFailureFallback: "none"
})
}
options={[
{ value: "soulseek", label: "Soulseek (Per-track)" },
{ value: "lidarr", label: "Lidarr (Full albums)" },
]}
disabled={isDisabled}
/>
</SettingsRow>
{settings.downloadSource === "soulseek" && (
<SettingsRow
label="When Soulseek Fails"
description="What to do if a track can't be found on Soulseek"
>
<SettingsSelect
value={settings.soulseekFallback || "none"}
onChange={(v) =>
onUpdate({
soulseekFallback: v as "none" | "lidarr",
})
}
options={[
{ value: "none", label: "Skip track" },
{
value: "lidarr",
label: "Download full album via Lidarr",
},
]}
/>
</SettingsRow>
)}
<SettingsRow
label={
settings.downloadSource === "soulseek"
? "When Soulseek Fails"
: "When Lidarr Fails"
}
description={
isDisabled
? "Requires both Soulseek and Lidarr to be configured"
: settings.downloadSource === "soulseek"
? "What to do if a track can't be found on Soulseek"
: "What to do if an album can't be found on Lidarr"
}
>
<SettingsSelect
value={settings.primaryFailureFallback || "none"}
onChange={(v) =>
onUpdate({
primaryFailureFallback: v as "none" | "lidarr" | "soulseek",
})
}
options={getFallbackOptions()}
disabled={isDisabled}
/>
</SettingsRow>
<SettingsRow
label="Soulseek Concurrent Downloads"
description="Number of simultaneous downloads when using Soulseek (1-10)"
>
<SettingsSelect
value={settings.soulseekConcurrentDownloads?.toString() || "4"}
onChange={(v) =>
onUpdate({
soulseekConcurrentDownloads: parseInt(v),
})
}
options={[
{ value: "1", label: "1" },
{ value: "2", label: "2" },
{ value: "3", label: "3" },
{ value: "4", label: "4 (Default)" },
{ value: "5", label: "5" },
{ value: "6", label: "6" },
{ value: "7", label: "7" },
{ value: "8", label: "8" },
{ value: "9", label: "9" },
{ value: "10", label: "10" },
]}
disabled={!isSoulseekConfigured}
/>
</SettingsRow>
</SettingsSection>
);
}