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"
17This python script is intended to perform a variety of utility functions to search and access text
18meta data, video and image URLs from the BBC iPlayer Web site. These routines process RSS feeds
19provided 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"
32import os, struct, sys, re, time, datetime, shutil,
urllib.request, urllib.parse, urllib.error, re
34from socket
import gethostname, gethostbyname
35from threading
import Thread
36from copy
import deepcopy
37from operator
import itemgetter, attrgetter
38from MythTV
import MythXML
39from .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)
61if isinstance(sys.stdout, io.TextIOWrapper):
67 from io
import StringIO
68 from lxml
import etree
70 sys.stderr.write(
'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
75 """Main interface to http://www.bbciplayer.com/
76 This is done to support a common naming framework
for all python Netvision plugins no matter their site
79 Supports search methods
80 The apikey
is a
not required to access http://www.bbciplayer.com/
90 search_all_languages = False,
92 """apikey (str/unicode):
93 Specify the target site API key. Applications need their own key in some cases
96 When
True, the returned meta data
is being returned has the key
and values massaged to match MythTV
97 When
False, the returned meta data
is being returned matches what target site returned
99 interactive (
True/
False): (This option
is not supported by all target site apis)
100 When
True, uses built-
in console UI
is used to select the correct show.
101 When
False, the first search result
is used.
103 select_first (
True/
False): (This option
is not supported currently implemented
in any grabbers)
104 Automatically selects the first series search result (rather
105 than showing the user a list of more than one series).
106 Is overridden by interactive =
False,
or specifying a custom_ui
109 shows verbose debugging information
111 custom_ui (xx_ui.BaseUI subclass): (This option
is not supported currently implemented
in any grabbers)
112 A callable subclass of interactive
class (overrides interactive option)
114 language (2 character language abbreviation): (This option
is not supported by all target site apis)
115 The language of the returned data. Is also the language search
116 uses. Default
is "en" (English). For full list, run..
118 search_all_languages (
True/
False): (This option
is not supported by all target site apis)
119 By default, a Netvision grabber will only search
in the language specified using
120 the language option. When this
is True, it will search
for the
127 if apikey
is not None:
128 self.
config[
'apikey'] = apikey
132 self.
config[
'debug_enabled'] = debug
140 self.
config[
'custom_ui'] = custom_ui
142 self.
config[
'interactive'] = interactive
144 self.
config[
'select_first'] = select_first
146 self.
config[
'search_all_languages'] = search_all_languages
148 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", }
151 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}
155 etree.XPath(
'.//a[@class="episode-title title-link cta-video"]', namespaces=self.
common.namespaces),
156 etree.XPath(
'.//div[@class="feature video"]', namespaces=self.
common.namespaces),
157 etree.XPath(
'.//atm:category[@term="TV"]', namespaces=self.
common.namespaces),
163 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+).*.+?Episode\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
165 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+)\\ \\-\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
167 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+).*.+?Part\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
169 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+)\\:\\ Programme\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
171 re.compile(
r'''^.+?Series\\ (?P<seasno>[0-9]+).*$''', re.UNICODE),
173 re.compile(
r'''^.+?Episode\\ (?P<seasno>[0-9]+).*$''', re.UNICODE),
178 self.
config[
'image_extentions'] = [
"png",
"jpg",
"bmp"]
188 ''' Read the MNV BBC iPlayer grabber "bbc_config.xml" configuration file
192 url =
'file://%s/nv_python_libs/configs/XML/bbc_config.xml' % (baseProcessingDir, )
193 if not os.path.isfile(url[7:]):
196 if self.
config[
'debug_enabled']:
201 except Exception
as e:
208 '''Read the bbciplayer_config.xml and user preference bbciplayer.xml file.
209 If the bbciplayer.xml file does not exist then copy the default.
217 if userPreferenceFile[0] ==
'~':
218 self.
bbciplayer_config.
find(
'userPreferenceFile').text =
"%s%s" % (os.path.expanduser(
"~"), userPreferenceFile[1:])
224 if not os.path.isdir(prefDir):
226 defaultConfig =
'%s/nv_python_libs/configs/XML/defaultUserPrefs/bbciplayer.xml' % (baseProcessingDir, )
231 if self.
config[
'debug_enabled']:
236 except Exception
as e:
242 '''Parse the item information (HTML or RSS/XML) to identify if the content is a video or
243 audio file. Set the contry code if a video
is detected
as it can only be played
in the
"UK"
244 return "uk" if a video type was detected.
245 return None if a video type was NOT detected.
249 if len(xpathP(item)):
257 ''' Check is there is any season or episode number information in an item's title
258 return array of season
and/
or episode numbers
259 return array
with None values
267 s_e[0], s_e[1] = match.groups()
270 s_e[0] = match.groups()[0]
273 s_e[1] = match.groups()[0]
285 playerUrl = self.
mythxml.getInternetContentUrl(
"nv_python_libs/configs/HTML/bbciplayer.html", \
290 '''Key word video search of the BBC iPlayer web site
291 return an array of matching item elements
298 searchVar =
'/?q=%s&page=%s' % (urllib.parse.quote(title.encode(
"utf-8")), pagenumber)
299 except UnicodeDecodeError:
300 searchVar =
'/?q=%s&page=%s' % (urllib.parse.quote(title), pagenumber)
304 if self.
config[
'debug_enabled']:
313 except Exception
as errormsg:
321 if resultTree
is None:
322 raise BBCVideoNotFound(
"No BBC Video matches found for search value (%s)" % title)
324 searchResults = resultTree.xpath(
'//result//li')
325 if not len(searchResults):
326 raise BBCVideoNotFound(
"No BBC Video matches found for search value (%s)" % title)
330 pubDate = datetime.datetime.now().strftime(self.
common.pubDateFormat)
336 urlType =
'fullscreen'
339 audioFilter = etree.XPath(
'contains(./@class,"audio") or contains(./../../@class,"audio")')
340 linkFilter = etree.XPath(
".//div[@class='episode-info ']//a")
341 titleFilter = etree.XPath(
".//div[@class='episode-info ']//a")
342 descFilter = etree.XPath(
".//div[@class='episode-info ']//p[@class='episode-synopsis']")
343 thumbnailFilter = etree.XPath(
".//span[@class='episode-image cta-play']//img")
345 for result
in searchResults:
346 tmpLink = linkFilter(result)
349 bbciplayerItem = etree.XML(self.
common.mnvItem)
351 audioTF = audioFilter(result)
353 link = tmpLink[0].attrib[
'href']
354 if urlType ==
'bigscreen':
355 link =
'http://www.bbc.co.uk/iplayer/bigscreen%s' % link.replace(
'/iplayer',
'')
356 elif urlType ==
'bbcweb':
357 link =
'http://www.bbc.co.uk'+ link
360 link = link.replace(
'/iplayer/episode/',
'')
361 index = link.find(
'/')
364 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text =
'true'
366 link =
'http://www.bbc.co.uk'+ link
367 link = self.
common.ampReplace(link)
369 title = self.
common.massageText(titleFilter(result)[0].attrib[
'title'].strip())
370 description = self.
common.massageText(etree.tostring(descFilter(result)[0], method=
"text", encoding=str).strip())
373 bbciplayerItem.find(
'title').text = title
374 bbciplayerItem.find(
'author').text =
'BBC'
375 bbciplayerItem.find(
'pubDate').text = pubDate
376 bbciplayerItem.find(
'description').text = description
377 bbciplayerItem.find(
'link').text = link
378 bbciplayerItem.xpath(
'.//media:thumbnail', namespaces=self.
common.namespaces)[0].attrib[
'url'] = self.
common.ampReplace(thumbnailFilter(result)[0].attrib[
'src'])
379 bbciplayerItem.xpath(
'.//media:content', namespaces=self.
common.namespaces)[0].attrib[
'url'] = link
386 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = countCode
389 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = s_e[0]
391 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = s_e[1]
392 itemDict[title.lower()] = bbciplayerItem
394 if not len(list(itemDict.keys())):
395 raise BBCVideoNotFound(
"No BBC Video matches found for search value (%s)" % title)
398 self.
channel[
'channel_numresults'] = len(itemDict)
400 return [itemDict, resultTree.xpath(
'//pageInfo')[0].text]
405 """Common name for a video search. Used to interface with MythTV plugin NetVision
410 except Exception
as e:
411 sys.stderr.write(
'%s' % e)
414 if self.
config[
'debug_enabled']:
415 print(
"self.userPrefs:")
416 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
426 data = self.
searchTitle(title, pagenumber, self.page_limit)
427 except BBCVideoNotFound
as msg:
428 sys.stderr.write(
"%s\n" % msg)
430 except BBCUrlError
as msg:
431 sys.stderr.write(
'%s\n' % msg)
433 except BBCHttpError
as msg:
436 except BBCRssError
as msg:
439 except Exception
as e:
440 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
444 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
447 itemCount = len(list(data[0].keys()))
448 if data[1] ==
'true':
449 self.
channel[
'channel_returned'] = itemCount
450 self.
channel[
'channel_startindex'] = itemCount
451 self.
channel[
'channel_numresults'] = itemCount+(self.page_limit*(int(pagenumber)-1)+1)
453 self.
channel[
'channel_returned'] = itemCount+(self.page_limit*(int(pagenumber)-1))
454 self.
channel[
'channel_startindex'] = self.
channel[
'channel_returned']
455 self.
channel[
'channel_numresults'] = self.
channel[
'channel_returned']
459 rssTree.append(channelTree)
462 for key
in sorted(data[0].keys()):
464 channelTree.append(data[0][key])
468 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
469 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))
474 '''Gather the BBC iPlayer feeds then get a max page of videos meta data in each of them
475 Display the results and exit
480 except Exception
as e:
481 sys.stderr.write(
'%s' % e)
484 if self.
config[
'debug_enabled']:
485 print(
"self.userPrefs:")
486 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
493 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
497 rssTree.append(channelTree)
500 searchResultTree = []
501 searchFilter = etree.XPath(
"//item")
502 userSearchStrings =
'userSearchStrings'
504 userSearch = self.
userPrefs.
find(userSearchStrings).xpath(
'./userSearch')
506 for searchDetails
in userSearch:
508 data = self.
searchTitle(searchDetails.find(
'searchTerm').text, 1, self.page_limit)
509 except BBCVideoNotFound
as msg:
510 sys.stderr.write(
"%s\n" % msg)
512 except BBCUrlError
as msg:
513 sys.stderr.write(
'%s\n' % msg)
515 except BBCHttpError
as msg:
518 except BBCRssError
as msg:
521 except Exception
as e:
522 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
524 dirElement = etree.XML(
'<directory></directory>')
525 dirElement.attrib[
'name'] = self.
common.massageText(searchDetails.find(
'dirName').text)
528 for key
in sorted(data[0].keys()):
530 dirElement.append(data[0][key])
532 channelTree.append(dirElement)
536 rssData = etree.XML(
'<xml></xml>')
537 for feedType
in [
'treeviewURLS',
'userFeeds']:
543 urlEnabled = rssFeed.attrib.get(
'enabled')
544 if urlEnabled ==
'false':
546 urlName = rssFeed.attrib.get(
'name')
548 uniqueName =
'%s;%s' % (urlName, rssFeed.text)
550 uniqueName =
'RSS;%s' % (rssFeed.text)
551 url = etree.XML(
'<url></url>')
552 etree.SubElement(url,
"name").text = uniqueName
553 etree.SubElement(url,
"href").text = rssFeed.text
554 etree.SubElement(url,
"filter").text =
"atm:title"
555 etree.SubElement(url,
"filter").text =
"//atm:entry"
556 etree.SubElement(url,
"parserType").text =
'xml'
559 if self.
config[
'debug_enabled']:
561 sys.stdout.write(etree.tostring(rssData, encoding=
'UTF-8', pretty_print=
True))
565 if rssData.find(
'url')
is not None:
567 resultTree = self.
common.getUrlData(rssData)
568 except Exception
as errormsg:
570 if self.
config[
'debug_enabled']:
572 sys.stdout.write(etree.tostring(resultTree, encoding=
'UTF-8', pretty_print=
True))
579 urlType =
'fullscreen'
582 feedFilter = etree.XPath(
'//url[text()=$url]')
583 itemFilter = etree.XPath(
'.//atm:entry', namespaces=self.
common.namespaces)
584 titleFilter = etree.XPath(
'.//atm:title', namespaces=self.
common.namespaces)
585 mediaFilter = etree.XPath(
'.//atm:category[@term="TV"]', namespaces=self.
common.namespaces)
586 linkFilter = etree.XPath(
'.//atm:link', namespaces=self.
common.namespaces)
587 descFilter1 = etree.XPath(
'.//atm:content', namespaces=self.
common.namespaces)
588 descFilter2 = etree.XPath(
'.//p')
589 itemThumbNail = etree.XPath(
'.//media:thumbnail', namespaces=self.
common.namespaces)
590 creationDate = etree.XPath(
'.//atm:updated', namespaces=self.
common.namespaces)
591 itemDwnLink = etree.XPath(
'.//media:content', namespaces=self.
common.namespaces)
592 itemLanguage = etree.XPath(
'.//media:content', namespaces=self.
common.namespaces)
593 rssName = etree.XPath(
'atm:title', namespaces=self.
common.namespaces)
595 categoryElement =
None
597 for result
in resultTree.findall(
'results'):
598 names = result.find(
'name').text.split(
';')
599 names[0] = self.
common.massageText(names[0])
600 if names[0] ==
'RSS':
601 names[0] = self.
common.massageText(rssName(result.find(
'result'))[0].text.replace(
'BBC iPlayer - ',
''))
604 url = feedFilter(self.
userPrefs, url=names[1])
606 if url[0].attrib.get(
'max'):
608 urlMax = int(url[0].attrib.get(
'max'))
611 elif url[0].getparent().attrib.get(
'globalmax'):
613 urlMax = int(url[0].getparent().attrib.get(
'globalmax'))
619 channelLanguage =
'en'
621 if names[0] != categoryDir:
622 if categoryDir
is not None:
623 channelTree.append(categoryElement)
624 categoryElement = etree.XML(
'<directory></directory>')
625 categoryElement.attrib[
'name'] = names[0]
627 categoryDir = names[0]
629 if self.
config[
'debug_enabled']:
630 print(
"Results: #Items(%s) for (%s)" % (len(itemFilter(result)), names))
635 itemDict = [(pd.text, pd.getparent())
for pd
in creationDate(result)]
636 itemList = sorted(itemDict, key=itemgetter(0), reverse=
True)
638 for tupleDate
in itemList:
639 itemData = tupleDate[1]
640 bbciplayerItem = etree.XML(self.
common.mnvItem)
641 tmpLink = linkFilter(itemData)
643 link = tmpLink[0].attrib[
'href']
644 if urlType ==
'bigscreen':
645 link = link.replace(
'/iplayer/',
'/iplayer/bigscreen/')
646 elif urlType ==
'bbcweb':
649 if len(mediaFilter(itemData)):
650 link = link.replace(
'http://www.bbc.co.uk/iplayer/episode/',
'')
651 index = link.find(
'/')
654 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text =
'true'
660 pubdate = creationDate(itemData)
662 pubdate = pubdate[0].text
663 pubdate = time.strptime(pubdate,
'%Y-%m-%dT%H:%M:%SZ')
664 pubdate = time.strftime(self.
common.pubDateFormat, pubdate)
666 pubdate = datetime.datetime.now().strftime(self.
common.pubDateFormat)
669 bbciplayerItem.find(
'title').text = self.
common.massageText(titleFilter(itemData)[0].text.strip())
670 bbciplayerItem.find(
'author').text = itemAuthor
671 bbciplayerItem.find(
'pubDate').text = pubdate
672 description = etree.HTML(etree.tostring(descFilter1(itemData)[0], method=
"text", encoding=str).strip())
673 description = etree.tostring(descFilter2(description)[1], method=
"text", encoding=str).strip()
674 bbciplayerItem.find(
'description').text = self.
common.massageText(description)
675 bbciplayerItem.find(
'link').text = link
676 itemDwnLink(bbciplayerItem)[0].attrib[
'url'] = link
678 itemThumbNail(bbciplayerItem)[0].attrib[
'url'] = self.
common.ampReplace(itemThumbNail(itemData)[0].attrib[
'url'])
681 itemLanguage(bbciplayerItem)[0].attrib[
'lang'] = channelLanguage
685 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = countCode
688 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = s_e[0]
690 etree.SubElement(bbciplayerItem,
"{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = s_e[1]
691 categoryElement.append(bbciplayerItem)
698 if categoryElement
is not None:
699 if categoryElement.xpath(
'.//item')
is not None:
700 channelTree.append(categoryElement)
703 if len(rssTree.xpath(
'//item')):
705 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
706 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))
def __init__(self, outstream, encoding=None)
def __getattr__(self, attr)
def getSeasonEpisode(self, title)
def getBBCConfig(self)
Start - Utility functions.
def displayTreeView(self)
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
def searchForVideos(self, title, pagenumber)
def processVideoUrl(self, url)
End of Utility functions.
def searchTitle(self, title, pagenumber, pagelen)
def getUserPreferences(self)
def setCountry(self, item)
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)