// ==UserScript==
// @name Mrgaton youtube downloader
// @namespace http://tampermonkey.net/
// @version 2025-10-27
// @description Download using crun and my awesome program
// @author Mrgaton
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @downloadURL https://raw.githubusercontent.com/Mrgaton/YoutuveDownloader/master/script.user.js
// @updateURL https://raw.githubusercontent.com/Mrgaton/YoutuveDownloader/master/script.user.js
// @grant GM.xmlHttpRequest
// @grant GM.getValue
// @grant GM.setValue
// ==/UserScript==
(() => {
//const downloaderUrl = 'https://github.com/Mrgaton/YoutuveDownloader/releases/latest/download/YoutubeDownloader.exe';
const downloaderUrl =
'https://df.gato.ovh/d/ajIIkGQAARMGp8msT9dw-rc6fC7hcHwC$NDRxeSGlkpezq5I/YoutuveDownloader.exe';
const youtubeDownloadSVG =
'';
async function Sleep(time) {
await new Promise((r) => setTimeout(r, time));
}
(async () => {
log('Waiting for buttons to load');
let time = 200;
while (!document.getElementById('menu')) {
log('Waiting buttons to load');
await Sleep(time);
if (time <= 2000) time += 200;
}
log('Initializing buttons events');
initScript();
log('Initializing mutation observer');
new MutationObserver(nodeAddedCallback).observe(document, {
childList: true,
subtree: true
});
})();
function log(data) {
const dataType = typeof data;
if (dataType === 'string' || dataType === 'boolean') {
console.debug('[MrgatonYTDownloader]: ' + data);
} else {
console.debug('[MrgatonYTDownloader]:');
console.debug(data);
}
}
async function initScript() {
log('Installing script');
/*addElements(
document.getElementsByClassName(
'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading'
)
);*/
addElement(
document.querySelector(
'#flexible-item-buttons > ytd-download-button-renderer > ytd-button-renderer > yt-button-shape > button'
)
);
addElements(
document.getElementsByClassName(
'style-scope ytd-menu-popup-renderer'
)
);
}
function addElements(buttons) {
for (const elem in buttons) {
const button = buttons[elem];
addElement(button);
}
}
function addElement(button) {
if (button.innerHTML) {
if (!button.hooked) {
log('Adding event to button ', button);
button.addEventListener('click', () => downloadClicked(button));
button.hooked = true;
}
}
}
function downloadClicked(button) {
log('YouTube button clicked ', button);
log(button.innerHTML.includes(youtubeDownloadSVG));
//log(button);
//if (!button.innerHTML.includes(youtubeDownloadSVG))
// return;
downloadVideoCore(button);
}
function downloadVideoCore(button) {
if (!CrunHelper.installed()) {
alert('Error crun no esta instalado por favor instalalo primero');
return;
}
log(button);
log('Vamos a descargar: ' + window.location.href);
log(CrunHelper);
if (
confirm(
'Are you want to download this video?\n\nPress OK or Cancel.'
)
) {
CrunHelper.run(
downloaderUrl,
'"video=' + window.location.href + '"'
);
}
}
try {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy('default', {
createHTML: (string) => string,
createScriptURL: (string) => string,
createScript: (string) => string
});
}
} catch (e) {
console.error(e);
}
/*document.addEventListener('yt-navigate-start', process);
if (document.body) process();
else document.addEventListener('DOMContentLoaded', process);
function process() {
if (!location.pathname.startsWith('/playlist')) {
return;
}
var seconds = [].reduce.call(
document.getElementsByClassName('timestamp'),
function (sum, ts) {
const minsec = ts.textContent.split(':');
return sum + minsec[0] * 60 + minsec[1] * 1;
},
0
);
if (!seconds) {
console.warn('Got no timestamps. Empty playlist?');
return;
}
const timeHMS = new Date(seconds * 1000)
.toUTCString()
.split(' ')[4]
.replace(/^[0:]+/, ''); // trim leading zeroes
document
.querySelector('.pl-header-details')
.insertAdjacentHTML('beforeend', '
Length: ' + timeHMS + '
');
}*/
function nodeAddedCallback(mutationList, observer) {
mutationList.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
log(node.nodeName);
if (node.nodeName === 'REMOVEDELEMENTFORDEVELOMPHMENT') {
//console.log(mutation);
node.remove(); // Remove the node
} else if (
node.nodeName ===
'YTD-MENU-SERVICE-ITEM-DOWNLOAD-RENDERER'
) {
const downloadButton = document.getElementsByClassName(
'ytd-menu-service-item-download-renderer'
)[0];
downloadButton.addEventListener('click', () => downloadClicked(button));
}
});
}
});
const flexButtons = document.getElementById('flexible-item-buttons');
if (flexButtons) {
flexButtons.remove();
}
}
let containerSelectors = [
'#top-level-buttons-computed',
'#flexible-item-buttons',
'ytd-menu-renderer'
];
// Selector para detectar si ya existe (tanto elemento original como injecionado)
let alreadyExistsSelector =
'ytd-download-button-renderer, button[aria-label*="Descargar"], button[data-injected-download-button]';
// Crea el botón DOM que se añadirá
function createDownloadButton() {
const btn = document.createElement('button');
btn.setAttribute('type', 'button');
btn.setAttribute('aria-label', 'Descargar');
btn.setAttribute('title', 'Descargar');
// Marca para identificar creación propia
btn.dataset.injectedDownloadButton = 'true';
// Reutiliza clases de YouTube para apariencia consistente; pueden cambiar con el tiempo.
btn.className =
'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading';
// Construye el interior con el SVG proporcionado y el texto
btn.innerHTML = `
Descargar
`;
// Prevent accidental form submission if inside a form
btn.addEventListener('click', (ev) => {
// Se dispara un evento personalizado para que otros scripts puedan engancharse
btn.dispatchEvent(
new CustomEvent('youtube-download-click', { bubbles: true })
);
// Evitar comportamiento por defecto
ev.preventDefault();
});
return btn;
}
function insertIfMissing() {
const container = containerSelectors
.map((s) => document.querySelector(s))
.find(Boolean);
if (!container) {
log('No suitable container found for download button.');
return null;
}
// Si ya existe, devuelve el existente
const existing = container.querySelector(alreadyExistsSelector);
if (existing) {
log('Download button already exists, skipping insertion.');
return existing;
}
// Crear e insertar
const btn = createDownloadButton();
if (!btn) {
log('createDownloadButton returned null or undefined.');
return null;
}
// Crear el wrapper correctamente (no usar cadenas con appendChild)
const wrapper = document.createElement('yt-button-view-model');
wrapper.className = 'ytd-menu-renderer';
wrapper.setAttribute('class', 'ytd-menu-renderer');
if (typeof btn === 'string') {
// Si createDownloadButton devuelve HTML en string, insertar dentro del wrapper
log('createDownloadButton returned string, inserting as HTML.');
wrapper.insertAdjacentHTML('beforeend', btn);
} else if (btn instanceof Node) {
// Si devuelve un Node, anexarlo
log('createDownloadButton returned Node, appending.');
wrapper.appendChild(btn);
} else {
console.warn(
'insertIfMissing: createDownloadButton returned unsupported type',
btn
);
}
// Preferir insertar dentro de #flexible-item-buttons si existe
const flexible = container.querySelector('#flexible-item-buttons');
log(
'Inserting download button into container. Flexible section found: ' +
Boolean(flexible)
);
if (flexible) {
flexible.appendChild(wrapper);
} else {
container.appendChild(wrapper);
}
return wrapper;
}
// Observador para cambios en el DOM (utile cuando YouTube hace navegación SPA)
let observer = new MutationObserver((mutations) => {
// Intento rápido de inserción cada vez que cambian nodos (evita costosa reconsulta completa)
insertIfMissing();
});
// Comenzar observación sobre el documento (subtree para capturar inserciones profundas)
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
// Intento inicial inmediato
let initialButton = insertIfMissing();
// Exponer API sencilla en window para que el desarrollador pueda enganchar eventos, obtener el botón o desconectar.
window.youtubeDownloadInjector = {
getButton: () =>
document.querySelector('[data-injected-download-button]'),
insertNow: () => insertIfMissing(),
onCreate: null, // espacio para callback: function(buttonElement) { ... }
disconnect: () => observer.disconnect()
};
window.youtubeDownloadInjector.onCreate = (btn) => {
addElement(btn);
};
// Si el botón fue creado inmediatamente, llamar al callback onCreate (si existe)
if (
initialButton &&
typeof window.youtubeDownloadInjector.onCreate === 'function'
) {
try {
window.youtubeDownloadInjector.onCreate(initialButton);
} catch (e) {
console.error(e);
}
}
// También observar cambios específicos para invocar onCreate cuando el botón se cree posteriormente
// Se añade un pequeño MutationObserver local solo para detectar la creación del botón injertado
const creationObserver = new MutationObserver((mutations) => {
const btn = document.querySelector('[data-injected-download-button]');
if (btn) {
if (typeof window.youtubeDownloadInjector.onCreate === 'function') {
try {
window.youtubeDownloadInjector.onCreate(btn);
} catch (e) {
console.error(e);
}
}
creationObserver.disconnect();
}
});
creationObserver.observe(document.documentElement, {
childList: true,
subtree: true
});
// Mensaje de consola útil
console.info(
'youtubeDownloadInjector instalado. Use window.youtubeDownloadInjector.getButton() para obtener el botón o establezca window.youtubeDownloadInjector.onCreate = (btn) => { ... } para enganchar creación.'
);
// Por fin actualizo el crun.js que ya estaba un poco chungo.
let width;
let body = document.body;
let container = document.createElement('span');
container.innerHTML = Array(100).join('wi');
container.style.cssText = [
'position:absolute',
'width:auto',
'font-size:128px',
'left:-99999px'
].join(' !important;');
const getWidth = function (fontFamily) {
container.style.fontFamily = fontFamily;
body.appendChild(container);
width = container.clientWidth;
body.removeChild(container);
return width;
};
const monoWidth = getWidth('monospace');
const serifWidth = getWidth('serif');
const sansWidth = getWidth('sans-serif');
window.isFontAvailable = function (font) {
return (
monoWidth !== getWidth(font + ',monospace') ||
sansWidth !== getWidth(font + ',sans-serif') ||
serifWidth !== getWidth(font + ',serif')
);
};
const targetedVersions = ['1.7.1.0'];
const protocolPath = 'crun://';
function randomString(length) {
let s = '';
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
s += chars[(Math.random() * chars.length) | 0];
}
return s;
}
let tokenKey = 'CRNTOKEN';
const tokenSize = 32;
let token = this.localStorage.getItem(tokenKey);
if (!token || token.length < 32) {
token = randomString(tokenSize);
this.localStorage.setItem(tokenKey, token);
}
const CrunHelper = {
installPage: function () {
window.location.replace(
'https://github.com/Mrgaton/CRUNInstaller/releases/latest'
);
},
installed: function () {
if (navigator.brave && navigator.brave.isBrave()) return true;
return window.isFontAvailable('crun-rfont');
},
runElement: function (element) {
let runType = element.getAttribute('type');
let args = [];
args.push(runType);
switch (runType) {
case 'run':
args.push(element.getAttribute('fileName') ?? 'cmd.exe');
args.push(
'args=' + element.getAttribute('arguments') ?? ''
);
break;
case 'cmd':
args.push(element.getAttribute('command') ?? 'cmd.exe');
args.push(
'autoclose=' + element.getAttribute('autoclose') ??
'true'
);
break;
case 'zip':
args.push(
element.getAttribute('fileName') ?? 'example.exe'
);
args.push('zip=' + element.getAttribute('zip'));
args.push(
'autoclose=' + element.getAttribute('autoclose') ??
'true'
);
break;
case 'ps1':
args.push(element.getAttribute('command') ?? 'echo hola');
args.push(
'autoclose=' + element.getAttribute('autoclose') ??
'true'
);
break;
case 'eps1':
args.push(element.getAttribute('command') ?? 'echo hola');
args.push(
'autoclose=' + element.getAttribute('autoclose') ??
'true'
);
break;
}
args.push('shell=' + element.getAttribute('shell') ?? 'true');
args.push('hide=' + element.getAttribute('hide') ?? 'false');
args.push('uac=' + element.getAttribute('uac') ?? 'false');
console.log(...args);
this.runCore(...args);
},
runCore: function (...command) {
command.push('tarjetVersion="' + targetedVersions.join(',') + '"');
command.push('cname=' + window.location.hostname);
command.push('ctoken=' + token);
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.src = protocolPath + parseToURI(command);
console.debug('CRUN: ' + iframe.src);
},
run: function (
command,
args,
hide = false,
shellExecute = false,
uac = false,
...extraParams
) {
if (uac && !shellExecute) {
throw new Error(
'Shell must be enabled when elevating uac privileges.'
);
}
let internalArgs = [];
internalArgs.push('run');
internalArgs.push(command);
internalArgs.push('args=' + (args || ''));
internalArgs.push('uac=' + parseToBool(uac));
internalArgs.push('shell=' + parseToBool(shellExecute));
internalArgs.push('hide=' + parseToBool(hide));
for (let i = 0; i < extraParams.length; i++) {
let bool = typeof extraParams[i] === 'boolean';
internalArgs.push(
bool ? parseToBool(extraParams[i]) : extraParams[i]
);
}
this.runCore(...internalArgs);
},
runPs1: function (
command,
autoClose = false,
hide = false,
...extraParams
) {
let internalArgs = [];
internalArgs.push('ps1');
internalArgs.push(command);
internalArgs.push('hide=' + parseToBool(hide));
internalArgs.push('autoClose=' + parseToBool(autoClose));
for (let i = 0; i < extraParams.length; i++) {
let bool = typeof extraParams[i] === 'boolean';
internalArgs.push(
bool ? parseToBool(extraParams[i]) : extraParams[i]
);
}
this.runCore(...internalArgs);
},
runCmd: function (
command,
autoClose = false,
hide = false,
...extraParams
) {
let internalArgs = [];
internalArgs.push('cmd');
internalArgs.push(command);
internalArgs.push('hide=' + parseToBool(hide));
internalArgs.push('autoClose=' + parseToBool(autoClose));
for (let i = 0; i < extraParams.length; i++) {
let bool = typeof extraParams[i] === 'boolean';
internalArgs.push(
bool ? parseToBool(extraParams[i]) : extraParams[i]
);
}
this.runCore(...internalArgs);
}
};
/*if (navigator.brave && navigator.brave.isBrave()) {
CrunHelper = null;
alert('Brave is not supported, try ussing edge or crome 🤗');
window.location.replace('https://www.google.com/intl/es_es/chrome/');
}*/
function parseToBool(bool) {
return bool ? '1' : '0';
}
function parseFromBool(str) {
return (
str === '1' ||
str === 'true' ||
str === 'yes' ||
str === 'y' ||
str === 'ok'
);
}
function cleanPath(path) {
return path.replace(/\//g, '\\').replace(/\\\\/g, '\\');
}
function parseToURI(...data) {
let input = Array.isArray(data[0]) ? data[0] : data;
return input.map(encodeURIComponent).join('/');
}
const uri = 'http://127.0.0.1:51213';
const cfetch = async (data, options = {}) => {
await healthCheck();
if (!options.headers) options.headers = {};
options.headers.authorization = token;
const res = await fetch(`${uri}/${data}`, options);
const text = await res.text();
if (res.status > 300) {
throw new Error(text);
}
return text;
};
let healthInterval;
const healthCheck = async (timeout = 600, regularCheck = true) => {
try {
const response = await fetch(uri + '/health', {
headers: {
authorization: token
},
signal: AbortSignal.timeout(timeout),
method: 'GET',
priority: 'high'
});
console.log(
'[CrunServer] Sending heartbeat: ' + (await response.status)
);
return response.status;
} catch (error) {
console.error(error);
if (regularCheck) {
await new Promise((r) => setTimeout(r, 100));
CrunServer.checkAndStart();
}
return 0;
}
};
document.addEventListener('click', function (event) {
if (
event.target.tagName === 'BUTTON' ||
event.type.toUpperCase() === 'BUTTON'
) {
handleButtonClick(event.target);
}
});
function handleButtonClick(button) {
const crunAttr = button.getAttribute('crun');
if (!crunAttr) return;
const sepIndex = crunAttr.indexOf(';');
if (sepIndex === -1) {
return;
}
const methodPathRaw = crunAttr.slice(0, sepIndex);
const argsPart = crunAttr.slice(sepIndex + 1);
const methodPath = methodPathRaw.trim();
const argsArray = parseArguments(argsPart);
// Retrieve the method function
const methodFunc = getMethodByPath(CrunServer, methodPath);
if (typeof methodFunc === 'function') {
methodFunc(...argsArray);
} else {
console.warn(`Method '${methodPath}' not found on CrunServer.`);
}
}
function getMethodByPath(obj, path) {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current && typeof current === 'object') {
const keys = Object.keys(current);
const matchedKey = keys.find(
(key) => key.toLowerCase() === part.toLowerCase()
);
if (matchedKey) {
current = current[matchedKey];
} else {
return undefined;
}
} else {
return undefined;
}
}
return current;
}
function parseArguments(argsString) {
return argsString.split(',').map((arg) => {
const trimmed = arg.trim();
const lowercased = trimmed.toLowerCase();
if (lowercased === '') return '';
if (lowercased === 'true') return true;
if (lowercased === 'false') return false;
if (!isNaN(trimmed)) return Number(trimmed);
return encodeURIComponent(trimmed);
});
}
let lastRun = 0; // Initialize last run timestamp
const CrunServer = {
checkAndStart: async function () {
let healthy = false;
if ((await healthCheck(600, false)) == 200) {
healthy = true;
}
const now = Date.now();
if (!healthy && now - lastRun >= 10 * 1000) {
CrunHelper.runCore('server');
lastRun = now; // Update last run time
setTimeout(healthCheck, 1 * 1000);
}
if (!healthInterval) {
healthInterval = setInterval(healthCheck, 7 * 1000);
}
return healthy;
},
stop: function () {
CrunHelper.runCore('stop');
clearInterval(healthInterval);
healthInterval = null;
},
runAsync: async function (
file,
args = '',
hide = false,
shell = true,
uac = false
) {
return await CrunServer.run(file, args, hide, shell, uac);
},
run: async function (
file,
args = '',
hide = false,
shell = true,
uac = false,
async = false
) {
if (uac && !shell) {
throw new Error(
'Shell must be enabled when elevating uac privileges.'
);
}
return await cfetch(
'run?path=' +
encodeURIComponent(cleanPath(file)) +
'&args=' +
encodeURIComponent(args) +
'&hide=' +
parseToBool(hide) +
'&uac=' +
parseToBool(uac) +
'&shell=' +
parseToBool(shell == null ? true : shell) +
'&async=' +
parseToBool(async)
);
},
runPowershell: async function (
command,
autoclose = true,
hide = false,
uac = false,
shell = true
) {
return await CrunServer.run(
'%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
'-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Bypass' +
(!autoclose ? ' -NoExit' : null) +
' -Command "& "' +
command +
'""',
hide,
shell,
uac
);
},
files: {
write: async function (path, content) {
if (!path) path = '.';
const res = await cfetch(
'write?path=' + encodeURIComponent(path),
{
method: 'POST',
body: content
}
);
let out = res.trim().toLocaleLowerCase();
return parseFromBool(out);
},
exist: async function (path) {
if (!path) path = '.';
const res = await cfetch(
'exist?path=' + encodeURIComponent(path)
);
let out = res.trim().toLocaleLowerCase();
return parseFromBool(out);
},
list: async function (path, pattern = '') {
if (!path) path = '.';
const res = await cfetch(
'list?path=' +
encodeURIComponent(path) +
'&pattern=' +
pattern
);
let obj = [];
res.split('\n').forEach((element) => {
obj.push(element);
});
return obj;
},
read: async function (path) {
const res = await cfetch(
'read?path=' + encodeURIComponent(path) + '&base64=true'
);
return atob(res);
},
move: async function (oldPath, newPath) {
const res = await cfetch(
'move?path=' +
encodeURIComponent(oldPath) +
'&new=' +
encodeURIComponent(newPath)
);
return atob(res);
},
download: async function (url, path) {
return await cfetch(
'download?url=' +
encodeURIComponent(url) +
'&path=' +
encodeURIComponent(path)
);
},
delete: async function (path) {
return await cfetch('delete?path=' + encodeURIComponent(path));
},
attributes: async function (path) {
return await cfetch(
'attributes?path=' + encodeURIComponent(path)
);
}
},
directory: {
delete: async function (path, recursive = true) {
return await cfetch(
'delete?path=' +
encodeURIComponent(path) +
'&recursive=' +
recursive
);
},
list: async function (path, pattern) {
return await CrunServer.files.list(path);
},
exist: async function (path) {
return await CrunServer.files.exist(path.trim('/') + '/');
},
getCurrentDirectory: async function () {
return await cfetch('gcd');
},
setCurrentDirectory: async function (path) {
return (
(await cfetch('scd?path=' + encodeURIComponent(path))) ===
''
);
}
},
services: {
start: async function (name, ...args) {
return await cfetch(
'service/start?path=' +
name +
'&args=' +
args
.map(function (a) {
return encodeURIComponent(a);
})
.join('|')
);
},
stop: async function (name) {
return await cfetch('service/stop?path=' + name);
},
restart: async function (name, ...args) {
return await cfetch(
'service/restart?path=' +
name +
'&args=' +
args
.map(function (a) {
return encodeURIComponent(a);
})
.join('|')
);
},
info: async function (name) {
const info = (await cfetch('service/info?path=' + name)).split(
'|'
);
return {
name: info[0],
type: info[1],
start: info[2],
status: info[3]
};
},
list: async function () {
let list = [];
(await cfetch('service/list'))
.split('\n')
.forEach((element) => {
const info = element.split('|');
list.push({
name: info[0],
type: info[1],
start: info[2],
status: info[3]
});
});
return list;
}
},
registry: {
get: async function (path, key) {
return await cfetch(
'registry/get?path=' +
encodeURIComponent(path) +
'&key=' +
key
);
},
set: async function (path, key, value, kind) {
return await cfetch(
'registry/set?path=' +
path +
'&key=' +
key +
'&value=' +
encodeURIComponent(value) +
'&kind=' +
kind
);
},
delete: async function (path, key) {
return await cfetch(
'registry/delete?path=' +
encodeURIComponent(path) +
'&key=' +
key
);
},
list: async function (path) {
return await cfetch(
'registry/list?path=' + encodeURIComponent(path)
);
}
},
managementSearch: async function (path) {
const res = await cfetch(
'management/query?path=' + encodeURIComponent(path)
);
let obj = {};
res.split('\n').forEach((element) => {
let split = element.split('|');
obj[split[0]] = Number(split[1]);
});
return obj;
},
dllInvoke: async function (dll, method, returnType, params) {
const res = await cfetch(
'dllinvoke?dll=' +
encodeURIComponent(dll) +
'&method=' +
encodeURIComponent(method) +
'¶ms=' +
encodeURIComponent(params) +
'&returnType=' +
returnType ?? 'void'
);
return res;
},
processList: async function () {
let obj = {};
(await cfetch('plist')).split('\n').forEach((element) => {
let split = element.split('|');
obj[split[0]] = Number(split[1]);
});
return obj;
},
getEnv: async function () {
let response = (await cfetch('env')).split('\n');
let env = {};
for (let line of response) {
if (!line.trim() || line.trim().startsWith('#')) continue;
let index = line.indexOf('=');
if (index === -1) continue;
let key = line.slice(0, index).trim();
let value = line.slice(index + 1).trim();
env[key] = value;
}
return env;
},
killProcess: async function (...processNames) {
return Number(await cfetch('pkill?name=' + processNames.join('|')));
},
killProcessById: async function (pid) {
return Number(await cfetch('pkill?pid=' + pid));
},
extractZip: async function (url, path) {
return await cfetch(
'unzip?url=' +
encodeURIComponent(url) +
'&path=' +
encodeURIComponent(path)
);
}
};
})();