Files
lidify/frontend/features/search/components/SimilarArtistsGrid.tsx
Your Name cc8d0f6969 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
2026-01-06 20:07:33 -06:00

79 lines
3.3 KiB
TypeScript

import Link from "next/link";
import Image from "next/image";
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[];
}
// Always proxy images through the backend for caching and mobile compatibility
const getProxiedImageUrl = (imageUrl: string | undefined): string | null => {
if (!imageUrl) return null;
return api.getCoverArtUrl(imageUrl, 300);
};
export function SimilarArtistsGrid({
discoverResults,
}: SimilarArtistsGridProps) {
const artistResults = discoverResults.filter((r) => r.type === "music");
if (artistResults.length <= 1) {
return null;
}
return (
<section>
<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"
>
{artistResults.slice(1, 7).map((result, index) => {
const artistId =
result.mbid || encodeURIComponent(result.name);
const imageUrl = getProxiedImageUrl(result.image);
return (
<Link
key={`artist-${artistId}-${index}`}
href={`/artist/${artistId}`}
data-tv-card
data-tv-card-index={index}
tabIndex={0}
>
<div className="bg-[#121212] hover:bg-[#181818] transition-all p-4 rounded-lg group cursor-pointer">
<div className="aspect-square bg-[#181818] rounded-full mb-4 flex items-center justify-center overflow-hidden relative">
{imageUrl ? (
<Image
src={imageUrl}
alt={result.name}
fill
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
className="object-cover group-hover:scale-110 transition-all"
unoptimized
/>
) : (
<Music className="w-12 h-12 text-gray-600" />
)}
</div>
<h3 className="text-base font-bold text-white line-clamp-1 mb-1">
{result.name}
</h3>
<p className="text-sm text-[#b3b3b3]">
{formatListeners(result.listeners)}
</p>
</div>
</Link>
);
})}
</div>
</section>
);
}