14 __title__ =
"bbciplayer_api - Simple-to-use Python interface to the BBC iPlayer RSS feeds (http://www.bbc.co.uk)"
15 __author__=
"R.D. Vaughan"
17 This python script is intended to perform a variety of utility functions to search and access text
18 meta data, video and image URLs from the BBC iPlayer Web site. These routines process RSS feeds
19 provided by BBC (http://www.bbc.co.uk). The specific BBC iPlayer RSS feeds that are processed are controled through a user XML preference file usually found at
20 "~/.mythtv/MythNetvision/userGrabberPrefs/bbciplayer.xml"
32 import os, struct, sys, re, time, datetime, shutil, urllib.request, urllib.parse, urllib.error, re
34 from socket
import gethostname, gethostbyname
35 from threading
import Thread
36 from copy
import deepcopy
37 from operator
import itemgetter, attrgetter
38 from MythTV
import MythXML
39 from .bbciplayer_exceptions
import (BBCUrlError, BBCHttpError, BBCRssError, BBCVideoNotFound, BBCConfigFileError, BBCUrlDownloadError)
43 """Wraps a stream with an encoder"""
52 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
53 if isinstance(obj, str):
55 self.
out.buffer.write(obj)
58 """Delegate everything but write to the stream"""
59 return getattr(self.
out, attr)
61 if isinstance(sys.stdout, io.TextIOWrapper):
67 from io
import StringIO
68 from lxml
import etree
69 except Exception
as e:
70 sys.stderr.write(
'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
78 for digit
in etree.LIBXML_VERSION:
79 version+=str(digit)+
'.'
80 version = version[:-1]
83 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
84 At least "libxml" version 2.7.2 must be installed. Your version is (%s).
90 """Main interface to http://www.bbciplayer.com/
91 This is done to support a common naming framework for all python Netvision plugins no matter their site
94 Supports search methods
95 The apikey is a not required to access http://www.bbciplayer.com/
101 select_first = False,
105 search_all_languages = False,
107 """apikey (str/unicode):
108 Specify the target site API key. Applications need their own key in some cases
111 When True, the returned meta data is being returned has the key and values massaged to match MythTV
112 When False, the returned meta data is being returned matches what target site returned
114 interactive (True/False): (This option is not supported by all target site apis)
115 When True, uses built-in console UI is used to select the correct show.
116 When False, the first search result is used.
118 select_first (True/False): (This option is not supported currently implemented in any grabbers)
119 Automatically selects the first series search result (rather
120 than showing the user a list of more than one series).
121 Is overridden by interactive = False, or specifying a custom_ui
124 shows verbose debugging information
126 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
127 A callable subclass of interactive class (overrides interactive option)
129 language (2 character language abbreviation): (This option is not supported by all target site apis)
130 The language of the returned data. Is also the language search
131 uses. Default is "en" (English). For full list, run..
133 search_all_languages (True/False): (This option is not supported by all target site apis)
134 By default, a Netvision grabber will only search in the language specified using
135 the language option. When this is True, it will search for the
142 if apikey
is not None:
143 self.
config[
'apikey'] = apikey
147 self.
config[
'debug_enabled'] = debug
151 self.
log_name =
'BBCiPlayer_Grabber'
155 self.
config[
'custom_ui'] = custom_ui
159 self.
config[
'select_first'] = select_first
161 self.
config[
'search_all_languages'] = search_all_languages
163 self.
error_messages = {
'BBCUrlError':
"! Error: The URL (%s) cause the exception error (%s)\n",
'BBCHttpError':
"! Error: An HTTP communications error with the BBC was raised (%s)\n",
'BBCRssError':
"! Error: Invalid RSS meta data\nwas received from the BBC error (%s). Skipping item.\n",
'BBCVideoNotFound':
"! Error: Video search with the BBC did not return any results (%s)\n",
'BBCConfigFileError':
"! Error: bbc_config.xml file missing\nit should be located in and named as (%s).\n",
'BBCUrlDownloadError':
"! Error: Downloading a RSS feed or Web page (%s).\n", }
166 self.
channel = {
'channel_title':
'BBC iPlayer',
'channel_link':
'http://www.bbc.co.uk',
'channel_description':
"BBC iPlayer is our service that lets you catch up with radio and television programmes from the past week.",
'channel_numresults': 0,
'channel_returned': 1,
'channel_startindex': 0}
170 etree.XPath(
'.//a[@class="episode-title title-link cta-video"]', namespaces=self.
common.namespaces),
171 etree.XPath(
'.//div[@class="feature video"]', namespaces=self.
common.namespaces),
172 etree.XPath(
'.//atm:category[@term="TV"]', namespaces=self.
common.namespaces),
178 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+).*.+?Episode\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
180 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+)\\ \\-\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
182 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+).*.+?Part\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
184 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+)\\:\\ Programme\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
186 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+).*$''', re.UNICODE),
188 re.compile(
r'''^.+?Episode\\ (?P<seasno>[0-9]+).*$''', re.UNICODE),
191 self.
channel_icon =
'%SHAREDIR%/mythnetvision/icons/bbciplayer.jpg'
193 self.
config[
'image_extentions'] = [
"png",
"jpg",
"bmp"]
203 ''' Read the MNV BBC iPlayer grabber "bbc_config.xml" configuration file
207 url =
'file://%s/nv_python_libs/configs/XML/bbc_config.xml' % (baseProcessingDir, )
208 if not os.path.isfile(url[7:]):
211 if self.
config[
'debug_enabled']:
216 except Exception
as e:
223 '''Read the bbciplayer_config.xml and user preference bbciplayer.xml file.
224 If the bbciplayer.xml file does not exist then copy the default.
232 if userPreferenceFile[0] ==
'~':
233 self.
bbciplayer_config.
find(
'userPreferenceFile').text =
"%s%s" % (os.path.expanduser(
"~"), userPreferenceFile[1:])
239 if not os.path.isdir(prefDir):
241 defaultConfig =
'%s/nv_python_libs/configs/XML/defaultUserPrefs/bbciplayer.xml' % (baseProcessingDir, )
246 if self.
config[
'debug_enabled']:
251 except Exception
as e:
257 '''Parse the item information (HTML or RSS/XML) to identify if the content is a video or
258 audio file. Set the contry code if a video is detected as it can only be played in the "UK"
259 return "uk" if a video type was detected.
260 return None if a video type was NOT detected.
264 if len(xpathP(item)):
272 ''' Check is there is any season or episode number information in an item's title
273 return array of season and/or episode numbers
274 return array with None values
282 s_e[0], s_e[1] = match.groups()
285 s_e[0] = match.groups()[0]
288 s_e[1] = match.groups()[0]
300 playerUrl = self.
mythxml.getInternetContentUrl(
"nv_python_libs/configs/HTML/bbciplayer.html", \
305 '''Key word video search of the BBC iPlayer web site
306 return an array of matching item elements
313 searchVar =
'/?q=%s&page=%s' % (urllib.parse.quote(title.encode(
"utf-8")), pagenumber)
314 except UnicodeDecodeError:
315 searchVar =
'/?q=%s&page=%s' % (urllib.parse.quote(title), pagenumber)
319 if self.
config[
'debug_enabled']:
328 except Exception
as errormsg:
336 if resultTree
is None:
337 raise BBCVideoNotFound(
"No BBC Video matches found for search value (%s)" % title)
339 searchResults = resultTree.xpath(
'//result//li')
340 if not len(searchResults):
341 raise BBCVideoNotFound(
"No BBC Video matches found for search value (%s)" % title)
345 pubDate = datetime.datetime.now().strftime(self.
common.pubDateFormat)
351 urlType =
'fullscreen'
354 audioFilter = etree.XPath(
'contains(./@class,"audio") or contains(./../../@class,"audio")')
355 linkFilter = etree.XPath(
".//div[@class='episode-info ']//a")
356 titleFilter = etree.XPath(
".//div[@class='episode-info ']//a")
357 descFilter = etree.XPath(
".//div[@class='episode-info ']//p[@class='episode-synopsis']")
358 thumbnailFilter = etree.XPath(
".//span[@class='episode-image cta-play']//img")
360 for result
in searchResults:
361 tmpLink = linkFilter(result)
364 bbciplayerItem = etree.XML(self.
common.mnvItem)
366 audioTF = audioFilter(result)
368 link = tmpLink[0].attrib[
'href']
369 if urlType ==
'bigscreen':
370 link =
'http://www.bbc.co.uk/iplayer/bigscreen%s' % link.replace(
'/iplayer',
'')
371 elif urlType ==
'bbcweb':
372 link =
'http://www.bbc.co.uk'+ link
375 link = link.replace(
'/iplayer/episode/',
'')
376 index = link.find(
'/')
379 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text =
'true'
381 link =
'http://www.bbc.co.uk'+ link
382 link = self.
common.ampReplace(link)
384 title = self.
common.massageText(titleFilter(result)[0].attrib[
'title'].strip())
385 description = self.
common.massageText(etree.tostring(descFilter(result)[0], method=
"text", encoding=str).strip())
388 bbciplayerItem.find(
'title').text = title
389 bbciplayerItem.find(
'author').text =
'BBC'
390 bbciplayerItem.find(
'pubDate').text = pubDate
391 bbciplayerItem.find(
'description').text = description
392 bbciplayerItem.find(
'link').text = link
393 bbciplayerItem.xpath(
'.//media:thumbnail', namespaces=self.
common.namespaces)[0].attrib[
'url'] = self.
common.ampReplace(thumbnailFilter(result)[0].attrib[
'src'])
394 bbciplayerItem.xpath(
'.//media:content', namespaces=self.
common.namespaces)[0].attrib[
'url'] = link
401 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = countCode
404 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = s_e[0]
406 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = s_e[1]
407 itemDict[title.lower()] = bbciplayerItem
409 if not len(list(itemDict.keys())):
410 raise BBCVideoNotFound(
"No BBC Video matches found for search value (%s)" % title)
413 self.
channel[
'channel_numresults'] = len(itemDict)
415 return [itemDict, resultTree.xpath(
'//pageInfo')[0].text]
420 """Common name for a video search. Used to interface with MythTV plugin NetVision
425 except Exception
as e:
426 sys.stderr.write(
'%s' % e)
429 if self.
config[
'debug_enabled']:
430 print(
"self.userPrefs:")
431 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
441 data = self.
searchTitle(title, pagenumber, self.page_limit)
442 except BBCVideoNotFound
as msg:
443 sys.stderr.write(
"%s\n" % msg)
445 except BBCUrlError
as msg:
446 sys.stderr.write(
'%s\n' % msg)
448 except BBCHttpError
as msg:
451 except BBCRssError
as msg:
454 except Exception
as e:
455 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
459 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
462 itemCount = len(list(data[0].keys()))
463 if data[1] ==
'true':
464 self.
channel[
'channel_returned'] = itemCount
465 self.
channel[
'channel_startindex'] = itemCount
466 self.
channel[
'channel_numresults'] = itemCount+(self.page_limit*(int(pagenumber)-1)+1)
468 self.
channel[
'channel_returned'] = itemCount+(self.page_limit*(int(pagenumber)-1))
469 self.
channel[
'channel_startindex'] = self.
channel[
'channel_returned']
470 self.
channel[
'channel_numresults'] = self.
channel[
'channel_returned']
474 rssTree.append(channelTree)
477 for key
in sorted(data[0].keys()):
479 channelTree.append(data[0][key])
483 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
484 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))
489 '''Gather the BBC iPlayer feeds then get a max page of videos meta data in each of them
490 Display the results and exit
495 except Exception
as e:
496 sys.stderr.write(
'%s' % e)
499 if self.
config[
'debug_enabled']:
500 print(
"self.userPrefs:")
501 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
508 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
512 rssTree.append(channelTree)
515 searchResultTree = []
516 searchFilter = etree.XPath(
"//item")
517 userSearchStrings =
'userSearchStrings'
519 userSearch = self.
userPrefs.
find(userSearchStrings).xpath(
'./userSearch')
521 for searchDetails
in userSearch:
523 data = self.
searchTitle(searchDetails.find(
'searchTerm').text, 1, self.page_limit)
524 except BBCVideoNotFound
as msg:
525 sys.stderr.write(
"%s\n" % msg)
527 except BBCUrlError
as msg:
528 sys.stderr.write(
'%s\n' % msg)
530 except BBCHttpError
as msg:
533 except BBCRssError
as msg:
536 except Exception
as e:
537 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
539 dirElement = etree.XML(
'<directory></directory>')
540 dirElement.attrib[
'name'] = self.
common.massageText(searchDetails.find(
'dirName').text)
543 for key
in sorted(data[0].keys()):
545 dirElement.append(data[0][key])
547 channelTree.append(dirElement)
551 rssData = etree.XML(
'<xml></xml>')
552 for feedType
in [
'treeviewURLS',
'userFeeds']:
558 urlEnabled = rssFeed.attrib.get(
'enabled')
559 if urlEnabled ==
'false':
561 urlName = rssFeed.attrib.get(
'name')
563 uniqueName =
'%s;%s' % (urlName, rssFeed.text)
565 uniqueName =
'RSS;%s' % (rssFeed.text)
566 url = etree.XML(
'<url></url>')
567 etree.SubElement(url,
"name").text = uniqueName
568 etree.SubElement(url,
"href").text = rssFeed.text
569 etree.SubElement(url,
"filter").text =
"atm:title"
570 etree.SubElement(url,
"filter").text =
"//atm:entry"
571 etree.SubElement(url,
"parserType").text =
'xml'
574 if self.
config[
'debug_enabled']:
576 sys.stdout.write(etree.tostring(rssData, encoding=
'UTF-8', pretty_print=
True))
580 if rssData.find(
'url')
is not None:
582 resultTree = self.
common.getUrlData(rssData)
583 except Exception
as errormsg:
585 if self.
config[
'debug_enabled']:
587 sys.stdout.write(etree.tostring(resultTree, encoding=
'UTF-8', pretty_print=
True))
594 urlType =
'fullscreen'
597 feedFilter = etree.XPath(
'//url[text()=$url]')
598 itemFilter = etree.XPath(
'.//atm:entry', namespaces=self.
common.namespaces)
599 titleFilter = etree.XPath(
'.//atm:title', namespaces=self.
common.namespaces)
600 mediaFilter = etree.XPath(
'.//atm:category[@term="TV"]', namespaces=self.
common.namespaces)
601 linkFilter = etree.XPath(
'.//atm:link', namespaces=self.
common.namespaces)
602 descFilter1 = etree.XPath(
'.//atm:content', namespaces=self.
common.namespaces)
603 descFilter2 = etree.XPath(
'.//p')
604 itemThumbNail = etree.XPath(
'.//media:thumbnail', namespaces=self.
common.namespaces)
605 creationDate = etree.XPath(
'.//atm:updated', namespaces=self.
common.namespaces)
606 itemDwnLink = etree.XPath(
'.//media:content', namespaces=self.
common.namespaces)
607 itemLanguage = etree.XPath(
'.//media:content', namespaces=self.
common.namespaces)
608 rssName = etree.XPath(
'atm:title', namespaces=self.
common.namespaces)
610 categoryElement =
None
612 for result
in resultTree.findall(
'results'):
613 names = result.find(
'name').text.split(
';')
614 names[0] = self.
common.massageText(names[0])
615 if names[0] ==
'RSS':
616 names[0] = self.
common.massageText(rssName(result.find(
'result'))[0].text.replace(
'BBC iPlayer - ',
''))
619 url = feedFilter(self.
userPrefs, url=names[1])
621 if url[0].attrib.get(
'max'):
623 urlMax = int(url[0].attrib.get(
'max'))
626 elif url[0].getparent().attrib.get(
'globalmax'):
628 urlMax = int(url[0].getparent().attrib.get(
'globalmax'))
634 channelLanguage =
'en'
636 if names[0] != categoryDir:
637 if categoryDir
is not None:
638 channelTree.append(categoryElement)
639 categoryElement = etree.XML(
'<directory></directory>')
640 categoryElement.attrib[
'name'] = names[0]
642 categoryDir = names[0]
644 if self.
config[
'debug_enabled']:
645 print(
"Results: #Items(%s) for (%s)" % (len(itemFilter(result)), names))
650 itemDict = [(pd.text, pd.getparent())
for pd
in creationDate(result)]
651 itemList = sorted(itemDict, key=itemgetter(0), reverse=
True)
653 for tupleDate
in itemList:
654 itemData = tupleDate[1]
655 bbciplayerItem = etree.XML(self.
common.mnvItem)
656 tmpLink = linkFilter(itemData)
658 link = tmpLink[0].attrib[
'href']
659 if urlType ==
'bigscreen':
660 link = link.replace(
'/iplayer/',
'/iplayer/bigscreen/')
661 elif urlType ==
'bbcweb':
664 if len(mediaFilter(itemData)):
665 link = link.replace(
'http://www.bbc.co.uk/iplayer/episode/',
'')
666 index = link.find(
'/')
669 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text =
'true'
675 pubdate = creationDate(itemData)
677 pubdate = pubdate[0].text
678 pubdate = time.strptime(pubdate,
'%Y-%m-%dT%H:%M:%SZ')
679 pubdate = time.strftime(self.
common.pubDateFormat, pubdate)
681 pubdate = datetime.datetime.now().strftime(self.
common.pubDateFormat)
684 bbciplayerItem.find(
'title').text = self.
common.massageText(titleFilter(itemData)[0].text.strip())
685 bbciplayerItem.find(
'author').text = itemAuthor
686 bbciplayerItem.find(
'pubDate').text = pubdate
687 description = etree.HTML(etree.tostring(descFilter1(itemData)[0], method=
"text", encoding=str).strip())
688 description = etree.tostring(descFilter2(description)[1], method=
"text", encoding=str).strip()
689 bbciplayerItem.find(
'description').text = self.
common.massageText(description)
690 bbciplayerItem.find(
'link').text = link
691 itemDwnLink(bbciplayerItem)[0].attrib[
'url'] = link
693 itemThumbNail(bbciplayerItem)[0].attrib[
'url'] = self.
common.ampReplace(itemThumbNail(itemData)[0].attrib[
'url'])
696 itemLanguage(bbciplayerItem)[0].attrib[
'lang'] = channelLanguage
700 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = countCode
703 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = s_e[0]
705 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = s_e[1]
706 categoryElement.append(bbciplayerItem)
713 if categoryElement
is not None:
714 if categoryElement.xpath(
'.//item')
is not None:
715 channelTree.append(categoryElement)
718 if len(rssTree.xpath(
'//item')):
720 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
721 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))