Files
shark/src/lib/services/spotify/converter.ts

216 lines
6.3 KiB
TypeScript

/**
* 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 };
}