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

@@ -6,6 +6,7 @@ import { useAudioControls } from "@/lib/audio-controls-context";
import { api } from "@/lib/api";
import { howlerEngine } from "@/lib/howler-engine";
import { audioSeekEmitter } from "@/lib/audio-seek-emitter";
import { dispatchQueryEvent } from "@/lib/query-events";
import {
useEffect,
useLayoutEffect,
@@ -71,7 +72,7 @@ export const HowlerAudioElement = memo(function HowlerAudioElement() {
} = useAudioPlayback();
// Controls context
const { pause, next } = useAudioControls();
const { pause, next, nextPodcastEpisode } = useAudioControls();
// Refs
const lastTrackIdRef = useRef<string | null>(null);
@@ -130,8 +131,12 @@ export const HowlerAudioElement = memo(function HowlerAudioElement() {
savePodcastProgress(true);
}
// Handle track advancement based on repeat mode
if (playbackType === "track") {
// Handle track advancement based on playback type
if (playbackType === "podcast") {
nextPodcastEpisode(); // Auto-advance to next episode
} else if (playbackType === "audiobook") {
pause();
} else if (playbackType === "track") {
if (repeatMode === "one") {
howlerEngine.seek(0);
howlerEngine.play();
@@ -208,7 +213,7 @@ export const HowlerAudioElement = memo(function HowlerAudioElement() {
howlerEngine.off("play", handlePlay);
howlerEngine.off("pause", handlePause);
};
}, [playbackType, currentTrack, currentAudiobook, currentPodcast, repeatMode, next, pause, setCurrentTimeFromEngine, setDuration, setIsPlaying, queue, setCurrentTrack, setCurrentAudiobook, setCurrentPodcast, setPlaybackType]);
}, [playbackType, currentTrack, currentAudiobook, currentPodcast, repeatMode, next, nextPodcastEpisode, pause, setCurrentTimeFromEngine, setDuration, setIsPlaying, queue, setCurrentTrack, setCurrentAudiobook, setCurrentPodcast, setPlaybackType]);
// Save audiobook progress
const saveAudiobookProgress = useCallback(
@@ -245,6 +250,8 @@ export const HowlerAudioElement = memo(function HowlerAudioElement() {
lastPlayedAt: new Date(),
},
});
dispatchQueryEvent("audiobook-progress-updated");
} catch (err) {
console.error(
"[HowlerAudioElement] Failed to save audiobook progress:",
@@ -277,6 +284,8 @@ export const HowlerAudioElement = memo(function HowlerAudioElement() {
duration,
isFinished
);
dispatchQueryEvent("podcast-progress-updated");
} catch (err) {
console.error(
"[HowlerAudioElement] Failed to save podcast progress:",