const {shell, ipcRenderer, clipboard} = require("electron");
const {default: YTDlpWrap} = require("yt-dlp-wrap-plus");
const {constants} = require("fs/promises");
const {homedir, platform} = require("os");
const {join} = require("path");
const {mkdirSync, accessSync, promises, existsSync} = require("fs");
const {execSync, spawn} = require("child_process");
const CONSTANTS = {
DOM_IDS: {
PASTE_URL_BTN: "pasteUrl",
LOADING_WRAPPER: "loadingWrapper",
INCORRECT_MSG: "incorrectMsg",
ERROR_BTN: "errorBtn",
ERROR_DETAILS: "errorDetails",
PATH_DISPLAY: "path",
SELECT_LOCATION_BTN: "selectLocation",
DOWNLOAD_LIST: "list",
CLEAR_BTN: "clearBtn",
HIDDEN_PANEL: "hidden",
CLOSE_HIDDEN_BTN: "closeHidden",
TITLE_CONTAINER: "title",
TITLE_INPUT: "titleName",
URL_INPUTS: ".url",
AUDIO_PRESENT_SECTION: "audioPresent",
QUIT_APP_BTN: "quitAppBtn",
VIDEO_FORMAT_SELECT: "videoFormatSelect",
AUDIO_FORMAT_SELECT: "audioFormatSelect",
AUDIO_FOR_VIDEO_FORMAT_SELECT: "audioForVideoFormatSelect",
VIDEO_DOWNLOAD_BTN: "videoDownload",
AUDIO_DOWNLOAD_BTN: "audioDownload",
EXTRACT_BTN: "extractBtn",
EXTRACT_SELECTION: "extractSelection",
EXTRACT_QUALITY_SELECT: "extractQualitySelect",
CUSTOM_ARGS_INPUT: "customArgsInput",
START_TIME: "min-time",
END_TIME: "max-time",
MIN_SLIDER: "min-slider",
MAX_SLIDER: "max-slider",
SLIDER_RANGE_HIGHLIGHT: "range-highlight",
SUB_CHECKED: "subChecked",
QUIT_CHECKED: "quitChecked",
POPUP_BOX: "popupBox",
POPUP_BOX_MAC: "popupBoxMac",
POPUP_TEXT: "popupText",
POPUP_SVG: "popupSvg",
YTDLP_DOWNLOAD_PROGRESS: "ytDlpDownloadProgress",
UPDATE_POPUP: "updatePopup",
UPDATE_POPUP_PROGRESS: "updateProgress",
UPDATE_POPUP_BAR: "progressBarFill",
MENU_ICON: "menuIcon",
MENU: "menu",
PREFERENCE_WIN: "preferenceWin",
ABOUT_WIN: "aboutWin",
PLAYLIST_WIN: "playlistWin",
HISTORY_WIN: "historyWin",
COMPRESSOR_WIN: "compressorWin",
},
LOCAL_STORAGE_KEYS: {
DOWNLOAD_PATH: "downloadPath",
YT_DLP_PATH: "ytdlp",
MAX_DOWNLOADS: "maxActiveDownloads",
PREFERRED_VIDEO_QUALITY: "preferredVideoQuality",
PREFERRED_AUDIO_QUALITY: "preferredAudioQuality",
PREFERRED_VIDEO_CODEC: "preferredVideoCodec",
SHOW_MORE_FORMATS: "showMoreFormats",
BROWSER_COOKIES: "browser",
PROXY: "proxy",
CONFIG_PATH: "configPath",
AUTO_UPDATE: "autoUpdate",
CLOSE_TO_TRAY: "closeToTray",
YT_DLP_CUSTOM_ARGS: "customYtDlpArgs",
},
};
const $ = (id) => document.getElementById(id);
class YtDownloaderApp {
constructor() {
this.state = {
ytDlp: null,
ytDlpPath: "",
ffmpegPath: "",
jsRuntimePath: "",
downloadDir: "",
maxActiveDownloads: 5,
currentDownloads: 0,
videoInfo: {
title: "",
thumbnail: "",
duration: 0,
extractor_key: "",
url: "",
},
downloadOptions: {
rangeCmd: "",
rangeOption: "",
subs: "",
subLangs: "",
},
preferences: {
videoQuality: 1080,
audioQuality: "",
videoCodec: "avc1",
showMoreFormats: false,
proxy: "",
browserForCookies: "",
customYtDlpArgs: "",
},
downloadControllers: new Map(),
downloadedItems: new Set(),
downloadQueue: [],
};
}
async initialize() {
await this._initializeTranslations();
this._setupDirectories();
this._configureTray();
this._configureAutoUpdate();
try {
this.state.ytDlpPath = await this._findOrDownloadYtDlp();
this.state.ytDlp = new YTDlpWrap(`"${this.state.ytDlpPath}"`);
this.state.ffmpegPath = await this._findFfmpeg();
this.state.jsRuntimePath = await this._getJsRuntimePath();
console.log("yt-dlp path:", this.state.ytDlpPath);
console.log("ffmpeg path:", this.state.ffmpegPath);
console.log("JS runtime path:", this.state.jsRuntimePath);
this._loadSettings();
this._addEventListeners();
ipcRenderer.send("ready-for-links");
} catch (error) {
console.error("Initialization failed:", error);
$(CONSTANTS.DOM_IDS.INCORRECT_MSG).textContent = error.message;
$(CONSTANTS.DOM_IDS.PASTE_URL_BTN).style.display = "none";
}
}
_setupDirectories() {
const userHomeDir = homedir();
const hiddenDir = join(userHomeDir, ".ytDownloader");
if (!existsSync(hiddenDir)) {
try {
mkdirSync(hiddenDir, {recursive: true});
} catch (error) {
console.log(error);
}
}
let defaultDownloadDir = join(userHomeDir, "Downloads");
if (platform() === "linux") {
try {
const xdgDownloadDir = execSync("xdg-user-dir DOWNLOAD")
.toString()
.trim();
if (xdgDownloadDir) {
defaultDownloadDir = xdgDownloadDir;
}
} catch (err) {
console.warn("Could not execute xdg-user-dir:", err.message);
}
}
const savedPath = localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.DOWNLOAD_PATH
);
if (savedPath) {
try {
accessSync(savedPath, constants.W_OK);
this.state.downloadDir = savedPath;
} catch {
console.warn(
`Cannot write to saved path "${savedPath}". Falling back to default.`
);
this.state.downloadDir = defaultDownloadDir;
localStorage.setItem(
CONSTANTS.LOCAL_STORAGE_KEYS.DOWNLOAD_PATH,
defaultDownloadDir
);
}
} else {
this.state.downloadDir = defaultDownloadDir;
}
$(CONSTANTS.DOM_IDS.PATH_DISPLAY).textContent = this.state.downloadDir;
if (!existsSync(this.state.downloadDir)) {
mkdirSync(this.state.downloadDir, {recursive: true});
}
}
_configureTray() {
if (
localStorage.getItem(CONSTANTS.LOCAL_STORAGE_KEYS.CLOSE_TO_TRAY) ===
"true"
) {
console.log("Tray is enabled.");
ipcRenderer.send("useTray", true);
}
}
_configureAutoUpdate() {
let autoUpdate = true;
if (
localStorage.getItem(CONSTANTS.LOCAL_STORAGE_KEYS.AUTO_UPDATE) ===
"false"
) {
autoUpdate = false;
}
if (
process.windowsStore ||
process.env.YTDOWNLOADER_AUTO_UPDATES === "0"
) {
autoUpdate = false;
}
ipcRenderer.send("autoUpdate", autoUpdate);
}
async _initializeTranslations() {
return new Promise((resolve) => {
document.addEventListener(
"translations-loaded",
() => {
window.i18n.translatePage();
resolve();
},
{once: true}
);
});
}
async _findOrDownloadYtDlp() {
const hiddenDir = join(homedir(), ".ytDownloader");
const defaultYtDlpName = platform() === "win32" ? "ytdlp.exe" : "ytdlp";
const defaultYtDlpPath = join(hiddenDir, defaultYtDlpName);
const isMacOS = platform() === "darwin";
const isFreeBSD = platform() === "freebsd";
let executablePath = null;
if (process.env.YTDOWNLOADER_YTDLP_PATH) {
if (existsSync(process.env.YTDOWNLOADER_YTDLP_PATH)) {
executablePath = process.env.YTDOWNLOADER_YTDLP_PATH;
} else {
throw new Error(
"YTDOWNLOADER_YTDLP_PATH is set, but no file exists there."
);
}
}
else if (isMacOS) {
const possiblePaths = [
"/opt/homebrew/bin/yt-dlp",
"/usr/local/bin/yt-dlp",
];
executablePath = possiblePaths.find((p) => existsSync(p));
if (!executablePath) {
$(CONSTANTS.DOM_IDS.POPUP_BOX_MAC).style.display = "block";
console.warn("Homebrew yt-dlp not found. Prompting user.");
return "";
}
}
else if (isFreeBSD) {
try {
executablePath = execSync("which yt-dlp").toString().trim();
} catch {
throw new Error(
"No yt-dlp found in PATH on FreeBSD. Please install it."
);
}
}
else {
const storedPath = localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.YT_DLP_PATH
);
if (storedPath && existsSync(storedPath)) {
executablePath = storedPath;
}
else {
executablePath = await this.ensureYtDlpBinary(defaultYtDlpPath);
}
}
localStorage.setItem(
CONSTANTS.LOCAL_STORAGE_KEYS.YT_DLP_PATH,
executablePath
);
this._runBackgroundUpdate(executablePath, isMacOS);
return executablePath;
}
_runBackgroundUpdate(executablePath, isMacOS) {
try {
if (isMacOS) {
const brewPaths = [
"/opt/homebrew/bin/brew",
"/usr/local/bin/brew",
];
const brewExec = brewPaths.find((p) => existsSync(p)) || "brew";
const brewUpdate = spawn(brewExec, ["upgrade", "yt-dlp"]);
brewUpdate.on("error", (err) =>
console.error("Failed to run 'brew upgrade yt-dlp':", err)
);
brewUpdate.stdout.on("data", (data) =>
console.log("yt-dlp brew update:", data.toString())
);
} else {
const updateProc = spawn(executablePath, ["-U"]);
updateProc.on("error", (err) =>
console.error(
"Failed to run background yt-dlp update:",
err
)
);
updateProc.stdout.on("data", (data) => {
const output = data.toString();
console.log("yt-dlp update check:", output);
if (output.toLowerCase().includes("updating to")) {
this._showPopup(i18n.__("updatingYtdlp"));
} else if (
output.toLowerCase().includes("updated yt-dlp to")
) {
this._showPopup(i18n.__("updatedYtdlp"));
}
});
}
} catch (err) {
console.warn("Error initiating background update:", err);
}
}
async ensureYtDlpBinary(defaultYtDlpPath) {
try {
await promises.access(defaultYtDlpPath);
return defaultYtDlpPath;
} catch {
console.log("yt-dlp not found, downloading...");
$(CONSTANTS.DOM_IDS.POPUP_BOX).style.display = "block";
$(CONSTANTS.DOM_IDS.POPUP_SVG).style.display = "inline";
document.querySelector("#popupBox p").textContent = i18n.__(
"downloadingNecessaryFilesWait"
);
try {
await YTDlpWrap.downloadFromGithub(
defaultYtDlpPath,
undefined,
undefined,
(progress, _d, _t) => {
$(
CONSTANTS.DOM_IDS.YTDLP_DOWNLOAD_PROGRESS
).textContent =
i18n.__("progress") +
`: ${(progress * 100).toFixed(2)}%`;
}
);
$(CONSTANTS.DOM_IDS.POPUP_BOX).style.display = "none";
localStorage.setItem(
CONSTANTS.LOCAL_STORAGE_KEYS.YT_DLP_PATH,
defaultYtDlpPath
);
return defaultYtDlpPath;
} catch (downloadError) {
$(CONSTANTS.DOM_IDS.YTDLP_DOWNLOAD_PROGRESS).textContent = "";
console.error("Failed to download yt-dlp:", downloadError);
document.querySelector("#popupBox p").textContent = i18n.__(
"errorFailedFileDownload"
);
$(CONSTANTS.DOM_IDS.POPUP_SVG).style.display = "none";
const tryAgainBtn = document.createElement("button");
tryAgainBtn.id = "tryBtn";
tryAgainBtn.textContent = i18n.__("tryAgain");
tryAgainBtn.addEventListener("click", () => {
ipcRenderer.send("reload");
});
document.getElementById("popup").appendChild(tryAgainBtn);
throw new Error("Failed to download yt-dlp.");
}
}
}
async _findFfmpeg() {
if (process.env.YTDOWNLOADER_FFMPEG_PATH) {
if (existsSync(process.env.YTDOWNLOADER_FFMPEG_PATH)) {
return process.env.YTDOWNLOADER_FFMPEG_PATH;
}
throw new Error(
"YTDOWNLOADER_FFMPEG_PATH is set, but no file exists there."
);
}
if (platform() === "freebsd") {
try {
return execSync("which ffmpeg").toString().trim();
} catch {
throw new Error(
"No ffmpeg found in PATH on FreeBSD. App may not work correctly."
);
}
}
return join(__dirname, "..", "ffmpeg", "bin");
}
async _getJsRuntimePath() {
const exeName = "node";
if (process.env.YTDOWNLOADER_NODE_PATH) {
if (existsSync(process.env.YTDOWNLOADER_NODE_PATH)) {
return `$node:"${process.env.YTDOWNLOADER_NODE_PATH}"`;
}
return "";
}
if (process.env.YTDOWNLOADER_DENO_PATH) {
if (existsSync(process.env.YTDOWNLOADER_DENO_PATH)) {
return `$deno:"${process.env.YTDOWNLOADER_DENO_PATH}"`;
}
return "";
}
if (platform() === "darwin") {
const possiblePaths = [
"/opt/homebrew/bin/deno",
"/usr/local/bin/deno",
];
for (const p of possiblePaths) {
if (existsSync(p)) {
return `deno:"${p}"`;
}
}
console.log("No Deno installation found");
return "";
}
let jsRuntimePath = join(__dirname, "..", exeName);
if (platform() === "win32") {
jsRuntimePath = join(__dirname, "..", `${exeName}.exe`);
}
if (existsSync(jsRuntimePath)) {
return `${exeName}:"${jsRuntimePath}"`;
} else {
return "";
}
}
_loadSettings() {
const prefs = this.state.preferences;
prefs.videoQuality =
Number(
localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.PREFERRED_VIDEO_QUALITY
)
) || 1080;
prefs.audioQuality =
localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.PREFERRED_AUDIO_QUALITY
) || "";
prefs.videoCodec =
localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.PREFERRED_VIDEO_CODEC
) || "avc1";
prefs.showMoreFormats =
localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.SHOW_MORE_FORMATS
) === "true";
prefs.proxy =
localStorage.getItem(CONSTANTS.LOCAL_STORAGE_KEYS.PROXY) || "";
prefs.browserForCookies =
localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.BROWSER_COOKIES
) || "";
prefs.customYtDlpArgs =
localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.YT_DLP_CUSTOM_ARGS
) || "";
prefs.configPath = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_KEYS.CONFIG_PATH) || "";
const maxDownloads = Number(
localStorage.getItem(CONSTANTS.LOCAL_STORAGE_KEYS.MAX_DOWNLOADS)
);
this.state.maxActiveDownloads = maxDownloads >= 1 ? maxDownloads : 5;
$(CONSTANTS.DOM_IDS.CUSTOM_ARGS_INPUT).value = prefs.customYtDlpArgs;
const downloadDir = localStorage.getItem(
CONSTANTS.LOCAL_STORAGE_KEYS.DOWNLOAD_PATH
);
if (downloadDir) {
this.state.downloadDir = downloadDir;
$(CONSTANTS.DOM_IDS.PATH_DISPLAY).textContent = downloadDir;
}
}
_addEventListeners() {
$(CONSTANTS.DOM_IDS.PASTE_URL_BTN).addEventListener("click", () =>
this.pasteAndGetInfo()
);
document.addEventListener("keydown", (event) => {
if (
((event.ctrlKey && event.key === "v") ||
(event.metaKey &&
event.key === "v" &&
platform() === "darwin")) &&
document.activeElement.tagName !== "INPUT" &&
document.activeElement.tagName !== "TEXTAREA"
) {
$(CONSTANTS.DOM_IDS.PASTE_URL_BTN).classList.add("active");
setTimeout(() => {
$(CONSTANTS.DOM_IDS.PASTE_URL_BTN).classList.remove(
"active"
);
}, 150);
this.pasteAndGetInfo();
}
});
$(CONSTANTS.DOM_IDS.VIDEO_DOWNLOAD_BTN).addEventListener("click", () =>
this.handleDownloadRequest("video")
);
$(CONSTANTS.DOM_IDS.AUDIO_DOWNLOAD_BTN).addEventListener("click", () =>
this.handleDownloadRequest("audio")
);
$(CONSTANTS.DOM_IDS.EXTRACT_BTN).addEventListener("click", () =>
this.handleDownloadRequest("extract")
);
$(CONSTANTS.DOM_IDS.CLOSE_HIDDEN_BTN).addEventListener("click", () =>
this._hideInfoPanel()
);
$(CONSTANTS.DOM_IDS.SELECT_LOCATION_BTN).addEventListener("click", () =>
ipcRenderer.send("select-location-main", "")
);
$(CONSTANTS.DOM_IDS.CLEAR_BTN).addEventListener("click", () =>
this._clearAllDownloaded()
);
$(CONSTANTS.DOM_IDS.ERROR_DETAILS).addEventListener("click", (e) => {
clipboard.writeText(e.target.innerText);
this._showPopup(i18n.__("copiedText"), false);
});
$(CONSTANTS.DOM_IDS.QUIT_APP_BTN).addEventListener("click", () => {
ipcRenderer.send("quit", "quit");
});
ipcRenderer.on("link", (event, text) => this.getInfo(text));
ipcRenderer.on("downloadPath", (event, downloadPath) => {
try {
accessSync(downloadPath[0], constants.W_OK);
const newPath = downloadPath[0];
$(CONSTANTS.DOM_IDS.PATH_DISPLAY).textContent = newPath;
this.state.downloadDir = newPath;
} catch (error) {
console.log(error);
this._showPopup(i18n.__("unableToAccessDir"), true);
}
});
ipcRenderer.on("download-progress", (_event, percent) => {
if (percent) {
const popup = $(CONSTANTS.DOM_IDS.UPDATE_POPUP);
const textEl = $(CONSTANTS.DOM_IDS.UPDATE_POPUP_PROGRESS);
const barEl = $(CONSTANTS.DOM_IDS.UPDATE_POPUP_BAR);
popup.style.display = "flex";
textEl.textContent = `${percent.toFixed(1)}%`;
barEl.style.width = `${percent}%`;
}
});
ipcRenderer.on("update-downloaded", (_event, _) => {
$(CONSTANTS.DOM_IDS.UPDATE_POPUP).style.display = "none";
});
const menuMapping = {
[CONSTANTS.DOM_IDS.PREFERENCE_WIN]: "/preferences.html",
[CONSTANTS.DOM_IDS.ABOUT_WIN]: "/about.html",
[CONSTANTS.DOM_IDS.HISTORY_WIN]: "/history.html",
};
const windowMapping = {
[CONSTANTS.DOM_IDS.PLAYLIST_WIN]: "/playlist.html",
[CONSTANTS.DOM_IDS.COMPRESSOR_WIN]: "/compressor.html",
};
Object.entries(menuMapping).forEach(([id, page]) => {
$(id)?.addEventListener("click", () => {
this._closeMenu();
ipcRenderer.send("load-page", join(__dirname, page));
});
});
Object.entries(windowMapping).forEach(([id, page]) => {
$(id)?.addEventListener("click", () => {
this._closeMenu();
ipcRenderer.send("load-win", join(__dirname, page));
});
});
const minSlider = $(CONSTANTS.DOM_IDS.MIN_SLIDER);
const maxSlider = $(CONSTANTS.DOM_IDS.MAX_SLIDER);
minSlider.addEventListener("input", () =>
this._updateSliderUI(minSlider)
);
maxSlider.addEventListener("input", () =>
this._updateSliderUI(maxSlider)
);
$(CONSTANTS.DOM_IDS.START_TIME).addEventListener(
"change",
this._handleTimeInputChange
);
$(CONSTANTS.DOM_IDS.END_TIME).addEventListener(
"change",
this._handleTimeInputChange
);
this._updateSliderUI(null);
}
pasteAndGetInfo() {
this.getInfo(clipboard.readText());
}
async getInfo(url) {
this._loadSettings();
this._defaultVideoToggle();
this._resetUIForNewLink();
this.state.videoInfo.url = url;
try {
const metadata = await this._fetchVideoMetadata(url);
console.log(metadata);
const durationInt =
metadata.duration == null ? null : Math.ceil(metadata.duration);
this.state.videoInfo = {
...this.state.videoInfo,
id: metadata.id,
title: metadata.title,
thumbnail: metadata.thumbnail,
duration: durationInt,
extractor_key: metadata.extractor_key,
};
this.setVideoLength(durationInt);
this._populateFormatSelectors(metadata.formats || []);
this._displayInfoPanel();
} catch (error) {
if (
error.message.includes("js-runtimes") &&
error.message.includes("no such option")
) {
this._showError(i18n.__("ytDlpUpdateRequired"), url);
} else {
this._showError(error.message, url);
}
} finally {
$(CONSTANTS.DOM_IDS.LOADING_WRAPPER).style.display = "none";
}
}
handleDownloadRequest(type) {
this._updateDownloadOptionsFromUI();
const downloadJob = {
type,
url: this.state.videoInfo.url,
title: this.state.videoInfo.title,
thumbnail: this.state.videoInfo.thumbnail,
options: {...this.state.downloadOptions},
uiSnapshot: {
videoFormat: $(CONSTANTS.DOM_IDS.VIDEO_FORMAT_SELECT).value,
audioForVideoFormat: $(
CONSTANTS.DOM_IDS.AUDIO_FOR_VIDEO_FORMAT_SELECT
).value,
audioFormat: $(CONSTANTS.DOM_IDS.AUDIO_FORMAT_SELECT).value,
extractFormat: $(CONSTANTS.DOM_IDS.EXTRACT_SELECTION).value,
extractQuality: $(CONSTANTS.DOM_IDS.EXTRACT_QUALITY_SELECT)
.value,
},
};
if (this.state.currentDownloads < this.state.maxActiveDownloads) {
this._startDownload(downloadJob);
} else {
this._queueDownload(downloadJob);
}
this._hideInfoPanel();
}
_fetchVideoMetadata(url) {
return new Promise((resolve, reject) => {
const {proxy, browserForCookies, configPath} =
this.state.preferences;
const args = [
"-j",
"--no-playlist",
"--no-warnings",
proxy ? "--proxy" : "",
proxy,
browserForCookies ? "--cookies-from-browser" : "",
browserForCookies,
this.state.jsRuntimePath
? `--no-js-runtimes --js-runtime ${this.state.jsRuntimePath}`
: "",
configPath ? "--config-location" : "",
configPath ? `"${configPath}"` : "",
`"${url}"`,
].filter(Boolean);
const process = this.state.ytDlp.exec(args, {shell: true});
console.log(
"Spawned yt-dlp with args:",
process.ytDlpProcess.spawnargs.join(" ")
);
let stdout = "";
let stderr = "";
process.ytDlpProcess.stdout.on("data", (data) => {
stdout += data;
});
process.ytDlpProcess.stderr.on("data", (data) => (stderr += data));
process.on("close", () => {
if (stdout) {
try {
resolve(JSON.parse(stdout));
} catch (e) {
reject(
new Error(
"Failed to parse yt-dlp JSON output: " +
(stderr || e.message)
)
);
}
} else {
reject(
new Error(
stderr || `yt-dlp exited with a non-zero code.`
)
);
}
});
process.on("error", (err) => reject(err));
});
}
_startDownload(job) {
this.state.currentDownloads++;
const randomId = "item_" + Math.random().toString(36).substring(2, 12);
const {downloadArgs, finalFilename, finalExt} =
this._prepareDownloadArgs(job);
this._createDownloadUI(randomId, job);
const controller = new AbortController();
this.state.downloadControllers.set(randomId, controller);
const downloadProcess = this.state.ytDlp.exec(downloadArgs, {
shell: true,
detached: false,
signal: controller.signal,
});
console.log(
"Spawned yt-dlp with args:",
downloadProcess.ytDlpProcess.spawnargs.join(" ")
);
downloadProcess
.on("progress", (progress) => {
this._updateProgressUI(randomId, progress);
})
.once("ytDlpEvent", () => {
const el = $(`${randomId}_prog`);
if (el) el.textContent = i18n.__("downloading");
})
.once("close", (code) => {
this._handleDownloadCompletion(
code,
randomId,
finalFilename,
finalExt,
job.thumbnail
);
})
.once("error", (error) => {
this.state.downloadedItems.add(randomId);
this._updateClearAllButton();
this._handleDownloadError(error, randomId);
});
}
_queueDownload(job) {
const randomId = "queue_" + Math.random().toString(36).substring(2, 12);
this.state.downloadQueue.push({...job, queueId: randomId});
const itemHTML = `
<div class="item" id="${randomId}">
<div class="itemIconBox">
<img src="${
job.thumbnail || "../assets/images/thumb.png"
}" alt="thumbnail" class="itemIcon" crossorigin="anonymous">
<span class="itemType">${i18n.__(
job.type === "video" ? "video" : "audio"
)}</span>
</div>
<div class="itemBody">
<div class="itemTitle">${job.title}</div>
<p>${i18n.__("preparing")}</p>
</div>
</div>`;
$(CONSTANTS.DOM_IDS.DOWNLOAD_LIST).insertAdjacentHTML(
"beforeend",
itemHTML
);
}
_processQueue() {
if (
this.state.downloadQueue.length > 0 &&
this.state.currentDownloads < this.state.maxActiveDownloads
) {
const nextJob = this.state.downloadQueue.shift();
$(nextJob.queueId)?.remove();
this._startDownload(nextJob);
}
}
_prepareDownloadArgs(job) {
const {type, url, title, options, uiSnapshot} = job;
const {rangeOption, rangeCmd, subs, subLangs} = options;
const {proxy, browserForCookies, configPath} = this.state.preferences;
let format_id, ext, audioForVideoFormat_id, audioFormat;
if (type === "video") {
const [videoFid, videoExt, _, videoCodec] =
uiSnapshot.videoFormat.split("|");
const [audioFid, audioExt] =
uiSnapshot.audioForVideoFormat.split("|");
format_id = videoFid;
audioForVideoFormat_id = audioFid;
const finalAudioExt = audioExt === "webm" ? "opus" : audioExt;
ext = videoExt;
if (videoExt === "mp4" && finalAudioExt === "opus") {
if (videoCodec.includes("avc")) ext = "mkv";
else if (videoCodec.includes("av01")) ext = "webm";
} else if (
videoExt === "webm" &&
["m4a", "mp4"].includes(finalAudioExt)
) {
ext = "mkv";
}
audioFormat =
audioForVideoFormat_id === "none"
? ""
: `+${audioForVideoFormat_id}`;
} else if (type === "audio") {
[format_id, ext] = uiSnapshot.audioFormat.split("|");
ext = ext === "webm" ? "opus" : ext;
} else {
ext =
{alac: "m4a"}[uiSnapshot.extractFormat] ||
uiSnapshot.extractFormat;
}
const invalidChars =
platform() === "win32" ? /[<>:"/\\|?*[\]`#]/g : /["/`#]/g;
let finalFilename = title
.replace(invalidChars, "")
.trim()
.slice(0, 100);
if (finalFilename.startsWith(".")) {
finalFilename = finalFilename.substring(1);
}
if (rangeCmd) {
let rangeTxt = rangeCmd.replace("*", "");
if (platform() === "win32") rangeTxt = rangeTxt.replace(/:/g, "_");
finalFilename += ` [${rangeTxt}]`;
}
const outputPath = `"${join(
this.state.downloadDir,
`${finalFilename}.${ext}`
)}"`;
const baseArgs = [
"--no-playlist",
"--no-mtime",
browserForCookies ? "--cookies-from-browser" : "",
browserForCookies,
proxy ? "--proxy" : "",
proxy,
configPath ? "--config-location" : "",
configPath ? `"${configPath}"` : "",
"--ffmpeg-location",
`"${this.state.ffmpegPath}"`,
this.state.jsRuntimePath
? `--no-js-runtimes --js-runtime ${this.state.jsRuntimePath}`
: "",
].filter(Boolean);
if (type === "audio") {
if (ext === "m4a" || ext === "mp3" || ext === "mp4") {
baseArgs.unshift("--embed-thumbnail");
}
} else if (type === "extract") {
if (ext === "mp3" || ext === "m4a") {
baseArgs.unshift("--embed-thumbnail");
}
}
let downloadArgs;
if (type === "extract") {
downloadArgs = [
"-x",
"--audio-format",
uiSnapshot.extractFormat,
"--audio-quality",
uiSnapshot.extractQuality,
"-o",
outputPath,
...baseArgs,
];
} else {
const formatString =
type === "video" ? `${format_id}${audioFormat}` : format_id;
downloadArgs = ["-f", formatString, "-o", outputPath, ...baseArgs];
}
if (subs) downloadArgs.push(subs);
if (subLangs) downloadArgs.push(subLangs);
if (rangeOption) downloadArgs.push(rangeOption, rangeCmd);
const customArgsString = $(
CONSTANTS.DOM_IDS.CUSTOM_ARGS_INPUT
).value.trim();
if (customArgsString) {
const customArgs = customArgsString.split(/\s+/);
downloadArgs.push(...customArgs);
}
downloadArgs.push(`"${url}"`);
return {downloadArgs, finalFilename, finalExt: ext};
}
_handleDownloadCompletion(code, randomId, filename, ext, thumbnail) {
this.state.currentDownloads--;
this.state.downloadControllers.delete(randomId);
if (code === 0) {
this._showDownloadSuccessUI(randomId, filename, ext, thumbnail);
this.state.downloadedItems.add(randomId);
this._updateClearAllButton();
} else if (code !== null) {
this._handleDownloadError(
new Error(`Download process exited with code ${code}.`),
randomId
);
}
this._processQueue();
if ($(CONSTANTS.DOM_IDS.QUIT_CHECKED).checked) {
ipcRenderer.send("quit", "quit");
}
}
_handleDownloadError(error, randomId) {
if (
error.name === "AbortError" ||
error.message.includes("AbortError")
) {
console.log(`Download ${randomId} was aborted.`);
this.state.currentDownloads = Math.max(
0,
this.state.currentDownloads - 1
);
this.state.downloadControllers.delete(randomId);
this._processQueue();
return;
}
this.state.currentDownloads--;
this.state.downloadControllers.delete(randomId);
console.error("Download Error:", error);
const progressEl = $(`${randomId}_prog`);
if (progressEl) {
progressEl.textContent = i18n.__("errorHoverForDetails");
progressEl.title = error.message;
}
this._processQueue();
}
_updateDownloadOptionsFromUI() {
const startTime = $(CONSTANTS.DOM_IDS.START_TIME).value;
const endTime = $(CONSTANTS.DOM_IDS.END_TIME).value;
const duration = this.state.videoInfo.duration;
const startSeconds = this.parseTime(startTime);
const endSeconds = this.parseTime(endTime);
if (
startSeconds === 0 &&
(endSeconds === duration || endSeconds === 0)
) {
this.state.downloadOptions.rangeCmd = "";
this.state.downloadOptions.rangeOption = "";
} else {
const start = startTime || "0";
const end = endTime || "inf";
this.state.downloadOptions.rangeCmd = `*${start}-${end}`;
this.state.downloadOptions.rangeOption = "--download-sections";
}
if ($(CONSTANTS.DOM_IDS.SUB_CHECKED).checked) {
this.state.downloadOptions.subs = "--write-subs";
this.state.downloadOptions.subLangs = "--sub-langs all";
} else {
this.state.downloadOptions.subs = "";
this.state.downloadOptions.subLangs = "";
}
}
_resetUIForNewLink() {
this._hideInfoPanel();
$(CONSTANTS.DOM_IDS.LOADING_WRAPPER).style.display = "flex";
$(CONSTANTS.DOM_IDS.INCORRECT_MSG).textContent = "";
$(CONSTANTS.DOM_IDS.ERROR_BTN).style.display = "none";
$(CONSTANTS.DOM_IDS.ERROR_DETAILS).style.display = "none";
$(CONSTANTS.DOM_IDS.VIDEO_FORMAT_SELECT).innerHTML = "";
$(CONSTANTS.DOM_IDS.AUDIO_FORMAT_SELECT).innerHTML = "";
const noAudioTxt = i18n.__("noAudio");
$(
CONSTANTS.DOM_IDS.AUDIO_FOR_VIDEO_FORMAT_SELECT
).innerHTML = `<option value="none|none">${noAudioTxt}</option>`;
}
_populateFormatSelectors(formats) {
const videoSelect = $(CONSTANTS.DOM_IDS.VIDEO_FORMAT_SELECT);
const audioSelect = $(CONSTANTS.DOM_IDS.AUDIO_FORMAT_SELECT);
const audioForVideoSelect = $(
CONSTANTS.DOM_IDS.AUDIO_FOR_VIDEO_FORMAT_SELECT
);
const NBSP = " ";
let maxVideoQualityLen = 0;
let maxAudioQualityLen = 0;
formats.forEach((format) => {
if (format.video_ext !== "none" && format.vcodec !== "none") {
const quality = `${format.height || "???"}p${
format.fps === 60 ? "60" : ""
}`;
if (quality.length > maxVideoQualityLen) {
maxVideoQualityLen = quality.length;
}
} else if (
format.acodec !== "none" &&
format.video_ext === "none"
) {
const formatNote =
i18n.__(format.format_note) || i18n.__("unknownQuality");
if (formatNote.length > maxAudioQualityLen) {
maxAudioQualityLen = formatNote.length;
}
}
});
const videoQualityPadding = maxVideoQualityLen;
const audioQualityPadding = maxAudioQualityLen;
const extPadding = 5;
const vcodecPadding = 5;
const filesizePadding = 10;
const {videoQuality, videoCodec, showMoreFormats} =
this.state.preferences;
let bestMatchHeight = 0;
formats.forEach((f) => {
if (
f.height &&
f.height <= videoQuality &&
f.height > bestMatchHeight &&
f.video_ext !== "none"
) {
bestMatchHeight = f.height;
}
});
if (bestMatchHeight === 0 && formats.length > 0) {
bestMatchHeight = Math.max(
...formats.filter((f) => f.height).map((f) => f.height)
);
}
const availableCodecs = new Set(
formats
.filter((f) => f.height === bestMatchHeight && f.vcodec)
.map((f) => f.vcodec.split(".")[0])
);
const finalCodec = availableCodecs.has(videoCodec)
? videoCodec
: [...availableCodecs].pop();
let isAVideoSelected = false;
formats.forEach((format) => {
let sizeInMB = null;
let isApprox = false;
if (format.filesize) {
sizeInMB = format.filesize / 1000000;
} else if (format.filesize_approx) {
sizeInMB = format.filesize_approx / 1000000;
isApprox = true;
} else if (this.state.videoInfo.duration && format.tbr) {
sizeInMB = (this.state.videoInfo.duration * format.tbr) / 8192;
isApprox = true;
}
const displaySize = sizeInMB
? `${isApprox ? "~" : ""}${sizeInMB.toFixed(2)} MB`
: i18n.__("unknownSize");
if (format.video_ext !== "none" && format.vcodec !== "none") {
if (
!showMoreFormats &&
(format.ext === "webm" || format.vcodec?.startsWith("vp"))
) {
return;
}
let isSelected = false;
if (
!isAVideoSelected &&
format.height === bestMatchHeight &&
format.vcodec?.startsWith(finalCodec)
) {
isSelected = true;
isAVideoSelected = true;
}
const quality = `${format.height || "???"}p${
format.fps === 60 ? "60" : ""
}`;
const hasAudio = format.acodec !== "none" ? " 🔊" : "";
const col1 = quality.padEnd(videoQualityPadding + 1, NBSP);
const col2 = format.ext.padEnd(extPadding, NBSP);
const col4 = displaySize.padEnd(filesizePadding, NBSP);
let optionText;
if (showMoreFormats) {
const vcodec = format.vcodec?.split(".")[0] || "";
const col3 = vcodec.padEnd(vcodecPadding, NBSP);
optionText = `${col1} | ${col2} | ${col3} | ${col4}${hasAudio}`;
} else {
optionText = `${col1} | ${col2} | ${col4}${hasAudio}`;
}
const option = `<option value="${format.format_id}|${
format.ext
}|${format.height}|${format.vcodec}" ${
isSelected ? "selected" : ""
}>${optionText}</option>`;
videoSelect.innerHTML += option;
} else if (
format.acodec !== "none" &&
format.video_ext === "none"
) {
if (!showMoreFormats && format.ext === "webm") return;
const audioExt = format.ext === "webm" ? "opus" : format.ext;
const formatNote =
i18n.__(format.format_note) || i18n.__("unknownQuality");
const audioExtPadded = audioExt.padEnd(extPadding, NBSP);
const audioQualityPadded = formatNote.padEnd(
audioQualityPadding,
NBSP
);
const audioSizePadded = displaySize.padEnd(
filesizePadding,
NBSP
);
const option_audio = `<option value="${format.format_id}|${audioExt}">${audioQualityPadded} | ${audioExtPadded} | ${audioSizePadded}</option>`;
audioSelect.innerHTML += option_audio;
audioForVideoSelect.innerHTML += option_audio;
}
});
if (
formats.every((f) => f.acodec === "none" || f.acodec === undefined)
) {
$(CONSTANTS.DOM_IDS.AUDIO_PRESENT_SECTION).style.display = "none";
} else {
$(CONSTANTS.DOM_IDS.AUDIO_PRESENT_SECTION).style.display = "block";
}
}
_displayInfoPanel() {
const info = this.state.videoInfo;
const titleContainer = $(CONSTANTS.DOM_IDS.TITLE_CONTAINER);
titleContainer.innerHTML = "";
titleContainer.append(
Object.assign(document.createElement("b"), {
textContent: i18n.__("title") + ": ",
}),
Object.assign(document.createElement("input"), {
className: "title",
id: CONSTANTS.DOM_IDS.TITLE_INPUT,
type: "text",
value: `${info.title} [${info.id}]`,
onchange: (e) => (this.state.videoInfo.title = e.target.value),
})
);
document
.querySelectorAll(CONSTANTS.DOM_IDS.URL_INPUTS)
.forEach((el) => {
el.value = info.url;
});
const hiddenPanel = $(CONSTANTS.DOM_IDS.HIDDEN_PANEL);
hiddenPanel.style.display = "inline-block";
hiddenPanel.classList.add("scaleUp");
}
_createDownloadUI(randomId, job) {
const itemHTML = `
<div class="item" id="${randomId}">
<div class="itemIconBox">
<img src="${
job.thumbnail || "../assets/images/thumb.png"
}" alt="thumbnail" class="itemIcon" crossorigin="anonymous">
<span class="itemType">${i18n.__(
job.type === "video" ? "video" : "audio"
)}</span>
</div>
<img src="../assets/images/close.png" class="itemClose" id="${randomId}_close">
<div class="itemBody">
<div class="itemTitle">${job.title}</div>
<strong class="itemSpeed" id="${randomId}_speed"></strong>
<div id="${randomId}_prog" class="itemProgress">${i18n.__(
"preparing"
)}</div>
</div>
</div>`;
$(CONSTANTS.DOM_IDS.DOWNLOAD_LIST).insertAdjacentHTML(
"beforeend",
itemHTML
);
$(`${randomId}_close`).addEventListener("click", () =>
this._cancelDownload(randomId)
);
}
_updateProgressUI(randomId, progress) {
const speedEl = $(`${randomId}_speed`);
const progEl = $(`${randomId}_prog`);
if (!speedEl || !progEl) return;
let fillEl = progEl.querySelector(".custom-progress-fill");
if (!fillEl) {
progEl.innerHTML = "";
const bar = document.createElement("div");
bar.className = "custom-progress";
fillEl = document.createElement("div");
fillEl.className = "custom-progress-fill";
bar.appendChild(fillEl);
progEl.appendChild(bar);
}
if (progress.percent === 100) {
fillEl.style.width = progress.percent + "%";
speedEl.textContent = "";
progEl.textContent = i18n.__("processing");
ipcRenderer.send("progress", 0);
return;
}
speedEl.textContent = `${i18n.__("speed")}: ${
progress.currentSpeed || "0 B/s"
}`;
fillEl.style.width = progress.percent + "%";
ipcRenderer.send("progress", progress.percent / 100);
}
_showDownloadSuccessUI(randomId, filename, ext, thumbnail) {
const progressEl = $(`${randomId}_prog`);
if (!progressEl) return;
const fullFilename = `${filename}.${ext}`;
const fullPath = join(this.state.downloadDir, fullFilename);
progressEl.innerHTML = "";
const link = document.createElement("b");
link.textContent = i18n.__("fileSavedClickToOpen");
link.style.cursor = "pointer";
link.onclick = () => {
ipcRenderer.send("show-file", fullPath);
};
progressEl.appendChild(link);
$(`${randomId}_speed`).textContent = "";
new Notification("ytDownloader", {
body: fullFilename,
icon: thumbnail,
}).onclick = () => {
shell.showItemInFolder(fullPath);
};
promises
.stat(fullPath)
.then((stat) => {
const fileSize = stat.size || 0;
ipcRenderer
.invoke("add-to-history", {
title: this.state.videoInfo.title,
url: this.state.videoInfo.url,
filename: filename,
filePath: fullPath,
fileSize: fileSize,
format: ext,
thumbnail: thumbnail,
duration: this.state.videoInfo.duration,
})
.catch((err) =>
console.error("Error adding to history:", err)
);
})
.catch((error) => console.error("Error saving to history:", error));
}
_showError(errorMessage, url) {
$(CONSTANTS.DOM_IDS.INCORRECT_MSG).textContent =
i18n.__("errorNetworkOrUrl");
$(CONSTANTS.DOM_IDS.ERROR_BTN).style.display = "inline-block";
const errorDetails = $(CONSTANTS.DOM_IDS.ERROR_DETAILS);
errorDetails.innerHTML = `<strong>URL: ${url}</strong><br><br>${errorMessage}`;
errorDetails.title = i18n.__("clickToCopy");
}
_hideInfoPanel() {
const panel = $(CONSTANTS.DOM_IDS.HIDDEN_PANEL);
if (panel.style.display !== "none") {
panel.classList.remove("scaleUp");
panel.classList.add("scale");
setTimeout(() => {
panel.style.display = "none";
panel.classList.remove("scale");
}, 400);
}
}
_showPopup(text, isError = false) {
let popupContainer = document.getElementById("popupContainer");
if (!popupContainer) {
popupContainer = document.createElement("div");
popupContainer.id = "popupContainer";
popupContainer.className = "popup-container";
document.body.appendChild(popupContainer);
}
const popup = document.createElement("span");
popup.textContent = text;
popup.classList.add("popup-item");
popup.style.background = isError ? "#ff6b6b" : "#54abde";
if (isError) {
popup.classList.add("popup-error");
}
popupContainer.appendChild(popup);
setTimeout(() => {
popup.style.opacity = "0";
setTimeout(() => {
popup.remove();
if (popupContainer.childElementCount === 0) {
popupContainer.remove();
}
}, 1000);
}, 2200);
}
_closeMenu() {
$(CONSTANTS.DOM_IDS.MENU_ICON).style.transform = "rotate(0deg)";
$(CONSTANTS.DOM_IDS.MENU).style.opacity = "0";
setTimeout(
() => ($(CONSTANTS.DOM_IDS.MENU).style.display = "none"),
500
);
}
_cancelDownload(id) {
if (this.state.downloadControllers.has(id)) {
this.state.downloadControllers.get(id).abort();
}
this.state.downloadQueue = this.state.downloadQueue.filter(
(job) => job.queueId !== id
);
this.state.downloadedItems.delete(id);
this._fadeAndRemoveItem(id);
this._updateClearAllButton();
}
_fadeAndRemoveItem(id) {
const item = $(id);
if (item) {
item.classList.add("scale");
setTimeout(() => item.remove(), 500);
}
}
_clearAllDownloaded() {
this.state.downloadedItems.forEach((id) => this._fadeAndRemoveItem(id));
this.state.downloadedItems.clear();
this._updateClearAllButton();
}
_updateClearAllButton() {
const btn = $(CONSTANTS.DOM_IDS.CLEAR_BTN);
btn.style.display =
this.state.downloadedItems.size > 1 ? "inline-block" : "none";
}
_defaultVideoToggle() {
let defaultWindow = "video";
if (localStorage.getItem("defaultWindow")) {
defaultWindow = localStorage.getItem("defaultWindow");
}
if (defaultWindow == "video") {
selectVideo();
} else {
selectAudio();
}
}
parseTime(timeString) {
const parts = timeString.split(":").map((p) => parseInt(p.trim(), 10));
let totalSeconds = 0;
if (parts.length === 3) {
const [hrs, mins, secs] = parts;
if (
isNaN(hrs) ||
isNaN(mins) ||
isNaN(secs) ||
mins < 0 ||
mins > 59 ||
secs < 0 ||
secs > 59
)
return NaN;
totalSeconds = hrs * 3600 + mins * 60 + secs;
} else if (parts.length === 2) {
const [mins, secs] = parts;
if (isNaN(mins) || isNaN(secs) || secs < 0 || secs > 59) return NaN;
totalSeconds = mins * 60 + secs;
} else if (parts.length === 1) {
const [secs] = parts;
if (isNaN(secs)) return NaN;
totalSeconds = secs;
} else {
return NaN;
}
return totalSeconds;
}
_formatTime(duration) {
if (duration === null) {
return "";
}
const hrs = Math.floor(duration / 3600);
const mins = Math.floor((duration % 3600) / 60);
const secs = Math.floor(duration % 60);
const paddedMins = String(mins).padStart(2, "0");
const paddedSecs = String(secs).padStart(2, "0");
if (hrs > 0) {
return `${hrs}:${paddedMins}:${paddedSecs}`;
} else {
return `${paddedMins}:${paddedSecs}`;
}
}
_updateSliderUI(movedSlider) {
const minSlider = $(CONSTANTS.DOM_IDS.MIN_SLIDER);
const maxSlider = $(CONSTANTS.DOM_IDS.MAX_SLIDER);
const minTimeDisplay = $(CONSTANTS.DOM_IDS.START_TIME);
const maxTimeDisplay = $(CONSTANTS.DOM_IDS.END_TIME);
const rangeHighlight = $(CONSTANTS.DOM_IDS.SLIDER_RANGE_HIGHLIGHT);
let minValue = parseInt(minSlider.value);
let maxValue = parseInt(maxSlider.value);
const minSliderVal = parseInt(minSlider.min);
const maxSliderVal = parseInt(minSlider.max);
const sliderRange = maxSliderVal - minSliderVal;
if (minValue >= maxValue) {
if (movedSlider && movedSlider.id === "min-slider") {
minValue = Math.max(minSliderVal, maxValue - 1);
minSlider.value = minValue;
} else {
maxValue = Math.min(maxSliderVal, minValue + 1);
maxSlider.value = maxValue;
}
}
minTimeDisplay.value = this._formatTime(minValue);
maxTimeDisplay.value = this._formatTime(maxValue);
const minPercent = ((minValue - minSliderVal) / sliderRange) * 100;
const maxPercent = ((maxValue - minSliderVal) / sliderRange) * 100;
rangeHighlight.style.left = `${minPercent}%`;
rangeHighlight.style.width = `${maxPercent - minPercent}%`;
}
_handleTimeInputChange = (e) => {
const input = e.target;
let newSeconds = this.parseTime(input.value);
const minSlider = $("min-slider");
const maxSlider = $("max-slider");
if (isNaN(newSeconds)) {
input.value = this._formatTime(
input.id === "min-time" ? minSlider.value : maxSlider.value
);
return;
}
const minSliderVal = parseInt(minSlider.min);
const maxSliderVal = parseInt(minSlider.max);
newSeconds = Math.max(minSliderVal, Math.min(maxSliderVal, newSeconds));
if (input.id === "min-time") {
if (newSeconds >= parseInt(maxSlider.value)) {
newSeconds = Math.max(
minSliderVal,
parseInt(maxSlider.value) - 1
);
}
minSlider.value = newSeconds;
} else {
if (newSeconds <= parseInt(minSlider.value)) {
newSeconds = Math.min(
maxSliderVal,
parseInt(minSlider.value) + 1
);
}
maxSlider.value = newSeconds;
}
this._updateSliderUI(null);
};
setVideoLength(duration) {
const minSlider = $(CONSTANTS.DOM_IDS.MIN_SLIDER);
const maxSlider = $(CONSTANTS.DOM_IDS.MAX_SLIDER);
if (typeof duration !== "number" || duration < 1) {
console.error(
"Invalid duration provided to setVideoLength. Must be a number greater than 0."
);
minSlider.max = 0;
maxSlider.max = 0;
minSlider.value = 0;
maxSlider.value = 0;
return;
}
minSlider.max = duration;
maxSlider.max = duration;
const defaultMin = 0;
const defaultMax = duration;
minSlider.value = defaultMin;
maxSlider.value = defaultMax;
this._updateSliderUI(null);
}
}
document.addEventListener("DOMContentLoaded", () => {
const app = new YtDownloaderApp();
app.initialize();
});