/** * Download Spotify playlist - converts tracks to Deezer via ISRC, adds to queue, and creates m3u8 file */ import { addToQueue } from '$lib/stores/downloadQueue'; import { trackExists } from '$lib/services/deezer/downloader'; import { writeM3U8, makeRelativePath, type M3U8Track } from '$lib/library/m3u8'; import { generateTrackPath } from '$lib/services/deezer/paths'; import { settings } from '$lib/stores/settings'; import { deezerAuth } from '$lib/stores/deezer'; import { deezerAPI } from '$lib/services/deezer'; import { setInfo, setSuccess, setWarning } from '$lib/stores/status'; import { get } from 'svelte/store'; import { mkdir } from '@tauri-apps/plugin-fs'; import { convertSpotifyTrackToDeezer, type SpotifyTrackInput } from './converter'; import type { DeezerTrack } from '$lib/types/deezer'; export interface SpotifyPlaylistTrack { id: number | string; track_id: string; name: string; artist_name: string; album_name: string; duration_ms: number; isrc?: string | null; } /** * Download a Spotify playlist by converting tracks to Deezer equivalents * - Converts all tracks via ISRC matching * - Adds converted tracks to the download queue (respects overwrite setting) * - Creates an m3u8 playlist file with relative paths * * @param playlistName - Name of the playlist * @param spotifyTracks - Array of Spotify track objects * @param playlistsFolder - Path to playlists folder * @param musicFolder - Path to music folder * @returns Object with m3u8 path and statistics */ export async function downloadSpotifyPlaylist( playlistName: string, spotifyTracks: SpotifyPlaylistTrack[], playlistsFolder: string, musicFolder: string ): Promise<{ m3u8Path: string; stats: { total: number; queued: number; skipped: number; failed: number; }; }> { const appSettings = get(settings); const authState = get(deezerAuth); // Ensure Deezer is authenticated if (!authState.loggedIn || !authState.arl) { throw new Error('Deezer authentication required for downloads'); } deezerAPI.setArl(authState.arl); console.log(`[SpotifyPlaylistDownloader] Starting download for playlist: ${playlistName}`); console.log(`[SpotifyPlaylistDownloader] Tracks: ${spotifyTracks.length}`); // Ensure playlists folder exists try { await mkdir(playlistsFolder, { recursive: true }); } catch (error) { // Folder might already exist } // Track statistics let queuedCount = 0; let skippedCount = 0; let failedCount = 0; // Track successful conversions for m3u8 generation const successfulTracks: Array<{ deezerTrack: DeezerTrack; spotifyTrack: SpotifyPlaylistTrack; }> = []; // Convert and queue each track for (const spotifyTrack of spotifyTracks) { try { // Convert Spotify track to Deezer const conversionInput: SpotifyTrackInput = { id: spotifyTrack.track_id, name: spotifyTrack.name, artists: [spotifyTrack.artist_name], album: spotifyTrack.album_name, duration_ms: spotifyTrack.duration_ms, isrc: spotifyTrack.isrc }; const conversionResult = await convertSpotifyTrackToDeezer(conversionInput); if (!conversionResult.success || !conversionResult.deezerTrack) { console.warn( `[SpotifyPlaylistDownloader] Failed to convert: ${spotifyTrack.name} - ${conversionResult.error}` ); failedCount++; continue; } const deezerPublicTrack = conversionResult.deezerTrack; // Fetch full track data from Deezer GW API (needed for download) const deezerTrackId = deezerPublicTrack.id.toString(); const deezerFullTrack = await deezerAPI.getTrack(deezerTrackId); if (!deezerFullTrack || !deezerFullTrack.SNG_ID) { console.warn(`[SpotifyPlaylistDownloader] Could not fetch full Deezer track data for ID: ${deezerTrackId}`); failedCount++; continue; } // Build DeezerTrack object const deezerTrack: DeezerTrack = { id: parseInt(deezerFullTrack.SNG_ID, 10), title: deezerFullTrack.SNG_TITLE, artist: deezerFullTrack.ART_NAME, artistId: parseInt(deezerFullTrack.ART_ID, 10), artists: [deezerFullTrack.ART_NAME], album: deezerFullTrack.ALB_TITLE, albumId: parseInt(deezerFullTrack.ALB_ID, 10), albumArtist: deezerFullTrack.ART_NAME, albumArtistId: parseInt(deezerFullTrack.ART_ID, 10), trackNumber: typeof deezerFullTrack.TRACK_NUMBER === 'number' ? deezerFullTrack.TRACK_NUMBER : parseInt(deezerFullTrack.TRACK_NUMBER, 10), discNumber: typeof deezerFullTrack.DISK_NUMBER === 'number' ? deezerFullTrack.DISK_NUMBER : parseInt(deezerFullTrack.DISK_NUMBER, 10), duration: typeof deezerFullTrack.DURATION === 'number' ? deezerFullTrack.DURATION : parseInt(deezerFullTrack.DURATION, 10), explicit: deezerFullTrack.EXPLICIT_LYRICS === 1, md5Origin: deezerFullTrack.MD5_ORIGIN, mediaVersion: deezerFullTrack.MEDIA_VERSION, trackToken: deezerFullTrack.TRACK_TOKEN }; // Check if track already exists (if overwrite is disabled) if (!appSettings.deezerOverwrite && appSettings.musicFolder) { const exists = await trackExists(deezerTrack, appSettings.musicFolder, appSettings.deezerFormat); if (exists) { console.log(`[SpotifyPlaylistDownloader] Skipping "${deezerTrack.title}" - already exists`); skippedCount++; // Still add to successful tracks for m3u8 generation successfulTracks.push({ deezerTrack, spotifyTrack }); continue; } } // Queue track for download await addToQueue({ source: 'deezer', type: 'track', title: deezerTrack.title, artist: deezerTrack.artist, totalTracks: 1, downloadObject: deezerTrack }); queuedCount++; successfulTracks.push({ deezerTrack, spotifyTrack }); console.log( `[SpotifyPlaylistDownloader] Queued: ${deezerTrack.title} (matched via ${conversionResult.matchMethod})` ); } catch (error) { console.error(`[SpotifyPlaylistDownloader] Error processing track ${spotifyTrack.name}:`, error); failedCount++; } } console.log( `[SpotifyPlaylistDownloader] Queued ${queuedCount} tracks, skipped ${skippedCount}, failed ${failedCount}` ); // Show queue status message if (queuedCount > 0) { const parts = [`Queued ${queuedCount} track${queuedCount !== 1 ? 's' : ''}`]; if (skippedCount > 0) parts.push(`${skippedCount} skipped`); if (failedCount > 0) parts.push(`${failedCount} not found`); setInfo(parts.join(', ')); } else if (skippedCount > 0) { setWarning(`All ${skippedCount} tracks already exist`); } else if (failedCount > 0) { setWarning(`Could not find ${failedCount} tracks on Deezer`); } // Generate m3u8 file using Deezer track paths const m3u8Tracks: M3U8Track[] = successfulTracks.map(({ deezerTrack, spotifyTrack }) => { // Generate expected path for this Deezer track const paths = generateTrackPath(deezerTrack, musicFolder, appSettings.deezerFormat, false); const absolutePath = `${paths.filepath}/${paths.filename}`; // Convert to relative path from playlists folder const relativePath = makeRelativePath(absolutePath, 'Music'); return { duration: deezerTrack.duration, artist: deezerTrack.artist, title: deezerTrack.title, path: relativePath }; }); // Write m3u8 file const m3u8Path = await writeM3U8(playlistName, m3u8Tracks, playlistsFolder); console.log(`[SpotifyPlaylistDownloader] Playlist saved to: ${m3u8Path}`); // Show success message for playlist creation setSuccess(`Playlist created: ${playlistName} (${successfulTracks.length} tracks)`); return { m3u8Path, stats: { total: spotifyTracks.length, queued: queuedCount, skipped: skippedCount, failed: failedCount } }; }