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:
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user