Original article: How to Download and Trim MP3s from YouTube with Python

Todos somos diferentes, pero creo que casi todos disfrutamos de escuchar música.

Si deseas mantener una versión local de los archivos de audio que escuchas con frecuencia, vas a necesitar descargarlos. A veces, también querrás recortar una parte de este archivo de audio en lugar de tener solo la versión completa disponible.

Puedes desarrollar un script en Python para hacer exactamente estas cosas. También puedes ampliarlo con funcionalidades adicionales si lo deseas. Y te mostraré cómo hacerlo en este tutorial.

Una nota sobre los derechos de autor

Si has utilizado Internet antes, es probable que seas consciente de que los problemas de derechos de autor pueden provocar mucha controversia, tanto en aquellos que defienden la gratuidad del contenido como en los que no.

La propia biblioteca que utilizaremos ha tenido sus propios problemas de derechos de autor.

Afortunadamente, tenemos material libre de derechos de autor disponible para que lo disfrutemos y utilicemos en nuestros programas. En este tutorial, usaremos el cuarto movimiento de la Novena Sinfonía de Beethoven, que es libre de copyright. Esta guía asume que usarás los siguientes métodos para descargar material libre de derechos de autor. ¡No uses esta información para infringir ningún derecho de autor!

Lo que vamos a hacer en este tutorial

Primero, vamos a instalar la dependencia básica, FFMPEG. Luego, instalaremos la biblioteca youtube-dl (que también funciona con Vimeo y muchas otras plataformas) para descargar audio desde una URL de YouTube y utilizarlo en el código de Python.

A continuación, vamos a descargar la librería pydub para recortar archivos de audio e implementaremos esta funcionalidad en nuestro código.

Por último, vamos a crear una interfaz de usuario amigable para que podamos reutilizar este script más adelante sin necesidad de editar el código.

Todo esto se va a ejecutar dentro de una función principal main() para que podamos separar la funcionalidad, la implementación y el uso.

Cómo instalar el paquete FFMPEG

Este paquete es fundamental en muchos programas multimedia (y en todos los de código abierto que he usado hasta ahora). Lo necesitaremos para ambas bibliotecas de Python que instalaremos muy pronto.

Cómo instalar en Linux

Si te encuentras en una máquina que usa Debian de base (como Ubuntu o Kali), este es el comando para instalar FFMPEG:

sudo apt-get install ffmpeg

Si estás usando otra distribución, las instrucciones están acá.

Cómo instalar en Windows

Primero, hay que instalar el gestor de paquetes Chocolatey. Las instrucciones están acá, te espero.

Después de instalarlo correctamente, descarga el paquete desde una instancia de Powershell con permisos de administrador.

choco install ffmpeg

Cómo instalar en Mac

Si todavía no lo tienes, instala homebrew. Luego, en la terminal:

brew install ffmpeg

Cómo descargar audio de URLs de YouTube

Primero, descarga el paquete youtube-dl de pip. Es uno de los más guardados en GitHub.

pip install youtube-dl

Vamos a importar este módulo y luego declarar una función que va a descargar el audio en formato mp3 con una calidad razonable desde una URL de YouTube.

import youtube_dl # cliente para muchos portales de multimedia

# descarga yt_url al mismo directorio desde donde corre el script
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()

El método .download() va a descargar gradualmente el flujo de audio como un archivo .webm. Una vez que detecta que el archivo completo está disponible, utilizará ffmpeg para convertirlo en un archivo de audio MP3. Esto significa que si ocurre algo, como que la conexión a Internet se interrumpa cuando el archivo esté al 90% descargado, la descarga se reanudará en el 90% en lugar de comenzar desde el principio, lo cual es bastante útil. Ten en cuenta que si ya tienes el archivo mp3 descargado, la descarga se reiniciará y sobrescribirá el archivo.

Ejecuta tu script de Python. La descarga de audio de esta interpretación del Cuarto Movimiento de la Novena Sinfonía de Beethoven debería generar un archivo MP3 de aproximadamente 33 MB con el título del video disponible localmente. Es posible que la descarga sea un poco lenta, así que aprovecha para hacerte una taza de té.

Como puedes ver, es posible pasar parámetros opcionales a youtube-dl (que también estarán disponibles como un programa independiente de línea de comandos fuera del script). Una de sus capacidades es descargar una serie de videos desde una URL de lista de reproducción. Si estás más interesado, puedes consultar su documentación. Mantendré el uso más sencillo a lo largo de este tutorial.

Cómo recortar el archivo descargado

Con el archivo descargado, ahora vamos a dividirlo localmente de forma arbitraria (puede que hayas considerado si es posible simplemente descargar un fragmento de YouTube. Todos los métodos fiables que encontré básicamente se reducirán a descargarlo completo y luego editarlo localmente). Para esto, utilizaremos la biblioteca pydub. Puedes instalarla de la siguiente manera:

pip install pydub

Esta es una biblioteca bastante útil que te permite manipular por completo el audio, reducir o aumentar el volumen en ciertos intervalos, repetir clips, y más. Por ahora, solo estamos interesados en recortar.

Para recortar nuestro archivo descargado, tendremos que obtener el nombre de archivo de nuestro MP3 recién descargado, convertir los puntos de inicio y final de nuestro intervalo de audio deseado de 'horas:minutos:segundos' a milisegundos, y finalmente utilizar pydub para dividir nuestro archivo de audio.

Cómo obtener el nombre del archivo

Lamentablemente, el método .download() no nos va a devolver el nombre de archivo generado, al cual tampoco tendremos acceso, ya que solo estamos pasando la URL como parámetro. Pero tenemos Python, y es una herramienta fantástica.

Sabemos que estamos buscando un archivo .mp3 que se generó justo antes de nuestra operación de búsqueda de nombre de archivo (Python es monohilo y va a ejecutar el código de forma síncrona por defecto). Obtendremos el nombre de nuestro archivo más reciente en el directorio del script, y eso será nuestro archivo.

Podemos realizar esta operación listando todos los archivos .mp3 en el directorio local, recopilando sus marcas de tiempo como números enteros (lo que significa el tiempo en milisegundos contados desde una fecha en el pasado determinada; esto significa que cuanto mayor sea el valor, más en el tiempo se creó el archivo) y obteniendo el archivo con el valor más alto.

Para esto, necesitaremos los módulos glob para navegar por el directorio y os para obtener información de la marca de tiempo, ambos disponibles de forma nativa.

import glob
import os

def newest_mp3_filename():
	# lista todos los mp3s en el directorio local
    list_of_mp3s = glob.glob('./*.mp3')
    # devuelve aquel con mayor valor de timestamp (último creado)
    return max(list_of_mp3s, key = os.path.getctime)

Cómo obtener HH:MM:SS en milisegundos

Una vez que cortemos nuestro archivo, pydub esperará intervalos de tiempo expresados en milisegundos. Pero para nosotros, los humanos, calcular el momento exacto en milisegundos cada vez que queramos recortar un video sería bastante molesto, así que le pediremos respetuosamente a la computadora que lo haga por nosotros.

Nuestra entrada será una cadena en el formato HH:MM:SS. Esto funciona bien si queremos recortar un video de más de una hora, pero la mayor parte del tiempo solo querremos obtener el intervalo de un minuto:segundo a otro. Así que también vamos a tener esto en cuenta.

Un milisegundo es 1/1000 de segundo, un minuto tiene 60 segundos y una hora tiene 60 minutos. Entonces, debemos obtener el valor para las horas, luego para los minutos, luego para los segundos, realizar la conversión a milisegundos en cada uno y luego sumar las partes para obtener el resultado.

def get_video_time_in_ms(video_timestamp):
    vt_split = video_timestamp.split(":")
    if (len(vt_split) == 3): # condicional en formato HH:MM:SS
        hours = int(vt_split[0]) * 60 * 60 * 1000
        minutes = int(vt_split[1]) * 60 * 1000
        seconds = int(vt_split[2]) * 1000
    else: # formato MM:SS
        hours = 0
        minutes = int(vt_split[0]) * 60 * 1000
        seconds = int(vt_split[1]) * 1000
    # marca de tiempo en milisegundos
    return hours + minutes + seconds

Cómo obtener el audio recortado

Ahora leeremos nuestro archivo MP3 como un objeto de pydub y cortaremos el intervalo deseado. La sintaxis es exactamente la misma que las operaciones de corte en cadenas y arreglos, pero en lugar de un índice para un elemento, usaremos milisegundos para momentos específicos dentro del audio.

def get_trimmed(mp3_filename, initial, final = ""):
    if (not mp3_filename):
    	# levanta una excepción para detener el programa
        raise Exception("No se encontró ningún MP3 en el directorio local.")
    # lee el mp3 como un objeto de Pydub
    sound = AudioSegment.from_mp3(mp3_filename)
    t0 = get_video_time_in_ms(initial)
    print("Comenzando proceso de recorte para el archivo ", mp3_filename, ".\n")
    print("Iniciando en ", initial, "...")
    if (len(final) > 0):
        print("...hasta ", final, ".\n")
        t1 = get_video_time_in_ms(final)
        return sound[t0:t1] # t0 hasta t1
    return sound[t0:] # t0 hasta el final

Cómo unirlo todo

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

En caso de que te lo estuvieras preguntando, el fragmento anterior se traduce como "Todos los hombres se convertirán en hermanos, dondequiera que tus suaves alas revuelen". Es un fragmento de "Oda a la alegría", un poema de Friedrich Schiller que forma la mayor parte de la letra de las partes corales del Cuarto Movimiento.

Este es el fragmento más famoso del movimiento más famoso de la sinfonía más famosa de Beethoven. Sea quien seas, cuando y cómo hayas crecido, es muy probable que reconozcas esta pieza.

Ahora vamos a poner todo lo que hemos hecho juntos. Descargaremos el audio de YouTube, cortaremos el coro de la "Oda a la Alegría" (desde el minuto 9:51 hasta el minuto 14:04) y lo guardaremos como <nombre> - TRIM.mp3.

Si has seguido el tutorial correctamente, actualiza tu función main() para ejecutar cada paso de manera que al final tengas el MP3 completo y su versión recortada como archivos disponibles en el directorio desde el que ejecutes el script. No olvides ejecutar la función main() al final del 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("El proceso finalizó con éxito. Guardando archivo recortado como ", trimmed_filename)
    # guarda archivo con nuevo nombre
    trimmed_file.export(trimmed_filename, format="mp3")

Cómo agregar interacción desde la línea de comandos

Para esta parte, necesitaremos el módulo sys de Python, que lee la entrada pasada desde la línea de comandos, entre otras cosas. Simplemente actualizaremos las variables en la función main() para leer la entrada desde la línea de comandos en lugar de los datos actualmente codificados en el script.

ARGV lee la entrada secuencialmente como un array, comenzando desde el índice 1 (el 0 representa el nombre del script de Python en ejecución). Lo configuraremos para que lea una URL como el primer argumento y, opcionalmente, instantes de inicio y finalización para el recorte.

import sys

def main():
    if (not len(sys.argv) > 1):
        print("Por favor inserta una URL soportada por youtube-dl como tu primer argumento.")
        return
    yt_url = sys.argv[1]
    download_audio(yt_url)
    if (not len(sys.argv > 2)): # terminar si no hay argumentos
        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("El proceso finalizó con éxito. Guardando archivo como ", trimmed_filename)
    # guarda archivo con nuevo nombre
    trimmed_file.export(trimmed_filename, format="mp3")

Ejecuta el archivo para probarlo. Recuerda actualizar el nombre del script con el mismo nombre que tienes en tu máquina.

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

Script final

Esta es la versión final con todo integrado. Ten en cuenta que los comentarios sobre los módulos están relacionados solo con para qué los estamos utilizando y que la función main() se está invocando en la última línea.

import youtube_dl # cliente para muchos portales de multimedia
import glob # operaciones de directorio
import os # interfaz para información provista por el sistema operativo
import sys # interfaz para línea de comandos
from pydub import AudioSegment # sólo operaciones de audio

def newest_mp3_filename():
    # lista todos los mp3 en el directorio local
    list_of_mp3s = glob.glob('./*.mp3')
    # devuelve último mp3 creado
    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): # condicional en formato HH:MM:SS 
        hours = int(vt_split[0]) * 60 * 60 * 1000
        minutes = int(vt_split[1]) * 60 * 1000
        seconds = int(vt_split[2]) * 1000
    else: # formato MM:SS 
        hours = 0
        minutes = int(vt_split[0]) * 60 * 1000
        seconds = int(vt_split[1]) * 1000
    # marca de tiempo en milisegundos
    return hours + minutes + seconds

def get_trimmed(mp3_filename, initial, final = ""):
    if (not mp3_filename):
        # levanta una excepción para interrumpir programa
        raise Exception("No MP3 found in local directory.")
    # lee mp3 como objeto de Pydub
    sound = AudioSegment.from_mp3(mp3_filename)
    t0 = get_video_time_in_ms(initial)
    print("Comenzando proceso de recorte para ", mp3_filename, ".\n")
    print("Comenzando desde ", initial, "...")
    if (len(final) > 0):
        print("...hasta ", final, ".\n")
        t1 = get_video_time_in_ms(final)
        return sound[t0:t1] # t0 hasta t1
    return sound[t0:] # t0 hasta el final



# descarga yt_url al mismo directorio donde corre el script
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("Por favor inserta una URL soportada por youtube-dl como tu primer argumento.")
        return
    yt_url = sys.argv[1]
    download_audio(yt_url)
    if (not len(sys.argv > 2)): # terminar si no hay argumentos
        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("El proceso finalizó con éxito. Descargando archivo como ", trimmed_filename)
    # descarga archivo con nuevo nombre
    trimmed_file.export(trimmed_filename, format="mp3")

# ejemplo
# python ytauddown.py https://www.youtube.com/watch?v=8OAPLk20epo 9:51 14:04
main()

Ejercicios sugeridos

  1. Detectar si la primera entrada es una URL válida o no. Consulta las expresiones regulares en Python si no sabes por dónde empezar.
  2. Detectar si las segundas y terceras entradas tienen un formato válido (horas:minutos:segundos O minutos:segundos).
  3. Agregar una opción para cambiar el nombre del archivo MP3 directamente desde la línea de comandos. Recuerda que los argumentos de ARGV se ejecutan en orden.
  4. Refactorizar este script para interactuar con su funcionalidad utilizando una interfaz gráfica de usuario (GUI). Puede ser una aplicación web o local, tu elección.

Consideraciones finales

Espero que disfrutes de este proyecto y le des un buen uso.

Recuerda que ganarse la vida como artista puede ser bastante difícil, especialmente para la mayoría que no cuenta con el respaldo de una empresa. No olvides apoyar a los artistas cuyo trabajo disfrutas siempre que puedas y también recuerda apoyar el software de código abierto.