mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 12:01:01 +00:00
feat(downloads): add download queue management and UI
This commit is contained in:
257
src/lib/services/deezer/queueManager.ts
Normal file
257
src/lib/services/deezer/queueManager.ts
Normal 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();
|
||||
Reference in New Issue
Block a user