Esta serie de tutoriales va a guiarte paso a paso en la construcción de un add-on para XBMC, usando la librería Plugin Tools y un ejemplo de add-on totalmente operativo como punto de partida. Veremos cómo funciona por dentro este add-on, como mejorarlo, e incluso cómo publicarlo en el repositorio oficial de XBMC o en un repositorio propio.

Para empezar te recomiendo que descargues el add-on de ejemplo que acompaña a la distribución de la librería Plugin Tools, y lo instales en tu equipo.

http://www.mimediacenter.info/descargas/plugin.video.mimediacenter-1.0.2.zip

Este add-on permite extraer y ver desde XBMC los vídeos de mi canal de YouTube, y utiliza a su vez el add-on oficial de YouTube para la reproducción. Si no tienes el add-on de YouTube se instalará automáticamente cuando vayas a ver cualquier vídeo.


Tu add-on para XBMC: Instalar dependencia con el add-on de YouTube

Instalación del add-on y preparación del entorno de desarrollo

Puedes descargar e instalar el add-on en XBMC siguiendo el procedimiento habitual, desde XBMC con la opción de «Instalar desde fichero ZIP» o descomprimiéndolo manualmente en el directorio de los add-ons.

Publicar un add-on en XBMC no implica nada más que tener un directorio con el nombre correcto en la carpeta «addons», y si quieres quitarlo de XBMC basta con que borres ese directorio.

Yo lo que hago habitualmente es trabajar sobre una carpeta donde almaceno todos los plugins (/Users/jesus/Plugins) y luego crear un enlace simbólico en el directorio de los add-ons de XBMC. De esa forma tengo todos los plugins en el mismo sitio, y sólo publico en XBMC aquel con el que estoy trabajando.

Los enlaces simbólicos pueden aplicarse en cualquier sistema, que yo sepa. Las instrucciones desde el terminal en mi Mac son:

$ cd /Users/jesus/Library/Application\ Support/XBMC/addons
$ ln -s /Users/jesus/Plugins/plugin.video.mimediacenter plugin.video.mimediacenter

Tengas donde tengas el código fuente del add-on, ya sea en la carpeta de XBMC o en un directorio separado como en mi caso, configura tu IDE favorito o simplemente prepara un editor de texto para empezar.

¿Qué hace el add-on de ejemplo?

La función de este add-on es sencilla. Basándose en el API de YouTube, se descarga el feed de vídeos de mi canal de YouTube desde esta URL:

http://gdata.youtube.com/feeds/api/users/tvalacarta/uploads

De ahí extrae los vídeos ordenados por fecha de subida, y los muestra de forma que el usuario puede verlos directamente desde XBMC.

El Add-on de XBMC para Mi media center

Punto de entrada

Para averiguar cual es el punto en el que comienza la ejecución del add-on debes abrir el fichero addon.xml, buscar el elemento de extensión «xbmc.python.pluginsource» y verás que su atributo «library» indica el módulo principal.

10  <extension point="xbmc.python.pluginsource" library="default.py">

Si abres el módulo default.py verás que tras definir las funciones que va a utilizar lo único que hace es ejecutar la primera de ellas: «run».

22 # Entry point
23 def run():
24    plugintools.log("tvalacarta.run")
25    
26    # Get params
27    params = plugintools.get_params()
28    
29    if params.get("action") is None:
30        main_list(params)
31    else:
32        action = params.get("action")
33        exec action+"(params)"
34    
35    plugintools.close_item_list()

La función «run» se ejecutará cuando abras el add-on la primera vez, y cada vez que el usuario haga una selección. Veamos su funcionamiento paso a paso:

24    plugintools.log("tvalacarta.run")

Esta línea añade al log de XBMC una entrada, que nos permitirá trazar en caso necesario por donde ha transcurrido la ejecución de nuestro código.

26    # Get params
27    params = plugintools.get_params()

El paso de parámetros a un add-on se realiza en XBMC siguiendo un esquema de URL de este tipo:

plugin://plugin.video.nombredeladdon/?parametro1=valor1¶metro2=valor2

Esta función get_params de plugintools lo que hace es extraer los parámetros y ponerlos en un diccionario para facilitar su acceso más adelante.

29    if params.get("action") is None:
30        main_list(params)
31    else:
32        action = params.get("action")
33        exec action+"(params)"

Según este condicional, si hay un parámetro «action» que tiene el nombre de la función a ejecutar, le pasa el control a esa función pasándole el diccionario de parámetros. En caso de que no haya un parámetro «action», la función llamada por defecto es «main_list».

35    plugintools.close_item_list()

La función que ejecute el add-on se encargará de añadir elementos a la lista de items de XBMC, y con esta llamada a close_item_list indicamos a XBMC que ya hemos terminado de añadir items. En este punto el control pasa de nuevo al usuario.

La función principal

El add-on del ejemplo sólo tiene dos funciones, una para leer los elementos del feed y la otra para reproducir el elemento elegido. En posteriores entregas de esta guía lo mejoraremos para añadir cosas como menús estáticos, configuración, buscador, etc.

Esta es la función «main_list()» completa, donde se hace la magia.

37 # Main menu
38 def main_list(params):
39     plugintools.log("tvalacarta.main_list "+repr(params))
40
41     # On first page, pagination parameters are fixed
42     if params.get("url") is None:
43         params["url"] = "http://gdata.youtube.com/feeds/api/users/"+YOUTUBE_CHANNEL_ID+"/uploads?start-index=1&max-results=10"
44 
45     # Fetch video list from YouTube feed
46     data = plugintools.read( params.get("url") )
47    
48     # Extract items from feed
49     pattern = ""
50     matches = plugintools.find_multiple_matches(data,"<entry>(.*?)</entry>")
51    
52     for entry in matches:
53         plugintools.log("entry="+entry)
54        
55         # Not the better way to parse XML, but clean and easy
56         title = plugintools.find_single_match(entry,"<titl[^>]+>([^<]+)</title>")
57         plot = plugintools.find_single_match(entry,"<media\:descriptio[^>]+>([^<]+)</media\:description>")
58         thumbnail = plugintools.find_single_match(entry,"<media\:thumbnail url='([^']+)'")
59         video_id = plugintools.find_single_match(entry,"http\://www.youtube.com/watch\?v\=([0-9A-Za-z_-]{11})")
60         url = "plugin://plugin.video.youtube/?path=/root/video&action=play_video&videoid="+video_id
61
62         # Appends a new item to the xbmc item list
63         plugintools.add_item( action="play" , title=title , plot=plot , url=url ,thumbnail=thumbnail , isPlayable=True, folder=False )
64     
65     # Calculates next page URL from actual URL
66     start_index = int( plugintools.find_single_match( params.get("url") ,"start-index=(\d+)") )
67     max_results = int( plugintools.find_single_match( params.get("url") ,"max-results=(\d+)") )
68     next_page_url = "http://gdata.youtube.com/feeds/api/users/"+YOUTUBE_CHANNEL_ID+"/uploads?start-index=%d&max-results=%d" % ( start_index+max_results , max_results)
69
70     plugintools.add_item( action="main_list" , title="<< Next page" , url=next_page_url , folder=True )

No es tan complicado como parece, ya que en la mayoría de los casos el add-on hace su trabajo en tres etapas bien diferenciadas.

  • Leer la fuente: Se lee el html de la página con la que se va a trabajar
  • Extraer datos de la fuente: Se extraen los elementos de la página que contienen la información relevante.
  • Construir la lista de items de XBMC: Combinando la información extraída se informa a XBMC de los elementos que debe mostrar.

Veámoslo paso a paso.

Primera etapa: Leer la fuente

Saltando la línea que traza la entrada a la función en la línea 39, lo primero que encontramos es esto:

41     # On first page, pagination parameters are fixed
42     if params.get("url") is None:
43         params["url"] = "http://gdata.youtube.com/feeds/api/users/"+YOUTUBE_CHANNEL_ID+"/uploads?start-index=1&max-results=10"

La URL de que se sacan los datos vendrá en un parámetro «url» en las siguientes invocaciones, pero la primera invocación no especifica ningún parámetro así que se asigna un valor por defecto usando el API de YouTube. Fíjate que añadimos los parámetros «start-index» y «max-results», que hacen la petición de 10 elementos empezando por el 1. Usaremos esto para la paginación un poco más adelante.

45     # Fetch video list from YouTube feed
46     data = plugintools.read( params.get("url") )

Ahora que estamos seguros de que la URL está en el parámetro «url», podemos usarlo para descargar el contenido que se va a mostrar en el add-on, y para ello se hace uso de la función read de plugintools. Esta función es muy sencilla por dentro en este punto, pero la complicaremos más a medida que necesitemos más.

Segunda etapa: Extraer los datos de la fuente

Esta es siempre la parte más compleja, cuando es necesario extraer los datos de lo que hemos descargado para convertirlos en elementos estructurados. Es complejo porque implica (normalmente) el uso de expresiones regulares, y las expresiones regulares dan un poco de miedo.

En este caso particular podríamos haber utilizado librerías de procesamiento de XML, pero para mantener el ejemplo sencillo he pensado que mejor atacábamos el problema con expresiones regulares.

Hagámoslo en dos pasos.

En primer lugar vamos a extraer todos los elementos «<entry>» que hay en el XML que nos hemos descargado, usando la función find_multiple_matches de plugintools. Recibe dos parámetros, el string con los datos que hemos descargado y la expresión regular a aplicar. Y devuelve una lista con lo que ha encontrado. No hagas caso a la línea 49, en este caso sobra aunque viene bien tenerla para futuros usos.

48     # Extract items from feed
49     pattern = ""
50     matches = plugintools.find_multiple_matches(data,"<entry>(.*?)</entry>")
51    
52     for entry in matches:
53         plugintools.log("entry="+entry)

Ahora que tenemos elementos «entry» en un array, podemos procesarlos uno por uno usando la función find_single_match de plugintools.

56         title = plugintools.find_single_match(entry,"<titl[^>]+>([^<]+)</title>")
57         plot = plugintools.find_single_match(entry,"<media\:descriptio[^>]+>([^<]+)</media\:description>")
58         thumbnail = plugintools.find_single_match(entry,"<media\:thumbnail url='([^']+)'")
59         video_id = plugintools.find_single_match(entry,"http\://www.youtube.com/watch\?v\=([0-9A-Za-z_-]{11})")

Para cada elemento «entry» obtenemos el título (title), la descripción (plot), la imagen (thumbnail) y el id que asigna YouTube al vídeo (video_id). Este es el valor que espera el add-on de YouTube en XBMC.

Si no entiendes muy bien la sintaxis de las expresiones regulares no te preocupes, volveremos a ello en próximas entregas de este tutorial y verás que son bastante sencillas en este caso concreto.

Tercera etapa: Construir la lista de items de XBMC

Ya solo queda añadir el elemento a la lista de «items» de XBMC, lo que hacemos nuevamente con la función add_item de plugintools.

60         url = "plugin://plugin.video.youtube/?path=/root/video&action=play_video&videoid="+video_id
61
62         # Appends a new item to the xbmc item list
63         plugintools.add_item( action="play" , title=title , plot=plot , url=url ,thumbnail=thumbnail , isPlayable=True, folder=False )

Un item de XBMC no es más que un título y una URL, aunque podemos ampliarlo con un thumbnail, descripción, fanart, productora de la peli, calidad del vídeo, … De momento vamos a limitarlo a los cuatro elementos que hemos extraido y deduciendo la URL a partir del video_id.

Observa que la URL del elemento es la llamada al add-on de YouTube, pasándole el video_id tal como lo hemos encontrado en el paso anterior.

Observa también que el parámetro «folder» de add_item está a False, indicando que cuando el usuario seleccione este elemento no va a encontrar niveles adicionales de navegación. Este parámetro «folder=False» se reserva normalmente para los vídeos.

Paginación

La descarga de datos desde la URL de YouTube está limitada a los 10 primeros elementos, pero cualquier canal de YouTube tiene muchos más vídeos. ¿Cómo hacer para que el usuario pueda ir descargando los 10 siguientes, con un elemento de tipo «Next page» como en la captura de pantalla del add-on?

65     # Calculates next page URL from actual URL
66     start_index = int( plugintools.find_single_match( params.get("url") ,"start-index=(\d+)") )
67     max_results = int( plugintools.find_single_match( params.get("url") ,"max-results=(\d+)") )

Lo primero es averiguar por qué número de elemento vamos, y para ello utilizamos nuevamente find_single_match y una expresión regular. Miramos a ver qué URL se ha empleado para la lista actual, y extraemos de ella el valor de start_index y max_results.

68     next_page_url = "http://gdata.youtube.com/feeds/api/users/"+YOUTUBE_CHANNEL_ID+"/uploads?start-index=%d&max-results=%d" % ( start_index+max_results , max_results)
69
70     plugintools.add_item( action="main_list" , title="<< Next page" , url=next_page_url , folder=True )

Con esos dos valores deducimos los que deberían emplearse en la URL de la siguiente página (max_results debe ser el mismo, y start_index será 11 para la segunda página, 21 para la tercera, …) y añadimos un nuevo item usando add_item. Cuidando que esta vez el parámetro «folder» sea «True», porque al elegir este item sí que van a obtenerse más items.

Respondiendo a la selección del usuario

Ya tenemos los items de los 10 primeros vídeos, y un item adicional para que el usuario pueda avanzar a la siguiente página.

Ahora bien, ¿cómo continúa la navegación del usuario a partir de este punto?

Es sencillo.

Una vez añadidos los items la función «main_list» termina, y la función «run()» finaliza la ejecución llamando a «plugintools.close_item_list()» como hemos visto más arriba. Así XBMC sabe que la lista ha terminado de cargarse, quita la espiral de carga y muestra al usuario la lista de items.

Cuando el usuario elija un ítem se volverá a invocar al add-on, pasándole como parámetros todos los del item tal como especificamos en la llamada a la función «add_item» para añadirlo.

Y esta es la clave, porque si queremos que la selección de un item dispare una acción concreta sólo tenemos que indicar la acción que queremos en el parámetro «action». Y luego definir una función con ese nombre en nuestro add-on.

Siguiendo nuestro ejemplo, cuando el usuario elija un vídeo se repetirá el ciclo anterior pero invocando esta vez a la función «play». Y esta función se encarga de reproducir el vídeo:

72 def play(params):
73     plugintools.play_resolved_url( params.get("url") )

Si por el contrario el usuario elige el elemento de página siguiente, la función llamada será «main_list» porque es la acción que se especificó al añadir este último item. Y como la URL pasada como parámetro será la que extrae los próximos 10 items, obtendremos como resultado la siguiente página.

Próxima entrada

La entrada de hoy ha sido algo más larga de lo que esperaba, pero si comprendes cómo funciona el add-on de ejemplo te debería resultar sencillo hacer un buen número de add-ons distintos.

Antes de explicar otras técnicas, mañana nos detendremos en ver cómo se personalizan los ficheros de identificación del add-on para que puedas empezar a hacer tus primeros proyectos.

10 comentarios

  1. Hola buenas: Estoy intentando agregar un canal al plugin pero no me entero muy bien como obtener el patron

  2. Hola:

    Gracias por los artículos. Una pregunta, ¿no sobre la línea pattern = «» ?

    Gracias

  3. Efectivamente, ya lo dice el texto.

    Sin embargo como acostumbro a programar un plugin usando otro anterior como referencia, a veces tengo ese hábito.

  4. Cierto, perdona la equivocación.

  5. hola, estoy intentando seguir el tutorial para desarrollar un ad-don y al intentar instalar el ejemplo que propones me da el error de dependencias incumplidas…. alguna idea??

    XBMC 13.0-5
    3.13.11.1-1-MANJARO x86_64

  6. Por cierto el ad-don de youtube lo tengo instalado del repositorio oficial…

  7. Para instalar ese add-on en Gotham hay que hacerlo manualmente (descomprimiendo el ZIP en el directorio «addons») o modificar el fichero addon.xml para pasar de esto:

    <import addon="xbmc.python" version="2.0"/>

    A esto:

    <import addon="xbmc.python" version="2.1.0"/>

    La parte del fichero addon.xml se explica aquí:

    http://www.mimediacenter.info/2012/12/25/como-programar-add-ons-en-xbmc-los-ficheros-de-identificacion/

  8. Estoy intentando cargar el addon de ejemplo y me dice que no tiene las dependencias, he modificado tal como indicas y entonces el error que me da es que no tiene
    la estructura correcta. ¿Me puedes ayudar?

    Muchas gracias de antemano

  9. No se ve bien el post, has dejado varias etiquetas sin cerrar, muy buena guia!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *