/** * [Subsonics Chopin - Frontend] * File: adminFormat.js * Description: Fonctions de formatage de l'interface d'administration (toutes protégées contre NaN) */ export function safeNum(value, fallback = 0) { const n = Number(value); return Number.isFinite(n) ? n : fallback; } // 1536 -> "1.5 Ko" export function formatBytes(bytes) { let value = safeNum(bytes); const units = ["o", "Ko", "Mo", "Go", "To"]; let i = 0; while (value >= 1024 && i < units.length - 1) { value /= 1024; i++; } return `${i === 0 ? value : value.toFixed(1)} ${units[i]}`; } // Durée longue en secondes -> "2j 03h 04m" / "3h 05m 12s" / "45s" export function formatUptime(seconds) { let s = Math.max(0, Math.floor(safeNum(seconds))); const d = Math.floor(s / 86400); s -= d * 86400; const h = Math.floor(s / 3600); s -= h * 3600; const m = Math.floor(s / 60); s -= m * 60; const pad = (n) => String(n).padStart(2, "0"); if (d > 0) return `${d}j ${pad(h)}h ${pad(m)}m`; if (h > 0) return `${h}h ${pad(m)}m ${pad(s)}s`; if (m > 0) return `${m}m ${pad(s)}s`; return `${s}s`; } // Position en millisecondes -> "3:05" / "1:02:05" export function formatClock(ms) { const total = Math.max(0, Math.floor(safeNum(ms) / 1000)); const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; const pad = (n) => String(n).padStart(2, "0"); return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`; } function toDate(value) { if (value === null || value === undefined || value === "") return null; const date = new Date(typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value); return isNaN(date.getTime()) ? null : date; } // Date ISO / timestamp -> "06/10/2026 21:31:11" export function formatDate(value) { const date = toDate(value); if (!date) return "Inconnue"; return date.toLocaleString("fr-FR"); } // Date ISO / timestamp -> "il y a 5 min" / "dans 3 min" export function formatRelative(value, now = Date.now()) { const date = toDate(value); if (!date) return "Jamais"; const diff = Math.round((date.getTime() - now) / 1000); const abs = Math.abs(diff); let text; if (abs < 5) return "à l'instant"; if (abs < 60) text = `${abs} s`; else if (abs < 3600) text = `${Math.floor(abs / 60)} min`; else if (abs < 86400) text = `${Math.floor(abs / 3600)} h`; else text = `${Math.floor(abs / 86400)} j`; return diff < 0 ? `il y a ${text}` : `dans ${text}`; } // Statut d'un player (état v2) -> libellé + couleur de Tag export function playerStatus(status) { switch (status) { case 'playing': return { label: "En lecture", color: "var(--text-success)", icon: "fa-solid fa-play" }; case 'paused': return { label: "En pause", color: "var(--text-warning)", icon: "fa-solid fa-pause" }; case 'loading': return { label: "Chargement", color: "var(--admin-color)", icon: "fa-solid fa-spinner" }; case 'stalled': return { label: "Bloqué", color: "var(--text-error)", icon: "fa-solid fa-triangle-exclamation" }; case 'idle': return { label: "Inactif", color: "var(--text-secondary)", icon: "fa-solid fa-circle" }; case 'offline': return { label: "Hors ligne", color: "var(--text-error)", icon: "fa-solid fa-circle" }; default: return { label: status || "Inconnu", color: "var(--text-secondary)", icon: "fa-solid fa-circle" }; } } // Avatar Discord d'un utilisateur (repli sur l'avatar par défaut si absent) export function userAvatar(user) { if (user?.avatar) return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}`; let index = 0; try { index = Number((BigInt(user?.id || 0) >> BigInt(22)) % BigInt(6)); } catch (e) { index = 0; } return `https://cdn.discordapp.com/embed/avatars/${index}.png`; } // Classes d'erreur yt-dlp (backend/src/youtube/errors.js) -> libellé français export const YOUTUBE_CLASSES = { BOT_CHECK: "Vérification anti-robot", IP_BLOCKED: "IP bloquée par YouTube", CAPTCHA: "Captcha demandé", RATE_LIMITED: "Limite de requêtes YouTube", HTTP_403: "Erreur 403 (accès refusé)", HTTP_429: "Erreur 429 (trop de requêtes)", AGE_RESTRICTED: "Restriction d'âge", LOGIN_REQUIRED: "Connexion requise", MEMBERS_ONLY: "Réservé aux membres", PRIVATE: "Vidéo privée", UNAVAILABLE: "Vidéo indisponible", GEO_BLOCKED: "Bloquée dans ce pays", POT_MISSING: "PO token manquant", SABR: "Flux SABR uniquement", JS_CHALLENGE: "Défi JavaScript non résolu", FORMAT_UNAVAILABLE: "Format indisponible", PAGE_RELOAD: "Page à recharger", COOKIES_ROTATED: "Cookies invalidés (rotation)", EMPTY_DOWNLOAD: "Téléchargement vide", BINARY_MISSING: "yt-dlp introuvable", TIMEOUT: "Délai dépassé", UNKNOWN: "Erreur inconnue" }; export function youtubeClass(cls) { if (!cls) return "Aucune"; return YOUTUBE_CLASSES[cls] || cls; } // Origine des cookies YouTube ("paste", "file", "legacy", "browser:firefox") export function cookieSource(source) { if (!source) return "Inconnue"; if (source === "paste") return "Texte collé"; if (source === "file") return "Fichier importé"; if (source === "legacy") return "Ancien fichier (reprise automatique)"; if (source.startsWith("browser:")) return "Navigateur (" + source.slice(8) + ")"; return source; } // État du compte YouTube (cookies.getStatus() ou overview.ytdlp.cookies) -> libellé + couleur de Tag export function youtubeAccount(cookies) { if (!cookies?.present) return { key: 'none', label: "Aucun compte", color: "var(--text-secondary)", icon: "fa-solid fa-user-slash" }; if (cookies.invalidatedAt) return { key: 'invalid', label: "Invalidé", color: "var(--text-error)", icon: "fa-solid fa-circle-xmark" }; if (!cookies.loggedIn) return { key: 'anonymous', label: "Non connecté", color: "var(--text-warning)", icon: "fa-solid fa-triangle-exclamation" }; return { key: 'valid', label: "Connecté", color: "var(--text-success)", icon: "fa-solid fa-circle-check" }; }