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