DEV Community

Otavio Monteagudo
Otavio Monteagudo

Posted on • Edited on

Download & Trim MP3 from Youtube with Python

Download & Trim MP3 from Youtube with Python

Intro

We are different people, but nearly all of us enjoy listening to music.
If you'd like to keep a local version of audio streams you often listen to, you'll need to download these files. Sometimes you'll also want to clip a portion of this audio file instead of having only the whole thing available.
If you'd like to develop a python script to do just these things that's also easy to extend with newer functionality, then this article will be useful.

Note on Copyright

As you probably know if you've used the internet for more than 10 minutes, copyright is a subject that makes a lot of people upset on both sides of the debate on how free it should be. The very library we'll use has had its share of copyright troubles. Thankfully there is copyright-free material available for us to both enjoy and play with in our programs, and as such we'll use the royalty-free Beethoven's 4th Movement of the 9th Symphony audio piece. This guide assumes you'll use the following methods to download copyright-free material as well.

Breakdown

First we're going to install the basic dependency, FFMPEG. Then we'll install the youtube-dl library (which also works with Vimeo and many other platforms) to download audio from an Youtube URL and use it on python code.
We'll then download the pydub library to trim audio files and implement this functionality in our code.
Finally, we'll create some friendly user interface so we can reuse this script later without having to edit the code.
This will all be executed inside a main() function so we can separate functionality implementation and use.

Installing FFMPEG

This package is at the core of many multimedia programs (and all of the open-source ones I've used so far). We are going to need it for both of the python libraries we'll install very soon.

Install on Linux

If you are on a Debian-based machine (such as Ubuntu or Kali):

sudo apt-get install ffmpeg

If you are using other kinds of distributions, install instructions are here.

Install on Windows

First, install the Chocolatey Package Manager. Install instructions are here, I'll be waiting.

Once chocolatey is properly installed, download the package from an administrative instance of Powershell:

choco install ffmpeg
Enter fullscreen mode Exit fullscreen mode

Install on Mac

If you don't have it already, install homebrew. Then, in a terminal:

brew install ffmpeg

Programatically Download Audio from Youtube URLs

First of all, download the youtube-dl package from pip, one of the most starred on github.

pip install youtube-dl

We're going to import this module and then declare a function that downloads the audio in mp3 format with reasonable quality from an Youtube URL.

import youtube_dl # client to many multimedia portals

# downloads yt_url to the same directory from which the script runs
def download_audio(yt_url):
    ydl_opts = {
        'format': 'bestaudio/best',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
    }
    with youtube_dl.YoutubeDL(ydl_opts) as ydl:
        ydl.download([yt_url])

def main():
    yt_url = "https://www.youtube.com/watch?v=8OAPLk20epo"
    download_audio(yt_url)

main()
Enter fullscreen mode Exit fullscreen mode

The .download() method will gradually download the audio stream as a .webm file. Once it detects the whole file is available, it will use ffmpeg to convert it to an MP3 audio file. This means that if something happens, say the internet connection goes down when the file is 90% downloaded, it'll resume download at 90% instead of from the beginning; pretty neat. Note that if you already have the mp3 file downloaded, the download will restart and overwrite the file.

Run your python script and the audio download for this interpretation of Beethoven's 4th Movement of the 9th Symphony should have a ~33 Mb MP3 file with the video's title available locally; it'll probably be somewhat slow, so go make yourself some tea.

As you can see, it is possible to pass optional parameters to youtube-dl (which will also be available as a standalone CLI program outside the script). One of its capabilities is downloading a series of videos from a playlist URL. If you are more interested, read their documentation, I'll keep usage to trivial through this tutorial.

Trim the Downloaded File

With the file downloaded, we're now going to arbitrarily slice it locally (you might have considered wheter it is possible to simply download a clip from youtube; all reliable methods I've found will essentially boil down to downloading the whole and then editing locally). For that we'll use the pydub library:

pip install pydub

This is a pretty nice library that enables you to completely manipulate the audio, reducing or boosting volume at certain intervals, repeating clips, etc. For now, we're just interested in trimming.
In order to trim our downloaded file, we'll have to get the filename of our newly downloaded MP3, convert the start and end points of our desired audio interval from 'hours:minutes:seconds' to milliseconds and finally use pydub to slice our audio file.

Getting the file name

Unfortunately, the .download() method won't return our generated filename, which we'll also won't have access to since we're just passing the URL as a parameter. However, we have python and this is a fantastic tool.
We know that we are looking for an .mp3 file which was generated just before our file name lookup operation (python is single-threaded and will execute code synchronously by default); we'll get the name of our most recent MP3 in the script directory and that's gonna be our file.
We can perform this operation by listing all .mp3 files in the local directory, collecting their timestamps as integers (which means time in milliseconds counted from a given date in the past, meaning that the higher the value, the further in time the file was created) and getting the file with the highest value.
For this we'll need the modules glob to navigate the directory and os to get timestamp information, both available natively.

import glob
import os

def newest_mp3_filename():
    # lists all mp3s in local directory
    list_of_mp3s = glob.glob('./*.mp3')
    # returns mp3 with highest timestamp value
    return max(list_of_mp3s, key = os.path.getctime)
Enter fullscreen mode Exit fullscreen mode

Getting HH:MM:SS as milliseconds

Once we slice our file, pydub will expect time intervals expressed in milliseconds. However, for we humans, calculating the exact moment in milliseconds every time we want to trim a video would be pretty annoying, so we'll respectfully ask the computer to do it for us.
Our input is going to be a string in the HH:MM:SS format if we want to trim a video longer than one hour, however, most of the time we'll just want to get the time interval from a minute:second interval to another, so we have to take this into consideration as well. A millisecond is 1/1000 of a second, a minute is 60 seconds and an hour is 60 minutes, so we have to get the value for hours, then for minutes, then for seconds, perform the millisecond conversion on each and then sum the parts to reach the result.

def get_video_time_in_ms(video_timestamp):
    vt_split = video_timestamp.split(":")
    if (len(vt_split) == 3): # if in HH:MM:SS format
        hours = int(vt_split[0]) * 60 * 60 * 1000
        minutes = int(vt_split[1]) * 60 * 1000
        seconds = int(vt_split[2]) * 1000
    else: # MM:SS format
        hours = 0
        minutes = int(vt_split[0]) * 60 * 1000
        seconds = int(vt_split[1]) * 1000
    # time point in miliseconds
    return hours + minutes + seconds
Enter fullscreen mode Exit fullscreen mode

Getting the Trimmed Audio

We'll now read our MP3 as a pydub object and slice our desired interval. The syntax is exactly the same as slicing operations on strings and arrays, but instead of an index for an element we'll use milliseconds for specific instants within the audio.

def get_trimmed(mp3_filename, initial, final = ""):
    if (not mp3_filename):
        # raise an error to immediately halt program execution
        raise Exception("No MP3 found in local directory.")
    # reads mp3 as a PyDub object
    sound = AudioSegment.from_mp3(mp3_filename)
    t0 = get_video_time_in_ms(initial)
    print("Beginning trimming process for file ", mp3_filename, ".\n")
    print("Starting from ", initial, "...")
    if (len(final) > 0):
        print("...up to ", final, ".\n")
        t1 = get_video_time_in_ms(final)
        return sound[t0:t1] # t0 up to t1
    return sound[t0:] # t0 up to the end
Enter fullscreen mode Exit fullscreen mode

Putting All Together

Alle Menschen werden Brüder,
Wo dein sanfter Flügel weilt.

-- Friedrich Schiller

In case you were wondering, the following fragment translates to "All men shall become brothers, wherever your gentle wings hover", a fragment of Ode to Joy, a poem by Friedrich Schiller which serves as most of the lyrics to the choral parts of the 4th Movement. This is the most famous fragment of the most famous movement of the most famous Beethoven symphony; whoever you are, whenever and however you were raised, you are very likely to recognize this piece.

We'll now put together what we have done. We'll download the audio from youtube, slice the Ode to Joy choral (from 9:51 to 14:04) and save it as <filename> - TRIM.mp3.

If you have followed the tutorial correctly, update your main() function to execute each step in a way that'll leave you with the full MP3 and the trimmed version of it. as files available in the directory you'll be running the script from. Don't forget to run the main() function at the end of the script.

def main():
    yt_url = "https://www.youtube.com/watch?v=8OAPLk20epo"
    download_audio(yt_url)
    initial = "9:51"
    final = "14:04"
    filename = newest_mp3_filename()
    trimmed_file = get_trimmed(filename, initial, final)
    trimmed_filename = "".join([filename.split(".mp3")[0], "- TRIM.mp3"])
    print("Process concluded successfully. Saving trimmed file as ", trimmed_filename)
    # saves file with newer filename
    trimmed_file.export(trimmed_filename, format="mp3")
Enter fullscreen mode Exit fullscreen mode

Adding User Interaction Directly from the CLI

For this part we'll need the sys python module, which reads input passed from the command line (among other things). We'll simply update the variables in the main() function to read input from the CLI instead of the currently hard-coded data. ARGV reads input sequentially as an array, starting from index 1 (0 represents the running python script name). We'll set it up to read an URL as the first argument, then (optional) initial and final trimming instants.

import sys

def main():
    if (not len(sys.argv) > 1):
        print("Please insert a multimedia-platform URL supported by youtube-dl as your first argument.")
        return
    yt_url = sys.argv[1]
    download_audio(yt_url)
    if (not len(sys.argv > 2)): # exit if no instants as args
        return
    initial = sys.argv[2]
    final = ""
    if (sys.argv[3]):
        final = sys.argv[3]
    filename = newest_mp3_filename()
    trimmed_file = get_trimmed(filename, initial, final)
    trimmed_filename = "".join([filename.split(".mp3")[0], "- TRIM.mp3"])
    print("Process concluded successfully. Saving trimmed file as ", trimmed_filename)
    # saves file with newer filename
    trimmed_file.export(trimmed_filename, format="mp3")
Enter fullscreen mode Exit fullscreen mode

Run the file to test it; remember to update the script name to the same one that's on your machine.

python ytauddown.py https://www.youtube.com/watch?v=8OAPLk20epo 9:51 14:04
Enter fullscreen mode Exit fullscreen mode

Final Script

This is the final version with everything put together. Note that the comments on modules are related only to what we're using them for and that the main() function is being invoked at the last line.

import youtube_dl # client to download from many multimedia portals
import glob # directory operations
import os # interface to os-provided info on files
import sys # interface to command line
from pydub import AudioSegment # only audio operations

def newest_mp3_filename():
    # lists all mp3s in local directory
    list_of_mp3s = glob.glob('./*.mp3')
    # returns mp3 with highest timestamp value
    return max(list_of_mp3s, key = os.path.getctime)

def get_video_time_in_ms(video_timestamp):
    vt_split = video_timestamp.split(":")
    if (len(vt_split) == 3): # if in HH:MM:SS format
        hours = int(vt_split[0]) * 60 * 60 * 1000
        minutes = int(vt_split[1]) * 60 * 1000
        seconds = int(vt_split[2]) * 1000
    else: # MM:SS format
        hours = 0
        minutes = int(vt_split[0]) * 60 * 1000
        seconds = int(vt_split[1]) * 1000
    # time point in miliseconds
    return hours + minutes + seconds

def get_trimmed(mp3_filename, initial, final = ""):
    if (not mp3_filename):
        # raise an error to immediately halt program execution
        raise Exception("No MP3 found in local directory.")
    # reads mp3 as a PyDub object
    sound = AudioSegment.from_mp3(mp3_filename)
    t0 = get_video_time_in_ms(initial)
    print("Beginning trimming process for file ", mp3_filename, ".\n")
    print("Starting from ", initial, "...")
    if (len(final) > 0):
        print("...up to ", final, ".\n")
        t1 = get_video_time_in_ms(final)
        return sound[t0:t1] # t0 up to t1
    return sound[t0:] # t0 up to the end



# downloads yt_url to the same directory from which the script runs
def download_audio(yt_url):
    ydl_opts = {
        'format': 'bestaudio/best',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
    }
    with youtube_dl.YoutubeDL(ydl_opts) as ydl:
        ydl.download([yt_url])

def main():
    if (not len(sys.argv) > 1):
        print("Please insert a multimedia-platform URL supported by youtube-dl as your first argument.")
        return
    yt_url = sys.argv[1]
    download_audio(yt_url)
    if (not len(sys.argv) > 2): # exit if no instants as args
        return
    initial = sys.argv[2]
    final = ""
    if (sys.argv[3]):
        final = sys.argv[3]
    filename = newest_mp3_filename()
    trimmed_file = get_trimmed(filename, initial, final)
    trimmed_filename = "".join([filename.split(".mp3")[0], "- TRIM.mp3"])
    print("Process concluded successfully. Saving trimmed file as ", trimmed_filename)
    # saves file with newer filename
    trimmed_file.export(trimmed_filename, format="mp3")

# example usage:
# python ytauddown.py https://www.youtube.com/watch?v=8OAPLk20epo 9:51 14:04
main()
Enter fullscreen mode Exit fullscreen mode

Suggested Exercises

  1. Detect whether the first input is a valid URL or not. Take a look at Python RegEx if you don't know where to start.
  2. Detect whether the second and third inputs are in the valid format (hours:minutes:seconds OR minutes:seconds).
  3. Add an option to rename the MP3 file directly from the CLI. Remember that ARGV arguments are executed in order.
  4. Refactor this script in order to interact with its functionality using a GUI. Can be either a web or local app, your choice.

Final Considerations

I hope you'll have fun with this project and put it to good use. Remember that making a living as an artist is pretty hard, especially for the far majority without corporate backing. Remember to support the artists whose work you enjoy whenever you can and also remember to support open-source software.

Top comments (0)