571 changes: 571 additions & 0 deletions mythtv/bindings/python/tvmaze/locales.py

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions mythtv/bindings/python/tvmaze/person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: UTF-8 -*-

# Copyright (c) 2020 Lachlan Mackenzie
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# ---------------------------------------------------
# Roland Ernst
# Changes implemented for MythTV:
# - added method utils.convert_date
#
# ---------------------------------------------------


from __future__ import unicode_literals

from . import utils


class Person(object):
def __init__(self, data):
self.id = data.get('id')
self.url = data.get('url')
self.name = data.get('name')
self.country = data.get('country')
self.birthday = utils.convert_date(data.get('birthday'))
self.deathday = utils.convert_date(data.get('deathday'))
self.gender = data.get('gender')
self.images = data.get('image')
self.links = data.get('_links')

def __str__(self):
return self.name


class Character(object):
def __init__(self, data, person):
self.id = data.get('id')
self.url = data.get('url')
self.name = data.get('name')
self.images = data.get('image')
self.links = data.get('_links')
# self.self = data.get('self')
# self.voice = data.get('voice')
self.person = Person(person)

def __str__(self):
# return self.name + ': ' + self.person
return self.name + ': ' + self.person.name


class Crew(Person):
def __init__(self, data):
super(Crew, self).__init__(data.get('person'))
self.job = data.get('type')

def __str__(self):
return self.job + ': ' + str(super())
56 changes: 56 additions & 0 deletions mythtv/bindings/python/tvmaze/season.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -*- coding: UTF-8 -*-

# Copyright (c) 2020 Lachlan Mackenzie
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# ---------------------------------------------------
# Roland Ernst
# Changes implemented for MythTV:
# - added method utils.convert_date
#
# ---------------------------------------------------


from __future__ import unicode_literals

import string
from . import utils


class Season(object):
def __init__(self, data, special=False):
self.id = data.get('id')
self.url = data.get('url')
self.number = data.get('number')
self.name = data.get('name')
self.num_episodes = data.get('episodeOrder')
self.episodes = {}
self.premiere_date = utils.convert_date(data.get('premiereDate'))
self.end_date = data.get('endDate')
self.network = data.get('network')
self.streaming_service = data.get('webChannel')
self.images = data.get('image')
self.summary = ""
self.summary = utils.strip_tags(data.get('summary'))
self.links = data.get('_links')

def __str__(self):
return string.capwords(' '.join(self.url.split('/')[-1].split('-')))
130 changes: 130 additions & 0 deletions mythtv/bindings/python/tvmaze/show.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# -*- coding: UTF-8 -*-

# Copyright (c) 2020 Lachlan Mackenzie
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# ---------------------------------------------------
# Roland Ernst
# Changes implemented for MythTV:
# - added method utils.convert_date
# - added embedded 'akas' and 'images'
#
# ---------------------------------------------------


from __future__ import unicode_literals

from . import utils
from .season import Season
from .episode import Episode
from .person import Crew, Character


class Show(object):
def __init__(self, data):
self.score = data.get('score') if 'score' in data else 100
show = data.get('show') if 'show' in data else data
self.id = show.get('id')
self.name = show.get('name')
self.url = show.get('url')
self.type = show.get('type')
self.language = show.get('language')
self.genres = show.get('genres')
self.status = show.get('status')
self.num_episodes = show.get('runtime')
self.seasons = {}
self._episode_list = []
self.specials = {}
self.cast = []
self.crew = []
self.akas = []
self.series_images = []
self._handle_embedded(data.get('_embedded'))
self.premiere_date = utils.convert_date(show.get('premiered'))
self.official_site = show.get('officialSite')
self.schedule = show.get('schedule')
self.rating = show.get('rating')
self.weight = show.get('weight')
self.network = show.get('network')
self.streaming_service = show.get('webChannel')
self.external_ids = show.get('externals')
self.images = show.get('image')
self.summary = utils.strip_tags(show.get('summary'))
self.links = show.get('_links')

def _handle_embedded(self, embedded):
if embedded is None:
return

special_num = 1
if 'seasons' in embedded:
seasons = [Season(season) for season in embedded.get('seasons')]
for season in seasons:
self.seasons[season.number] = season

if 'seasons' in embedded and 'episodes' in embedded:
episodes = [Episode(episode) for episode in embedded.get('episodes')]
for episode in episodes:
if not episode.special:
self.seasons[episode.season].episodes[episode.number] = episode
else:
episode.season = 0
episode.number = special_num
special_num += 1
self.specials[episode.id] = episode

if 'seasons' not in embedded and 'episodes' in embedded:
episodes = [Episode(episode) for episode in embedded.get('episodes')]
for episode in episodes:
if not episode.special:
self._episode_list.append(episode)
else:
episode.season = 0
episode.number = special_num
special_num += 1
self.specials[episode.id] = episode

self.cast = [Character(c.get('character'), c.get('person')) for c in embedded.get('cast')] if 'cast' in embedded else []
self.crew = [Crew(c) for c in embedded.get('crew')] if 'crew' in embedded else []

if 'akas' in embedded:
self.akas = [Alias(aka) for aka in embedded.get('akas')]

if 'images' in embedded:
self.series_images = [i for i in embedded.get('images')]

def __str__(self):
return str(self.id) + ': ' + self.name


class Alias:
def __init__(self, data):
self.name = data.get('name')
if data['country'] is not None:
self.country = data.get('country')
else:
self.country = {}
self.country['name'] = 'Original Country'
self.country['code'] = 'OG'
self.country['timezome'] = 'Original Country Timezone'

def __str__(self):
return self.country.get('name') + ': ' + self.name
216 changes: 216 additions & 0 deletions mythtv/bindings/python/tvmaze/tvmaze_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# -*- coding: UTF-8 -*-

# Copyright (c) 2020 Lachlan Mackenzie
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# ---------------------------------------------------
# Roland Ernst
# Changes implemented for MythTV:
# - added support for requests.Session()
#
# ---------------------------------------------------


from __future__ import unicode_literals

from datetime import datetime
from requests import codes as requestcodes
import sys
import time
from dateutil import parser
from pprint import pprint

from . import endpoints
from .show import Show, Alias
from .episode import Episode
from .season import Season
from .person import Character, Person, Crew
from .embed import Embed


MYTHTV_TVMAZE_API_VERSION = "0.1.0"

# set this to true for showing raw json data
JSONDEBUG = False

# Set an open requests session here
ReqSession = None


def set_session(s):
global ReqSession
ReqSession = s


def _query_api(url, params=None):
if JSONDEBUG:
print(params)

http_retries = 0
res = None
while http_retries < 2:
if ReqSession is None:
if JSONDEBUG:
print("Using requests directly, without cache")
print(url)
import requests
res = requests.get(url, params)
else:
if JSONDEBUG:
print("Using Request Session %s" % ReqSession)
print(url)
res = ReqSession.get(url, params=params)
if JSONDEBUG:
print(res.url)
print(res.request.headers)
if res.status_code == 429:
# wait a bit and do a retry once
if JSONDEBUG:
print("Rate Limiting, caused by 'HTTP Too Many Requests'")
if http_retries == 1:
print('Error: Exiting due to rate limitation')
sys.exit(1)
time.sleep(10)
http_retries += 1
elif res.status_code != requestcodes.OK:
print('Page request was unsuccessful: ' + str(res.status_code), res.reason)
sys.exit(1)
else:
if JSONDEBUG:
print("Successful http request '%s':" % res.url)
break

if JSONDEBUG:
pprint(res.json())
if res is None:
return None
else:
return res.json()


def search_show(show_name):
res = _query_api(endpoints.search_show_name, {'q': show_name})
return [Show(show) for show in res] if res is not None else []


def search_show_best_match(show_name, embed=None):
embed = Embed(embed)
res = _query_api(endpoints.search_show_best_match, {'q': show_name, embed.key: embed.value})
return Show(res) if res is not None else None


def get_show_external(imdb_id=None, tvdb_id=None, tvrage_id=None):
if len(list(filter(None, [imdb_id, tvdb_id, tvrage_id]))) == 0:
return None
if imdb_id is not None:
return _get_show_external_id('imdb', imdb_id)
if tvdb_id is not None:
return _get_show_external_id('thetvdb', tvdb_id)
if tvrage_id is not None:
return _get_show_external_id('tvrage', tvrage_id)


def _get_show_external_id(external_name, external_id):
res = _query_api(endpoints.search_external_show_id, {external_name: external_id})
return Show(res) if res is not None else None


def get_show(tvmaze_id, populated=False, embed=None):
embed = Embed(embed) if not populated else Embed(['seasons', 'cast', 'crew', 'akas', 'images'])
res = _query_api(endpoints.show_information.format(str(tvmaze_id)), {embed.key: embed.value})
# print(res.keys())
if populated:
episodes = [episode for episode in _get_show_episode_list_raw(tvmaze_id, specials=True)]
# print(episodes)
res['_embedded']['episodes'] = episodes
return Show(res) if res is not None else None


def get_show_episode_list(tvmaze_id, specials=False):
specials = 1 if specials else None
res = _get_show_episode_list_raw(tvmaze_id, specials)
return [Episode(episode) for episode in res] if res is not None else []


def _get_show_episode_list_raw(tvmaze_id, specials):
return _query_api(endpoints.show_episode_list.format(str(tvmaze_id)), {'specials': specials})


def get_show_specials(tvmaze_id):
res = _query_api(endpoints.show_episode_list.format(str(tvmaze_id)), {'specials': 1})
specials = [Episode(episode) for episode in res if episode['number'] is None] if res is not None else []
special_num = 1
for special in specials:
special.season = 0
special.number = special_num
special_num += 1
return specials


def get_show_episode(tvmaze_id, season, episode):
res = _query_api(endpoints.show_episode.format(str(tvmaze_id)), {'season': season, 'number': episode})
return Episode(res) if res is not None else None


def get_show_episodes_by_date(tvmaze_id, date_input):
if type(date_input) is str:
try:
date = parser.parse(date_input)
except parser._parser.ParserError: ### XXX check this
return []
elif type(date_input) is datetime:
date = date_input
else:
return []
res = _query_api(endpoints.show_episodes_on_date.format(str(tvmaze_id)), {'date': date.isoformat()[:10]})
return [Episode(episode) for episode in res] if res is not None else []


def get_show_season_list(tvmaze_id):
res = _query_api(endpoints.show_season_list.format(str(tvmaze_id)))
return [Season(season) for season in res] if res is not None else []


def get_season_episode_list(tvmaze_season_id):
res = _query_api(endpoints.season_episode_list.format(str(tvmaze_season_id)))
# episode['number'] is None when it is classed as a special, for now don't include specials
return [Episode(episode) for episode in res if episode['number'] is not None] if res is not None else []


def get_show_aliases(tvmaze_id):
res = _query_api(endpoints.show_alias_list.format(str(tvmaze_id)))
return [Alias(alias) for alias in res] if res is not None else []


def get_episode_information(tvmaze_episode_id, embed=None):
embed = Embed(embed)
res = _query_api(endpoints.episode_information.format(str(tvmaze_episode_id)), {embed.key: embed.value})
return Episode(res) if res is not None else None


def get_show_cast(tvmaze_id):
res = _query_api(endpoints.show_cast.format(str(tvmaze_id)))
return [Character(cast['character'], cast['person']) for cast in res]


def get_show_crew(tvmaze_id):
res = _query_api(endpoints.show_crew.format(str(tvmaze_id)))
return [Crew(crew_member) for crew_member in res]
68 changes: 68 additions & 0 deletions mythtv/bindings/python/tvmaze/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- coding: UTF-8 -*-

from __future__ import unicode_literals

from datetime import datetime
import sys


if sys.version_info[0] == 2:
from HTMLParser import HTMLParser
from StringIO import StringIO


class MLStripper(HTMLParser):
def __init__(self):
self.reset()
self.text = StringIO()

def handle_data(self, d):
self.text.write(d)

def get_data(self):
return self.text.getvalue()

else:
from io import StringIO
from html.parser import HTMLParser

class MLStripper(HTMLParser):
def __init__(self):
super().__init__()
self.reset()
self.strict = False
self.convert_charrefs= True
self.text = StringIO()

def handle_data(self, d):
self.text.write(d)

def get_data(self):
return self.text.getvalue()


def strip_tags(html):
if html is not None and html is not "":
s = MLStripper()
s.feed(html)
return s.get_data()
else:
return ""


def convert_date(tstring):
if tstring is None or tstring == '':
return None
try:
return datetime.strptime(tstring, '%Y-%m-%d').date()
except(TypeError, ValueError):
return None


def convert_time(tstring):
if tstring is None or tstring == '':
return None
try:
return datetime.strptime(tstring, '%Y-%m-%d')
except(TypeError, ValueError):
return None
553 changes: 553 additions & 0 deletions mythtv/programs/scripts/metadata/Television/tvmaze.py

Large diffs are not rendered by default.

350 changes: 350 additions & 0 deletions mythtv/programs/scripts/metadata/Television/tvmaze_tests.txt

Large diffs are not rendered by default.