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:
124
frontend/hooks/useMetadataDisplay.ts
Normal file
124
frontend/hooks/useMetadataDisplay.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Hook for computing display values from override/canonical fields
|
||||
* Pattern: displayValue = userOverride ?? canonicalValue
|
||||
*/
|
||||
|
||||
interface ArtistDisplayData {
|
||||
name: string;
|
||||
summary: string | null;
|
||||
heroUrl: string | null;
|
||||
genres: string[];
|
||||
hasUserOverrides: boolean;
|
||||
// Original values for tooltip/reset display
|
||||
originalName?: string;
|
||||
originalSummary?: string | null;
|
||||
}
|
||||
|
||||
interface AlbumDisplayData {
|
||||
title: string;
|
||||
year: number | null;
|
||||
coverUrl: string | null;
|
||||
genres: string[];
|
||||
hasUserOverrides: boolean;
|
||||
// Original values for tooltip/reset display
|
||||
originalTitle?: string;
|
||||
originalYear?: number | null;
|
||||
}
|
||||
|
||||
interface TrackDisplayData {
|
||||
title: string;
|
||||
trackNo: number | null;
|
||||
hasUserOverrides: boolean;
|
||||
// Original value for tooltip/reset display
|
||||
originalTitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute display data for an artist, merging user overrides with canonical data
|
||||
*/
|
||||
export function useArtistDisplayData(artist: any): ArtistDisplayData {
|
||||
if (!artist) {
|
||||
return {
|
||||
name: "Unknown Artist",
|
||||
summary: null,
|
||||
heroUrl: null,
|
||||
genres: [],
|
||||
hasUserOverrides: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: artist.displayName ?? artist.name ?? "Unknown Artist",
|
||||
summary: artist.userSummary ?? artist.summary ?? artist.bio ?? null,
|
||||
heroUrl: artist.userHeroUrl ?? artist.heroUrl ?? artist.image ?? null,
|
||||
genres: mergeGenres(artist.userGenres, artist.genres ?? artist.tags),
|
||||
hasUserOverrides: artist.hasUserOverrides ?? false,
|
||||
originalName: artist.displayName ? artist.name : undefined,
|
||||
originalSummary: artist.userSummary
|
||||
? artist.summary ?? artist.bio
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute display data for an album, merging user overrides with canonical data
|
||||
*/
|
||||
export function useAlbumDisplayData(album: any): AlbumDisplayData {
|
||||
if (!album) {
|
||||
return {
|
||||
title: "Unknown Album",
|
||||
year: null,
|
||||
coverUrl: null,
|
||||
genres: [],
|
||||
hasUserOverrides: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: album.displayTitle ?? album.title ?? "Unknown Album",
|
||||
year: album.displayYear ?? album.year ?? null,
|
||||
coverUrl: album.userCoverUrl ?? album.coverUrl ?? null,
|
||||
genres: mergeGenres(album.userGenres, album.genres),
|
||||
hasUserOverrides: album.hasUserOverrides ?? false,
|
||||
originalTitle: album.displayTitle ? album.title : undefined,
|
||||
originalYear:
|
||||
album.displayYear !== undefined && album.displayYear !== null
|
||||
? album.year
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute display data for a track, merging user overrides with canonical data
|
||||
*/
|
||||
export function useTrackDisplayData(track: any): TrackDisplayData {
|
||||
if (!track) {
|
||||
return {
|
||||
title: "Unknown Track",
|
||||
trackNo: null,
|
||||
hasUserOverrides: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: track.displayTitle ?? track.title ?? "Unknown Track",
|
||||
trackNo: track.displayTrackNo ?? track.trackNo ?? null,
|
||||
hasUserOverrides: track.hasUserOverrides ?? false,
|
||||
originalTitle: track.displayTitle ? track.title : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge user genres with canonical genres (user genres first for priority)
|
||||
* Deduplicates the result
|
||||
*/
|
||||
function mergeGenres(
|
||||
userGenres?: string[],
|
||||
canonicalGenres?: string[]
|
||||
): string[] {
|
||||
const user = Array.isArray(userGenres) ? userGenres : [];
|
||||
const canonical = Array.isArray(canonicalGenres) ? canonicalGenres : [];
|
||||
|
||||
// Merge with user genres taking precedence, then deduplicate
|
||||
return [...new Set([...user, ...canonical])];
|
||||
}
|
||||
Reference in New Issue
Block a user