PageRenderTime 55ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/plugin.video.iplayer/lib/iplayer2.py

http://xbmc-iplayerv2.googlecode.com/
Python | 1020 lines | 940 code | 51 blank | 29 comment | 68 complexity | e478e79c48fa601aaf4a6cb630628111 MD5 | raw file
  1. #!/usr/bin/python
  2. # Python libs
  3. import re, time, os, string, sys
  4. import urllib, urllib2
  5. import logging
  6. import xml.dom.minidom as dom
  7. import md5
  8. import traceback
  9. from pprint import pformat
  10. from socket import timeout as SocketTimeoutError
  11. # XBMC libs
  12. import xbmcgui
  13. # external libs
  14. import listparser
  15. import stations
  16. try:
  17. # python >= 2.5
  18. from xml.etree import ElementTree as ET
  19. except:
  20. # python 2.4 has to use the plugin's version of elementtree
  21. from elementtree import ElementTree as ET
  22. import httplib2
  23. import utils
  24. __scriptid__ = "plugin.video.iplayer"
  25. __addoninfo__ = utils.get_addoninfo(__scriptid__)
  26. __addon__ = __addoninfo__["addon"]
  27. sys.path.append(os.path.join(__addoninfo__["path"], 'lib', 'httplib2'))
  28. import socks
  29. #print "iplayer2 logging to stdout"
  30. logging.basicConfig(
  31. stream=sys.stdout,
  32. level=logging.DEBUG,
  33. format='iplayer2.py: %(levelname)4s %(message)s',)
  34. # me want 2.5!!!
  35. def any(iterable):
  36. for element in iterable:
  37. if element:
  38. return True
  39. return False
  40. # http://colinm.org/blog/on-demand-loading-of-flickr-photo-metadata
  41. # returns immediately for all previously-called functions
  42. def call_once(fn):
  43. called_by = {}
  44. def result(self):
  45. if self in called_by:
  46. return
  47. called_by[self] = True
  48. fn(self)
  49. return result
  50. # runs loader before decorated function
  51. def loaded_by(loader):
  52. def decorator(fn):
  53. def result(self, *args, **kwargs):
  54. loader(self)
  55. return fn(self, *args, **kwargs)
  56. return result
  57. return decorator
  58. rss_cache = {}
  59. self_closing_tags = ['alternate', 'mediator']
  60. re_selfclose = re.compile('<([a-zA-Z0-9]+)( ?.*)/>', re.M | re.S)
  61. def get_proxy():
  62. proxy_server = None
  63. proxy_type_id = 0
  64. proxy_port = 8080
  65. proxy_user = None
  66. proxy_pass = None
  67. try:
  68. proxy_server = __addon__.getSetting('proxy_server')
  69. proxy_type_id = __addon__.getSetting('proxy_type')
  70. proxy_port = int(__addon__.getSetting('proxy_port'))
  71. proxy_user = __addon__.getSetting('proxy_user')
  72. proxy_pass = __addon__.getSetting('proxy_pass')
  73. except:
  74. pass
  75. if proxy_type_id == '0': proxy_type = socks.PROXY_TYPE_HTTP_NO_TUNNEL
  76. elif proxy_type_id == '1': proxy_type = socks.PROXY_TYPE_HTTP
  77. elif proxy_type_id == '2': proxy_type = socks.PROXY_TYPE_SOCKS4
  78. elif proxy_type_id == '3': proxy_type = socks.PROXY_TYPE_SOCKS5
  79. proxy_dns = True
  80. return (proxy_type, proxy_server, proxy_port, proxy_dns, proxy_user, proxy_pass)
  81. def get_httplib():
  82. http = None
  83. try:
  84. if __addon__.getSetting('proxy_use') == 'true':
  85. (proxy_type, proxy_server, proxy_port, proxy_dns, proxy_user, proxy_pass) = get_proxy()
  86. logging.info("Using proxy: type %i rdns: %i server: %s port: %s user: %s pass: %s", proxy_type, proxy_dns, proxy_server, proxy_port, "***", "***")
  87. http = httplib2.Http(proxy_info = httplib2.ProxyInfo(proxy_type, proxy_server, proxy_port, proxy_dns, proxy_user, proxy_pass))
  88. else:
  89. http = httplib2.Http()
  90. except:
  91. raise
  92. logging.error('Failed to initialize httplib2 module')
  93. return http
  94. http = get_httplib()
  95. def fix_selfclosing(xml):
  96. return re_selfclose.sub('<\\1\\2></\\1>', xml)
  97. def set_http_cache(dir):
  98. try:
  99. cache = httplib2.FileCache(dir, safe=lambda x: md5.new(x).hexdigest())
  100. http.cache = cache
  101. except:
  102. pass
  103. class NoItemsError(Exception):
  104. def __init__(self, reason=None):
  105. self.reason = reason
  106. def __str__(self):
  107. reason = self.reason or '<no reason given>'
  108. return "Programme unavailable ('%s')" % (reason)
  109. class memoize(object):
  110. def __init__(self, func):
  111. self.func = func
  112. self._cache = {}
  113. def __call__(self, *args, **kwds):
  114. key = args
  115. if kwds:
  116. items = kwds.items()
  117. items.sort()
  118. key = key + tuple(items)
  119. if key in self._cache:
  120. return self._cache[key]
  121. self._cache[key] = result = self.func(*args, **kwds)
  122. return result
  123. def httpretrieve(url, filename):
  124. data = httpget(url)
  125. f = open(filename, 'wb')
  126. f.write(data)
  127. f.close()
  128. def httpget(url):
  129. resp = ''
  130. data = ''
  131. try:
  132. start_time = time.clock()
  133. if http:
  134. resp, data = http.request(url, 'GET')
  135. else:
  136. raise
  137. sec = time.clock() - start_time
  138. logging.info('URL Fetch took %2.2f sec for %s', sec, url)
  139. return data
  140. except:
  141. traceback.print_exc(file=sys.stdout)
  142. # disabling this for now - want to know if it is still needed with current xbmc
  143. #try:
  144. # # fallback to urllib to avoid a bug in httplib which often
  145. # # occurs during searches
  146. # f = urllib.urlopen(url)
  147. # data = f.read()
  148. # f.close()
  149. # return data
  150. #except:
  151. dialog = xbmcgui.Dialog()
  152. dialog.ok('Network Error', 'Failed to fetch URL', url)
  153. logging.error( 'Network Error. Failed to fetch URL %s' % url )
  154. return data
  155. # ElementTree addes {namespace} as a prefix for each element tag
  156. # This function removes these prefixes
  157. def xml_strip_namespace(tree):
  158. for elem in tree.getiterator():
  159. elem.tag = elem.tag.split('}')[1]
  160. def parse_entry_id(entry_id):
  161. # tag:bbc.co.uk,2008:PIPS:b00808sc
  162. r = re.compile('PIPS:([0-9a-z]{8})')
  163. matches = r.findall(entry_id)
  164. if not matches: return None
  165. return matches[0]
  166. def get_provider():
  167. provider = ""
  168. try:
  169. provider_id = __addon__.getSetting('provider')
  170. except:
  171. pass
  172. if provider_id == '1': provider = 'akamai'
  173. elif provider_id == '2': provider = 'limelight'
  174. elif provider_id == '3': provider = 'level3'
  175. return provider
  176. def get_protocol():
  177. protocol = "rtmp"
  178. try:
  179. protocol_id = __addon__.getSetting('protocol')
  180. except:
  181. pass
  182. if protocol_id == '1': protocol = 'rtmpt'
  183. return protocol
  184. def get_port():
  185. port = 1935
  186. protocol = get_protocol()
  187. if protocol == 'rtmpt': port = 80
  188. return port
  189. def get_thumb_dir():
  190. thumb_dir = os.path.join(__addoninfo__['path'], 'resources', 'media')
  191. if utils.get_os() == "xbox":
  192. thumb_dir = os.path.join(thumb_dir, 'xbox')
  193. return thumb_dir
  194. class media(object):
  195. def __init__(self, item, media_node):
  196. self.item = item
  197. self.href = None
  198. self.kind = None
  199. self.method = None
  200. self.width, self.height = None, None
  201. self.bitrate = None
  202. self.read_media_node(media_node)
  203. @property
  204. def url(self):
  205. # no longer used. will remove later
  206. if self.connection_method == 'resolve':
  207. logging.info("Resolving URL %s", self.connection_href)
  208. page = urllib2.urlopen(self.connection_href)
  209. page.close()
  210. url = page.geturl()
  211. logging.info("URL resolved to %s", url)
  212. return page.geturl()
  213. else:
  214. return self.connection_href
  215. @property
  216. def application(self):
  217. """
  218. The type of stream represented as a string.
  219. i.e. 'captions', 'flashhd', 'flashhigh', 'flashmed', 'flashwii', 'mobile', 'mp3', 'real', 'aac'
  220. """
  221. tep = {}
  222. tep['captions', 'application/ttaf+xml', None, 'http', None] = 'captions'
  223. tep['video', 'video/mp4', 'h264', 'rtmp', 3200] = 'h264 3200'
  224. tep['video', 'video/mp4', 'h264', 'rtmp', 1500] = 'h264 1500'
  225. tep['video', 'video/mp4', 'h264', 'rtmp', 796] = 'h264 800'
  226. tep['video', 'video/mp4', 'h264', 'rtmp', 480] = 'h264 480'
  227. tep['video', 'video/mp4', 'h264', 'rtmp', 396] = 'h264 400'
  228. tep['video', 'video/x-flv', 'vp6', 'rtmp', 512] = 'flashmed'
  229. tep['video', 'video/x-flv', 'spark', 'rtmp', 800] = 'flashwii'
  230. tep['video', 'video/mpeg', 'h264', 'http', 184] = 'mobile'
  231. tep['audio', 'audio/mpeg', 'mp3', 'rtmp', None] = 'mp3'
  232. tep['audio', 'audio/mp4', 'aac', 'rtmp', None] = 'aac'
  233. tep['audio', 'audio/wma', 'wma', 'http', None] = 'wma'
  234. tep['video', 'video/mp4', 'h264', 'http', 516] = 'iphonemp3'
  235. me = (self.kind, self.mimetype, self.encoding, self.connection_protocol, self.bitrate)
  236. return tep.get(me, None)
  237. def read_media_node(self, media):
  238. """
  239. Reads media info from a media XML node
  240. media: media node from BeautifulStoneSoup
  241. """
  242. self.kind = media.get('kind')
  243. self.mimetype = media.get('type')
  244. self.encoding = media.get('encoding')
  245. self.width, self.height = media.get('width'), media.get('height')
  246. self.live = media.get('live') == 'true'
  247. self.service = media.get('service')
  248. try:
  249. self.bitrate = int(media.get('bitrate'))
  250. except:
  251. if media.get('bitrate') != None:
  252. logging.info("bitrate = " + '"' + media.get('bitrate') + '"')
  253. self.bitrate = None
  254. self.connection_kind = None
  255. self.connection_live = None
  256. self.connection_protocol = None
  257. self.connection_href = None
  258. self.connection_method = None
  259. # try to find a stream from users preference
  260. conn = None
  261. provider = get_provider()
  262. if provider != "":
  263. for c in media.findall('connection'):
  264. if c.get('kind') == provider:
  265. conn = c
  266. break
  267. if conn == None:
  268. conn = media.find('connection')
  269. if conn == None:
  270. return
  271. self.connection_kind = conn.get('kind')
  272. self.connection_protocol = conn.get('protocol')
  273. if self.mimetype[:5] == 'audio':
  274. self.kind = 'audio'
  275. self.bitrate = None
  276. # some akamai rtmp streams (radio) don't specify rtmp protocol
  277. if self.connection_protocol == None and self.connection_kind == 'akamai':
  278. self.connection_protocol = 'rtmp'
  279. if self.connection_kind in ['http', 'sis']:
  280. self.connection_href = conn.get('href')
  281. self.connection_protocol = 'http'
  282. if self.kind == 'captions':
  283. self.connection_method = None
  284. elif self.connection_protocol == 'rtmp':
  285. server = conn.get('server')
  286. identifier = conn.get('identifier')
  287. auth = conn.get('authString')
  288. application = conn.get('application')
  289. # sometimes we don't get a rtmp application for akamai
  290. if application == None and self.connection_kind == 'akamai':
  291. application = "ondemand"
  292. timeout = __addon__.getSetting('stream_timeout')
  293. swfplayer = 'http://www.bbc.co.uk/emp/10player.swf'
  294. params = dict(protocol = get_protocol(), port = get_port(), server = server, auth = auth, ident = identifier, app = application)
  295. if self.connection_kind == 'limelight':
  296. # note that librtmp has a small issue with constructing the tcurl here. we construct it ourselves for now (fixed in later librtmp)
  297. self.connection_href = "%(protocol)s://%(server)s:%(port)s/ app=%(app)s?%(auth)s tcurl=%(protocol)s://%(server)s:%(port)s/%(app)s?%(auth)s playpath=%(ident)s" % params
  298. else:
  299. self.connection_href = "%(protocol)s://%(server)s:%(port)s/%(app)s?%(auth)s playpath=%(ident)s" % params
  300. self.connection_href += " swfurl=%s swfvfy=true timeout=%s" % (swfplayer, timeout)
  301. else:
  302. logging.error("connectionkind %s unknown", self.connection_kind)
  303. if self.connection_protocol and __addon__.getSetting('enhanceddebug') == 'true':
  304. logging.info("protocol: %s - kind: %s - type: %s - encoding: %s, - bitrate: %s" %
  305. (self.connection_protocol, self.connection_kind, self.mimetype, self.encoding, self.bitrate))
  306. logging.info("conn href: %s", self.connection_href)
  307. @property
  308. def programme(self):
  309. return self.item.programme
  310. class item(object):
  311. """
  312. Represents an iPlayer programme item. Most programmes consist of 2 such items,
  313. (1) the ident, and (2) the actual programme. The item specifies the properties
  314. of the media available, such as whether it's a radio/TV programme, if it's live,
  315. signed, etc.
  316. """
  317. def __init__(self, programme, item_node):
  318. """
  319. programme: a programme object that represents the 'parent' of this item.
  320. item_node: an XML &lt;item&gt; node representing this item.
  321. """
  322. self.programme = programme
  323. self.identifier = None
  324. self.service = None
  325. self.guidance = None
  326. self.masterbrand = None
  327. self.alternate = None
  328. self.duration = ''
  329. self.medias = None
  330. self.read_item_node(item_node)
  331. def read_item_node(self, node):
  332. """
  333. Reads the specified XML &lt;item&gt; node and sets this instance's
  334. properties.
  335. """
  336. self.kind = node.get('kind')
  337. self.identifier = node.get('identifier')
  338. logging.info('Found item: %s, %s', self.kind, self.identifier)
  339. if self.kind in ['programme', 'radioProgramme']:
  340. self.live = node.get('live') == 'true'
  341. #self.title = node.get('title')
  342. self.group = node.get('group')
  343. self.duration = node.get('duration')
  344. #self.broadcast = node.broadcast
  345. nf = node.find('service')
  346. if nf: self.service = nf.text and nf.get('id')
  347. nf = node.find('masterbrand')
  348. if nf: self.masterbrand = nf.text and nf.get('id')
  349. nf = node.find('alternate')
  350. if nf: self.alternate = nf.text and nf.get('id')
  351. nf = node.find('guidance')
  352. if nf: self.guidance = nf.text
  353. @property
  354. def is_radio(self):
  355. """ True if this stream is a radio programme. """
  356. return self.kind == 'radioProgramme'
  357. @property
  358. def is_tv(self):
  359. """ True if this stream is a TV programme. """
  360. return self.kind == 'programme'
  361. @property
  362. def is_ident(self):
  363. """ True if this stream is an ident. """
  364. return self.kind == 'ident'
  365. @property
  366. def is_programme(self):
  367. """ True if this stream is a programme (TV or Radio). """
  368. return self.is_radio or self.is_tv
  369. @property
  370. def is_live(self):
  371. """ True if this stream is being broadcast live. """
  372. return self.live
  373. @property
  374. def is_signed(self):
  375. """ True if this stream is 'signed' for the hard-of-hearing. """
  376. return self.alternate == 'signed'
  377. def mediaselector_url(self, suffix):
  378. if suffix == None:
  379. return "http://www.bbc.co.uk/mediaselector/4/mtis/stream/%s" % self.identifier
  380. return "http://www.bbc.co.uk/mediaselector/4/mtis/stream/%s/%s" % (self.identifier, suffix)
  381. def medialist(self, suffix = None):
  382. """
  383. Returns a list of all the media available for this item.
  384. """
  385. if self.medias: return self.medias
  386. url = self.mediaselector_url(suffix)
  387. logging.info("Stream XML URL: %s", url)
  388. xml = httpget(url)
  389. tree = ET.XML(xml)
  390. xml_strip_namespace(tree)
  391. medias = []
  392. for m in tree.findall('media'):
  393. medias.append(media(self, m))
  394. return medias
  395. @property
  396. def media(self):
  397. """
  398. Returns a list of all the media available for this item.
  399. """
  400. if self.medias: return self.medias
  401. medias = []
  402. # this was needed before due to authentication changes (the auth from the main xml didnt work so you had to request specific xmls for each quality)
  403. #for m in ['iplayer_streaming_h264_flv_hd', 'iplayer_streaming_h264_flv_high', 'iplayer_streaming_h264_flv', 'iplayer_streaming_h264_flv_lo']:
  404. # medias.extend(self.medialist(m))
  405. medias.extend(self.medialist())
  406. self.medias = medias
  407. if medias == None or len(medias) == 0:
  408. d = xbmcgui.Dialog()
  409. d.ok('Error fetching media info', 'Please check network access to IPlayer by playing iplayer content via a web browser')
  410. return
  411. return medias
  412. def get_media_for(self, application):
  413. """
  414. Returns a media object for the given application type.
  415. """
  416. medias = [m for m in self.media if m.application == application]
  417. if not medias:
  418. return None
  419. return medias[0]
  420. def get_medias_for(self, applications):
  421. """
  422. Returns a dictionary of media objects for the given application types.
  423. """
  424. medias = [m for m in self.media if m.application in applications]
  425. d = {}.fromkeys(applications)
  426. for m in medias:
  427. d[m.application] = m
  428. return d
  429. class programme(object):
  430. """
  431. Represents an individual iPlayer programme, as identified by an 8-letter PID,
  432. and contains the programme title, subtitle, broadcast time and list of playlist
  433. items (e.g. ident and then the actual programme.)
  434. """
  435. def __init__(self, pid):
  436. self.pid = pid
  437. self.meta = {}
  438. self._items = []
  439. self._related = []
  440. @call_once
  441. def read_playlist(self):
  442. logging.info('Read playlist for %s...', self.pid)
  443. self.parse_playlist(self.playlist)
  444. def get_playlist_xml(self):
  445. """ Downloads and returns the XML for a PID from the iPlayer site. """
  446. try:
  447. url = self.playlist_url
  448. xml = httpget(url)
  449. return xml
  450. except SocketTimeoutError:
  451. logging.error("Timed out trying to download programme XML")
  452. raise
  453. def parse_playlist(self, xmlstr):
  454. #logging.info('Parsing playlist XML... %s', xml)
  455. #xml.replace('<summary/>', '<summary></summary>')
  456. #xml = fix_selfclosing(xml)
  457. #soup = BeautifulStoneSoup(xml, selfClosingTags=self_closing_tags)
  458. tree = ET.XML(xmlstr)
  459. xml_strip_namespace(tree)
  460. self.meta = {}
  461. self._items = []
  462. self._related = []
  463. logging.info('Found programme: %s', tree.find('title').text)
  464. self.meta['title'] = tree.find('title').text
  465. self.meta['summary'] = string.lstrip(tree.find('summary').text, ' ')
  466. self.meta['updated'] = tree.find('updated').text
  467. if tree.find('noitems'):
  468. logging.info('No playlist items: %s', tree.find('noitems').get('reason'))
  469. self.meta['reason'] = tree.find('noitems').get('reason')
  470. self._items = [item(self, i) for i in tree.findall('item')]
  471. rId = re.compile('concept_pid:([a-z0-9]{8})')
  472. for link in tree.findall('relatedlink'):
  473. i = {}
  474. i['title'] = link.find('title').text
  475. #i['summary'] = item.summary # FIXME looks like a bug in BSS
  476. i['pid'] = (rId.findall(link.find('id').text) or [None])[0]
  477. i['programme'] = programme(i['pid'])
  478. self._related.append(i)
  479. def get_thumbnail(self, size='large', tvradio='tv'):
  480. """
  481. Returns the URL of a thumbnail.
  482. size: '640x360'/'biggest'/'largest' or '512x288'/'big'/'large' or None
  483. """
  484. if size in ['640x360', '640x', 'x360', 'biggest', 'largest']:
  485. return "http://www.bbc.co.uk/iplayer/images/episode/%s_640_360.jpg" % (self.pid)
  486. elif size in ['512x288', '512x', 'x288', 'big', 'large']:
  487. return "http://www.bbc.co.uk/iplayer/images/episode/%s_512_288.jpg" % (self.pid)
  488. elif size in ['178x100', '178x', 'x100', 'small']:
  489. return "http://www.bbc.co.uk/iplayer/images/episode/%s_178_100.jpg" % (self.pid)
  490. elif size in ['150x84', '150x', 'x84', 'smallest']:
  491. return "http://www.bbc.co.uk/iplayer/images/episode/%s_150_84.jpg" % (self.pid)
  492. else:
  493. return os.path.join(get_thumb_dir(), '%s.png' % tvradio)
  494. def get_url(self):
  495. """
  496. Returns the programmes episode page.
  497. """
  498. return "http://www.bbc.co.uk/iplayer/episode/%s" % (self.pid)
  499. @property
  500. def playlist_url(self):
  501. return "http://www.bbc.co.uk/iplayer/playlist/%s" % self.pid
  502. @property
  503. def playlist(self):
  504. return self.get_playlist_xml()
  505. def get_updated(self):
  506. return self.meta['updated']
  507. @loaded_by(read_playlist)
  508. def get_title(self):
  509. return self.meta['title']
  510. @loaded_by(read_playlist)
  511. def get_summary(self):
  512. return self.meta['summary']
  513. @loaded_by(read_playlist)
  514. def get_related(self):
  515. return self._related
  516. @loaded_by(read_playlist)
  517. def get_items(self):
  518. if not self._items:
  519. raise NoItemsError(self.meta['reason'])
  520. return self._items
  521. @property
  522. def programme(self):
  523. for i in self.items:
  524. if i.is_programme:
  525. return i
  526. return None
  527. title = property(get_title)
  528. summary = property(get_summary)
  529. updated = property(get_updated)
  530. thumbnail = property(get_thumbnail)
  531. related = property(get_related)
  532. items = property(get_items)
  533. #programme = memoize(programme)
  534. class programme_simple(object):
  535. """
  536. Represents an individual iPlayer programme, as identified by an 8-letter PID,
  537. and contains the programme pid, title, subtitle etc
  538. """
  539. def __init__(self, pid, entry):
  540. self.pid = pid
  541. self.meta = {}
  542. self.meta['title'] = entry.title
  543. self.meta['summary'] = string.lstrip(entry.summary, ' ')
  544. self.meta['updated'] = entry.updated
  545. self.categories = []
  546. for c in entry.categories:
  547. #if c != 'TV':
  548. self.categories.append(c.rstrip())
  549. self._items = []
  550. self._related = []
  551. @call_once
  552. def read_playlist(self):
  553. pass
  554. def get_playlist_xml(self):
  555. pass
  556. def parse_playlist(self, xml):
  557. pass
  558. def get_thumbnail(self, size='large', tvradio='tv'):
  559. """
  560. Returns the URL of a thumbnail.
  561. size: '640x360'/'biggest'/'largest' or '512x288'/'big'/'large' or None
  562. """
  563. if size in ['640x360', '640x', 'x360', 'biggest', 'largest']:
  564. return "http://www.bbc.co.uk/iplayer/images/episode/%s_640_360.jpg" % (self.pid)
  565. elif size in ['512x288', '512x', 'x288', 'big', 'large']:
  566. return "http://www.bbc.co.uk/iplayer/images/episode/%s_512_288.jpg" % (self.pid)
  567. elif size in ['178x100', '178x', 'x100', 'small']:
  568. return "http://www.bbc.co.uk/iplayer/images/episode/%s_178_100.jpg" % (self.pid)
  569. elif size in ['150x84', '150x', 'x84', 'smallest']:
  570. return "http://www.bbc.co.uk/iplayer/images/episode/%s_150_84.jpg" % (self.pid)
  571. else:
  572. return os.path.join(get_thumb_dir(), '%s.png' % tvradio)
  573. def get_url(self):
  574. """
  575. Returns the programmes episode page.
  576. """
  577. return "http://www.bbc.co.uk/iplayer/episode/%s" % (self.pid)
  578. @property
  579. def playlist_url(self):
  580. return "http://www.bbc.co.uk/iplayer/playlist/%s" % self.pid
  581. @property
  582. def playlist(self):
  583. return self.get_playlist_xml()
  584. def get_updated(self):
  585. return self.meta['updated']
  586. @loaded_by(read_playlist)
  587. def get_title(self):
  588. return self.meta['title']
  589. @loaded_by(read_playlist)
  590. def get_summary(self):
  591. return self.meta['summary']
  592. @loaded_by(read_playlist)
  593. def get_related(self):
  594. return self._related
  595. @loaded_by(read_playlist)
  596. def get_items(self):
  597. if not self._items:
  598. raise NoItemsError(self.meta['reason'])
  599. return self._items
  600. @property
  601. def programme(self):
  602. for i in self.items:
  603. if i.is_programme:
  604. return i
  605. return None
  606. title = property(get_title)
  607. summary = property(get_summary)
  608. updated = property(get_updated)
  609. thumbnail = property(get_thumbnail)
  610. related = property(get_related)
  611. items = property(get_items)
  612. class feed(object):
  613. def __init__(self, tvradio=None, channel=None, category=None, searchcategory=None, atoz=None, searchterm=None, radio=None):
  614. """
  615. Creates a feed for the specified channel/category/whatever.
  616. tvradio: type of channel - 'tv' or 'radio'. If a known channel is specified, use 'auto'.
  617. channel: name of channel, e.g. 'bbc_one'
  618. category: category name, e.g. 'drama'
  619. subcategory: subcategory name, e.g. 'period_drama'
  620. atoz: A-Z listing for the specified letter
  621. """
  622. if tvradio == 'auto':
  623. if not channel and not searchterm:
  624. raise Exception, "Must specify channel or searchterm when using 'auto'"
  625. elif channel in stations.channels_tv:
  626. self.tvradio = 'tv'
  627. elif channel in stations.channels_radio:
  628. self.tvradio = 'radio'
  629. else:
  630. raise Exception, "TV channel '%s' not recognised." % self.channel
  631. elif tvradio in ['tv', 'radio']:
  632. self.tvradio = tvradio
  633. else:
  634. self.tvradio = None
  635. self.channel = channel
  636. self.category = category
  637. self.searchcategory = searchcategory
  638. self.atoz = atoz
  639. self.searchterm = searchterm
  640. self.radio = radio
  641. def create_url(self, listing):
  642. """
  643. <channel>/['list'|'popular'|'highlights']
  644. 'categories'/<category>(/<subcategory>)(/['tv'/'radio'])/['list'|'popular'|'highlights']
  645. """
  646. assert listing in ['list', 'popular', 'highlights'], "Unknown listing type"
  647. if self.searchcategory:
  648. path = ['categories']
  649. if self.category:
  650. path += [self.category]
  651. if self.tvradio:
  652. path += [self.tvradio]
  653. path += ['list']
  654. elif self.category:
  655. if self.channel:
  656. path = [self.channel, 'categories', self.category]
  657. else:
  658. path = ['categories', self.category, self.tvradio]
  659. path += ['list']
  660. elif self.searchterm:
  661. path = ['search']
  662. if self.tvradio:
  663. path += [self.tvradio]
  664. path += ['?q=%s' % self.searchterm]
  665. elif self.channel:
  666. path = [self.channel]
  667. if self.atoz:
  668. path += ['atoz', self.atoz]
  669. path += [listing]
  670. elif self.atoz:
  671. path = ['atoz', self.atoz, listing]
  672. if self.tvradio:
  673. path += [self.tvradio]
  674. else:
  675. assert listing != 'list', "Can't list at tv/radio level'"
  676. path = [listing, self.tvradio]
  677. return "http://feeds.bbc.co.uk/iplayer/" + '/'.join(path)
  678. def get_name(self, separator=' '):
  679. """
  680. A readable title for this feed, e.g. 'BBC One' or 'TV Drama' or 'BBC One Drama'
  681. separator: string to separate name parts with, defaults to ' '. Use None to return a list (e.g. ['TV', 'Drama']).
  682. """
  683. path = []
  684. # if got a channel, don't need tv/radio distinction
  685. if self.channel:
  686. assert self.channel in stations.channels_tv or self.channel in stations.channels_radio, 'Unknown channel'
  687. #print self.tvradio
  688. if self.tvradio == 'tv':
  689. path.append(stations.channels_tv.get(self.channel, '(TV)'))
  690. else:
  691. path.append(stations.channels_radio.get(self.channel, '(Radio)'))
  692. elif self.tvradio:
  693. # no channel
  694. medium = 'TV'
  695. if self.tvradio == 'radio': medium = 'Radio'
  696. path.append(medium)
  697. if self.searchterm:
  698. path += ['Search results for %s' % self.searchterm]
  699. if self.searchcategory:
  700. if self.category:
  701. path += ['Category %s' % self.category]
  702. else:
  703. path += ['Categories']
  704. if self.atoz:
  705. path.append("beginning with %s" % self.atoz.upper())
  706. if separator != None:
  707. return separator.join(path)
  708. else:
  709. return path
  710. def channels(self):
  711. """
  712. Return a list of available channels.
  713. """
  714. if self.channel: return None
  715. if self.tvradio == 'tv': return stations.channels_tv_list
  716. if self.tvradio == 'radio':
  717. if radio:
  718. return channels_radio_type_list[radio]
  719. else:
  720. return stations.channels_radio_list
  721. return None
  722. def channels_feed(self):
  723. """
  724. Return a list of available channels as a list of feeds.
  725. """
  726. if self.channel:
  727. logging.warning("%s doesn\'t have any channels!", self.channel)
  728. return None
  729. if self.tvradio == 'tv':
  730. return [feed('tv', channel=ch) for (ch, title) in stations.channels_tv_list]
  731. if self.tvradio == 'radio':
  732. if self.radio:
  733. return [feed('radio', channel=ch) for (ch, title) in stations.channels_radio_type_list[self.radio]]
  734. else:
  735. return [feed('radio', channel=ch) for (ch, title) in stations.channels_radio_list]
  736. return None
  737. def subcategories(self):
  738. raise NotImplementedError('Sub-categories not yet supported')
  739. @classmethod
  740. def is_atoz(self, letter):
  741. """
  742. Return False if specified letter is not a valid 'A to Z' directory entry.
  743. Otherwise returns the directory name.
  744. >>> feed.is_atoz('a'), feed.is_atoz('z')
  745. ('a', 'z')
  746. >>> feed.is_atoz('0'), feed.is_atoz('9')
  747. ('0-9', '0-9')
  748. >>> feed.is_atoz('123'), feed.is_atoz('abc')
  749. (False, False)
  750. >>> feed.is_atoz('big british castle'), feed.is_atoz('')
  751. (False, False)
  752. """
  753. l = letter.lower()
  754. if len(l) != 1 and l != '0-9':
  755. return False
  756. if l in '0123456789': l = "0-9"
  757. if l not in 'abcdefghijklmnopqrstuvwxyz0-9':
  758. return False
  759. return l
  760. def sub(self, *args, **kwargs):
  761. """
  762. Clones this feed, altering the specified parameters.
  763. >>> feed('tv').sub(channel='bbc_one').channel
  764. 'bbc_one'
  765. >>> feed('tv', channel='bbc_one').sub(channel='bbc_two').channel
  766. 'bbc_two'
  767. >>> feed('tv', channel='bbc_one').sub(category='drama').category
  768. 'drama'
  769. >>> feed('tv', channel='bbc_one').sub(channel=None).channel
  770. >>>
  771. """
  772. d = self.__dict__.copy()
  773. d.update(kwargs)
  774. return feed(**d)
  775. def get(self, subfeed):
  776. """
  777. Returns a child/subfeed of this feed.
  778. child: can be channel/cat/subcat/letter, e.g. 'bbc_one'
  779. """
  780. if self.channel and subfeed in categories:
  781. # no children: channel feeds don't support categories
  782. return None
  783. elif self.category:
  784. # no children: TODO support subcategories
  785. return None
  786. elif subfeed in categories:
  787. return self.sub(category=subfeed)
  788. elif self.is_atoz(subfeed):
  789. return self.sub(atoz=self.is_atoz(subfeed))
  790. else:
  791. if subfeed in stations.channels_tv: return feed('tv', channel=subfeed)
  792. if subfeed in stations.channels_radiot: return feed('radio', channel=subfeed)
  793. # TODO handle properly oh pants
  794. return None
  795. @classmethod
  796. def read_rss(self, url):
  797. logging.info('Read RSS: %s', url)
  798. if url not in rss_cache:
  799. logging.info('Feed URL not in cache, requesting...')
  800. xml = httpget(url)
  801. progs = listparser.parse(xml)
  802. if not progs: return []
  803. d = []
  804. for entry in progs.entries:
  805. pid = parse_entry_id(entry.id)
  806. p = programme_simple(pid, entry)
  807. d.append(p)
  808. logging.info('Found %d entries', len(d))
  809. rss_cache[url] = d
  810. else:
  811. logging.info('RSS found in cache')
  812. return rss_cache[url]
  813. def popular(self):
  814. return self.read_rss(self.create_url('popular'))
  815. def highlights(self):
  816. return self.read_rss(self.create_url('highlights'))
  817. def list(self):
  818. return self.read_rss(self.create_url('list'))
  819. def categories(self):
  820. # quick and dirty category extraction and count
  821. url = self.create_url('list')
  822. xml = httpget(url)
  823. categories = []
  824. doc = dom.parseString(xml)
  825. root = doc.documentElement
  826. for entry in root.getElementsByTagName( "entry" ):
  827. summary = entry.getElementsByTagName( "summary" )[0].firstChild.nodeValue
  828. title = re.sub('programmes currently available from BBC iPlayer', '', summary, 1)
  829. url = None
  830. # search for the url for this entry
  831. for link in entry.getElementsByTagName( "link" ):
  832. if link.hasAttribute( "rel" ):
  833. rel = link.getAttribute( "rel" )
  834. if rel == 'self':
  835. url = link.getAttribute( "href" )
  836. #break
  837. if url:
  838. category = re.findall( "iplayer/categories/(.*?)/list", url, re.DOTALL )[0]
  839. categories.append([title, category])
  840. return categories
  841. @property
  842. def is_radio(self):
  843. """ True if this feed is for radio. """
  844. return self.tvradio == 'radio'
  845. @property
  846. def is_tv(self):
  847. """ True if this feed is for tv. """
  848. return self.tvradio == 'tv'
  849. name = property(get_name)
  850. tv = feed('tv')
  851. radio = feed('radio')
  852. def test():
  853. tv = feed('tv')
  854. print tv.popular()
  855. print tv.channels()
  856. print tv.get('bbc_one')
  857. print tv.get('bbc_one').list()
  858. for c in tv.get('bbc_one').categories():
  859. print c
  860. #print tv.get('bbc_one').channels()
  861. #print tv.categories()
  862. #print tv.get('drama').list()
  863. #print tv.get('drama').get_subcategory('period').list()
  864. if __name__ == '__main__':
  865. test()