feat(downloads): add download queue management and UI

This commit is contained in:
2025-10-01 09:48:03 -04:00
parent 759ebc71f6
commit ef4b85433c
9 changed files with 847 additions and 60 deletions

View File

@@ -0,0 +1,257 @@
/**
* Deezer Queue Manager
* Handles download queue processing with configurable concurrency
*/
import { downloadTrack } from './downloader';
import { deezerAPI } from '../deezer';
import {
downloadQueue,
updateQueueItem,
setCurrentJob,
getNextQueuedItem,
type QueueItem
} from '$lib/stores/downloadQueue';
import { settings } from '$lib/stores/settings';
import { get } from 'svelte/store';
import type { DeezerTrack } from '$lib/types/deezer';
export class DeezerQueueManager {
private isProcessing = false;
private abortController: AbortController | null = null;
/**
* Start processing the queue
*/
async start(): Promise<void> {
if (this.isProcessing) {
console.log('[DeezerQueueManager] Already processing queue');
return;
}
this.isProcessing = true;
this.abortController = new AbortController();
console.log('[DeezerQueueManager] Starting queue processor');
try {
await this.processQueue();
} catch (error) {
console.error('[DeezerQueueManager] Queue processing error:', error);
} finally {
this.isProcessing = false;
this.abortController = null;
}
}
/**
* Stop processing the queue
*/
stop(): void {
console.log('[DeezerQueueManager] Stopping queue processor');
this.isProcessing = false;
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}
/**
* Main queue processing loop
*/
private async processQueue(): Promise<void> {
while (this.isProcessing) {
const queueState = get(downloadQueue);
const nextItem = getNextQueuedItem(queueState);
if (!nextItem) {
// No more items to process
console.log('[DeezerQueueManager] Queue empty, stopping');
break;
}
console.log(`[DeezerQueueManager] Processing item: ${nextItem.title}`);
// Set as current job
await setCurrentJob(nextItem.id);
await updateQueueItem(nextItem.id, { status: 'downloading' });
try {
// Process based on type
if (nextItem.type === 'track') {
await this.downloadSingleTrack(nextItem);
} else if (nextItem.type === 'album' || nextItem.type === 'playlist') {
await this.downloadCollection(nextItem);
}
// Mark as completed
await updateQueueItem(nextItem.id, {
status: 'completed',
progress: 100
});
} catch (error) {
console.error(`[DeezerQueueManager] Error downloading ${nextItem.title}:`, error);
await updateQueueItem(nextItem.id, {
status: 'failed',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
// Clear current job
await setCurrentJob(null);
}
}
/**
* Download a single track
*/
private async downloadSingleTrack(item: QueueItem): Promise<void> {
const track = item.downloadObject as DeezerTrack;
const appSettings = get(settings);
if (!appSettings.musicFolder) {
throw new Error('Music folder not configured');
}
// Get user data for license token
const userData = await deezerAPI.getUserData();
const licenseToken = userData.USER?.OPTIONS?.license_token;
if (!licenseToken) {
throw new Error('License token not found');
}
// Get download URL
const downloadURL = await deezerAPI.getTrackDownloadUrl(
track.trackToken!,
appSettings.deezerFormat,
licenseToken
);
if (!downloadURL) {
throw new Error('Failed to get download URL');
}
// Update progress
await updateQueueItem(item.id, {
currentTrack: {
title: track.title,
artist: track.artist,
progress: 0
}
});
// Download the track
const filePath = await downloadTrack(
track,
downloadURL,
appSettings.musicFolder,
appSettings.deezerFormat,
(progress) => {
// Update progress in queue
updateQueueItem(item.id, {
progress: progress.percentage,
currentTrack: {
title: track.title,
artist: track.artist,
progress: progress.percentage
}
});
}
);
console.log(`[DeezerQueueManager] Downloaded: ${filePath}`);
}
/**
* Download a collection (album/playlist) with concurrency control
*/
private async downloadCollection(item: QueueItem): Promise<void> {
const tracks = item.downloadObject as DeezerTrack[];
const appSettings = get(settings);
const concurrency = appSettings.deezerConcurrency;
console.log(`[DeezerQueueManager] Downloading collection with concurrency: ${concurrency}`);
const results: (string | Error)[] = [];
let completedCount = 0;
let failedCount = 0;
// Simple concurrent queue implementation
const queue = [...tracks];
const running: Promise<void>[] = [];
const downloadNext = async (): Promise<void> => {
const track = queue.shift();
if (!track) return;
try {
await updateQueueItem(item.id, {
currentTrack: {
title: track.title,
artist: track.artist,
progress: 0
}
});
const userData = await deezerAPI.getUserData();
const licenseToken = userData.USER?.OPTIONS?.license_token;
if (!licenseToken) {
throw new Error('License token not found');
}
const downloadURL = await deezerAPI.getTrackDownloadUrl(
track.trackToken!,
appSettings.deezerFormat,
licenseToken
);
if (!downloadURL) {
throw new Error('Failed to get download URL');
}
const filePath = await downloadTrack(
track,
downloadURL,
appSettings.musicFolder!,
appSettings.deezerFormat
);
results.push(filePath);
completedCount++;
} catch (error) {
console.error(`[DeezerQueueManager] Error downloading track ${track.title}:`, error);
results.push(error as Error);
failedCount++;
}
// Update progress
const totalTracks = item.totalTracks;
const progress = ((completedCount + failedCount) / totalTracks) * 100;
await updateQueueItem(item.id, {
progress,
completedTracks: completedCount,
failedTracks: failedCount
});
// Continue with next track
if (queue.length > 0) {
await downloadNext();
}
};
// Start initial batch of concurrent downloads
for (let i = 0; i < Math.min(concurrency, tracks.length); i++) {
running.push(downloadNext());
}
// Wait for all downloads to complete
await Promise.all(running);
console.log(`[DeezerQueueManager] Collection complete: ${completedCount} succeeded, ${failedCount} failed`);
}
}
// Singleton instance
export const deezerQueueManager = new DeezerQueueManager();