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.
@@ -0,0 +1,54 @@
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
|
||||
from .movies import get_artworks, get_movie_details, search_movie
|
||||
from .nfo import get_movie_id_from_nfo
|
||||
from .utils import create_uuid, logger
|
||||
|
||||
ADDON_SETTINGS = xbmcaddon.Addon()
|
||||
|
||||
|
||||
def run():
|
||||
qs = sys.argv[2][1:]
|
||||
params = dict(urllib.parse.parse_qsl(qs))
|
||||
handle = int(sys.argv[1])
|
||||
|
||||
_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:
|
||||
uuid = create_uuid()
|
||||
ADDON_SETTINGS.setSetting("uuid", uuid)
|
||||
settings["uuid"] = uuid
|
||||
|
||||
if action:
|
||||
if action == 'find' and 'title' in params:
|
||||
logger.debug("should search for movie")
|
||||
search_movie(title, settings, handle, year)
|
||||
elif action == 'getdetails' and 'url' in params:
|
||||
logger.debug("should get movie details")
|
||||
get_movie_details(urllib.parse.unquote_plus(params["url"]), settings, handle)
|
||||
elif action == 'NfoUrl' and 'nfo' in params:
|
||||
logger.debug("should parse nfo")
|
||||
get_movie_id_from_nfo(params['nfo'], 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)
|
||||
else:
|
||||
logger.debug("unhandled action")
|
||||
else:
|
||||
logger.debug("no action to act on")
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
@@ -0,0 +1,442 @@
|
||||
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',
|
||||
}
|
||||
|
||||
COUNTRIES_MAP = {
|
||||
'abw': 'Aruba',
|
||||
'afg': 'Afghanistan',
|
||||
'ago': 'Angola',
|
||||
'aia': 'Anguilla',
|
||||
'ala': 'Åland Islands',
|
||||
'alb': 'Albania',
|
||||
'and': 'Andorra',
|
||||
'are': 'United Arab Emirates',
|
||||
'arg': 'Argentina',
|
||||
'arm': 'Armenia',
|
||||
'asm': 'American Samoa',
|
||||
'ata': 'Antarctica',
|
||||
'atf': 'French Southern Territories',
|
||||
'atg': 'Antigua and Barbuda',
|
||||
'aus': 'Australia',
|
||||
'aut': 'Austria',
|
||||
'aze': 'Azerbaijan',
|
||||
'bdi': 'Burundi',
|
||||
'bel': 'Belgium',
|
||||
'ben': 'Benin',
|
||||
'bes': 'Bonaire, Sint Eustatius and Saba',
|
||||
'bfa': 'Burkina Faso',
|
||||
'bgd': 'Bangladesh',
|
||||
'bgr': 'Bulgaria',
|
||||
'bhr': 'Bahrain',
|
||||
'bhs': 'Bahamas',
|
||||
'bih': 'Bosnia and Herzegovina',
|
||||
'blm': 'Saint Barthélemy',
|
||||
'blr': 'Belarus',
|
||||
'blz': 'Belize',
|
||||
'bmu': 'Bermuda',
|
||||
'bol': 'Bolivia',
|
||||
'bra': 'Brazil',
|
||||
'brb': 'Barbados',
|
||||
'brn': 'Brunei Darussalam',
|
||||
'btn': 'Bhutan',
|
||||
'bvt': 'Bouvet Island',
|
||||
'bwa': 'Botswana',
|
||||
'caf': 'Central African Republic',
|
||||
'can': 'Canada',
|
||||
'cck': 'Cocos (Keeling) Islands',
|
||||
'che': 'Swiss Confederation',
|
||||
'chl': 'Chile',
|
||||
'chn': 'China',
|
||||
'civ': 'Ivory Coast',
|
||||
'cmr': 'Cameroon',
|
||||
'cod': 'Congo',
|
||||
'cog': 'Republic of the Congo',
|
||||
'cok': 'Cook Islands',
|
||||
'col': 'Colombia',
|
||||
'com': 'Comoros',
|
||||
'cpv': 'Cape Verde',
|
||||
'cri': 'Costa Rica',
|
||||
'cub': 'Cuba',
|
||||
'cuw': 'Curaçao',
|
||||
'cxr': 'Christmas Island',
|
||||
'cym': 'Cayman Islands',
|
||||
'cyp': 'Cyprus',
|
||||
'cze': 'Czech Republic',
|
||||
'deu': 'Germany',
|
||||
'dji': 'Djibouti',
|
||||
'dma': 'Dominica',
|
||||
'dnk': 'Denmark',
|
||||
'dom': 'Dominican Republic',
|
||||
'dza': 'Algeria',
|
||||
'ecu': 'Ecuador',
|
||||
'egy': 'Egypt',
|
||||
'eri': 'Eritrea',
|
||||
'esh': 'Western Sahara',
|
||||
'esp': 'Spain',
|
||||
'est': 'Estonia',
|
||||
'eth': 'Ethiopia',
|
||||
'fin': 'Finland',
|
||||
'fji': 'Fiji',
|
||||
'flk': 'The Falkland Islands',
|
||||
'fra': 'France',
|
||||
'fro': 'The Faroe Islands',
|
||||
'fsm': 'Micronesia',
|
||||
'gab': 'Gabon',
|
||||
'gbr': 'Great Britain',
|
||||
'geo': 'Georgia',
|
||||
'ggy': 'Guernsey',
|
||||
'gha': 'Ghana',
|
||||
'gib': 'Gibraltar',
|
||||
'gin': 'Guinea',
|
||||
'glp': 'Guadeloupe',
|
||||
'gmb': 'Gambia',
|
||||
'gnb': 'Guinea-Bissau',
|
||||
'gnq': 'Equatorial Guinea',
|
||||
'grc': 'Greece',
|
||||
'grd': 'Grenada',
|
||||
'grl': 'Greenland',
|
||||
'gtm': 'Guatemala',
|
||||
'guf': 'French Guiana',
|
||||
'gum': 'Guam',
|
||||
'guy': 'Guyana',
|
||||
'hkg': 'Hong Kong',
|
||||
'hmd': 'Heard Island and McDonald Islands',
|
||||
'hnd': 'Honduras',
|
||||
'hrv': 'Croatia',
|
||||
'hti': 'Haiti',
|
||||
'hun': 'Hungary',
|
||||
'idn': 'Indonesia',
|
||||
'imn': 'Isle of Man',
|
||||
'ind': 'India',
|
||||
'iot': 'British Indian Ocean Territory',
|
||||
'irl': 'Ireland',
|
||||
'irn': 'Iran',
|
||||
'irq': 'Iraq',
|
||||
'isl': 'Iceland',
|
||||
'isr': 'Israel',
|
||||
'ita': 'Italy',
|
||||
'jam': 'Jamaica',
|
||||
'jey': 'Jersey',
|
||||
'jor': 'Jordan',
|
||||
'jpn': 'Japan',
|
||||
'kaz': 'Kazakhstan',
|
||||
'ken': 'Kenya',
|
||||
'kgz': 'Kyrgyzstan',
|
||||
'khm': 'Cambodia',
|
||||
'kir': 'Kiribati',
|
||||
'kna': 'Saint Christopher and Nevis',
|
||||
'kor': 'South Korea',
|
||||
'kwt': 'Kuwait',
|
||||
'lao': 'Laos',
|
||||
'lbn': 'Lebanon',
|
||||
'lbr': 'Liberia',
|
||||
'lby': 'Libya',
|
||||
'lca': 'Saint Lucia',
|
||||
'lie': 'Liechtenstein',
|
||||
'lka': 'Sri Lanka',
|
||||
'lso': 'Lesotho',
|
||||
'ltu': 'Lithuania',
|
||||
'lux': 'Luxembourg',
|
||||
'lva': 'Latvia',
|
||||
'mac': 'Macao',
|
||||
'maf': 'Saint Martin',
|
||||
'mar': 'Morocco',
|
||||
'mco': 'Monaco',
|
||||
'mda': 'Moldova',
|
||||
'mdg': 'Madagascar',
|
||||
'mdv': 'Maldives',
|
||||
'mex': 'Mexico',
|
||||
'mhl': 'Marshall Islands',
|
||||
'mkd': 'Macedonia',
|
||||
'mli': 'Mali',
|
||||
'mlt': 'Malta',
|
||||
'mmr': 'Myanmar',
|
||||
'mne': 'Montenegro',
|
||||
'mng': 'Mongolia',
|
||||
'mnp': 'Northern Mariana Islands',
|
||||
'moz': 'Mozambique',
|
||||
'mrt': 'Mauritania',
|
||||
'msr': 'Montserrat',
|
||||
'mtq': 'Martinique',
|
||||
'mus': 'Mauritius',
|
||||
'mwi': 'Malawi',
|
||||
'mys': 'Malaysia',
|
||||
'myt': 'Mayotte',
|
||||
'nam': 'Namibia',
|
||||
'ncl': 'New Caledonia',
|
||||
'ner': 'Niger',
|
||||
'nfk': 'Norfolk Island',
|
||||
'nga': 'Nigeria',
|
||||
'nic': 'Nicaragua',
|
||||
'niu': 'Niue',
|
||||
'nld': 'The Netherlands',
|
||||
'nor': 'Norway',
|
||||
'npl': 'Nepal',
|
||||
'nru': 'Nauru',
|
||||
'nzl': 'New Zealand',
|
||||
'omn': 'Oman',
|
||||
'pak': 'Pakistan',
|
||||
'pan': 'Panama',
|
||||
'pcn': 'Pitcairn',
|
||||
'per': 'Peru',
|
||||
'phl': 'Philippines',
|
||||
'plw': 'Republic of Palau',
|
||||
'png': 'Papua New Guinea',
|
||||
'pol': 'Poland',
|
||||
'pri': 'Puerto Rico',
|
||||
'prk': 'North Korea',
|
||||
'prt': 'Portugal',
|
||||
'pry': 'Paraguay',
|
||||
'pse': 'Palestine, State of',
|
||||
'pyf': 'French Polynesia',
|
||||
'qat': 'Qatar',
|
||||
'reu': 'Réunion',
|
||||
'rou': 'Romania',
|
||||
'rus': 'Russia',
|
||||
'rwa': 'Rwanda',
|
||||
'sau': 'Saudi Arabia',
|
||||
'sdn': 'Sudan',
|
||||
'sen': 'Senegal',
|
||||
'sgp': 'Singapore',
|
||||
'sgs': 'South Georgia',
|
||||
'shn': 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
'sjm': 'Svalbard and Jan Mayen',
|
||||
'slb': 'Solomon Islands',
|
||||
'sle': 'Sierra Leone',
|
||||
'slv': 'El Salvador',
|
||||
'smr': 'San Marino',
|
||||
'som': 'Somali Republic',
|
||||
'spm': 'Saint Pierre and Miquelon',
|
||||
'srb': 'Serbia',
|
||||
'ssd': 'South Sudan',
|
||||
'stp': 'São Tomé and Príncipe',
|
||||
'sur': 'Suriname',
|
||||
'svk': 'Slovakia',
|
||||
'svn': 'Slovenia',
|
||||
'swe': 'Sweden',
|
||||
'swz': 'Swaziland',
|
||||
'sxm': 'Sint Maarten',
|
||||
'syc': 'Seychelles',
|
||||
'syr': 'Syrian Arab Republic',
|
||||
'tca': 'Turks and Caicos Islands',
|
||||
'tcd': 'Chad',
|
||||
'tgo': 'Togo',
|
||||
'tha': 'Thailand',
|
||||
'tjk': 'Tajikistan',
|
||||
'tkl': 'Tokelau',
|
||||
'tkm': 'Turkmenistan',
|
||||
'tls': 'Timor-Leste',
|
||||
'ton': 'Tonga',
|
||||
'tto': 'Trinidad and Tobago',
|
||||
'tun': 'Tunisia',
|
||||
'tur': 'Turkey',
|
||||
'tuv': 'Tuvalu',
|
||||
'twn': 'Taiwan',
|
||||
'tza': 'Tanzania',
|
||||
'uga': 'Uganda',
|
||||
'ukr': 'Ukraine',
|
||||
'umi': 'United States Minor Outlying Islands',
|
||||
'unk': 'Kosovo',
|
||||
'ury': 'Uruguay',
|
||||
'usa': 'USA',
|
||||
'uzb': 'Uzbekistan',
|
||||
'vat': 'Vatican City',
|
||||
'vct': 'Saint Vincent and the Grenadines',
|
||||
'ven': 'Venezuela',
|
||||
'vgb': 'British Virgin Islands',
|
||||
'vir': 'Virgin Islands of the United States',
|
||||
'vnm': 'Vietnam',
|
||||
'vut': 'Vanuatu',
|
||||
'wlf': 'Wallis and Futuna',
|
||||
'wsm': 'Samoa',
|
||||
'yem': 'Yemen',
|
||||
'zaf': 'South Africa',
|
||||
'zmb': 'Zambia',
|
||||
'zwe': 'Zimbabwe',
|
||||
}
|
||||
|
||||
REVERSED_COUNTRIES_MAP = {country: code for code, country in COUNTRIES_MAP.items()}
|
||||
@@ -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,392 @@
|
||||
import enum
|
||||
import re
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from . import tvdb
|
||||
from .constants import COUNTRIES_MAP
|
||||
from .utils import logger, get_language, get_rating_country_code
|
||||
|
||||
|
||||
ARTWORK_URL_PREFIX = "https://artworks.thetvdb.com"
|
||||
|
||||
SUPPORTED_REMOTE_IDS = {
|
||||
'IMDB': 'imdb',
|
||||
'TheMovieDB.com': 'tmdb',
|
||||
}
|
||||
|
||||
MAX_IMAGES_NUMBER = 10
|
||||
|
||||
# Some skins use those names to display rating icons
|
||||
RATING_COUNTRY_MAP = {
|
||||
'gbr': 'UK',
|
||||
'nld': 'NL',
|
||||
}
|
||||
|
||||
|
||||
class ArtworkType(enum.IntEnum):
|
||||
POSTER = 14
|
||||
BACKGROUND = 15
|
||||
BANNER = 16
|
||||
ICON = 18
|
||||
CLEARART = 24
|
||||
CLEARLOGO = 25
|
||||
|
||||
|
||||
def search_movie(title, settings, handle, year=None) -> None:
|
||||
# add the found shows to the list
|
||||
|
||||
tvdb_client = tvdb.Client(settings)
|
||||
kwargs = {'limit': 10}
|
||||
if year is not None:
|
||||
kwargs['year'] = year
|
||||
search_results = tvdb_client.search(title, type="movie", **kwargs)
|
||||
if not search_results:
|
||||
return
|
||||
language = get_language(settings)
|
||||
items = []
|
||||
for movie in search_results:
|
||||
name = None
|
||||
translations = movie.get('translations') or {}
|
||||
if translations:
|
||||
name = translations.get(language)
|
||||
if not name:
|
||||
translations.get('eng')
|
||||
if not name:
|
||||
name = movie['name']
|
||||
if movie.get('year'):
|
||||
name += f' ({movie["year"]})'
|
||||
liz = xbmcgui.ListItem(name, offscreen=True)
|
||||
url = str(movie['tvdb_id'])
|
||||
is_folder = True
|
||||
items.append((url, liz, is_folder))
|
||||
xbmcplugin.addDirectoryItems(
|
||||
handle,
|
||||
items,
|
||||
len(items)
|
||||
)
|
||||
|
||||
|
||||
def get_movie_details(id, settings, handle):
|
||||
# get the details of the found series
|
||||
tvdb_client = tvdb.Client(settings)
|
||||
|
||||
language = get_language(settings)
|
||||
movie = tvdb_client.get_movie_details_api(id, language=language)
|
||||
if not movie:
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle, False, xbmcgui.ListItem(offscreen=True))
|
||||
return
|
||||
liz = xbmcgui.ListItem(movie["name"], offscreen=True)
|
||||
people = get_cast(movie)
|
||||
liz.setCast(people["cast"])
|
||||
genres = get_genres(movie)
|
||||
duration_minutes = movie.get('runtime') or 0
|
||||
details = {
|
||||
'title': movie["name"],
|
||||
'plot': movie["overview"],
|
||||
'plotoutline': movie["overview"],
|
||||
'mediatype': 'movie',
|
||||
'writer': people["writers"],
|
||||
'director': people["directors"],
|
||||
'genre': genres,
|
||||
'duration': duration_minutes * 60,
|
||||
}
|
||||
premiere_date = get_premiere_date(movie)
|
||||
if premiere_date is not None:
|
||||
details["year"] = premiere_date["year"]
|
||||
details["premiered"] = premiere_date["date"]
|
||||
rating_country_code = get_rating_country_code(settings)
|
||||
rating = get_rating(movie, rating_country_code)
|
||||
if rating:
|
||||
details["mpaa"] = rating
|
||||
|
||||
country = movie.get("originalCountry", None)
|
||||
if country:
|
||||
details["country"] = COUNTRIES_MAP.get(country, '')
|
||||
|
||||
studio = get_studio(movie)
|
||||
if studio:
|
||||
details["studio"] = studio
|
||||
|
||||
if settings.get('get_tags'):
|
||||
tags = get_tags(movie)
|
||||
if tags:
|
||||
details["tag"] = tags
|
||||
|
||||
trailer = get_trailer(movie)
|
||||
if trailer:
|
||||
details["trailer"] = trailer
|
||||
|
||||
set_ = get_set(movie)
|
||||
set_poster = None
|
||||
if set_:
|
||||
set_info = tvdb_client.get_movie_set_info(set_["id"], settings)
|
||||
details["set"] = set_info["name"]
|
||||
details["setoverview"] = set_info["overview"]
|
||||
first_movie_in_set_id = set_info["movie_id"]
|
||||
if first_movie_in_set_id:
|
||||
first_movie_in_set = tvdb_client.get_movie_details_api(first_movie_in_set_id,
|
||||
language=language)
|
||||
set_poster = first_movie_in_set["image"]
|
||||
liz.setInfo('video', details)
|
||||
|
||||
unique_ids = get_unique_ids(movie)
|
||||
liz.setUniqueIDs(unique_ids, 'tvdb')
|
||||
|
||||
add_artworks(movie, liz, set_poster, language=language)
|
||||
xbmcplugin.setResolvedUrl(handle=handle, succeeded=True, listitem=liz)
|
||||
|
||||
|
||||
def get_cast(movie):
|
||||
cast = []
|
||||
directors = []
|
||||
writers = []
|
||||
characters = movie.get('characters') or ()
|
||||
for char in characters:
|
||||
if char["peopleType"] == "Actor":
|
||||
d = {
|
||||
'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
|
||||
d['thumbnail'] = thumbnail
|
||||
cast.append(d)
|
||||
if char["peopleType"] == "Director":
|
||||
directors.append(char["personName"])
|
||||
if char["peopleType"] == "Writer":
|
||||
writers.append(char["personName"])
|
||||
return {
|
||||
"directors": directors,
|
||||
"writers": writers,
|
||||
"cast": cast,
|
||||
}
|
||||
|
||||
|
||||
def get_artworks_from_movie(movie: dict, language='eng') -> dict:
|
||||
|
||||
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 = movie.get("artworks") or ()
|
||||
posters = []
|
||||
backgrounds = []
|
||||
banners = []
|
||||
icons = []
|
||||
cleararts = []
|
||||
clearlogos = []
|
||||
for art in artworks:
|
||||
art_type = art.get('type')
|
||||
if art_type == ArtworkType.POSTER:
|
||||
posters.append(art)
|
||||
elif art_type == ArtworkType.BACKGROUND:
|
||||
backgrounds.append(art)
|
||||
elif art_type == ArtworkType.BANNER:
|
||||
banners.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)
|
||||
posters.sort(key=sorter, reverse=True)
|
||||
backgrounds.sort(key=sorter, reverse=True)
|
||||
banners.sort(key=sorter, reverse=True)
|
||||
icons.sort(key=sorter, reverse=True)
|
||||
cleararts.sort(key=sorter, reverse=True)
|
||||
clearlogos.sort(key=sorter, reverse=True)
|
||||
artwork_dict = {
|
||||
'poster': posters[:MAX_IMAGES_NUMBER],
|
||||
'fanart': backgrounds[:MAX_IMAGES_NUMBER],
|
||||
'banner': banners[:MAX_IMAGES_NUMBER],
|
||||
'icon': banners[:MAX_IMAGES_NUMBER],
|
||||
'clearart': cleararts[:MAX_IMAGES_NUMBER],
|
||||
'clearlogo': clearlogos[:MAX_IMAGES_NUMBER]
|
||||
}
|
||||
return artwork_dict
|
||||
|
||||
|
||||
def add_artworks(movie, liz, set_poster=None, language='eng'):
|
||||
|
||||
artworks = get_artworks_from_movie(movie, language=language)
|
||||
fanarts = artworks.pop('fanart')
|
||||
|
||||
if set_poster:
|
||||
liz.addAvailableArtwork(set_poster, 'set.poster')
|
||||
|
||||
for image_type, images in artworks.items():
|
||||
for image in images:
|
||||
image_url = image.get('image') or ''
|
||||
if ARTWORK_URL_PREFIX not in image_url:
|
||||
image_url = ARTWORK_URL_PREFIX + image_url
|
||||
liz.addAvailableArtwork(image_url, image_type)
|
||||
|
||||
fanart_items = []
|
||||
for fanart in fanarts:
|
||||
image = fanart.get("image", "")
|
||||
thumb = fanart["thumbnail"]
|
||||
if ARTWORK_URL_PREFIX not in image:
|
||||
image = ARTWORK_URL_PREFIX + image
|
||||
thumb = ARTWORK_URL_PREFIX + thumb
|
||||
fanart_items.append(
|
||||
{'image': image, 'preview': thumb})
|
||||
if fanarts:
|
||||
liz.setAvailableFanart(fanart_items)
|
||||
|
||||
|
||||
def get_artworks(id, settings, handle):
|
||||
tvdb_client = tvdb.Client(settings)
|
||||
movie = tvdb_client.get_series_details_api(id, settings)
|
||||
if not movie:
|
||||
xbmcplugin.setResolvedUrl(
|
||||
handle, False, xbmcgui.ListItem(offscreen=True))
|
||||
return
|
||||
liz = xbmcgui.ListItem(id, offscreen=True)
|
||||
language = get_language(settings)
|
||||
add_artworks(movie, liz, language=language)
|
||||
xbmcplugin.setResolvedUrl(handle=handle, succeeded=True, listitem=liz)
|
||||
|
||||
|
||||
def get_premiere_date(movie):
|
||||
releases = movie.get("releases")
|
||||
if not releases:
|
||||
return None
|
||||
if len(releases) > 1:
|
||||
releases.sort(key=lambda r: r['date'])
|
||||
date_str = releases[0]['date']
|
||||
year = int(date_str.split("-")[0])
|
||||
return {
|
||||
"year": year,
|
||||
"date": date_str,
|
||||
}
|
||||
|
||||
|
||||
def get_genres(movie):
|
||||
genres = movie.get('genres') or ()
|
||||
return [genre["name"] for genre in genres]
|
||||
|
||||
|
||||
def get_rating(movie, rating_country_code):
|
||||
ratings = movie.get("contentRatings")
|
||||
rating = ""
|
||||
if ratings:
|
||||
if len(ratings) == 1:
|
||||
rating = ratings[0]["name"]
|
||||
country_code = ratings[0]['country']
|
||||
if country_code in RATING_COUNTRY_MAP:
|
||||
country = RATING_COUNTRY_MAP[country_code]
|
||||
else:
|
||||
country = COUNTRIES_MAP.get(country_code)
|
||||
if country is not None and country_code != 'usa':
|
||||
rating = f'{country}:{rating}'
|
||||
else:
|
||||
rating = f'Rated {rating}'
|
||||
if not rating:
|
||||
usa_rating = ''
|
||||
local_rating = ''
|
||||
for rating in ratings:
|
||||
if rating['country'] == 'usa':
|
||||
usa_rating = f"Rated {rating['name']}"
|
||||
elif rating_country_code != 'usa' and rating['country'] == rating_country_code:
|
||||
local_rating = rating['name']
|
||||
country_code = rating['country']
|
||||
if country_code in RATING_COUNTRY_MAP:
|
||||
country = RATING_COUNTRY_MAP[country_code]
|
||||
else:
|
||||
country = COUNTRIES_MAP.get(country_code)
|
||||
if country is not None:
|
||||
local_rating = f'{country}:{local_rating}'
|
||||
rating = local_rating if local_rating else usa_rating
|
||||
return rating
|
||||
|
||||
|
||||
def get_studio(movie):
|
||||
studios = movie.get("studios", [])
|
||||
if not studios or len(studios) == 0:
|
||||
return None
|
||||
name = studios[0]["name"]
|
||||
return name
|
||||
|
||||
|
||||
def get_tags(movie):
|
||||
tags = []
|
||||
tag_options = movie.get("tagOptions", [])
|
||||
if tag_options:
|
||||
for tag in tag_options:
|
||||
tags.append(tag["name"])
|
||||
return tags
|
||||
|
||||
|
||||
def get_set(movie):
|
||||
lists = movie.get("lists", None)
|
||||
if not lists:
|
||||
return None
|
||||
|
||||
name = ""
|
||||
id = 0
|
||||
score = -1.0
|
||||
logger.debug(lists)
|
||||
for l in lists:
|
||||
if l["isOfficial"] and l["score"] > score:
|
||||
score = l["score"]
|
||||
name = l["name"]
|
||||
id = l["id"]
|
||||
if name and id:
|
||||
logger.debug("name and id in get set")
|
||||
logger.debug(name)
|
||||
logger.debug(id)
|
||||
return {
|
||||
"name": name,
|
||||
"id": id,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_trailer(movie):
|
||||
trailer_url = ""
|
||||
originalLang = movie.get("originalLanguage", None)
|
||||
if not originalLang:
|
||||
originalLang = "eng"
|
||||
|
||||
trailers = movie.get("trailers", None)
|
||||
if not trailers:
|
||||
return None
|
||||
|
||||
for trailer in trailers:
|
||||
if trailer["language"] == originalLang:
|
||||
trailer_url = trailer["url"]
|
||||
|
||||
match = re.search("youtube", trailer_url)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
trailer_id_match = re.search("\?v=[A-z]+", trailer_url)
|
||||
if not trailer_id_match:
|
||||
return None
|
||||
trailer_id = trailer_id_match.group(0)
|
||||
url = f'plugin://plugin.video.youtube/play/?video_id={trailer_id}'
|
||||
return url
|
||||
|
||||
|
||||
def get_unique_ids(movie):
|
||||
unique_ids = {'tvdb': movie['id']}
|
||||
remote_ids = movie.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,50 @@
|
||||
import re
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from .simple_requests import HTTPError
|
||||
from .tvdb import Request
|
||||
from .utils import logger
|
||||
|
||||
MOVIE_URL_REGEX = re.compile(r'https?://thetvdb.com/movies/([\w-]+)', re.I)
|
||||
TVDB_ID_HTML_REGEX = re.compile(r'<strong>TheTVDB\.com Movie ID</strong>\s+?<span>(\d+?)</span>', re.I)
|
||||
TVDB_ID_XML_REGEX = re.compile(r'<uniqueid type="tvdb"[^>]*?>(\d+?)</uniqueid>', re.I)
|
||||
|
||||
|
||||
def _get_tvdb_id_from_slug(movie_url):
|
||||
try:
|
||||
html = Request.make_web_request(movie_url)
|
||||
except HTTPError as exc:
|
||||
logger.error(str(exc))
|
||||
return None
|
||||
match = TVDB_ID_HTML_REGEX.search(html)
|
||||
if match is not None:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_movie_id_from_nfo(nfo, plugin_handle):
|
||||
logger.debug(f'Parsing NFO:\n{nfo}')
|
||||
movie_url_match = MOVIE_URL_REGEX.search(nfo)
|
||||
tvdb_id = None
|
||||
if movie_url_match is not None:
|
||||
movie_url = movie_url_match.group(0)
|
||||
tvdb_id = _get_tvdb_id_from_slug(movie_url)
|
||||
logger.debug(f'Movie matched by TheTVDB URL in NFO: {tvdb_id}')
|
||||
if tvdb_id is None:
|
||||
tvdb_id_xml_match = TVDB_ID_XML_REGEX.search(nfo)
|
||||
if tvdb_id_xml_match is not None:
|
||||
tvdb_id = tvdb_id_xml_match.group(1)
|
||||
logger.debug(f'Movie matched by uniqueid XML tag in NFO: {tvdb_id}')
|
||||
if tvdb_id is None:
|
||||
logger.debug('Unable to match the movie by NFO')
|
||||
return
|
||||
list_item = xbmcgui.ListItem(offscreen=True)
|
||||
list_item.setUniqueIDs({'tvdb': tvdb_id}, 'tvdb')
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=plugin_handle,
|
||||
url=tvdb_id,
|
||||
listitem=list_item,
|
||||
isFolder=True
|
||||
)
|
||||
@@ -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,241 @@
|
||||
import urllib.parse
|
||||
from pprint import pformat
|
||||
from typing import Optional
|
||||
|
||||
from . import simple_requests as requests
|
||||
from .simple_requests import HTTPError
|
||||
from .utils import logger
|
||||
|
||||
apikey = "edae60dc-1b44-4bac-8db7-65c0aaf5258b"
|
||||
apikey_with_pin = "51bdbd35-bcd5-40d9-9bc3-788e24454baf"
|
||||
|
||||
USER_AGENT = 'TheTVDB v.4 Movies Scraper for Kodi'
|
||||
|
||||
|
||||
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 API 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']
|
||||
logger.debug(f'API response:\n{pformat(data)}')
|
||||
self.cache[url] = data
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def make_web_request(url):
|
||||
logger.debug(f"about to make request to web 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_url(self, id, extended=False):
|
||||
url = "{}/artwork/{}".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 list_url(self, id, extended=False):
|
||||
url = "{}/lists/{}".format(self.base_url, id)
|
||||
if extended:
|
||||
url = "{}/extended".format(url)
|
||||
return url
|
||||
|
||||
def movie_translation_url(self, id, language="eng"):
|
||||
url = f'{self.base_url}/movies/{id}/translations/{language}'
|
||||
return url
|
||||
|
||||
def list_translation_url(self, id, language="eng"):
|
||||
url = f'{self.base_url}/lists/{id}/translations/{language}'
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
auth_token = self.auth.get_token()
|
||||
self.request = Request(auth_token)
|
||||
|
||||
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_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, id, 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_list_extended(self, id: int) -> dict:
|
||||
"""Returns a movie translation dictionary"""
|
||||
url = self.url.list_url(id, True)
|
||||
return self.request.make_api_request(url)
|
||||
|
||||
def get_list_translation(self, id: int, lang: str) -> Optional[dict]:
|
||||
"""Returns a movie translation dictionary"""
|
||||
url = self.url.list_translation_url(id, lang)
|
||||
result = self.request.make_api_request(url)
|
||||
if result:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
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_movie_details_api(self, id, language="eng") -> dict:
|
||||
try:
|
||||
movie = self.get_movie_extended(id)
|
||||
except HTTPError as exc:
|
||||
logger.error(str(exc))
|
||||
return {}
|
||||
try:
|
||||
english_translation = self.get_movie_translation(id, 'eng')
|
||||
except HTTPError:
|
||||
movie['overview'] = ''
|
||||
else:
|
||||
movie['overview'] = english_translation.get('overview') or ''
|
||||
if language != 'eng':
|
||||
try:
|
||||
translation = self.get_movie_translation(id, language)
|
||||
except HTTPError as exc:
|
||||
logger.debug(str(exc))
|
||||
pass
|
||||
else:
|
||||
translated_name = translation.get("name")
|
||||
if translated_name:
|
||||
movie["name"] = translated_name
|
||||
translated_overview = translation.get("overview")
|
||||
if translated_overview:
|
||||
movie["overview"] = translated_overview
|
||||
return movie
|
||||
|
||||
def get_movie_set_info(self, id, settings):
|
||||
list_ = self.get_list_extended(id)
|
||||
lang = settings.get("language", "eng")
|
||||
name = list_.get("name", "")
|
||||
overview = list_.get('overview', '')
|
||||
try:
|
||||
trans = self.get_list_translation(id, lang)
|
||||
if trans:
|
||||
name = trans.get("name") or name
|
||||
overview = trans.get("overview") or overview
|
||||
except requests.HTTPError:
|
||||
pass
|
||||
movie_id = None
|
||||
entities = list_.get("entities", [])
|
||||
if not entities:
|
||||
return None
|
||||
for item in entities:
|
||||
if item["movieId"] is not None:
|
||||
movie_id = item["movieId"]
|
||||
break
|
||||
return {
|
||||
"movie_id": movie_id,
|
||||
"name": name,
|
||||
"overview": overview,
|
||||
}
|
||||
|
||||
|
||||
class Client:
|
||||
_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
|
||||
@@ -0,0 +1,50 @@
|
||||
import uuid
|
||||
|
||||
import xbmc
|
||||
from xbmcaddon import Addon
|
||||
|
||||
from .constants import LANGUAGES_MAP, REVERSED_COUNTRIES_MAP
|
||||
|
||||
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(message):
|
||||
logger.log(message, xbmc.LOGDEBUG)
|
||||
|
||||
|
||||
def create_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
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_rating_country_code(path_settings):
|
||||
rating_country = path_settings.get('rating_country')
|
||||
if rating_country is None:
|
||||
rating_country = ADDON.getSetting('rating_country') or 'USA'
|
||||
return REVERSED_COUNTRIES_MAP[rating_country]
|
||||
Reference in New Issue
Block a user