import { LyricLine, TrackData } from '../types'; export const parseLrc = (lrcContent: string): LyricLine[] => { if (!lrcContent) return []; const lines = lrcContent.split('\n'); const lyrics: LyricLine[] = []; const timeRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/; lines.forEach((line) => { const match = timeRegex.exec(line); if (match) { const minutes = parseInt(match[1], 10); const seconds = parseInt(match[2], 10); const milliseconds = parseInt(match[3], 10); // Calculate total time in seconds const time = minutes * 60 + seconds + milliseconds / (match[3].length === 3 ? 1000 : 100); const text = line.replace(timeRegex, '').trim(); if (text) { lyrics.push({ time, text }); } } }); return lyrics.sort((a, b) => a.time - b.time); }; export const formatTime = (seconds: number): string => { if (isNaN(seconds)) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; // Helper to get value from multiple potential keys (case-insensitive) const getValue = (item: any, keys: string[]) => { for (const key of keys) { if (item[key] !== undefined && item[key] !== null && item[key] !== "") return item[key]; const lowerKey = key.toLowerCase(); if (item[lowerKey] !== undefined && item[lowerKey] !== null && item[lowerKey] !== "") return item[lowerKey]; } return ""; }; // Helper to resolve relative URLs against the playlist URL const resolveUrl = (url: string, baseUrl: string) => { if (!url) return ""; try { // If url is absolute, this ignores baseUrl return new URL(url, baseUrl).href; } catch (e) { return url; } }; export const fetchPlaylist = async (url: string): Promise => { try { let response; let fetchError; // 1. Try direct fetch first try { response = await fetch(url); if (!response.ok) throw new Error('Direct fetch status not ok'); } catch (e) { console.warn("Direct fetch failed, attempting proxy fallback 1.", e); fetchError = e; response = undefined; } // 2. Try AllOrigins Proxy if direct failed if (!response || !response.ok) { try { // allorigins.win is good for bypassing CORS and Mixed Content const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`; response = await fetch(proxyUrl); if (!response.ok) throw new Error('AllOrigins status not ok'); } catch (e) { console.warn("AllOrigins proxy failed, attempting proxy fallback 2.", e); fetchError = e; response = undefined; } } // 3. Try CORS Proxy IO as last resort if (!response || !response.ok) { try { // corsproxy.io const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(url)}`; response = await fetch(proxyUrl); if (!response.ok) throw new Error('CorsProxyIO status not ok'); } catch(e) { console.warn("CorsProxyIO failed.", e); fetchError = e; response = undefined; } } if (!response || !response.ok) { console.error("All fetch attempts failed."); throw fetchError || new Error('Failed to fetch playlist'); } const data = await response.json(); // Map the external JSON format to our internal TrackData interface return data.map((item: any) => { // Use helper to find keys even if casing differs or alternative names are used let coverUrl = getValue(item, ['Cover', 'img', 'image', 'pic', 'poster']); let audioUrl = getValue(item, ['Audio', 'src', 'url', 'mp3', 'sound']); let lyricsSource = getValue(item, ['Lyrics', 'lrc']); const title = getValue(item, ['Title', 'name', 'song']); const artist = getValue(item, ['Artist', 'author', 'singer']); // Resolve relative URLs (if the JSON contains filenames instead of full URLs) coverUrl = resolveUrl(coverUrl, url); audioUrl = resolveUrl(audioUrl, url); lyricsSource = resolveUrl(lyricsSource, url); // Fix Mixed Content for Cover: Route HTTP images through Weserv proxy if (coverUrl && coverUrl.startsWith('http:')) { coverUrl = `https://images.weserv.nl/?url=${encodeURIComponent(coverUrl)}`; } // Fix Mixed Content for Audio if (audioUrl && audioUrl.startsWith('http:')) { // Strategy: Prefer native HTTPS upgrade if possible, otherwise proxy. // Qiniu (clouddn.com) and other major CDNs usually support HTTPS. if (audioUrl.includes('.clouddn.com') || audioUrl.includes('aliyuncs.com')) { audioUrl = audioUrl.replace('http:', 'https:'); } else { // For others, use a proxy that supports binary streaming. // corsproxy.io is usually good. audioUrl = `https://corsproxy.io/?${encodeURIComponent(audioUrl)}`; } } return { title: title || "Unknown Title", artist: artist || "Unknown Artist", coverUrl: coverUrl, audioUrl: audioUrl, lyrics: [], lyricsSource: lyricsSource }; }); } catch (error) { console.error("Error fetching playlist:", error); return []; } };