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

@@ -9,8 +9,9 @@ interface DiscoverHeroProps {
export function DiscoverHero({ playlist, config }: DiscoverHeroProps) {
// Calculate total duration
const totalDuration = playlist?.tracks?.reduce((sum, t) => sum + (t.duration || 0), 0) || 0;
const totalDuration =
playlist?.tracks?.reduce((sum, t) => sum + (t.duration || 0), 0) || 0;
const formatTotalDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
@@ -37,26 +38,25 @@ export function DiscoverHero({ playlist, config }: DiscoverHeroProps) {
Discover Weekly
</h1>
<p className="text-sm text-white/60 mb-2 line-clamp-2">
Your personalized playlist of new music, curated based on your listening history.
Your personalized playlist of new music, curated based
on your listening history.
</p>
<div className="flex flex-wrap items-center gap-1 text-sm text-white/70">
{playlist && (
<>
<span>
Week of {format(new Date(playlist.weekStart), "MMM d, yyyy")}
Week of{" "}
{format(
new Date(playlist.weekStart),
"MMM d, yyyy"
)}
</span>
<span className="mx-1"></span>
<span>{playlist.totalCount} songs</span>
{totalDuration > 0 && (
<span>, {formatTotalDuration(totalDuration)}</span>
)}
{playlist.unavailableCount > 0 && (
<>
<span className="mx-1"></span>
<span className="text-orange-400">
{playlist.unavailableCount} unavailable
</span>
</>
<span>
, {formatTotalDuration(totalDuration)}
</span>
)}
</>
)}
@@ -64,7 +64,11 @@ export function DiscoverHero({ playlist, config }: DiscoverHeroProps) {
<>
<span className="mx-1"></span>
<span>
Updated {format(new Date(config.lastGeneratedAt), "MMM d")}
Updated{" "}
{format(
new Date(config.lastGeneratedAt),
"MMM d"
)}
</span>
</>
)}

View File

@@ -40,6 +40,10 @@ export function TrackList({
onLike,
}: TrackListProps) {
const formatDuration = (seconds: number) => {
// Defensive handling for invalid/missing duration
if (!seconds || isNaN(seconds) || seconds < 0) {
return "--:--";
}
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -78,7 +82,9 @@ export function TrackList({
<span
className={cn(
"text-sm group-hover:hidden",
isTrackPlaying ? "text-[#ecb200]" : "text-gray-400"
isTrackPlaying
? "text-[#ecb200]"
: "text-gray-400"
)}
>
{isTrackPlaying && isPlaying ? (
@@ -95,7 +101,10 @@ export function TrackList({
<div className="w-10 h-10 bg-[#282828] rounded shrink-0 overflow-hidden">
{track.coverUrl ? (
<Image
src={api.getCoverArtUrl(track.coverUrl, 80)}
src={api.getCoverArtUrl(
track.coverUrl,
80
)}
alt={track.album}
width={40}
height={40}
@@ -112,7 +121,9 @@ export function TrackList({
<p
className={cn(
"text-sm font-medium truncate",
isTrackPlaying ? "text-[#ecb200]" : "text-white"
isTrackPlaying
? "text-[#ecb200]"
: "text-white"
)}
>
{track.title}
@@ -153,7 +164,11 @@ export function TrackList({
? "text-purple-400 hover:text-purple-300"
: "text-gray-400 hover:text-white"
)}
title={track.isLiked ? "Unlike" : "Keep in library"}
title={
track.isLiked
? "Unlike"
: "Keep in library"
}
>
<Heart
className={cn(