Cuaderno de GNUtas

Ytel y la búsqueda de vídeos de youtube en Emacs (con Invidious)

El paquete ytel es una biblioteca externa para GNU Emacs que permite buscar los vídeos de youtube mediante una interfaz basada en elfeed, o sea, que quien ya conozca este popular navegador-agregador de feeds, se moverá en ytel como pez en el agua (un ejemplo del aspecto de la interfaz en la fig. 1). A esto hay que sumar dos características importantes más. La primera, que las búsquedas la realiza, de fábrica, mediante la api de Invidious, que es, por otra parte, la forma que recomendamos aquí para buscar y ver vídeos de youtube, pues se trata de software libre, no ejecuta javascript propietario y está descentralizada a través de unas cuantas instancias que van surgiendo por la red. La segunda es que ytel consiste, más que en una aplicación acabada, en una base con herramientas (funciones, variables) para que nos construyamos nuestra propia interfaz de vídeos de youtube en Emacs a gusto, partiendo de la funcionalidad de las búsquedas. Aquí contaré cómo me he apañado la mía, integrando ytel con EMMS, el Emacs multimedia system del que ya hablamos algo aquí y aquí.

Como Invidious tiene unas cuantas instancias activas y disponibles por internet, conviene declarar antes cuál preferimos para realizar las búsquedas de los vídeos. Así que empezamos por configurar esta variable con nuestra instancia preferida:

(setq ytel-invidious-api-url "https://invidious.snopyta.org")

Ahora bien, por saturación de usuarios a veces una instancia puede quedar temporalmente fuera de juego, y suele ser buena práctica diversificar entre instancias para no dar pie a estas saturaciones. Yo he optado por definir una sencilla lista Ido para cambiar de instancia rápidamente. Primero, esta variable con unas cuantas instancias:

(setq instancias-invidious '("yt.iswleuven.be"
                             "invidious.xyz"
                             "invidious.snopyta.org"
                             "inviou.site"
                             "invidious.site"
                             "invidious.tube"
                             "vid.mint.lgbt"
                             "invidio.us"
                             "invidious.glie.town"))

A continuación la función que crea la lista de instancias para que Ido navegue por ellas:

(defun crea-lista-instancias-invidious ()
  (mapcar 'identity instancias-invidious))

Y la lista Ido que devuelve el candidato como variable:

(defun ido-instancias-invidious (argumento)
  (interactive  (list (ido-completing-read "Instancias de Invidious: " (crea-lista-instancias-invidious) nil t)))
  (setq instancia-candidata argumento))

Y entonces ya podemos definir una función para cambiar de instancia cómodamente:

(defun ytel-otra-instancia-invidious-api ()
  "Escoge entre una lista de instancias de Invidious para definir
la url de la api."
  (interactive)
  (let ((instancia (instancia-invidious)))
    (setq ytel-invidious-api-url (concat "https://"  instancia))))

Pero lo esperable (y lo que sucede el 90% de las veces) es que con la instancia por defecto funcione todo bien.

Sigamos configurando. Ya tenemos resuelto el tema de las búsquedas. Ahora queda, por ejemplo, cómo reproducir el vídeo escogido mediante EMMS. Para ello, definimos esta función:

(defun ytel-reproduce ()
   "Reproduce un vídeo de Youtube desde ytel mediante EMMS"
   (interactive)
   (let* ((video (ytel-get-current-video))
          (id    (ytel-video-id video)))
     (emms-play-url (concat ytel-invidious-api-url "/watch?v=" id))
     "--ytdl-format=bestvideo[height<=?720]+bestaudio/best"))

Es decir, cuando lanzamos una búsqueda de ytel se nos muestra, a la manera como lo hace elfeed, una lista de vídeos candidatos. Con el cursor en el vídeo escogido, llamamos a la función anterior.

Pero aún hay más: ¿Y si queremos descargar el vídeo en la posición del cursor mediante youtube-dl, ya sea el vídeo completo o sólo el audio? Pues es muy simple. Suponiendo que ya tenemos creado un directorio permanente donde guardar los vídeos/audios descargados, podemos definir estas dos funciones, basadas en la anterior (nótese que en este caso ponemos la url de youtube para asegurar):

(defun ytel-dl-video-completo ()
  (interactive)
  (let* ((video (ytel-get-current-video))
         (id    (ytel-video-id video))
         (comando (concat
                   "youtube-dl -o '~/Vídeos/y-dl/%(title)s.%(ext)s' "
                   "https://www.youtube.com/watch?v=" id)))
    (message "descargando vídeo completo")
    (start-process-shell-command comando "*proceso-ydl*" comando)))

(defun ytel-dl-solo-audio ()
  (interactive)
  (let* ((video (ytel-get-current-video))
         (id    (ytel-video-id video))
         (comando (concat
                   "youtube-dl -x -o '~/Vídeos/y-dl/%(title)s.%(ext)s' "
                   "https://www.youtube.com/watch?v=" id)))
    (message "descargando vídeo sólo audio")
    (start-process-shell-command comando "*proceso-ydl-audio*" comando)))

Sendas llamadas al comando de shell youtube-dl se hacen de forma asíncrona mediante start-process-shell-command, y la salida de dicho comando va a parar a un búfer temporal, donde podrems ver el estado del proceso de descarga, como si lo estuviésemos haciendo desde la terminal.

Por último, y si no queda otra, podemos definir también esta función para abrir el vídeo (ay) en la página de youtube con el navegador externo:

(defun ytel-abre-en-youtube ()
  "Reproduce un vídeo de Youtube en la página de Youtube en
firefox, la opción evitable :-("
  (interactive)
  (let* ((video (ytel-get-current-video))
         (id    (ytel-video-id video)))
    (browse-url (concat "https://www.youtube.com/watch?v=" id))
    "--ytdl-format=bestvideo[height<=?720]+bestaudio/best"))

Y ya sólo nos queda definir los atajos de rigor, que en ytel pueden ser simples teclas sin prefijo (de fábrica ya incluye ytel algunos, como la tecla «s» para iniciar una búsqueda).

(with-eval-after-load 'ytel
  (define-key ytel-mode-map "m" #'ytel-reproduce)
  (define-key ytel-mode-map "y" #'ytel-abre-en-youtube)
  (define-key ytel-mode-map "q" #'ytel-dl-video-completo)
  (define-key ytel-mode-map "w" #'ytel-dl-solo-audio)
  (define-key ytel-mode-map "x" #'ytel-otra-instancia-invidious-api))

ytel.png

Figura 1: Ejemplo de una lista de vídeos en ytel

Actualización de 26/02/22: añadir miniaturas de los vídeos

¿Es posible hacer que la lista de vídeos desplegados en un búfer de ytel se muestren con su respectiva miniatura? Sí, si añadimos unos fáciles y rápidos apaños.

Primero, necesitamos estas útiles funciones, que hemos tomado prestadas de aquí, con algunas leves modificaciones propias, especialmente en el ancho de la miniatura del vídeo. Su cometido es el de descargar la imagen de la miniatura del servidor de YouTube y crear la imagen con un ancho determinado (en este caso, hemos escogido el valor de 240):

(defun descarga-con-nombre-archivo (url)
  "Descarga archivo desde la web y devuelve su nombre como cadena
de texto"
  (let ((image-buf (url-retrieve-synchronously url)))
    (when image-buf
      (with-current-buffer image-buf
        (goto-char (point-min))
        (when (looking-at "HTTP/")
          (delete-region (point-min)
                         (progn (re-search-forward "\n[\n]+")
                                (point))))
        (buffer-substring-no-properties (point-min) (point-max))))))

(defun descarga-imagen-thumb (url)
  "Descarga URL como imagen"
  (create-image (descarga-con-nombre-archivo url) (image-type-from-file-name url) t :width 240))

Definimos esta función para formatear la imagen de la miniatura, siguiendo la estructura del resto de funciones similares en ytel. Toma como argumento la id del vídeo, y mediante la variable local format-url-thumb establecemos la url donde, por norma, debe estar siempre la miniatura:

(defun ytel--format-video-thumb (id)
  (let* ((format-url-thumb (format "https://img.youtube.com/vi/%s/0.jpg" id)))
    (setq img-ytel (descarga-imagen-thumb format-url-thumb))
    (propertize " " 'display img-ytel)))

Definimos una versión modificada de ytel--insert-video, que se encarga de insertar en el búfer cada vídeo. Nuestra versión incluirá la función anterior.

(defun mi-ytel--insert-video (video)
  "Insert `VIDEO' in the current buffer."
  (insert (ytel--format-video-published (ytel-video-published video))
          " "
          (ytel--format-author (ytel-video-author video))
          " "
          (ytel--format-video-length (ytel-video-length video))
          " "
          (ytel--format-title (ytel-video-title video))
          " "
          (ytel--format-video-views (ytel-video-views video))
          "\n\n"
          (ytel--format-video-thumb (ytel-video-id video))
          "\n\n"))

También definimos una nueva función para obtener la información del vídeo cuando están activos las miniaturas, ya que los números de línea del búfer se han alterado:

(defun mi-ytel-posicion-actual-cuando-thumbnails ()
  (interactive)
  (let ((hasta (save-excursion
                 (end-of-line)
                 (point)))
        (desde (save-excursion
                 (goto-char (point-min))
                 (point))))
    (save-restriction
      (narrow-to-region desde hasta)
      (save-excursion
        (goto-char (point-min))
        (let
            ((pos 0))
          (save-excursion
            (while
                (re-search-forward "^[[:digit:]]+" nil t)
              (setf pos (+ pos 1)))
            (setq pos-actual pos)))))
    pos-actual))

(defun mi-ytel-get-current-video ()
  "Get the currently selected video."
  (let ((pos (1- (mi-ytel-posicion-actual-cuando-thumbnails))))
    (aref ytel-videos pos)))

Y, por último, dos comandos que activan o desactivan las funciones de reemplazo, seguido de una recarga de la lista de vídeos buscados. Si queremos siempre que se muestren las miniaturas, nos saltamos esto y añadimos simplemente en nuestro ~/.emacs la línea de advice-add. Aquí preferimos contar con ambas versiones para que se nos muestre a voluntad una lista con o sin miniaturas.

(defun mi-ytel-cambia-a-thumbnails ()
  (interactive)
  (advice-add 'ytel--insert-video :override #'mi-ytel--insert-video)
  (advice-add 'ytel-get-current-video :override #'mi-ytel-get-current-video)
  (when ytel-search-term
    (ytel-search ytel-search-term))
  (message "Miniaturas activadas"))

(defun mi-ytel-quita-thumbnails ()
  (interactive)
  (advice-remove 'ytel--insert-video #'mi-ytel--insert-video)
  (advice-remove 'ytel-get-current-video #'mi-ytel-get-current-video)
  (when ytel-search-term
    (ytel-search ytel-search-term))
  (message "Miniaturas desactivadas"))

Es necesario definir dos funciones para ir hacia adelante o atrás en los vídeos (teniendo en cuenta el caso en que están activas las miniaturas: el cursor se sitúa a inicios de la parte con la fecha y el título del vídeo):

(defun mi-ytel-next-video ()
  (interactive)
  (end-of-line)
  (re-search-forward "^[[:digit:]]+" nil t)
  (beginning-of-line))

(defun mi-ytel-previous-video ()
  (interactive)
  (beginning-of-line)
  (re-search-backward "^[[:digit:]]+" nil t)
  (beginning-of-line))

Y, para terminar, añadimos estos atajos al ytel-mode-map:

(define-key ytel-mode-map "T" #'mi-ytel-cambia-a-thumbnails)
(define-key ytel-mode-map "X" #'mi-ytel-quita-thumbnails)
(define-key ytel-mode-map "p" #'mi-ytel-previous-video)
(define-key ytel-mode-map "n" #'mi-ytel-next-video)

ytel2.png

Figura 2: Lista de vídeos en el búfer de ytel mostrando miniaturas

Actualización de 16/04/22: precaución con resultados de búsqueda vacíos

Me he dado cuenta (tal vez por alguna modificación en Invidious) que en ocasiones los resultados obtenidos de diferentes campos (autor del vídeo, fecha, etc.) se devuelven vacíos. O sea, que tienen valor nil y producen un error al confeccionar el búfer de resultados. Esto suele ocurrir cuando el resultado es la información de algún canal determinado de Youtube. Para prevenir esto, es necesario sobreescribir la función original ytel--insert-video, así como nuestra versión para generar la lista con miniaturas:

(defun ytel--insert-video (video)
  "Insert `VIDEO' in the current buffer."
  (if (not (and (ytel-video-published video)
                (ytel-video-author video)
                (ytel-video-length video)
                (ytel-video-title video)
                (ytel-video-views video)))
      ;; es necesario insertar una entrada falsa, para poder obtener
      ;; la información correcta de los vídeos según la posición en el
      ;; búfer
      (insert "2022-04-16 Vídeo no disponible")
    (insert (ytel--format-video-published (ytel-video-published video))
            " "
            (ytel--format-author (ytel-video-author video))
            " "
            (ytel--format-video-length (ytel-video-length video))
            " "
            (ytel--format-title (ytel-video-title video))
            " "
            (ytel--format-video-views (ytel-video-views video)))))
(defun mi-ytel--insert-video (video)
  "Insert `VIDEO' in the current buffer."
  (if (not (and (ytel-video-published video)
                (ytel-video-author video)
                (ytel-video-length video)
                (ytel-video-title video)
                (ytel-video-views video)
                (ytel-video-id video)))
      ;; es necesario insertar una entrada falsa, para poder obtener
      ;; la información correcta de los vídeos según la posición en el
      ;; búfer (y aquí añadimos dos líneas blancas antes y después)
      (insert "\n2022-04-16 Vídeo no disponible\n")
    (insert (ytel--format-video-published (ytel-video-published video))
            " "
            (ytel--format-author (ytel-video-author video))
            " "
            (ytel--format-video-length (ytel-video-length video))
            " "
            (ytel--format-title (ytel-video-title video))
            " "
            (ytel--format-video-views (ytel-video-views video))
            "\n\n"
            (ytel--format-video-thumb (ytel-video-id video))
            "\n\n")))

Publicado: 29/08/21

Última actualización: 16/04/22


Índice general

Acerca de...

Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.

© Juan Manuel Macías
Creado con esmero en
GNU Emacs