Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aandrew-me
GitHub Repository: aandrew-me/ytDownloader
Path: blob/main/src/compressor.js
448 views
1
const {exec, execSync} = require("child_process");
2
const path = require("path");
3
const {ipcRenderer, shell} = require("electron");
4
const os = require("os");
5
const si = require("systeminformation");
6
const {existsSync} = require("fs");
7
8
document.addEventListener("translations-loaded", () => {
9
window.i18n.translatePage();
10
});
11
12
let menuIsOpen = false;
13
14
getId("menuIcon").addEventListener("click", () => {
15
if (menuIsOpen) {
16
getId("menuIcon").style.transform = "rotate(0deg)";
17
menuIsOpen = false;
18
let count = 0;
19
let opacity = 1;
20
const fade = setInterval(() => {
21
if (count >= 10) {
22
getId("menu").style.display = "none";
23
clearInterval(fade);
24
} else {
25
opacity -= 0.1;
26
getId("menu").style.opacity = opacity.toFixed(3).toString();
27
count++;
28
}
29
}, 50);
30
} else {
31
getId("menuIcon").style.transform = "rotate(90deg)";
32
menuIsOpen = true;
33
34
setTimeout(() => {
35
getId("menu").style.display = "flex";
36
getId("menu").style.opacity = "1";
37
}, 150);
38
}
39
});
40
41
const ffmpeg = `"${getFfmpegPath()}"`;
42
43
console.log(ffmpeg);
44
45
const vaapi_device = "/dev/dri/renderD128";
46
47
// Checking GPU
48
si.graphics().then((info) => {
49
console.log({gpuInfo: info});
50
const gpuDevices = info.controllers;
51
52
gpuDevices.forEach((gpu) => {
53
// NVIDIA
54
const gpuName = gpu.vendor.toLowerCase();
55
const gpuModel = gpu.model.toLowerCase();
56
57
if (gpuName.includes("nvidia") || gpuModel.includes("nvidia")) {
58
document.querySelectorAll(".nvidia_opt").forEach((opt) => {
59
opt.style.display = "block";
60
});
61
} else if (
62
gpuName.includes("advanced micro devices") ||
63
gpuModel.includes("amd")
64
) {
65
if (os.platform() == "win32") {
66
document.querySelectorAll(".amf_opt").forEach((opt) => {
67
opt.style.display = "block";
68
});
69
} else {
70
document.querySelectorAll(".vaapi_opt").forEach((opt) => {
71
opt.style.display = "block";
72
});
73
}
74
} else if (gpuName.includes("intel")) {
75
if (os.platform() == "win32") {
76
document.querySelectorAll(".qsv_opt").forEach((opt) => {
77
opt.style.display = "block";
78
});
79
} else if (os.platform() != "darwin") {
80
document.querySelectorAll(".vaapi_opt").forEach((opt) => {
81
opt.style.display = "block";
82
});
83
}
84
} else {
85
if (os.platform() == "darwin") {
86
document
87
.querySelectorAll(".videotoolbox_opt")
88
.forEach((opt) => {
89
opt.style.display = "block";
90
});
91
}
92
}
93
});
94
});
95
96
/** @type {File[]} */
97
let files = [];
98
let activeProcesses = new Set();
99
let currentItemId = "";
100
let isCancelled = false;
101
102
/**
103
* @param {string} id
104
*/
105
function getId(id) {
106
return document.getElementById(id);
107
}
108
109
// File Handling
110
const dropZone = document.querySelector(".drop-zone");
111
const fileInput = getId("fileInput");
112
const selectedFilesDiv = getId("selected-files");
113
114
dropZone.addEventListener("dragover", (e) => {
115
e.preventDefault();
116
dropZone.classList.add("dragover");
117
});
118
119
dropZone.addEventListener("dragleave", () => {
120
dropZone.classList.remove("dragover");
121
});
122
123
dropZone.addEventListener("drop", (e) => {
124
e.preventDefault();
125
dropZone.classList.remove("dragover");
126
// @ts-ignore
127
console.log(e.dataTransfer);
128
files = Array.from(e.dataTransfer.files);
129
updateSelectedFiles();
130
});
131
132
fileInput.addEventListener("change", (e) => {
133
// @ts-ignore
134
files = Array.from(e.target.files);
135
updateSelectedFiles();
136
});
137
138
getId("custom-folder-select").addEventListener("click", (e) => {
139
ipcRenderer.send("get-directory", "");
140
});
141
142
function updateSelectedFiles() {
143
const fileList = files
144
.map((f) => `${f.name} (${formatBytes(f.size)})<br/>`)
145
.join("\n");
146
selectedFilesDiv.innerHTML = fileList || "No files selected";
147
}
148
149
// Compression Logic
150
getId("compress-btn").addEventListener("click", startCompression);
151
getId("cancel-btn").addEventListener("click", cancelCompression);
152
153
async function startCompression() {
154
if (files.length === 0) return alert("Please select files first!");
155
156
const settings = getEncoderSettings();
157
158
for (const file of files) {
159
const itemId =
160
"f" + Math.random().toFixed(10).toString().slice(2).toString();
161
currentItemId = itemId;
162
163
const outputPath = generateOutputPath(file, settings);
164
165
try {
166
await compressVideo(file, settings, itemId, outputPath);
167
168
if (isCancelled) {
169
isCancelled = false;
170
} else {
171
updateProgress("success", "", itemId);
172
const fileSavedElement = document.createElement("b");
173
fileSavedElement.textContent = i18n.__("fileSavedClickToOpen");
174
fileSavedElement.onclick = () => {
175
ipcRenderer.send("show-file", outputPath);
176
};
177
getId(itemId + "_prog").appendChild(fileSavedElement);
178
currentItemId = "";
179
}
180
} catch (error) {
181
const errorElement = document.createElement("div");
182
errorElement.onclick = () => {
183
ipcRenderer.send("error_dialog", error.message);
184
};
185
errorElement.textContent = i18n.__("errorClickForDetails");
186
updateProgress("error", "", itemId);
187
getId(itemId + "_prog").appendChild(errorElement);
188
currentItemId = "";
189
}
190
}
191
}
192
193
function cancelCompression() {
194
activeProcesses.forEach((child) => {
195
child.stdin.write("q");
196
isCancelled = true;
197
});
198
activeProcesses.clear();
199
updateProgress("error", "Cancelled", currentItemId);
200
}
201
202
/**
203
* @param {File} file
204
*/
205
function generateOutputPath(file, settings) {
206
console.log({settings});
207
const output_extension = settings.extension;
208
const parsed_file = path.parse(file.path);
209
210
let outputDir = settings.outputPath || parsed_file.dir;
211
212
if (output_extension == "unchanged") {
213
return path.join(
214
outputDir,
215
`${parsed_file.name}${settings.outputSuffix}${parsed_file.ext}`
216
);
217
}
218
219
return path.join(
220
outputDir,
221
`${parsed_file.name}_compressed.${output_extension}`
222
);
223
}
224
225
/**
226
* @param {File} file
227
* @param {{ encoder: any; speed: any; videoQuality: any; audioQuality?: any; audioFormat: string, extension: string }} settings
228
* @param {string} itemId
229
* @param {string} outputPath
230
*/
231
async function compressVideo(file, settings, itemId, outputPath) {
232
const command = buildFFmpegCommand(file, settings, outputPath);
233
234
console.log("Command: " + command);
235
236
return new Promise((resolve, reject) => {
237
const child = exec(command, (error) => {
238
if (error) reject(error);
239
else resolve();
240
});
241
242
activeProcesses.add(child);
243
child.on("exit", (_code) => {
244
activeProcesses.delete(child);
245
});
246
247
let video_info = {
248
duration: "",
249
bitrate: "",
250
};
251
252
createProgressItem(
253
path.basename(file.path),
254
"progress",
255
`Starting...`,
256
itemId
257
);
258
259
child.stderr.on("data", (data) => {
260
// console.log(data)
261
const duration_match = data.match(/Duration:\s*([\d:.]+)/);
262
if (duration_match) {
263
video_info.duration = duration_match[1];
264
}
265
266
// const bitrate_match = data.match(/bitrate:\s*([\d:.]+)/);
267
// if (bitrate_match) {
268
// // Bitrate in kb/s
269
// video_info.bitrate = bitrate_match[1];
270
// }
271
272
const progressTime = data.match(/time=(\d+:\d+:\d+\.\d+)/);
273
274
const totalSeconds = timeToSeconds(video_info.duration);
275
276
const currentSeconds =
277
progressTime && progressTime.length > 1
278
? timeToSeconds(progressTime[1])
279
: null;
280
281
if (currentSeconds && !isCancelled) {
282
const progress = Math.round(
283
(currentSeconds / totalSeconds) * 100
284
);
285
286
getId(
287
itemId + "_prog"
288
).innerHTML = `<progress class="progressBarCompress" min=0 max=100 value=${progress}>`;
289
}
290
});
291
});
292
}
293
294
/**
295
* @param {File} file
296
* @param {{ encoder: string; speed: string; videoQuality: string; audioQuality: string; audioFormat: string }} settings
297
* @param {string} outputPath
298
*/
299
function buildFFmpegCommand(file, settings, outputPath) {
300
const inputPath = file.path;
301
302
console.log("Output path: " + outputPath);
303
304
const args = ["-hide_banner", "-y", "-stats", "-i", `"${inputPath}"`];
305
306
switch (settings.encoder) {
307
case "copy":
308
args.push("-c:v", "copy");
309
break;
310
case "x264":
311
args.push(
312
"-c:v",
313
"libx264",
314
"-preset",
315
settings.speed,
316
"-vf",
317
"format=yuv420p",
318
"-crf",
319
parseInt(settings.videoQuality).toString()
320
);
321
break;
322
case "x265":
323
args.push(
324
"-c:v",
325
"libx265",
326
"-vf",
327
"format=yuv420p",
328
"-preset",
329
settings.speed,
330
"-crf",
331
parseInt(settings.videoQuality).toString()
332
);
333
break;
334
// Intel windows
335
case "qsv":
336
args.push(
337
"-c:v",
338
"h264_qsv",
339
"-vf",
340
"format=yuv420p",
341
"-preset",
342
settings.speed,
343
"-global_quality",
344
parseInt(settings.videoQuality).toString()
345
);
346
break;
347
// Linux amd and intel
348
case "vaapi":
349
args.push(
350
"-vaapi_device",
351
vaapi_device,
352
"-vf",
353
"format=nv12,hwupload",
354
"-c:v",
355
"h264_vaapi",
356
"-qp",
357
parseInt(settings.videoQuality).toString()
358
);
359
break;
360
case "hevc_vaapi":
361
args.push(
362
"-vaapi_device",
363
vaapi_device,
364
"-vf",
365
"format=nv12,hwupload",
366
"-c:v",
367
"hevc_vaapi",
368
"-qp",
369
parseInt(settings.videoQuality).toString()
370
);
371
break;
372
// Nvidia windows and linux
373
case "nvenc":
374
args.push(
375
"-c:v",
376
"h264_nvenc",
377
"-vf",
378
"format=yuv420p",
379
"-preset",
380
getNvencPreset(settings.speed),
381
"-rc",
382
"vbr",
383
"-cq",
384
parseInt(settings.videoQuality).toString()
385
);
386
break;
387
// Amd windows
388
case "hevc_amf":
389
let amf_hevc_quality = "balanced";
390
391
if (settings.speed == "slow") {
392
amf_hevc_quality = "quality";
393
} else if (settings.speed == "fast") {
394
amf_hevc_quality = "speed";
395
}
396
397
args.push(
398
"-c:v",
399
"hevc_amf",
400
"-vf",
401
"format=yuv420p",
402
"-quality",
403
amf_hevc_quality,
404
"-rc",
405
"cqp",
406
"-qp_i",
407
parseInt(settings.videoQuality).toString(),
408
"-qp_p",
409
parseInt(settings.videoQuality).toString()
410
);
411
break;
412
case "amf":
413
let amf_quality = "balanced";
414
415
if (settings.speed == "slow") {
416
amf_quality = "quality";
417
} else if (settings.speed == "fast") {
418
amf_quality = "speed";
419
}
420
421
args.push(
422
"-c:v",
423
"h264_amf",
424
"-vf",
425
"format=yuv420p",
426
"-quality",
427
amf_quality,
428
"-rc",
429
"cqp",
430
"-qp_i",
431
parseInt(settings.videoQuality).toString(),
432
"-qp_p",
433
parseInt(settings.videoQuality).toString(),
434
"-qp_b",
435
parseInt(settings.videoQuality).toString()
436
);
437
break;
438
case "videotoolbox":
439
args.push(
440
"-c:v",
441
"-vf",
442
"format=yuv420p",
443
"h264_videotoolbox",
444
"-q:v",
445
parseInt(settings.videoQuality).toString()
446
);
447
break;
448
}
449
450
// args.push("-vf", "scale=trunc(iw*1/2)*2:trunc(ih*1/2)*2,format=yuv420p");
451
452
args.push("-c:a", settings.audioFormat, `"${outputPath}"`);
453
454
return `${ffmpeg} ${args.join(" ")}`;
455
}
456
457
/**
458
*
459
* @returns {{ encoder: string; speed: string; videoQuality: string; audioQuality?: string; audioFormat: string, extension: string, outputPath:string }} settings
460
*/
461
function getEncoderSettings() {
462
return {
463
// @ts-ignore
464
encoder: getId("encoder").value,
465
// @ts-ignore
466
speed: getId("compression-speed").value,
467
// @ts-ignore
468
videoQuality: getId("video-quality").value,
469
// @ts-ignore
470
audioFormat: getId("audio-format").value,
471
// @ts-ignore
472
extension: getId("file_extension").value,
473
outputPath: getId("custom-folder-path").textContent,
474
// @ts-ignore
475
outputSuffix: getId("output-suffix").value,
476
};
477
}
478
479
/**
480
* @param {string | number} speed
481
*/
482
function getNvencPreset(speed) {
483
const presets = {fast: "p3", medium: "p4", slow: "p5"};
484
return presets[speed] || "p4";
485
}
486
487
/**
488
* @param {string} status
489
* @param {string} data
490
* @param {string} itemId
491
*/
492
function updateProgress(status, data, itemId) {
493
if (status == "success" || status == "error") {
494
const item = getId("itemId");
495
496
if (item) {
497
getId(itemId).classList.remove("progress");
498
getId(itemId).classList.add(status);
499
}
500
}
501
502
if (itemId) {
503
getId(itemId + "_prog").textContent = data;
504
}
505
}
506
507
/**
508
* @param {string} filename
509
* @param {string} status
510
* @param {string} data
511
* @param {string} itemId
512
*/
513
function createProgressItem(filename, status, data, itemId) {
514
const statusElement = getId("compression-status");
515
const newStatus = document.createElement("div");
516
newStatus.id = itemId;
517
newStatus.className = `status-item ${status}`;
518
const visibleFilename = filename.substring(0, 45);
519
newStatus.innerHTML = `
520
<div class="filename">${visibleFilename}</div>
521
<div id="${itemId + "_prog"}" class="itemProgress">${data}</div>
522
`;
523
statusElement.append(newStatus);
524
}
525
526
/**
527
* @param {any} bytes
528
*/
529
function formatBytes(bytes) {
530
const units = ["B", "KB", "MB", "GB"];
531
let size = bytes;
532
let unitIndex = 0;
533
while (size >= 1024 && unitIndex < units.length - 1) {
534
size /= 1024;
535
unitIndex++;
536
}
537
return `${size.toFixed(2)} ${units[unitIndex]}`;
538
}
539
540
/**
541
* @param {string} timeStr
542
*/
543
function timeToSeconds(timeStr) {
544
if (!timeStr) {
545
return 0;
546
}
547
548
const [hh, mm, ss] = timeStr.split(":").map(parseFloat);
549
return hh * 3600 + mm * 60 + ss;
550
}
551
552
function getFfmpegPath() {
553
if (
554
process.env.YTDOWNLOADER_FFMPEG_PATH &&
555
existsSync(process.env.YTDOWNLOADER_FFMPEG_PATH)
556
) {
557
console.log("Using FFMPEG from YTDOWNLOADER_FFMPEG_PATH");
558
return process.env.YTDOWNLOADER_FFMPEG_PATH;
559
}
560
561
switch (os.platform()) {
562
case "win32":
563
return path.join(__dirname, "..", "ffmpeg", "bin", "ffmpeg.exe");
564
case "freebsd":
565
try {
566
return execSync("which ffmpeg").toString("utf8").trim();
567
} catch (error) {
568
console.error("ffmpeg not found on FreeBSD:", error);
569
return "";
570
}
571
default:
572
return path.join(__dirname, "..", "ffmpeg", "bin", "ffmpeg");
573
}
574
}
575
576
getId("themeToggle").addEventListener("change", () => {
577
document.documentElement.setAttribute("theme", getId("themeToggle").value);
578
localStorage.setItem("theme", getId("themeToggle").value);
579
});
580
581
getId("output-folder-input").addEventListener("change", (e) => {
582
const checked = e.target.checked;
583
584
if (!checked) {
585
getId("custom-folder-select").style.display = "block";
586
} else {
587
getId("custom-folder-select").style.display = "none";
588
getId("custom-folder-path").textContent = "";
589
getId("custom-folder-path").style.display = "none";
590
}
591
});
592
593
const storageTheme = localStorage.getItem("theme");
594
if (storageTheme) {
595
document.documentElement.setAttribute("theme", storageTheme);
596
getId("themeToggle").value = storageTheme;
597
} else {
598
document.documentElement.setAttribute("theme", "frappe");
599
getId("themeToggle").value = "frappe";
600
}
601
602
ipcRenderer.on("directory-path", (_event, msg) => {
603
let customFolderPathItem = getId("custom-folder-path");
604
605
customFolderPathItem.textContent = msg;
606
customFolderPathItem.style.display = "inline";
607
});
608
609
function closeMenu() {
610
getId("menuIcon").style.transform = "rotate(0deg)";
611
let count = 0;
612
let opacity = 1;
613
const fade = setInterval(() => {
614
if (count >= 10) {
615
clearInterval(fade);
616
} else {
617
opacity -= 0.1;
618
getId("menu").style.opacity = String(opacity);
619
count++;
620
}
621
}, 50);
622
}
623
624
// Menu
625
getId("preferenceWin").addEventListener("click", () => {
626
closeMenu();
627
menuIsOpen = false;
628
ipcRenderer.send("load-page", __dirname + "/preferences.html");
629
});
630
631
getId("playlistWin").addEventListener("click", () => {
632
closeMenu();
633
menuIsOpen = false;
634
ipcRenderer.send("load-win", __dirname + "/playlist.html");
635
});
636
637
getId("aboutWin").addEventListener("click", () => {
638
closeMenu();
639
menuIsOpen = false;
640
ipcRenderer.send("load-page", __dirname + "/about.html");
641
});
642
643
getId("historyWin").addEventListener("click", () => {
644
closeMenu();
645
menuIsOpen = false;
646
ipcRenderer.send("load-page", __dirname + "/history.html");
647
});
648
649
getId("homeWin").addEventListener("click", () => {
650
closeMenu();
651
menuIsOpen = false;
652
ipcRenderer.send("load-win", __dirname + "/index.html");
653
});
654
655