//utils.js export const QUALITY = 'LOSSLESS'; export const REPEAT_MODE = { OFF: 0, ALL: 1, ONE: 2 }; export const AUDIO_QUALITIES = { HI_RES_LOSSLESS: 'HI_RES_LOSSLESS', LOSSLESS: 'LOSSLESS', HIGH: 'HIGH', LOW: 'LOW' }; export const QUALITY_PRIORITY = ['HI_RES_LOSSLESS', 'LOSSLESS', 'HIGH', 'LOW']; export const QUALITY_TOKENS = { HI_RES_LOSSLESS: ['HI_RES_LOSSLESS', 'HIRES_LOSSLESS', 'HIRESLOSSLESS', 'HIFI_PLUS', 'HI_RES_FLAC', 'HI_RES', 'HIRES', 'MASTER', 'MASTER_QUALITY', 'MQA'], LOSSLESS: ['LOSSLESS', 'HIFI'], HIGH: ['HIGH', 'HIGH_QUALITY'], LOW: ['LOW', 'LOW_QUALITY'] }; export const RATE_LIMIT_ERROR_MESSAGE = 'Too Many Requests. Please wait a moment and try again.'; export const SVG_PLAY = ''; export const SVG_PAUSE = ''; export const SVG_VOLUME = ''; export const SVG_MUTE = ''; export const formatTime = (seconds) => { if (isNaN(seconds)) return '0:00'; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${String(s).padStart(2, '0')}`; }; export const createPlaceholder = (text, isLoading = false) => { return `
${text}
`; }; export const trackDataStore = new WeakMap(); export const sanitizeForFilename = (value) => { if (!value) return 'Unknown'; return value .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .trim(); }; export const getExtensionForQuality = (quality) => { switch (quality) { case 'LOW': case 'HIGH': return 'm4a'; default: return 'flac'; } }; export const buildTrackFilename = (track, quality) => { const extension = getExtensionForQuality(quality); const trackNumber = Number(track.trackNumber); const padded = Number.isFinite(trackNumber) && trackNumber > 0 ? `${trackNumber}`.padStart(2, '0') : '00'; const artistName = sanitizeForFilename(track.artist?.name); const albumTitle = sanitizeForFilename(track.album?.title); const trackTitle = sanitizeForFilename(getTrackTitle(track)); return `${artistName} - ${albumTitle} - ${padded} ${trackTitle}.${extension}`; }; const sanitizeToken = (value) => { if (!value) return ''; return value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, '_'); }; export const normalizeQualityToken = (value) => { if (!value) return null; const token = sanitizeToken(value); for (const [quality, aliases] of Object.entries(QUALITY_TOKENS)) { if (aliases.includes(token)) { return quality; } } return null; }; export const deriveQualityFromTags = (rawTags) => { if (!Array.isArray(rawTags)) return null; const candidates = []; for (const tag of rawTags) { if (typeof tag !== 'string') continue; const normalized = normalizeQualityToken(tag); if (normalized && !candidates.includes(normalized)) { candidates.push(normalized); } } return pickBestQuality(candidates); }; export const pickBestQuality = (candidates) => { let best = null; let bestRank = Infinity; for (const candidate of candidates) { if (!candidate) continue; const rank = QUALITY_PRIORITY.indexOf(candidate); const currentRank = rank === -1 ? Infinity : rank; if (currentRank < bestRank) { best = candidate; bestRank = currentRank; } } return best; }; export const deriveTrackQuality = (track) => { if (!track) return null; const candidates = [ deriveQualityFromTags(track.mediaMetadata?.tags), deriveQualityFromTags(track.album?.mediaMetadata?.tags), normalizeQualityToken(track.audioQuality) ]; return pickBestQuality(candidates); }; export const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); export const hasExplicitContent = (item) => { return item?.explicit === true || item?.explicitLyrics === true; }; export const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; export const getTrackTitle = (track, { fallback = 'Unknown Title' } = {}) => { if (!track?.title) return fallback; return track?.version ? `${track.title} (${track.version})` : track.title; };