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

@@ -19,6 +19,15 @@ interface MetadataEditorProps {
rgMbid?: string;
coverUrl?: string;
heroUrl?: string;
// Original values for comparison (when user overrides exist)
_originalName?: string;
_originalBio?: string | null;
_originalGenres?: string[];
_originalHeroUrl?: string | null;
_originalTitle?: string;
_originalYear?: number | null;
_originalCoverUrl?: string | null;
_hasUserOverrides?: boolean;
};
onSave?: (updatedData: any) => void;
}
@@ -36,7 +45,9 @@ export function MetadataEditor({
}: MetadataEditorProps) {
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isResetting, setIsResetting] = useState(false);
const [formData, setFormData] = useState(currentData);
const hasOverrides = currentData._hasUserOverrides ?? false;
const handleOpen = () => {
setFormData(currentData);
@@ -48,6 +59,35 @@ export function MetadataEditor({
setFormData(currentData);
};
const handleReset = async () => {
if (
!confirm(
"Reset all metadata to original values? This cannot be undone."
)
) {
return;
}
setIsResetting(true);
try {
if (type === "artist") {
await api.resetArtistMetadata(id);
} else if (type === "album") {
await api.resetAlbumMetadata(id);
} else {
await api.resetTrackMetadata(id);
}
toast.success("Metadata reset to original values");
onSave?.(null);
setIsOpen(false);
} catch (error: any) {
toast.error(error.message || "Failed to reset metadata");
} finally {
setIsResetting(false);
}
};
const handleSave = async () => {
setIsSaving(true);
try {
@@ -144,6 +184,24 @@ export function MetadataEditor({
}
className="w-full px-4 py-2 bg-[#181818] border border-white/10 rounded text-white focus:border-white/30 focus:outline-none"
/>
{type === "artist" &&
currentData._originalName &&
currentData._originalName !==
(formData.name || "") && (
<p className="mt-1 text-xs text-gray-500">
Original:{" "}
{currentData._originalName}
</p>
)}
{type !== "artist" &&
currentData._originalTitle &&
currentData._originalTitle !==
(formData.title || "") && (
<p className="mt-1 text-xs text-gray-500">
Original:{" "}
{currentData._originalTitle}
</p>
)}
</div>
{/* Bio (Artist only) */}
@@ -160,6 +218,18 @@ export function MetadataEditor({
rows={6}
className="w-full px-4 py-2 bg-[#181818] border border-white/10 rounded text-white focus:border-white/30 focus:outline-none resize-none"
/>
{currentData._originalBio &&
currentData._originalBio !==
(formData.bio || "") && (
<p className="mt-1 text-xs text-gray-500">
Original:{" "}
{currentData._originalBio.substring(
0,
100
)}
...
</p>
)}
</div>
)}
@@ -180,6 +250,14 @@ export function MetadataEditor({
}
className="w-full px-4 py-2 bg-[#181818] border border-white/10 rounded text-white focus:border-white/30 focus:outline-none"
/>
{currentData._originalYear &&
currentData._originalYear !==
(formData.year || null) && (
<p className="mt-1 text-xs text-gray-500">
Original:{" "}
{currentData._originalYear}
</p>
)}
</div>
)}
@@ -206,6 +284,21 @@ export function MetadataEditor({
placeholder="Rock, Alternative, Indie"
className="w-full px-4 py-2 bg-[#181818] border border-white/10 rounded text-white focus:border-white/30 focus:outline-none"
/>
{currentData._originalGenres &&
currentData._originalGenres.length > 0 &&
JSON.stringify(
currentData._originalGenres.sort()
) !==
JSON.stringify(
(formData.genres || []).sort()
) && (
<p className="mt-1 text-xs text-gray-500">
Original:{" "}
{currentData._originalGenres.join(
", "
)}
</p>
)}
</div>
{/* MusicBrainz ID */}
@@ -268,6 +361,24 @@ export function MetadataEditor({
placeholder="https://..."
className="w-full px-4 py-2 bg-[#181818] border border-white/10 rounded text-white focus:border-white/30 focus:outline-none text-sm"
/>
{type === "artist" &&
currentData._originalHeroUrl &&
currentData._originalHeroUrl !==
(formData.heroUrl || "") && (
<p className="mt-1 text-xs text-gray-500 truncate">
Original:{" "}
{currentData._originalHeroUrl}
</p>
)}
{type === "album" &&
currentData._originalCoverUrl &&
currentData._originalCoverUrl !==
(formData.coverUrl || "") && (
<p className="mt-1 text-xs text-gray-500 truncate">
Original:{" "}
{currentData._originalCoverUrl}
</p>
)}
{/* Image Preview */}
{(formData.heroUrl || formData.coverUrl) && (
<div className="mt-2">
@@ -295,6 +406,17 @@ export function MetadataEditor({
{/* Footer */}
<div className="flex items-center justify-end gap-3 p-6 border-t border-white/10">
{hasOverrides && (
<button
onClick={handleReset}
disabled={isSaving || isResetting}
className="px-6 py-2 rounded-full bg-red-500/20 hover:bg-red-500/30 text-red-400 font-bold transition-all border border-red-500/30 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isResetting
? "Resetting..."
: "Reset to Original"}
</button>
)}
<button
onClick={handleClose}
className="px-6 py-2 rounded-full bg-white/10 hover:bg-white/20 text-white font-bold transition-all"