转载自 http://bbs.htpc1.com/thread-198731-1-1.html 稍做改动和整理
xbmc视频插件采用python语言编程,插件启动后会下载网页,截取其中的一些内容(文字或网址),交给xbmc或显示视频的目录,或播放视频。
学习本教程必须具备python、html的基础知识。
python是一种简单易学的计算机语言,网上的《简明Python教程》看一两天就能入门(如果有其它语言编程基础的话),还有个《HTML教程》,也是很容易学习的。
【一、插件的结构】
每个插件保存在各自的文件夹里,打开xbmc插件的安装目录addons(C:\Documents and Settings\johnny.he\Application Data\XBMC\addons),可以看到很多安装好的插件的文件夹。
一个典型的视频插件的文件夹里通常要有这三个文件:
addon.xml:说明文件,用来告诉xbmc该插件所提供的内容以及安装要求;
icon.png:图标文件,256x256像素,xbmc插件清单旁边显示的就是这个图标;
*.py:python主程序文件,名字随便起,addon.xml中有一项会用到这个文件名。
如果插件的子程序和用到的数据较多,可以在下面再开子文件夹来保存。
重点看addon.xml的结构:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.hello"
name="helloworld演示"
version="0.0.1"
provider-name="Johnny">
<extension point="xbmc.python.pluginsource" library="helloworld.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<platform>all</platform>
<summary>hello world plugin</summary>
<summary lang="zh">helloworld演示插件</summary>
<description>hello world plugin, python script</description>
<description lang="zh">helloworld演示插件,python脚本</description>
<disclaimer>For demonstration only</disclaimer>
</extension>
</addon>
首先第一行申明这是一个xml文件,且是utf-8编码的,接着是<addon>元素,在<addon>和</addon>之间,有一个或多个<extension>元素,每个<extension>告诉xbmc这个插件扩展了哪一部分的功能,只有最后一个<extension>比较特别,只是用来向用户做个说明。
<addon>元素有4个属性:id, version, name, provider-name
id属性:插件id,必须是唯一的,小写,插件zip文件解压后,以id命名文件夹,所以不能重名,建议以plugin.<类别>.<唯一名>的形式命名,类别可以是video, audio, script等;
version属性:插件版本号,格式为x.x.x,xbmc用来确定是否应升级插件;
name属性:插件名称;
provider-name属性:插件作者。
<extension>元素通过point属性告诉xbmc该插件扩展的类型,有多种类型,用python写的视频插件属xbmc.python.pluginsource类型,同时还应加上一个library属性告诉xbmc启动插件时运行哪个python程序。<provides>video</provides>告诉xbmc这是对视频功能的扩展。最后一个xbmc.addon.metadata类型的<extension>元素用来说明插件的运行环境、摘要、详情、申明等,英文的必须有,中文的可有可无。
插件做好后,把所有文件压缩进zip文件包中发行,xbmc在安装插件时会检查其中的addon.xml,确认格式无误后,才把它解压到addons目录下,成为一个可用的插件。
【二、做一个hello world版的插件】
很多计算机语言的教程开篇都有“hello world”的最简单的例子,让读者马上体验一下该编程语言的特点,在这里我们也做一个最简单的xbmc插件,只要会复制粘贴就能做出来。它显示一个“Hello, World!"的菜单,点击菜单可看一则网络视频。
做法:
1、在xbmc的插件目录addons下新建一个子目录plugin.video.hello,它的目录名也是插件的id名,用来存放插件的3个文件
2、把下面的icon.png文件拷进新建的目录
3、用记事本编辑和保存一个名为addon.xml的文件,内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.hello"
name="helloworld插件"
version="0.0.1"
provider-name="Johnny">
<extension point="xbmc.python.pluginsource" library="helloworld.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<platform>all</platform>
<summary>hello world plugin</summary>
<summary lang="zh">helloworld</summary>
<description>hello world plugin, python script</description>
<description lang="zh">helloworld插件测试,python开发</description>
<disclaimer>For demonstration only</disclaimer>
</extension>
</addon>
4、再用记事本生成一个名为helloworld.py的文件,内容如下:
# -*- coding: utf-8 -*-
# helloworld.py
# hello world demo
import xbmcplugin, xbmcgui
url='http://qiyi.soooner.com/videos2/movie/20130326/83ab91dc7bed45d9875f22d5d9eda6bf.f4v'
url_a='http://tv1.btv.com.cn/asset/2012/03/27/BTV1_20120327_183405049_742928_23560.mp4'
handle=int(sys.argv[1])
listitem=xbmcgui.ListItem('xinwen')
xbmcplugin.addDirectoryItem(handle, url_a, listitem)
xbmcplugin.endOfDirectory(handle)
注意:以上两个文本文件都必须以utf-8的编码方式保存。记事本用“文件-另存为”保存文件,选择“编码”为“UTF-8"。注意复制时不能“添油加醋”,在语句前加点空格什么的。
启动xbmc,进入视频扩展功能,就可以看到一个新加入的插件”helloworld插件“,进入插件看看是什么效果。
helloworld.py程序的说明:
前3行为注释,同时第1行说明本文件采用utf-8编码;
import导入xbmc的两个库,即xbmc对外提供的对外编程接口API,使用其中的类、函数可以操控xbmc的界面;
handle=...从命令行参数中获得句柄,句柄是xbmc启动插件脚本时赋予脚本的一个整数值;
xbmcgui.ListItem(...)创建一个ListItem类的实例对象,它带有"xinwen"的文本标签;
xbmcplugin.addDirectoryItem(...)把上面的ListItem对象和视频网址url作为一个菜单项添加到菜单上;
xbmcplugin.endOfDirectory(...)告诉xbmc菜单项已全部添加完毕,可以显示了。
小试验:
把程序的最后一行去掉会怎么样?
把url换成普通网页的网址会怎么样?
不用担心,尽管试,你的xbmc没那么容易崩溃。
【三、构建多级目录(上)】
假设有这样一个视频网站,网站首页上有3大栏目,进入大栏目可看到5个小栏目,小栏目再进去可以看到10个视频链接。针对网站这样的布局,很容易设想出要做的视频插件要有3级目录,进入到第3级,也就是末级,才开始观看视频。
在做这样的插件前,我们必须首先学习xbmcplugin.addDirectoryItem()这个函数的用法,这是构建目录时用到的最为关键的一个函数。
先看函数的原型(原文在这里)
addDirectoryItem(handle, url, listitem [,isFolder, totalItems]) -- Callback function to pass directory contents back to XBMC.
- Returns a bool for successful completion.
handle : integer - handle the plugin was started with.
url : string - url of the entry. would be plugin:// for another virtual directory
listitem : ListItem - item to add.
isFolder : [opt] bool - True=folder / False=not a folder(default).
totalItems : [opt] integer - total number of items that will be passed.(used for progressbar)
这个函数向xbmc传递目录的内容,即往目录上添加目录项,返回值表示是否成功。
函数有5个输入参数,其中前3个必需,后2个可选。
handle: 整数值,句柄,xbmc启动插件时赋予插件一个句柄
url: 字符串,视频的网址。如果是另一个目录的话则必须以plugin://打头,plugin://后跟插件id名,意思是下一级子目录由这个插件生成
listitem: ListItem对象,要加入的目录项,ListItem对象中有很多属性和方法,用来存放一个视频的标题、图片、路径等,详细见这里,这里我们只设置它的label属性
isFolder: 布尔值,True表示本项是个目录,点下去还有子目录,False表示本项是末级目录,缺省值是False
totalItems: 整数值,要加入的目录项的总数,xbmc用来确定目录生成时进度条的进度,不给这个参数,进度条上无进度显示
为加深对这个函数的理解,请你做个小试验,把下面的代码复制到helloworld.py中,启动xbmc,进入helloworld插件看看,再去查看xbmc的运行日志文件xmbc.log,仔细研究日志中"BEGIN"和“END”之间的日志记录,把url的值换成另一个,再试一次,再去看日志记录有什么不同,直到试完5个替换值,然后把xbmcplugin.addDirectoryItem()中的isFolder参数的值由True换成False,再来一次,再看看日志记录。经过这样的试验后,基本上你就可以搞清楚xbmcplugin.addDirectoryItem()这个函数该怎么用才能正确地生成目录而不会出错。
# -*- coding: utf-8 -*-
# helloworld.py
# hello world demo2
import xbmcplugin, xbmcgui
url='plugin://plugin.video.hello'
#url='plugin://plugin.noname.noname'
#url='plugin://plugin.video.tudou/'
#url='plugin://plugin.video.hello/abc/index.htm?url=example&name=123'
#url='http://tv1.btv.com.cn/asset/2012/03/27/BTV1_20120327_183405049_742928_23560.mp4'
print 'BEGIN: '+sys.argv[0]+'|'+sys.argv[1]+'|'+sys.argv[2]+'|'
handle=int(sys.argv[1])
listitem=xbmcgui.ListItem('Hello, World!')
xbmcplugin.addDirectoryItem(handle, url, listitem, True)
xbmcplugin.endOfDirectory(handle)
print 'END'
程序说明:
增加了两个print语句,一个打印插件脚本启动时xbmc传递给脚本的3个参数,另一个报告脚本执行完毕。
在xbmc中运行插件程序和一般环境中运行python程序不同,是没有调试器可用的,程序的运行状况又不能显示到xbmc界面上,程序的调试只能靠在程序中加些print语句打印变量值,再到xbmc的日志记录中去查看。
【三、构建多级目录(下)】
经过上一篇的小试验,我们了解了xbmc调用python脚本生成目录的机制,以及xbmcplugin.addDirectoryItem()的用法,现在做一个小结:
1、插件脚本生成完目录后会退出不再运行,等到点击某个目录项,xbmc又再次调用脚本生成新的目录,但是也有可能不再调用脚本,而是直接播放视频,这取决于生成该目录项时所使用的isFolder参数是True还是False;
2、如果生成目录项时的isFolder参数是True,目录项点击后,xbmc会根据生成目录项时的另一个参数url调用对应的脚本生成下一级目录;
3、xbmc调用脚本时会传递3个参数:
sys.argv[0]: 格式:plugin://插件id名/部分网址,首次调用时无部分网址
sys.argv[1]: 句柄,通常是0,末级目录是-1
sys.argv[2]: 部分网址(问号以后),首次调用时为空白
显然要形成多级目录,需要xbmc在用户的操作下多次调用脚本,而我们可以通过精心设置url的值,把生成目录所需要的有关参数传递给即将运行的脚本,如网页的网址和处理模式、目录的名称等。
这里作为举例说明,我把url设计成这种形式:plugin://插件id名/?下一级目录的级数,因为我的脚本只需要知道目录级数就能把目录做出来。
下面的代码就是针对开头提到那个假想网站所作,它生成3X5X10的目录,对应网站的3大栏目、5小栏目、10个视频的架构
# -*- coding: utf-8 -*-
# helloworld.py
# hello world demo3
import xbmcplugin, xbmcgui
# 各级目录的名称,项目数
dirs=[('MainDir', 3), ('SecondDir', 5), ('Video', 10)]
# 准备构建目录要用到的handle, url, isFolder变量
handle=int(sys.argv[1])
dir_level=0
isFolder=True
if sys.argv[2]!='':
dir_level=int(sys.argv[2][1:])
url=sys.argv[0]+'?'+str(dir_level+1)
if dir_level==2: # 如果是最后一级目录
isFolder=False
url='http://tv1.btv.com.cn/asset/2012/03/27/BTV1_20120327_183405049_742928_23560.mp4'
# 构建n条目录项
for i in range(dirs[dir_level][1]):
listitem=xbmcgui.ListItem(dirs[dir_level][0]+str(i+1))
xbmcplugin.addDirectoryItem(handle, url, listitem, isFolder)
# 目录构建完了,显示吧
xbmcplugin.endOfDirectory(handle)
【四、下载和处理网页】
上一篇我们把多级目录做出来了,不过是个“死”的目录,要想做成一个“活”的目录,还需要下载网页,从中取出有关内容做为目录项的名字,这样在网站更新时,目录也一样会有变化。
在python里下载网页,只需下面2条语句就搞定了:
import urllib
urllib.urlopen('http://live.sympy.org').readlines()
下载了网页,剩下的就是过滤出我们想要的内容,python里提供了正则表达式这一强大的文本处理工具。我们构造一个正则表达式让它去匹配网页中的具有某种特征的字符串,把这些字符串取出来,就做成了我们的目录。这有点象把一块有很多洞洞的板放在一幅画上,我们只看露出来的那部分画。这部分代码在插件脚本中占大头,也最繁琐,要不停地尝试,直到找到合适的正则表达式。
这里以“BTV在线”网站为例说明。首先观察网站的架构,很象我们说过的“大小栏目-视频”架构,仔细看各网页页面,布局、风格都差不多。
再打开网页源文件看,发现有关“大小栏目-视频”部分的html标签也很“整齐”,显然网站内容是服务器端的程序自动生成的,而非人工编辑生成的,这真是方便了用程序进行处理。
以下在python的交互环境下逐条输入执行
>>> import urllib
>>> file=urllib.urlopen('http://www.btv.com.cn/btvindex/xw/node_15140.htm')
>>> data=file.read()
>>> file.close()
>>> len(data)
31424
>>> import re
>>> r1=re.compile('<div class="tab_L">(.+?)<div class="tab_R">', re.DOTALL)
>>> m1=r1.search(data)
>>> m1
<_sre.SRE_Match object at 0xb7307360>
>>> data2=m1.group()
>>> r2=re.compile('<ul>(.+?)</ul>', re.DOTALL)
>>> m2=r2.findall(data2)
>>> len(m2)
40
>>> r3=re.compile('<a .*href="(.+?)".*>(.+?)</a>', re.DOTALL)
>>> m3=r3.search(m2[0])
>>> m3
<_sre.SRE_Match object at 0xb72e9c38>
>>> m3.groups()
('content/2012-04/01/content_4452273.htm', '\xe6\xb8\x85\xe6\x98\x8e\xe6\x97\xb6\xe8\x8a\x82 \xe5\xa4\x96\xe5\x9b\xbd\xe4\xbd\xbf\xe8\x8a\x82\xe4\xbd\x93\xe9\xaa\x8c\xe4\xb8\xad\xe5\x9b\xbd\xe6\x96\x87\xe5\x8c\x96')
>>> print m3.group(2)
到这里各目录项的url和名字弄出来了。看起来似乎我一下子就找到了那个正则表达式,不,是3个,每个逐步地缩小过滤的范围。
实际上上面的代码是在通过多次试错成功后整理出来的,期间要通过Web Developer或者Firebug之类的网页工具观察网页的结构(这两个都是firefox上的插件),确定出查找数据的步骤和方法。
3个正则表达式:
<div class="tab_L">(.+?)<div class="tab_R">
<li>(.+?)</li>
<a .*href="(.+?)".*>(.+?)</a>
第1个把要找的数据定位到某个<div>块内,第2个分出n条可做为目录的数据,第3个取出目录项的url和名称。嘿嘿,似乎有(.+?)这个东东就够用了,它能取出俩字符串之间的内容。
看来可以动手写脚本了,且慢,我们才分析了一个网页,怎么就知道这3个正则表达式适用于所有的网页呢?还要多看几个网页才行...
果然,发现有一类网页,上面的正则表达式不管用了,必须重新设计,比如这个就是,很快我们又设计了一组正则表达式去处理这个网页,等我们把整个网站看完了,有可能需要n组正则表达式。
为应对这种情况,设计插件脚本时,要考虑有个参数传递给脚本,告诉它用哪组正则表达式干活。程序的架构基本上是每一类网页设一个函数处理,有n类网页就有n个函数。
不多说了,放上我写好的“BTV在线”插件0.0.1版,装好就能用。
【五、进一步改进】
1、直接播放视频
listitem=xbmcgui.ListItem(queries['name'])
xbmc.Player().play(url, listitem)
这样可以少一级目录,用xbmc库中的函数操控xbmc播放器,如果视频有好几段,需要给播放器做一个playlist。
2、用urllib2库下载网页
req=urllib2.Request(url)
req.add_header('User-Agent', user_agent)
response=urllib2.urlopen(req)
data=response.read()
response.close()
把xbmc伪装成IE下载网页,还可以改Referer标头,如果网站有防盗链措施的话。
3、网页的解压缩、编码转换
import gzip, StringIO
data=gzip.GzipFile(StringIO.StringIO(data)).read()
有些网站的网页需要做这样的处理
4、给目录加上图标
listitem=xbmcgui.ListItem(name, thumbnailImage=thumbnail)
xbmcplugin.addDirectoryItem(handle, query, listitem, True, total_items)
如果网站上有好看的图片,为什么不把它加到目录上呢?
5、异常处理
try:
data=get_data(base)
except:
error(func+'(): get_data()')
给容易发生异常的语句加上try/except,避免出错时xbmc提示“脚本错误”。
6、分页
name='下一页'
match=re.search('.*<a .*href=(.+?)>'+name+'</a>', data)
if match:
listitem=xbmcgui.ListItem('<'+name+'>')
xbmcplugin.addDirectoryItem(handle, query, listitem, True)
增加一个“下一页”的目录项