",
"license": "AGPL-3.0-only",
"devDependencies": {
"electron": "^26.2.2",
- "electron-builder": "^24.6.4",
+ "electron-builder": "^25.1.7",
"eslint": "^7.32.0",
"jest": "^27.3.0"
},
"dependencies": {
"adm-zip": "^0.5.10",
- "axios": "^0.21.4",
- "bootstrap": "^4.5.3",
+ "amqplib": "^0.10.4",
+ "axios": "^1.7.7",
+ "bootstrap": "^5.3.3",
"bootstrap-icons": "^1.3.0",
+ "bootstrap4-toggle": "^3.6.1",
"bottleneck": "^2.19.5",
"dotenv": "^16.3.1",
"electron-updater": "^6.1.4",
@@ -50,9 +55,12 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/StefanLobbenmeier/youtube-dl-gui.git"
+ "url": "https://github.com/mp3butcher/youtube-dl-gui.git"
},
"build": {
+ "extraResources": [
+ "binaries/send_traffic_to_videodownloader.py"
+ ],
"afterPack": "./build/appimage-fix.js",
"appId": "com.jelleglebbeek.youtube-dl-gui",
"asarUnpack": "**/binaries/*",
diff --git a/preload.js b/preload.js
index 5d14c467..7b84281f 100644
--- a/preload.js
+++ b/preload.js
@@ -23,7 +23,8 @@ contextBridge.exposeInMainWorld(
"getSubtitles",
"getSelectedSubtitles",
"getLog",
- "saveLog"
+ "saveLog",
+ "setScannerEnabled"
];
if (validChannels.includes(channel)) {
return await ipcRenderer.invoke(channel, data);
diff --git a/renderer/renderer.css b/renderer/renderer.css
index 5318ba12..5de02d43 100644
--- a/renderer/renderer.css
+++ b/renderer/renderer.css
@@ -128,6 +128,7 @@ select {
/* URL FORM */
.url-input {
+ display: inline-block;
margin-top: calc(40px + 1.5rem); /* Compensate the fixed navbar */
margin-bottom: 1.5rem;
}
@@ -147,6 +148,43 @@ select {
transform: scale(1.5);
}
+/* scanner toggle button*/
+.btns-input {
+ display: inline;
+ padding-left: 0px;
+ margin-top: calc(40px + 1.5rem); /* Compensate the fixed navbar */
+ margin-bottom: 1.5rem;
+}
+
+.btns-input .toggle-handle{
+ background-color:var(--font-color) !important;
+ border-color: var(--font-color) !important;
+}
+
+.btns-input .toggle-on {
+ background-color: var(--secondary-highlight-color);
+ border-color: var(--secondary-highlight-color);
+}
+
+.btns-input .toggle-off {
+ background-color: var(--secondary-bg-color);
+ border-color: var(--secondary-bg-color);
+}
+
+.btns-input .toggle {
+ background-color: var(--secondary-bg-color) !important;
+}
+
+.btns-input .btn {
+ border-color: transparent !important;
+ color: var(--font-color) !important;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 0.5;
+ border-top-right-radius: 3px !important;
+ border-bottom-right-radius: 3px !important;
+}
+
#settingsBtn {
margin-left: 1rem;
}
@@ -216,6 +254,7 @@ select {
}
.video-card .handle {
+ z-index: -1;
position: absolute;
left: 0.25rem;
top: calc(50% + 2rem);
@@ -415,6 +454,7 @@ option.audio {
.controls .buttons * {
margin: 0 5px;
+ width: 140px;
}
.controls .buttons i:before {
diff --git a/renderer/renderer.html b/renderer/renderer.html
index 17809640..514ba309 100644
--- a/renderer/renderer.html
+++ b/renderer/renderer.html
@@ -5,6 +5,7 @@
Open Video Downloader
+
@@ -12,6 +13,7 @@
+
@@ -22,9 +24,16 @@
-
+
+
+
+
+
+
+
+
@@ -177,6 +186,7 @@
Video metadata
+
@@ -460,6 +470,11 @@ Advanced
+
+
+
+
+
-
-
diff --git a/renderer/renderer.js b/renderer/renderer.js
index d08f911c..b0652d53 100644
--- a/renderer/renderer.js
+++ b/renderer/renderer.js
@@ -145,7 +145,7 @@ async function init() {
$('#download-type').on('change', async () => {
updateAllVideoSettings();
await getSettings();
- sendSettings();
+ await sendSettings();
});
$('#infoModal .img-overlay, #infoModal .info-img').on('click', () => {
@@ -186,8 +186,8 @@ async function init() {
});
$('#settingsModal .apply').on('click', () => {
- $('#settingsModal').modal("hide");
sendSettings();
+ $('#settingsModal').modal("hide");
});
$('#maxConcurrent').on('input', () => {
@@ -208,6 +208,15 @@ async function init() {
$('#settingsModal').modal("show");
});
+ $('#scannerBtn').on('change', async () => {
+ try{
+ await window.main.invoke('setScannerEnabled',{value: $('#scannerBtn').prop('checked')});
+ } catch (e) {
+ //Catch mainly proxu AbortError
+ console.error(e);
+ }
+ });
+
$('#defaultConcurrent').on('click', () => {
window.main.invoke("settingsAction", {action: "get"}).then((settings) => {
$('#concurrentLabel').html(`Max concurrent jobs (${settings.defaultConcurrent})`);
@@ -1003,6 +1012,7 @@ async function getSettings() {
$('#downloadThumbnail').prop('checked', settings.downloadThumbnail);
$('#keepUnmerged').prop('checked', settings.keepUnmerged);
$('#avoidFailingToSaveDuplicateFileName').prop('checked', settings.avoidFailingToSaveDuplicateFileName);
+ $('#allowUnsafeFileExtensions').prop('checked', settings.allowUnsafeFileExtensions);
$('#calculateTotalSize').prop('checked', settings.calculateTotalSize);
$('#maxConcurrent').val(settings.maxConcurrent);
$('#settingsModal #retries').val(settings.retries);
@@ -1015,7 +1025,7 @@ async function getSettings() {
window.settings = settings;
}
-function sendSettings() {
+async function sendSettings() {
let settings = {
updateBinary: $('#updateBinary').prop('checked'),
updateApplication: $('#updateApplication').prop('checked'),
@@ -1039,6 +1049,7 @@ function sendSettings() {
downloadThumbnail: $('#downloadThumbnail').prop('checked'),
keepUnmerged: $('#keepUnmerged').prop('checked'),
avoidFailingToSaveDuplicateFileName: $('#avoidFailingToSaveDuplicateFileName').prop('checked'),
+ allowUnsafeFileExtensions: $('#allowUnsafeFileExtensions').prop('checked'),
calculateTotalSize: $('#calculateTotalSize').prop('checked'),
sizeMode: $('#sizeSetting').val(),
splitMode: $('#splitMode').val(),
@@ -1050,7 +1061,7 @@ function sendSettings() {
theme: $('#theme').val()
}
window.settings = settings;
- window.main.invoke("settingsAction", {action: "save", settings});
+ await window.main.invoke("settingsAction", {action: "save", setting:settings});
updateEncodingDropdown(settings.enableEncoding);
toggleWhiteMode(settings.theme);
}
@@ -1138,7 +1149,10 @@ function showInfoModal(info, identifier) {
}
$(modal).find('img').prop("src", data.thumbnail);
$(modal).find('.modal-title').html(data.title);
- $(modal).find('#info-description').html(data.description == null ? "No description was found." : data.description);
+ let hs = "";
+ data.headers.forEach((h) => { hs = hs + h.k + ": " + h.v + "\n" } );
+ $(modal).find('#headers-string').html(hs);
+ $(modal).find('#info-description').html(data.description == null ? 'no description': data.description);
$(modal).find('.uploader').html('Uploader: ' + (data.uploader == null ? "Unknown" : data.uploader));
$(modal).find('.extractor').html('Extractor: ' + (data.extractor == null ? "Unknown" : data.extractor));
$(modal).find('.url').html('URL: ' + '' + data.url + '');
diff --git a/tests/InfoQuery.test.js b/tests/InfoQuery.test.js
index 2c186570..02d1087f 100644
--- a/tests/InfoQuery.test.js
+++ b/tests/InfoQuery.test.js
@@ -44,6 +44,6 @@ function instanceBuilder() {
},
settings: {}
};
- return [env, new InfoQuery("http://url.link", "test__id", env)];
+ return [env, new InfoQuery("http://url.link", [], "test__id", env)];
}
diff --git a/tests/InfoQueryList.test.js b/tests/InfoQueryList.test.js
index 93036914..e908606b 100644
--- a/tests/InfoQueryList.test.js
+++ b/tests/InfoQueryList.test.js
@@ -8,19 +8,19 @@ describe("create video", () => {
it('returns a video', () => {
const instance = instanceBuilder();
jest.spyOn(video.prototype, 'setMetadata').mockImplementation(() => {});
- const result = instance.createVideo({test: "data"}, "https://test.url");
+ const result = instance.createVideo({test: "data"}, "https://test.url",[]);
expect(result).toBeInstanceOf(video);
});
it('sets the metadata', () => {
const instance = instanceBuilder();
const metadataSpy = jest.spyOn(video.prototype, 'setMetadata').mockImplementation(() => {});
- instance.createVideo({test: "data"}, "https://test.url");
+ instance.createVideo({test: "data"}, "https://test.url",[]);
expect(metadataSpy).toBeCalledTimes(1);
});
it('uses data.entries[0] when appropriate', () => {
const instance = instanceBuilder();
const metadataSpy = jest.spyOn(video.prototype, 'setMetadata').mockImplementation(() => {});
- instance.createVideo({entries: [{formats: "insert_formats_here"}]}, "https://test.url");
+ instance.createVideo({entries: [{formats: "insert_formats_here"}]}, "https://test.url",[]);
expect(metadataSpy).toBeCalledWith({formats: "insert_formats_here"});
});
});
diff --git a/tests/MitmproxyUpdater.test.js b/tests/MitmproxyUpdater.test.js
new file mode 100644
index 00000000..07ad855f
--- /dev/null
+++ b/tests/MitmproxyUpdater.test.js
@@ -0,0 +1,129 @@
+const MitmproxyUpdater = require("../modules/MitmProxyUpdater");
+const fs = require("fs");
+const axios = require("axios");
+const { PassThrough } = require('stream');
+const os = require('os');
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ console.error = jest.fn().mockImplementation(() => {});
+ console.log = jest.fn().mockImplementation(() => {});
+})
+
+describe("writeVersionInfo", () => {
+ it('writes the version to a file', () => {
+ jest.spyOn(fs.promises, 'writeFile').mockResolvedValue("");
+ const instance = new MitmproxyUpdater({ mitmproxyVersion: "a/test/path" });
+ instance.writeVersionInfo("v2.0.0-test1");
+ expect(fs.promises.writeFile).toBeCalledTimes(1);
+ expect(fs.promises.writeFile).toBeCalledWith("a/test/path", "{\"version\":\"v2.0.0-test1\"}");
+ });
+});
+
+describe("getLocalVersion", () => {
+ it('returns null when when the file does not exist', () => {
+ jest.spyOn(fs.promises, 'readFile').mockRejectedValue("ENOTFOUND");
+ const instance = new MitmproxyUpdater({ ytdlVersion: "a/test/path" });
+ return instance.getLocalVersion().then((data) => {
+ expect(data).toBe(null);
+ });
+ });
+ it('returns the version property from the json file', () => {
+ jest.spyOn(fs.promises, 'readFile').mockResolvedValue("{\"version\": \"v2.0.0-test1\"}")
+ jest.spyOn(fs.promises, 'access').mockResolvedValue("");
+ const instance = new MitmproxyUpdater({ mitmproxyVersion: "a/test/path", mitmproxy: "Mitmproxy/path" });
+ return instance.getLocalVersion().then((data) => {
+ expect(data).toBe("v2.0.0-test1");
+ });
+ });
+ it('returns null when Mitmproxy is unset or false', () => {
+ jest.spyOn(fs.promises, 'readFile').mockResolvedValue("{\"version\": \"v2.0.0-test1\"}")
+ jest.spyOn(fs.promises, 'access').mockResolvedValue("");
+ const instance = new MitmproxyUpdater({ mitmproxyVersion: "a/test/path" });
+ return instance.getLocalVersion().then((data) => {
+ expect(data).toBe(null);
+ });
+ });
+});
+
+describe('getRemoteVersion', () => {
+ it('returns null on error', () => {
+ const axiosGetSpy = jest.spyOn(axios, 'get').mockRejectedValue({response: null});
+ jest.spyOn(os, 'arch').mockReturnValue('x64');
+ const instance = new MitmproxyUpdater({platform: "darwin"});
+ return instance.getRemoteVersion().then((data) => {
+ expect(data).toEqual({
+ remoteVersion: "10.4.2",
+ remoteMitmProxyUrl: "https://github.com/mp3butcher/mitmproxy/releases/download/zipped/mitmproxy.macos-arm.zip"
+ });
+ expect(axiosGetSpy).toBeCalledTimes(0);
+ });
+ });
+ it('returns object with the links and the version', async () => {
+ const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValue({
+ data: { version: "4.2.1", bin: { "windows-32": { mitmproxy: "Mitmproxy/link"} } },
+ });
+ jest.spyOn(os, 'arch').mockReturnValue('ia32');
+ Object.defineProperty(process, "platform", {
+ value: "win32"
+ });
+ const instance = new MitmproxyUpdater({platform: "win32"});
+ const result = await instance.getRemoteVersion();
+ expect(result).toEqual({
+ remoteVersion: "10.4.2",
+ remoteMitmProxyUrl: "https://github.com/mp3butcher/mitmproxy/releases/download/zipped/mitmproxy.windows-amd64.zip"
+ });
+ expect(axiosGetSpy).toBeCalledTimes(0);
+ });
+});
+
+describe('checkUpdate', () => {
+ it('does nothing when local and remote version are the same', () => {
+ const win = {webContents: {send: jest.fn()}};
+ const instance = new MitmproxyUpdater({platform: "win32"}, win);
+ instance.paths.setPermissions = jest.fn();
+ const downloadUpdateSpy = jest.spyOn(instance, 'downloadUpdate');
+ jest.spyOn(instance, 'getLocalVersion').mockResolvedValue("v2.0.0");
+ jest.spyOn(instance, 'getRemoteVersion').mockResolvedValue({ remoteMitmproxyUrl: "link", remoteVersion: "v2.0.0" });
+ return instance.checkUpdate().then(() => {
+ expect(downloadUpdateSpy).not.toBeCalled();
+ expect(instance.win.webContents.send).not.toBeCalled();
+ });
+ });
+ it('does nothing when remote version returned null', () => {
+ const win = {webContents: {send: jest.fn()}};
+ const instance = new MitmproxyUpdater({platform: "win32"}, win);
+ instance.paths.setPermissions = jest.fn();
+ const downloadUpdateSpy = jest.spyOn(instance, 'downloadUpdate');
+ jest.spyOn(instance, 'getLocalVersion').mockResolvedValue("v2.0.0");
+ jest.spyOn(instance, 'getRemoteVersion').mockResolvedValue({ remoteMitmproxyUrl: "link", remoteVersion: null });
+ return instance.checkUpdate().then(() => {
+ expect(downloadUpdateSpy).not.toBeCalled();
+ expect(instance.win.webContents.send).not.toBeCalled();
+ });
+ });
+ it('downloads the latest remote version when local version is null', () => {
+ const win = {webContents: {send: jest.fn()}};
+ const instance = new MitmproxyUpdater({platform: "win32"}, win);
+ instance.paths.setPermissions = jest.fn();
+ const downloadUpdateSpy = jest.spyOn(instance, 'downloadUpdate').mockResolvedValue("");
+ jest.spyOn(instance, 'getLocalVersion').mockResolvedValue(null);
+ jest.spyOn(instance, 'getRemoteVersion').mockResolvedValue({ remoteMitmproxyUrl: "link", remoteVersion: "v2.0.0" });
+ return instance.checkUpdate().then(() => {
+ expect(downloadUpdateSpy).toBeCalledTimes(1);
+ expect(instance.win.webContents.send).toBeCalledTimes(1);
+ });
+ });
+ it('downloads the latest remote version when local version is different', () => {
+ const win = {webContents: {send: jest.fn()}};
+ const instance = new MitmproxyUpdater({platform: "win32", ytdl: "a/path/to"}, win);
+ instance.paths.setPermissions = jest.fn();
+ const downloadUpdateSpy = jest.spyOn(instance, 'downloadUpdate').mockResolvedValue("");
+ jest.spyOn(instance, 'getLocalVersion').mockResolvedValue("2021.03.10");
+ jest.spyOn(instance, 'getRemoteVersion').mockResolvedValue({ remoteMitmproxyUrl: "link", remoteFfprobeUrl: "link", remoteVersion: "2021.10.10" });
+ return instance.checkUpdate().then(() => {
+ expect(downloadUpdateSpy).toBeCalledTimes(1);
+ expect(instance.win.webContents.send).toBeCalledTimes(1);
+ });
+ });
+});
diff --git a/tests/Settings.test.js b/tests/Settings.test.js
index adcd8f84..63743c1d 100644
--- a/tests/Settings.test.js
+++ b/tests/Settings.test.js
@@ -15,7 +15,7 @@ jest.mock('electron', () => ({
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, false, true, 'spoof', false, false, true, '%(title).200s-(%(height)sp%(fps).0d).%(ext)s', '%(title).200s-(%(height)sp%(fps).0d).%(ext)s', 'click', '49', 8, 6, 2, true, 'video', true, 'C:\\Users\\user\\cookies.txt', false, '', '', 'https://sponsor.ajay.app', true, false, false, false, false, true, 'dark');
+const defaultSettingsInstance = new Settings({settings: 'tests/test-settings.json'}, env, 'none', 'none', 'test/path', '', '', true, false, true, 'spoof', false, false, true, '%(title).200s-(%(height)sp%(fps).0d).%(ext)s', '%(title).200s-(%(height)sp%(fps).0d).%(ext)s', 'click', '49', 8, 6, 2, true, 'video', true, 'C:\\Users\\user\\cookies.txt', false, '', '', 'https://sponsor.ajay.app', true, false, false, false, false, false, true, 'dark');
const defaultSettings = {
outputFormat: 'none',
audioOutputFormat: 'none',
@@ -49,6 +49,7 @@ const defaultSettings = {
downloadThumbnail: false,
keepUnmerged: false,
avoidFailingToSaveDuplicateFileName: false,
+ allowUnsafeFileExtensions: false,
calculateTotalSize: true,
theme: 'dark',
version: '2.0.0-test1',
diff --git a/tests/Utils.test.js b/tests/Utils.test.js
index 868d704e..2893e451 100644
--- a/tests/Utils.test.js
+++ b/tests/Utils.test.js
@@ -98,13 +98,13 @@ describe('extractPlaylistUrls', () => {
expect(console.error).toBeCalledTimes(1);
});
it('returns playlist urls',() => {
- expect(Utils.extractPlaylistUrls({entries: [{url: "1"}, {url: "2"}]})).toContainEqual(["1", "2"])
+ expect(Utils.extractPlaylistUrls({entries: [{url: "1"}, {url: "2"}]})).toContainEqual([{headers: undefined, url: "1"}, {headers: undefined, url: "2"}])
});
it('returns already done (queried) urls', () => {
- expect(Utils.extractPlaylistUrls({entries: [{url: "1", formats: ["format"]}, {url: "2"}]})).toContainEqual([{url: "1", formats: ["format"]}]);
+ expect(Utils.extractPlaylistUrls({entries: [{url: "1", formats: ["format"]}, {url: "2"}]})).toContainEqual([{url: "1", headers: undefined, formats: ["format"]}]);
});
it('uses the webpage_url when url is null', () => {
- expect(Utils.extractPlaylistUrls({entries: [{webpage_url: "url"}]})).toContainEqual(["url"]);
+ expect(Utils.extractPlaylistUrls({entries: [{webpage_url: "url"}]})).toContainEqual([{url:"url", "headers": undefined}]);
});
});
diff --git a/tests/Video.test.js b/tests/Video.test.js
index 33357d86..ce2d616c 100644
--- a/tests/Video.test.js
+++ b/tests/Video.test.js
@@ -68,7 +68,7 @@ function instanceBuilder(type) {
nameFormatMode: "%(title).200s-(%(height)sp%(fps).0d).%(ext)s"
}
};
- const video = new Video("http://test.url", type, env);
+ const video = new Video("http://test.url", [], type, env);
let formats = [];
const displayNames = ["144p12", "1080p", "480p", "480p30", "480p29", "1080p60"];
for (const name of displayNames) {
diff --git a/tests/test-settings.json b/tests/test-settings.json
index 95899223..d94b4b89 100644
--- a/tests/test-settings.json
+++ b/tests/test-settings.json
@@ -32,6 +32,7 @@
"downloadThumbnail": false,
"keepUnmerged": false,
"avoidFailingToSaveDuplicateFileName": false,
+ "allowUnsafeFileExtensions": false,
"calculateTotalSize": true,
"theme": "dark",
"version": "2.0.0-test1"