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
@@ -0,0 +1,35 @@
import { Audiobook } from "../types";
import { AudiobookCard } from "@/components/ui/AudiobookCard";
import { api } from "@/lib/api";
interface LibraryAudiobooksGridProps {
audiobooks: Audiobook[];
}
export function LibraryAudiobooksGrid({
audiobooks,
}: LibraryAudiobooksGridProps) {
const getCoverUrl = (coverUrl: string | null, size = 300) => {
if (!coverUrl) return null;
return api.getCoverArtUrl(coverUrl, size);
};
return (
<div
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 3xl:grid-cols-10 gap-4"
data-tv-section="search-results-audiobooks"
>
{audiobooks.slice(0, 6).map((audiobook, index) => (
<AudiobookCard
key={audiobook.id}
id={audiobook.id}
title={audiobook.title}
author={audiobook.author || "Unknown Author"}
coverUrl={audiobook.coverUrl}
index={index}
getCoverUrl={getCoverUrl}
/>
))}
</div>
);
}
@@ -25,6 +25,7 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
const formattedTracks = tracks.map((t) => ({
id: t.id,
title: t.title,
displayTitle: t.displayTitle,
duration: t.duration,
artist: {
id: t.album.artist.id,
@@ -64,9 +65,7 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
key={track.id}
className={cn(
"flex items-center gap-3 p-2 rounded-md group transition-colors",
isCurrentTrack
? "bg-white/10"
: "hover:bg-white/5"
isCurrentTrack ? "bg-white/10" : "hover:bg-white/5"
)}
>
{/* Play Button / Track Number */}
@@ -101,7 +100,9 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<span className="text-gray-500 text-xs"></span>
<span className="text-gray-500 text-xs">
</span>
</div>
)}
</div>
@@ -111,14 +112,19 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
<p
className={cn(
"text-sm font-medium truncate",
isCurrentTrack ? "text-[#ecb200]" : "text-white"
isCurrentTrack
? "text-[#ecb200]"
: "text-white"
)}
>
{track.title}
</p>
<p className="text-xs text-gray-400 truncate">
<Link
href={`/artist/${track.album.artist.mbid || track.album.artist.id}`}
href={`/artist/${
track.album.artist.mbid ||
track.album.artist.id
}`}
className="hover:underline hover:text-white"
onClick={(e) => e.stopPropagation()}
>
@@ -145,5 +151,3 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
</div>
);
}
@@ -4,6 +4,7 @@ import { Music } from "lucide-react";
import { cn } from "@/utils/cn";
import { DiscoverResult } from "../types";
import { api } from "@/lib/api";
import { formatListeners } from "@/lib/format";
interface SimilarArtistsGridProps {
discoverResults: DiscoverResult[];
@@ -29,7 +30,10 @@ export function SimilarArtistsGrid({
<h2 className="text-2xl font-bold text-white mb-6">
Similar Artists
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 3xl:grid-cols-10 gap-4" data-tv-section="search-results-artists">
<div
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 3xl:grid-cols-10 gap-4"
data-tv-section="search-results-artists"
>
{artistResults.slice(1, 7).map((result, index) => {
const artistId =
result.mbid || encodeURIComponent(result.name);
@@ -61,7 +65,9 @@ export function SimilarArtistsGrid({
<h3 className="text-base font-bold text-white line-clamp-1 mb-1">
{result.name}
</h3>
<p className="text-sm text-[#b3b3b3]">Artist</p>
<p className="text-sm text-[#b3b3b3]">
{formatListeners(result.listeners)}
</p>
</div>
</Link>
);
@@ -3,6 +3,7 @@ import Image from "next/image";
import { Music } from "lucide-react";
import { api } from "@/lib/api";
import { Artist, DiscoverResult } from "../types";
import { formatListeners } from "@/lib/format";
interface TopResultProps {
libraryArtist?: Artist;