export const HOST_NAME = "com.youtubedl.native"; export function updateText(node, value) { if (node.textContent === value) return; if (node.childNodes.length === 1 && node.firstChild.nodeType === 3) node.firstChild.data = value; else node.textContent = value; } export function stabilizeDownloadSpeed(job, previous, now = Date.now()) { if (job.status !== "running" || job.stage !== "downloading") return { ...job, speedUpdatedAt: null }; if (Number.isFinite(job.speed) && job.speed >= 0) return { ...job, speedUpdatedAt: now }; const recent = previous?.status === "running" && previous.stage === job.stage && previous.track === job.track && Number.isFinite(previous.speed) && previous.speed >= 0 && Number.isFinite(previous.speedUpdatedAt) && now >= previous.speedUpdatedAt && now - previous.speedUpdatedAt < 1500; return { ...job, speed: recent ? previous.speed : null, speedUpdatedAt: recent ? previous.speedUpdatedAt : null }; } export const SUPPORTED_HELPER_VERSION = "1.3.2"; const MINIMUM_EXTENSION_VERSION = "1.4.5"; function compareVersions(left, right) { if (![left, right].every(value => typeof value === "string" && /^\d+(?:\.\d+){1,3}$/.test(value))) return null; const a = left.split(".").map(Number), b = right.split(".").map(Number); for (let index = 0; index < Math.max(a.length, b.length); index++) { const difference = (a[index] || 0) - (b[index] || 0); if (difference) return Math.sign(difference); } return 0; } export function checkHelperCompatibility(hardware, extensionVersion) { const helperVersion = hardware?.version || hardware?.bundle?.version; const helperOrder = compareVersions(helperVersion, SUPPORTED_HELPER_VERSION); const minimumExtension = hardware?.minimumExtensionVersion || MINIMUM_EXTENSION_VERSION; const extensionOrder = compareVersions(extensionVersion, minimumExtension); const versions = { helperVersion: helperVersion || "не определена", extensionVersion }; if (helperOrder === null) return { ...versions, compatible: false, update: "helper", message: "Не удалось определить версию помощника. Установите актуальный помощник и проверьте подключение." }; if (extensionOrder === null) return { ...versions, compatible: false, update: "extension", message: "Не удалось проверить совместимость версий. Обновите расширение и проверьте подключение." }; if (helperOrder < 0) return { ...versions, compatible: false, update: "helper", message: `Помощник ${helperVersion} устарел для расширения ${extensionVersion}. Установите помощник ${SUPPORTED_HELPER_VERSION} и нажмите «Проверить». Обновление компонентов не обновляет сам помощник.` }; if (extensionOrder < 0) return { ...versions, compatible: false, update: "extension", message: `Для помощника ${helperVersion} нужно расширение ${minimumExtension} или новее. Обновите расширение и перезагрузите вкладку YouTube.` }; if (helperOrder > 0) return { ...versions, compatible: false, update: "extension", message: `Помощник ${helperVersion} новее версии, поддерживаемой расширением ${extensionVersion}. Обновите расширение и перезагрузите вкладку YouTube.` }; return { ...versions, compatible: true, update: null, message: "Версии совместимы" }; } export function errorText(message = "") { if (/sign in to confirm|not a bot|cookies.*authentication|login required/i.test(message)) { return "YouTube отклонил запрос загрузчика и просит проверку на бота. Повторите запрос или обновите yt-dlp в компонентах. Дополнительно можно подключить вход кнопкой «Использовать вход в YouTube». Помощник работает; переустановка не нужна."; } return String(message); } export const STAGES = { preparing: "Подготовка", downloading: "Скачивание", merging: "Объединение дорожек", remuxing: "Подготовка файла", finalizing: "Финализация файла", encoding: "Перекодирование", audio: "Обработка аудио", saving: "Сохранение файла", installing: "Установка", updating: "Обновление компонентов", complete: "Готово", error: "Ошибка", cancelled: "Отменено", cancelling: "Останавливаю обработку", }; export function canonicalUrl(value) { try { const url = new URL(value); if (url.protocol !== "https:" || url.username || url.password || (url.port && url.port !== "443")) return null; let id; const parts = url.pathname.split("/").filter(Boolean); if (url.hostname === "youtu.be" && parts.length === 1) id = parts[0]; else if (["www.youtube.com", "youtube.com", "m.youtube.com", "music.youtube.com"].includes(url.hostname)) { if (url.pathname === "/watch") id = url.searchParams.get("v"); else if (parts.length === 2 && ["shorts", "live", "embed"].includes(parts[0])) id = parts[1]; } return /^[\w-]{11}$/.test(id || "") ? `https://www.youtube.com/watch?v=${id}` : null; } catch { return null; } } export function bytes(value) { if (!Number.isFinite(value) || value <= 0) return ""; const units = ["Б", "КБ", "МБ", "ГБ", "ТБ"]; const power = Math.min(4, Math.floor(Math.log(value) / Math.log(1024))); return `${(value / 1024 ** power).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} ${units[power]}`; } export function duration(seconds) { if (!Number.isFinite(seconds) || seconds < 0) return ""; const rounded = Math.floor(seconds); return rounded >= 3600 ? `${Math.floor(rounded / 3600)}:${String(Math.floor(rounded / 60) % 60).padStart(2, "0")}:${String(rounded % 60).padStart(2, "0")}` : `${Math.floor(rounded / 60)}:${String(rounded % 60).padStart(2, "0")}`; } export function codecName(value = "") { if (/^(avc|h264)/i.test(value)) return "H.264"; if (/^(hev|hvc|hevc)/i.test(value)) return "HEVC"; if (/^(av01|av1)/i.test(value)) return "AV1"; if (/^vp0?9/i.test(value)) return "VP9"; if (/^(mp4a|aac)/i.test(value)) return "AAC"; return value.split(".")[0].toUpperCase(); } export function progressText(job) { const stage = ["updating", "merging", "remuxing", "finalizing", "encoding", "audio"].includes(job.stage) && job.message ? job.message : STAGES[job.stage] || "Обработка"; const track = job.stage === "downloading" ? (job.track === "audio" ? " · аудио" : " · видео") : ""; const engine = ["encoding", "audio"].includes(job.stage) && job.engine ? ` · ${{ nvenc: "NVIDIA NVENC", amf: "AMD AMF", cpu: "CPU", copy: "без перекодирования" }[job.engine] || job.engine}` : ["merging", "remuxing"].includes(job.stage) && !job.message ? " · без перекодирования" : ""; return `${stage}${track}${engine}`; } export function downloadSizeText(job) { const megabytes = value => `${(value / 1048576).toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} МБ`; if (job.status === "complete" && Number.isFinite(job.result?.size) && job.result.size >= 0) return `Размер файла: ${megabytes(job.result.size)}`; const value = job.stage === "saving" ? job.copied : job.downloaded; if (!["downloading", "saving"].includes(job.stage) || !Number.isFinite(value) || value < 0) return ""; const downloaded = `${job.stage === "saving" ? "Сохранено" : "Скачано"} ${megabytes(value)}`; return Number.isFinite(job.total) && job.total > 0 ? `${downloaded} из ${megabytes(job.total)}` : downloaded; } export function processingText(job) { if (!["merging", "remuxing", "finalizing", "encoding", "audio"].includes(job.stage)) return ""; const parts = []; if (Number.isFinite(job.processedSeconds) && job.processedSeconds >= 0) parts.push(`Обработано ${duration(job.processedSeconds)}${job.durationSeconds > 0 ? ` из ${duration(job.durationSeconds)}` : ""}`); if (Number.isFinite(job.written) && job.written >= 0) parts.push(`Записано ${(job.written / 1048576).toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} МБ`); return parts.join(" · "); } export function preferenceOptions(metadata, preference = {}) { let videoId = preference.videoId || "best", audioId = preference.audioId || "best"; const quality = preference.quality; if (videoId !== "best" && quality?.height) { let candidates = metadata.videos.filter(video => video.height <= quality.height); if (!candidates.length) candidates = metadata.videos; candidates = [...candidates].sort((a, b) => b.height - a.height || (Number(codecName(b.codec) === quality.codec) - Number(codecName(a.codec) === quality.codec)) || Math.abs(a.fps - (quality.fps || 30)) - Math.abs(b.fps - (quality.fps || 30))); videoId = candidates[0]?.id || "best"; } else if (videoId !== "best" && !metadata.videos.some(video => video.id === videoId)) videoId = "best"; if (audioId !== "best") { const candidates = preference.audioLanguage ? metadata.audios.filter(audio => audio.language === preference.audioLanguage) : metadata.audios; audioId = candidates.find(audio => audio.id === audioId)?.id || candidates[0]?.id || "best"; } const mode = ["video", "video_only", "audio"].includes(preference.mode) ? preference.mode : "video"; return { mode, videoId, audioId, container: mode === "audio" ? preference.audioContainer || "m4a" : preference.videoContainer || "mp4", codec: preference.codec || "auto", engine: preference.engine || "auto", preset: preference.preset || "balanced", bitrate: preference.bitrate || 256, folder: preference.folder || "", workFolder: preference.workFolder || "", filename: preference.filename || "" }; }