refactor: extract Qobuz adapters and unify DisplayTrack type
- Create $lib/adapters/qobuzAdapters.ts with: - formatDuration/formatDurationMinutes (removed duplicates) - getQobuzImage helper - formatQuality/formatAlbumQuality helpers - convertQobuzAlbum converter - convertQobuzArtist converter - Add unified DisplayTrack type to $lib/types/index.ts - Update +page.svelte: - Import adapters instead of inline functions - Remove duplicate converter functions (−57 lines) - Add handleDisplayTrackPlay for view components - Fix TypeScript type narrowing in view closures
This commit is contained in:
1 parent
30a33cfc95
commit
5be437dd4e
7 files changed
+190
-85
No files matched your search
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Qobuz Adapters
|
||||
*
|
||||
* Centralizes Qobuz API -> UI model conversion functions.
|
||||
* Eliminates duplicate formatting logic across the codebase.
|
||||
*/
|
||||
|
||||
import type {
|
||||
QobuzAlbum,
|
||||
QobuzArtist,
|
||||
AlbumDetail,
|
||||
ArtistDetail
|
||||
} from '$lib/types';
|
||||
|
||||
// ============ Formatting Utilities ============
|
||||
|
||||
/**
|
||||
* Format duration in seconds to "M:SS" format
|
||||
*/
|
||||
export function formatDuration(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for formatDuration (they were identical)
|
||||
*/
|
||||
export const formatDurationMinutes = formatDuration;
|
||||
|
||||
/**
|
||||
* Extract best available image from Qobuz image object
|
||||
*/
|
||||
export function getQobuzImage(image?: { large?: string; thumbnail?: string; small?: string }): string {
|
||||
return image?.large || image?.thumbnail || image?.small || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format quality string from bit depth and sampling rate
|
||||
*/
|
||||
export function formatQuality(
|
||||
hires: boolean | undefined,
|
||||
bitDepth: number | undefined,
|
||||
samplingRate: number | undefined
|
||||
): string {
|
||||
if (hires && bitDepth && samplingRate) {
|
||||
return `${bitDepth}bit/${samplingRate}kHz`;
|
||||
}
|
||||
return 'CD Quality';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format album quality string (different format with dash)
|
||||
*/
|
||||
export function formatAlbumQuality(
|
||||
hires: boolean | undefined,
|
||||
bitDepth: number | undefined,
|
||||
samplingRate: number | undefined
|
||||
): string {
|
||||
if (hires && bitDepth && samplingRate) {
|
||||
return `${bitDepth}-Bit / ${samplingRate} kHz`;
|
||||
}
|
||||
return 'CD Quality';
|
||||
}
|
||||
|
||||
// ============ Model Converters ============
|
||||
|
||||
/**
|
||||
* Convert Qobuz API album response to UI AlbumDetail model
|
||||
*/
|
||||
export function convertQobuzAlbum(album: QobuzAlbum): AlbumDetail {
|
||||
const artwork = getQobuzImage(album.image);
|
||||
const quality = formatAlbumQuality(
|
||||
album.hires_streamable,
|
||||
album.maximum_bit_depth,
|
||||
album.maximum_sampling_rate
|
||||
);
|
||||
|
||||
return {
|
||||
id: album.id,
|
||||
artwork,
|
||||
title: album.title,
|
||||
artist: album.artist?.name || 'Unknown Artist',
|
||||
artistId: album.artist?.id,
|
||||
year: album.release_date_original?.split('-')[0] || '',
|
||||
label: album.label?.name || '',
|
||||
genre: album.genre?.name || '',
|
||||
quality,
|
||||
trackCount: album.tracks_count || album.tracks?.items?.length || 0,
|
||||
duration: formatDuration(album.duration || 0),
|
||||
tracks: album.tracks?.items?.map((track, index) => ({
|
||||
id: track.id,
|
||||
number: index + 1,
|
||||
title: track.title,
|
||||
artist: track.performer?.name,
|
||||
duration: formatDuration(track.duration),
|
||||
durationSeconds: track.duration,
|
||||
quality: track.hires_streamable ? 'Hi-Res' : 'CD',
|
||||
hires: track.hires_streamable,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
albumId: album.id,
|
||||
artistId: track.performer?.id ?? album.artist?.id,
|
||||
isrc: track.isrc
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Qobuz API artist response to UI ArtistDetail model
|
||||
*/
|
||||
export function convertQobuzArtist(artist: QobuzArtist): ArtistDetail {
|
||||
const image = getQobuzImage(artist.image);
|
||||
|
||||
return {
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
image,
|
||||
albumsCount: artist.albums_count,
|
||||
biography: artist.biography,
|
||||
albums: artist.albums?.items?.map(album => {
|
||||
const artwork = getQobuzImage(album.image);
|
||||
const quality = formatQuality(
|
||||
album.hires_streamable,
|
||||
album.maximum_bit_depth,
|
||||
album.maximum_sampling_rate
|
||||
);
|
||||
return {
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artwork,
|
||||
year: album.release_date_original?.split('-')[0],
|
||||
quality
|
||||
};
|
||||
}) || [],
|
||||
totalAlbums: artist.albums?.total || artist.albums_count || 0
|
||||
};
|
||||
}
|
||||
@@ -247,7 +247,7 @@
|
||||
onShareQobuz: onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined,
|
||||
onShareSonglink: onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined,
|
||||
onGoToAlbum: onTrackGoToAlbum ? () => onTrackGoToAlbum(album.id) : undefined,
|
||||
onGoToArtist: album.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(album.artistId) : undefined
|
||||
onGoToArtist: album.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(album.artistId!) : undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -289,8 +289,8 @@
|
||||
onAddToPlaylist={onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined}
|
||||
onShareQobuz={onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined}
|
||||
onShareSonglink={onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined}
|
||||
onGoToAlbum={track.album?.id && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.album.id) : undefined}
|
||||
onGoToArtist={(track.performer?.id || artist.id) && onTrackGoToArtist ? () => onTrackGoToArtist(track.performer?.id || artist.id) : undefined}
|
||||
onGoToAlbum={track.album?.id && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.album!.id) : undefined}
|
||||
onGoToArtist={(track.performer?.id || artist.id) && onTrackGoToArtist ? () => onTrackGoToArtist(track.performer?.id ?? artist.id) : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -640,8 +640,8 @@
|
||||
onAddToPlaylist: !track.isLocal && onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined,
|
||||
onShareQobuz: !track.isLocal && onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined,
|
||||
onShareSonglink: !track.isLocal && onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined,
|
||||
onGoToAlbum: !track.isLocal && track.albumId && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.albumId) : undefined,
|
||||
onGoToArtist: !track.isLocal && track.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(track.artistId) : undefined
|
||||
onGoToAlbum: !track.isLocal && track.albumId && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.albumId!) : undefined,
|
||||
onGoToArtist: !track.isLocal && track.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(track.artistId!) : undefined
|
||||
}}
|
||||
/>
|
||||
{#if track.isLocal}
|
||||
|
||||
@@ -393,8 +393,8 @@
|
||||
onAddToPlaylist={onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined}
|
||||
onShareQobuz={onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined}
|
||||
onShareSonglink={onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined}
|
||||
onGoToAlbum={track.album?.id && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.album.id) : undefined}
|
||||
onGoToArtist={track.performer?.id && onTrackGoToArtist ? () => onTrackGoToArtist(track.performer.id) : undefined}
|
||||
onGoToAlbum={track.album?.id && onTrackGoToAlbum ? (() => { const albumId = track.album!.id!; return () => onTrackGoToAlbum(albumId); })() : undefined}
|
||||
onGoToArtist={track.performer?.id && onTrackGoToArtist ? (() => { const artistId = track.performer!.id!; return () => onTrackGoToArtist(artistId); })() : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,6 +134,30 @@ export interface PlaylistTrack {
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified display track interface used across views
|
||||
* Compatible with PlaylistTrack, FavoritesTrack, and ArtistTrack displays
|
||||
*/
|
||||
export interface DisplayTrack {
|
||||
id: number;
|
||||
number?: number;
|
||||
title: string;
|
||||
artist?: string;
|
||||
album?: string;
|
||||
albumArt?: string;
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
duration: string;
|
||||
durationSeconds: number;
|
||||
hires?: boolean;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
isrc?: string;
|
||||
isLocal?: boolean;
|
||||
localTrackId?: number;
|
||||
artworkPath?: string;
|
||||
}
|
||||
|
||||
// ============ Local Library Types ============
|
||||
|
||||
export interface LocalLibraryTrack {
|
||||
|
||||
+21
-78
@@ -118,16 +118,26 @@
|
||||
AlbumDetail,
|
||||
ArtistDetail,
|
||||
PlaylistTrack,
|
||||
DisplayTrack,
|
||||
LocalLibraryTrack,
|
||||
SongLinkResponse
|
||||
} from '$lib/types';
|
||||
|
||||
// Adapters
|
||||
import {
|
||||
convertQobuzAlbum,
|
||||
convertQobuzArtist,
|
||||
formatDuration
|
||||
} from '$lib/adapters/qobuzAdapters';
|
||||
|
||||
// Services
|
||||
import {
|
||||
playTrack,
|
||||
setToastCallback as setPlaybackToastCallback,
|
||||
checkTrackFavorite,
|
||||
toggleTrackFavorite,
|
||||
showTrackNotification,
|
||||
updateLastfmNowPlaying,
|
||||
cleanup as cleanupPlayback
|
||||
} from '$lib/services/playbackService';
|
||||
|
||||
@@ -250,41 +260,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function convertQobuzAlbum(album: QobuzAlbum): AlbumDetail {
|
||||
const artwork = album.image?.large || album.image?.thumbnail || album.image?.small || '';
|
||||
const quality = album.hires_streamable && album.maximum_bit_depth && album.maximum_sampling_rate
|
||||
? `${album.maximum_bit_depth}-Bit / ${album.maximum_sampling_rate} kHz`
|
||||
: 'CD Quality';
|
||||
|
||||
return {
|
||||
id: album.id,
|
||||
artwork,
|
||||
title: album.title,
|
||||
artist: album.artist?.name || 'Unknown Artist',
|
||||
artistId: album.artist?.id,
|
||||
year: album.release_date_original?.split('-')[0] || '',
|
||||
label: album.label?.name || '',
|
||||
genre: album.genre?.name || '',
|
||||
quality,
|
||||
trackCount: album.tracks_count || album.tracks?.items?.length || 0,
|
||||
duration: formatDurationMinutes(album.duration || 0),
|
||||
tracks: album.tracks?.items?.map((track, index) => ({
|
||||
id: track.id,
|
||||
number: index + 1,
|
||||
title: track.title,
|
||||
artist: track.performer?.name,
|
||||
duration: formatDuration(track.duration),
|
||||
durationSeconds: track.duration,
|
||||
quality: track.hires_streamable ? 'Hi-Res' : 'CD',
|
||||
hires: track.hires_streamable,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
albumId: album.id,
|
||||
artistId: track.performer?.id ?? album.artist?.id,
|
||||
isrc: track.isrc
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
|
||||
async function handleArtistClick(artistId: number) {
|
||||
try {
|
||||
@@ -301,43 +276,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function convertQobuzArtist(artist: QobuzArtist): ArtistDetail {
|
||||
const image = artist.image?.large || artist.image?.thumbnail || artist.image?.small;
|
||||
|
||||
return {
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
image,
|
||||
albumsCount: artist.albums_count,
|
||||
biography: artist.biography,
|
||||
albums: artist.albums?.items?.map(album => {
|
||||
const artwork = album.image?.large || album.image?.thumbnail || album.image?.small || '';
|
||||
const quality = album.hires_streamable && album.maximum_bit_depth && album.maximum_sampling_rate
|
||||
? `${album.maximum_bit_depth}bit/${album.maximum_sampling_rate}kHz`
|
||||
: 'CD Quality';
|
||||
return {
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artwork,
|
||||
year: album.release_date_original?.split('-')[0],
|
||||
quality
|
||||
};
|
||||
}) || [],
|
||||
totalAlbums: artist.albums?.total || artist.albums_count || 0
|
||||
};
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatDurationMinutes(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Album-specific queue track builder (needs selectedAlbum context)
|
||||
function buildAlbumQueueTrack(track: Track): BackendQueueTrack {
|
||||
@@ -828,14 +766,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePlaylistTrackPlay(track: PlaylistTrack) {
|
||||
console.log('Playing playlist track:', track);
|
||||
/**
|
||||
* Handle playback of DisplayTrack (used by ArtistDetailView, PlaylistDetailView, FavoritesView)
|
||||
* This is fire-and-forget to match view callback signatures
|
||||
*/
|
||||
function handleDisplayTrackPlay(track: DisplayTrack): void {
|
||||
console.log('Playing display track:', track);
|
||||
|
||||
const quality = track.hires && track.bitDepth && track.samplingRate
|
||||
? `${track.bitDepth}bit/${track.samplingRate}kHz`
|
||||
: 'CD Quality';
|
||||
|
||||
await playTrack({
|
||||
// Fire-and-forget async call
|
||||
playTrack({
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist || 'Unknown Artist',
|
||||
@@ -1207,7 +1150,7 @@
|
||||
artist={selectedArtist}
|
||||
onBack={goBack}
|
||||
onAlbumClick={handleAlbumClick}
|
||||
onTrackPlay={handlePlaylistTrackPlay}
|
||||
onTrackPlay={handleDisplayTrackPlay}
|
||||
onTrackPlayNext={queueQobuzTrackNext}
|
||||
onTrackPlayLater={queueQobuzTrackLater}
|
||||
onTrackAddFavorite={handleAddToFavorites}
|
||||
@@ -1228,7 +1171,7 @@
|
||||
<PlaylistDetailView
|
||||
playlistId={selectedPlaylistId}
|
||||
onBack={goBack}
|
||||
onTrackPlay={handlePlaylistTrackPlay}
|
||||
onTrackPlay={handleDisplayTrackPlay}
|
||||
onTrackPlayNext={queuePlaylistTrackNext}
|
||||
onTrackPlayLater={queuePlaylistTrackLater}
|
||||
onTrackAddFavorite={handleAddToFavorites}
|
||||
@@ -1248,7 +1191,7 @@
|
||||
{:else if activeView === 'favorites'}
|
||||
<FavoritesView
|
||||
onAlbumClick={handleAlbumClick}
|
||||
onTrackPlay={handlePlaylistTrackPlay}
|
||||
onTrackPlay={handleDisplayTrackPlay}
|
||||
onArtistClick={handleArtistClick}
|
||||
onTrackPlayNext={queuePlaylistTrackNext}
|
||||
onTrackPlayLater={queuePlaylistTrackLater}
|
||||
|
||||
Reference in new issue
Block a user