Updated kodi settings on Lenovo

This commit is contained in:
2026-03-22 22:28:43 +01:00
parent 725dfa7157
commit 32b5a81da6
10925 changed files with 575678 additions and 5511 deletions

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de_DE\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Sprache"
msgctxt "#30001"
msgid "Season Type"
msgstr "Staffel Typ"
msgctxt "#30002"
msgid "Default"
msgstr "Standard"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absolut (Eine Staffel)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternativ (PIN benötigt)"
msgctxt "#30006"
msgid "Gender"
msgstr "Geschlecht"
msgctxt "#30007"
msgid "Male"
msgstr "Männlich"
msgctxt "#30008"
msgid "Female"
msgstr "Weiblich"
msgctxt "#30009"
msgid "Other"
msgstr "Geburtsjahr"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Geburtsjahr"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_AU\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Language"
msgctxt "#30001"
msgid "Season Type"
msgstr "Season Type"
msgctxt "#30002"
msgid "Default"
msgstr "Default"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absolute (Single Season)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternate (Requires PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "Gender"
msgctxt "#30007"
msgid "Male"
msgstr "Male"
msgctxt "#30008"
msgid "Female"
msgstr "Female"
msgctxt "#30009"
msgid "Other"
msgstr "Other"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Birth Year"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,73 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvdb.com
# Addon Provider: XBMC Foundation
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_GB\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Language"
msgctxt "#30001"
msgid "Season Type"
msgstr "Season Type"
msgctxt "#30002"
msgid "Default"
msgstr "Default"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absolute (Single Season)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternate (Requires PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "Gender"
msgctxt "#30007"
msgid "Male"
msgstr "Male"
msgctxt "#30008"
msgid "Female"
msgstr "Female"
msgctxt "#30009"
msgid "Other"
msgstr "Other"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Birth Year"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"
msgctxt "#30012"
msgid "Get tags"
msgstr ""
msgctxt "#30013"
msgid "Preferred content rating country"
msgstr ""

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvdb.com
# Addon Provider: XBMC Foundation
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_US\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Language"
msgctxt "#30001"
msgid "Season Type"
msgstr "Season Type"
msgctxt "#30002"
msgid "Default"
msgstr "Default"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absolute (Single Season)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternate (Requires PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "Gender"
msgctxt "#30007"
msgid "Male"
msgstr "Male"
msgctxt "#30008"
msgid "Female"
msgstr "Female"
msgctxt "#30009"
msgid "Other"
msgstr "Other"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Birth Year"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr_FR\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Langue"
msgctxt "#30001"
msgid "Season Type"
msgstr "Type de Saison"
msgctxt "#30002"
msgid "Default"
msgstr "Défaut"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absolu (Saison Unique)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternatif (Nécessite un PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "Genre"
msgctxt "#30007"
msgid "Male"
msgstr "Homme"
msgctxt "#30008"
msgid "Female"
msgstr "Femme"
msgctxt "#30009"
msgid "Other"
msgstr "Autre"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Année de naissance"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: he_IL\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "שפה"
msgctxt "#30001"
msgid "Season Type"
msgstr "סוג עונה"
msgctxt "#30002"
msgid "Default"
msgstr "ברירת מחדל"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "אבסולוטי (עונה יחידה)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "אלטרנטיבי (מצריך PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "מגדר"
msgctxt "#30007"
msgid "Male"
msgstr "זכר"
msgctxt "#30008"
msgid "Female"
msgstr "נקבה"
msgctxt "#30009"
msgid "Other"
msgstr "אחר"
msgctxt "#30010"
msgid "Birth Year"
msgstr "שנת לידה"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt_BR\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Idioma"
msgctxt "#30001"
msgid "Season Type"
msgstr "Tipo de Temporada"
msgctxt "#30002"
msgid "Default"
msgstr "Padrão"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absoluto (Temporada Única)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternativo (Requer um PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "Género"
msgctxt "#30007"
msgid "Male"
msgstr "Masculino"
msgctxt "#30008"
msgid "Female"
msgstr "Feminino"
msgctxt "#30009"
msgid "Other"
msgstr "Outro"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Ano de Nascimento"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt_PT\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Idioma"
msgctxt "#30001"
msgid "Season Type"
msgstr "Tipo de Temporada"
msgctxt "#30002"
msgid "Default"
msgstr "Predefinição"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absoluto (Temporada Única)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternativo (Requer um PIN)"
msgctxt "#30006"
msgid "Gender"
msgstr "Género"
msgctxt "#30007"
msgid "Male"
msgstr "Masculino"
msgctxt "#30008"
msgid "Female"
msgstr "Feminino"
msgctxt "#30009"
msgid "Other"
msgstr "Outro"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Ano de Nascimento"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -0,0 +1,66 @@
# Kodi Media Center language file
# Addon Name: The TVDB
# Addon id: metadata.tvshows.thetvdb.com.v4.python
# Addon Provider: TVDB Team
msgid ""
msgstr ""
"Project-Id-Version: XBMC Addons\n"
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Kodi Translation Team\n"
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: tr_TR\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgctxt "#30000"
msgid "Language"
msgstr "Dil"
msgctxt "#30001"
msgid "Season Type"
msgstr "Season Type"
msgctxt "#30002"
msgid "Default"
msgstr "Kusur"
msgctxt "#30003"
msgid "Absolute (Single Season)"
msgstr "Absolute (Single Season)"
msgctxt "#30004"
msgid "DVD"
msgstr "DVD"
msgctxt "#30005"
msgid "Alternate (Requires PIN)"
msgstr "Alternatif"
msgctxt "#30006"
msgid "Gender"
msgstr "Cinsiyet"
msgctxt "#30007"
msgid "Male"
msgstr "Erkek"
msgctxt "#30008"
msgid "Female"
msgstr "Erkek"
msgctxt "#30009"
msgid "Other"
msgstr "Other"
msgctxt "#30010"
msgid "Birth Year"
msgstr "Doğum yılı"
msgctxt "#30011"
msgid "PIN"
msgstr "PIN"

View File

@@ -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)

View File

@@ -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()}

View File

@@ -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

View File

@@ -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

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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]

View File

@@ -0,0 +1,317 @@
<?xml version="1.0" ?>
<settings version="1">
<section id="metadata.tvshows.thetvdb.com.v4.python">
<category id="general" label="128" help="">
<group id="1">
<setting id="language" type="string" label="30000" help="">
<level>0</level>
<default>English</default>
<constraints>
<options>
<option>Abkhaz</option>
<option>Afar</option>
<option>Afrikaans</option>
<option>Akan</option>
<option>Albanian</option>
<option>Amharic</option>
<option>Arabic</option>
<option>Aragonese</option>
<option>Armenian</option>
<option>Assamese</option>
<option>Avaric</option>
<option>Avestan</option>
<option>Aymara</option>
<option>Azerbaijani</option>
<option>Bambara</option>
<option>Bashkir</option>
<option>Basque</option>
<option>Belarusian</option>
<option>Bengali</option>
<option>Bihari</option>
<option>Bislama</option>
<option>Bosnian</option>
<option>Breton</option>
<option>Bulgarian</option>
<option>Burmese</option>
<option>Catalan</option>
<option>Chamorro</option>
<option>Chechen</option>
<option>Chichewa</option>
<option>Chinese - Cantonese</option>
<option>Chinese - China</option>
<option>Chinese - Taiwan</option>
<option>Chuvash</option>
<option>Cornish</option>
<option>Corsican</option>
<option>Cree</option>
<option>Croatian</option>
<option>Czech</option>
<option>Danish</option>
<option>Divehi</option>
<option>Dutch</option>
<option>Dzongkha</option>
<option>English</option>
<option>Esperanto</option>
<option>Estonian</option>
<option>Ewe</option>
<option>Faroese</option>
<option>Fijian</option>
<option>Finnish</option>
<option>French</option>
<option>Fula</option>
<option>Galician</option>
<option>Georgian</option>
<option>German</option>
<option>Greek</option>
<option>Guaraní</option>
<option>Gujarati</option>
<option>Haitian</option>
<option>Hausa</option>
<option>Hebrew</option>
<option>Herero</option>
<option>Hindi</option>
<option>Hiri Motu</option>
<option>Hungarian</option>
<option>Icelandic</option>
<option>Ido</option>
<option>Igbo</option>
<option>Indonesian</option>
<option>Interlingua</option>
<option>Interlingue</option>
<option>Inuktitut</option>
<option>Inupiaq</option>
<option>Irish</option>
<option>Italian</option>
<option>Japanese</option>
<option>Javanese</option>
<option>Kalaallisut</option>
<option>Kannada</option>
<option>Kanuri</option>
<option>Kashmiri</option>
<option>Kazakh</option>
<option>Khmer</option>
<option>Kikuyu</option>
<option>Kinyarwanda</option>
<option>Kirghiz</option>
<option>Kirundi</option>
<option>Komi</option>
<option>Kongo</option>
<option>Korean</option>
<option>Kurdish</option>
<option>Kwanyama</option>
<option>Lao</option>
<option>Latin</option>
<option>Latvian</option>
<option>Limburgish</option>
<option>Lingala</option>
<option>Lithuanian</option>
<option>Luba-Katanga</option>
<option>Luganda</option>
<option>Luxembourgish</option>
<option>Macedonian</option>
<option>Malagasy</option>
<option>Malay</option>
<option>Malayalam</option>
<option>Maltese</option>
<option>Manx</option>
<option>Marathi</option>
<option>Marshallese</option>
<option>Mongolian</option>
<option>Māori</option>
<option>Nauru</option>
<option>Navajo</option>
<option>Ndonga</option>
<option>Nepali</option>
<option>North Ndebele</option>
<option>Northern Sami</option>
<option>Norwegian</option>
<option>Nuosu</option>
<option>Occitan</option>
<option>Ojibwe</option>
<option>Old Church Slavonic</option>
<option>Oriya</option>
<option>Oromo</option>
<option>Ossetian</option>
<option>Panjabi</option>
<option>Pashto</option>
<option>Persian</option>
<option>Polish</option>
<option>Portuguese - Brazil</option>
<option>Portuguese - Portugal</option>
<option>Pāli</option>
<option>Quechua</option>
<option>Romanian</option>
<option>Romansh</option>
<option>Russian</option>
<option>Samoan</option>
<option>Sango</option>
<option>Sanskrit</option>
<option>Sardinian</option>
<option>Scottish Gaelic</option>
<option>Serbian</option>
<option>Shona</option>
<option>Sindhi</option>
<option>Sinhala</option>
<option>Slovak</option>
<option>Slovene</option>
<option>Somali</option>
<option>South Ndebele</option>
<option>Southern Sotho</option>
<option>Spanish</option>
<option>Sundanese</option>
<option>Swahili</option>
<option>Swati</option>
<option>Swedish</option>
<option>Tagalog</option>
<option>Tahitian</option>
<option>Tajik</option>
<option>Tamil</option>
<option>Tatar</option>
<option>Telugu</option>
<option>Thai</option>
<option>Tibetan Standard</option>
<option>Tigrinya</option>
<option>Tonga</option>
<option>Tsonga</option>
<option>Tswana</option>
<option>Turkish</option>
<option>Turkmen</option>
<option>Twi</option>
<option>Uighur</option>
<option>Ukrainian</option>
<option>Urdu</option>
<option>Uzbek</option>
<option>Venda</option>
<option>Vietnamese</option>
<option>Volapük</option>
<option>Walloon</option>
<option>Welsh</option>
<option>Western Frisian</option>
<option>Wolof</option>
<option>Xhosa</option>
<option>Yiddish</option>
<option>Yoruba</option>
<option>Zhuang</option>
<option>Zulu</option>
</options>
</constraints>
<control type="list" format="string">
<heading>30000</heading>
</control>
</setting>
<setting id="rating_country" type="string" label="30013" help="">
<level>0</level>
<default>USA</default>
<constraints>
<options>
<option>Argentina</option>
<option>Armenia</option>
<option>Australia</option>
<option>Austria</option>
<option>Belgium</option>
<option>Brazil</option>
<option>Bulgaria</option>
<option>Cambodia</option>
<option>Canada</option>
<option>Chile</option>
<option>Colombia</option>
<option>Croatia</option>
<option>Denmark</option>
<option>Ecuador</option>
<option>El Salvador</option>
<option>Estonia</option>
<option>Finland</option>
<option>France</option>
<option>Germany</option>
<option>Great Britain</option>
<option>Greece</option>
<option>Hong Kong</option>
<option>Hungary</option>
<option>Iceland</option>
<option>India</option>
<option>Indonesia</option>
<option>Ireland</option>
<option>Israel</option>
<option>Italy</option>
<option>Jamaica</option>
<option>Japan</option>
<option>Kazakhstan</option>
<option>Latvia</option>
<option>Lithuania</option>
<option>Malaysia</option>
<option>Maldives</option>
<option>Malta</option>
<option>Mexico</option>
<option>Morocco</option>
<option>New Zealand</option>
<option>Nigeria</option>
<option>Norway</option>
<option>Peru</option>
<option>Philippines</option>
<option>Poland</option>
<option>Portugal</option>
<option>Romania</option>
<option>Russia</option>
<option>Saudi Arabia</option>
<option>Singapore</option>
<option>Slovakia</option>
<option>Slovenia</option>
<option>South Africa</option>
<option>South Korea</option>
<option>Spain</option>
<option>Sweden</option>
<option>Taiwan</option>
<option>Thailand</option>
<option>The Netherlands</option>
<option>Turkey</option>
<option>Ukraine</option>
<option>United Arab Emirates</option>
<option>USA</option>
<option>Venezuela</option>
<option>Vietnam</option>
</options>
</constraints>
<control type="list" format="string">
<heading>30013</heading>
</control>
</setting>
<setting id="get_tags" type="boolean" label="30012" help="">
<level>0</level>
<default>true</default>
<control type="toggle"/>
</setting>
<setting id="gender" type="string" label="30006" help="">
<level>0</level>
<default>Other</default>
<constraints>
<options>
<option label="30007">Male</option>
<option label="30008">Female</option>
<option label="30009">Other</option>
</options>
</constraints>
<control type="list" format="string">
<heading>30003</heading>
</control>
</setting>
<setting id="year" type="integer" label="30010" help="">
<level>0</level>
<default>1900</default>
<control type="edit" format="integer">
<heading>30010</heading>
</control>
</setting>
<setting id="pin" type="string" label="30011" help="">
<level>0</level>
<default/>
<constraints>
<allowempty>true</allowempty>
</constraints>
<control type="edit" format="string">
<heading>30011</heading>
</control>
</setting>
</group>
</category>
</section>
</settings>