Diversión con python: Reproductor de youtube
Internet está plagada de cursos. Este no es uno de ellos.
En vez de hacer una calculadora o diseñar una base de datos para registrar nuestros avances desarrolladorísticos vamos a hacer algo molón y a lo que se le puede sacar una utilidad más allá del reto personal.
No vamos a perder el tiempo: si conceptos como variables, funciones, valores de retorno o parámetros no te suenan de nada éste es el sitio adecuado. Vas a subirte a la bici y sin quitarle los ruedines a descender por una carretera de montaña.
Objetivo: Quiero ver videos de youtube en un portátil con un Celeron M, 1 gb de ram y tarjeta de video con memoria compartida.
A lo largo de este texto explicaré lo mejor que pueda como me lo monto para ver videos en un trasto que, sinceramente, tiene cierto valor sentimental.
Preparando el terreno
Aqui hay gente que dirá yo uso tal o cuál, si quieres desarrollar necesitas X o Y (incluso hablándote de distribuciones)...
Chorradas. Abre una terminal, la que quieras. Personalmente uso Konsole o Terminology pero he usado Stjerm durante muchísimo tiempo.
Ahora vamos a crear una estructura donde guardar nuestro proyecto. Yo aqui recomiendo crear un directorio llamado "proyectos, programas, fuentes..."
Se puede llamar como se quiera, siempre que quede claro y sea genérico. Dentro de ese directorio se pueden crear otros por lenguajes o como se considere. Yo ahí ya meto los proyectos, y esto quedaria así:
mkdir -p ~/programas/proyectube
Nombre tope gama para un proyecto tope gama.
Ahora hablaremos de ciertas cosillas con la idea de hacer bien las cosas y ser merecedores de usar Vim algún día:
El sitio donde guardamos el código del programa ha de estar bien ordenado. No hay que fliparse, estructuras con 3 o 4 directorios ya son un dolor de incómodas para trabajar rápido. Todo dejado caer en el directorio raíz es de ser un cochino, que diría mi madre.
Si pensamos empaquetarlo, y si somos personas de bien deberiamos hacerlo, a mi me gusta crear un directorio para el código. Mi estructura pasa a ser:
mkdir ~/programas/proyectube/src
Ahora vamos a lo que hemos venido.
Ideaca
La idea está nspirada en el visor de videos de SXMO pero realizada de una forma simple y didáctica desarrollada con el lenguaje preferido por todos: Python3.
Para empezar son necesarias varias cosas: Una terminal, un editor de texto (el que sea, no importa) y una silla.
Vamos a crear nuestro primer código de python. Para eso nos vamos a nuestro directorio de trabajo y creamos el fichero "proyectube.py".
Dentro pondremos el shebang:
#!/usr/bin/python3
guardamos y le damos permisos de ejecución. En otros cursos aquí te harían un "hola mundo" pero pasando.
En mi caso he elegido QT para la interfaz gráfica, así que usaré pySide. Aquí habra gente acomodada que te dirá "Usa QtDesigner/Glade/laChorradaDeTurno". Pasa de ellos, no necesitamos esas horteradas barrocas. Ya sabías a lo que venias.
Mi editor de texto de referencia es Vim, lógicamente que cada cual use el que quiera. No importa porque cuando estéis preparados para sentir el calor fraternal de Vim iréis solos a su encuentro.
Diseño inicial
Como venía diciendo el diseño inicial... A ver, quiero un cuadro para poner texto, un botonazo y ya. Como mucho y si me vengo arriba que pase solo al siguiente vídeo y ya si me engorilo de verdad lo mismo hasta flechitas para pasar el rato entretenido dando atrás y alante.
Dicho esto vamos a obviar el uso de herramientas de diseño, a este curso se viene a sufrir.
Se empieza por el principio. Para hacer una aplicación con QT lo primero es definir la aplicación asi que toca abrir proyectube.py y meter código:
#!/usr/bin/python3
import os,sys
import random
import subprocess
from requests import get
from yt_dlp import YoutubeDL
import PySide2.QtGui
from PySide2.QtWidgets import QApplication,QVBoxLayout,QLabel,QLineEdit,QPushButton,QWidget
import PySide2.QtCore
Empezamos fuerte. Para interactuar con youtube decido usar yt_dlp, una versión más molona de youtube-downloader.
El resto: os y sys las queremos siempre. subprocess es habitual, requests por el tema de internet y las propias de pyside. El lector avezado observará que solo cargo los widgets que voy a usar. Es una manía que tengo.
Lógicamente para que esto no de errores tenemos que instalar las librerías necesarias de python, pyside es probable que ya la tengamos pero yt-dlp no. Para instalar librerías se puede hacer desde nuestra distro o usando pip3, el gestor de paquetes de python.
Mi recomendación por tal de no "ensuciar" nuestro ordenador es usar un entorno virtual de python o una máquina virtual aunque en el caso concreto de yt-dlp la librería de python se instalará al instalar el propio programa yt-dlp.
Tirando líneas
Lo primero que haré será diseñar la ventana. Es simple, un cuadro de búsqueda y un botonazo.
def ventanuca():
wdg=QWidget()
lay=QVBoxLayout() # Prefiero gridLayout pero en este caso...
wdg.setLayout(lay)
lay.addWidget(QLabel("buscar:"))
txt=QLineEdit()
lay.addWidget(txt)
btn=QPushButton("Dale")
lay.addWidget(btn)
wdg.show()
return(wdg)
#def ventanuca
Tiramos unas líneas pero no a lo loco. Se crea la ventana como un QWidget. En QT aunque siempre se da la matraca con "yo creo mi mainwindow y le meto un central widget" realmente cualquier widget que no pertenece a nadie (sin padre, en el argot) es una ventana. Chulo, ¿que no?.
Para "dibujar" la ventana y los controles en QT se usan los "layouts". Un "layout" básicamente es la forma en la que organizamos la disposición de los elementos en pantalla. Por ejemplo QVBoxLayout coloca los elementos uno debajo del otro. QHBoxLayout los coloca en sentido horizontal y QGridLayout, que es el que deberíamos acostumbrar a usar, nos permite colocarlos sobre una rejilla. Es decir: Si añadimos un qgridlayout los elementos los colocaremos en la fila y columna que queramos de una rejilla "imaginaria" (no tanto). (0,0) Sería la esquina superior izquierda, (0,1) sería la posición "de al lado". (1,1) la de debajo. Podemos crear tantas filas y columnas como necesitemos y lógicamente es la opción más flexible de todos. Esto se verá en otra entrega.
app=QApplication(["proyectube"])
app.exec_()
Y aqui la tenemos, nuestra aplicación de QT. Lo que metemos entre corchetes es el nombre, pon el que te apetezca pero algo chulo haz el favor.
De momento no tenemos nada más alla de lo que queríamos. Ahora falta darle alguna utilidad.
Afilando los dedos
Llegados este punto es hora de enfrentarnos a la cruda realidad. Nuestra aplicación no sirve absolutamente para nada pero eso sí, tiene buena presencia.
Es hora de hacer que las cosas bonitas del mundo sucedan.
Como se ha comentado antes después de ver las opciones se ha decidido usar la librería yt-dlp. Lo primero cuando trabajamos con una libreria es tener a mano su documentación (la de yt-dlp está aquí. Ejemplos de código están bien pero sin documentación estamos vendidos. De hecho éste debería ser un punto importante al valorar si usar esto o aquello: siempre hacia el que mejor documentación ofrezca.
Vamos a definir una función que haga cosas:
def suerte(busca):
print("Busco {}".format(busca))
Llegados a este punto el lector ya tendrá la madurez suficiente como para tener una charla incómoda...
¿Que hay del debug? ¿Tomas precauciones cuando tecleas? ¿Cuidas la higiene de tu código?
En python existen librerías para debug que son realmente buenas, en este caso como no necesitamos nada implementaremos una funcion "debug".
Para ello definiremos una variable que controlará el modo debug y una función para imprimir por pantalla
def _debug(mensaje):
if DBG==True:
print("{}".format(mensaje))
Y por el amor de la humanidad... formatea las cadenas a imprimir con format y no con cualquiera de las otras formas que se puede en python. Format ofrece muchas cosas buenas y ninguna mala.
Ahora podemos controlar el debug usando la variable DBG (hay que declararla en el cuerpo principal del programa).
Asi pues suerte pasaría a ser:
def suerte(busca):
_debug(busca)
Ahora que ya la hemos definido vamos a conectarla a nuestro botonazo. Cuando se habla de "conectar" se está hablando de que función se va a llamar cuando el programa emita una señal concreta. Una señal es lo que un usuario de linux lanza con el comando "kill" o "killall". Estos comandos lanzan unas señales definidas en linux como KILL, TERM, o USR1. Bueno, pues en nuestro programa podemos definir las señales que queramos, emitirlas cuando queramos y "conectarlas" a la función que queramos.
btn.clicked.connect(funcion)
Con esa línea tan bonita le estamos diciendo que queremos que la señal "clicked" del botón ejecute la función "función". Cuando se usan liberias gráficas, como QT en este caso, todos los elementos tienen ya sus propias señales consultables desde la documentación que ya deberías tener a mano.
El punto adecuado del código para establecer estas conexiones puede cambiar. personalmente a mi me gusta ponerlo cuando defino los elementos:
...
btn=QPushButton("Dale")
btn.clicked.connect(suerte)
...
Peeeero, un momento. Nuevamente el que observe se hará una pregunta: ¿y el parámetro?. Ay, el parámetro, vaya descuido.. Efectivamente la función "suerte" pide un parámetro y en nuestra señal no estamos pasando ninguno.
¿btn.clicked.connect(suerte(btn.txt())?
¿btn.clicked.connect(suerte,args=btn.txt())?
¡No!, ¡Detente insensato!. No escribas código a lo loco. En QT hay varias formas de hacer esto: la elegante y la que vamos a usar.
La elegante consiste en crear un signalMapper (mapeador de señales) y usarlo. La que vamos a usar consiste en usar una expresión lambda para pasar el parámetro:
....
btn=QPushButton("¡Dale!")
btn.clicked.connect(lambda x:suerte(txt.text()))
...
Esta expresión lamba se ejecuta y llama a la función suerte con el texto que se haya introducido en la pantalla.
Si se inicializa DBG=True y se ejecuta desde la terminal (./proyectube) aparecerá un bonito mensaje de debug con el contenido a buscar. Y en este punto estamos preparados para el éxito total.
Dando caña
Si tenemos la documentación de yt-dl a mano este código está claro: Creamos una nueva función que devolverá el resultado de la búsqueda y de los resultados cogemos el 1ro, de ahí lo de "suerte" ;)
def _buscar(busqueda):
resultado={}
with YoutubeDL(OPCIONES_YDL) as ydl:
try:
get(busqueda)
except:
result = ydl.extract_info(f"ytsearch:{busqueda}", download=False)['entries'][0]
else:
result = ydl.extract_info(busqueda, download=False)
return resultado
#def _buscar
def suerte(busqueda):
resultado_busqueda=_buscar(busqueda)
video_seleccionado=resultado_busqueda[0]
_debug("Buscado:{0}\nVideo: {1}".format(busqueda,video_seleccionado))
#def suerte
Ahora que ya tenemos el video nos tocará poder verlo. No hay que tener reparos en crear funciones: es cierto lo que algunos en su suprema fumada comentan de que cualquier llamada a una función es "hacer perder el tiempo al procesador" pero, sinceramente, si no hay razones del nivel "es que si no lo hago así explota el planeta" no hay que tener reparos en crearlas.
Así que vamos allá:
def _buscar(busqueda):
resultado={}
with YoutubeDL(OPCIONES_YDL) as ydl:
#Captura de errores. Todo lo que va dentro de un _try/except_ se ejecuta con el control de errores activado
#Puede parecer buena idea meter mucho código dentro de un control de errores pero es lo peor que puedes hacer
#Peor incluso que la de tratar de aprender algo de este documento.
#Si todo va bien tendremos un resultado, si algo falla cogemos el primer resultado
try:
get(busqueda)
except:
result = ydl.extract_info(f"ytsearch:{busqueda}", download=False)['entries'][0]
else:
result = ydl.extract_info(busqueda, download=False)
return resultado
#def _buscar
def _reproducir_video(url):
cmd=[REPRODUCTOR,OPCIONES_REPRODUCTOR,"{}".format(url)]
subprocess.run(cmd)
#def _reproducir_video
Y añadimos la llamada a esta función tan chula desde suerte:
def suerte(busqueda):
resultado_busqueda=_buscar(busqueda)
video_seleccionado=resultado_busqueda[0]
_debug("Buscado:{0}\nVideo: {1}".format(busqueda,video_seleccionado))
_reproducir_video(video_seleccionado)
#def suerte
Aquí está todo el código escrito hasta ahora con algunos añadidos necesarios (mira la documentación de yt_dlp para conocerlos):
#!/usr/bin/python3
import os,sys
import random
import subprocess
from requests import get
from yt_dlp import YoutubeDL
import PySide2.QtGui
from PySide2.QtWidgets import QApplication,QVBoxLayout,QLabel,QLineEdit,QPushButton,QWidget
import PySide2.QtCore
#FALSAS CONSTANTES
#En python no hay constantes como tal. Por lo común son variables normales que se colocan al principio del código en mayúsculas.
OPCIONES_YDL = {'format': 'bestaudio', 'noplaylist':'True'}
FORMATOS_PREFERIDOS=["(720p)","(480p)","(360p)","(240p)"]
REPRODUCTOR="mplayer" # --> Reproductor a usar <--
OPCIONES_REPRODUCTOR=""
def _reproducir_video(url):
cmd=[REPRODUCTOR,OPCIONES_REPRODUCTOR,"{}".format(url)]
subprocess.run(cmd)
#def _reproducir_video
def _buscar(busqueda):
resultado={}
with YoutubeDL(OPCIONES_YDL) as ydl:
try:
get(busqueda)
except:
result = ydl.extract_info(f"ytsearch:{busqueda}", download=False)['entries'][0]
else:
result = ydl.extract_info(busqueda, download=False)
return resultado
#def _buscar
def suerte(busqueda):
resultado_busqueda=_buscar(busqueda)
video_seleccionado=resultado_busqueda[0]
_debug("Buscado:{0}\nVideo: {1}".format(busqueda,video_seleccionado))
_reproducir_video(video_seleccionado)
#def suerte
def ventanuca():
wdg=QWidget()
lay=QVBoxLayout() # Prefiero gridLayout pero en este caso...
wdg.setLayout(lay)
lay.addWidget(QLabel("buscar:"))
txt=QLineEdit()
lay.addWidget(txt)
btn=QPushButton("Dale")
btn.clicked.connect(lambda x:suerte(txt.text()))
lay.addWidget(btn)
wdg.show()
return(wdg)
#def ventanuca
app=QApplication(["proyectube"])
wdg=ventanuca()
app.exec_()
Oh, oh, oh. ¿Que me dices? ¿Que no funciona? Que lástima... justo aquí acaba la primera entrega.
Es broma, esto va del tirón.
Como se teclea a lo loco hemos cometido un error grave: hemos tratado como una lista (array) algo que no es una lista. Muy bien. Estupendo. ¿Miramos la documentación o copiamos y pegamos? Así no se puede, de verdad.
Una vez invertimos 5 minutos en leer la documentación ya sabemos lo que sucede. Cuando buscamos no tenemos una lista de videos sino un diccionario de datos. Un diccionario es una estrutura (des)organizada para guardar información estructurada. Es decir: Si yo tengo vasos de diferentes colores y los guardo en la cocina en el armario esto podría representarse como: cocina->armario->vasos->colores
Si además tuviera tenedores en una cajonera esto quedaría:
cocina -> armario -> vasos
-> cajonera -> cubiertos -> tenedores
y si ademas quiero organizar los vasos por colores y nombrar a las cucharas:
cocina -> armario -> vasos -> colores
-> cajonera -> cubiertos -> tenedores
-> cucharas
Y eso es un diccionario y la razón por la que tocará crear otra función para analizar los resultados.
def _seleccionar_video(resultados):
#Del diccionario cogemos lo que pertenece a "formats", en la documentación pone que és.
formats=resultados.get("formats",{})
url=""
#Recorremos los elementos del diccionario
for video in formats:
#En python podemos usar el método _get_ de un diccionario para que nos devuelva un valor si existe o lo inicialice por defecto.
#En este caso se pide el valor de _vcodec_ y si no se encontrase se asignará _nohay_. Normalmente se asignaría None o "", [], {}...
if video.get("vcodec","nohay")!="nohay":
#Python es bonito. Cogemos el valor de _format_ y lo cargamos en una lista (array) usando "-" como separador.
#De esa lista cogemos el último elemento (-1), lo partimos por " " y nuevamente cogemos el último
#Para entender esta operatoria hace falta ver que datos en bruto tenemos. ¿Has implementado _debug?
#Sería un buen momento para usarlo
resolucion=video.get("format","").split("-")[-1].strip().split(" ")[-1]
extension=video.get("video_ext","")
channels=video.get("audio_channels","none")
#La búsqueda de resolución es IA (Inteligencia Anómala)
#Descartamos los canales y comprobamos si la resolución del video está entre las que queremos
if resolucion==FORMATOS_PREFERIDOS[0] and channels!=None:
url=video.get("url")
break
elif channels!=None and resolucion in FORMATOS_PREFERIDOS:
#mis mejores deseos
url=video.get("url")
elif channels!=None and url=="":
#por si acaso nos quedamos con el primero.
#"El que va primer, va davant (Lo que va primero, va delante)" que decimos en mi tierra.
url=video.get("url")
return(url)
#def _selecciona_video
Y ahora si podemos fliparlo, he aquí el código de la bestia reproductora:
#!/usr/bin/python3
import os,sys
import random
import subprocess
from requests import get
from yt_dlp import YoutubeDL
import PySide2.QtGui
from PySide2.QtWidgets import QApplication,QVBoxLayout,QLabel,QLineEdit,QPushButton,QWidget
import PySide2.QtCore
#FALSAS CONSTANTES
OPCIONES_YDL = {'format': 'bestaudio', 'noplaylist':'True'}
FORMATOS_PREFERIDOS=["(720p)","(480p)","(360p)","(240p)"]
REPRODUCTOR="mplayer" # --> Reproductor a usar <--
OPCIONES_REPRODUCTOR=""
DBG=True
def _debug(mensaje):
if DBG==True:
print("{}".format(mensaje))
def _reproducir_video(url):
cmd=[REPRODUCTOR,OPCIONES_REPRODUCTOR,"{}".format(url)]
subprocess.run(cmd)
#def _reproducir_video
def _seleccionar_video(resultados):
formatos=resultados.get("formats",{})
url=""
for video in formatos:
if video.get("vcodec","none")!="none":
resolucion=video.get("format","").split("-")[-1].strip().split(" ")[-1]
extension=video.get("video_ext","")
canales=video.get("audio_channels","none")
if resolucion==FORMATOS_PREFERIDOS[0] and canales!=None:
url=video.get("url")
break
elif canales!=None and resolucion in FORMATOS_PREFERIDOS:
#mis mejores deseos
url=video.get("url")
elif canales!=None and url=="":
#coge el primero, por si acaso...
url=video.get("url")
return(url)
#def _seleccionar_video
def _buscar(busqueda):
resultado={}
with YoutubeDL(OPCIONES_YDL) as ydl:
try:
get(busqueda)
except:
resultado = ydl.extract_info(f"ytsearch:{busqueda}", download=False)['entries'][0]
else:
resultado = ydl.extract_info(busqueda, download=False)
return resultado
#def _buscar
def suerte(busqueda):
resultado_busqueda=_buscar(busqueda)
video_seleccionado=_seleccionar_video(resultado_busqueda)
_debug("Buscado:{0}\nVideo: {1}".format(busqueda,video_seleccionado))
_reproducir_video(video_seleccionado)
#def suerte
def ventanuca():
wdg=QWidget()
lay=QVBoxLayout() # Prefiero gridLayout pero en este caso...
wdg.setLayout(lay)
lay.addWidget(QLabel("buscar:"))
txt=QLineEdit()
lay.addWidget(txt)
btn=QPushButton("Dale")
btn.clicked.connect(lambda x:suerte(txt.text()))
lay.addWidget(btn)
wdg.show()
return(wdg)
#def ventanuca
app=QApplication(["proyectube"])
wdg=ventanuca()
app.exec_()
Palabras finales
En próximas entregas veremos como tener reproducción contínua, crearemos señales, añadiremos controles... O no. Pero en cualquier caso nos divertiremos.
A lo largo del texto se habrá observado que hay funciones que se llaman "def suerte" y otras que empiezan con un _
como def _buscar
. Es una forma visual de distinguir funciones públicas de funciones que deberían no serlo. En python todas las funciones son públicas pero por facilitar la vida humana aquellas que se comportan como privadas se marcan con _
. Lo mismo pasa con las constantes: en python no existen pero está el consenso de colocarlas al principio del código y en mayúsculas. Si lo de "públicas" o "privadas" no se entiende no pasa nada, una simple búsqueda en wikipedia o donde gustemos nos lo explicará.
¡Salud!