EDIT: This code is now public on GitHub with more focused purpose in case anyone is interested. Thanks for the help!

This is only used on Mac machines. I know that there is a Python implementation of youtube-dl and several for ffmpeg, however I chose to use subprocess so that I can see the real-time stdout/stderr of them while videos are downloading/encoding.

# The goal

Easy to use (for non-programmers) interactive command line interface that can:

2. Optionally burn subtitles into the video.
• Either subtitles that the uploader entered or YouTube auto-generated subtitles (if they exist).

This is not meant to be an all-encompassing program for youtube-dl and ffmpeg and their many options, it is simply intended to perform the above 2 tasks easily.

# My main concerns

1. Ensuring that if this program fails on a particular video (or for any reason), it fails elegantly such that the lamen user doesn't have to do anything magical to get back on track and try another video.

2. In the long run I would like to build a GUI for this but I have no experience doing that yet. I'm hoping that I'm programming this in a way that is relatively easy to turn into a program with a GUI later, so any advice in that area will be greatly appreciated.

# The Code

#!/usr/bin/python

import shutil
import os
import urllib2
import subprocess

# Set paths
HOME = os.path.expanduser("~")

SUPPORTED_SUBS = ['.srt', '.sbv', '.sub', '.mpsub', '.lrc', '.cap', '.smi',
'.sami', '.rt', '.vtt', '.ttml', '.dfxp', '.scc', '.stl',
'.tds', '.cin', '.asc']

# Clear terminal window
def clear():
p = subprocess.Popen(['clear'])
p.communicate()

def intro_message():
print '-----------------------------------------------------------------'
print '-----------------------------------------------------------------\n'

# Create necessary directories
def make_dirs():
if not os.path.exists(folder):
os.mkdir(folder)

def get_url():
if not check_url(url):
print 'Something went wrong, please ensure you entered a valid URL.'
get_url()
return url

# Check if URL is valid
def check_url(url):
try:
urllib2.urlopen(url)
return True
except:
return False

def get_captions():
user_input = raw_input('Burn subtitles into video? (yes/no): ')
if not user_input[0].lower() == 'y':
return False
return True

# If user wants captions, ask if they want them auto-generated by YouTube
# This is not available for all videos
def get_auto_captions():
user_input = raw_input('Auto-generated subtitles? (yes/no): ')
if not user_input[0].lower() == 'y':
return False
return True

# Return tuple of filepaths to use for encoding/burning/moving
def get_files():
vid = subs = None
for f in files:
if f.startswith('.'):
continue
if os.path.splitext(f)[1] in SUPPORTED_SUBS:
subs = f
if os.path.splitext(f)[1] == '.mp4':
vid = f

# Burn in subs with FFMPEG
def burn_captions(VIDPATH, SUBSPATH):
filename = os.path.basename(VIDPATH)
# Prepend filename with "CC_" to indicate that it has captions
OUTPATH = os.path.join(ENCODING, 'CC_' + filename)
p = subprocess.Popen(['ffmpeg', '-i', VIDPATH, '-vf',
'subtitles={0}'.format(SUBSPATH),
OUTPATH])
p.communicate()
return [OUTPATH]

if not captions:
dl_with_no_cc(url)
elif auto_captions:
dl_with_auto_cc(url)
elif captions:
dl_with_cc(url)

def dl_with_no_cc(url):
'-f bestvideo[ext=mp4]+bestaudio[ext=m4a]/best '
'--merge-output-format mp4 '
'--restrict-filenames '
'--prefer-ffmpeg '
'-o "{path}%(title)s.%(ext)s" '
shell=True)
p.communicate()

def dl_with_cc(url):
'-f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best" '
'--merge-output-format mp4 '
'--write-srt '
'--sub-lang en '
'--restrict-filenames '
'--prefer-ffmpeg '
'-o "{path}%(title)s.%(ext)s" {url}'.format(
url=url)),
shell=True)
p.communicate()

def dl_with_auto_cc(url):
'-f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best" '
'--merge-output-format mp4 '
'--write-auto-sub '
'--sub-lang en '
'--restrict-filenames '
'--prefer-ffmpeg '
'-o "{path}%(title)s.%(ext)s" {url}'.format(
url=url)),
shell=True)
p.communicate()

def move_file(filepath):

def cleanup():
try:
shutil.rmtree(ENCODING)
except OSError:
pass

def main():
while True:
cleanup()
clear()
intro_message()
url = get_url()
captions = get_captions()
auto_captions = get_auto_captions() if captions else False
make_dirs()
files = get_files()
if captions:
files = burn_captions(*files)
move_file(filepath=files[0])
cleanup()

if __name__ == '__main__':
main()


From a first glance it looks okay, but there many things that can be written in a nicer / more idiomatic way.

The choice of using subprocess is completely fine - for the reason you mentioned, but also since libraries depending on their interface can also be much harder to use than their command line counterparts. Note that you don't seem to use the more complex features of subprocess, so perhaps os.system will suffice for you (at the moment) to save the reader some time.

• Python 3 compatibility would be good; in particular using print() as a function instead of a statement springs to mind.
• The comments above the functions are almost docstrings, they just have to be put into the functions - docstrings are a good idea to have documentation easily available in the REPL.
• Instead of the os.path.join call I'd probably do the expansion inline as the path with the ~ included is more amenable to putting it into a configuration file or exposing as a command line argument. Btw. though the calls as they are work thanks to how the join function works, joining HOME over and over again looks wrong, it should be:

DOWNLOAD_LOCATION = os.path.expanduser('~/Desktop/YT_Downloads/')


If that's not what you meant replace the DOWNLOAD_LOCATION with HOME of course.

• The SUPPORTED_SUBS list can be a set, that rarely matters though.
• For the clear function there are probably better ways (using escape codes) but it's probably alright here.
• In make_dirs it should be most likely os.makedirs as not every user might have ~/Desktop in the first place.
• AFAIK the CPython interpreter doesn't optimise tail recursion, so calling the function itself like in get_url is bad and in this case also wrong as no value is returned, respectively the first wrong value is returned. Consider using a loop, e.g.:

def get_url():
while True:
if check_url(url):
return url
print('Something went wrong, please ensure you entered a valid URL.')

• In get_captions and get_auto_captions the return value should be the value from the test immediately. The current code reads almost like if True == True:, it's just unnecessary complexity. Also consider moving this into a function like yes_or_no('<prompt here>').

• The dl_... functions are mostly duplicated code, better to have common arguments somewhere in a constant and then just add the additional things with format.
• I don't quite get the calling convention for burn_captions - if you have just two values, write it that way. The whole block in main looks like it can fail at any moment given the right filenames or something - if get_files doesn't match anything there will be an exception raised just because of the None values, thus terminating the whole program.

2. So as long as you have modular functions and keep in mind that you have separate processes running and you have a plan to pass in user input in some other way than by querying like get_url/get_captions/get_auto_captions currently do, then it should be relatively easy to adapt.