Para comprender mejor como funciona por dentro un canal multiplataforma en pelisalacarta, vamos a analizar paso a paso uno de los sencillos. He elegido el canal «seriematic», una web de series con una estructura muy limpia que se refleja claramente en el código del módulo Python.
El canal seriematic puedes encontrarlo en esta dirección.
Cabecera
# -*- coding: utf-8 -*- #------------------------------------------------------------ # pelisalacarta - XBMC Plugin # Canal para seriematic # http://blog.tvalacarta.info/plugin-xbmc/pelisalacarta/ #------------------------------------------------------------
Lo primero que te vas a encontrar es la cabecera donde se incluyen algunas líneas de comentario con la información sobre el módulo Python que estás viendo.
Es especialmente importante la primera línea, que le especifica al intérprete de Python y a los editores sobre la codificación que emplea el fichero para los caracteres internacionales. Si esta información no está indicada Python dará un error al ejecutar el módulo, y si está informada erróneamente los caracteres internacionales se mostrarán mal.
import urlparse,urllib2,urllib,re import os, sys
A continuación se incluyen los paquetes estándar de Python más habituales, entre los que suelen encontrarse los de expresiones regulares (paquete re), manipulación de URL (urllib), así como utilidades de bajo nivel (paquetes os y sys).
try: from core import logger from core import config from core import scrapertools from core.item import Item from servers import servertools except: # En Plex Media server lo anterior no funciona... from Code.core import logger from Code.core import config from Code.core import scrapertools from Code.core.item import Item
Los módulos del paquete «core» de pelisalacarta se encargan de las funciones comunes a todos los canales, y es donde se encuentra resuelto el problema de la multiplataforma.
Basta con que uses el módulo «logger» para escribir un log, o el módulo «config» para leer la configuración, y puedes olvidarte de cómo se resuelve cada uno de esos problemas en las diferentes plataformas.
CHANNELNAME = "seriematic" DEBUG = True def isGeneric(): return True
A continuación unas pocas líneas de relleno (boilerplate code) que se ponen siempre. Una constante CHANNELNAME con el nombre del canal para simplificar el copy-paste de código entre canales, otra para usar en depuración, y la función «isGeneric» que permite identificar a este canal como multiplataforma.
Menú principal
def mainlist(item): logger.info("[seriematic.py] mainlist") itemlist = [] itemlist.append( Item(channel=CHANNELNAME, title="Series", action="series", url="http://www.seriematic.com/series.php")) itemlist.append( Item(channel=CHANNELNAME, title="Miniseries", action="series", url="http://www.seriematic.com/miniseries.php")) itemlist.append( Item(channel=CHANNELNAME, title="Dibujos", action="series", url="http://www.seriematic.com/dibujos.php")) itemlist.append( Item(channel=CHANNELNAME, title="Anime", action="series", url="http://www.seriematic.com/manga.php")) return itemlist
El menú principal del canal se tiene que implementar en una función de nombre «mainlist», que devuelve una lista Python con las entradas que deben mostrarse al usuario.
Cada entrada de esta lista consiste en un objeto «Item», que es una representación más genérica del elemento «ListItem» de XBMC. En su versión más sencilla permite almacenar los valores básicos:
- channel: Nombre del canal donde está la acción a realizar
- action: Nombre de la función dentro del canal que realiza la acción cuando el usuario seleccione esta entrada
- title: El texto que se debe mostrar en pantalla al usuario cuando se liste esta entrada
- url: La URL asociada a esta entrada, si es necesario. Si la acción consiste en descargar un listado de categorías, en este atributo vendrá la URL.
Navegación
Supongamos que el usuario elige ahora la entrada titulada «Series». Como ese item tiene la acción «series», ese es el nombre de la siguiente función que se ejecuta en el módulo Python.
def series(item): logger.info("[seriematic.py] series") # Descarga la página data = scrapertools.cachePage(item.url) # Extrae las entradas patronvideos = '<td><a href="([^"]+)">([^<]+)</a></td>' matches = re.compile(patronvideos,re.DOTALL).findall(data) if DEBUG: scrapertools.printMatches(matches) itemlist = [] for match in matches: scrapedtitle = match[1] scrapedplot = "" scrapedurl = urlparse.urljoin(item.url,match[0]) scrapedthumbnail = "" # Añade al listado itemlist.append( Item(channel=CHANNELNAME, action="episodios", title=scrapedtitle, url=scrapedurl , thumbnail=scrapedthumbnail , plot=scrapedplot , folder=True) ) return itemlist
La función recibe como parámetro el item seleccionado por si quieres utilizar la URL para descargar la página, como ocurre en este caso.
La página descargada analizada con una expresión regular para obtener los nombres de las series, y para cada coincidencia encontrada se añade un elemento a la lista de items marcando esta vez como acción «episodios».
A su vez la acción «episodios» vuelve a hacer un procesamiento similar, pero ahora se buscan episodios de la serie elegida utilizando la URL que ha venido como parámetro.
def episodios(item): logger.info("[seriematic.py] episodios") # Descarga la página data = scrapertools.cachePage(item.url) # Extrae las entradas patronvideos = '<\!-- Column 1 start -->(.*?)<\!-- Column 1 end -->' matches = re.compile(patronvideos,re.DOTALL).findall(data) if DEBUG: scrapertools.printMatches(matches) itemlist = [] for elemento in matches: patronvideos = '<tr><td>([^<]+)<a href="([^"]+)">([^<]+)' patronvideos += '</a></td></tr>' matches2 = re.compile(patronvideos,re.DOTALL).findall(elemento) for match in matches2: scrapedtitle = match[0]+" "+match[2] scrapedplot = "" scrapedurl = urlparse.urljoin(item.url,match[1]) scrapedthumbnail = "" # Añade al listado itemlist.append( Item(channel=CHANNELNAME, action="videos", title=scrapedtitle , url=scrapedurl , thumbnail=scrapedthumbnail , plot=scrapedplot , folder=True) ) return itemlist
Observa que en este caso se aplican dos expresiones regulares en dos bucles «for» anidados. Esta técnica permite simplificar expresiones regulares complejas, aislando primero la zona del HTML donde está lo que te interesa para en una segunda pasada extraer los elementos.
Los vídeos
Cada entrada que devuelve la función «episodios» es un episodio de la serie, así que sólo falta recuperar las diferentes alternativas (o mirrors) de cada episodio. Esto se hace en la función «vídeos».
def videos(item): logger.info("[seriematic.py] videos") # Descarga la página data = scrapertools.cachePage(item.url) # Extrae las entradas patronvideos = '<tr><td>([^<]+)<script type="text/javascript">' patronvideos += 'p1.\'([^\']+)\'\,\'V\'\)\;</script>' patronvideos += '<img src="[^"]+" alt="[^"]+"[^>]+/>([^<]+)<' matches = re.compile(patronvideos,re.DOTALL).findall(data) if DEBUG: scrapertools.printMatches(matches) itemlist = [] for match in matches: scrapedtitle = match[0]+" "+match[2] scrapedplot = "" scrapedurl = match[1] scrapedthumbnail = "" server="Megavideo" # Añade al listado itemlist.append( Item(channel=CHANNELNAME, action="play", title=scrapedtitle , url=scrapedurl , server=server , folder=False) ) return itemlist
Esta función busca las diferentes alternativas de vídeo a reproducir, y las añade a la lista con tres diferencias básicas respecto a lo que hemos visto en el resto de casos:
- server: Cada entrada es un vídeo alojado en un servidor, aunque en este canal sólo se encuentran vídeos de "Megavideo". La URL corresponde con la de la página de Megavideo, y el parámetro server sirve para que el plugin sepa obtener el vídeo a partir de ella.
- action: La acción para que el vídeo se reproduzca es "play". No hace falta que pongas esta función, ya ha sido definida por tí para que no tengas que repetirla cada vez, aunque si añades una en el canal tendrá prioridad sobre la estándar.
- folder: Estas entradas ya no tienen más navegación, así que dejan de ser "carpetas" para ser "archivos" siguiendo el símil de un sistema de ficheros.