MythTV master
pbs_api.py
Go to the documentation of this file.
1# -*- coding: UTF-8 -*-
2
3# ----------------------
4# Name: pbs_api - Simple-to-use Python interface to the PBS RSS feeds
5# (http://video.pbs.org/)
6# Python Script
7# Author: R.D. Vaughan
8# Purpose: This python script is intended to perform a variety of utility functions to
9# search and access text metadata, video and image URLs from PBS Web site.
10#
11# License:Creative Commons GNU GPL v2
12# (http://creativecommons.org/licenses/GPL/2.0/)
13#-------------------------------------
14__title__ ="pbs_api - Simple-to-use Python interface to the PBS videos (http://video.pbs.org/)"
15__author__="R.D. Vaughan"
16__purpose__='''
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 PBS Web site. These routines process videos
19provided by PBS (http://video.pbs.org/). The specific PBS RSS feeds that are processed are controled through a user XML preference file usually found at
20"~/.mythtv/MythNetvision/userGrabberPrefs/pbs.xml"
21'''
22
23__version__="v0.1.1"
24# 0.1.0 Initial development
25# 0.1.1 Debug code was should have been commented out. This has been corrected.
26
27import os, struct, sys, re, time, datetime, shutil, urllib.request, urllib.parse, urllib.error
28
29import logging
30from threading import Thread
31from copy import deepcopy
32from operator import itemgetter, attrgetter
33
34from .pbs_exceptions import (PBSUrlError, PBSHttpError, PBSRssError, PBSVideoNotFound, PBSConfigFileError, PBSUrlDownloadError)
35import io
36
37class OutStreamEncoder(object):
38 """Wraps a stream with an encoder"""
39 def __init__(self, outstream, encoding=None):
40 self.out = outstream
41 if not encoding:
42 self.encoding = sys.getfilesystemencoding()
43 else:
44 self.encoding = encoding
45
46 def write(self, obj):
47 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
48 if isinstance(obj, str):
49 obj = obj.encode(self.encoding)
50 self.out.buffer.write(obj)
51
52 def __getattr__(self, attr):
53 """Delegate everything but write to the stream"""
54 return getattr(self.out, attr)
55
56if isinstance(sys.stdout, io.TextIOWrapper):
57 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
58 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
59
60
61try:
62 from io import StringIO
63 from lxml import etree
64except Exception as e:
65 sys.stderr.write('\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
66 sys.exit(1)
67
68
69# Used for debugging
70#import nv_python_libs.mashups.mashups_api as target
71try:
72 '''Import the python mashups support classes
73 '''
74 import nv_python_libs.mashups.mashups_api as mashups_api
75except Exception as e:
76 sys.stderr.write('''
77The subdirectory "nv_python_libs/mashups" containing the modules mashups_api and
78mashups_exceptions.py (v0.1.0 or greater),
79They should have been included with the distribution of pbs.py.
80Error(%s)
81''' % e)
82 sys.exit(1)
83if mashups_api.__version__ < '0.1.0':
84 sys.stderr.write("\n! Error: Your current installed mashups_api.py version is (%s)\nYou must at least have version (0.1.0) or higher.\n" % mashups_api.__version__)
85 sys.exit(1)
86
87
88class Videos(object):
89 """Main interface to http://video.pbs.org/
90 This is done to support a common naming framework for all python Netvision plugins no matter their
91 site target.
92
93 Supports search methods
94 The apikey is a not required to access http://video.pbs.org/
95 """
96 def __init__(self,
97 apikey,
98 mythtv = True,
99 interactive = False,
100 select_first = False,
101 debug = False,
102 custom_ui = None,
103 language = None,
104 search_all_languages = False,
105 ):
106 """apikey (str/unicode):
107 Specify the target site API key. Applications need their own key in some cases
108
109 mythtv (True/False):
110 When True, the returned meta data is being returned has the key and values massaged to match MythTV
111 When False, the returned meta data is being returned matches what target site returned
112
113 interactive (True/False): (This option is not supported by all target site apis)
114 When True, uses built-in console UI is used to select the correct show.
115 When False, the first search result is used.
116
117 select_first (True/False): (This option is not supported currently implemented in any grabbers)
118 Automatically selects the first series search result (rather
119 than showing the user a list of more than one series).
120 Is overridden by interactive = False, or specifying a custom_ui
121
122 debug (True/False):
123 shows verbose debugging information
124
125 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
126 A callable subclass of interactive class (overrides interactive option)
127
128 language (2 character language abbreviation): (This option is not supported by all target site apis)
129 The language of the returned data. Is also the language search
130 uses. Default is "en" (English). For full list, run..
131
132 search_all_languages (True/False): (This option is not supported by all target site apis)
133 By default, a Netvision grabber will only search in the language specified using
134 the language option. When this is True, it will search for the
135 show in any language
136
137 """
138 self.config = {}
139
140 if apikey is not None:
141 self.config['apikey'] = apikey
142 else:
143 pass # PBS does not require an apikey
144
145 self.config['debug_enabled'] = debug # show debugging messages
146 self.common = common
147 self.common.debug = debug # Set the common function debug level
148
149 self.log_name = 'PBS_Grabber'
150 self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name)
151 self.logger = self.common.logger # Setups the logger (self.log.debug() etc)
152
153 self.config['custom_ui'] = custom_ui
154
155 self.config['interactive'] = interactive
156
157 self.config['select_first'] = select_first
158
159 self.config['search_all_languages'] = search_all_languages
160
161 self.error_messages = {'PBSUrlError': "! Error: The URL (%s) cause the exception error (%s)\n", 'PBSHttpError': "! Error: An HTTP communications error with the PBS was raised (%s)\n", 'PBSRssError': "! Error: Invalid RSS meta data\nwas received from the PBS error (%s). Skipping item.\n", 'PBSVideoNotFound': "! Error: Video search with the PBS did not return any results (%s)\n", 'PBSConfigFileError': "! Error: pbs_config.xml file missing\nit should be located in and named as (%s).\n", 'PBSUrlDownloadError': "! Error: Downloading a RSS feed or Web page (%s).\n", }
162
163 # Channel details and search results
164 self.channel = {'channel_title': 'PBS', 'channel_link': 'http://video.pbs.org/', 'channel_description': "Discover award-winning programming – right at your fingertips – on PBS Video. Catch the episodes you may have missed and watch your favorite shows whenever you want.", 'channel_numresults': 0, 'channel_returned': 1, 'channel_startindex': 0}
165
166 self.channel_icon = '%SHAREDIR%/mythnetvision/icons/pbs.png'
167
168 self.config['image_extentions'] = ["png", "jpg", "bmp"] # Acceptable image extentions
169
170 # Initialize Mashups api variables
171 mashups_api.common = self.common
172 self.mashups_api = mashups_api.Videos('')
173 self.mashups_api.channel = self.channel
174 if language:
175 self.mashups_api.config['language'] = self.config['language']
176 self.mashups_api.config['debug_enabled'] = self.config['debug_enabled']
177 self.mashups_api.getUserPreferences = self.getUserPreferences
178 # end __init__()
179
180
185
186 def getPBSConfig(self):
187 ''' Read the MNV PBS grabber "pbs_config.xml" configuration file
188 return nothing
189 '''
190 # Read the grabber pbs_config.xml configuration file
191 url = 'file://%s/nv_python_libs/configs/XML/pbs_config.xml' % (baseProcessingDir, )
192 if not os.path.isfile(url[7:]):
193 raise PBSConfigFileError(self.error_messages['PBSConfigFileError'] % (url[7:], ))
194
195 if self.config['debug_enabled']:
196 print(url)
197 print()
198 try:
199 self.pbs_config = etree.parse(url)
200 except Exception as e:
201 raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
202 return
203 # end getPBSConfig()
204
205
207 '''Read the pbs_config.xml and user preference pbs.xml file.
208 If the pbs.xml file does not exist then create it.
209 If the pbs.xml file is too old then update it.
210 return nothing
211 '''
212 # Get pbs_config.xml
213 self.getPBSConfig()
214
215 # Check if the pbs.xml file exists
216 userPreferenceFile = self.pbs_config.find('userPreferenceFile').text
217 if userPreferenceFile[0] == '~':
218 self.pbs_config.find('userPreferenceFile').text = "%s%s" % (os.path.expanduser("~"), userPreferenceFile[1:])
219 if os.path.isfile(self.pbs_config.find('userPreferenceFile').text):
220 # Read the grabber pbs_config.xml configuration file
221 url = 'file://%s' % (self.pbs_config.find('userPreferenceFile').text, )
222 if self.config['debug_enabled']:
223 print(url)
224 print()
225 try:
226 self.userPrefs = etree.parse(url)
227 except Exception as e:
228 raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
229 # Check if the pbs.xml file is too old
230 nextUpdateSecs = int(self.userPrefs.find('updateDuration').text)*86400 # seconds in a day
231 nextUpdate = time.localtime(os.path.getmtime(self.pbs_config.find('userPreferenceFile').text)+nextUpdateSecs)
232 now = time.localtime()
233 if nextUpdate > now or self.Search:
234 self.mashups_api.userPrefs = self.userPrefs
235 return
236 create = False
237 else:
238 create = True
239
240 # If required create/update the pbs.xml file
241 self.updatePBS(create)
242 return
243 # end getUserPreferences()
244
245 def updatePBS(self, create=False):
246 ''' Create or update the pbs.xml user preferences file
247 return nothing
248 '''
249 userPBS = '''
250<userPBS>
251<!--
252 All PBS shows that have represented on the http://video.pbs.org/ web page are included
253 in as directories. A user may enable it disable an individual show so that it will be
254 included in the treeview. By default ALL shows are enabled.
255 NOTE: As the search is based on the treeview data disabling shows will also reduve the
256 number of possible search results .
257 Updates to the "pbs.xml" file is made every X number of days as determined by the value of
258 the "updateDuration" element in this file. The default is every 3 days.
259-->
260<!-- Number of days between updates to the config file -->
261<updateDuration>3</updateDuration>
262
263<!--
264 The PBS Search
265 "enabled" If you want to remove a source URL then change the "enabled" attribute to "false".
266 "xsltFile" The XSLT file name that is used to translate data into MNV item format
267 "type" The source type "xml", "html" and "xhtml"
268 "url" The link that is used to retrieve the information from the Internet
269 "pageFunction" Identifies a XPath extension function that returns the start page/index for the
270 specific source.
271 "mnvsearch" (optional) Identifies that search items are to include items from the MNV table using the
272 mnvsearch_api.py functions. This attributes value must match the "feedtitle" value
273 as it is in the "internetcontentarticles" table. When present the "xsltFile",
274 "url" and "pageFunction" attributes are left empty as they will be ignored.
275-->
276<search name="PBS Search">
277 <subDirectory name="PBS">
278 <sourceURL enabled="true" name="PBS" xsltFile="" type="xml" url="" pageFunction="" mnvsearch="PBS"/>
279 </subDirectory>
280</search>
281
282<!--
283 The PBS Video RSS feed and HTML URLs.
284 "globalmax" (optional) Is a way to limit the number of items processed per source for all
285 treeview URLs. A value of zero (0) means there are no limitations.
286 "max" (optional) Is a way to limit the number of items processed for an individual sourceURL.
287 This value will override any "globalmax" setting. A value of zero (0) means
288 there are no limitations and would be the same if the attribute was no included at all.
289 "enabled" If you want to remove a source URL then change the "enabled" attribute to "false".
290 "xsltFile" The XSLT file name that is used to translate data into MNV item format
291 "type" The source type "xml", "html" and "xhtml"
292 "url" The link that is used to retrieve the information from the Internet
293 "parameter" (optional) Specifies source specific parameter that are passed to the XSLT stylesheet.
294 Multiple parameters require the use of key/value pairs. Example:
295 parameter="key1:value1;key2:value2" with the ";" as the separator value.
296-->
297
298'''
299
300 # Get the current show links from the PBS web site
301 showData = self.common.getUrlData(self.pbs_config.find('treeviewUrls'))
302
303 if self.config['debug_enabled']:
304 print("create(%s)" % create)
305 print("showData:")
306 sys.stdout.write(etree.tostring(showData, encoding='UTF-8', pretty_print=True))
307 print()
308
309 # If there is any data then add to new pbs.xml element tree
310 showsDir = showData.xpath('//directory')
311 if len(showsDir):
312 for dirctory in showsDir:
313 userPBS+=etree.tostring(dirctory, encoding='UTF-8', pretty_print=True)
314 userPBS+='</userPBS>'
315 userPBS = etree.XML(userPBS)
316
317 if self.config['debug_enabled']:
318 print("Before any merging userPBS:")
319 sys.stdout.write(etree.tostring(userPBS, encoding='UTF-8', pretty_print=True))
320 print()
321
322 # If there was an existing pbs.xml file then add any relevant user settings to this new pbs.xml
323 if not create:
324 userPBS.find('updateDuration').text = self.userPrefs.find('updateDuration').text
325 for showElement in self.userPrefs.xpath("//sourceURL[@enabled='false']"):
326 showName = showElement.getparent().attrib['name']
327 sourceName = showElement.attrib['name']
328 elements = userPBS.xpath("//sourceURL[@name=$showName]", showName=showName, sourceName=sourceName)
329 if len(elements):
330 elements[0].attrib['enabled'] = 'false'
331
332 if self.config['debug_enabled']:
333 print("After any merging userPBS:")
334 sys.stdout.write(etree.tostring(userPBS, encoding='UTF-8', pretty_print=True))
335 print()
336
337 # Save the pbs.xml file
338 prefDir = self.pbs_config.find('userPreferenceFile').text.replace('/pbs.xml', '')
339 if not os.path.isdir(prefDir):
340 os.makedirs(prefDir)
341 fd = open(self.pbs_config.find('userPreferenceFile').text, 'w')
342 fd.write('<userPBS>\n'+''.join(etree.tostring(element, encoding='UTF-8', pretty_print=True) for element in userPBS)+'</userPBS>')
343 fd.close()
344
345 # Read the refreshed user config file
346 try:
347 self.userPrefs = etree.parse(self.pbs_config.find('userPreferenceFile').text)
348 self.mashups_api.userPrefs = self.userPrefs
349 except Exception as e:
350 raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
351 return
352 # end updatePBS()
353
354
359
360 def searchForVideos(self, title, pagenumber):
361 """Common name for a video search. Used to interface with MythTV plugin NetVision
362 """
363 self.mashups_api.page_limit = self.page_limit
364 self.mashups_api.grabber_title = self.grabber_title
365 self.mashups_api.mashup_title = self.mashup_title
366 self.mashups_api.channel_icon = self.channel_icon
367 self.mashups_api.mashup_title = 'pbs'
368
369 # Easier for debugging
370# self.mashups_api.searchForVideos(title, pagenumber)
371# print
372# sys.exit()
373
374 try:
375 self.Search = True
376 self.mashups_api.Search = True
377 self.mashups_api.searchForVideos(title, pagenumber)
378 except Exception as e:
379 sys.stderr.write("! Error: During a PBS Video search (%s)\nError(%s)\n" % (title, e))
380 sys.exit(1)
381
382 sys.exit(0)
383 # end searchForVideos()
384
386 '''Gather all videos for each PBS show
387 Display the results and exit
388 '''
389 self.mashups_api.page_limit = self.page_limit
390 self.mashups_api.grabber_title = self.grabber_title
391 self.mashups_api.mashup_title = self.mashup_title
392 self.mashups_api.channel_icon = self.channel_icon
393 self.mashups_api.mashup_title = 'pbs'
394
395 # Easier for debugging
396# self.mashups_api.displayTreeView()
397# print
398# sys.exit(1)
399
400 try:
401 self.Search = False
402 self.mashups_api.Search = False
404 except Exception as e:
405 sys.stderr.write("! Error: During a PBS Video treeview\nError(%s)\n" % (e))
406 sys.exit(1)
407
408 sys.exit(0)
409 # end displayTreeView()
410# end Videos() class
def __init__(self, outstream, encoding=None)
Definition: pbs_api.py:39
def searchForVideos(self, title, pagenumber)
End of Utility functions.
Definition: pbs_api.py:360
def updatePBS(self, create=False)
Definition: pbs_api.py:245
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
Definition: pbs_api.py:105
def getPBSConfig(self)
Start - Utility functions.
Definition: pbs_api.py:186
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)
static void print(const QList< uint > &raw_minimas, const QList< uint > &raw_maximas, const QList< float > &minimas, const QList< float > &maximas)