| 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()) |
| 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('-'))) |
| 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 |
| 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] |
| 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 |