MythTV master
giantbomb_api.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# -*- coding: UTF-8 -*-
3# ----------------------
4# Name: giantbomb_api.py Simple-to-use Python interface to the GiantBomb's API (api.giantbomb.com)
5# Python Script
6# Author: R.D. Vaughan
7# Purpose: This python script is intended to perform a variety of utility functions to search and
8# access text metadata and image URLs from GiantBomb. These routines are based on the
9# GiantBomb api. Specifications for this api are published at:
10# https://www.giantbomb.com/api/documentation/
11#
12# License:Creative Commons GNU GPL v2
13# (https://creativecommons.org/licenses/GPL/2.0/)
14#-------------------------------------
15__title__ ="giantbomb_api - Simple-to-use Python interface to The GiantBomb's API (www.giantbomb.com/api)";
16__author__="R.D. Vaughan"
17__purpose__='''
18This python script is intended to perform a variety of utility functions to search and access text
19metadata and image URLs from GiantBomb. These routines are based on the GiantBomb api. Specifications
20for this api are published at https://www.giantbomb.com/api/documentation/
21'''
22
23__version__="v0.2.0"
24# 0.1.0 Initial development
25# 0.2.0 R. Ernst: switched to python requests and added python3 compatibility
26
27import os, struct, sys, datetime, time, re
28import requests
29from copy import deepcopy
30
31IS_PY2 = sys.version_info[0] == 2
32if not IS_PY2:
33 unicode = str
34 unichr = chr
35
36from .giantbomb_exceptions import (GiantBombBaseError, GiantBombHttpError, GiantBombXmlError, GiantBombGameNotFound,)
37
38
39class OutStreamEncoder(object):
40 """Wraps a stream with an encoder"""
41 def __init__(self, outstream, encoding=None):
42 self.out = outstream
43 if not encoding:
44 self.encoding = sys.getfilesystemencoding()
45 else:
46 self.encoding = encoding
47
48 def write(self, obj):
49 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
50 if isinstance(obj, unicode):
51 obj = obj.encode(self.encoding)
52 if IS_PY2:
53 self.out.write(obj)
54 else:
55 self.out.buffer.write(obj)
56
57 def __getattr__(self, attr):
58 """Delegate everything but write to the stream"""
59 return getattr(self.out, attr)
60sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
61sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
62
63
64try:
65 if IS_PY2:
66 from StringIO import StringIO
67 else:
68 from io import StringIO
69 from lxml import etree
70except Exception as e:
71 sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
72 sys.exit(1)
73
74# Check that the lxml library is current enough
75# From the lxml documents it states: (https://lxml.de/installation.html)
76# "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
77# Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package
78version = ''
79for digit in etree.LIBXML_VERSION:
80 version+=str(digit)+'.'
81version = version[:-1]
82if version < '2.7.2':
83 sys.stderr.write(u'''
84! Error - The installed version of the "lxml" python library "libxml" version is too old.
85 At least "libxml" version 2.7.2 must be installed. Your version is (%s).
86''' % version)
87 sys.exit(1)
88
89
91 '''Methods that query api.giantbomb.com for metadata and outputs the results to stdout any errors are output
92 to stderr.
93 '''
94 def __init__(self,
95 apikey,
96 debug = False,
97 ):
98 """apikey (str/unicode):
99 Specify the api.giantbomb.com API key. Applications need their own key.
100 See https://www.giantbomb.com/api/ to get your own API key
101
102 debug (True/False):
103 shows verbose debugging information
104 """
105 self.config = {}
106
107 self.config['apikey'] = apikey
108 self.config['debug'] = debug
109 self.config['searchURL'] = u'https://www.giantbomb.com/api/search'
110 self.config['dataURL'] = u'https://www.giantbomb.com/api/game/%s'
111 # giantbomb.com now requires a unique 'User-Agent':
112 self.config['headers'] = {"User-Agent": 'MythTV giantbomb grabber 0.2'}
113
114 self.error_messages = {'GiantBombHttpError': u"! Error: A connection error to api.giantbomb.com was raised (%s)\n", 'GiantBombXmlError': u"! Error: Invalid XML was received from api.giantbomb.com (%s)\n", 'GiantBombBaseError': u"! Error: An error was raised (%s)\n", }
115
116 self.baseProcessingDir = os.path.dirname( os.path.realpath( __file__ ))
117
118 self.pubDateFormat = u'%a, %d %b %Y %H:%M:%S GMT'
119 self.xmlParser = etree.XMLParser(remove_blank_text=True)
120
121 self.supportedJobList = ["actor", "author", "producer", "executive producer", "director", "cinematographer", "composer", "editor", "casting", "voice actor", "music", "writer", "technical director", "design director", ]
123 'actor': 'Actor',
124 'author': 'Author',
125 'producer': 'Producer',
126 'executive producer': 'Executive Producer',
127 'director': 'Director',
128 'cinematographer': 'Cinematographer',
129 'composer': 'Composer',
130 'editor': 'Editor',
131 'casting': 'Casting',
132 'voice actor': 'Actor',
133 'music': 'Composer',
134 'writer': 'Author',
135 'technical director': 'Director',
136 'design director': 'Director',
137 }
138 # end __init__()
139
140
141 def massageText(self, text):
142 '''Removes HTML markup from a text string.
143 @param text The HTML source.
144 @return The plain text. If the HTML source contains non-ASCII
145 entities or character references, this is a Unicode string.
146 '''
147 def fixup(m):
148 text = m.group(0)
149 if text[:1] == "<":
150 return "" # ignore tags
151 if text[:2] == "&#":
152 try:
153 if text[:3] == "&#x":
154 return unichr(int(text[3:-1], 16))
155 else:
156 return unichr(int(text[2:-1]))
157 except ValueError:
158 pass
159 elif text[:1] == "&":
160 if IS_PY2:
161 from htmlentitydefs import entitydefs
162 else:
163 from html.entities import entitydefs
164 entity = entitydefs.get(text[1:-1])
165 if entity:
166 if entity[:2] == "&#":
167 try:
168 return unichr(int(entity[2:-1]))
169 except ValueError:
170 pass
171 else:
172 return unicode(entity, "iso-8859-1")
173 return text # leave as is
174 if IS_PY2:
175 text23 = self.ampReplace(re.sub(u"(?s)<[^>]*>|&#?\w+;", fixup, self.textUtf8(text))).replace(u'\n',u' ')
176 else:
177 text23 = self.ampReplace(re.sub(r"(?s)<[^>]*>|&#?\w+;", fixup, self.textUtf8(text))).replace('\n',' ')
178 return text23
179 # end massageText()
180
181
182 def textUtf8(self, text):
183 if text is None:
184 return text
185 try:
186 return unicode(text, 'utf8')
187 except UnicodeDecodeError:
188 return u''
189 except (UnicodeEncodeError, TypeError):
190 return text
191 # end textUtf8()
192
193
194 def ampReplace(self, text):
195 '''Replace all "&" characters with "&amp;"
196 '''
197 text = self.textUtf8(text)
198 return text.replace(u'&amp;',u'~~~~~').replace(u'&',u'&amp;').replace(u'~~~~~', u'&amp;')
199 # end ampReplace()
200
201
202 def htmlToString(self, context, html):
203 ''' Remove HTML tags and LFs from a string
204 return the string without HTML tags or LFs
205 '''
206 if not len(html):
207 return u""
208 return self.massageText(html).strip().replace(u'\n', u' ').replace(u'’', u"&apos;").replace(u'“', u"&apos;")
209 # end htmlToString()
210
211 def getHtmlData(self, context, *args):
212 ''' Take a HTML string and convert it to an HTML element. Then apply a filter and return
213 the results.
214 return filter array
215 return an empty array if the filter failed to find any values.
216 '''
217 xpathFilter = None
218 if len(args) > 1:
219 xpathFilter = args[0]
220 htmldata = args[1]
221 else:
222 htmldata = args[0]
223 if not htmldata:
224 return []
225 htmlElement = etree.HTML(htmldata)
226 if not xpathFilter:
227 return htmlElement
228 filteredData = htmlElement.xpath(xpathFilter)
229 if len(filteredData):
230 if xpathFilter.find('@') != -1:
231 return filteredData[0]
232 else:
233 return filteredData[0].text
234 return u''
235 # end getHtmlData()
236
237 def pubDate(self, context, *inputArgs):
238 '''Convert a date/time string in a specified format into a pubDate. The default is the
239 MNV item format
240 return the formatted pubDate string
241 return on error return the original date string
242 '''
243 args = []
244 for arg in inputArgs:
245 args.append(arg)
246 if args[0] == u'':
247 return datetime.datetime.now().strftime(self.pubDateFormat)
248 index = args[0].find('+')
249 if index == -1:
250 index = args[0].find('-')
251 if index != -1 and index > 5:
252 args[0] = args[0][:index].strip()
253 args[0] = args[0].replace(',', u'').replace('.', u'')
254 try:
255 if len(args) > 2:
256 pubdate = time.strptime(args[0], args[2])
257 elif len(args) > 1:
258 args[1] = args[1].replace(',', u'').replace('.', u'')
259 if args[1].find('GMT') != -1:
260 args[1] = args[1][:args[1].find('GMT')].strip()
261 args[0] = args[0][:args[0].rfind(' ')].strip()
262 try:
263 pubdate = time.strptime(args[0], args[1])
264 except ValueError:
265 if args[1] == '%a %d %b %Y %H:%M:%S':
266 pubdate = time.strptime(args[0], '%a %d %B %Y %H:%M:%S')
267 elif args[1] == '%a %d %B %Y %H:%M:%S':
268 pubdate = time.strptime(args[0], '%a %d %b %Y %H:%M:%S')
269 if len(args) > 2:
270 return time.strftime(args[2], pubdate)
271 else:
272 return time.strftime(self.pubDateFormat, pubdate)
273 else:
274 return datetime.datetime.now().strftime(self.pubDateFormat)
275 except Exception as err:
276 sys.stderr.write(u'! Error: pubDate variables(%s) error(%s)\n' % (args, err))
277 return args[0]
278 # end pubDate()
279
280 def futureReleaseDate(self, context, gameElement):
281 '''Convert the "expected" release date into the default MNV item format.
282 return the formatted pubDate string
283 return If there is not enough information to make a date then return an empty string
284 '''
285 try:
286 if gameElement.find('expected_release_year').text is not None:
287 year = gameElement.find('expected_release_year').text
288 else:
289 year = None
290 if gameElement.find('expected_release_quarter').text is not None:
291 quarter = gameElement.find('expected_release_quarter').text
292 else:
293 quarter = None
294 if gameElement.find('expected_release_month').text is not None:
295 month = gameElement.find('expected_release_month').text
296 else:
297 month = None
298 except:
299 return u''
300 if not year:
301 return u''
302 if month and not quarter:
303 pubdate = time.strptime((u'%s-%s-01' % (year, month)), '%Y-%m-%d')
304 elif not month and quarter:
305 month = str((int(quarter)*3))
306 pubdate = time.strptime((u'%s-%s-01' % (year, month)), '%Y-%m-%d')
307 else:
308 pubdate = time.strptime((u'%s-12-01' % (year, )), '%Y-%m-%d')
309
310 return time.strftime('%Y-%m-%d', pubdate)
311 # end futureReleaseDate()
312
313 def findImages(self, context, *args):
314 '''Parse the "image" and "description" elements for images and put in a persistant array
315 return True when there are images available
316 return False if there are no images
317 '''
318 def makeImageElement(typeImage, url, thumb):
319 ''' Create a single Image element
320 return the image element
321 '''
322 imageElement = etree.XML(u"<image></image>")
323 imageElement.attrib['type'] = typeImage
324 imageElement.attrib['url'] = url
325 imageElement.attrib['thumb'] = thumb
326 return imageElement
327 # end makeImageElement()
328
329 superImageFilter = etree.XPath('.//super_url/text()')
331 for imageElement in args[0]:
332 imageList = superImageFilter(imageElement)
333 if len(imageList):
334 for image in imageList:
335 self.imageElements.append(makeImageElement('coverart', image, image.replace(u'super', u'thumb')))
336 htmlElement = self.getHtmlData('dummy', etree.tostring(args[1][0], method="text", encoding=unicode).strip())
337 if len(htmlElement):
338 for image in htmlElement[0].xpath('.//a/img/@src'):
339 if image.find('screen') == -1:
340 continue
341 if image.find('thumb') == -1:
342 continue
343 self.imageElements.append(makeImageElement('screenshot', image.replace(u'thumb', u'super'), image))
344
345 if len(args) > 2:
346 for imageElement in args[2]:
347 imageList = superImageFilter(imageElement)
348 if len(imageList):
349 for image in imageList:
350 self.imageElements.append(makeImageElement('screenshot', image, image.replace(u'super', u'thumb')))
351
352 if not len(self.imageElements):
353 return False
354 return True
355 # end findImages()
356
357 def getImages(self, context, arg):
358 '''Return an array of image elements that was created be a previous "findImages" function call
359 return the array of image elements
360 '''
361 return self.imageElements
362 # end getImages()
363
364 def supportedJobs(self, context, *inputArgs):
365 '''Validate that the job category is supported by the
366 Universal Metadata Format item format
367 return True is supported
368 return False if not supported
369 '''
370 if type([]) == type(inputArgs[0]):
371 tmpCopy = inputArgs[0]
372 else:
373 tmpCopy = [inputArgs[0]]
374 for job in tmpCopy:
375 if job.lower() in self.supportedJobList:
376 return True
377 return False
378 # end supportedJobs()
379
380 def translateName(self, context, *inputArgs):
381 '''Translate a tag name into the Universal Metadata Format item equivalent
382 return the translated tag equivalent
383 return the input name as the name does not need translating and is already been validated
384 '''
385 name = inputArgs[0]
386 name = name.lower()
387 if name in list(self.tagTranslations.keys()):
388 return self.tagTranslations[name]
389 return name
390 # end translateName()
391
392 def buildFuncDict(self):
393 """ Build a dictionary of the XPath extention function for the XSLT stylesheets
394 Returns nothing
395 """
396 self.FuncDict = {
397 'htmlToString': self.htmlToString,
398 'getHtmlData': self.getHtmlData,
399 'pubDate': self.pubDate,
400 'futureReleaseDate': self.futureReleaseDate,
401 'findImages': self.findImages,
402 'getImages': self.getImages,
403 'supportedJobs': self.supportedJobs,
404 'translateName': self.translateName,
405 }
406 return
407 # end buildFuncDict()
408
409 def gameSearch(self, gameTitle):
410 """Display a Game query in XML format:
411 https://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format
412 Returns nothing
413 """
414 with requests.Session() as ReqSession:
415 url = self.config['searchURL']
416
417 params = {}
418 params["api_key"] = self.config['apikey']
419 params["format"] = 'xml'
420 params["page"] = 0
421 params["query"] = gameTitle
422 params["resources"] = "game"
423
424 headers = self.config['headers']
425
426 res = ReqSession.get(url, params=params, headers=headers)
427
428 try:
429 queryResult = etree.fromstring(res.content)
430 except Exception as errmsg:
431 sys.stderr.write(u"! Error: Invalid XML was received from www.giantbomb.com (%s)\n" % errmsg)
432 sys.exit(1)
433
434 queryXslt = etree.XSLT(etree.parse(u'%s/XSLT/giantbombQuery.xsl' % self.baseProcessingDir))
435 gamebombXpath = etree.FunctionNamespace('https://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format')
436 gamebombXpath.prefix = 'gamebombXpath'
437 self.buildFuncDict()
438 for key in list(self.FuncDict.keys()):
439 gamebombXpath[key] = self.FuncDict[key]
440
441 items = queryXslt(queryResult)
442
443 if items.getroot() is not None:
444 if len(items.xpath('//item')):
445 sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, ))
446 sys.exit(0)
447 # end gameSearch()
448
449 def gameData(self, gameId):
450 """Display a Game details in XML format:
451 https://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format
452 Returns nothing
453 """
454 with requests.Session() as ReqSession:
455 url = self.config['dataURL'] % gameId
456
457 params = {}
458 params["api_key"] = self.config['apikey']
459 params["format"] = 'xml'
460
461 headers = self.config['headers']
462
463 res = ReqSession.get(url, params=params, headers=headers)
464
465 try:
466 videoResult = etree.fromstring(res.content)
467 except Exception as errmsg:
468 sys.stderr.write(u"! Error: Invalid XML was received from www.giantbomb.com (%s)\n" % errmsg)
469 sys.exit(1)
470
471 gameXslt = etree.XSLT(etree.parse(u'%s/XSLT/giantbombGame.xsl' % self.baseProcessingDir))
472 gamebombXpath = etree.FunctionNamespace('https://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format')
473 gamebombXpath.prefix = 'gamebombXpath'
474 self.buildFuncDict()
475 for key in list(self.FuncDict.keys()):
476 gamebombXpath[key] = self.FuncDict[key]
477 items = gameXslt(videoResult)
478
479 if items.getroot() is not None:
480 if len(items.xpath('//item')):
481 sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, ))
482 sys.exit(0)
483 # end gameData()
484
485# end Class gamedbQueries()
486
487def main():
488 """Simple example of using giantbomb_api - it just
489 searches for any Game with the word "Grand" in its title and returns a list of matches
490 in Universal XML format. Also gets game details using a GameBomb#.
491 """
492 # api.giantbomb.com api key provided for MythTV
493 api_key = "b5883a902a8ed88b15ce21d07787c94fd6ad9f33"
494 gamebomb = gamedbQueries(api_key)
495 # Output a dictionary of matching movie titles
496 gamebomb.gameSearch(u'Grand')
497 print()
498 # Output a dictionary of matching movie details for GiantBomb number '19995'
499 gamebomb.gameData(u'19995')
500# end main()
501
502if __name__ == '__main__':
503 main()
def __init__(self, outstream, encoding=None)
def htmlToString(self, context, html)
def findImages(self, context, *args)
def pubDate(self, context, *inputArgs)
def translateName(self, context, *inputArgs)
def getImages(self, context, arg)
def __init__(self, apikey, debug=False)
def futureReleaseDate(self, context, gameElement)
def getHtmlData(self, context, *args)
def supportedJobs(self, context, *inputArgs)
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)