diff --git a/main.js b/main.js index e58946f1..8b27dc08 100644 --- a/main.js +++ b/main.js @@ -61,6 +61,9 @@ function startCriticalHandlers(env) { taskList.load(); } + //Send the saved download type to the renderer + win.webContents.send("videoAction", {action: "setDownloadType", type: env.settings.downloadType}); + env.errorHandler = new ErrorHandler(win, queryManager, env); if(appStarting) { @@ -71,6 +74,16 @@ function startCriticalHandlers(env) { taskList.restore() }); + //Send the log for a specific download to renderer + ipcMain.handle("getLog", (event, identifier) => { + return env.logger.get(identifier); + }); + + //Save the log when renderer asks main + ipcMain.handle("saveLog", (event, identifier) => { + return env.logger.save(identifier); + }) + //Catch all console.log calls, print them to stdout and send them to the renderer devtools. console.log = (arg) => { process.stdout.write(arg + "\n"); @@ -205,6 +218,7 @@ function createWindow(env) { } app.on('ready', async () => { + app.setAppUserModelId("com.jelleglebbeek.youtube-dl-gui"); env = new Environment(app, analytics); await env.initialize(); createWindow(env); diff --git a/modules/Environment.js b/modules/Environment.js index a51dfd80..1d36fc96 100644 --- a/modules/Environment.js +++ b/modules/Environment.js @@ -2,6 +2,7 @@ const Bottleneck = require("bottleneck"); const Filepaths = require("./Filepaths"); const Settings = require("./persistence/Settings"); const DetectPython = require("./DetectPython"); +const Logger = require("./persistence/Logger"); const fs = require("fs").promises; class Environment { @@ -15,6 +16,7 @@ class Environment { this.mainAudioQuality = "best"; this.mainDownloadSubs = false; this.doneAction = "Do nothing"; + this.logger = new Logger(this); this.paths = new Filepaths(app, this); this.downloadLimiter = new Bottleneck({ trackDoneStatus: true, diff --git a/modules/Filepaths.js b/modules/Filepaths.js index 190a2128..68c01590 100644 --- a/modules/Filepaths.js +++ b/modules/Filepaths.js @@ -38,10 +38,10 @@ class Filepaths { break; } case "win32portable": - this.persistentPath = path.join(this.app.getPath('appData'), "youtube-dl-gui-portable"); + this.persistentPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR , "open-video-downloader"); this.unpackedPrefix = path.join(path.dirname(this.appPath), "app.asar.unpacked"); this.packedPrefix = this.appPath; - await this.createAppDataFolder(); + await this.createPortableFolder(); this.ffmpeg = path.join(this.persistentPath, "ffmpeg.exe"); this.ytdl = path.join(this.persistentPath, "youtube-dl.exe"); this.icon = path.join(this.packedPrefix, "renderer/img/icon.png"); @@ -97,7 +97,7 @@ class Filepaths { } detectPlatform() { - if(this.appPath.includes("\\AppData\\Local\\Temp\\")) return "win32portable"; + if(process.env.PORTABLE_EXECUTABLE_DIR != null) return "win32portable"; else if(this.appPath.includes("WindowsApps")) return "win32app" else return process.platform; } @@ -112,30 +112,67 @@ class Filepaths { } async createAppDataFolder() { - await new Promise((resolve) => { - mkdirp(this.persistentPath).then(made => { - if (made != null) { - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/youtube-dl.exe"), path.join(this.persistentPath, "youtube-dl.exe")); - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/ffmpeg.exe"), path.join(this.persistentPath, "ffmpeg.exe")); - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/AtomicParsley.exe"), path.join(this.persistentPath, "AtomicParsley.exe")); - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/ytdlVersion"), path.join(this.persistentPath, "ytdlVersion")); - } - resolve(); - }) - }) + const from = path.join(this.unpackedPrefix + "binaries"); + const toCopy = ["youtube-dl.exe", "ffmpeg.exe", "ytdlVersion", "AtomicParsley.exe"]; + await this.copyFiles(from, this.persistentPath, toCopy); + } + + async createPortableFolder() { + try { + await fs.promises.access(process.env.PORTABLE_EXECUTABLE_DIR, fs.constants.W_OK); + if(await this.migrateExistingAppDataFolder()) return; + const from = path.join(this.unpackedPrefix + "binaries"); + const toCopy = ["youtube-dl.exe", "ffmpeg.exe", "ytdlVersion", "AtomicParsley.exe"]; + await this.copyFiles(from, this.persistentPath, toCopy); + } catch (e) { + setTimeout(() => console.error(e), 5000); + this.persistentPath = path.join(this.app.getPath("appData"), "open-video-downloader"); + await this.createAppDataFolder(); + } + } + + async migrateExistingAppDataFolder() { + const from = path.join(this.app.getPath("appData"), "youtube-dl-gui-portable"); + try { + await fs.promises.access(from, fs.constants.W_OK); + const toCopy = ["youtube-dl.exe", "ffmpeg.exe", "ytdlVersion", "userSettings", "taskList"]; + await this.copyFiles(from, this.persistentPath, toCopy); + try { + await fs.promises.rmdir(from, {recursive: true}); + } catch (e) { + console.error(e); + } + return true; + } catch (e) { + return false; + } } async createHomeFolder() { + const from = path.join(this.unpackedPrefix + "binaries"); + const toCopy = ["youtube-dl-unix", "ffmpeg-linux", "ytdlVersion"]; + await this.copyFiles(from, this.persistentPath, toCopy); + } + + async copyFiles(from, to, files) { await new Promise((resolve) => { - mkdirp(this.persistentPath).then(made => { + mkdirp(to).then(made => { if (made != null) { - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/youtube-dl-unix"), path.join(this.persistentPath, "youtube-dl-unix")); - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/ffmpeg-linux"), path.join(this.persistentPath, "ffmpeg")); - fs.copyFileSync(path.join(this.unpackedPrefix, "binaries/ytdlVersion"), path.join(this.persistentPath, "ytdlVersion")); + for (const file of files) { + this.copyFile(from, to, file); + } } resolve(); - }) - }) + }); + }); + } + + copyFile(from, to, filename) { + try { + fs.copyFileSync(path.join(from, filename), path.join(to, filename)); + } catch (e) { + console.error("Could not copy " + filename + " to " + to + "."); + } } } diff --git a/modules/QueryManager.js b/modules/QueryManager.js index 46ce662e..9130b434 100644 --- a/modules/QueryManager.js +++ b/modules/QueryManager.js @@ -321,6 +321,7 @@ class QueryManager { this.managedVideos = this.managedVideos.filter(item => item.identifier !== video.identifier); this.playlistMetadata = this.playlistMetadata.filter(item => item.video_url !== video.url && item.playlist_url !== video.url); this.window.webContents.send("videoAction", { action: "remove", identifier: video.identifier }) + this.environment.logger.clear(video.identifier); } onError(identifier) { diff --git a/modules/download/DownloadQuery.js b/modules/download/DownloadQuery.js index 98d3a8e7..5ebc6fee 100644 --- a/modules/download/DownloadQuery.js +++ b/modules/download/DownloadQuery.js @@ -21,10 +21,15 @@ class DownloadQuery extends Query { let args = []; let output = path.join(this.environment.settings.downloadPath, Utils.resolvePlaylistPlaceholders(this.environment.settings.nameFormat, this.playlistMeta)); if(this.video.audioOnly) { - let numeralAudioQuality = (this.video.audioQuality === "best") ? "0" : "9"; + let audioQuality = this.video.audioQuality; + if(audioQuality === "best") { + audioQuality = "0"; + } else if(audioQuality === "worst") { + audioQuality = "9"; + } const audioOutputFormat = this.environment.settings.audioOutputFormat; args = [ - '--extract-audio', '--audio-quality', numeralAudioQuality, + '--extract-audio', '--audio-quality', audioQuality, '--ffmpeg-location', this.environment.paths.ffmpeg, '--no-mtime', '-o', output, @@ -101,6 +106,16 @@ class DownloadQuery extends Query { const perLine = liveData.split("\n"); for(const line of perLine) { this.video.setFilename(line); + if(line.lastIndexOf("[download]") !== line.indexOf("[download]")) { + const splitLines = line.split("["); + for(const splitLine of splitLines) { + if(splitLine.trim() !== "") { + this.environment.logger.log(this.video.identifier, "[" + splitLine.trim()); + } + } + } else { + this.environment.logger.log(this.video.identifier, line); + } } if (!liveData.includes("[download]")) return; if (!initialReset) { diff --git a/modules/exceptions/ErrorHandler.js b/modules/exceptions/ErrorHandler.js index 826eadd3..7ffef608 100644 --- a/modules/exceptions/ErrorHandler.js +++ b/modules/exceptions/ErrorHandler.js @@ -19,7 +19,7 @@ class ErrorHandler { console.error("An error has occurred but no error message was given.") return false; } - if(stderr.includes("WARNING")) { + if(stderr.trim().startsWith("WARNING:")) { console.warn(stderr); return; } diff --git a/modules/exceptions/errorDefinitions.json b/modules/exceptions/errorDefinitions.json index 79973b22..c81584a0 100644 --- a/modules/exceptions/errorDefinitions.json +++ b/modules/exceptions/errorDefinitions.json @@ -45,6 +45,11 @@ "Unsupported URL" ] }, + { + "code": "URL not found", + "description": "This is an incorrect or non-existing URL.", + "trigger": "[Errno 11001]" + }, { "code": "Private or non-existent playlist", "description": "This playlist does not exist or is private.", @@ -195,7 +200,7 @@ }, { "code": "Binaries missing/corrupted", - "description": "Please restart the app, or disable antivirus.", + "description": "Please restart the app and disable antivirus.", "trigger": [ "Command failed with ENOENT: resources\\app.asar.unpacked\\binaries\\youtube-dl.exe", "Command failed with ENOENT: binaries/youtube-dl.exe", diff --git a/modules/info/InfoQueryList.js b/modules/info/InfoQueryList.js index e8ed63cb..c3a48f4a 100644 --- a/modules/info/InfoQueryList.js +++ b/modules/info/InfoQueryList.js @@ -13,7 +13,7 @@ class InfoQueryList { } async start() { - let result = await new Promise(((resolve) => { + return await new Promise(((resolve) => { let totalMetadata = []; let playlistUrls = Utils.extractPlaylistUrls(this.query); for (const videoData of playlistUrls[1]) { @@ -43,7 +43,6 @@ class InfoQueryList { }); } })); - return result; } createVideo(data, url) { diff --git a/modules/persistence/Logger.js b/modules/persistence/Logger.js new file mode 100644 index 00000000..425fb3d6 --- /dev/null +++ b/modules/persistence/Logger.js @@ -0,0 +1,62 @@ +const {dialog} = require("electron"); +const path = require("path"); +const fs = require("fs"); + +class Logger { + constructor(environment) { + this.environment = environment; + this.logs = {}; + } + + log(identifier, line) { + if(line == null || line === "") return; + let trimmedLine; + if(line === "done") { + trimmedLine = "Download finished"; + } else if(line === "killed") { + trimmedLine = "Download stopped"; + } else { + trimmedLine = line.replace(/[\n\r]/g, ""); + } + if(identifier in this.logs) { + this.logs[identifier].push(trimmedLine); + } else { + this.logs[identifier] = [trimmedLine]; + } + } + + get(identifier) { + return this.logs[identifier]; + } + + clear(identifier) { + delete this.logs[identifier]; + } + + async save(identifier) { + const logLines = this.logs[identifier]; + let log = ""; + for(const line of logLines) { + log += line + "\n"; + } + const date = new Date().toLocaleString() + .replace(", ", "-") + .replace(/\//g, "-") + .replace(/:/g, "-") + let result = await dialog.showSaveDialog(this.environment.win, { + defaultPath: path.join(this.environment.settings.downloadPath, "ytdl-log-" + date.slice(0, date.length - 6)), + buttonLabel: "Save metadata", + filters: [ + { name: "txt", extensions: ["txt"] }, + { name: "All Files", extensions: ["*"] }, + ], + properties: ["createDirectory"] + }); + if(!result.canceled) { + fs.promises.writeFile(result.filePath, log).then(() => console.log("Download log saved.")); + } + } + +} + +module.exports = Logger; diff --git a/modules/persistence/Settings.js b/modules/persistence/Settings.js index e218e3cf..867cc377 100644 --- a/modules/persistence/Settings.js +++ b/modules/persistence/Settings.js @@ -3,7 +3,13 @@ const { globalShortcut, clipboard } = require('electron'); const fs = require("fs").promises; class Settings { - constructor(paths, env, outputFormat, audioOutputFormat, downloadPath, proxy, rateLimit, autoFillClipboard, globalShortcut, spoofUserAgent, validateCertificate, enableEncoding, taskList, nameFormat, nameFormatMode, sizeMode, splitMode, maxConcurrent, updateBinary, updateApplication, cookiePath, statSend, downloadMetadata, downloadThumbnail, keepUnmerged, calculateTotalSize, theme) { + constructor( + paths, env, outputFormat, audioOutputFormat, downloadPath, + proxy, rateLimit, autoFillClipboard, noPlaylist, globalShortcut, spoofUserAgent, + validateCertificate, enableEncoding, taskList, nameFormat, nameFormatMode, + sizeMode, splitMode, maxConcurrent, updateBinary, downloadType, updateApplication, cookiePath, + statSend, downloadMetadata, downloadThumbnail, keepUnmerged, calculateTotalSize, theme + ) { this.paths = paths; this.env = env this.outputFormat = outputFormat == null ? "none" : outputFormat; @@ -12,6 +18,7 @@ class Settings { this.proxy = proxy == null ? "" : proxy; this.rateLimit = rateLimit == null ? "" : rateLimit; this.autoFillClipboard = autoFillClipboard == null ? true : autoFillClipboard; + this.noPlaylist = noPlaylist == null ? false : noPlaylist; this.globalShortcut = globalShortcut == null ? true : globalShortcut; this.spoofUserAgent = spoofUserAgent == null ? true : spoofUserAgent; this.validateCertificate = validateCertificate == null ? false : validateCertificate; @@ -27,6 +34,7 @@ class Settings { this.splitMode = splitMode == null? "49" : splitMode; this.maxConcurrent = (maxConcurrent == null || maxConcurrent <= 0) ? Math.round(os.cpus().length / 2) : maxConcurrent; //Max concurrent is standard half of the system's available cores this.updateBinary = updateBinary == null ? true : updateBinary; + this.downloadType = downloadType == null ? "video" : downloadType; this.updateApplication = updateApplication == null ? true : updateApplication; this.cookiePath = cookiePath; this.statSend = statSend == null ? false : statSend; @@ -47,6 +55,7 @@ class Settings { data.proxy, data.rateLimit, data.autoFillClipboard, + data.noPlaylist, data.globalShortcut, data.spoofUserAgent, data.validateCertificate, @@ -58,6 +67,7 @@ class Settings { data.splitMode, data.maxConcurrent, data.updateBinary, + data.downloadType, data.updateApplication, data.cookiePath, data.statSend, @@ -82,6 +92,7 @@ class Settings { this.proxy = settings.proxy; this.rateLimit = settings.rateLimit; this.autoFillClipboard = settings.autoFillClipboard; + this.noPlaylist = settings.noPlaylist; this.globalShortcut = settings.globalShortcut; this.spoofUserAgent = settings.spoofUserAgent; this.validateCertificate = settings.validateCertificate; @@ -100,6 +111,7 @@ class Settings { this.env.changeMaxConcurrent(settings.maxConcurrent); } this.updateBinary = settings.updateBinary; + this.downloadType = settings.downloadType; this.updateApplication = settings.updateApplication; this.theme = settings.theme; this.save(); @@ -117,6 +129,7 @@ class Settings { proxy: this.proxy, rateLimit: this.rateLimit, autoFillClipboard: this.autoFillClipboard, + noPlaylist: this.noPlaylist, globalShortcut: this.globalShortcut, spoofUserAgent: this.spoofUserAgent, validateCertificate: this.validateCertificate, @@ -129,6 +142,7 @@ class Settings { maxConcurrent: this.maxConcurrent, defaultConcurrent: Math.round(os.cpus().length / 2), updateBinary: this.updateBinary, + downloadType: this.downloadType, updateApplication: this.updateApplication, cookiePath: this.cookiePath, statSend: this.statSend, @@ -142,7 +156,10 @@ class Settings { } save() { - fs.writeFile(this.paths.settings, JSON.stringify(this.serialize()), "utf8").then(() => console.log("Saved settings file.")); + console.log(this.serialize()); + fs.writeFile(this.paths.settings, JSON.stringify(this.serialize()), "utf8").then(() => { + console.log("Saved settings file.") + }); } setGlobalShortcuts() { diff --git a/modules/types/Query.js b/modules/types/Query.js index 150822f0..8f7bf1d4 100644 --- a/modules/types/Query.js +++ b/modules/types/Query.js @@ -45,6 +45,12 @@ class Query { args.push(this.environment.settings.rateLimit + "K"); } + if(this.environment.settings.noPlaylist) { + args.push("--no-playlist"); + } else { + args.push("--yes-playlist") + } + args.push(url) //Url must always be added as the final argument let command = this.environment.paths.ytdl; //Set the command to be executed @@ -98,6 +104,7 @@ class Query { resolve("done"); }); this.process.stderr.on("data", (data) => { + cb(data.toString()); if(this.environment.errorHandler.checkError(data.toString(), this.identifier)) { cb("killed"); resolve("killed"); diff --git a/package-lock.json b/package-lock.json index efdea96c..1420c6b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "youtube-dl-gui", - "version": "2.2.1", + "version": "2.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.2.1", + "version": "2.2.2", "license": "AGPL-3.0-only", "dependencies": { "@sentry/electron": "^2.5.0", @@ -22,6 +22,7 @@ "jquery": "^3.5.1", "mkdirp": "^1.0.4", "popper.js": "^1.16.1", + "sortablejs": "^1.14.0", "user-agents": "^1.0.586", "windowbar": "^1.7.4" }, @@ -8386,6 +8387,11 @@ "node": ">=0.10.0" } }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16298,6 +16304,11 @@ } } }, + "sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index d2478051..f0964752 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "jquery": "^3.5.1", "mkdirp": "^1.0.4", "popper.js": "^1.16.1", + "sortablejs": "^1.14.0", "user-agents": "^1.0.586", "windowbar": "^1.7.4" }, diff --git a/preload.js b/preload.js index 817974d4..13ce1ba9 100644 --- a/preload.js +++ b/preload.js @@ -40,7 +40,9 @@ contextBridge.exposeInMainWorld( "getDoneActions", "setDoneAction", "getSubtitles", - "getSelectedSubtitles" + "getSelectedSubtitles", + "getLog", + "saveLog" ]; if (validChannels.includes(channel)) { return await ipcRenderer.invoke(channel, data); diff --git a/renderer/.eslintrc.js b/renderer/.eslintrc.js index b934e894..a1944402 100644 --- a/renderer/.eslintrc.js +++ b/renderer/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { + "globals": { + "Sortable": "readonly" + }, "env": { "browser": true, "es2021": true, @@ -148,7 +151,7 @@ module.exports = { "no-multiple-empty-lines": "error", "no-negated-condition": "off", "no-nested-ternary": "error", - "no-new": "error", + "no-new": "off", "no-new-func": "error", "no-new-object": "error", "no-new-wrappers": "error", diff --git a/renderer/renderer.css b/renderer/renderer.css index 13e253a7..e9cdc5cb 100644 --- a/renderer/renderer.css +++ b/renderer/renderer.css @@ -204,6 +204,7 @@ select { } .video-card { + position: relative; max-width: 700px; min-height: 140px; max-height: 155px; @@ -214,6 +215,18 @@ select { box-shadow: 0 0.1em 0.15em var(--tertiary-bg-color); } +.video-card .handle { + position: absolute; + left: 0.25rem; + top: calc(50% + 2rem); + margin: -2rem 0; + padding: 2rem 0; + transform: translateY(-50%) scale(1.5); + filter: brightness(55%); + cursor: move; + z-index: 100; +} + .video-card p { white-space: nowrap; overflow: hidden; @@ -266,7 +279,7 @@ select { color: var(--font-color); } -.video-card i:hover { +.video-card i:hover :not(.bi-grip-vertical) { transition: 150ms color; filter: brightness(55%); } @@ -500,6 +513,27 @@ option.audio { } } +/* LOG MODAL */ +#logModal { + padding: 0 5rem; +} + +#logModal .modal-dialog { + max-width: 1200px; + margin: 1.75rem auto !important; +} + +#logModal .log { + overflow-y: auto; + max-height: 550px; + border-color: var(--tertiary-bg-color); + background-color: var(--secondary-bg-color); +} + +#logModal .log p { + margin-bottom: 0.5rem; +} + /* INFO MODAL */ #info-description { resize: none; diff --git a/renderer/renderer.html b/renderer/renderer.html index 11969533..258b969a 100644 --- a/renderer/renderer.html +++ b/renderer/renderer.html @@ -11,6 +11,7 @@ + @@ -79,6 +80,7 @@
+
Video thumbnail
@@ -108,6 +110,13 @@ - -
+
+ + +
+
+ + +
diff --git a/renderer/renderer.js b/renderer/renderer.js index 31147dd7..445e554e 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -3,6 +3,7 @@ let linkCopied = false; let progressCooldown = []; let sizeCooldown = []; let sizeCache = []; +let logUpdateTask; (function () { init(); })(); @@ -47,6 +48,15 @@ async function init() { $('#downloadBtn').click(); }) + //Init draggable cards + const dragArea = document.querySelector(".video-cards"); + new Sortable(dragArea, { + animation: 200, + sort: true, + draggable: ".video-card", + handle: ".handle" + }); + //Init the when done dropdown $('.dropdown-toggle').dropdown(); const availableOptions = await window.main.invoke('getDoneActions'); @@ -124,7 +134,13 @@ async function init() { updateSize($(card).prop('id'), false); }); - $('#download-quality, #download-type').on('change', () => updateAllVideoSettings()); + $('#download-quality').on('change', () => updateAllVideoSettings()); + + $('#download-type').on('change', async () => { + updateAllVideoSettings(); + await getSettings(); + sendSettings(); + }); $('#infoModal .img-overlay, #infoModal .info-img').on('click', () => { window.main.invoke("videoAction", {action: "downloadThumb", url: $('#infoModal .info-img').attr("src")}); @@ -142,6 +158,11 @@ async function init() { $('#authModal').modal("hide"); }); + $('#logModal .dismiss').on('click', () => { + $('#logModal').modal("hide"); + clearInterval(logUpdateTask); + }); + $('#subsModal .dismiss').on('click', () => { $('#subsModal').modal("hide"); }); @@ -160,34 +181,7 @@ async function init() { $('#settingsModal .apply').on('click', () => { $('#settingsModal').modal("hide"); - let settings = { - updateBinary: $('#updateBinary').prop('checked'), - updateApplication: $('#updateApplication').prop('checked'), - autoFillClipboard: $('#autoFillClipboard').prop('checked'), - globalShortcut: $('#globalShortcut').prop('checked'), - outputFormat: $('#outputFormat').val(), - audioOutputFormat: $('#audioOutputFormat').val(), - proxy: $('#proxySetting').val(), - spoofUserAgent: $('#spoofUserAgent').prop('checked'), - validateCertificate: $('#validateCertificate').prop('checked'), - enableEncoding: $('#enableEncoding').prop('checked'), - taskList: $('#taskList').prop('checked'), - nameFormatMode: $('#nameFormat').val(), - nameFormat: $('#nameFormatCustom').val(), - downloadMetadata: $('#downloadMetadata').prop('checked'), - downloadThumbnail: $('#downloadThumbnail').prop('checked'), - keepUnmerged: $('#keepUnmerged').prop('checked'), - calculateTotalSize: $('#calculateTotalSize').prop('checked'), - sizeMode: $('#sizeSetting').val(), - splitMode: $('#splitMode').val(), - rateLimit: $('#ratelimitSetting').val(), - maxConcurrent: parseInt($('#maxConcurrent').val()), - theme: $('#theme').val() - } - window.settings = settings; - window.main.invoke("settingsAction", {action: "save", settings}); - updateEncodingDropdown(settings.enableEncoding); - toggleWhiteMode(settings.theme); + sendSettings(); }); $('#maxConcurrent').on('input', () => { @@ -203,35 +197,9 @@ async function init() { } }) - $('#settingsBtn').on('click', () => { - window.main.invoke("settingsAction", {action: "get"}).then((settings) => { - $('#updateBinary').prop('checked', settings.updateBinary); - $('#updateApplication').prop('checked', settings.updateApplication); - $('#spoofUserAgent').prop('checked', settings.spoofUserAgent); - $('#validateCertificate').prop('checked', settings.validateCertificate); - $('#enableEncoding').prop('checked', settings.enableEncoding); - $('#taskList').prop('checked', settings.taskList); - $('#autoFillClipboard').prop('checked', settings.autoFillClipboard); - $('#globalShortcut').prop('checked', settings.globalShortcut); - $('#ratelimitSetting').val(settings.rateLimit); - $('#proxySetting').val(settings.proxy); - $('#nameFormatCustom').val(settings.nameFormat).prop("disabled", settings.nameFormatMode === "custom"); - $('#nameFormat').val(settings.nameFormatMode); - $('#outputFormat').val(settings.outputFormat); - $('#audioOutputFormat').val(settings.audioOutputFormat); - $('#downloadMetadata').prop('checked', settings.downloadMetadata); - $('#downloadThumbnail').prop('checked', settings.downloadThumbnail); - $('#keepUnmerged').prop('checked', settings.keepUnmerged); - $('#calculateTotalSize').prop('checked', settings.calculateTotalSize); - $('#maxConcurrent').val(settings.maxConcurrent); - $('#concurrentLabel').html(`Max concurrent jobs (${settings.maxConcurrent})`); - $('#sizeSetting').val(settings.sizeMode); - $('#splitMode').val(settings.splitMode); - $('#settingsModal').modal("show"); - $('#theme').val(settings.theme); - $('#version').html("Version: " + settings.version); - window.settings = settings; - }); + $('#settingsBtn').on('click', async () => { + await getSettings(); + $('#settingsModal').modal("show"); }); $('#defaultConcurrent').on('click', () => { @@ -269,7 +237,11 @@ async function init() { }) $('#infoModal .json').on('click', () => { - window.main.invoke('videoAction', {action: "downloadInfo", identifier: $('#infoModal .identifier').html()}) + window.main.invoke('videoAction', {action: "downloadInfo", identifier: $('#infoModal .identifier').html()}); + }); + + $('#logModal .save').on('click', () => { + window.main.invoke('saveLog', $('#logModal .identifier').html()); }); $('#clearBtn').on('click', () => { @@ -329,10 +301,11 @@ async function init() { } $(card).find('.progress').addClass("d-flex"); $(card).find('.metadata.left').html('Speed: ' + "0.00MiB/s").show(); - $(card).find('.metadata.right').html('ETA: ' + "Unknown"); + $(card).find('.metadata.right').html('ETA: ' + "Unknown").show(); $(card).find('.options').addClass("d-flex"); $(card).find('select').addClass("d-none"); $(card).find('.download-btn, .download-btn i, .subtitle-btn, .subtitle-btn i').addClass("disabled"); + changeDownloadIconToLog(card); if($(card).hasClass("unified")) { $(card).find('.metadata.left, .metadata.right').empty(); $(card).find('.info').addClass("d-flex").removeClass("d-none"); @@ -408,6 +381,10 @@ async function init() { case "setUnified": setUnifiedPlaylist(arg); break; + case "setDownloadType": + $('#download-type').val(arg.type); + updateAllVideoSettings(); + break; } }); @@ -483,9 +460,11 @@ function updateGlobalDownloadQuality() { const currentFormats = []; $('.video-cards').children().each(function() { if($(this).find('.download-btn i').hasClass("disabled")) return; - $(this).find('.custom-select.download-quality option.video').each(function() { - formats.push($(this).val()); - }) + if($(this).find('.custom-select.download-type').val() !== "audio") { + $(this).find('.custom-select.download-quality option.video').each(function () { + formats.push($(this).val()); + }) + } }) const sortedFormats = [...new Set(formats)]; sortedFormats.sort((a, b) => { @@ -520,7 +499,7 @@ function parseFormatString(string) { } async function addVideo(args) { - await settingExists(); + await getSettings(); let template = $('.template.video-card').clone(); $(template).removeClass('template'); $(template).prop('id', args.identifier); @@ -570,10 +549,10 @@ async function addVideo(args) { $(template).find('.subtitle-btn, .subtitle-btn i').addClass("disabled"); } - setCodecs(template, args.audioCodecs, args.formats); + setCodecs(template, args.audioCodecs, args.formats); $(template).find('.custom-select.download-quality').on('change', function () { - updateCodecs(template, this.value); + updateCodecs(template, this.value); }); $(template).find('.custom-select.download-type').change(); @@ -605,10 +584,11 @@ async function addVideo(args) { updateGlobalDownloadQuality(); $(template).find('.progress').addClass("d-flex"); $(template).find('.metadata.left').html('Speed: ' + "0.00MiB/s").show(); - $(template).find('.metadata.right').html('ETA: ' + "Unknown"); + $(template).find('.metadata.right').html('ETA: ' + "Unknown").show(); $(template).find('.options').addClass("d-flex"); $(template).find('select').addClass("d-none"); $(template).find('.download-btn i, .download-btn, .subtitle-btn, .subtitle-btn i').addClass("disabled"); + changeDownloadIconToLog(template); }); $(template).find('.subtitle-btn').on('click', () => { @@ -681,7 +661,7 @@ function removeVideo(card) { } async function setUnifiedPlaylist(args) { - await settingExists(); + await getSettings(); const card = getCard(args.identifier); $(card).addClass("unified"); $(card).append(``); @@ -744,6 +724,7 @@ async function setUnifiedPlaylist(args) { $(card).find('.metadata.info').html('Downloading playlist...'); $(card).find('select').addClass("d-none"); $(card).find('.download-btn i, .download-btn, .subtitle-btn, .subtitle-btn i').addClass("disabled"); + changeDownloadIconToLog(card); }); setCodecs(card, args.audioCodecs, args.formats); @@ -854,7 +835,7 @@ function updateProgress(args) { } if(!progressCooldown.includes(args.identifier)) { progressCooldown.push(args.identifier); - $(card).find('.metadata.right').html('ETA: ' + args.progress.eta); + $(card).find('.metadata.right').html('ETA: ' + args.progress.eta).show(); $(card).find('.metadata.left').html('Speed: ' + args.progress.speed).show(); setTimeout(() => { progressCooldown = progressCooldown.filter(item => item !== args.identifier); @@ -928,7 +909,11 @@ async function updateVideoSettings(identifier) { if(qualityValue === "best") { $(card).find('.custom-select.download-quality').val($(card).find(`.custom-select.download-quality option.${classValue}:first`).val()); } else if(qualityValue === "worst") { - $(card).find('.custom-select.download-quality').val($(card).find(`.custom-select.download-quality option.${classValue}:last`).val()); + if(isAudio) { + $(card).find('.custom-select.download-quality').val("worst"); + } else { + $(card).find('.custom-select.download-quality').val($(card).find(`.custom-select.download-quality option.${classValue}:last`).val()); + } } else if(!isAudio) { const formats = []; $(card).find('.custom-select.download-quality option.video').each(function() { @@ -959,7 +944,7 @@ async function updateVideoSettings(identifier) { } updateCodecs(card, $(card).find('.custom-select.download-quality').val()) if($(card).hasClass("unified")) return; - await settingExists(); + await getSettings(); if(oldQuality != null && oldType != null && (oldQuality !== $(card).find('.custom-select.download-quality').val() || oldType !== $(card).find('.custom-select.download-type').val())) { updateSize(identifier, false); } else if(window.settings.sizeMode === "full") { @@ -986,14 +971,72 @@ function updateAllVideoSettings() { }); } -async function settingExists() { - if(window.settings == null) { - window.settings = await window.main.invoke("settingsAction", {action: "get"}); +async function getSettings() { + const settings = await window.main.invoke("settingsAction", {action: "get"}); + $('#updateBinary').prop('checked', settings.updateBinary); + $('#updateApplication').prop('checked', settings.updateApplication); + $('#spoofUserAgent').prop('checked', settings.spoofUserAgent); + $('#validateCertificate').prop('checked', settings.validateCertificate); + $('#enableEncoding').prop('checked', settings.enableEncoding); + $('#taskList').prop('checked', settings.taskList); + $('#autoFillClipboard').prop('checked', settings.autoFillClipboard); + $('#noPlaylist').prop('checked', settings.noPlaylist); + $('#globalShortcut').prop('checked', settings.globalShortcut); + $('#ratelimitSetting').val(settings.rateLimit); + $('#proxySetting').val(settings.proxy); + $('#nameFormatCustom').val(settings.nameFormat).prop("disabled", settings.nameFormatMode === "custom"); + $('#nameFormat').val(settings.nameFormatMode); + $('#outputFormat').val(settings.outputFormat); + $('#audioOutputFormat').val(settings.audioOutputFormat); + $('#downloadMetadata').prop('checked', settings.downloadMetadata); + $('#downloadThumbnail').prop('checked', settings.downloadThumbnail); + $('#keepUnmerged').prop('checked', settings.keepUnmerged); + $('#calculateTotalSize').prop('checked', settings.calculateTotalSize); + $('#maxConcurrent').val(settings.maxConcurrent); + $('#concurrentLabel').html(`Max concurrent jobs (${settings.maxConcurrent})`); + $('#sizeSetting').val(settings.sizeMode); + $('#splitMode').val(settings.splitMode); + $('#theme').val(settings.theme); + $('#version').html("Version: " + settings.version); + window.settings = settings; +} + +function sendSettings() { + let settings = { + updateBinary: $('#updateBinary').prop('checked'), + updateApplication: $('#updateApplication').prop('checked'), + autoFillClipboard: $('#autoFillClipboard').prop('checked'), + noPlaylist: $('#noPlaylist').prop('checked'), + globalShortcut: $('#globalShortcut').prop('checked'), + outputFormat: $('#outputFormat').val(), + audioOutputFormat: $('#audioOutputFormat').val(), + proxy: $('#proxySetting').val(), + spoofUserAgent: $('#spoofUserAgent').prop('checked'), + validateCertificate: $('#validateCertificate').prop('checked'), + enableEncoding: $('#enableEncoding').prop('checked'), + taskList: $('#taskList').prop('checked'), + nameFormatMode: $('#nameFormat').val(), + nameFormat: $('#nameFormatCustom').val(), + downloadMetadata: $('#downloadMetadata').prop('checked'), + downloadThumbnail: $('#downloadThumbnail').prop('checked'), + keepUnmerged: $('#keepUnmerged').prop('checked'), + calculateTotalSize: $('#calculateTotalSize').prop('checked'), + sizeMode: $('#sizeSetting').val(), + splitMode: $('#splitMode').val(), + rateLimit: $('#ratelimitSetting').val(), + maxConcurrent: parseInt($('#maxConcurrent').val()), + downloadType: $('#download-type').val(), + theme: $('#theme').val() } + console.log(settings) + window.settings = settings; + window.main.invoke("settingsAction", {action: "save", settings}); + updateEncodingDropdown(settings.enableEncoding); + toggleWhiteMode(settings.theme); } async function updateTotalSize() { - await settingExists(); + await getSettings(); if(!window.settings.calculateTotalSize) return; let total = 0; for(const elem of sizeCache) { @@ -1139,12 +1182,60 @@ function changeSubsToRetry(url, card) { .find('i').removeClass("disabled"); } +function changeDownloadIconToLog(card) { + if(card == null) return; + $(card).find('.download-btn i') + .unbind() + .removeClass("bi-download") + .removeClass("disabled") + .addClass("bi-journal-text") + .on('click', () => { + const id = $(card).prop('id'); + $('#logModal').modal("show").find('.identifier').html(id); + $('#logModal .log').html("Loading log..."); + openLog(id); + logUpdateTask = setInterval(() => openLog(id), 1000); + }); + $(card).find('.download-btn') + .unbind() + .removeClass("disabled"); +} + +function openLog(identifier) { + const logBox = $('#logModal .log'); + window.main.invoke("getLog", identifier).then(log => { + if(log == null) { + $(logBox).val("No log was found for this video.") + } else { + let fullLog = ""; + for(const line of log) { + if(line.startsWith("WARNING")) { + const pre = line.slice(0, line.indexOf("W")) + "" + line.slice(line.indexOf("W")); + const suffixed = pre.slice(0, pre.indexOf(":") + 1) + "" + pre.slice(pre.indexOf(":") + 1); + fullLog += "

" + suffixed + "

"; + } else if(line.startsWith("ERROR")) { + const pre = line.slice(0, line.indexOf("E")) + "" + line.slice(line.indexOf("E")); + const suffixed = pre.slice(0, pre.indexOf(":") + 1) + "" + pre.slice(pre.indexOf(":") + 1); + fullLog += "

" + suffixed + "

"; + } else if(line.startsWith("[")) { + const pre = line.slice(0, line.indexOf("[")) + "" + line.slice(line.indexOf("[")); + const suffixed = pre.slice(0, pre.indexOf("]") + 1) + "" + pre.slice(pre.indexOf("]") + 1); + fullLog += "

" + suffixed + "

"; + } else { + fullLog += "

" + line + "

"; + } + } + $(logBox).html(fullLog); + } + }) +} + function setError(code, description, unexpected, identifier, url) { let card = getCard(identifier); $(card).append(``); $(card).find('.progress-bar').removeClass("progress-bar-striped").removeClass("progress-bar-animated").css("width", "100%").css('background-color', 'var(--error-color)'); $(card).find('.buttons').children().each(function() { - if($(this).hasClass("remove-btn") || $(this).hasClass("info-btn")) { + if($(this).hasClass("remove-btn") || $(this).hasClass("info-btn") || $(this).find("i").hasClass("bi-journal-text")) { $(this).removeClass("disabled").find('i').removeClass("disabled"); } else if($(this).hasClass("subtitle-btn")) { changeSubsToRetry(url, card); diff --git a/tests/Filepaths.test.js b/tests/Filepaths.test.js index 58a68b8d..c5b79d6e 100644 --- a/tests/Filepaths.test.js +++ b/tests/Filepaths.test.js @@ -68,12 +68,13 @@ describe('generate filepaths', () => { await instance.generateFilepaths(); expect(instance.createHomeFolder).toBeCalledTimes(1); }); - it('calls create appdata folder when this version is used', async () => { + it('calls create portable folder when this version is used', async () => { const instance = instanceBuilder(true, true); instance.platform = "win32portable"; - instance.createAppDataFolder = jest.fn().mockResolvedValue(undefined); + process.env.PORTABLE_EXECUTABLE_DIR = "test/dir/for/portable/executable"; + instance.createPortableFolder = jest.fn().mockResolvedValue(undefined); await instance.generateFilepaths(); - expect(instance.createAppDataFolder).toBeCalledTimes(1); + expect(instance.createPortableFolder).toBeCalledTimes(1); }) it('sets permissions on darwin and linux', async () => { const platforms = ["win32", "linux", "darwin"]; diff --git a/tests/Logger.test.js b/tests/Logger.test.js new file mode 100644 index 00000000..3a045eba --- /dev/null +++ b/tests/Logger.test.js @@ -0,0 +1,116 @@ +const fs = require("fs").promises; +const {dialog} = require("electron"); +const Logger = require("../modules/persistence/Logger"); + +const downloadPath = "a/download/path"; +const savePath = "path/to/log"; + +jest.mock('electron', () => ({ + dialog: { + showSaveDialog: jest.fn().mockResolvedValue({canceled: false, filePath: "path/to/log"}), + }, +})); + +beforeEach(() => { + jest.clearAllMocks(); + fs.writeFile = jest.fn().mockResolvedValue(""); + console.log = jest.fn().mockImplementation(() => {}); +}); + +describe("log", () => { + it("adds the line to the log", () => { + const instance = instanceBuilder(); + instance.logs = { + "identifier": ["first line"] + } + instance.log("identifier", "second line"); + expect(instance.logs.identifier).toEqual(["first line", "second line"]); + }); + it("creates a new log if it doesn't exist", () => { + const instance = instanceBuilder(); + instance.log("identifier", "first line"); + expect(instance.logs["identifier"]).toEqual(["first line"]); + }); + it("Replaces done and killed with readable values", () => { + const values = [{val: "done", res: "Download finished"}, {val: "killed", res: "Download stopped"}]; + for(const value of values) { + const instance = instanceBuilder(); + instance.log("identifier", value.val); + expect(instance.logs["identifier"]).toEqual([value.res]); + } + }); + it("trims all new lines from the log line", () => { + const instance = instanceBuilder(); + instance.log("identifier", "\nfirst line \na line break\n"); + expect(instance.logs["identifier"]).toEqual(["first line a line break"]); + }); + it("ignores the call if the line is empty or falsy", () => { + const instance = instanceBuilder(); + const values = ["", null, undefined]; + for(const value of values) { + instance.log("identifier", value); + expect(instance.logs["identifier"]).toBeUndefined(); + } + }) +}); + +describe("get", () => { + it("returns the log associated with the identifier", () => { + const instance = instanceBuilder(); + const log = ["first line", "second line"]; + const identifier = "identifier"; + instance.logs[identifier] = log; + expect(instance.get(identifier)).toEqual(log); + }); +}); + +describe("clear", () => { + it("removes the log associated with the identifier", () => { + const instance = instanceBuilder(); + const identifier = "identifier"; + instance.logs[identifier] = ["first line"]; + instance.clear(identifier); + expect(instance.logs[identifier]).toBeUndefined(); + }); +}); + +describe("save", () => { + it("saves the log", async () => { + const instance = instanceBuilder(); + const log = ["first line", "second line"]; + const identifier = "identifier"; + + instance.logs[identifier] = log; + await instance.save(identifier); + expect(fs.writeFile).toBeCalledTimes(1); + expect(fs.writeFile).toHaveBeenCalledWith(savePath, "first line\nsecond line\n"); + }); + it("asks the user where to save", async () => { + const instance = instanceBuilder(); + const log = ["first line", "second line"]; + const identifier = "identifier"; + + instance.logs[identifier] = log; + await instance.save(identifier); + expect(dialog.showSaveDialog).toBeCalledTimes(1); + }); + it("doesn't save when the user cancels", async () => { + dialog.showSaveDialog = jest.fn().mockResolvedValue({canceled: true, filePath: "path/to/log"}) + const instance = instanceBuilder(); + const log = ["first line", "second line"]; + const identifier = "identifier"; + instance.logs[identifier] = log; + await instance.save(identifier); + expect(fs.writeFile).not.toBeCalled(); + }) +}) + +function instanceBuilder() { + const environment = { + win: "i'm a window", + settings: { + downloadPath: downloadPath + } + } + return new Logger(environment); +} diff --git a/tests/Settings.test.js b/tests/Settings.test.js index aed4b0b7..07514a92 100644 --- a/tests/Settings.test.js +++ b/tests/Settings.test.js @@ -2,8 +2,8 @@ const fs = require('fs').promises; const os = require("os"); const Settings = require('../modules/persistence/Settings'); const env = {version: "2.0.0-test1", app: {getPath: jest.fn().mockReturnValue("test/path")}}; -const defaultSettingsInstance = new Settings({settings: "tests/test-settings.json"}, env, "none", "none", "test/path", "", "", true, true, true, false, false, true, "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "click", "49", 8, true, true, "C:\\Users\\user\\cookies.txt", false, true, false, false, true, "dark"); -const defaultSettings = "{\"outputFormat\":\"none\",\"audioOutputFormat\":\"none\",\"downloadPath\":\"test/path\",\"proxy\":\"\",\"rateLimit\":\"\",\"autoFillClipboard\":true,\"globalShortcut\":true,\"spoofUserAgent\":true,\"validateCertificate\":false,\"enableEncoding\":false,\"taskList\":true,\"nameFormat\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"nameFormatMode\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"sizeMode\":\"click\",\"splitMode\":\"49\",\"maxConcurrent\":8,\"defaultConcurrent\":8,\"updateBinary\":true,\"updateApplication\":true,\"statSend\":false,\"downloadMetadata\":true,\"downloadThumbnail\":false,\"keepUnmerged\":false,\"calculateTotalSize\":true,\"theme\":\"dark\",\"version\":\"2.0.0-test1\"}" +const defaultSettingsInstance = new Settings({settings: "tests/test-settings.json"}, env, "none", "none", "test/path", "", "", true, false, true, true, false, false, true, "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "%(title).200s-(%(height)sp%(fps).0d).%(ext)s", "click", "49", 8, true, "video", true, "C:\\Users\\user\\cookies.txt", false, true, false, false, true, "dark"); +const defaultSettings = "{\"outputFormat\":\"none\",\"audioOutputFormat\":\"none\",\"downloadPath\":\"test/path\",\"proxy\":\"\",\"rateLimit\":\"\",\"autoFillClipboard\":true,\"noPlaylist\":false,\"globalShortcut\":true,\"spoofUserAgent\":true,\"validateCertificate\":false,\"enableEncoding\":false,\"taskList\":true,\"nameFormat\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"nameFormatMode\":\"%(title).200s-(%(height)sp%(fps).0d).%(ext)s\",\"sizeMode\":\"click\",\"splitMode\":\"49\",\"maxConcurrent\":8,\"defaultConcurrent\":8,\"updateBinary\":true,\"downloadType\":\"video\",\"updateApplication\":true,\"statSend\":false,\"downloadMetadata\":true,\"downloadThumbnail\":false,\"keepUnmerged\":false,\"calculateTotalSize\":true,\"theme\":\"dark\",\"version\":\"2.0.0-test1\"}" describe('Load settings from file', () => { beforeEach(() => { diff --git a/tests/test-settings.json b/tests/test-settings.json index a9fdfc91..09c39c33 100644 --- a/tests/test-settings.json +++ b/tests/test-settings.json @@ -1 +1 @@ -{"outputFormat":"none","audioOutputFormat":"none","downloadPath": "test/path","proxy": "","rateLimit": "","autoFillClipboard":true,"globalShortcut":true,"spoofUserAgent":true,"validateCertificate": false,"enableEncoding": false,"taskList":true,"nameFormat":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","nameFormatMode":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","sizeMode":"click","splitMode":"49","maxConcurrent":8,"defaultConcurrent":8,"updateBinary":true,"updateApplication":true,"cookiePath":"C:\\Users\\user\\cookies.txt","statSend":false,"downloadMetadata":true,"downloadThumbnail":false,"keepUnmerged":false,"calculateTotalSize":true,"theme": "dark","version":"2.0.0-test1"} +{"outputFormat":"none","audioOutputFormat":"none","downloadPath": "test/path","proxy": "","rateLimit": "","autoFillClipboard":true,"noPlaylist": false,"globalShortcut":true,"spoofUserAgent":true,"validateCertificate": false,"enableEncoding": false,"taskList":true,"nameFormat":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","nameFormatMode":"%(title).200s-(%(height)sp%(fps).0d).%(ext)s","sizeMode":"click","splitMode":"49","maxConcurrent":8,"defaultConcurrent":8,"updateBinary":true,"downloadType":"video","updateApplication":true,"cookiePath":"C:\\Users\\user\\cookies.txt","statSend":false,"downloadMetadata":true,"downloadThumbnail":false,"keepUnmerged":false,"calculateTotalSize":true,"theme": "dark","version":"2.0.0-test1"}