mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 12:01:01 +00:00
feat(dz): add playlist download, existence check, and improved queue handling
Add ability to download entire playlists as M3U8 files, with UI integration and per-track download actions. Implement track existence checking to avoid duplicate downloads, respecting the overwrite setting. Improve queue manager to sync downloaded tracks to the library incrementally. Refactor playlist parsing and metadata reading to use the Rust backend for better performance and accuracy. Update UI to reflect track existence and download status in playlist views. BREAKING CHANGE: Deezer playlist and track download logic now relies on Rust backend for metadata and new existence checking; some APIs and internal behaviors have changed.
This commit is contained in:
@@ -412,7 +412,8 @@ export class DeezerAPI {
|
||||
console.log('[DEBUG] Getting track download URL...', { trackToken, format, licenseToken });
|
||||
|
||||
try {
|
||||
const cookieHeader = this.getCookieHeader();
|
||||
// media.deezer.com ONLY needs arl cookie, not sid or other cookies
|
||||
const cookieHeader = this.arl ? `arl=${this.arl}` : '';
|
||||
console.log('[DEBUG] Sending request to media.deezer.com with cookies:', cookieHeader);
|
||||
|
||||
const response = await fetch('https://media.deezer.com/v1/get_url', {
|
||||
|
||||
@@ -5,13 +5,24 @@
|
||||
|
||||
import { deezerAPI } from '$lib/services/deezer';
|
||||
import { addToQueue } from '$lib/stores/downloadQueue';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
import { deezerAuth } from '$lib/stores/deezer';
|
||||
import { trackExists } from './downloader';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
/**
|
||||
* Fetch track metadata and add to download queue
|
||||
* Respects the overwrite setting - skips tracks that already exist if overwrite is false
|
||||
* @param trackId - Deezer track ID
|
||||
* @returns Promise that resolves when track is added to queue
|
||||
* @returns Promise that resolves when track is added to queue (or skipped message)
|
||||
*/
|
||||
export async function addDeezerTrackToQueue(trackId: string): Promise<void> {
|
||||
export async function addDeezerTrackToQueue(trackId: string): Promise<{ added: boolean; reason?: string }> {
|
||||
// Ensure ARL is set for authentication
|
||||
const authState = get(deezerAuth);
|
||||
if (authState.arl) {
|
||||
deezerAPI.setArl(authState.arl);
|
||||
}
|
||||
|
||||
// Fetch full track data from GW API
|
||||
const trackInfo = await deezerAPI.getTrack(trackId);
|
||||
|
||||
@@ -66,9 +77,9 @@ export async function addDeezerTrackToQueue(trackId: string): Promise<void> {
|
||||
albumId: trackInfo.ALB_ID,
|
||||
albumArtist: trackInfo.ART_NAME,
|
||||
albumArtistId: trackInfo.ART_ID,
|
||||
trackNumber: trackInfo.TRACK_NUMBER || 1,
|
||||
discNumber: trackInfo.DISK_NUMBER || 1,
|
||||
duration: trackInfo.DURATION,
|
||||
trackNumber: typeof trackInfo.TRACK_NUMBER === 'number' ? trackInfo.TRACK_NUMBER : parseInt(trackInfo.TRACK_NUMBER, 10),
|
||||
discNumber: typeof trackInfo.DISK_NUMBER === 'number' ? trackInfo.DISK_NUMBER : parseInt(trackInfo.DISK_NUMBER, 10),
|
||||
duration: typeof trackInfo.DURATION === 'number' ? trackInfo.DURATION : parseInt(trackInfo.DURATION, 10),
|
||||
explicit: trackInfo.EXPLICIT_LYRICS === 1,
|
||||
md5Origin: trackInfo.MD5_ORIGIN,
|
||||
mediaVersion: trackInfo.MEDIA_VERSION,
|
||||
@@ -84,6 +95,18 @@ export async function addDeezerTrackToQueue(trackId: string): Promise<void> {
|
||||
copyright: trackInfo.COPYRIGHT
|
||||
};
|
||||
|
||||
// Check if we should skip this track (if it exists and overwrite is false)
|
||||
const appSettings = get(settings);
|
||||
|
||||
if (!appSettings.deezerOverwrite && appSettings.musicFolder) {
|
||||
const exists = await trackExists(track, appSettings.musicFolder, appSettings.deezerFormat);
|
||||
|
||||
if (exists) {
|
||||
console.log(`[AddToQueue] Skipping "${track.title}" - already exists`);
|
||||
return { added: false, reason: 'already_exists' };
|
||||
}
|
||||
}
|
||||
|
||||
// Add to queue (queue manager runs continuously in background)
|
||||
await addToQueue({
|
||||
source: 'deezer',
|
||||
@@ -93,4 +116,6 @@ export async function addDeezerTrackToQueue(trackId: string): Promise<void> {
|
||||
totalTracks: 1,
|
||||
downloadObject: track
|
||||
});
|
||||
|
||||
return { added: true };
|
||||
}
|
||||
|
||||
@@ -124,18 +124,13 @@ export async function downloadTrack(
|
||||
|
||||
// Apply tags (works for both MP3 and FLAC)
|
||||
console.log('Tagging audio file...');
|
||||
try {
|
||||
await tagAudioFile(
|
||||
finalPath,
|
||||
track,
|
||||
appSettings.embedCoverArt ? coverData : undefined,
|
||||
appSettings.embedLyrics
|
||||
);
|
||||
console.log('Tagging complete!');
|
||||
} catch (error) {
|
||||
console.error('Failed to tag audio file:', error);
|
||||
// Non-fatal error - file is still downloaded, just not tagged
|
||||
}
|
||||
await tagAudioFile(
|
||||
finalPath,
|
||||
track,
|
||||
appSettings.embedCoverArt ? coverData : undefined,
|
||||
appSettings.embedLyrics
|
||||
);
|
||||
console.log('Tagging complete!');
|
||||
|
||||
// Save LRC sidecar file if enabled
|
||||
if (appSettings.saveLrcFile && track.lyrics?.sync) {
|
||||
|
||||
93
src/lib/services/deezer/playlistDownloader.ts
Normal file
93
src/lib/services/deezer/playlistDownloader.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Download Deezer playlist - adds tracks to queue and creates m3u8 file
|
||||
*/
|
||||
|
||||
import { addDeezerTrackToQueue } from './addToQueue';
|
||||
import { writeM3U8, makeRelativePath, type M3U8Track } from '$lib/library/m3u8';
|
||||
import { generateTrackPath } from './paths';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
import { get } from 'svelte/store';
|
||||
import type { DeezerTrack } from '$lib/types/deezer';
|
||||
import { mkdir } from '@tauri-apps/plugin-fs';
|
||||
|
||||
/**
|
||||
* Download a Deezer playlist
|
||||
* - Adds all tracks to the download queue (respects overwrite setting)
|
||||
* - Creates an m3u8 playlist file with relative paths
|
||||
*
|
||||
* @param playlistName - Name of the playlist
|
||||
* @param tracks - Array of DeezerTrack objects
|
||||
* @param playlistsFolder - Path to playlists folder
|
||||
* @param musicFolder - Path to music folder
|
||||
* @returns Path to created m3u8 file
|
||||
*/
|
||||
export async function downloadDeezerPlaylist(
|
||||
playlistName: string,
|
||||
tracks: DeezerTrack[],
|
||||
playlistsFolder: string,
|
||||
musicFolder: string
|
||||
): Promise<string> {
|
||||
const appSettings = get(settings);
|
||||
|
||||
console.log(`[PlaylistDownloader] Starting download for playlist: ${playlistName}`);
|
||||
console.log(`[PlaylistDownloader] Tracks: ${tracks.length}`);
|
||||
|
||||
// Ensure playlists folder exists
|
||||
try {
|
||||
await mkdir(playlistsFolder, { recursive: true });
|
||||
} catch (error) {
|
||||
// Folder might already exist
|
||||
}
|
||||
|
||||
// Add all tracks to download queue
|
||||
// Note: Tracks from cache don't have md5Origin/mediaVersion/trackToken needed for download
|
||||
// So we need to call addDeezerTrackToQueue which fetches full data from API
|
||||
// We add a small delay between requests to avoid rate limiting
|
||||
let addedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
try {
|
||||
const result = await addDeezerTrackToQueue(track.id.toString());
|
||||
if (result.added) {
|
||||
addedCount++;
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
|
||||
// Add delay between requests to avoid rate limiting (except after last track)
|
||||
if (i < tracks.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[PlaylistDownloader] Error adding track ${track.title}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PlaylistDownloader] Added ${addedCount} tracks to queue, skipped ${skippedCount}`);
|
||||
|
||||
// Generate m3u8 file
|
||||
const m3u8Tracks: M3U8Track[] = tracks.map(track => {
|
||||
// Generate expected path for this track
|
||||
const paths = generateTrackPath(track, musicFolder, appSettings.deezerFormat, false);
|
||||
const absolutePath = `${paths.filepath}/${paths.filename}`;
|
||||
|
||||
// Convert to relative path from playlists folder
|
||||
const relativePath = makeRelativePath(absolutePath, 'Music');
|
||||
|
||||
return {
|
||||
duration: track.duration,
|
||||
artist: track.artist,
|
||||
title: track.title,
|
||||
path: relativePath
|
||||
};
|
||||
});
|
||||
|
||||
// Write m3u8 file
|
||||
const m3u8Path = await writeM3U8(playlistName, m3u8Tracks, playlistsFolder);
|
||||
|
||||
console.log(`[PlaylistDownloader] Playlist saved to: ${m3u8Path}`);
|
||||
|
||||
return m3u8Path;
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
type QueueItem
|
||||
} from '$lib/stores/downloadQueue';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
import { deezerAuth } from '$lib/stores/deezer';
|
||||
import { syncTrackPaths } from '$lib/library/incrementalSync';
|
||||
import { get } from 'svelte/store';
|
||||
import type { DeezerTrack } from '$lib/types/deezer';
|
||||
|
||||
@@ -121,6 +123,13 @@ export class DeezerQueueManager {
|
||||
throw new Error('Music folder not configured');
|
||||
}
|
||||
|
||||
// Set ARL for authentication
|
||||
const authState = get(deezerAuth);
|
||||
if (!authState.arl) {
|
||||
throw new Error('Deezer ARL not found - please log in');
|
||||
}
|
||||
deezerAPI.setArl(authState.arl);
|
||||
|
||||
// Get user data for license token
|
||||
const userData = await deezerAPI.getUserData();
|
||||
const licenseToken = userData.USER?.OPTIONS?.license_token;
|
||||
@@ -169,6 +178,14 @@ export class DeezerQueueManager {
|
||||
);
|
||||
|
||||
console.log(`[DeezerQueueManager] Downloaded: ${filePath}`);
|
||||
|
||||
// Trigger incremental library sync for this track
|
||||
try {
|
||||
await syncTrackPaths([filePath]);
|
||||
} catch (error) {
|
||||
console.error('[DeezerQueueManager] Error syncing track to library:', error);
|
||||
// Non-fatal - track is downloaded, just not in database yet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +276,18 @@ export class DeezerQueueManager {
|
||||
await Promise.all(running);
|
||||
|
||||
console.log(`[DeezerQueueManager] Collection complete: ${completedCount} succeeded, ${failedCount} failed`);
|
||||
|
||||
// Trigger incremental library sync for all successfully downloaded tracks
|
||||
if (completedCount > 0) {
|
||||
try {
|
||||
const successfulPaths = results.filter(r => typeof r === 'string') as string[];
|
||||
await syncTrackPaths(successfulPaths);
|
||||
console.log(`[DeezerQueueManager] Synced ${successfulPaths.length} tracks to library`);
|
||||
} catch (error) {
|
||||
console.error('[DeezerQueueManager] Error syncing collection to library:', error);
|
||||
// Non-fatal - tracks are downloaded, just not in database yet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user