Всем привет. Речь в топике пойдёт о создании плагина (программного дополнения, аддона) к замечательной программе XBMC. Уровень сложности: для начинающих. Понадобятся знания HTML и общее представление о работе сайтов; не помешает знать как выглядит Python. Не ждите под катом уникальных алгоритмов и магического кода, это скорее отправная точка и общее объяснение механики работы плагинов. Код будет, надеюсь, наглядным.
Некоторые из вас могут спросить: «Ведь есть репозиторий seppius, который решает почти все проблемы с воспроизведением онлайн-контента в рунете (в контексте XBMC). Зачем велосипеды?». Я приведу свои доводы в небольшом предисловии.
Предисловие
Я занялся разработкой плагинов XBMC, когда у меня появился лишний ноутбук. Было принято решение, чтобы мёртвым грузом не валялся, сделать мультимедиа приставку для телевизора (TV был лишён связи с Всемирной паутиной). XBMC был шикарным решением проблемы, но вот незадача: живу-то я в Кыргызстане. Весь трафик с IP-адресов не относящихся к нашей стране очень дорог («внешка», не знаю есть ли у вас подобное? Лишь пару месяцев назад стали появляться доступные безлимитные тарифы). А вот внутренний трафик практически бесплатен (многие популярные сайты с медиа-контентом вообще не тарифицируются у многих провайдеров), а вот XBMC как-будто вообще никто до меня не пользовался. Придумано, сделано. К моменту написания статьи мной охвачены все нужные ресурсы. Теперь занимаюсь разработкой PVR плагина.
Я уже не пишу на Python код такой, какой будет приведён в статье. Но это было моей отправной точкой и пониманием механизмов работы дополнений.
Подготовка
Нам понадобятся:
- Python 2.x не перепутайте с 3.x (здесь сказано что в XBMC используется версия 2.4, но мне кажется что информация устарела);
- XBMC 11 Eden или XBMC 12 Frodo (сейчас для нас разницы нет, но лучше использовать последнюю версию, хоть она и RC)
- Ваш любимый текстовый редактор
Устанавливаем вышеуказанные. Путь у Вас может отличаться – в скобках я указал свой: Python (C:Python, далее PathPython)
Не будет лишнем знать где лежит лог-файл XBMC:
Windows: %APPDATA%XBMCxbmc.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'] # список “лишних” ссылок
for a in haystack:
if needle == a:
return False
return True
и изменим последние строчки:
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
Проверяем и видим, что здесь исключать ничего не надо:
Тут мы использовали <h1> <a href="(.+?)">(.+?)</a> </h1>
(да-да, с пробелами), т. к. в исходном коде тоже с пробелами и если бы мы использовали просто <a href="(.+?)">(.+?)</a>
, то получили бы все ссылки со страницы. А оно нам надо?
Часть вторая. Адаптируемся к XBMC
Для начала было бы неплохо создать папку (где Вам больше всего нравится). И название можно было бы оставить «Новая папка (∞)», но не в этот раз. Прочитав Рекомендации к разработке дополнений можно узнать, что название папки составляется по следующему шаблону:
<addonType>[.<mediaType>].<yourPluginName>
Следуя этому шаблону (я надеюсь, Вы всё-таки прочтёте рекомендации) название будет следующим:
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>
Остановимся подробнее. Из строки <addon id="plugin.video.linecinema" name="LineCinema" version="0.0.1" provider-name="noname">
XBMC узнает, что директория с плагином называется «plugin.video.linecinema», отображаемое название «LineCinema», версия 0.0.1, имя автора «noname».
Из строки <requires><import addon="xbmc.python" version="1.0"/></requires>
, что для работы просто необходима библиотека xbmc.python версии 1.0.
Далее <extension point="xbmc.python.pluginsource" provides="video" library="addon.py"><provides>video</provides></extension>
точка входа – файл «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 должен выглядеть следующим образом:
# -*- 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']
for a in haystack:
if needle == a:
return False
return True
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>
А вот и ссылка: http://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)
С кодом закончили. Сравните свой результат с моим:
# -*- 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']
for a in haystack:
if needle == a:
return False
return True
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.
Полезные ссылки
- ZIP с исходниками
- Официальные руководства: HOW-TO:Write plugins for XBMC (wiki) и XBMC Addon Developers Guide (pdf)
- Неофициальные репозитории аддонов: 3rd party add-on repositories
- Форум XBMC Russia
- XBMC Community Forum
Спасибо за внимание. Надеюсь было интересно… или полезно. Не исключаю, что у меня ужасная манера изложения, но это первый опыт в написании статей.
P.S. Пожалуйста, не кидайте в меня тухлые помидоры.
Автор: delletenebre