Files
YouTubeDL/extension/shared.js
T
DepressedCat 5f2aad00f4
Check extension and helper / check (push) Canceled after 0s
Publish YouTubeDL 1.4.5 and website on Gitea
2026-10-10 23:53:17 +03:00

127 lines
10 KiB
JavaScript

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