[1] Youtube Proxy

   |   2 minute read   |   Using 338 words

The following script1 runs on vanilla Node.js and requires an executable of youtube-dl. If it’s not located in the same directory as the script, change the line

var job = spawn('./youtube-dl', args);

to something like

var job = spawn('~/path/to/youtube-dl', args);

Usage examples

Download a video in MP4 from YouTube:

http://example.com:8567/https://www.youtube.com/watch?v=XXXXXXX

Download a video from YouTube and extract audio in M4A format:

http://example.com:8567/https://www.youtube.com/watch?v=XXXXXXX.m4a

Currently, the headers it sets are designed for downloading. You can change them so that it plays in your browser, but I mainly wanted it for downloading videos on my phone and Firefox on Android wouldn’t download it without them.

Also, be aware that it needs a “downloads” directory that is not created automatically.

The Code

var spawn  = require('child_process').spawn,  
    fs     = require('fs'),  
    http   = require('http'),  
    mime   = require('mime');  

var server = http.createServer(function(req, res) {  
    var m   = req.url.match(/\/(.*)\.(.*?)/),  
    format  = m[2],  
    url     = m[1];  

    if (!/^.*\.[a-z0-9]{3,5}$/.test(req.url)) {  
        format  = 'mp4';  
        url     = req.url.match(/\/(.*)/)[1];  
    }  

    var file     = url.replace(/[\/:?!&=\.]/g, '') + '.' + format,  
        mimeType = mime.lookup(file),  
        args     = ['-o', 'downloads/' + file];  

    if (['m4a', 'mp3', 'opus'].indexOf(format) !== -1) {
        args = args.concat(['-x', '--audio-format']);  
    } else {  
        args.push('-f');  
    }  
    args.push(format);  
    args.push(url);  

    console.log(args.join(' '));  
    var job = spawn('./youtube-dl', args);  
    job.on('close', function(code, signal) {  
        var fileStream = fs.createReadStream('downloads/' + file);  
        pipeReadstream(req, res, fileStream, mimeType, file, function(err) {  
            console.log('error: ' + err);  
        });  
    });  

    job.stdout.on('data', function(data) {  
        console.log('stdout: ' + data);  
    });  

    job.stderr.on('data', function(data) {  
        console.log('stderr: ' + data);  
    });  
});  

server.listen(8567);  

// Pipe some stream as HTTP response  
function pipeReadstream(req, res, readStream, mimeType, filename, cb) {  
    var headWritten = false;  

    readStream.on('data', function(data) {  
        if (!headWritten) {  
            res.writeHead(200, {  
                'Content-Disposition': 'attachment; filename=' + filename,  
                'Content-Type': mimeType  
            });  
            headWritten = true;  
        }  

        var flushed = res.write(data);  
        // Pause the read stream when the write stream gets saturated  
        if (!flushed) {  
            readStream.pause();  
        }  
    });  

    res.on('drain', function() {  
        // Resume the read stream when the write stream gets hungry  
        readStream.resume();  
    });  

    readStream.on('end',   function()    { res.end(); });  
    readStream.on('error', function(err) { cb(err); });  
}

  1. Original comment on /tech/: https://lainchan.org/tech/res/5644.html#5658,
    original source code at https://pastebin.com/raw.php?i=amxBtmmj ↩︎