mirror of
https://github.com/markuryy/shark.git
synced 2026-02-01 20:41:03 +00:00
234 lines
7.9 KiB
TypeScript
234 lines
7.9 KiB
TypeScript
/**
|
|
* 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
|
|
}
|
|
};
|
|
}
|