nex_music/utils/parsers.ts

152 lines
5.3 KiB
TypeScript

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<TrackData[]> => {
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 [];
}
};