Skip to content

Instantly share code, notes, and snippets.

@davawen
Created May 4, 2022 17:44
Show Gist options
  • Save davawen/2fabba1edc49bb4ce95473fb2c805828 to your computer and use it in GitHub Desktop.
Save davawen/2fabba1edc49bb4ce95473fb2c805828 to your computer and use it in GitHub Desktop.
const fs = require('fs');
const { spawn } = require('child_process');
async function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function main(index_url, concurrent_downloads, output_dir) {
let index = await fetch(index_url, {
method: 'GET',
headers: {
'Accept-Language': "en-US,en;q=0.5",
'Accept-Encoding': "gzip, deflate, br"
}
}).then(r => r.text()).catch(err => {
console.error(err);
console.log("Couldn't fetch index m3u8");
process.exit(-1);
});
index = index.split('\n');
let video_snippets_url = [];
let names = [];
for(const line of index) {
try {
let url = new URL(line);
let name = url.href.split('/');
name = name[name.length - 1];
video_snippets_url.push(url);
names.push(name);
}
catch(e) {
// Not a snippet url
}
}
// video_snippets_url.reverse();
/** @type {Promise<Buffer>[]} */
let buffer_promises = Array(video_snippets_url.length).fill(null);
let output_size = 0;
let total_size = 0;
let done = 0;
let num_downloading = 0;
let interrupt = false;
const terminate = async (code) => {
console.log(`\n\nTerminating prematurly, cleaning up and dumping video buffer to ${output_dir}.`);
interrupt = true;
// All synchronous
for(let i = 0; i < buffer_promises.length; i++) {
let name = names[i];
let path = `${process.argv[3]}/${name}`;
if(fs.existsSync(name) && fs.statSync(name).size > 1024) continue;
let buffer = await buffer_promises[i];
if(buffer == null) continue;
fs.writeFileSync(path, buffer);
}
process.exit(code);
};
console.log();
video_snippets_url.forEach(async (url, idx) => {
let name = names[idx];
let path = `${process.argv[3]}/${name}`;
buffer_promises[idx] = new Promise(async resolve => {
if(fs.existsSync(path) && fs.statSync(path).size > 1024) { // already downloaded
fs.readFile(path, (_, data) => resolve(data));
done++;
return;
}
while(num_downloading >= concurrent_downloads) {
if(interrupt) return resolve(null);
await sleep(50);
}
num_downloading++;
total_size = Math.floor(output_size / done * video_snippets_url.length); // Extrapolate from currently downloaded
console.log(`\x1b[1A\x1b[1G\x1b[2KFetching ${name}... ${Math.floor(output_size / 1024)}/${Math.floor(total_size / 1024)} KiB (${Math.floor(done / video_snippets_url.length * 100)} %)`);
let array_buffer = new ArrayBuffer(0);
while(array_buffer.byteLength < 1024) { // if less than a kilobyte of data, interpret it as failure
if(interrupt) return resolve(null);
try {
array_buffer = await fetch(url).then(res => res.arrayBuffer());
await sleep(10); // Time between retries
}
catch(err) {
console.error(err);
resolve(null);
terminate(-1);
return;
}
}
num_downloading--;
done++;
resolve(Buffer.from(array_buffer));
output_size += array_buffer.byteLength;
});
});
process.once('SIGINT', terminate);
const buffers = await Promise.all(buffer_promises);
// Create appendable write stream
let stream = fs.createWriteStream("output.ts", { flags: 'a' });
for(const b of buffers) {
stream.write(b);
}
stream.end();
console.log(`\nFinished writing file! ${output_size} bytes.`);
console.log(`Converting to mp4.`);
let ffmpeg = spawn("ffmpeg", [ "-i", "-", "-acodec", "copy", "-vcodec", "copy", "output.mp4" ]);
ffmpeg.on("exit", (code) => { console.log(`Finished converting to mp4! (code ${code})`) });
for(const b of buffers) {
ffmpeg.stdin.write(b);
}
ffmpeg.stdin.end();
}
if(process.argv.length < 3 || !process.argv[2] || !process.argv[3]) {
console.log("Usage: node download_guya.js {m3u8 request url} {output folder} [concurrent downloads]");
process.exit(-1);
}
fs.mkdirSync(process.argv[3], { recursive: true });
let workers = 0;
if(!process.argv[4]) {
console.log("No or invalid count given, concurrent downloads set to 10");
workers = 10;
}
else {
workers = parseInt(process.argv[4]);
if(workers == 0) workers = 10;
}
main(process.argv[2], workers, process.argv[3]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment