mirror of
https://github.com/markuryy/shark.git
synced 2026-07-28 07:41:02 +00:00
feat(spotify): hook existing download queue
This commit is contained in:
215
src/lib/services/spotify/converter.ts
Normal file
215
src/lib/services/spotify/converter.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Spotify to Deezer track conversion utilities
|
||||
* Matches Spotify tracks to Deezer tracks using ISRC codes (primary) or metadata search (fallback)
|
||||
*/
|
||||
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { deezerAPI } from '../deezer';
|
||||
|
||||
export interface SpotifyTrackInput {
|
||||
id: string;
|
||||
name: string;
|
||||
artists: string[];
|
||||
album: string;
|
||||
duration_ms: number;
|
||||
isrc?: string | null;
|
||||
}
|
||||
|
||||
export interface DeezerMatchResult {
|
||||
success: boolean;
|
||||
deezerTrack?: any;
|
||||
matchMethod?: 'isrc' | 'metadata' | 'none';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Deezer for a track by ISRC code
|
||||
* This is the primary and most reliable matching method
|
||||
*/
|
||||
export async function searchDeezerByISRC(isrc: string): Promise<any | null> {
|
||||
if (!isrc || isrc.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[Converter] Searching Deezer by ISRC: ${isrc}`);
|
||||
|
||||
const url = `https://api.deezer.com/2.0/track/isrc:${encodeURIComponent(isrc)}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
connectTimeout: 30000
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[Converter] ISRC search failed with status: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Check if we got an error response
|
||||
if (result.error) {
|
||||
console.warn(`[Converter] ISRC search returned error:`, result.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Valid track found
|
||||
if (result.id) {
|
||||
console.log(`[Converter] Found Deezer track by ISRC: ${result.id} - ${result.title}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[Converter] Error searching by ISRC:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Deezer for a track by metadata (title + artist)
|
||||
* Used as fallback when ISRC is not available or doesn't match
|
||||
*/
|
||||
export async function searchDeezerByMetadata(
|
||||
title: string,
|
||||
artist: string,
|
||||
durationMs?: number
|
||||
): Promise<any | null> {
|
||||
try {
|
||||
// Build search query: "artist title"
|
||||
const query = `${artist} ${title}`.trim();
|
||||
console.log(`[Converter] Searching Deezer by metadata: "${query}"`);
|
||||
|
||||
const searchResults = await deezerAPI.searchTracks(query, 10);
|
||||
|
||||
if (!searchResults.data || searchResults.data.length === 0) {
|
||||
console.warn(`[Converter] No results found for: "${query}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find best match
|
||||
// Priority: exact title match, then duration match (±2 seconds)
|
||||
const durationSec = durationMs ? Math.floor(durationMs / 1000) : undefined;
|
||||
|
||||
for (const track of searchResults.data) {
|
||||
// Check title similarity (case-insensitive)
|
||||
const titleMatch = track.title.toLowerCase() === title.toLowerCase();
|
||||
|
||||
// Check duration if available (within 2 seconds tolerance)
|
||||
const durationMatch = !durationSec || Math.abs(track.duration - durationSec) <= 2;
|
||||
|
||||
if (titleMatch && durationMatch) {
|
||||
console.log(`[Converter] Found exact match by metadata: ${track.id} - ${track.title}`);
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match, return first result as best guess
|
||||
const firstResult = searchResults.data[0];
|
||||
console.log(`[Converter] Using first result as best match: ${firstResult.id} - ${firstResult.title}`);
|
||||
return firstResult;
|
||||
} catch (error) {
|
||||
console.error('[Converter] Error searching by metadata:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Spotify track to Deezer track ID
|
||||
* Uses ISRC matching first, falls back to metadata search
|
||||
*/
|
||||
export async function convertSpotifyTrackToDeezer(
|
||||
spotifyTrack: SpotifyTrackInput
|
||||
): Promise<DeezerMatchResult> {
|
||||
console.log(`[Converter] Converting Spotify track: ${spotifyTrack.name} by ${spotifyTrack.artists.join(', ')}`);
|
||||
|
||||
// Try ISRC matching first (most reliable)
|
||||
if (spotifyTrack.isrc) {
|
||||
const deezerTrack = await searchDeezerByISRC(spotifyTrack.isrc);
|
||||
if (deezerTrack) {
|
||||
return {
|
||||
success: true,
|
||||
deezerTrack,
|
||||
matchMethod: 'isrc'
|
||||
};
|
||||
}
|
||||
console.log(`[Converter] ISRC match failed for: ${spotifyTrack.isrc}`);
|
||||
}
|
||||
|
||||
// Fallback to metadata search
|
||||
const artist = spotifyTrack.artists[0] || 'Unknown';
|
||||
const deezerTrack = await searchDeezerByMetadata(
|
||||
spotifyTrack.name,
|
||||
artist,
|
||||
spotifyTrack.duration_ms
|
||||
);
|
||||
|
||||
if (deezerTrack) {
|
||||
return {
|
||||
success: true,
|
||||
deezerTrack,
|
||||
matchMethod: 'metadata'
|
||||
};
|
||||
}
|
||||
|
||||
// No match found
|
||||
console.warn(`[Converter] Could not find Deezer match for: ${spotifyTrack.name} by ${artist}`);
|
||||
return {
|
||||
success: false,
|
||||
matchMethod: 'none',
|
||||
error: 'No match found on Deezer'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert multiple Spotify tracks to Deezer track IDs
|
||||
* Returns both successful conversions and failed tracks
|
||||
*/
|
||||
export async function convertSpotifyTracksBatch(
|
||||
spotifyTracks: SpotifyTrackInput[]
|
||||
): Promise<{
|
||||
conversions: Array<{ spotifyId: string; deezerId: string; matchMethod: string }>;
|
||||
failures: Array<{ spotifyId: string; name: string; artist: string; error: string }>;
|
||||
}> {
|
||||
const conversions: Array<{ spotifyId: string; deezerId: string; matchMethod: string }> = [];
|
||||
const failures: Array<{ spotifyId: string; name: string; artist: string; error: string }> = [];
|
||||
|
||||
console.log(`[Converter] Converting ${spotifyTracks.length} Spotify tracks to Deezer...`);
|
||||
|
||||
for (const track of spotifyTracks) {
|
||||
try {
|
||||
const result = await convertSpotifyTrackToDeezer(track);
|
||||
|
||||
if (result.success && result.deezerTrack) {
|
||||
conversions.push({
|
||||
spotifyId: track.id,
|
||||
deezerId: result.deezerTrack.id.toString(),
|
||||
matchMethod: result.matchMethod || 'unknown'
|
||||
});
|
||||
} else {
|
||||
failures.push({
|
||||
spotifyId: track.id,
|
||||
name: track.name,
|
||||
artist: track.artists[0] || 'Unknown',
|
||||
error: result.error || 'Unknown error'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Converter] Error converting track ${track.name}:`, error);
|
||||
failures.push({
|
||||
spotifyId: track.id,
|
||||
name: track.name,
|
||||
artist: track.artists[0] || 'Unknown',
|
||||
error: error instanceof Error ? error.message : 'Conversion error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Converter] Conversion complete: ${conversions.length} successful, ${failures.length} failed`);
|
||||
|
||||
return { conversions, failures };
|
||||
}
|
||||
Reference in New Issue
Block a user