Skip to content

Commit 69eaeac

Browse files
committed
getting ready for combined downloads
1 parent 3a4abd6 commit 69eaeac

File tree

5 files changed

+196
-11
lines changed

5 files changed

+196
-11
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
![VideoSnatcher Logo](./assets/icon.png)
22
# VideoSnatcher
33
VideoSnatcher by JEMcats-Software allows for you to download videos from Youtube and similer platforms.
4+
45
## To-Do List
56
- [ ] Add Dark Mode
67
- [ ] Add Windows Support
8+
- [ ] Add Download Progress Bar
79
- [ ] Launch v1.0.0
810

11+
## Requirements
12+
You MUST have Rosetta installed if you are on a Mac with Apple Silicon. To do so please run this command in terminal.
13+
```
14+
/usr/sbin/softwareupdate --install-rosetta
15+
```
16+
917
## Support
1018
For questions open a discussion.
1119

index.js

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const fs = require('fs');
55
const path = require('path');
66
const os = require('os');
77
const https = require('https'); // Add this import
8+
const { v4: uuidv4 } = require('uuid');
9+
810

911
const expressApp = express();
1012
let serverPort, ytWin;
@@ -80,11 +82,23 @@ function ensureFile(filePath, url) {
8082
}
8183

8284
function parseVideoFormats(output) {
83-
const filtered = output.split('\n').filter(line =>
84-
line.trim() && !/(?:\[|\]|Downloading|Available formats|ID|-------------------)/.test(line)
85-
);
85+
const filtered = output.split('\n').filter(line => {
86+
line = line.trim();
87+
if (!line) return false;
88+
if (/(Downloading|Available formats|ID|-------------------)/.test(line)) return false;
89+
const matches = line.match(/\[[^\]]*\]/g);
90+
if (matches) {
91+
for (const m of matches) {
92+
const content = m.slice(1, -1);
93+
// Allow only if content has exactly two letters.
94+
if (!/^[A-Za-z]{2}$/.test(content)) return false;
95+
}
96+
}
97+
return true;
98+
});
8699

87100
const result = filtered.map(line => {
101+
if (line.toLowerCase().includes('quic')) return null; // Skip lines containing 'quic'
88102
let parts = line.split(/\s{2,}/).map(x => x.trim()).flatMap(part =>
89103
part.split('|').map(x => x.trim()).filter(Boolean)
90104
);
@@ -101,7 +115,7 @@ function parseVideoFormats(output) {
101115
}
102116
}
103117
return parts;
104-
}).filter(parts => parts.length > 1 && !parts.includes('images'));
118+
}).filter(parts => parts && parts.length > 1 && !parts.includes('images'));
105119

106120
// Remove duplicate entries
107121
const unique = Array.from(new Set(result.map(a => JSON.stringify(a)))).map(e => JSON.parse(e));
@@ -178,13 +192,13 @@ expressApp.get('/get_vid_options', (req, res) => {
178192

179193
if (!videoUrl) return res.status(400).send('Missing "url" parameter');
180194

181-
let cookiePart = videoUrl.includes("youtube.com")
195+
let parameters = videoUrl.includes("youtube.com")
182196
? ` --cookies "${path.join(userDataDir, 'yt-cookie.txt')}"`
183197
: '';
184-
cookiePart = vimeoPass
198+
parameters = vimeoPass
185199
? ` --video-password "${vimeoPass}"`
186200
: '';
187-
const command = `"${path.join(executablesDir, 'yt-dlp-mac')}"${cookiePart} -F "${videoUrl}"`;
201+
const command = `"${path.join(executablesDir, 'yt-dlp-mac')}"${parameters} -F "${videoUrl}"`;
188202

189203
exec(command, { encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout) => {
190204
if (error) {
@@ -193,7 +207,6 @@ expressApp.get('/get_vid_options', (req, res) => {
193207
}
194208
try {
195209
const parsed = parseVideoFormats(stdout);
196-
console.log(parsed);
197210
res.send(parsed);
198211
} catch (e) {
199212
console.error(e);
@@ -202,6 +215,49 @@ expressApp.get('/get_vid_options', (req, res) => {
202215
});
203216
});
204217

218+
expressApp.get('/download_vid', (req, res) => {
219+
const videoUrl = req.query.url;
220+
const vimeoPass = req.query.vimeoPass;
221+
const videoFormat = req.query.id;
222+
const videoType = req.query.type;
223+
224+
if (!videoUrl) return res.status(400).send('Missing "url" parameter');
225+
if (!videoFormat) return res.status(400).send('Missing "format" parameter');
226+
227+
let parameters = videoUrl.includes("youtube.com")
228+
? ` --cookies "${path.join(userDataDir, 'yt-cookie.txt')}"`
229+
: '';
230+
parameters = vimeoPass && vimeoPass !== undefined
231+
? ` --video-password "${vimeoPass}"`
232+
: '';
233+
234+
if (videoFormat.includes('vid') && videoFormat.includes('aud')) {
235+
const video_download_id = uuidv4();
236+
const audio_download_id = uuidv4();
237+
const [videoId, audioId] = videoFormat
238+
.split(',')
239+
.map(part => part.split(':')[1].trim());
240+
241+
} else {
242+
const video_download_id = uuidv4();
243+
244+
const command = `"${path.join(executablesDir, 'yt-dlp-mac')}"${parameters} -f "${videoFormat}" "${videoUrl}" -o "${path.join(baseDir, 'OutputFiles')}/${video_download_id}.${videoType}"`;
245+
exec(command, { encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout) => {
246+
if (error) {
247+
console.error(error);
248+
return res.status(500).send('Error downloading video');
249+
}
250+
try {
251+
const download_path = `${path.join(baseDir, 'OutputFiles')}/${video_download_id}.${videoType}`
252+
res.send(JSON.stringify({ download_path }));
253+
} catch (e) {
254+
console.error(e);
255+
res.status(500).send('Error downloading video');
256+
}
257+
});
258+
}
259+
});
260+
205261
expressApp.get('/show_supported_list', (req, res) => {
206262
createListWindow();
207263
res.send('Success');
@@ -235,6 +291,10 @@ app.whenReady().then(() => {
235291
path.join(executablesDir, 'yt-dlp-mac'),
236292
'https://jemcats.software/github_pages/VideoSnatcher/files/yt-dlp-mac'
237293
);
294+
ensureFile(
295+
path.join(executablesDir, 'ffmpeg-mac'),
296+
'https://jemcats.software/github_pages/VideoSnatcher/files/ffmpeg-mac'
297+
);
238298
createMainWindow();
239299
});
240300

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"dependencies": {
5858
"dialogs": "^2.0.1",
5959
"express": "^4.21.2",
60+
"uuid": "^11.1.0",
6061
"yt-dlp-wrap": "^2.3.12"
6162
}
6263
}

web/index.html

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
min-height: 100vh;
6868
font-family: sans-serif;
6969
position: relative;
70+
flex-direction: column;
7071
}
7172

7273
.icon-container {
@@ -87,6 +88,7 @@
8788
/* max-w-lg */
8889
width: 100%;
8990
text-align: center;
91+
margin-bottom: 2rem;
9092
}
9193

9294
.link-blue {
@@ -168,6 +170,75 @@
168170
<p onclick="view_supported()" class="link-blue">Over 900 sites supported!</p>
169171
<div id="loader" style="margin-bottom: 10px; display: none;" class="loader mx-auto"></div>
170172
</div>
173+
<div id="video_options" style="display: none;" class="card">
174+
<table style="width: 100%; border-collapse: collapse; margin-top: 1rem;">
175+
<thead>
176+
<tr style="background-color: #f1f5f9; /* Tailwind's bg-gray-200 */">
177+
<th style="padding: 0.5rem; border: 1px solid #e5e7eb; /* Tailwind's border-gray-300 */">Resolution</th>
178+
<th style="padding: 0.5rem; border: 1px solid #e5e7eb;">Contains</th>
179+
<th style="padding: 0.5rem; border: 1px solid #e5e7eb;">Format</th>
180+
<th style="padding: 0.5rem; border: 1px solid #e5e7eb;">Size</th>
181+
<th style="padding: 0.5rem; border: 1px solid #e5e7eb;">Download</th>
182+
</tr>
183+
</thead>
184+
<tbody id="video_options_table">
185+
<!-- Rows will be dynamically added here -->
186+
</tbody>
187+
</table>
188+
<div id="downloader_loader" style="margin-bottom: 10px; margin-top: 10px; display: none;" class="loader mx-auto"></div>
189+
<script>
190+
function populateTable(options) {
191+
const tableBody = document.getElementById("video_options_table");
192+
tableBody.innerHTML = ""; // Clear existing rows
193+
options.forEach(option => {
194+
const row = document.createElement("tr");
195+
row.style.border = "1px solid #e5e7eb";
196+
row.innerHTML = `
197+
<td style="padding: 0.5rem; text-align: center;">${option.res || "N/A"}</td>
198+
<td style="padding: 0.5rem; text-align: center;">${option.contains || "N/A"}</td>
199+
<td style="padding: 0.5rem; text-align: center;">${option.format || "N/A"}</td>
200+
<td style="padding: 0.5rem; text-align: center;">${option.size || "N/A"}</td>
201+
<td style="padding: 0.5rem; text-align: center;">
202+
<button onclick="downloadVideo('${option.id}', '${option.url}', '${option.pass}', '${option.format}')" style="background-color: #2563eb; color: white; padding: 0.25rem 0.5rem; border: none; border-radius: 0.25rem; cursor: pointer;">
203+
Download
204+
</button>
205+
</td>
206+
`;
207+
tableBody.appendChild(row);
208+
});
209+
}
210+
</script>
211+
</div>
212+
<script>
213+
function downloadVideo(id, url, pass, format) {
214+
document.getElementById("downloader_loader").style.display = "";
215+
let fetch_url;
216+
if (pass && pass !== "undefined") {
217+
fetch_url = `http://localhost:${getParam("port")}/download_vid?url=${url}&id=${id}&vimeoPass=${pass}&type=${format}`;
218+
} else {
219+
fetch_url = `http://localhost:${getParam("port")}/download_vid?url=${url}&id=${id}&format=${format}`;
220+
}
221+
fetch(fetch_url)
222+
.then(response => {
223+
if (response.status === 200) {
224+
response.json().then(data => {
225+
alert("Download finished!");
226+
document.getElementById("downloader_loader").style.display = "none";
227+
});
228+
} else {
229+
response.text().then(text => {
230+
alert("Failed to start download. Please try again.");
231+
document.getElementById("downloader_loader").style.display = "none";
232+
});
233+
}
234+
})
235+
.catch(error => {
236+
alert("Failed to start download. Please try again.");
237+
document.getElementById("downloader_loader").style.display = "none";
238+
});
239+
}
240+
</script>
241+
</div>
171242
<script>
172243
document.getElementById('url_input').addEventListener('input', function (event) {
173244
const inputField = event.target;
@@ -184,16 +255,22 @@
184255
let current_vid_options = [];
185256
const urlParams = new URLSearchParams(window.location.search);
186257

187-
function parseArrayToDictionary(arr) {
258+
function parseArrayToDictionary(arr, url, pass) {
188259
const result = {};
189260
result.id = arr[0];
261+
result.url = url;
262+
console.log(pass)
263+
if (pass) {
264+
result.pass = pass;
265+
}
190266
arr.forEach(value => {
191267
if (value.includes('x')) {
192268
result.res = value;
193269
} else if (value.includes('only') && value !== 'only') {
194270
result.contains = value;
195-
} else if (value.endsWith('MiB')) {
271+
} else if (value.endsWith('iB')) {
196272
result.size = value;
273+
result.size = result.size.replace("~", "")
197274
} else if (['mp4', 'mp3', 'webm'].includes(value)) {
198275
result.format = value;
199276
}
@@ -256,8 +333,34 @@
256333
document.getElementById("loader").style.display = "none";
257334
current_vid_options = [];
258335
data.forEach(option => {
259-
current_vid_options.push(parseArrayToDictionary(option));
336+
if (vimeo_pass.value !== "") {
337+
current_vid_options.push(parseArrayToDictionary(option, urlInput.value, vimeo_pass.value));
338+
} else {
339+
current_vid_options.push(parseArrayToDictionary(option, urlInput.value));
340+
}
341+
});
342+
343+
// Add combined options
344+
const videoOnlyOptions = current_vid_options.filter(opt => opt.contains === "video only");
345+
const audioOnlyOptions = current_vid_options.filter(opt => opt.contains === "audio only");
346+
347+
videoOnlyOptions.forEach(videoOption => {
348+
const lastAudioOption = audioOnlyOptions[audioOnlyOptions.length - 1];
349+
if (lastAudioOption) {
350+
current_vid_options.push({
351+
res: videoOption.res,
352+
contains: "combined",
353+
format: videoOption.format,
354+
size: videoOption.size,
355+
id: `vid: ${videoOption.id}, aud: ${lastAudioOption.id}`,
356+
url: videoOption.url,
357+
pass: videoOption.pass
358+
});
359+
}
260360
});
361+
362+
populateTable(current_vid_options);
363+
document.getElementById("video_options").style.display = "";
261364
console.log(current_vid_options);
262365
});
263366
return;

0 commit comments

Comments
 (0)