mirror of
https://github.com/markuryy/shark.git
synced 2026-02-01 20:41:03 +00:00
feat(spotify): hook existing download queue
This commit is contained in:
233
src/lib/services/spotify/playlistDownloader.ts
Normal file
233
src/lib/services/spotify/playlistDownloader.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user