Updated kodi settings on Lenovo
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import xbmcplugin
|
||||
|
||||
from .artwork import get_artworks
|
||||
from .episodes import get_episode_details, get_series_episodes
|
||||
from .nfo import get_show_id_from_nfo
|
||||
from .series import get_series_details, search_series
|
||||
from .utils import create_uuid, logger, ADDON
|
||||
|
||||
|
||||
def run():
|
||||
handle = int(sys.argv[1])
|
||||
qs = sys.argv[2][1:]
|
||||
params = dict(urllib.parse.parse_qsl(qs))
|
||||
logger.debug("THE TVDB TV SHOWS SCRAPER V.4")
|
||||
logger.debug(params)
|
||||
|
||||
_action = params.get("action", "")
|
||||
action = urllib.parse.unquote_plus(_action)
|
||||
_settings = params.get("pathSettings", "{}")
|
||||
settings = json.loads(_settings)
|
||||
_title = params.get("title", "")
|
||||
title = urllib.parse.unquote_plus(_title)
|
||||
year = params.get("year", None)
|
||||
|
||||
uuid = settings.get("uuid", None)
|
||||
if not uuid or uuid == "":
|
||||
uuid = create_uuid()
|
||||
ADDON.setSetting("uuid", uuid)
|
||||
settings["uuid"] = uuid
|
||||
|
||||
|
||||
logger.debug("settings:")
|
||||
logger.debug(settings)
|
||||
if 'action' in params:
|
||||
if action == 'find' and title is not None:
|
||||
logger.debug("about to call search series")
|
||||
search_series(title, settings, handle, year)
|
||||
elif action == 'getdetails' and 'url' in params:
|
||||
logger.debug("about to call get series details")
|
||||
get_series_details(
|
||||
urllib.parse.unquote_plus(params["url"]), settings, handle)
|
||||
elif action == 'getepisodelist' and 'url' in params:
|
||||
logger.debug("about to call get series episodes")
|
||||
get_series_episodes(
|
||||
urllib.parse.unquote_plus(params["url"]), settings, handle)
|
||||
elif action == 'getepisodedetails' and 'url' in params:
|
||||
logger.debug("about to call get episode details")
|
||||
get_episode_details(
|
||||
urllib.parse.unquote_plus(params["url"]), settings, handle)
|
||||
elif action == 'getartwork' and 'id' in params:
|
||||
logger.debug("about to call get artworks")
|
||||
get_artworks(urllib.parse.unquote_plus(
|
||||
params["id"]), settings, handle)
|
||||
elif params['action'].lower() == 'nfourl':
|
||||
logger.debug('performing nfourl action')
|
||||
get_show_id_from_nfo(params['nfo'], settings, handle)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
@@ -0,0 +1,40 @@
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from .tvdb import Client, get_artworks_from_show, get_language
|
||||
|
||||
MAX_IMAGES_NUMBER = 10
|
||||
|
||||
|
||||
def add_artworks(show, liz, language):
|
||||
|
||||
artworks = get_artworks_from_show(show, language)
|
||||
fanarts = artworks.pop("fanarts")
|
||||
season_posters = artworks.pop("season_posters")
|
||||
|
||||
for art_type, images in artworks.items():
|
||||
for image in images[:MAX_IMAGES_NUMBER]:
|
||||
liz.addAvailableArtwork(image['image'], art_type)
|
||||
|
||||
for image, season_number in season_posters:
|
||||
liz.addAvailableArtwork(image, 'poster', season=season_number)
|
||||
|
||||
fanart_items = []
|
||||
for fanart in fanarts[:MAX_IMAGES_NUMBER]:
|
||||
fanart_items.append(
|
||||
{'image': fanart["image"], 'preview': fanart["thumbnail"]})
|
||||
liz.setAvailableFanart(fanart_items)
|
||||
|
||||
|
||||
def get_artworks(id, settings, handle):
|
||||
tvdb_client = Client(settings)
|
||||
|
||||
show = tvdb_client.get_series_details_api(id, settings)
|
||||
if not show:
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle, False, xbmcgui.ListItem(offscreen=True))
|
||||
return
|
||||
liz = xbmcgui.ListItem(id, offscreen=True)
|
||||
language = get_language(settings)
|
||||
add_artworks(show, liz, language)
|
||||
xbmcplugin.setResolvedUrl(handle=handle, succeeded=True, listitem=liz)
|
||||
@@ -0,0 +1,187 @@
|
||||
LANGUAGES_MAP = {
|
||||
'Abkhaz': 'abk',
|
||||
'Afar': 'aar',
|
||||
'Afrikaans': 'afr',
|
||||
'Akan': 'aka',
|
||||
'Albanian': 'sqi',
|
||||
'Amharic': 'amh',
|
||||
'Arabic': 'ara',
|
||||
'Aragonese': 'arg',
|
||||
'Armenian': 'hye',
|
||||
'Assamese': 'asm',
|
||||
'Avaric': 'ava',
|
||||
'Avestan': 'ave',
|
||||
'Aymara': 'aym',
|
||||
'Azerbaijani': 'aze',
|
||||
'Bambara': 'bam',
|
||||
'Bashkir': 'bak',
|
||||
'Basque': 'eus',
|
||||
'Belarusian': 'bel',
|
||||
'Bengali': 'ben',
|
||||
'Bihari': 'bih',
|
||||
'Bislama': 'bis',
|
||||
'Bosnian': 'bos',
|
||||
'Breton': 'bre',
|
||||
'Bulgarian': 'bul',
|
||||
'Burmese': 'mya',
|
||||
'Catalan': 'cat',
|
||||
'Chamorro': 'cha',
|
||||
'Chechen': 'che',
|
||||
'Chichewa': 'nya',
|
||||
'Chinese - Cantonese': 'yue',
|
||||
'Chinese - China': 'zho',
|
||||
'Chinese - Taiwan': 'zhtw',
|
||||
'Chuvash': 'chv',
|
||||
'Cornish': 'cor',
|
||||
'Corsican': 'cos',
|
||||
'Cree': 'cre',
|
||||
'Croatian': 'hrv',
|
||||
'Czech': 'ces',
|
||||
'Danish': 'dan',
|
||||
'Divehi': 'div',
|
||||
'Dutch': 'nld',
|
||||
'Dzongkha': 'dzo',
|
||||
'English': 'eng',
|
||||
'Esperanto': 'epo',
|
||||
'Estonian': 'est',
|
||||
'Ewe': 'ewe',
|
||||
'Faroese': 'fao',
|
||||
'Fijian': 'fij',
|
||||
'Finnish': 'fin',
|
||||
'French': 'fra',
|
||||
'Fula': 'ful',
|
||||
'Galician': 'glg',
|
||||
'Georgian': 'kat',
|
||||
'German': 'deu',
|
||||
'Greek': 'ell',
|
||||
'Guaraní': 'grn',
|
||||
'Gujarati': 'guj',
|
||||
'Haitian': 'hat',
|
||||
'Hausa': 'hau',
|
||||
'Hebrew': 'heb',
|
||||
'Herero': 'her',
|
||||
'Hindi': 'hin',
|
||||
'Hiri Motu': 'hmo',
|
||||
'Hungarian': 'hun',
|
||||
'Icelandic': 'isl',
|
||||
'Ido': 'ido',
|
||||
'Igbo': 'ibo',
|
||||
'Indonesian': 'ind',
|
||||
'Interlingua': 'ina',
|
||||
'Interlingue': 'ile',
|
||||
'Inuktitut': 'iku',
|
||||
'Inupiaq': 'ipk',
|
||||
'Irish': 'gle',
|
||||
'Italian': 'ita',
|
||||
'Japanese': 'jpn',
|
||||
'Javanese': 'jav',
|
||||
'Kalaallisut': 'kal',
|
||||
'Kannada': 'kan',
|
||||
'Kanuri': 'kau',
|
||||
'Kashmiri': 'kas',
|
||||
'Kazakh': 'kaz',
|
||||
'Khmer': 'khm',
|
||||
'Kikuyu': 'kik',
|
||||
'Kinyarwanda': 'kin',
|
||||
'Kirghiz': 'kir',
|
||||
'Kirundi': 'run',
|
||||
'Komi': 'kom',
|
||||
'Kongo': 'kon',
|
||||
'Korean': 'kor',
|
||||
'Kurdish': 'kur',
|
||||
'Kwanyama': 'kua',
|
||||
'Lao': 'lao',
|
||||
'Latin': 'lat',
|
||||
'Latvian': 'lav',
|
||||
'Limburgish': 'lim',
|
||||
'Lingala': 'lin',
|
||||
'Lithuanian': 'lit',
|
||||
'Luba-Katanga': 'lub',
|
||||
'Luganda': 'lug',
|
||||
'Luxembourgish': 'ltz',
|
||||
'Macedonian': 'mkd',
|
||||
'Malagasy': 'mlg',
|
||||
'Malay': 'msa',
|
||||
'Malayalam': 'mal',
|
||||
'Maltese': 'mlt',
|
||||
'Manx': 'glv',
|
||||
'Marathi': 'mar',
|
||||
'Marshallese': 'mah',
|
||||
'Mongolian': 'mon',
|
||||
'Māori': 'mri',
|
||||
'Nauru': 'nau',
|
||||
'Navajo': 'nav',
|
||||
'Ndonga': 'ndo',
|
||||
'Nepali': 'nep',
|
||||
'North Ndebele': 'nde',
|
||||
'Northern Sami': 'sme',
|
||||
'Norwegian': 'nor',
|
||||
'Nuosu': 'iii',
|
||||
'Occitan': 'oci',
|
||||
'Ojibwe': 'oji',
|
||||
'Old Church Slavonic': 'chu',
|
||||
'Oriya': 'ori',
|
||||
'Oromo': 'orm',
|
||||
'Ossetian': 'oss',
|
||||
'Panjabi': 'pan',
|
||||
'Pashto': 'pus',
|
||||
'Persian': 'fas',
|
||||
'Polish': 'pol',
|
||||
'Portuguese - Brazil': 'pt',
|
||||
'Portuguese - Portugal': 'por',
|
||||
'Pāli': 'pli',
|
||||
'Quechua': 'que',
|
||||
'Romanian': 'ron',
|
||||
'Romansh': 'roh',
|
||||
'Russian': 'rus',
|
||||
'Samoan': 'smo',
|
||||
'Sango': 'sag',
|
||||
'Sanskrit': 'san',
|
||||
'Sardinian': 'srd',
|
||||
'Scottish Gaelic': 'gla',
|
||||
'Serbian': 'srp',
|
||||
'Shona': 'sna',
|
||||
'Sindhi': 'snd',
|
||||
'Sinhala': 'sin',
|
||||
'Slovak': 'slk',
|
||||
'Slovene': 'slv',
|
||||
'Somali': 'som',
|
||||
'South Ndebele': 'nbl',
|
||||
'Southern Sotho': 'sot',
|
||||
'Spanish': 'spa',
|
||||
'Sundanese': 'sun',
|
||||
'Swahili': 'swa',
|
||||
'Swati': 'ssw',
|
||||
'Swedish': 'swe',
|
||||
'Tagalog': 'tgl',
|
||||
'Tahitian': 'tah',
|
||||
'Tajik': 'tgk',
|
||||
'Tamil': 'tam',
|
||||
'Tatar': 'tat',
|
||||
'Telugu': 'tel',
|
||||
'Thai': 'tha',
|
||||
'Tibetan Standard': 'bod',
|
||||
'Tigrinya': 'tir',
|
||||
'Tonga': 'ton',
|
||||
'Tsonga': 'tso',
|
||||
'Tswana': 'tsn',
|
||||
'Turkish': 'tur',
|
||||
'Turkmen': 'tuk',
|
||||
'Twi': 'twi',
|
||||
'Uighur': 'uig',
|
||||
'Ukrainian': 'ukr',
|
||||
'Urdu': 'urd',
|
||||
'Uzbek': 'uzb',
|
||||
'Venda': 'ven',
|
||||
'Vietnamese': 'vie',
|
||||
'Volapük': 'vol',
|
||||
'Walloon': 'wln',
|
||||
'Welsh': 'cym',
|
||||
'Western Frisian': 'fry',
|
||||
'Wolof': 'wol',
|
||||
'Xhosa': 'xho',
|
||||
'Yiddish': 'yid',
|
||||
'Yoruba': 'yor',
|
||||
'Zhuang': 'zha',
|
||||
'Zulu': 'zul',
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import json
|
||||
|
||||
from . import tvdb
|
||||
from .nfo import parse_episode_guide_url
|
||||
from .utils import logger
|
||||
from .series import get_unique_ids, ARTWORK_URL_PREFIX
|
||||
|
||||
|
||||
# add the episodes of a series to the list
|
||||
|
||||
|
||||
def get_series_episodes(show_ids, settings, handle):
|
||||
logger.debug(f'Find episodes of tvshow with id {id}')
|
||||
try:
|
||||
all_ids = json.loads(show_ids)
|
||||
show_id = all_ids.get('tvdb')
|
||||
if not show_id:
|
||||
show_id = str(show_ids)
|
||||
except (ValueError, AttributeError):
|
||||
show_id = str(show_ids)
|
||||
if show_id.isdigit():
|
||||
logger.error(
|
||||
'using deprecated episodeguide format, this show should be refreshed or rescraped')
|
||||
if not show_id:
|
||||
raise RuntimeError(
|
||||
'No tvdb show id found in episode guide, this show should be refreshed or rescraped')
|
||||
elif not str(show_id).isdigit():
|
||||
# Kodi has a bug: when a show directory contains an XML NFO file with
|
||||
# episodeguide URL, that URL is always passed here regardless of
|
||||
# the actual parsing result in get_show_id_from_nfo()
|
||||
parse_result = parse_episode_guide_url(show_id)
|
||||
if not parse_result:
|
||||
return
|
||||
|
||||
if parse_result.provider == 'thetvdb':
|
||||
show_id = parse_result.show_id
|
||||
logger.debug(f'Changed show id to {show_id}')
|
||||
|
||||
client = tvdb.Client(settings)
|
||||
episodes = client.get_series_episodes_api(show_id, settings)
|
||||
|
||||
if not episodes:
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle, False, xbmcgui.ListItem(offscreen=True))
|
||||
return
|
||||
|
||||
for ep in episodes:
|
||||
liz = xbmcgui.ListItem(ep['name'], offscreen=True)
|
||||
details = {
|
||||
'title': ep['name'],
|
||||
'season': ep['seasonNumber'],
|
||||
'episode': ep['number'],
|
||||
}
|
||||
date_string = ep.get("aired")
|
||||
if date_string:
|
||||
year = int(date_string.split("-")[0])
|
||||
details['premiered'] = details['date'] = date_string
|
||||
details['year'] = year
|
||||
details['aired'] = ep['aired']
|
||||
logger.debug("details in episodes.py")
|
||||
logger.debug(details)
|
||||
liz.setInfo('video', details)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=handle,
|
||||
url=str(ep['id']),
|
||||
listitem=liz,
|
||||
isFolder=True
|
||||
)
|
||||
|
||||
# get the details of the found episode
|
||||
def get_episode_details(id, settings, handle):
|
||||
logger.debug(f'Find info of episode with id {id}')
|
||||
client = tvdb.Client(settings)
|
||||
ep = client.get_episode_details_api(id, settings)
|
||||
if not ep:
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle, False, xbmcgui.ListItem(offscreen=True))
|
||||
return
|
||||
liz = xbmcgui.ListItem(ep["name"], offscreen=True)
|
||||
cast = get_episode_cast(ep)
|
||||
rating = get_rating(ep)
|
||||
tags = get_tags(ep)
|
||||
duration_minutes = ep.get('runtime') or 0
|
||||
|
||||
details = {
|
||||
'title': ep["name"],
|
||||
'plot': ep["overview"],
|
||||
'plotoutline': ep["overview"],
|
||||
'premiered': ep["aired"],
|
||||
'aired': ep["aired"],
|
||||
'mediatype': 'episode',
|
||||
'director': cast["directors"],
|
||||
'writer': cast["writers"],
|
||||
'mpaa': rating,
|
||||
'duration': duration_minutes * 60,
|
||||
}
|
||||
|
||||
if ep.get("airsAfterSeason"):
|
||||
details['sortseason'] = ep.get("airsAfterSeason")
|
||||
details['sortepisode'] = 4096
|
||||
if ep.get("airsBeforeSeason"):
|
||||
details['sortseason'] = ep.get("airsBeforeSeason")
|
||||
details['sortepisode'] = 0
|
||||
if ep.get("airsBeforeEpisode"):
|
||||
details['sortepisode'] = ep.get("airsBeforeEpisode")
|
||||
if tags:
|
||||
details["tag"] = tags
|
||||
|
||||
|
||||
liz.setInfo('video', details)
|
||||
|
||||
unique_ids = get_unique_ids(ep)
|
||||
liz.setUniqueIDs(unique_ids, 'tvdb')
|
||||
guest_stars = cast['guest_stars']
|
||||
if guest_stars:
|
||||
liz.setCast(guest_stars)
|
||||
if ep.get("image"):
|
||||
liz.addAvailableArtwork(ep["image"], 'thumb')
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle=handle,
|
||||
succeeded=True,
|
||||
listitem=liz)
|
||||
|
||||
|
||||
def get_episode_cast(ep):
|
||||
cast = defaultdict(list)
|
||||
characters = ep.get('characters')
|
||||
if characters:
|
||||
for char in characters:
|
||||
if char['peopleType'] == 'Writer':
|
||||
cast['writers'].append(char['personName'])
|
||||
elif char['peopleType'] == 'Director':
|
||||
cast['writers'].append(char['personName'])
|
||||
elif char['peopleType'] == 'Guest Star':
|
||||
person_info = {'name': char.get('personName') or ''}
|
||||
thumbnail = char.get('image') or char.get('personImgURL') or ''
|
||||
if thumbnail and not thumbnail.startswith(ARTWORK_URL_PREFIX):
|
||||
thumbnail = ARTWORK_URL_PREFIX + thumbnail
|
||||
if thumbnail:
|
||||
person_info['thumbnail'] = thumbnail
|
||||
cast['guest_stars'].append(person_info)
|
||||
return cast
|
||||
|
||||
|
||||
def get_rating(ep):
|
||||
ratings = ep.get("contentRatings", [])
|
||||
rating = ''
|
||||
if len(ratings) == 1:
|
||||
rating = ratings[0]['country'] + ': ' + ratings[0]["name"]
|
||||
if not rating:
|
||||
for r in ratings:
|
||||
if r["country"] == "usa":
|
||||
rating = 'USA: ' + r["name"]
|
||||
return rating
|
||||
|
||||
|
||||
def get_tags(ep):
|
||||
tags = []
|
||||
tag_options = ep.get("tagOptions", [])
|
||||
if tag_options:
|
||||
for tag in tag_options:
|
||||
tags.append(tag["name"])
|
||||
return tags
|
||||
@@ -0,0 +1,150 @@
|
||||
# coding: utf-8
|
||||
# (c) Roman Miroshnychenko <roman1972@gmail.com> 2020
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""Exception logger with extended diagnostic info"""
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from platform import uname
|
||||
from pprint import pformat
|
||||
from typing import Text, Callable, Generator
|
||||
|
||||
import xbmc
|
||||
|
||||
from .utils import logger
|
||||
|
||||
|
||||
def _format_vars(variables):
|
||||
# type: (dict) -> Text
|
||||
"""
|
||||
Format variables dictionary
|
||||
|
||||
:param variables: variables dict
|
||||
:return: formatted string with sorted ``var = val`` pairs
|
||||
"""
|
||||
var_list = [(var, val) for var, val in variables.items()
|
||||
if not (var.startswith('__') or var.endswith('__'))]
|
||||
var_list.sort(key=lambda i: i[0])
|
||||
lines = []
|
||||
for var, val in var_list:
|
||||
lines.append('{} = {}'.format(var, pformat(val)))
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _format_code_context(frame_info):
|
||||
# type: (tuple) -> Text
|
||||
context = ''
|
||||
if frame_info[4] is not None:
|
||||
for i, line in enumerate(frame_info[4], frame_info[2] - frame_info[5]):
|
||||
if i == frame_info[2]:
|
||||
context += '{}:>{}'.format(str(i).rjust(5), line)
|
||||
else:
|
||||
context += '{}: {}'.format(str(i).rjust(5), line)
|
||||
return context
|
||||
|
||||
|
||||
FRAME_INFO_TEMPLATE = """File:
|
||||
{file_path}:{lineno}
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Code context:
|
||||
{code_context}
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Local variables:
|
||||
{local_vars}
|
||||
====================================================================================================
|
||||
"""
|
||||
|
||||
|
||||
def _format_frame_info(frame_info):
|
||||
# type: (tuple) -> Text
|
||||
return FRAME_INFO_TEMPLATE.format(
|
||||
file_path=frame_info[1],
|
||||
lineno=frame_info[2],
|
||||
code_context=_format_code_context(frame_info),
|
||||
local_vars=_format_vars(frame_info[0].f_locals)
|
||||
)
|
||||
|
||||
|
||||
EXCEPTION_TEMPLATE = """
|
||||
*********************************** Unhandled exception detected ***********************************
|
||||
####################################################################################################
|
||||
Diagnostic info
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Exception type : {exc_type}
|
||||
Exception value : {exc}
|
||||
System info : {system_info}
|
||||
Python version : {python_version}
|
||||
Kodi version : {kodi_version}
|
||||
sys.argv : {sys_argv}
|
||||
----------------------------------------------------------------------------------------------------
|
||||
sys.path:
|
||||
{sys_path}
|
||||
####################################################################################################
|
||||
Stack Trace
|
||||
====================================================================================================
|
||||
{stack_trace}
|
||||
************************************* End of diagnostic info ***************************************
|
||||
"""
|
||||
|
||||
|
||||
@contextmanager
|
||||
def log_exception(logger_func=logger.error):
|
||||
# type: (Callable[[Text], None]) -> Generator[None, None, None]
|
||||
"""
|
||||
Diagnostic helper context manager
|
||||
|
||||
It controls execution within its context and writes extended
|
||||
diagnostic info to the Kodi log if an unhandled exception
|
||||
happens within the context. The info includes the following items:
|
||||
|
||||
- System info
|
||||
- Python version
|
||||
- Kodi version
|
||||
- Module path.
|
||||
- Stack trace including:
|
||||
* File path and line number where the exception happened
|
||||
* Code fragment where the exception has happened.
|
||||
* Local variables at the moment of the exception.
|
||||
|
||||
After logging the diagnostic info the exception is re-raised.
|
||||
|
||||
Example::
|
||||
|
||||
with debug_exception():
|
||||
# Some risky code
|
||||
raise RuntimeError('Fatal error!')
|
||||
|
||||
:param logger_func: logger function that accepts a single argument
|
||||
that is a log message.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except Exception as exc:
|
||||
stack_trace = ''
|
||||
for frame_info in inspect.trace(5):
|
||||
stack_trace += _format_frame_info(frame_info)
|
||||
message = EXCEPTION_TEMPLATE.format(
|
||||
exc_type=exc.__class__.__name__,
|
||||
exc=exc,
|
||||
system_info=uname(),
|
||||
python_version=sys.version.replace('\n', ' '),
|
||||
kodi_version=xbmc.getInfoLabel('System.BuildVersion'),
|
||||
sys_argv=pformat(sys.argv),
|
||||
sys_path=pformat(sys.path),
|
||||
stack_trace=stack_trace
|
||||
)
|
||||
logger_func(message)
|
||||
raise exc
|
||||
@@ -0,0 +1,86 @@
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from .simple_requests import HTTPError
|
||||
from .tvdb import Request
|
||||
from .utils import logger
|
||||
|
||||
SHOW_ID_FROM_EPISODE_GUIDE_REGEXPS = (
|
||||
r'(thetvdb)\.com[\w=&\?/{}\":,]+\"id\":(\d+)',
|
||||
r'(thetvdb)\.com/.*?series/(\d+)',
|
||||
r'(thetvdb)\.com[\w=&\?/]*[&\?]+id=(\d+)',
|
||||
)
|
||||
|
||||
SHOW_ID_REGEXPS = (
|
||||
r'<uniqueid type=\"(tvdb)\".*>(\d+)</uniqueid>',
|
||||
r'(thetvdb)\.com/.*?series/([\w\s\d()-]+)',
|
||||
r'(thetvdb)\.com[\w=&\?/]*[&\?]+id=(\d+)',
|
||||
)
|
||||
|
||||
SERIES_URL_REGEX = re.compile(r'https?://[w.]*?thetvdb.com/series/([\w-]+)', re.I)
|
||||
TVDB_ID_REGEX = re.compile(r'<strong>TheTVDB\.com Series ID</strong>\s+?<span>(\d+?)</span>', re.I)
|
||||
|
||||
UrlParseResult = namedtuple('UrlParseResult', ['provider', 'show_id'])
|
||||
|
||||
|
||||
def get_show_id_from_nfo(nfo, settings, plugin_handle):
|
||||
"""
|
||||
Get show info by NFO file contents
|
||||
|
||||
This function is called first instead of find_show
|
||||
if a NFO file is found in a TV show folder
|
||||
|
||||
:param nfo: the contents of a NFO file
|
||||
"""
|
||||
logger.debug(f'Parsing NFO file:\n{nfo}')
|
||||
if '<episodedetails>' in nfo:
|
||||
return # skip episode NFOs
|
||||
parse_result = _parse_nfo_url(nfo)
|
||||
if parse_result is not None:
|
||||
if parse_result.provider in ('tvdb', 'thetvdb'):
|
||||
list_item = xbmcgui.ListItem(offscreen=True)
|
||||
list_item.setUniqueIDs({'tvdb': parse_result.show_id}, 'tvdb')
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=plugin_handle,
|
||||
url=parse_result.show_id,
|
||||
listitem=list_item,
|
||||
isFolder=True
|
||||
)
|
||||
|
||||
|
||||
def parse_episode_guide_url(episode_guide):
|
||||
"""Extract show ID from episode guide string"""
|
||||
for regexp in SHOW_ID_FROM_EPISODE_GUIDE_REGEXPS:
|
||||
show_id_match = re.search(regexp, episode_guide, re.I)
|
||||
if show_id_match:
|
||||
return UrlParseResult(show_id_match.group(1), show_id_match.group(2))
|
||||
return None
|
||||
|
||||
|
||||
def _parse_nfo_url(nfo):
|
||||
"""Extract show ID from NFO file contents"""
|
||||
series_url_match = SERIES_URL_REGEX.search(nfo)
|
||||
if series_url_match is not None:
|
||||
result = _get_tvdb_id_from_slug(series_url_match.group(0))
|
||||
if result is not None:
|
||||
return result
|
||||
for regexp in SHOW_ID_REGEXPS:
|
||||
show_id_match = re.search(regexp, nfo, re.I)
|
||||
if show_id_match:
|
||||
return UrlParseResult(show_id_match.group(1), show_id_match.group(2))
|
||||
return None
|
||||
|
||||
|
||||
def _get_tvdb_id_from_slug(series_url):
|
||||
try:
|
||||
html = Request.make_web_request(series_url)
|
||||
except HTTPError as exc:
|
||||
logger.error(f'Error {exc.response.status_code} for URL {series_url}')
|
||||
return None
|
||||
match = TVDB_ID_REGEX.search(html)
|
||||
if match is not None:
|
||||
return UrlParseResult('thetvdb', match.group(1))
|
||||
return None
|
||||
@@ -0,0 +1,175 @@
|
||||
from pprint import pformat
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import json
|
||||
|
||||
from . import tvdb
|
||||
from .artwork import add_artworks
|
||||
from .tvdb import get_language
|
||||
from .utils import logger
|
||||
|
||||
SUPPORTED_REMOTE_IDS = {
|
||||
'IMDB': 'imdb',
|
||||
'TheMovieDB.com': 'tmdb',
|
||||
}
|
||||
|
||||
ARTWORK_URL_PREFIX = 'https://artworks.thetvdb.com'
|
||||
|
||||
|
||||
def search_series(title, settings, handle, year=None) -> None:
|
||||
# add the found shows to the list
|
||||
logger.debug(f'Searching for TV show "{title}", year="{year}"')
|
||||
|
||||
tvdb_client = tvdb.Client(settings)
|
||||
if year is None:
|
||||
search_results = tvdb_client.search(title, type="series", limit=10)
|
||||
else:
|
||||
search_results = tvdb_client.search(title, year=year, type="series", limit=10)
|
||||
if not search_results:
|
||||
logger.debug(f"No results found for '{title}' where year='{year}'. Falling back to search without year criteria.")
|
||||
search_results = tvdb_client.search(title, type="series", limit=10)
|
||||
|
||||
logger.debug(f'Search results {search_results}')
|
||||
|
||||
if not search_results:
|
||||
return
|
||||
|
||||
language = get_language(settings)
|
||||
items = []
|
||||
for show in search_results:
|
||||
show_name = None
|
||||
translations = show.get('translations') or {}
|
||||
if translations:
|
||||
show_name = translations.get(language)
|
||||
if not show_name:
|
||||
show_name = translations.get('eng')
|
||||
if not show_name:
|
||||
show_name = show['name']
|
||||
year = show.get('year')
|
||||
if year:
|
||||
show_name += f' ({year})'
|
||||
|
||||
liz = xbmcgui.ListItem(show_name, offscreen=True)
|
||||
url = str(show['tvdb_id'])
|
||||
is_folder = True
|
||||
items.append((url, liz, is_folder))
|
||||
|
||||
xbmcplugin.addDirectoryItems(
|
||||
handle,
|
||||
items,
|
||||
len(items)
|
||||
)
|
||||
|
||||
|
||||
def get_series_details(id, settings, handle):
|
||||
# get the details of the found series
|
||||
logger.debug(f'Find info of tvshow with id {id}')
|
||||
tvdb_client = tvdb.Client(settings)
|
||||
show = tvdb_client.get_series_details_api(id, settings)
|
||||
if not show:
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle, False, xbmcgui.ListItem(offscreen=True))
|
||||
return
|
||||
|
||||
showId = {'tvdb': str(show["id"])}
|
||||
for remoteId in show.get('remoteIds'):
|
||||
if remoteId.get('sourceName') == "IMDB":
|
||||
showId['imdb'] = remoteId.get('id')
|
||||
if remoteId.get('sourceName') == "TheMovieDB.com":
|
||||
showId['tmdb'] = remoteId.get('id')
|
||||
|
||||
details = {'title': show["name"],
|
||||
'tvshowtitle': show["name"],
|
||||
'plot': show["overview"],
|
||||
'plotoutline': show["overview"],
|
||||
'episodeguide': json.dumps(showId),
|
||||
'mediatype': 'tvshow',
|
||||
}
|
||||
name = show["name"]
|
||||
year_str = show.get("firstAired") or ''
|
||||
if year_str:
|
||||
year = int(year_str.split("-")[0])
|
||||
logger.debug(f"series year_str: {year_str}")
|
||||
details["premiered"] = year_str
|
||||
details['year'] = year
|
||||
name = f'{name} ({year})'
|
||||
studio = get_studio(show)
|
||||
if studio:
|
||||
details["studio"] = studio
|
||||
genres = get_genres(show)
|
||||
details["genre"] = genres
|
||||
country = show.get("originalCountry", None)
|
||||
if country:
|
||||
details["country"] = country
|
||||
status = show.get('status')
|
||||
if status:
|
||||
details['status'] = status['name']
|
||||
liz = xbmcgui.ListItem(name, offscreen=True)
|
||||
logger.debug(f"series details: {pformat(details)}")
|
||||
liz.setInfo('video', details)
|
||||
liz = set_cast(liz, show)
|
||||
unique_ids = get_unique_ids(show)
|
||||
liz.setUniqueIDs(unique_ids, 'tvdb')
|
||||
language = tvdb.get_language(settings)
|
||||
add_artworks(show, liz, language)
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle=handle,
|
||||
succeeded=True,
|
||||
listitem=liz)
|
||||
|
||||
|
||||
def set_cast(liz, show):
|
||||
cast = []
|
||||
characters = show.get('characters') or ()
|
||||
for char in characters:
|
||||
if char["peopleType"] == "Actor":
|
||||
data = {
|
||||
'name': char['personName'],
|
||||
'role': char['name'],
|
||||
}
|
||||
thumbnail = char.get('image') or char.get('personImgURL')
|
||||
if thumbnail:
|
||||
if not thumbnail.startswith(ARTWORK_URL_PREFIX):
|
||||
thumbnail = ARTWORK_URL_PREFIX + thumbnail
|
||||
data['thumbnail'] = thumbnail
|
||||
cast.append(data)
|
||||
liz.setCast(cast)
|
||||
return liz
|
||||
|
||||
|
||||
def get_genres(show):
|
||||
return [genre["name"] for genre in show.get("genres", ())]
|
||||
|
||||
|
||||
def get_studio(show):
|
||||
companies = show.get("companies", ())
|
||||
if not companies:
|
||||
return None
|
||||
studio = None
|
||||
if len(companies) == 1:
|
||||
return companies[0]['name']
|
||||
for company in companies:
|
||||
if company["primaryCompanyType"] == 1:
|
||||
studio = company["name"]
|
||||
return studio
|
||||
|
||||
|
||||
def get_tags(show):
|
||||
tags = []
|
||||
tag_options = show.get("tagOptions", ())
|
||||
if tag_options:
|
||||
for tag in tag_options:
|
||||
tags.append(tag["name"])
|
||||
return tags
|
||||
|
||||
|
||||
def get_unique_ids(show):
|
||||
unique_ids = {'tvdb': show['id']}
|
||||
remote_ids = show.get('remoteIds')
|
||||
if remote_ids:
|
||||
for remote_id_info in remote_ids:
|
||||
source_name = remote_id_info.get('sourceName')
|
||||
if source_name in SUPPORTED_REMOTE_IDS:
|
||||
unique_ids[SUPPORTED_REMOTE_IDS[source_name]] = remote_id_info['id']
|
||||
return unique_ids
|
||||
@@ -0,0 +1,32 @@
|
||||
class PathSpecificSettings:
|
||||
# read-only shim for typed `xbmcaddon.Addon().getSetting*` methods
|
||||
def __init__(self, settings_dict, log_fn):
|
||||
self.data = settings_dict
|
||||
self.log = log_fn
|
||||
|
||||
def getSettingBool(self, id):
|
||||
return self._inner_get_setting(id, bool, False)
|
||||
|
||||
def getSettingInt(self, id):
|
||||
return self._inner_get_setting(id, int, 0)
|
||||
|
||||
def getSettingNumber(self, id):
|
||||
return self._inner_get_setting(id, float, 0.0)
|
||||
|
||||
def getSettingString(self, id):
|
||||
return self._inner_get_setting(id, str, '')
|
||||
|
||||
def _inner_get_setting(self, setting_id, setting_type, default):
|
||||
value = self.data.get(setting_id)
|
||||
if isinstance(value, setting_type):
|
||||
return value
|
||||
self._log_bad_value(value, setting_id)
|
||||
return default
|
||||
|
||||
def _log_bad_value(self, value, setting_id):
|
||||
if value is None:
|
||||
self.log(
|
||||
f"requested setting ({setting_id}) was not found.")
|
||||
else:
|
||||
self.log(
|
||||
f'failed to load value "{value}" for setting {setting_id}')
|
||||
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) 2021, Roman Miroshnychenko
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
A simple library for making HTTP requests with API similar to the popular "requests" library
|
||||
|
||||
It depends only on the Python standard library.
|
||||
|
||||
Supported:
|
||||
* HTTP methods: GET, POST
|
||||
* HTTP and HTTPS.
|
||||
* Disabling SSL certificates validation.
|
||||
* Request payload as form data and JSON.
|
||||
* Custom headers.
|
||||
* Basic authentication.
|
||||
* Gzipped response content.
|
||||
|
||||
Not supported:
|
||||
* Cookies.
|
||||
* File upload.
|
||||
"""
|
||||
import gzip
|
||||
import io
|
||||
import json as _json
|
||||
import ssl
|
||||
from base64 import b64encode
|
||||
from email.message import Message
|
||||
from typing import Optional, Dict, Any, Tuple, Union, List
|
||||
from urllib import request as url_request
|
||||
from urllib.error import HTTPError as _HTTPError
|
||||
from urllib.parse import urlparse, urlencode
|
||||
|
||||
__all__ = [
|
||||
'RequestException',
|
||||
'ConnectionError',
|
||||
'HTTPError',
|
||||
'get',
|
||||
'post',
|
||||
]
|
||||
|
||||
|
||||
class RequestException(IOError):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class ConnectionError(RequestException):
|
||||
|
||||
def __init__(self, message: str, url: str):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.url = url
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'ConnectionError for url {self.url}: {self.message}'
|
||||
|
||||
|
||||
class HTTPError(RequestException):
|
||||
|
||||
def __init__(self, response: 'Response'):
|
||||
self.response = response
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'HTTPError: {self.response.status_code} for url: {self.response.url}'
|
||||
|
||||
|
||||
class HTTPMessage(Message):
|
||||
|
||||
def update(self, dct: Dict[str, str]) -> None:
|
||||
for key, value in dct.items():
|
||||
self[key] = value
|
||||
|
||||
|
||||
class Response:
|
||||
NULL = object()
|
||||
|
||||
def __init__(self):
|
||||
self.encoding: str = 'utf-8'
|
||||
self.status_code: int = -1
|
||||
self.headers: Dict[str, str] = {}
|
||||
self.url: str = ''
|
||||
self.content: bytes = b''
|
||||
self._text = None
|
||||
self._json = self.NULL
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'<Response [{self.status_code}]>'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.status_code < 400
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""
|
||||
:return: Response payload as decoded text
|
||||
"""
|
||||
if self._text is None:
|
||||
self._text = self.content.decode(self.encoding)
|
||||
return self._text
|
||||
|
||||
def json(self) -> Optional[Union[Dict[str, Any], List[Any]]]:
|
||||
try:
|
||||
if self._json is self.NULL:
|
||||
self._json = _json.loads(self.content)
|
||||
return self._json
|
||||
except ValueError as exc:
|
||||
raise ValueError('Response content is not a valid JSON') from exc
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
if not self.ok:
|
||||
raise HTTPError(self)
|
||||
|
||||
|
||||
def _create_request(url_structure, params=None, data=None, headers=None, auth=None, json=None):
|
||||
query = url_structure.query
|
||||
if params is not None:
|
||||
separator = '&' if query else ''
|
||||
query += separator + urlencode(params)
|
||||
full_url = url_structure.scheme + '://' + url_structure.netloc + url_structure.path
|
||||
if query:
|
||||
full_url += '?' + query
|
||||
prepared_headers = HTTPMessage()
|
||||
if headers is not None:
|
||||
prepared_headers.update(headers)
|
||||
body = None
|
||||
if json is not None:
|
||||
body = _json.dumps(json).encode('utf-8')
|
||||
prepared_headers['Content-Type'] = 'application/json'
|
||||
if body is None and data is not None:
|
||||
body = urlencode(data).encode('utf-8')
|
||||
prepared_headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
if auth is not None:
|
||||
encoded_credentials = b64encode((auth[0] + ':' + auth[1]).encode('utf-8')).decode('utf-8')
|
||||
prepared_headers['Authorization'] = f'Basic {encoded_credentials}'
|
||||
if 'Accept-Encoding' not in prepared_headers:
|
||||
prepared_headers['Accept-Encoding'] = 'gzip'
|
||||
return url_request.Request(full_url, body, prepared_headers)
|
||||
|
||||
|
||||
def post(url: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
auth: Optional[Tuple[str, str]] = None,
|
||||
timeout: Optional[float] = None,
|
||||
verify: bool = True,
|
||||
json: Optional[Dict[str, Any]] = None) -> Response:
|
||||
"""
|
||||
POST request
|
||||
|
||||
This function assumes that a request body should be encoded with UTF-8
|
||||
and by default sends Accept-Encoding: gzip header to receive response content compressed.
|
||||
|
||||
:param url: URL
|
||||
:param params: URL query params
|
||||
:param data: request payload as form data. If "data" or "json" are passed
|
||||
then a POST request is sent
|
||||
:param headers: additional headers
|
||||
:param auth: a tuple of (login, password) for Basic authentication
|
||||
:param timeout: request timeout in seconds
|
||||
:param verify: verify SSL certificates
|
||||
:param json: request payload as JSON. This parameter has precedence over "data", that is,
|
||||
if it's present then "data" is ignored.
|
||||
:return: Response object
|
||||
"""
|
||||
url_structure = urlparse(url)
|
||||
request = _create_request(url_structure, params, data, headers, auth, json)
|
||||
context = None
|
||||
if url_structure.scheme == 'https':
|
||||
context = ssl.SSLContext()
|
||||
if not verify:
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
context.check_hostname = False
|
||||
fp = None
|
||||
try:
|
||||
r = fp = url_request.urlopen(request, timeout=timeout, context=context)
|
||||
content = fp.read()
|
||||
except _HTTPError as exc:
|
||||
r = exc
|
||||
fp = exc.fp
|
||||
content = fp.read()
|
||||
except Exception as exc:
|
||||
raise ConnectionError(str(exc), request.full_url) from exc
|
||||
finally:
|
||||
if fp is not None:
|
||||
fp.close()
|
||||
response = Response()
|
||||
response.status_code = r.status if hasattr(r, 'status') else r.getstatus()
|
||||
response.headers = r.headers
|
||||
response.url = r.url if hasattr(r, 'url') else r.geturl()
|
||||
if r.headers.get('Content-Encoding') == 'gzip':
|
||||
temp_fo = io.BytesIO(content)
|
||||
gzip_file = gzip.GzipFile(fileobj=temp_fo)
|
||||
content = gzip_file.read()
|
||||
response.content = content
|
||||
return response
|
||||
|
||||
|
||||
def get(url: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
auth: Optional[Tuple[str, str]] = None,
|
||||
timeout: Optional[float] = None,
|
||||
verify: bool = True) -> Response:
|
||||
"""
|
||||
GET request
|
||||
|
||||
This function by default sends Accept-Encoding: gzip header
|
||||
to receive response content compressed.
|
||||
|
||||
:param url: URL
|
||||
:param params: URL query params
|
||||
:param headers: additional headers
|
||||
:param auth: a tuple of (login, password) for Basic authentication
|
||||
:param timeout: request timeout in seconds
|
||||
:param verify: verify SSL certificates
|
||||
:return: Response object
|
||||
"""
|
||||
return post(url=url, params=params, headers=headers, auth=auth, timeout=timeout, verify=verify)
|
||||
@@ -0,0 +1,593 @@
|
||||
import enum
|
||||
import urllib.parse
|
||||
|
||||
from resources.lib import simple_requests as requests
|
||||
from resources.lib.constants import LANGUAGES_MAP
|
||||
from resources.lib.utils import logger, ADDON
|
||||
|
||||
apikey = "edae60dc-1b44-4bac-8db7-65c0aaf5258b"
|
||||
apikey_with_pin = "51bdbd35-bcd5-40d9-9bc3-788e24454baf"
|
||||
|
||||
USER_AGENT = 'TheTVDB v.4 TV Scraper for Kodi'
|
||||
|
||||
|
||||
class ArtworkType(enum.IntEnum):
|
||||
BANNER = 1
|
||||
POSTER = 2
|
||||
BACKGROUND = 3
|
||||
ICON = 5
|
||||
SEASONPOSTER = 7
|
||||
CLEARART = 22
|
||||
CLEARLOGO = 23
|
||||
|
||||
|
||||
class SeasonType(enum.IntEnum):
|
||||
DEFAULT = 1
|
||||
ABSOLUTE = 2
|
||||
DVD = 3
|
||||
ALTERNATE = 4
|
||||
REGIONAL = 5
|
||||
ALTDVD = 6
|
||||
|
||||
|
||||
class Auth:
|
||||
logger.debug("logging in")
|
||||
|
||||
def __init__(self, url, apikey, pin="", **kwargs):
|
||||
loginInfo = {"apikey": apikey}
|
||||
if pin:
|
||||
loginInfo["pin"] = pin
|
||||
loginInfo["apikey"] = apikey_with_pin
|
||||
loginInfo.update(kwargs)
|
||||
logger.debug("body in auth call")
|
||||
logger.debug(loginInfo)
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=loginInfo)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
self.token = response.json()['data']['token']
|
||||
|
||||
def get_token(self):
|
||||
return self.token
|
||||
|
||||
|
||||
class Request:
|
||||
def __init__(self, auth_token):
|
||||
self.auth_token = auth_token
|
||||
self.cache = {}
|
||||
|
||||
def make_api_request(self, url):
|
||||
logger.debug(f"about to make request to url {url}")
|
||||
logger.debug(url)
|
||||
data = self.cache.get(url, None)
|
||||
if data:
|
||||
return data
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json',
|
||||
'Authorization': f'Bearer {self.auth_token}'
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
data = response.json()['data']
|
||||
self.cache[url] = data
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def make_web_request(url):
|
||||
logger.debug(f"about to make request to url {url}")
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'text/html',
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
class Url:
|
||||
def __init__(self):
|
||||
self.base_url = "https://api4.thetvdb.com/v4"
|
||||
|
||||
def login_url(self):
|
||||
return "{}/login".format(self.base_url)
|
||||
|
||||
def artwork_status_url(self):
|
||||
return "{}/artwork/statuses".format(self.base_url)
|
||||
|
||||
def artwork_types_url(self):
|
||||
return "{}/artwork/types".format(self.base_url)
|
||||
|
||||
def artwork_url(self, id, extended=False):
|
||||
url = "{}/artwork/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def awards_url(self, page):
|
||||
if page < 0:
|
||||
page = 0
|
||||
url = "{}/awards?page={}".format(self.base_url, page)
|
||||
return url
|
||||
|
||||
def award_url(self, id, extended=False):
|
||||
url = "{}/awards/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def awards_categories_url(self):
|
||||
url = "{}/awards/categories".format(self.base_url)
|
||||
return url
|
||||
|
||||
def award_category_url(self, id, extended=False):
|
||||
url = "{}/awards/categories/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def content_ratings_url(self):
|
||||
url = "{}/content/ratings".format(self.base_url)
|
||||
return url
|
||||
|
||||
def countries_url(self):
|
||||
url = "{}/countries".format(self.base_url)
|
||||
return url
|
||||
|
||||
def companies_url(self, page=0):
|
||||
url = "{}/companies?page={}".format(self.base_url, page)
|
||||
return url
|
||||
|
||||
def company_url(self, id):
|
||||
url = "{}/companies/{}".format(self.base_url, id)
|
||||
return url
|
||||
|
||||
def all_series_url(self, page=0):
|
||||
url = "{}/series".format(self.base_url)
|
||||
return url
|
||||
|
||||
def series_url(self, id, extended=False):
|
||||
url = "{}/series/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def movies_url(self, page=0):
|
||||
url = "{}/movies".format(self.base_url, id)
|
||||
return url
|
||||
|
||||
def movie_url(self, id, extended=False):
|
||||
url = "{}/movies/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def season_url(self, id, extended=False):
|
||||
url = "{}/seasons/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended?meta=episodes".format(url)
|
||||
return url
|
||||
|
||||
def episode_url(self, id, extended=False):
|
||||
url = "{}/episodes/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def episode_translation_url(self, id: int, language: str = "eng"):
|
||||
url = "{}/episodes/{}/translations/{}".format(
|
||||
self.base_url, id, language)
|
||||
return url
|
||||
|
||||
def person_url(self, id, extended=False):
|
||||
url = "{}/people/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def character_url(self, id):
|
||||
url = "{}/characters/{}".format(self.base_url, id)
|
||||
return url
|
||||
|
||||
def people_types_url(self, id):
|
||||
url = "{}/people/types".format(self.base_url)
|
||||
return url
|
||||
|
||||
def source_types_url(self):
|
||||
url = "{}/sources/types".format(self.base_url)
|
||||
return url
|
||||
|
||||
def updates_url(self, since=0):
|
||||
url = "{}/updates?since={}".format(self.base_url, since)
|
||||
return url
|
||||
|
||||
def tag_options_url(self):
|
||||
url = "{}/tags/options".format(self.base_url)
|
||||
return url
|
||||
|
||||
def tag_option_url(self, id):
|
||||
url = "{}/tags/options/{}".format(self.base_url, id)
|
||||
return url
|
||||
|
||||
def search_url(self, query, filters):
|
||||
filters["query"] = query
|
||||
qs = urllib.parse.urlencode(filters)
|
||||
url = "{}/search?{}".format(self.base_url, qs)
|
||||
return url
|
||||
|
||||
def series_translation_url(self, id: int, language="eng"):
|
||||
url = "{}/series/{}/translations/{}".format(
|
||||
self.base_url, id, language)
|
||||
return url
|
||||
|
||||
def series_season_episodes_url(self, id: int, season_type_number: int = 1, page: int = 0):
|
||||
season_type = SeasonType(season_type_number).name.lower()
|
||||
url = "{}/series/{}/episodes/{}?page={}".format(
|
||||
self.base_url, id, season_type, page)
|
||||
return url
|
||||
|
||||
|
||||
class TVDB:
|
||||
def __init__(self, apikey: str, pin="", **kwargs):
|
||||
self.url = Url()
|
||||
login_url = self.url.login_url()
|
||||
self.auth = Auth(login_url, apikey, pin, **kwargs)
|
||||
auth_token = self.auth.get_token()
|
||||
self.request = Request(auth_token)
|
||||
|
||||
def get_artwork_statuses(self) -> list:
|
||||
"""Returns a list of artwork statuses"""
|
||||
url = self.url.artwork_status_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_artwork_types(self) -> list:
|
||||
"""Returns a list of artwork types"""
|
||||
url = self.url.artwork_types_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_artwork(self, id: int) -> dict:
|
||||
"""Returns an artwork dictionary"""
|
||||
url = self.url.artwork_url(id)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_artwork_extended(self, id: int) -> dict:
|
||||
"""Returns an artwork extended dictionary"""
|
||||
url = self.url.artwork_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_awards(self, page=0) -> list:
|
||||
"""Returns a list of awards"""
|
||||
url = self.url.awards_url(page)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_award(self, id: int) -> dict:
|
||||
"""Returns an award dictionary"""
|
||||
url = self.url.award_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_award_extended(self, id: int) -> dict:
|
||||
"""Returns an award extended dictionary"""
|
||||
url = self.url.award_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_award_categories(self) -> list:
|
||||
"""Returns a list of award categories"""
|
||||
url = self.url.awards_categories_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_award_category(self, id: int) -> dict:
|
||||
"""Returns an artwork category dictionary"""
|
||||
url = self.url.award_category_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_award_category_extended(self, id: int) -> dict:
|
||||
"""Returns an award category extended dictionary"""
|
||||
url = self.url.award_category_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_content_ratings(self) -> list:
|
||||
"""Returns a list of content ratings"""
|
||||
url = self.url.content_ratings_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_countries(self) -> list:
|
||||
"""Returns a list of countries"""
|
||||
url = self.url.countries_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_companies(self, page=0) -> list:
|
||||
"""Returns a list of companies"""
|
||||
url = self.url.companies_url(page)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_company(self, id: int) -> dict:
|
||||
"""Returns a company dictionary"""
|
||||
url = self.url.company_url(id)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_series(self, page=0) -> list:
|
||||
"""Returns a list of series"""
|
||||
url = self.url.all_series_url(page)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_series(self, id: int) -> dict:
|
||||
"""Returns a series dictionary"""
|
||||
url = self.url.series_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_series_extended(self, id: int) -> dict:
|
||||
"""Returns an series extended dictionary"""
|
||||
url = self.url.series_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_series_translation(self, id: int, lang: str) -> dict:
|
||||
"""Returns a series translation dictionary"""
|
||||
url = self.url.series_translation_url(id, lang)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_movies(self, page=0) -> list:
|
||||
"""Returns a list of movies"""
|
||||
url = self.url.movies_url(page)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_movie(self, id: int) -> dict:
|
||||
"""Returns a movie dictionary"""
|
||||
url = self.url.movie_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_movie_extended(self, id: int) -> dict:
|
||||
"""Returns a movie extended dictionary"""
|
||||
url = self.url.movie_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_movie_translation(self, lang: str) -> dict:
|
||||
"""Returns a movie translation dictionary"""
|
||||
url = self.url.movie_translation_url(id, lang)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_season(self, id: int) -> dict:
|
||||
"""Returns a season dictionary"""
|
||||
url = self.url.season_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_season_extended(self, id: int) -> dict:
|
||||
"""Returns a season extended dictionary"""
|
||||
url = self.url.season_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_episode(self, id: int) -> dict:
|
||||
"""Returns an episode dictionary"""
|
||||
url = self.url.episode_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_episode_translation(self, id: int, lang: str) -> dict:
|
||||
"""Returns an episode translation dictionary"""
|
||||
url = self.url.episode_translation_url(id, lang)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_episode_extended(self, id: int) -> dict:
|
||||
"""Returns an episode extended dictionary"""
|
||||
url = self.url.episode_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_person(self, id: int) -> dict:
|
||||
"""Returns a person dictionary"""
|
||||
url = self.url.person_url(id, False)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_person_extended(self, id: int) -> dict:
|
||||
"""Returns a person extended dictionary"""
|
||||
url = self.url.person_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_character(self, id: int) -> dict:
|
||||
"""Returns a character dictionary"""
|
||||
url = self.url.character_url(id)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_people_types(self) -> list:
|
||||
"""Returns a list of people types"""
|
||||
url = self.url.people_types_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_sourcetypes(self) -> list:
|
||||
"""Returns a list of sourcetypes"""
|
||||
url = self.url.source_types_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_updates(self, since: int) -> list:
|
||||
"""Returns a list of updates"""
|
||||
url = self.url.updates_url(since)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_all_tag_options(self, page=0) -> list:
|
||||
"""Returns a list of tag options"""
|
||||
url = self.url.tag_options_url()
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_tag_option(self, id: int) -> dict:
|
||||
"""Returns a tag option dictionary"""
|
||||
url = self.url.tag_option_url(id)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def search(self, query, **kwargs) -> list:
|
||||
"""Returns a list of search results"""
|
||||
url = self.url.search_url(query, kwargs)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_series_season_episodes(self, id: int, season_type: int = 1):
|
||||
page = 0
|
||||
episodes = []
|
||||
while True:
|
||||
url = self.url.series_season_episodes_url(id, season_type, page)
|
||||
res = self.request.make_api_request(url).get("episodes", [])
|
||||
page += 1
|
||||
if not res:
|
||||
break
|
||||
episodes.extend(res)
|
||||
return episodes
|
||||
|
||||
def get_series_details_api(self, id, settings=None) -> dict:
|
||||
settings = settings or {}
|
||||
series = self.get_series_extended(id)
|
||||
language = get_language(settings)
|
||||
try:
|
||||
translation = self.get_series_translation(id, language)
|
||||
except requests.HTTPError as exc:
|
||||
logger.warning(f'{language} translation is not available: {exc}')
|
||||
translation = {}
|
||||
overview = translation.get("overview") or ''
|
||||
name = translation.get("name") or ''
|
||||
if not (overview or name) and translation.get('language') != 'eng':
|
||||
try:
|
||||
english_info = self.get_series_translation(id, 'eng')
|
||||
except requests.HTTPError as exc:
|
||||
logger.warning(f'eng info is not available: {exc}')
|
||||
english_info = {}
|
||||
if not overview:
|
||||
overview = english_info.get('overview') or ''
|
||||
if not name:
|
||||
name = english_info.get('name') or ''
|
||||
if name:
|
||||
series["name"] = name
|
||||
series["overview"] = overview
|
||||
return series
|
||||
|
||||
def get_series_episodes_api(self, id, settings):
|
||||
season_type = get_season_type(settings)
|
||||
result = self.get_series_season_episodes(id, season_type)
|
||||
if not result:
|
||||
season_type_name = SeasonType(season_type).name.lower()
|
||||
logger.warning(
|
||||
f'No episodes returned for show {id}, season type "{season_type_name}"')
|
||||
return result
|
||||
|
||||
def get_episode_details_api(self, id, settings):
|
||||
try:
|
||||
ep = self.get_episode_extended(id)
|
||||
except requests.HTTPError as e:
|
||||
logger.warning(f'No episode found with id={id}. [error: {e}]')
|
||||
return None
|
||||
|
||||
trans = None
|
||||
|
||||
primary_language = get_language(settings)
|
||||
language_attempties = [primary_language] if primary_language == "eng" else [
|
||||
primary_language, "eng"]
|
||||
for language in language_attempties:
|
||||
try:
|
||||
trans = self.get_episode_translation(id, language)
|
||||
break
|
||||
except requests.HTTPError as e:
|
||||
logger.warning(
|
||||
f'No episode found with id={id} and language={language}. [error: {e}]')
|
||||
|
||||
if not trans:
|
||||
return None
|
||||
|
||||
overview = trans.get("overview") or ''
|
||||
name = trans.get("name") or ''
|
||||
if not (overview and name) and trans['language'] != 'eng':
|
||||
try:
|
||||
english_info = self.get_episode_translation(id, 'eng')
|
||||
if not overview:
|
||||
overview = english_info.get('overview') or ''
|
||||
if not name:
|
||||
name = english_info.get('name') or ''
|
||||
except requests.HTTPError as e:
|
||||
logger.warning(
|
||||
f'No episode found with id={id} and language=eng . [error: {e}]')
|
||||
ep["overview"] = overview
|
||||
ep["name"] = name
|
||||
return ep
|
||||
|
||||
|
||||
def get_language(path_settings):
|
||||
language = path_settings.get('language')
|
||||
if language is None:
|
||||
language = ADDON.getSetting('language') or 'English'
|
||||
language_code = LANGUAGES_MAP.get(language, 'eng')
|
||||
return language_code
|
||||
|
||||
|
||||
def get_season_type(settings):
|
||||
season_type_str = settings.get("season_type", "1")
|
||||
return int(season_type_str)
|
||||
|
||||
|
||||
class Client(object):
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, settings=None):
|
||||
settings = settings or {}
|
||||
if cls._instance is None:
|
||||
pin = settings.get("pin", "")
|
||||
gender = settings.get("gender", "Other")
|
||||
uuid = settings.get("uuid", "")
|
||||
birth_year = settings.get("year", "")
|
||||
cls._instance = TVDB(apikey, pin=pin, gender=gender,
|
||||
birthYear=birth_year, uuid=uuid)
|
||||
return cls._instance
|
||||
|
||||
|
||||
def get_artworks_from_show(show: dict, language: str = 'eng'):
|
||||
|
||||
def sorter(item):
|
||||
item_language = item.get('language')
|
||||
score = item.get('score') or 0
|
||||
if item_language == language:
|
||||
return 3, score
|
||||
if item_language is None:
|
||||
return 2, score
|
||||
if item_language == 'eng':
|
||||
return 1, score
|
||||
return 0, score
|
||||
|
||||
artworks = show.get("artworks", [{}])
|
||||
seasons = show.get("seasons", [{}])
|
||||
banners = []
|
||||
posters = []
|
||||
fanarts = []
|
||||
icons = []
|
||||
cleararts = []
|
||||
clearlogos = []
|
||||
season_posters = []
|
||||
for art in artworks:
|
||||
art_type = art.get('type')
|
||||
if art_type == ArtworkType.BANNER:
|
||||
banners.append(art)
|
||||
elif art_type == ArtworkType.POSTER:
|
||||
posters.append(art)
|
||||
elif art_type == ArtworkType.BACKGROUND:
|
||||
fanarts.append(art)
|
||||
elif art_type == ArtworkType.ICON:
|
||||
icons.append(art)
|
||||
elif art_type == ArtworkType.CLEARART:
|
||||
cleararts.append(art)
|
||||
elif art_type == ArtworkType.CLEARLOGO:
|
||||
clearlogos.append(art)
|
||||
elif art_type == ArtworkType.SEASONPOSTER:
|
||||
season_id = art.get("seasonId", -1)
|
||||
season = next((season for season in seasons if season.get("id", -2) == season_id), None)
|
||||
if season:
|
||||
season_posters.append( (art.get("image", ""), season.get("number", 0) ) )
|
||||
|
||||
banners.sort(key=sorter, reverse=True)
|
||||
posters.sort(key=sorter, reverse=True)
|
||||
fanarts.sort(key=sorter, reverse=True)
|
||||
artwork_dict = {
|
||||
'banner': banners,
|
||||
'poster': posters,
|
||||
'icon': icons,
|
||||
'clearart': cleararts,
|
||||
'clearlogo': clearlogos,
|
||||
|
||||
'fanarts': fanarts,
|
||||
'season_posters': season_posters,
|
||||
}
|
||||
return artwork_dict
|
||||
@@ -0,0 +1,38 @@
|
||||
import uuid
|
||||
|
||||
import xbmc
|
||||
from xbmcaddon import Addon
|
||||
|
||||
ADDON = Addon()
|
||||
ADDON_ID = ADDON.getAddonInfo('id')
|
||||
|
||||
|
||||
class logger:
|
||||
log_message_prefix = '[{} ({})]: '.format(
|
||||
ADDON_ID, ADDON.getAddonInfo('version'))
|
||||
|
||||
@staticmethod
|
||||
def log(message, level=xbmc.LOGDEBUG):
|
||||
message = logger.log_message_prefix + str(message)
|
||||
xbmc.log(message, level)
|
||||
|
||||
@staticmethod
|
||||
def info(message):
|
||||
logger.log(message, xbmc.LOGINFO)
|
||||
|
||||
@staticmethod
|
||||
def error(message):
|
||||
logger.log(message, xbmc.LOGERROR)
|
||||
|
||||
@staticmethod
|
||||
def debug(*messages):
|
||||
for message in messages:
|
||||
logger.log(message, xbmc.LOGDEBUG)
|
||||
|
||||
@staticmethod
|
||||
def warning(message):
|
||||
logger.log(message, xbmc.LOGWARNING)
|
||||
|
||||
|
||||
def create_uuid():
|
||||
return str(uuid.uuid4())
|
||||
Reference in New Issue
Block a user