4 __title__ =
"TVmaze.com"
5 __author__ =
"Roland Ernst, Steve Erlenborn"
12 from optparse
import OptionParser
16 """lxml.etree.tostring is a bytes object in python3, and a str in python2.
18 sys.stdout.write(etostr.decode(
"utf-8"))
28 m._inv_trans[m._global_type[k]](v)
38 from MythTV.tvmaze
import tvmaze_api
as tvmaze
40 artlist = tvmaze.get_show_artwork(tvmaze_show_id)
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)
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)
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)
65 return posterList, fanartList, bannerList
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
77 tvmaze.set_session(opts.session)
80 print(
"Function 'buildList' called with argument '%s'" % tvtitle)
82 showlist = tvmaze.search_show(tvtitle)
85 print(
"tvmaze.search_show(%s) returned :" % tvtitle)
88 for k, v
in l.__dict__.items():
91 tree = etree.XML(
u'<metadata></metadata>')
93 for show_info
in showlist:
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']))
102 m.popularity =
check_item(m, (
"popularity", float(show_info.weight)), ignore=
False)
103 except (TypeError, ValueError):
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))
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]})
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]})
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]})
133 tree.append(m.toXML())
135 print_etree(etree.tostring(tree, encoding=
'UTF-8', pretty_print=
True,
136 xml_declaration=
True))
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
152 print(
"Function 'buildNumbers' called with arguments: " +
153 (
" ".join([
"'%s'" % i
for i
in args])))
157 tvmaze.set_session(opts.session)
166 inetref = int(args[0])
168 inetrefList = [inetref]
174 best_show_quality = 0.5
176 showlist = tvmaze.search_show(tvtitle)
189 for show_info
in showlist:
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)
196 match_quality = float(len(show_info.name) - distance) / len(show_info.name)
197 if match_quality >= best_show_quality:
200 if match_quality == best_show_quality:
201 inetrefList.append(inetref)
204 inetrefList = [inetref]
205 best_show_quality = match_quality
206 except(TypeError, ValueError):
211 dtInLocalZone = datetime.strptime(tvsubtitle,
"%Y-%m-%d %H:%M:%S")
216 best_ep_quality = 0.5
217 tree = etree.XML(
u'<metadata></metadata>')
218 for inetref
in inetrefList:
222 show_info = tvmaze.get_show(inetref)
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')
231 show_tz = show_country.get(
'timezone')
234 dtInTgtZone = dtInLocalZone.astimezone(posixtzinfo(show_tz))
236 except (ValueError, AttributeError)
as e:
238 print(
'show_tz =%s, except = %s' % (show_tz, e))
245 episodes = tvmaze.get_show_episodes_by_date(inetref, dtInTgtZone)
249 early_match_list = []
250 minTimeDelta = timedelta(minutes=60)
251 for i, ep
in enumerate(episodes):
253 epInTgtZone = datetime.fromIso(ep.timestamp, tz = posixtzinfo(show_tz))
255 durationDelta = timedelta(minutes=ep.duration)
257 durationDelta = timedelta(minutes=0)
259 if epInTgtZone == dtInTgtZone:
261 print(
'Recording matches \'%s\' at %s' % (ep, epInTgtZone))
262 time_match_list.append(i)
263 minTimeDelta = timedelta(minutes=0)
266 elif epInTgtZone < dtInTgtZone < epInTgtZone+durationDelta:
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)
276 elif epInTgtZone-minTimeDelta <= dtInTgtZone < epInTgtZone:
278 if epInTgtZone - dtInTgtZone == minTimeDelta:
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:
285 print(
'Episode \'%s\' is new closest. Offset = %s' \
286 % (ep, epInTgtZone - dtInTgtZone))
287 minTimeDelta = epInTgtZone - dtInTgtZone
288 early_match_list = [i]
290 if not time_match_list:
292 time_match_list = early_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
300 tree.append(item.toXML())
304 episodes = tvmaze.get_show_episode_list(inetref)
307 for i, ep
in enumerate(episodes):
309 print(
"tvmaze.get_show_episode_list(%s) returned :" % inetref)
310 for k, v
in ep.__dict__.items():
312 distance = levenshtein(ep.name, tvsubtitle)
313 if len(tvsubtitle) >= len(ep.name):
314 match_quality = float(len(tvsubtitle) - distance) / len(tvsubtitle)
316 match_quality = float(len(ep.name) - distance) / len(ep.name)
319 if match_quality >= best_ep_quality:
320 if match_quality == best_ep_quality:
321 min_dist_list.append(i)
323 print(
'"%s" added to best list, match_quality = %g' % (ep.name, match_quality))
326 tree = etree.XML(
u'<metadata></metadata>')
328 best_ep_quality = match_quality
330 print(
'"%s" is new best match_quality = %g' % (ep.name, match_quality))
338 ep_index = min_dist_list.pop()
339 season_nr = str(episodes[ep_index].season)
340 episode_id = episodes[ep_index].id
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))
351 tree.append(item.toXML())
355 print_etree(etree.tostring(tree, encoding=
'UTF-8', pretty_print=
True,
356 xml_declaration=
True))
359 raise Exception(
"Cannot find any episode with timestamp matching '%s'." % tvsubtitle)
362 raise Exception(
"Cannot find any episode with subtitle '%s'." % tvsubtitle)
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.
373 from lxml
import etree
374 from MythTV.tvmaze
import tvmaze_api
as tvmaze
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
388 tvmaze.set_session(opts.session)
391 if tvmaze_episode_id
is None:
392 episodes = tvmaze.get_show_episode_list(inetref)
393 for ep
in (episodes):
395 print(
"tvmaze.get_show_episode_list(%s) returned :" % inetref)
396 for k, v
in ep.__dict__.items():
398 if ep.season == int(season)
and ep.number == int(episode):
399 tvmaze_episode_id = ep.id
401 print(
" Found tvmaze_episode_id : %d" % tvmaze_episode_id)
405 tree = etree.XML(
u'<metadata></metadata>')
408 tree.append(item.toXML())
409 print_etree(etree.tostring(tree, encoding=
'UTF-8', pretty_print=
True,
410 xml_declaration=
True))
415 This routine returns a video metadata item for one episode.
417 from MythTV
import VideoMetadata
418 from MythTV.tvmaze
import tvmaze_api
as tvmaze
419 from MythTV.tvmaze
import locales
423 show_info = tvmaze.get_show(inetref, populated=
True)
426 ep_info = tvmaze.get_episode_information(episode_id)
428 if show_info.genres
is not None and len(show_info.genres) > 0:
429 for g
in show_info.genres:
431 if g
is not None and len(g) > 0:
432 m.categories.append(g)
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))
443 sinfo = show_info.network[
'name']
444 if sinfo
is not None and len(sinfo) > 0:
445 m.studios.append(sinfo)
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']))
453 m.popularity =
check_item(m, (
"popularity", float(show_info.weight)), ignore=
False)
454 except (TypeError, ValueError):
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))
464 m.runtime =
check_item(m, (
"runtime", int(ep_info.duration)))
466 for actor
in show_info.cast:
468 if len(actor.person.name) > 0
and len(actor.name) > 0:
469 d = {
'name': actor.person.name,
'character': actor.name,
'job':
'Actor'}
475 for member
in show_info.crew:
477 if len(member.name) > 0
and len(member.job) > 0:
478 d = {
'name': member.name,
'job': member.job}
484 season_info = show_info.seasons[int(season)]
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']})
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]}
500 if image_entry
not in m.images:
501 m.images.append(image_entry)
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]})
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]})
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']})
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
531 tvmaze.set_session(opts.session)
534 print(
"Function 'buildCollection' called with argument '%s'" % tvinetref)
536 show_info = tvmaze.get_show(tvinetref)
538 for k, v
in show_info.__dict__.items():
541 tree = etree.XML(
u'<metadata></metadata>')
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:
548 if g
is not None and len(g) > 0:
549 m.categories.append(g)
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))
559 m.popularity =
check_item(m, (
"popularity", float(show_info.weight)), ignore=
False)
560 except (TypeError, ValueError):
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))
566 sinfo = show_info.network[
'name']
567 if sinfo
is not None and len(sinfo) > 0:
568 m.studios.append(sinfo)
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]})
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]})
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]})
593 tree.append(m.toXML())
595 print_etree(etree.tostring(tree, encoding=
'UTF-8', pretty_print=
True,
596 xml_declaration=
True))
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))
621 print(
"Failed to import python lxml library.")
624 import requests_cache
627 print(
"Failed to import python-requests or python-request-cache library.")
632 print(
"Failed to import MythTV bindings. Check your `configure` output "
633 "to make sure installation was not disabled due to external "
636 from MythTV.tvmaze
import tvmaze_api
as tvmaze
638 print(
"File location: ", tvmaze.__file__)
639 print(
"TVMAZE Script Version: ", __version__)
640 print(
"TVMAZE-API version: ", tvmaze.MYTHTV_TVMAZE_API_VERSION)
643 print(
"Failed to import PyTVmaze library. This should have been included "
644 "with the python MythTV bindings.")
646 print(
"Everything appears in order.")
652 Main executor for MythTV's tvmaze grabber.
655 parser = OptionParser()
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 "
676 parser.add_option(
'--doctest', action=
"store_true", default=
False,
677 dest=
"doctest", help=
"Run doctests")
679 opts, args = parser.parse_args()
682 print(
"Args: ", args)
683 print(
"Opts: ", opts)
688 with open(
"tvmaze_tests.txt")
as f:
689 dtests =
"".join(f.readlines())
690 main.__doc__ += dtests
694 doctest.testmod(verbose=opts.debug, optionflags=doctest.ELLIPSIS)
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.")
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')
717 import requests_cache
718 requests_cache.install_cache(cache_name, backend=
'sqlite', expire_after=3600)
720 with requests.Session()
as s:
721 s.headers.update({
'Accept':
'application/json',
722 'User-Agent':
'mythtv tvmaze grabber %s' % __version__})
727 if (len(args) != 1)
or (len(args[0]) == 0):
728 sys.stdout.write(
'ERROR: tvmaze -M requires exactly one non-empty argument')
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')
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')
747 if opts.collectiondata:
749 if (len(args) != 1)
or (len(args[0]) == 0):
750 sys.stdout.write(
'ERROR: tvmaze -C requires exactly one non-empty argument')
756 sys.stdout.write(
'ERROR: ' + str(sys.exc_info()[0]) +
' : ' + str(sys.exc_info()[1]) +
'\n')
760 if __name__ ==
"__main__":