MythTV master
tvmaze.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: UTF-8 -*-
3
4__title__ = "TVmaze.com"
5__author__ = "Roland Ernst, Steve Erlenborn"
6__version__ = "0.5.1"
7
8
9import sys
10import os
11import shlex
12from optparse import OptionParser
13
14
15def print_etree(etostr):
16 """lxml.etree.tostring is a bytes object in python3, and a str in python2.
17 """
18 sys.stdout.write(etostr.decode("utf-8"))
19
20
21def check_item(m, mitem, ignore=True):
22 # item is a tuple of (str, value)
23 # ToDo: Add this to the 'Metadata' class of MythTV's python bindings
24 try:
25 k, v = mitem
26 if v is None:
27 return None
28 m._inv_trans[m._global_type[k]](v)
29 return v
30 except:
31 if ignore:
32 return None
33 else:
34 raise
35
36
37def get_show_art_lists(tvmaze_show_id):
38 from MythTV.tvmaze import tvmaze_api as tvmaze
39
40 artlist = tvmaze.get_show_artwork(tvmaze_show_id)
41
42 #--------------------------------------------------------------------------
43 # The Main flag is true for artwork which is "official" from the Network.
44 # Under the theory that "official" artwork should be high quality, we want
45 # those artworks to be located at the beginning of the generated list.
46 #--------------------------------------------------------------------------
47 posterList = [(art_item.original, art_item.medium) for art_item in artlist \
48 if (art_item.main and (art_item.type == 'poster'))]
49 posterNorm = [(art_item.original, art_item.medium) for art_item in artlist \
50 if ((not art_item.main) and (art_item.type == 'poster'))]
51 posterList.extend(posterNorm)
52
53 fanartList = [(art_item.original, art_item.medium) for art_item in artlist \
54 if (art_item.main and (art_item.type == 'background'))]
55 fanartNorm = [(art_item.original, art_item.medium) for art_item in artlist \
56 if ((not art_item.main) and (art_item.type == 'background'))]
57 fanartList.extend(fanartNorm)
58
59 bannerList = [(art_item.original, art_item.medium) for art_item in artlist \
60 if (art_item.main and (art_item.type == 'banner'))]
61 bannerNorm = [(art_item.original, art_item.medium) for art_item in artlist \
62 if ((not art_item.main) and (art_item.type == 'banner'))]
63 bannerList.extend(bannerNorm)
64
65 return posterList, fanartList, bannerList
66
67
68def buildList(tvtitle, opts):
69 # option -M title
70 from lxml import etree
71 from MythTV import VideoMetadata
72 from MythTV.tvmaze import tvmaze_api as tvmaze
73 from MythTV.tvmaze import locales
74
75 # set the session
76 if opts.session:
77 tvmaze.set_session(opts.session)
78
79 if opts.debug:
80 print("Function 'buildList' called with argument '%s'" % tvtitle)
81
82 showlist = tvmaze.search_show(tvtitle)
83
84 if opts.debug:
85 print("tvmaze.search_show(%s) returned :" % tvtitle)
86 for l in showlist:
87 print(l, type(l))
88 for k, v in l.__dict__.items():
89 print(k, " : ", v)
90
91 tree = etree.XML(u'<metadata></metadata>')
92
93 for show_info in showlist:
94 m = VideoMetadata()
95 m.title = check_item(m, ("title", show_info.name), ignore=False)
96 m.description = check_item(m, ("description", show_info.summary))
97 m.inetref = check_item(m, ("inetref", str(show_info.id)), ignore=False)
98 m.collectionref = check_item(m, ("collectionref", str(show_info.id)), ignore=False)
99 m.language = check_item(m, ("language", str(locales.Language.getstored(show_info.language))))
100 m.userrating = check_item(m, ("userrating", show_info.rating['average']))
101 try:
102 m.popularity = check_item(m, ("popularity", float(show_info.weight)), ignore=False)
103 except (TypeError, ValueError):
104 pass
105 if show_info.premiere_date:
106 m.releasedate = check_item(m, ("releasedate", show_info.premiere_date))
107 m.year = check_item(m, ("year", show_info.premiere_date.year))
108
109 posterList, fanartList, bannerList = get_show_art_lists(show_info.id)
110
111 # Generate one image line for each type of artwork
112 if posterList:
113 posterEntry = posterList[0]
114 if (posterEntry[0] is not None) and (posterEntry[1] is not None):
115 m.images.append({'type': 'coverart', 'url': posterEntry[0], 'thumb': posterEntry[1]})
116 elif posterEntry[0] is not None:
117 m.images.append({'type': 'coverart', 'url': posterEntry[0]})
118
119 if fanartList:
120 fanartEntry = fanartList[0]
121 if (fanartEntry[0] is not None) and (fanartEntry[1] is not None):
122 m.images.append({'type': 'fanart', 'url': fanartEntry[0], 'thumb': fanartEntry[1]})
123 elif fanartEntry[0] is not None:
124 m.images.append({'type': 'fanart', 'url': fanartEntry[0]})
125
126 if bannerList:
127 bannerEntry = bannerList[0]
128 if (bannerEntry[0] is not None) and (bannerEntry[1] is not None):
129 m.images.append({'type': 'banner', 'url': bannerEntry[0], 'thumb': bannerEntry[1]})
130 elif bannerEntry[0] is not None:
131 m.images.append({'type': 'banner', 'url': bannerEntry[0]})
132
133 tree.append(m.toXML())
134
135 print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
136 xml_declaration=True))
137
138
139def buildNumbers(args, opts):
140 # either option -N <inetref> <subtitle> e.g. -N 69 "Elizabeth Keen"
141 # or option -N <inetref> <date time> e.g. -N 69 "2021-01-29 19:00:00"
142 # or option -N <title> <subtitle> e.g. -N "The Blacklist" "Elizabeth Keen"
143 # or option -N <title> <date time> e.g. -N "The Blacklist" "2021-01-29 19:00:00"
144 from MythTV.utility import levenshtein
145 from MythTV.utility.dt import posixtzinfo
146 from MythTV.tvmaze import tvmaze_api as tvmaze
147 from MythTV import datetime
148 from lxml import etree
149 from datetime import timedelta
150
151 if opts.debug:
152 print("Function 'buildNumbers' called with arguments: " +
153 (" ".join(["'%s'" % i for i in args])))
154
155 # set the session
156 if opts.session:
157 tvmaze.set_session(opts.session)
158
159 dtInLocalZone = None
160 # ToDo:
161 # below check shows a deficiency of the MythTV grabber API itself:
162 # TV-Shows or Movies with an integer as title are not recognized correctly.
163 # see https://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format
164 # and https://code.mythtv.org/trac/ticket/11850
165 try:
166 inetref = int(args[0])
167 tvsubtitle = args[1]
168 inetrefList = [inetref]
169
170 except ValueError:
171 tvtitle = args[0]
172 tvsubtitle = args[1]
173 inetrefList = [] # inetrefs for shows with title matches
174 best_show_quality = 0.5 # require at least this quality on string match
175
176 showlist = tvmaze.search_show(tvtitle)
177
178 # It's problematic to make decisions solely upon the Levenshtein distance.
179 # If the strings are really long or really short, a simple rule, such as
180 # "accept any distance < 6" can provide misleading results.
181 # To establish a more useful measurement, we'll use the Levenshtein
182 # distance to figure out the ratio (0 - 1) of matching characters in the
183 # longer string, and call this 'match_quality'.
184 # "Risk", "Call" -> distance = 4
185 # match_quality = (4 - 4) / 4 = 0
186 # "In Sickness and in Health", "Sickness and Health" -> distance = 6
187 # match_quality = (25 - 6)/25 = .76
188
189 for show_info in showlist:
190 try:
191 inetref = int(show_info.id)
192 distance = levenshtein(show_info.name.lower(), tvtitle.lower())
193 if len(tvtitle) > len(show_info.name):
194 match_quality = float(len(tvtitle) - distance) / len(tvtitle)
195 else:
196 match_quality = float(len(show_info.name) - distance) / len(show_info.name)
197 if match_quality >= best_show_quality:
198 #if opts.debug:
199 #print ('show_info =', show_info, ', match_quality =', match_quality)
200 if match_quality == best_show_quality:
201 inetrefList.append(inetref)
202 else:
203 # Any items previously appended for a lesser match need to be eliminated
204 inetrefList = [inetref]
205 best_show_quality = match_quality
206 except(TypeError, ValueError):
207 pass
208
209 # check whether the 'subtitle' is really a timestamp
210 try:
211 dtInLocalZone = datetime.strptime(tvsubtitle, "%Y-%m-%d %H:%M:%S") # defaults to local timezone
212 except ValueError:
213 dtInLocalZone = None
214
215 matchesFound = 0
216 best_ep_quality = 0.5 # require at least this quality on string match
217 tree = etree.XML(u'<metadata></metadata>')
218 for inetref in inetrefList:
219 dtInTgtZone = None
220 if dtInLocalZone:
221 try:
222 show_info = tvmaze.get_show(inetref)
223 # Some cases have 'network' = None, but webChannel != None. If we
224 # find such a case, we'll set show_network to the webChannel.
225 show_network = show_info.network
226 if show_network is None:
227 show_network = show_info.streaming_service
228 show_country = show_network.get('country')
229 # Some webChannels don't specify country or timezone
230 if show_country:
231 show_tz = show_country.get('timezone')
232 else:
233 show_tz = None
234 dtInTgtZone = dtInLocalZone.astimezone(posixtzinfo(show_tz))
235
236 except (ValueError, AttributeError) as e:
237 if opts.debug:
238 print('show_tz =%s, except = %s' % (show_tz, e))
239 dtInTgtZone = None
240
241 if dtInTgtZone:
242 # get episode info based on inetref and datetime in target zone
243 try:
244 #print('get_show_episodes_by_date(', inetref, ',', dtInTgtZone, ')')
245 episodes = tvmaze.get_show_episodes_by_date(inetref, dtInTgtZone)
246 except SystemExit:
247 episodes = []
248 time_match_list = []
249 early_match_list = []
250 minTimeDelta = timedelta(minutes=60)
251 for i, ep in enumerate(episodes):
252 if ep.timestamp:
253 epInTgtZone = datetime.fromIso(ep.timestamp, tz = posixtzinfo(show_tz))
254 if ep.duration:
255 durationDelta = timedelta(minutes=ep.duration)
256 else:
257 durationDelta = timedelta(minutes=0)
258
259 if epInTgtZone == dtInTgtZone:
260 if opts.debug:
261 print('Recording matches \'%s\' at %s' % (ep, epInTgtZone))
262 time_match_list.append(i)
263 minTimeDelta = timedelta(minutes=0)
264 # Consider it a match if the recording starts late,
265 # but within the duration of the show.
266 elif epInTgtZone < dtInTgtZone < epInTgtZone+durationDelta:
267 # Recording start time is within the range of this episode
268 if opts.debug:
269 print('Recording in range of \'%s\' (%s ... %s)' \
270 % (ep, epInTgtZone, epInTgtZone+durationDelta))
271 time_match_list.append(i)
272 minTimeDelta = timedelta(minutes=0)
273 # Consider it a match if the recording is a little bit early. This helps cases
274 # where you set up a rule to record, at say 9:00, and the broadcaster uses a
275 # slightly odd start time, like 9:05.
276 elif epInTgtZone-minTimeDelta <= dtInTgtZone < epInTgtZone:
277 # Recording started earlier than this episode, so see if it's the closest match
278 if epInTgtZone - dtInTgtZone == minTimeDelta:
279 if opts.debug:
280 print('Adding episode \'%s\' to closest list. Offset = %s' \
281 % (ep, epInTgtZone - dtInTgtZone))
282 early_match_list.append(i)
283 elif epInTgtZone - dtInTgtZone < minTimeDelta:
284 if opts.debug:
285 print('Episode \'%s\' is new closest. Offset = %s' \
286 % (ep, epInTgtZone - dtInTgtZone))
287 minTimeDelta = epInTgtZone - dtInTgtZone
288 early_match_list = [i]
289
290 if not time_match_list:
291 # No exact matches found, so use the list of the closest episode(s)
292 time_match_list = early_match_list
293
294 if time_match_list:
295 for ep_index in time_match_list:
296 season_nr = str(episodes[ep_index].season)
297 episode_id = episodes[ep_index].id
298 item = buildSingleItem(inetref, season_nr, episode_id)
299 if item is not None:
300 tree.append(item.toXML())
301 matchesFound += 1
302 else:
303 # get episode based on subtitle
304 episodes = tvmaze.get_show_episode_list(inetref)
305
306 min_dist_list = []
307 for i, ep in enumerate(episodes):
308 if 0 and opts.debug:
309 print("tvmaze.get_show_episode_list(%s) returned :" % inetref)
310 for k, v in ep.__dict__.items():
311 print(k, " : ", v)
312 distance = levenshtein(ep.name, tvsubtitle)
313 if len(tvsubtitle) >= len(ep.name):
314 match_quality = float(len(tvsubtitle) - distance) / len(tvsubtitle)
315 else:
316 match_quality = float(len(ep.name) - distance) / len(ep.name)
317 #if opts.debug:
318 #print('inetref', inetref, 'episode =', ep.name, ', distance =', distance, ', match_quality =', match_quality)
319 if match_quality >= best_ep_quality:
320 if match_quality == best_ep_quality:
321 min_dist_list.append(i)
322 if opts.debug:
323 print('"%s" added to best list, match_quality = %g' % (ep.name, match_quality))
324 else:
325 # Any items previously appended for a lesser match need to be eliminated
326 tree = etree.XML(u'<metadata></metadata>')
327 min_dist_list = [i]
328 best_ep_quality = match_quality
329 if opts.debug:
330 print('"%s" is new best match_quality = %g' % (ep.name, match_quality))
331
332 # The list is constructed in order of oldest season to newest.
333 # If episodes with equivalent match quality show up in multiple
334 # seasons, we want to list the most recent first. To accomplish
335 # this, we'll process items starting at the end of the list, and
336 # proceed to the beginning.
337 while min_dist_list:
338 ep_index = min_dist_list.pop()
339 season_nr = str(episodes[ep_index].season)
340 episode_id = episodes[ep_index].id
341 if opts.debug:
342 episode_nr = str(episodes[ep_index].number)
343 print("tvmaze.get_show_episode_list(%s) returned :" % inetref)
344 print("with season : %s and episode %s" % (season_nr, episode_nr))
345 print("Chosen episode index '%d' based on match quality %g"
346 % (ep_index, best_ep_quality))
347
348 # we have now inetref, season, episode_id
349 item = buildSingleItem(inetref, season_nr, episode_id)
350 if item is not None:
351 tree.append(item.toXML())
352 matchesFound += 1
353
354 if matchesFound > 0:
355 print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
356 xml_declaration=True))
357 else:
358 if dtInLocalZone:
359 raise Exception("Cannot find any episode with timestamp matching '%s'." % tvsubtitle)
360 else:
361 # tvmaze.py -N 4711 "Episode 42"
362 raise Exception("Cannot find any episode with subtitle '%s'." % tvsubtitle)
363
364
365def buildSingle(args, opts, tvmaze_episode_id=None):
366 """
367 The tvmaze api returns different id's for season, episode and series.
368 MythTV stores only the series-id, therefore we need to fetch the correct id's
369 for season and episode for that series-id.
370 """
371 # option -D inetref season episode
372
373 from lxml import etree
374 from MythTV.tvmaze import tvmaze_api as tvmaze
375
376 if opts.debug:
377 dstr = "Function 'buildSingle' called with arguments: " + \
378 (" ".join(["'%s'" % i for i in args]))
379 if tvmaze_episode_id is not None:
380 dstr += " tvmaze_episode_id = %d" % tvmaze_episode_id
381 print(dstr)
382 inetref = args[0]
383 season = args[1]
384 episode = args[2]
385
386 # set the session
387 if opts.session:
388 tvmaze.set_session(opts.session)
389
390 # get the episode_id if not provided:
391 if tvmaze_episode_id is None:
392 episodes = tvmaze.get_show_episode_list(inetref)
393 for ep in (episodes):
394 if 0 and opts.debug:
395 print("tvmaze.get_show_episode_list(%s) returned :" % inetref)
396 for k, v in ep.__dict__.items():
397 print(k, " : ", v)
398 if ep.season == int(season) and ep.number == int(episode):
399 tvmaze_episode_id = ep.id
400 if opts.debug:
401 print(" Found tvmaze_episode_id : %d" % tvmaze_episode_id)
402 break
403
404 # build xml:
405 tree = etree.XML(u'<metadata></metadata>')
406 item = buildSingleItem(inetref, season, tvmaze_episode_id)
407 if item is not None:
408 tree.append(item.toXML())
409 print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
410 xml_declaration=True))
411
412
413def buildSingleItem(inetref, season, episode_id):
414 """
415 This routine returns a video metadata item for one episode.
416 """
417 from MythTV import VideoMetadata
418 from MythTV.tvmaze import tvmaze_api as tvmaze
419 from MythTV.tvmaze import locales
420
421 # get global info for all seasons/episodes:
422 posterList, fanartList, bannerList = get_show_art_lists(inetref)
423 show_info = tvmaze.get_show(inetref, populated=True)
424
425 # get info for season episodes:
426 ep_info = tvmaze.get_episode_information(episode_id)
427 m = VideoMetadata()
428 if show_info.genres is not None and len(show_info.genres) > 0:
429 for g in show_info.genres:
430 try:
431 if g is not None and len(g) > 0:
432 m.categories.append(g)
433 except:
434 pass
435 m.title = check_item(m, ("title", show_info.name), ignore=False)
436 m.subtitle = check_item(m, ("title", ep_info.name), ignore=False)
437 m.season = check_item(m, ("season", ep_info.season), ignore=False)
438 m.episode = check_item(m, ("episode", ep_info.number), ignore=False)
439 m.description = check_item(m, ("description", ep_info.summary))
440 if m.description is None:
441 m.description = check_item(m, ("description", show_info.summary))
442 try:
443 sinfo = show_info.network['name']
444 if sinfo is not None and len(sinfo) > 0:
445 m.studios.append(sinfo)
446 except:
447 pass
448 m.inetref = check_item(m, ("inetref", str(show_info.id)), ignore=False)
449 m.collectionref = check_item(m, ("inetref", str(show_info.id)), ignore=False)
450 m.language = check_item(m, ("language", str(locales.Language.getstored(show_info.language))))
451 m.userrating = check_item(m, ("userrating", show_info.rating['average']))
452 try:
453 m.popularity = check_item(m, ("popularity", float(show_info.weight)), ignore=False)
454 except (TypeError, ValueError):
455 pass
456 # prefer episode airdate dates:
457 if ep_info.airdate:
458 m.releasedate = check_item(m, ("releasedate", ep_info.airdate))
459 m.year = check_item(m, ("year", ep_info.airdate.year))
460 elif show_info.premiere_date:
461 m.releasedate = check_item(m, ("releasedate", show_info.premiere_date))
462 m.year = check_item(m, ("year", show_info.premiere_date.year))
463 if ep_info.duration:
464 m.runtime = check_item(m, ("runtime", int(ep_info.duration)))
465
466 for actor in show_info.cast:
467 try:
468 if len(actor.person.name) > 0 and len(actor.name) > 0:
469 d = {'name': actor.person.name, 'character': actor.name, 'job': 'Actor'}
470
471 m.people.append(d)
472 except:
473 pass
474
475 for member in show_info.crew:
476 try:
477 if len(member.name) > 0 and len(member.job) > 0:
478 d = {'name': member.name, 'job': member.job}
479 m.people.append(d)
480 except:
481 pass
482
483 # get info for dedicated season:
484 season_info = show_info.seasons[int(season)]
485 #for k, v in season_info.__dict__.items():
486 #print(k, " : ", v)
487
488 # prefer season coverarts over series coverart:
489 if season_info.images is not None and len(season_info.images) > 0:
490 m.images.append({'type': 'coverart', 'url': season_info.images['original'],
491 'thumb': season_info.images['medium']})
492
493 # generate series coverart, fanart, and banners
494 for posterEntry in posterList:
495 if (posterEntry[0] is not None) and (posterEntry[1] is not None):
496 image_entry = {'type': 'coverart', 'url': posterEntry[0], 'thumb': posterEntry[1]}
497 elif posterEntry[0] is not None:
498 image_entry = {'type': 'coverart', 'url': posterEntry[0]}
499 # Avoid duplicate coverart entries
500 if image_entry not in m.images:
501 m.images.append(image_entry)
502
503 for fanartEntry in fanartList:
504 if (fanartEntry[0] is not None) and (fanartEntry[1] is not None):
505 m.images.append({'type': 'fanart', 'url': fanartEntry[0], 'thumb': fanartEntry[1]})
506 elif fanartEntry[0] is not None:
507 m.images.append({'type': 'fanart', 'url': fanartEntry[0]})
508
509 for bannerEntry in bannerList:
510 if (bannerEntry[0] is not None) and (bannerEntry[1] is not None):
511 m.images.append({'type': 'banner', 'url': bannerEntry[0], 'thumb': bannerEntry[1]})
512 elif bannerEntry[0] is not None:
513 m.images.append({'type': 'banner', 'url': bannerEntry[0]})
514
515 # screenshot is associated to episode
516 if ep_info.images is not None and len(ep_info.images) > 0:
517 m.images.append({'type': 'screenshot', 'url': ep_info.images['original'],
518 'thumb': ep_info.images['medium']})
519 return m
520
521
522def buildCollection(tvinetref, opts):
523 # option -C inetref
524 from lxml import etree
525 from MythTV import VideoMetadata
526 from MythTV.tvmaze import tvmaze_api as tvmaze
527 from MythTV.tvmaze import locales
528
529 # set the session
530 if opts.session:
531 tvmaze.set_session(opts.session)
532
533 if opts.debug:
534 print("Function 'buildCollection' called with argument '%s'" % tvinetref)
535
536 show_info = tvmaze.get_show(tvinetref)
537 if opts.debug:
538 for k, v in show_info.__dict__.items():
539 print(k, " : ", v)
540
541 tree = etree.XML(u'<metadata></metadata>')
542 m = VideoMetadata()
543 m.title = check_item(m, ("title", show_info.name), ignore=False)
544 m.description = check_item(m, ("description", show_info.summary))
545 if show_info.genres is not None and len(show_info.genres) > 0:
546 for g in show_info.genres:
547 try:
548 if g is not None and len(g) > 0:
549 m.categories.append(g)
550 except:
551 pass
552 m.inetref = check_item(m, ("inetref", str(show_info.id)), ignore=False)
553 m.collectionref = check_item(m, ("collectionref", str(show_info.id)), ignore=False)
554 m.imdb = check_item(m, ("imdb", str(show_info.external_ids['imdb'])))
555 m.language = check_item(m, ("language", str(locales.Language.getstored(show_info.language))))
556 m.userrating = check_item(m, ("userrating", show_info.rating['average']))
557 m.runtime = check_item(m, ("runtime", show_info.runtime))
558 try:
559 m.popularity = check_item(m, ("popularity", float(show_info.weight)), ignore=False)
560 except (TypeError, ValueError):
561 pass
562 if show_info.premiere_date:
563 m.releasedate = check_item(m, ("releasedate", show_info.premiere_date))
564 m.year = check_item(m, ("year", show_info.premiere_date.year))
565 try:
566 sinfo = show_info.network['name']
567 if sinfo is not None and len(sinfo) > 0:
568 m.studios.append(sinfo)
569 except:
570 pass
571
572 posterList, fanartList, bannerList = get_show_art_lists(show_info.id)
573
574 # Generate image lines for every piece of artwork
575 for posterEntry in posterList:
576 if (posterEntry[0] is not None) and (posterEntry[1] is not None):
577 m.images.append({'type': 'coverart', 'url': posterEntry[0], 'thumb': posterEntry[1]})
578 elif posterEntry[0] is not None:
579 m.images.append({'type': 'coverart', 'url': posterEntry[0]})
580
581 for fanartEntry in fanartList:
582 if (fanartEntry[0] is not None) and (fanartEntry[1] is not None):
583 m.images.append({'type': 'fanart', 'url': fanartEntry[0], 'thumb': fanartEntry[1]})
584 elif fanartEntry[0] is not None:
585 m.images.append({'type': 'fanart', 'url': fanartEntry[0]})
586
587 for bannerEntry in bannerList:
588 if (bannerEntry[0] is not None) and (bannerEntry[1] is not None):
589 m.images.append({'type': 'banner', 'url': bannerEntry[0], 'thumb': bannerEntry[1]})
590 elif bannerEntry[0] is not None:
591 m.images.append({'type': 'banner', 'url': bannerEntry[0]})
592
593 tree.append(m.toXML())
594
595 print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True,
596 xml_declaration=True))
597
598
600 from lxml import etree
601 version = etree.XML(u'<grabber></grabber>')
602 etree.SubElement(version, "name").text = __title__
603 etree.SubElement(version, "author").text = __author__
604 etree.SubElement(version, "thumbnail").text = 'tvmaze.png'
605 etree.SubElement(version, "command").text = 'tvmaze.py'
606 etree.SubElement(version, "type").text = 'television'
607 etree.SubElement(version, "description").text = \
608 'Search and metadata downloads for tvmaze.com'
609 etree.SubElement(version, "version").text = __version__
610 print_etree(etree.tostring(version, encoding='UTF-8', pretty_print=True,
611 xml_declaration=True))
612 sys.exit(0)
613
614
616 err = 0
617 try:
618 import lxml
619 except:
620 err = 1
621 print("Failed to import python lxml library.")
622 try:
623 import requests
624 import requests_cache
625 except:
626 err = 1
627 print("Failed to import python-requests or python-request-cache library.")
628 try:
629 import MythTV
630 except:
631 err = 1
632 print("Failed to import MythTV bindings. Check your `configure` output "
633 "to make sure installation was not disabled due to external "
634 "dependencies")
635 try:
636 from MythTV.tvmaze import tvmaze_api as tvmaze
637 if opts.debug:
638 print("File location: ", tvmaze.__file__)
639 print("TVMAZE Script Version: ", __version__)
640 print("TVMAZE-API version: ", tvmaze.MYTHTV_TVMAZE_API_VERSION)
641 except:
642 err = 1
643 print("Failed to import PyTVmaze library. This should have been included "
644 "with the python MythTV bindings.")
645 if not err:
646 print("Everything appears in order.")
647 sys.exit(err)
648
649
650def main():
651 """
652 Main executor for MythTV's tvmaze grabber.
653 """
654
655 parser = OptionParser()
656
657 parser.add_option('-v', "--version", action="store_true", default=False,
658 dest="version", help="Display version and author")
659 parser.add_option('-t', "--test", action="store_true", default=False,
660 dest="test", help="Perform self-test for dependencies.")
661 parser.add_option('-M', "--list", action="store_true", default=False,
662 dest="tvlist", help="Get TV Shows matching search.")
663 parser.add_option('-D', "--data", action="store_true", default=False,
664 dest="tvdata", help="Get TV Show data.")
665 parser.add_option('-C', "--collection", action="store_true", default=False,
666 dest="collectiondata", help="Get Collection data.")
667 parser.add_option('-N', "--numbers", action="store_true", default=False,
668 dest="tvnumbers", help="Get Season and Episode numbers")
669 parser.add_option('-l', "--language", metavar="LANGUAGE", default=u'en',
670 dest="language", help="Specify language for filtering.")
671 parser.add_option('-a', "--area", metavar="COUNTRY", default=None,
672 dest="country", help="Specify country for custom data.")
673 parser.add_option('--debug', action="store_true", default=False,
674 dest="debug", help=("Disable caching and enable raw "
675 "data output."))
676 parser.add_option('--doctest', action="store_true", default=False,
677 dest="doctest", help="Run doctests")
678
679 opts, args = parser.parse_args()
680
681 if opts.debug:
682 print("Args: ", args)
683 print("Opts: ", opts)
684
685 if opts.doctest:
686 import doctest
687 try:
688 with open("tvmaze_tests.txt") as f:
689 dtests = "".join(f.readlines())
690 main.__doc__ += dtests
691 except IOError:
692 pass
693 # perhaps try optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
694 doctest.testmod(verbose=opts.debug, optionflags=doctest.ELLIPSIS)
695
696 if opts.version:
698
699 if opts.test:
700 performSelfTest(opts)
701
702 if opts.debug:
703 import requests
704 else:
705 confdir = os.environ.get('MYTHCONFDIR', '')
706 if (not confdir) or (confdir == '/'):
707 confdir = os.environ.get('HOME', '')
708 if (not confdir) or (confdir == '/'):
709 print("Unable to find MythTV directory for metadata cache.")
710 sys.exit(1)
711 confdir = os.path.join(confdir, '.mythtv')
712 cachedir = os.path.join(confdir, 'cache')
713 if not os.path.exists(cachedir):
714 os.makedirs(cachedir)
715 cache_name = os.path.join(cachedir, 'py3tvmaze')
716 import requests
717 import requests_cache
718 requests_cache.install_cache(cache_name, backend='sqlite', expire_after=3600)
719
720 with requests.Session() as s:
721 s.headers.update({'Accept': 'application/json',
722 'User-Agent': 'mythtv tvmaze grabber %s' % __version__})
723 opts.session = s
724 try:
725 if opts.tvlist:
726 # option -M title
727 if (len(args) != 1) or (len(args[0]) == 0):
728 sys.stdout.write('ERROR: tvmaze -M requires exactly one non-empty argument')
729 sys.exit(1)
730 buildList(args[0], opts)
731
732 if opts.tvnumbers:
733 # either option -N inetref subtitle
734 # or option -N title subtitle
735 if (len(args) != 2) or (len(args[0]) == 0) or (len(args[1]) == 0):
736 sys.stdout.write('ERROR: tvmaze -N requires exactly two non-empty arguments')
737 sys.exit(1)
738 buildNumbers(args, opts)
739
740 if opts.tvdata:
741 # option -D inetref season episode
742 if (len(args) != 3) or (len(args[0]) == 0) or (len(args[1]) == 0) or (len(args[2]) == 0):
743 sys.stdout.write('ERROR: tvmaze -D requires exactly three non-empty arguments')
744 sys.exit(1)
745 buildSingle(args, opts)
746
747 if opts.collectiondata:
748 # option -C inetref
749 if (len(args) != 1) or (len(args[0]) == 0):
750 sys.stdout.write('ERROR: tvmaze -C requires exactly one non-empty argument')
751 sys.exit(1)
752 buildCollection(args[0], opts)
753 except:
754 if opts.debug:
755 raise
756 sys.stdout.write('ERROR: ' + str(sys.exc_info()[0]) + ' : ' + str(sys.exc_info()[1]) + '\n')
757 sys.exit(1)
758
759
760if __name__ == "__main__":
761 main()
def buildVersion()
Definition: tvmaze.py:599
def main()
Definition: tvmaze.py:650
def print_etree(etostr)
Definition: tvmaze.py:15
def get_show_art_lists(tvmaze_show_id)
Definition: tvmaze.py:37
def buildNumbers(args, opts)
Definition: tvmaze.py:139
def buildCollection(tvinetref, opts)
Definition: tvmaze.py:522
def buildList(tvtitle, opts)
Definition: tvmaze.py:68
def check_item(m, mitem, ignore=True)
Definition: tvmaze.py:21
def buildSingleItem(inetref, season, episode_id)
Definition: tvmaze.py:413
def performSelfTest(opts)
Definition: tvmaze.py:615
def buildSingle(args, opts, tvmaze_episode_id=None)
Definition: tvmaze.py:365
static void print(const QList< uint > &raw_minimas, const QList< uint > &raw_maximas, const QList< float > &minimas, const QList< float > &maximas)