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
Using youtube-dl and ffmpeg
Easy to use (for non-programmers) interactive command line interface that can:
- Download a YouTube video.
- 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
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.
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("~")
DOWNLOAD_LOCATION = os.path.join(HOME, 'Desktop/YT_Downloads/')
DOWNLOADING = os.path.join(HOME, DOWNLOAD_LOCATION, '.downloading/')
ENCODING = os.path.join(HOME, DOWNLOAD_LOCATION, '.encoding/')
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 'Follow instructions to download a YouTube video:'
print 'Files will be downloaded to: {path}'.format(path=DOWNLOAD_LOCATION)
print '-----------------------------------------------------------------\n'
# Create necessary directories
def make_dirs():
for folder in [DOWNLOAD_LOCATION, DOWNLOADING, ENCODING]:
if not os.path.exists(folder):
os.mkdir(folder)
# Prompt user to enter YouTube link
def get_url():
url = raw_input('Enter YouTube link: ')
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
# Ask user if they would like to burn subtitles into video after download
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
files = os.listdir(DOWNLOADING)
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
return (os.path.join(DOWNLOADING, vid) if vid else None,
os.path.join(DOWNLOADING, subs) if subs else None)
# 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])
# Wait for download to finish
p.communicate()
return [OUTPATH]
def download(url, captions, auto_captions):
if not captions:
dl_with_no_cc(url)
elif auto_captions:
dl_with_auto_cc(url)
elif captions:
dl_with_cc(url)
# Download video with no captions
def dl_with_no_cc(url):
p = subprocess.Popen(('youtube-dl '
'-f bestvideo[ext=mp4]+bestaudio[ext=m4a]/best '
'--merge-output-format mp4 '
'--restrict-filenames '
'--prefer-ffmpeg '
'-o "{path}%(title)s.%(ext)s" '
'{url}'.format(path=DOWNLOADING, url=url)),
shell=True)
# Wait for download to finish
p.communicate()
# Download video + closed captions
def dl_with_cc(url):
p = subprocess.Popen(('youtube-dl '
'-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(
path=DOWNLOADING,
url=url)),
shell=True)
# Wait for download to finish
p.communicate()
# Download video + auto-generated captions
def dl_with_auto_cc(url):
p = subprocess.Popen(('youtube-dl '
'-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(
path=DOWNLOADING,
url=url)),
shell=True)
# Wait for download to finish
p.communicate()
def move_file(filepath):
shutil.move(filepath, DOWNLOAD_LOCATION)
# Remove downloads/encodes so we can start another
def cleanup():
try:
shutil.rmtree(DOWNLOADING)
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()
download(url, captions, auto_captions)
files = get_files()
if captions:
files = burn_captions(*files)
move_file(filepath=files[0])
cleanup()
if __name__ == '__main__':
main()