Skip to content

Instantly share code, notes, and snippets.

@sammdu
Last active September 10, 2020 00:16
Show Gist options
  • Save sammdu/59785713c336a6c312b5880f6ed6ffb0 to your computer and use it in GitHub Desktop.
Save sammdu/59785713c336a6c312b5880f6ed6ffb0 to your computer and use it in GitHub Desktop.
[ytdl.py] Download a list of songs from YouTube, at the best audio quality. Can optionally download videos as well.
#!/usr/bin/env python3
import subprocess
from argparse import ArgumentParser
from pathlib import Path
from sys import exit, stderr
# [1/3] COMMAND LINE ARGUMENTS
parser = ArgumentParser()
parser.add_argument(
"playlist",
metavar="PLAYLIST_FILE",
nargs="?",
default="list.txt",
help='text file containing a list of URLs to download; default to "list.txt"',
)
parser.add_argument(
"-v",
"--video",
action="store_true", # true if provided, false if omitted
help="download video instead of audio",
)
parser.add_argument(
"-k",
"--keep-original",
action="store_true", # true if provided, false if omitted
help="keep the original file name and don't do any cleanups, "
+ "such as removing dots, spaces, single-quotes, and ampersands",
)
parser.add_argument(
"-n",
"--no-metadata",
action="store_true", # true if provided, false if omitted
help="prevent writing the full title information to the file's metadata using ffmpeg",
)
parser.add_argument(
"-o",
"--output",
metavar="OUTPUT_DIRECTORY",
default="./",
help="where to store the downloaded files; default to the current directory",
)
args = parser.parse_args()
# [2/3] FUNCTION AND CLASS DEFINITIONS
class ansicolors:
# put one of these sequences at the beginning of a string
RED = "\033[91m"
GREEN = "\033[92m"
BLUE = "\033[94m"
YELLOW = "\033[93m"
# put this sequence after a string to end the colored area
END = "\033[0m"
def filename_cleanup(filename):
"""
removes undesired characters by applying the defined
replacement table to the string; also makes letters
lower-case
"""
# define replacement table
rtable = {
".": "",
",": "",
" ": "-",
"‘": "",
"’": "",
"(": "",
")": "",
"[": "",
"]": "",
"&": "and",
"---": "-",
}
# apply replacement table
for key in rtable:
filename = filename.replace(key, rtable[key])
# make all letters lower-case
filename = filename.lower()
return filename
def get_title(link):
"""
fetch the title of the linked YouTube video;
include the title text, artist, uploader, and video ID
"""
title = subprocess.check_output(
[
"youtube-dl",
"--get-filename",
"-o",
"%(title)s-%(artist)s-%(uploader)s_%(id)s",
link,
],
text=True,
)
return title.strip()
def download_file(url, type, path, title=""):
"""
execute youtube-dl command to download the file at the URL;
- type can either be 'audio' or 'video';
- if title is supplied, reconstruct the file name with the title,
otherwise leave it default
"""
# enforce that the type is either "audio" or "video"
assert type in ["audio", "video"]
# initialize the command to be populated
command = ["youtube-dl"]
# supply media type
if type == "audio":
command.append("-f bestaudio")
else:
command.append("-f best")
# supply title if given
if title:
command.append("-o")
command.append(f"{title}.%(ext)s")
# append URL
command.append(url)
# execute 'command' in 'path'
subprocess.run(command, cwd=path)
def write_metadata(title, filename, path):
"""
write the title to the output file using ffmpeg
"""
# find the exact path of the file
file = list(Path(path).glob(f"{filename}*"))[0]
# rename the file to file.tmp
file_tmp = Path(file).rename(f"{file}.tmp")
# write metadata to a new file
subprocess.run(
[
"ffmpeg",
"-i",
f"{file_tmp}",
"-metadata",
f"title={title}",
"-c",
"copy",
f"{file}",
]
)
# delete the temporary file
Path(file_tmp).unlink()
# [3/3] MAIN LOGIC
try:
with open(args.playlist, "r") as playlist: # open the playlist file in read mode
for line in playlist: # go over the playlist line by line
if line != "\n": # skip empty lines
# split URL line at the first '&' character to truncate extra parameters
line_stripped = line.split("&")[0].strip()
# print the current URL in red (YouTube) color
print(f"\n● {ansicolors.RED}{line_stripped}{ansicolors.END}")
# if not prevented, clean up title
if not args.keep_original:
title_raw = get_title(line_stripped) # prefetch the title
title = filename_cleanup(title_raw) # apply the cleanup functoin
else:
title = ""
# download audio by default, otherwise download video
if not args.video:
download_file(
url=line_stripped, type="audio", path=args.output, title=title
)
else:
download_file(
url=line_stripped, type="video", path=args.output, title=title
)
# if not prevented, write metadata information to the file
if not args.no_metadata:
write_metadata(title_raw, title, args.output)
except FileNotFoundError:
print(
f"""
{ansicolors.YELLOW}{args.playlist}{ansicolors.END} not found.
Please check that you supplied the correct file name for the playlist.
For more information, see "ytdl.py --help"
""",
file=stderr, # write to STDERR
)
exit(1)
@sammdu
Copy link
Author

sammdu commented Feb 19, 2020

Requirements:

Usage:

usage: ytdl.py [-h] [-v] [-k] [-n] [-o OUTPUT_DIRECTORY] [PLAYLIST_FILE]

positional arguments:
  PLAYLIST_FILE         text file containing a list of URLs to download; default to
                        "list.txt"

optional arguments:
  -h, --help            show this help message and exit
  -v, --video           download video instead of audio
  -k, --keep-original   keep the original file name and don't do any cleanups, such as
                        removing dots, spaces, single-quotes, and ampersands
  -n, --no-metadata     prevent writing the full title information to the file's metadata
                        using ffmpeg
  -o OUTPUT_DIRECTORY, --output OUTPUT_DIRECTORY
                        where to store the downloaded files; default to the current directory

Instructions:

  1. Create an empty folder, put this script into the folder.
  2. Create a text file next to the script, this is your playlist file. By default, list.txt is used. You may supply the file name later if you named it something else:
    python3 ./ytdl.py something_else.txt
    
  3. In the playlist file, paste URLs of all YouTube videos you would like to download as audio files; one URL per line, like this:
    https://www.youtube.com/watch?v=dQw4w9WgXcQ
    https://www.youtube.com/watch?v=djV11Xbc914
    https://www.youtube.com/watch?v=I_izvAbhExY
    
    Empty lines are ignored so feel free to use them as a partitioning tool.
  4. Run the script. It will download every URL specified in the list to the current folder, at the best available audio quality.
    python3 ./ytdl.py
    

Options:

  1. If you wish to specify an output folder rather than downloading the files right next to the script, use the -o option, followed by the intended output directory, as such:
    ./ytdl.py -o ./download/
    
    The example above will output the downloaded files into the download folder next to the script.
  2. If you wish to download videos instead of audio, use the -v or --video option, as such:
    python3 ./ytdl.py -v
    
    it will download videos in every URL at the best quality available.
  3. By default, the script will remove from the file names all spaces, periods, replace ampersands with the word "and", make all letters lower case, and more, to make the files easier to work with from the command line. If you wish to keep the default title of the video and video ID as the file name, use the -k or --keep-original option, as such:
    python3 ./ytdl.py -k
    
  4. By default, the script will write the title, artist, uploader, and video ID information to the file's metadata title. If you would like to keep the metadata of the files blank, use the -n or --no-metadata option, as such:
    python3 ./ytdl.py -n
    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment