May 04, 2014 21:27 / 4 comments / flask python youtube

YouTube screenshot

I was cruising through some old bookmarked YouTube videos this evening and was sad to see that a few of my favorites were no longer available. I was able to search and find them again, but I decided it might not be a bad idea to save my favorites to my media server. I installed youtube-dl and was impressed by how well it just worked. Since I wanted to store these videos on my media server and watch them on Plex I then scp-ed the video files to the media server. Then it hit me -- why not just write a tiny web frontend for youtube-dl? Once I had a web app, then I could write a chrome extension to communicate with it...

With Flask the whole process took about 20 minutes, so I thought I'd share in case anyone else would find this useful.

The Flask App

I created a fresh virtualenv on my media server and installed the following packages:

Then I placed the following script in the root of the virtualenv. The code is intentionally simplistic since youtube-dl will be doing all the hard work:

import subprocess
import sys

from flask import Flask, flash, redirect, request, render_template, url_for

DEBUG = False
SECRET_KEY = 'this is needed for flash messages'

BINARY = '/path/to/bin/youtube-dl'
DEST_DIR = '/path/to/my/videos'
OUTPUT_TEMPLATE = '%s/%%(title)s-%%(id)s.%%(ext)s' % DEST_DIR

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def download():
    if request.method == 'POST':
        url = request.form['url']
        p = subprocess.Popen([BINARY, '-o', OUTPUT_TEMPLATE, '-q', url])
        flash('Successfully downloaded!', 'success')
        return redirect(url_for('download'))
    return render_template('download.html')

if __name__ == '__main__':'', port=8801)

When a video URL is POSTed to the download view, it will shell out to the youtube-dl binary and block until the download finishes. I'm specifying an output template for youtube-dl to use when saving the downloaded video, ensuring everything ends up in a directory watched by Plex.

A minimal template

I made a simple template using bootstrap, which looks like:

<!doctype html>
    <link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/youtuber.min.css') }}" />
    <script src="{{ url_for('static', filename='js/jquery-1.11.0.min.js') }}" type="text/javascript"></script>
    <script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
    <div class="container">
      {% for category, message in get_flashed_messages(with_categories=true) %}
        <div class="alert alert-{{ category }} alert-dismissable">
          <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
          <p>{{ message }}</p>
      {% endfor %}

      <form action="{{ url_for('download') }}" method="post">
        <div class="form-group">
          <label for="url">URL</label>
          <input type="text" name="url" class="form-control" id="url" placeholder="url" />
        <button type="submit" class="btn btn-primary">Download</button>

The Chrome Extension

The final piece to the puzzle is a Chrome extension that adds a button to the toolbar which, when clicked, will send the currently open URL to the Flask app for downloading (via an ajax request).

This extension will add a toolbar-icon and run in the background waiting for click events. In addition to a simple 16x16 icon, here are the three files required for the extension.

manifest.json stores metadata about the extension, including the name of the background script and the icon used in the toolbar:

/* manifest.json */
  "background": {"scripts": ["background.js"]},
  "browser_action": {
    "default_icon": "youtuber.png",
    "default_title": "youtuber"
  "name": "YouTuber",
  "description": "Store youtube vids on media server",
  "icons": {"16": "youtuber.png"},
  "permissions": [
  "version": "0.1",
  "manifest_version": 2

background.js binds a click handler which executes our YouTube script, making the ajax request.

/* background.js */
chrome.browserAction.onClicked.addListener(function(tab) {
  chrome.tabs.executeScript(, {file: "youtuber.js"});

Finally, here is youtuber.js, the script that POSTs the URL to the Flask app running on my media server. When the download finishes an ugly little message pops up letting me know it's done.

var youtubeURL = location.href;
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
  if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
    alert('Finished downloading ' + document.title);
  } else if (xmlhttp.readyState == 4) {
    alert('Something went wrong: ' + xmlhttp.status);
}'POST', 'http://my.server.url:8801/', true);
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xmlhttp.send('url=' + encodeURIComponent(youtubeURL));

Now any time I'm watching a YouTube video I want to save, I simply click the little toolbar icon and it shows up on my media server!

Thanks for reading

Thanks for taking the time to read this post. I hope you found it useful!

Comments (4)

Rod Montgomery | may 2014, at 09:38am

Plex is great, Flask is great, thanks for sharing this complete example.

However, for this particular use case there's already an excellent bookmarklet available from the Plex team that will queue up videos from all the big video streaming sites.

That said, I'm already thinking of a couple other projects that could use the pattern you've demonstrated in a concise bit of Flask -- nice!

Charles | may 2014, at 09:42am

will queue up videos from all the big video streaming sites.

That sounds very useful, thanks for sharing. Does that bookmarklet also download the video files themselves for you? Just to be clear, the Flask app + extension I've presented in this post will download the files themselves.

Rod Montgomery | may 2014, at 10:24am

As far as I know the Plex feature will add media to the Plex Queue fmor the bookmarklet or a link in an email message. It doesn't download the content and add it to the library, but it does support a wide range of media sites/sources, shown here:

Even so, the example you made looks great... thanks again.

kmonsoor | may 2014, at 11:51am

nice and concise article. going to get my hands dirty soon on this. these short examples enables newbies like me. hope to get more from you.

Commenting has been closed, but please feel free to contact me