Как стать автором
Обновить

Пишем свой плагин для XBMC. Пока без блекджека и всех остальных

Время на прочтение 13 мин
Количество просмотров 43K

Всем привет. Речь в топике пойдёт о создании плагина (программного дополнения, аддона) к замечательной программе XBMC. Уровень сложности: для начинающих. Понадобятся знания HTML и общее представление о работе сайтов; не помешает знать как выглядит Python. Не ждите под катом уникальных алгоритмов и магического кода, это скорее отправная точка и общее объяснение механики работы плагинов. Код будет, надеюсь, наглядным.

Некоторые из вас могут спросить: «Ведь есть репозиторий seppius, который решает почти все проблемы с воспроизведением онлайн-контента в рунете (в контексте XBMC). Зачем велосипеды?». Я приведу свои доводы в небольшом предисловии.



Предисловие


Я занялся разработкой плагинов XBMC, когда у меня появился лишний ноутбук. Было принято решение, чтобы мёртвым грузом не валялся, сделать мультимедиа приставку для телевизора (TV был лишён связи с Всемирной паутиной). XBMC был шикарным решением проблемы, но вот незадача: живу-то я в Кыргызстане. Весь трафик с IP-адресов не относящихся к нашей стране очень дорог («внешка», не знаю есть ли у вас подобное? Лишь пару месяцев назад стали появляться доступные безлимитные тарифы). А вот внутренний трафик практически бесплатен (многие популярные сайты с медиа-контентом вообще не тарифицируются у многих провайдеров). Придумано, сделано. К моменту написания статьи мной охвачены все нужные ресурсы. Теперь занимаюсь разработкой PVR плагина.

Я уже не пишу на Python код такой, какой будет приведён в статье. Но это было моей отправной точкой и пониманием механизмов работы дополнений.

Подготовка


Нам понадобятся:
  • Python 2.x не перепутайте с 3.x (здесь сказано что в XBMC используется версия 2.4, но мне кажется что информация устарела);
  • XBMC 11 Eden или XBMC 12 Frodo (сейчас для нас разницы нет, но лучше использовать последнюю версию, хоть она и RC)
  • Ваш любимый текстовый редактор

Устанавливаем вышеуказанные. Путь у Вас может отличаться – в скобках я указал свой: Python (C:\Python\, далее PathPython)

Не будет лишнем знать где лежит лог-файл XBMC:
Windows: %APPDATA%\XBMC\xbmc.log
Linux: $HOME/.xbmc/temp/xbmc.log
Mac OS X: /Users//Library/Logs/xbmc.log

Часть первая. Тренировка


Отмечу, что мои знания Python'а были практически нулевые. Поэтому, если и Вы с ним никогда не сталкивались, то пугаться не стóит. В изучении той области функционала, которая нам нужна, он очень прост. Ну, по крайней мере, мне так показалось.

В качестве подопытного кролика был выбран сайт LineCinema. Почему? Потому что гладиолус я зашёл в тему Запросы на создание плагинов (XBMC Russia) и на одной из последних страниц (если хотите точности, то на 87-ой) увидел запрос для этого сайта. Далее просто Сайт.

Итак, создаём в текстовом редакторе новый файл и пишем следующий код:

# -*- coding: utf-8 -*-

# Импортируем нужные нам библиотеки
import urllib, urllib2, re, sys, os

# Функция для получения исходного кода web-страниц
def GetHTML(url):
    headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3', 'Content-Type':'application/x-www-form-urlencoded'}
    conn = urllib2.urlopen(urllib2.Request(url, urlencode({}), headers))
    html = conn.read()
    conn.close()
    
    return html 

# Тест на работоспособность
html = GetHTML('http://www.linecinema.org/')
print html

Сохраняем PathPython/test.py

Открываем Командную строку и переходим в папку PathPython. Выполняем:

python test.py

Результатом её выполнения должен быть исходный код страницы Сайта.
Визуально

Теперь, в браузере открываем Сайт и лезем в исходный код (различные веб-инспекторы и firebug нам не помогут). Здесь надо обратить внимание на строчку

<meta http-equiv="Content-Type" content="text/html; charset=windows-1251" />

и не забыть, что кодировка страницы windows-1251.

Ищем ссылки раздела «Фильмы По Жанрам» и узнаём, что они имеют вид:

<a href="/newsz/drama-online/" title="" class="mainmenu">Драма</a><br />

Переключаемся (или открываем, если успели закрыть) на test.py. Удаляем строку
print html

И на её место пишем

genre_links = re.compile('<a href="(.+?)" title="" class="mainmenu">(.+?)</a><br />').findall(html.decode('windows-1251'))

# Сопоставляем результаты первого и второго (.+?) с переменными url и title (будут присвоены в порядке указания)
for url, title in genre_links:
    print title + ' [' + url + ']'

Где (.+?) регулярное выражение, пропускающее все символы (про регулярные выражения Python можно почитать на хабре).

Сохраняем файл и повторяем в Командной строке

python test.py

На этот раз результатом будут названия жанров и ссылка.
Визуально

Но есть лишние ссылки, которые в XBMC будут бесполезны: «Главная», «Заказать фильм», «Тех. Поддержка». Что с ними делать? Мы их исключим. Для начала грубо, хотя можно было бы использовать BeautifulSoup.

Напишем функцию isLinkUseful(), специально для Сайта, т. к. «лишние» ссылки будут объявлены внутри:

def isLinkUseful(needle):
    haystack = ['/index.php', '/newsz/Televydenye/100432-2008-3-11-432.html', '/newsz/500183-tex-podderzhka.html']  # список “лишних” ссылок
    return needle not in haystack

и изменим последние строчки:

for url, title in genre_links:
    if isLinkUseful(url):
        print title + ' [' + url + ']'

Проверяем. Всё как надо:
Визуально

Ещё немного тренировки. Теперь проверим один из разделов. Я взял «Документальный» (/newsz/dokumentalnyij-online/). Изменим код:

#url = 'http://www.linecinema.org/'
#html = GetHTML(url)
#genre_links = re.compile('<a href="(.+?)">(.+?)</a>').findall(html.decode('windows-1251'))
#for url, title in genre_links:
    #if isLinkUseful(url):
        #print title + ' [' + url + ']'

url = 'http://www.linecinema.org/newsz/dokumentalnyij-online/'
html = GetHTML(url)
genre_links = re.compile('<h1> <a href="(.+?)">(.+?)</a>   </h1>').findall(html.decode('windows-1251'))

for url, title in genre_links:
    print title

Проверяем и видим, что здесь исключать ничего не надо:
Визуально

Тут мы использовали

(.+?)

(да-да, с пробелами), т. к. в исходном коде тоже с пробелами и если бы мы использовали просто (.+?), то получили бы все ссылки со страницы. А оно нам надо?

Часть вторая. Адаптируемся к XBMC


Для начала было бы неплохо создать папку (где Вам больше всего нравится). И название можно было бы оставить «Новая папка (∞)», но не в этот раз. Прочитав Рекомендации к разработке дополнений можно узнать, что название папки составляется по следующему шаблону:

[.].

Следуя этому шаблону (я надеюсь, Вы всё-таки прочтёте рекомендации) название будет следующим:

plugin.video.linecinema

Зайдем внутрь папки и заполним пустоту. Официальные требования к видео/аудио/некоторым другим add-on’ам (если Вы до сих пор не в курсе, мы делаем именно видео-плагин) гласят, что структура должна быть следующей:

addon.py
addon.xml
changelog.txt
fanart.jpg
icon.png
LICENSE.txt
/resources
    settings.xml
    /language/
    /lib/
    /media/

  • addon.py – здесь будет находится основной код плагина. В принципе, название не имеет значения (так как мы сами укажем его в файле addon.xml). В большинстве плагинов он назван default.py
  • addon.xml – сообщает XBMC: тип плагина (видео, аудио, изображения, скрипт и так далее); какой файл надо выполнить при обращении (тот самый *.py); платформу и зависимости; версию, автора и описание плагина. О форматировании можно почитать здесь.
  • changelog.txt, LICENSE.txt – надеюсь, понятно из названия.
  • fanart.jpg – фоновое изображение плагина. Ссылка на рекомендации.
  • icon.png – "лицо" нашего плагина. Рекомендации.
  • /resources/settings.xml – хранит в себе настройки плагина (ну, к примеру, логин/пароль для авторизации на сайтах), т.е. то, что мы хотим использовать в addon.py. Хранит не сами значения конечно же, а разметку формы, куда мы будем эти значения вписывать. Описание и формат.
  • /resources/language/ - языковые файлы. Если хотите, чтобы Ваш плагин попал в официальный репозиторий, то абсолютно всё надо переводить.
  • /resources/lib/ - лучшее место для хранения дополнительных библиотек Python.
  • /resources/media/ - сюда прятать изображения, звуки, видео и т. п.


Из всего вышеперечисленного мы задействуем: addon.py, addon.xml и… всё. Но в конце статьи можно найти архив, в котором сохранена вся структура.

Код файла addon.xml:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<addon id="plugin.video.linecinema"
       name="LineCinema"
       version="0.0.1"
       provider-name="noname">
  <requires>
    <import addon="xbmc.python" version="1.0"/>
  </requires>
  <extension point="xbmc.python.pluginsource" provides="video" library="addon.py">
    <provides>video</provides>
  </extension>
  <extension point="xbmc.addon.metadata">
    <summary>LineCinema.org</summary>
    <description>
Привет, Хабрахабр.

Это тестовый плагин для сайта LineCinema.org
    </description>
    <platform>all</platform>
  </extension>
</addon>

Остановимся подробнее. Из строки XBMC узнает, что директория с плагином называется "plugin.video.linecinema", отображаемое название "LineCinema", версия 0.0.1, имя автора "noname".

Из строки />
, что для работы просто необходима библиотека xbmc.python версии 1.0.

Далее video точка входа – файл "addon.py". А сам плагин надо разместить во вкладке "Видео дополнения".

Если остались непонятные места, то попробуйте перечитать формат файла addon.xml.

Перейдём к файлу addon.py:

# -*- coding: utf-8 -*-

import urllib, urllib2, re, sys
import xbmcplugin, xbmcgui

def get_params():
    param=[]
    paramstring=sys.argv[2]
    if len(paramstring)>=2:
        params=sys.argv[2]
        cleanedparams=params.replace('?','')
        if (params[len(params)-1]=='/'):
            params=params[0:len(params)-2]
        pairsofparams=cleanedparams.split('&')
        param={}
        for i in range(len(pairsofparams)):
            splitparams={}
            splitparams=pairsofparams[i].split('=')
            if (len(splitparams))==2:
                param[splitparams[0]]=splitparams[1]
                            
    return param

xbmcplugin и xbmcgui – модули из библиотеки xbmc.python.

Функция get_params() – честно говоря, даже не пытался разобраться в том, что она делает. Шестым чувством догадываюсь, что разбивает строку обращения плагина на параметры, позволяющее сохранять переменные при переходе по директориям плагина. Если непонятно, то не кидайте тухлое яйцо в меня. Это функция, которая очень часто встречается в плагинах, взята из одного из них.

Пишем дальше:

def addLink(title, url):
    item = xbmcgui.ListItem(title, iconImage='DefaultVideo.png', thumbnailImage='')
    item.setInfo( type='Video', infoLabels={'Title': title} )

    xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=item)


def addDir(title, url, mode):
    sys_url = sys.argv[0] + '?title=' + urllib.quote_plus(title) + '&url=' + urllib.quote_plus(url) + '&mode=' + urllib.quote_plus(str(mode))

    item = xbmcgui.ListItem(title, iconImage='DefaultFolder.png', thumbnailImage='')
    item.setInfo( type='Video', infoLabels={'Title': title} )

    xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=sys_url, listitem=item, isFolder=True)


addDir(title, url, mode) – будет создавать пункт для перехода по ссылке и отображения новых пунктов. Mode – переменная, хранящая номер функции для обработки ссылки (ниже будет понятно что к чему).

addLink(title, url) – будет создавать пункт, при переходе на который начнётся воспроизведение url. В url должна храниться прямая ссылка на видео/аудио/изображение и т.п.

Добавляем ниже:

params = get_params()
url    = None
title  = None
mode   = None

try:    title = urllib.unquote_plus(params['title'])
except: pass

try:    url = urllib.unquote_plus(params['url'])
except: pass

try:    mode = int(params['mode'])
except: pass

xbmcplugin.endOfDirectory(int(sys.argv[1]))

Это будут наши "глобальные переменные". А xbmcplugin.endOfDirectory(int(sys.argv[1])) сообщит XBMC, что это конец "директории" и больше пунктов не будет.

Осталось совсем немного. Добавляем наши функции getHTML(url) и isLinkUseful(needle) из первой части и теперь addon.py должен выглядеть следующим образом:
addon.py
# -*- coding: utf-8 -*-

import urllib, urllib2, re, sys
import xbmcplugin, xbmcgui

def getHTML(url):
    headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3', 'Content-Type':'application/x-www-form-urlencoded'}
    conn = urllib2.urlopen(urllib2.Request(url, urllib.urlencode({}), headers))
    
    html = conn.read()
    conn.close()
    
    return html

def isLinkUseful(needle):
    haystack = ['/index.php', '/newsz/Televydenye/100432-2008-3-11-432.html', '/newsz/500183-tex-podderzhka.html']
    return needle not in haystack

def get_params():
    param=[]
    paramstring=sys.argv[2]
    if len(paramstring)>=2:
        params=sys.argv[2]
        cleanedparams=params.replace('?','')
        if (params[len(params)-1]=='/'):
            params=params[0:len(params)-2]
        pairsofparams=cleanedparams.split('&')
        param={}
        for i in range(len(pairsofparams)):
            splitparams={}
            splitparams=pairsofparams[i].split('=')
            if (len(splitparams))==2:
                param[splitparams[0]]=splitparams[1]
                            
    return param


def addLink(title, url):
    item = xbmcgui.ListItem(title, iconImage='DefaultVideo.png', thumbnailImage='')
    item.setInfo( type='Video', infoLabels={'Title': title} )

    xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=item)


def addDir(title, url, mode):
    sys_url = sys.argv[0] + '?title=' + urllib.quote_plus(title) + '&url=' + urllib.quote_plus(url) + '&mode=' + urllib.quote_plus(str(mode))

    item = xbmcgui.ListItem(title, iconImage='DefaultFolder.png', thumbnailImage='')
    item.setInfo( type='Video', infoLabels={'Title': title} )

    xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=sys_url, listitem=item, isFolder=True)


params = get_params()
url    = None
title  = None
mode   = None

try:    title = urllib.unquote_plus(params['title'])
except: pass

try:    url = urllib.unquote_plus(params['url'])
except: pass

try:    mode = int(params['mode'])
except: pass

xbmcplugin.endOfDirectory(int(sys.argv[1]))


У Вас так же? Если да, то отлично.

Теперь опять вспомним первую часть и проанализируем Сайт: у нас будет список жанров; было бы неплохо найти и показать какие фильмы есть в каждой из категорий; отобразить ссылку на просмотр фильма. Получается нам нужны три функции. Назовём их так: Categories() – список жанров, Movies() – список доступных фильмов, Videos() – ссылки на видео. Приступим:

def Categories():
	url = 'http://www.linecinema.org'
	html = getHTML(url)
	genre_links = re.compile('<a href="(.+?)" title="" class="mainmenu">(.+?)</a><br />').findall(html.decode('windows-1251').encode('utf-8'))

	for link, title in genre_links:
	    if isLinkUseful(link):
	        addDir(title, url + link, 20)


addDir(title, url + link, 20) заменил нам print title + ' [' + url + ']'. Categories() не будет иметь входных параметров - url нам надо объявить самим. Обратите внимание, что последний слеш убран – он будет компенсирован переменной link.

Цифра 20 – сообщит, что следующая функция будет Movies(url). Но ведь магии не бывает и нам надо написать для этого в конец файла, перед xbmcplugin.endOfDirectory(int(sys.argv[1])), проверку:

if mode == None:
    Categories()

elif mode == 20:
    Movies(url)

elif mode == 30:
    Videos(url, title)

Чем это попахивает тут? Не говнокодом ли? Ну точно, им самым. Но такая модель помогла мне разобраться что к чему, сейчас я её, конечно же, не использую. А вот мой первый плагин был точно таким же.

Не отвлекаемся и парсим список фильмов:

def Movies(url):
	html = getHTML(url)
	movie_links = re.compile('<h1> <a href="(.+?)">(.+?)</a>   </h1>').findall(html.decode('windows-1251').encode('utf-8'))

	for link, title in movie_links:
	    addDir(title[:-12], link, 30)

Опять код из первой части. title[:-12] что это за страшный смайлик? Если убрать [:-12], то вы увидите, что название будет иметь вид "Умопомрачительный фильмец / Magnificent movie (2013) HDRip онлайн". И зачем нам "онлайн"? Убрать его!

Теперь надо выдернуть прямую ссылку на видео. Открываем любую ссылку фильма с Сайта и смотрим его исходный код. Ого, да тут нас ждали! Видим вот такой блок:

<script language="javascript"> 
	flashvars = { 
	uid: "player_uppod", 
	bufferproc2 :1, 
	bufferproc_reloadsec :10, 
	st: "/templates/linecinema-dle90/swf/video30-365.txt", 
	file: "http://st7.linecinema.org/s/820e31e7cbe3e8c362785b733db56c57/film10/Druzea.navek.DVDRip.flv" 
	}; 
	params = {bgcolor:"#ffffff", allowFullScreen:"true", allowScriptAccess:"always",wmode:"opaque"}; 
	attributes = { 
	id: "player_uppod", 
	name: "player_uppod" 
	}; 
	swfobject.embedSWF("/templates/linecinema-dle90/swf/uppod.swf", "player_uppod", "570", "440", "10.0.0",false,flashvars,params,attributes); 
	</script>

А вот и ссылка: st7.linecinema.org/s/820e31e7cbe3e8c362785b733db56c57/film10/Druzea.navek.DVDRip.flv (у Вас может отличаться). Исходя из этого делаем функцию:

def Videos(url, title):
	html = getHTML(url)
	link = re.compile('file:   "(.+?)"').findall(html.decode('windows-1251').encode('utf-8'))[0]

	addLink(title, link)

С кодом закончили. Сравните свой результат с моим:

Файл addon.py
# -*- coding: utf-8 -*-

import urllib, urllib2, re, sys
import xbmcplugin, xbmcgui

def getHTML(url):
    headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3', 'Content-Type':'application/x-www-form-urlencoded'}
    conn = urllib2.urlopen(urllib2.Request(url, urllib.urlencode({}), headers))
    
    html = conn.read()
    conn.close()
    
    return html

def isLinkUseful(needle):
    haystack = ['/index.php', '/newsz/Televydenye/100432-2008-3-11-432.html', '/newsz/500183-tex-podderzhka.html']
    return needle not in haystack

def Categories():
	url = 'http://www.linecinema.org'
	html = getHTML(url)
	genre_links = re.compile('<a href="(.+?)" title="" class="mainmenu">(.+?)</a><br />').findall(html.decode('windows-1251').encode('utf-8'))

	for link, title in genre_links:
	    if isLinkUseful(link):
	        addDir(title, url + link, 20)

def Movies(url):
	html = getHTML(url)
	movie_links = re.compile('<h1> <a href="(.+?)">(.+?)</a>   </h1>').findall(html.decode('windows-1251').encode('utf-8'))

	for link, title in movie_links:
	    addDir(title[:-12], link, 30)

def Videos(url, title):
	html = getHTML(url)
	link = re.compile('file:   "(.+?)"').findall(html.decode('windows-1251').encode('utf-8'))[0]

	addLink(title, link)


def get_params():
    param=[]
    paramstring=sys.argv[2]
    if len(paramstring)>=2:
        params=sys.argv[2]
        cleanedparams=params.replace('?','')
        if (params[len(params)-1]=='/'):
            params=params[0:len(params)-2]
        pairsofparams=cleanedparams.split('&')
        param={}
        for i in range(len(pairsofparams)):
            splitparams={}
            splitparams=pairsofparams[i].split('=')
            if (len(splitparams))==2:
                param[splitparams[0]]=splitparams[1]
                            
    return param


def addLink(title, url):
    item = xbmcgui.ListItem(title, iconImage='DefaultVideo.png', thumbnailImage='')
    item.setInfo( type='Video', infoLabels={'Title': title} )

    xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=item)


def addDir(title, url, mode):
    sys_url = sys.argv[0] + '?title=' + urllib.quote_plus(title) + '&url=' + urllib.quote_plus(url) + '&mode=' + urllib.quote_plus(str(mode))

    item = xbmcgui.ListItem(title, iconImage='DefaultFolder.png', thumbnailImage='')
    item.setInfo( type='Video', infoLabels={'Title': title} )

    xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=sys_url, listitem=item, isFolder=True)


params = get_params()
url    = None
title  = None
mode   = None

try:    title = urllib.unquote_plus(params['title'])
except: pass

try:    url = urllib.unquote_plus(params['url'])
except: pass

try:    mode = int(params['mode'])
except: pass

if mode == None:
    Categories()

elif mode == 20:
    Movies(url)

elif mode == 30:
    Videos(url, title)

xbmcplugin.endOfDirectory(int(sys.argv[1]))


Наконец-то, перейдём к установке плагина. Существует два простых способа: установка из репозитория и установка из файла ZIP. Нам подходит второй. Запаковываем в ZIP нашу папку (папку!) любым архиватором. Затем запускаем XBMC, переходим Система (Настройки) > Дополнения > Установить из файла ZIP и указываем путь до нашего архива. После сообщения о том, что "Дополнение успешно включено", идём в главное меню Видео > Дополнения > LineCinema.

Скриншоты





Полезные ссылки




Спасибо за внимание. Надеюсь было интересно… или полезно. Не исключаю, что у меня ужасная манера изложения, но это первый опыт в написании статей.

P.S. Пожалуйста, не кидайте в меня тухлые помидоры.
Теги:
Хабы:
+34
Комментарии 9
Комментарии Комментарии 9

Публикации

Истории

Работа

Python разработчик
130 вакансий
Data Scientist
66 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн