feat: Update cover art retrieval to use higher resolution images and enhance download manager with new features

This commit is contained in:
2025-08-10 02:06:39 +00:00
committed by GitHub
parent 7e6a28e4f4
commit 7a1c7e1eae
8 changed files with 189 additions and 46 deletions

View File

@@ -60,7 +60,7 @@ export function useFavoriteAlbums() {
id: album.id,
name: album.name,
artist: album.artist,
coverArt: album.coverArt ? api.getCoverArtUrl(album.coverArt, 64) : undefined
coverArt: album.coverArt ? api.getCoverArtUrl(album.coverArt, 300) : undefined
};
addFavoriteAlbum(favoriteAlbum);
}

View File

@@ -7,9 +7,14 @@ export interface DownloadProgress {
completed: number;
total: number;
failed: number;
status: 'idle' | 'starting' | 'downloading' | 'complete' | 'error';
status: 'idle' | 'starting' | 'downloading' | 'complete' | 'error' | 'paused';
currentSong?: string;
currentArtist?: string;
currentAlbum?: string;
error?: string;
downloadSpeed?: number; // In bytes per second
timeRemaining?: number; // In seconds
percentComplete?: number; // 0-100
}
export interface OfflineItem {
@@ -19,6 +24,10 @@ export interface OfflineItem {
artist: string;
downloadedAt: number;
size?: number;
bitRate?: number;
duration?: number;
format?: string;
lastPlayed?: number;
}
export interface OfflineStats {
@@ -28,6 +37,13 @@ export interface OfflineStats {
metaSize: number;
downloadedAlbums: number;
downloadedSongs: number;
lastDownload: number | null;
downloadErrors: number;
remainingStorage: number | null;
autoDownloadEnabled: boolean;
downloadQuality: 'original' | 'high' | 'medium' | 'low';
downloadOnWifiOnly: boolean;
priorityContent: string[]; // IDs of albums or playlists that should always be available offline
}
class DownloadManager {
@@ -121,30 +137,107 @@ class DownloadManager {
});
}
async downloadSong(song: Song): Promise<void> {
async downloadSong(
song: Song,
options?: { quality?: 'original' | 'high' | 'medium' | 'low', priority?: boolean }
): Promise<void> {
const songWithUrl = {
...song,
streamUrl: this.getDownloadUrl(song.id),
streamUrl: this.getDownloadUrl(song.id, options?.quality),
offlineUrl: `offline-song-${song.id}`,
duration: song.duration,
bitRate: song.bitRate,
size: song.size
bitRate: song.bitRate,
size: song.size,
priority: options?.priority || false,
quality: options?.quality || 'original'
};
return this.sendMessage('DOWNLOAD_SONG', songWithUrl);
}
async downloadQueue(songs: Song[]): Promise<void> {
const songsWithUrls = songs.map(song => ({
...song,
streamUrl: this.getDownloadUrl(song.id),
offlineUrl: `offline-song-${song.id}`,
duration: song.duration,
bitRate: song.bitRate,
size: song.size
}));
async downloadQueue(
songs: Song[],
options?: {
quality?: 'original' | 'high' | 'medium' | 'low',
priority?: boolean,
onProgressUpdate?: (progress: DownloadProgress) => void
}
): Promise<void> {
if (!this.worker) {
throw new Error('Service Worker not available');
}
return this.sendMessage('DOWNLOAD_QUEUE', { songs: songsWithUrls });
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
const { type, data } = event.data;
switch (type) {
case 'DOWNLOAD_PROGRESS':
if (options?.onProgressUpdate) {
options.onProgressUpdate(data);
}
break;
case 'DOWNLOAD_COMPLETE':
resolve();
break;
case 'DOWNLOAD_ERROR':
reject(new Error(data.error));
break;
}
};
const songsWithUrls = songs.map(song => ({
...song,
streamUrl: this.getDownloadUrl(song.id, options?.quality),
offlineUrl: `offline-song-${song.id}`,
duration: song.duration,
bitRate: song.bitRate,
size: song.size,
priority: options?.priority || false,
quality: options?.quality || 'original'
}));
this.worker!.postMessage({
type: 'DOWNLOAD_QUEUE',
data: { songs: songsWithUrls }
}, [channel.port2]);
});
}
async pauseDownloads(): Promise<void> {
return this.sendMessage('PAUSE_DOWNLOADS', {});
}
async resumeDownloads(): Promise<void> {
return this.sendMessage('RESUME_DOWNLOADS', {});
}
async cancelDownloads(): Promise<void> {
return this.sendMessage('CANCEL_DOWNLOADS', {});
}
async setDownloadPreferences(preferences: {
quality: 'original' | 'high' | 'medium' | 'low',
wifiOnly: boolean,
autoDownloadRecent: boolean,
autoDownloadFavorites: boolean,
maxStoragePercent: number,
priorityContent?: string[] // IDs of albums or playlists
}): Promise<void> {
return this.sendMessage('SET_DOWNLOAD_PREFERENCES', preferences);
}
async getDownloadPreferences(): Promise<{
quality: 'original' | 'high' | 'medium' | 'low',
wifiOnly: boolean,
autoDownloadRecent: boolean,
autoDownloadFavorites: boolean,
maxStoragePercent: number,
priorityContent: string[]
}> {
return this.sendMessage('GET_DOWNLOAD_PREFERENCES', {});
}
async enableOfflineMode(settings: {
@@ -177,15 +270,25 @@ class DownloadManager {
return this.sendMessage('GET_OFFLINE_ITEMS', {});
}
private getDownloadUrl(songId: string): string {
private getDownloadUrl(songId: string, quality?: 'original' | 'high' | 'medium' | 'low'): string {
const api = getNavidromeAPI();
if (!api) throw new Error('Navidrome server not configured');
// Use direct download to fetch original file; browser handles transcoding/decoding.
// Fall back to stream URL if the server does not allow downloads.
if (typeof (api as any).getDownloadUrl === 'function') {
return (api as any).getDownloadUrl(songId);
// Use direct download to fetch original file by default
if (quality === 'original' || !quality) {
if (typeof (api as any).getDownloadUrl === 'function') {
return (api as any).getDownloadUrl(songId);
}
}
return api.getStreamUrl(songId);
// For other quality settings, use the stream URL with appropriate parameters
const maxBitRate = quality === 'high' ? 320 :
quality === 'medium' ? 192 :
quality === 'low' ? 128 : undefined;
const format = quality === 'low' ? 'mp3' : undefined; // Use mp3 for low quality, original otherwise
return api.getStreamUrl(songId, { maxBitRate, format });
}
// LocalStorage fallback for browsers without service worker support
@@ -309,7 +412,14 @@ export function useOfflineDownloads() {
imageSize: 0,
metaSize: 0,
downloadedAlbums: 0,
downloadedSongs: 0
downloadedSongs: 0,
lastDownload: null,
downloadErrors: 0,
remainingStorage: null,
autoDownloadEnabled: false,
downloadQuality: 'original',
downloadOnWifiOnly: true,
priorityContent: []
});
useEffect(() => {