diff --git a/.pylintrc b/.pylintrc index 3d4c950..9c6363c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,6 +9,9 @@ disable= raise-missing-from, # This does not work on Python 2.7, remove later super-with-arguments, too-few-public-methods, + too-many-arguments, too-many-branches, + too-many-instance-attributes, + too-many-locals, max-line-length=160 max-statements=70 diff --git a/Makefile b/Makefile index 8f898a7..bd0737e 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,12 @@ check-translations: ) @tests/check_for_unused_translations.py +update-translations: + @echo -e "$(white)=$(blue) Updating languages$(reset)" + @-$(foreach lang,$(languages), \ + tests/update_translations.py resources/language/resource.language.$(lang)/strings.po resources/language/resource.language.en_gb/strings.po; \ + ) + check-addon: clean build @echo ">>> Running addon checks" $(eval TMPDIR := $(shell mktemp -d)) diff --git a/addon.xml b/addon.xml index 8cc8aa4..19a1816 100644 --- a/addon.xml +++ b/addon.xml @@ -3,17 +3,16 @@ + - - + executable - - + - + String.StartsWith(System.BuildVersion,18) + Window.IsVisible(tvguide) | Window.IsVisible(tvsearch) @@ -27,7 +26,7 @@ This add-on integrates IPTV Channels from other Add-ons in the Kodi PVR. Это дополнение интегрирует IPTV каналы из других дополнений в Kodi PVR. Ez a kiegészítő lehetővé teszi más kiegészítők számára, hogy saját IPTV csatornákat publikáljanak a Kodi PVR felületébe. - Το πρόσθετο αυτο ενσωματώνει τα κανάλια IPTV από άλλα πρόσθετα στο PVR του Kodi + Το πρόσθετο αυτο ενσωματώνει τα κανάλια IPTV από άλλα πρόσθετα στο PVR του Kodi. all GPL-3.0-only v0.2.2 (2020-12-07) diff --git a/addon_entry.py b/addon_entry.py new file mode 100644 index 0000000..f2ba730 --- /dev/null +++ b/addon_entry.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Addon entry point""" + +from __future__ import absolute_import, division, unicode_literals + +from xbmcaddon import Addon + +from resources.lib import kodiutils, kodilogging + +# Reinitialise ADDON every invocation to fix an issue that settings are not fresh. +kodiutils.ADDON = Addon() +kodilogging.ADDON = Addon() + +if __name__ == '__main__': + from sys import argv + from resources.lib.addon import run + + run(argv) diff --git a/context.py b/context_entry.py similarity index 59% rename from context.py rename to context_entry.py index 1b4a35a..bead060 100644 --- a/context.py +++ b/context_entry.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, unicode_literals -from resources.lib.functions import run +if __name__ == '__main__': + from resources.lib.addon import run -run([-1, 'play_from_contextmenu']) + run(['/play']) diff --git a/default.py b/default.py deleted file mode 100644 index 3fc7da6..0000000 --- a/default.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -"""Script entry point""" - -from __future__ import absolute_import, division, unicode_literals -import sys -from resources.lib.functions import run - -if len(sys.argv) > 1: - run(sys.argv) -else: - run([-1, 'open_settings']) diff --git a/requirements.txt b/requirements.txt index 8f506c6..eaa662a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ pytest-cov pytest-timeout python-dateutil mock +git+git://github.com/dagwieers/kodi-plugin-routing.git@setup#egg=routing #sakee git+git://github.com/retrospect-addon/kodi.emulator.ascii.git@master#egg=sakee diff --git a/resources/language/resource.language.el_gr/strings.po b/resources/language/resource.language.el_gr/strings.po index 0db791a..6e8581e 100644 --- a/resources/language/resource.language.el_gr/strings.po +++ b/resources/language/resource.language.el_gr/strings.po @@ -1,17 +1,100 @@ +# Kodi Media Center language file +# Addon Name: IPTV Manager +# Addon id: service.iptv.manager msgid "" msgstr "" +"Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -### CONTEXT MENU +# MENU +msgctxt "#30001" +msgid "Refresh channels and guide now…" +msgstr "" + +msgctxt "#30002" +msgid "IPTV Manager Settings…" +msgstr "" + +msgctxt "#30003" +msgid "Manage Sources…" +msgstr "" + +msgctxt "#30004" +msgid "Sources" +msgstr "" + +msgctxt "#30010" +msgid "Supported Add-ons" +msgstr "" + +msgctxt "#30011" +msgid "Custom Sources" +msgstr "" + +msgctxt "#30012" +msgid "Add Source…" +msgstr "" + +msgctxt "#30013" +msgid "Delete Source" +msgstr "" + +msgctxt "#30020" +msgid "Name" +msgstr "" + +msgctxt "#30021" +msgid "Modify the name of this source." +msgstr "" + +msgctxt "#30022" +msgid "Enabled" +msgstr "" + +msgctxt "#30023" +msgid "Enable or disable this source." +msgstr "" + +msgctxt "#30024" +msgid "Yes" +msgstr "" + +msgctxt "#30025" +msgid "No" +msgstr "" + +msgctxt "#30026" +msgid "Playlist" +msgstr "" + +msgctxt "#30027" +msgid "Select the channel data in M3U format for this source." +msgstr "" + +msgctxt "#30028" +msgid "EPG" +msgstr "" + +msgctxt "#30029" +msgid "Select the EPG data in XMLTV format for this source." +msgstr "" + +msgctxt "#30030" +msgid "Enter URL…" +msgstr "" + +msgctxt "#30031" +msgid "Browse for file…" +msgstr "" + +# CONTEXT MENU msgctxt "#30600" msgid "Play with IPTV Manager" msgstr "Αναπαραγωγή με τον διαχειριστή IPTV" - -### MESSAGES +# MESSAGES msgctxt "#30700" msgid "Are you sure you want to setup IPTV Simple for the use with IPTV Manager? Any existing configuration of IPTV Simple will be overwritten." msgstr "Είστε σίγουροι ότι θέλετε να ρυθμίσετε το IPTV Simple για χρήση με τον διαχειριστή IPTV; Όλες οι υπάρχουσες ρυθμίσεις του IPTV simple θα επαναγραφτούν." @@ -40,8 +123,7 @@ msgctxt "#30706" msgid "This program isn't available to play." msgstr "Το πρόγραμμα αυτό είναι μη διαθέσιμο για αναπαραγωγή." - -### SETTINGS +# SETTINGS msgctxt "#30800" msgid "Channels" msgstr "Κανάλια" @@ -77,3 +159,23 @@ msgstr "Άνοιγμα των ρυθμίσεων του IPTV Simple" msgctxt "#30824" msgid "Automatically restart IPTV Simple to refresh the data" msgstr "Αυτόματη ανανέωση του IPTV Simple για ανανέωση των δεδομένων" + +msgctxt "#30880" +msgid "Expert" +msgstr "" + +msgctxt "#30881" +msgid "Logging" +msgstr "" + +msgctxt "#30882" +msgid "Enable debug logging" +msgstr "" + +msgctxt "#30883" +msgid "Install Kodi Logfile Uploader…" +msgstr "" + +msgctxt "#30884" +msgid "Open Kodi Logfile Uploader…" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index c976c65..0b6c1b9 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1,3 +1,6 @@ +# Kodi Media Center language file +# Addon Name: IPTV Manager +# Addon id: service.iptv.manager msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" @@ -5,13 +8,95 @@ msgstr "" "Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -### CONTEXT MENU +# MENU +msgctxt "#30001" +msgid "Refresh channels and guide now…" +msgstr "" + +msgctxt "#30002" +msgid "IPTV Manager Settings…" +msgstr "" + +msgctxt "#30003" +msgid "Manage Sources…" +msgstr "" + +msgctxt "#30004" +msgid "Sources" +msgstr "" + +msgctxt "#30010" +msgid "Supported Add-ons" +msgstr "" + +msgctxt "#30011" +msgid "Custom Sources" +msgstr "" + +msgctxt "#30012" +msgid "Add Source…" +msgstr "" + +msgctxt "#30013" +msgid "Delete Source" +msgstr "" + +msgctxt "#30020" +msgid "Name" +msgstr "" + +msgctxt "#30021" +msgid "Modify the name of this source." +msgstr "" + +msgctxt "#30022" +msgid "Enabled" +msgstr "" + +msgctxt "#30023" +msgid "Enable or disable this source." +msgstr "" + +msgctxt "#30024" +msgid "Yes" +msgstr "" + +msgctxt "#30025" +msgid "No" +msgstr "" + +msgctxt "#30026" +msgid "Playlist" +msgstr "" + +msgctxt "#30027" +msgid "Select the channel data in M3U format for this source." +msgstr "" + +msgctxt "#30028" +msgid "EPG" +msgstr "" + +msgctxt "#30029" +msgid "Select the EPG data in XMLTV format for this source." +msgstr "" + +msgctxt "#30030" +msgid "Enter URL…" +msgstr "" + +msgctxt "#30031" +msgid "Browse for file…" +msgstr "" + + +# CONTEXT MENU msgctxt "#30600" msgid "Play with IPTV Manager" msgstr "" -### MESSAGES +# MESSAGES msgctxt "#30700" msgid "Are you sure you want to setup IPTV Simple for the use with IPTV Manager? Any existing configuration of IPTV Simple will be overwritten." msgstr "" @@ -41,7 +126,7 @@ msgid "This program isn't available to play." msgstr "" -### SETTINGS +# SETTINGS msgctxt "#30800" msgid "Channels" msgstr "" @@ -77,3 +162,23 @@ msgstr "" msgctxt "#30824" msgid "Automatically restart IPTV Simple to refresh the data" msgstr "" + +msgctxt "#30880" +msgid "Expert" +msgstr "" + +msgctxt "#30881" +msgid "Logging" +msgstr "" + +msgctxt "#30882" +msgid "Enable debug logging" +msgstr "" + +msgctxt "#30883" +msgid "Install Kodi Logfile Uploader…" +msgstr "" + +msgctxt "#30884" +msgid "Open Kodi Logfile Uploader…" +msgstr "" diff --git a/resources/language/resource.language.hu_hu/strings.po b/resources/language/resource.language.hu_hu/strings.po index dabdd67..b328591 100644 --- a/resources/language/resource.language.hu_hu/strings.po +++ b/resources/language/resource.language.hu_hu/strings.po @@ -1,17 +1,100 @@ +# Kodi Media Center language file +# Addon Name: IPTV Manager +# Addon id: service.iptv.manager msgid "" msgstr "" +"Language: hu\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: hu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -### CONTEXT MENU +# MENU +msgctxt "#30001" +msgid "Refresh channels and guide now…" +msgstr "" + +msgctxt "#30002" +msgid "IPTV Manager Settings…" +msgstr "" + +msgctxt "#30003" +msgid "Manage Sources…" +msgstr "" + +msgctxt "#30004" +msgid "Sources" +msgstr "" + +msgctxt "#30010" +msgid "Supported Add-ons" +msgstr "" + +msgctxt "#30011" +msgid "Custom Sources" +msgstr "" + +msgctxt "#30012" +msgid "Add Source…" +msgstr "" + +msgctxt "#30013" +msgid "Delete Source" +msgstr "" + +msgctxt "#30020" +msgid "Name" +msgstr "" + +msgctxt "#30021" +msgid "Modify the name of this source." +msgstr "" + +msgctxt "#30022" +msgid "Enabled" +msgstr "" + +msgctxt "#30023" +msgid "Enable or disable this source." +msgstr "" + +msgctxt "#30024" +msgid "Yes" +msgstr "" + +msgctxt "#30025" +msgid "No" +msgstr "" + +msgctxt "#30026" +msgid "Playlist" +msgstr "" + +msgctxt "#30027" +msgid "Select the channel data in M3U format for this source." +msgstr "" + +msgctxt "#30028" +msgid "EPG" +msgstr "" + +msgctxt "#30029" +msgid "Select the EPG data in XMLTV format for this source." +msgstr "" + +msgctxt "#30030" +msgid "Enter URL…" +msgstr "" + +msgctxt "#30031" +msgid "Browse for file…" +msgstr "" + +# CONTEXT MENU msgctxt "#30600" msgid "Play with IPTV Manager" msgstr "Lejátszás ezzel: IPTV Manager" - -### MESSAGES +# MESSAGES msgctxt "#30700" msgid "Are you sure you want to setup IPTV Simple for the use with IPTV Manager? Any existing configuration of IPTV Simple will be overwritten." msgstr "Biztos szeretnéd, hogy az IPTV Simple ezentúl az IPTV Manager-t használja forrásként? A teljes jelenlegi konfiguráció felülírásra kerül." @@ -40,8 +123,7 @@ msgctxt "#30706" msgid "This program isn't available to play." msgstr "Ezt a műsort jelenleg nem lehet lejátszani." - -### SETTINGS +# SETTINGS msgctxt "#30800" msgid "Channels" msgstr "Csatornák" @@ -77,3 +159,23 @@ msgstr "IPTV Simple beállításainak megnyitása…" msgctxt "#30824" msgid "Automatically restart IPTV Simple to refresh the data" msgstr "IPTV Simple automatikus újraindítása, frissítés után" + +msgctxt "#30880" +msgid "Expert" +msgstr "" + +msgctxt "#30881" +msgid "Logging" +msgstr "" + +msgctxt "#30882" +msgid "Enable debug logging" +msgstr "" + +msgctxt "#30883" +msgid "Install Kodi Logfile Uploader…" +msgstr "" + +msgctxt "#30884" +msgid "Open Kodi Logfile Uploader…" +msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index f238250..f4554ed 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -1,18 +1,101 @@ +# Kodi Media Center language file +# Addon Name: IPTV Manager +# Addon id: service.iptv.manager msgid "" msgstr "" +"Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -### CONTEXT MENU +# MENU +msgctxt "#30001" +msgid "Refresh channels and guide now…" +msgstr "" + +msgctxt "#30002" +msgid "IPTV Manager Settings…" +msgstr "" + +msgctxt "#30003" +msgid "Manage Sources…" +msgstr "" + +msgctxt "#30004" +msgid "Sources" +msgstr "" + +msgctxt "#30010" +msgid "Supported Add-ons" +msgstr "" + +msgctxt "#30011" +msgid "Custom Sources" +msgstr "" + +msgctxt "#30012" +msgid "Add Source…" +msgstr "" + +msgctxt "#30013" +msgid "Delete Source" +msgstr "" + +msgctxt "#30020" +msgid "Name" +msgstr "" + +msgctxt "#30021" +msgid "Modify the name of this source." +msgstr "" + +msgctxt "#30022" +msgid "Enabled" +msgstr "" + +msgctxt "#30023" +msgid "Enable or disable this source." +msgstr "" + +msgctxt "#30024" +msgid "Yes" +msgstr "" + +msgctxt "#30025" +msgid "No" +msgstr "" + +msgctxt "#30026" +msgid "Playlist" +msgstr "" + +msgctxt "#30027" +msgid "Select the channel data in M3U format for this source." +msgstr "" + +msgctxt "#30028" +msgid "EPG" +msgstr "" + +msgctxt "#30029" +msgid "Select the EPG data in XMLTV format for this source." +msgstr "" + +msgctxt "#30030" +msgid "Enter URL…" +msgstr "" + +msgctxt "#30031" +msgid "Browse for file…" +msgstr "" + +# CONTEXT MENU msgctxt "#30600" msgid "Play with IPTV Manager" msgstr "Afspelen met IPTV Manager" - -### MESSAGES +# MESSAGES msgctxt "#30700" msgid "Are you sure you want to setup IPTV Simple for the use with IPTV Manager? Any existing configuration of IPTV Simple will be overwritten." msgstr "Bent u zeker om IPTV Simple in te stellen om te gebruiken met IPTV Manager? De bestaande configuratie van IPTV Simple zal worden overschreven." @@ -41,8 +124,7 @@ msgctxt "#30706" msgid "This program isn't available to play." msgstr "Dit programma is niet beschikbaar om af te spelen." - -### SETTINGS +# SETTINGS msgctxt "#30800" msgid "Channels" msgstr "Kanalen" @@ -78,3 +160,23 @@ msgstr "Open de IPTV Simple instellingen…" msgctxt "#30824" msgid "Automatically restart IPTV Simple to refresh the data" msgstr "Herstart IPTV Simple automatisch om de data te vernieuwen" + +msgctxt "#30880" +msgid "Expert" +msgstr "" + +msgctxt "#30881" +msgid "Logging" +msgstr "" + +msgctxt "#30882" +msgid "Enable debug logging" +msgstr "" + +msgctxt "#30883" +msgid "Install Kodi Logfile Uploader…" +msgstr "" + +msgctxt "#30884" +msgid "Open Kodi Logfile Uploader…" +msgstr "" diff --git a/resources/language/resource.language.ro_ro/strings.po b/resources/language/resource.language.ro_ro/strings.po index 4d95923..a0cdad5 100644 --- a/resources/language/resource.language.ro_ro/strings.po +++ b/resources/language/resource.language.ro_ro/strings.po @@ -1,17 +1,100 @@ +# Kodi Media Center language file +# Addon Name: IPTV Manager +# Addon id: service.iptv.manager msgid "" msgstr "" +"Language: ro\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ro\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -### CONTEXT MENU +# MENU +msgctxt "#30001" +msgid "Refresh channels and guide now…" +msgstr "" + +msgctxt "#30002" +msgid "IPTV Manager Settings…" +msgstr "" + +msgctxt "#30003" +msgid "Manage Sources…" +msgstr "" + +msgctxt "#30004" +msgid "Sources" +msgstr "" + +msgctxt "#30010" +msgid "Supported Add-ons" +msgstr "" + +msgctxt "#30011" +msgid "Custom Sources" +msgstr "" + +msgctxt "#30012" +msgid "Add Source…" +msgstr "" + +msgctxt "#30013" +msgid "Delete Source" +msgstr "" + +msgctxt "#30020" +msgid "Name" +msgstr "" + +msgctxt "#30021" +msgid "Modify the name of this source." +msgstr "" + +msgctxt "#30022" +msgid "Enabled" +msgstr "" + +msgctxt "#30023" +msgid "Enable or disable this source." +msgstr "" + +msgctxt "#30024" +msgid "Yes" +msgstr "" + +msgctxt "#30025" +msgid "No" +msgstr "" + +msgctxt "#30026" +msgid "Playlist" +msgstr "" + +msgctxt "#30027" +msgid "Select the channel data in M3U format for this source." +msgstr "" + +msgctxt "#30028" +msgid "EPG" +msgstr "" + +msgctxt "#30029" +msgid "Select the EPG data in XMLTV format for this source." +msgstr "" + +msgctxt "#30030" +msgid "Enter URL…" +msgstr "" + +msgctxt "#30031" +msgid "Browse for file…" +msgstr "" + +# CONTEXT MENU msgctxt "#30600" msgid "Play with IPTV Manager" msgstr "Redă cu IPTV Manager" - -### MESSAGES +# MESSAGES msgctxt "#30700" msgid "Are you sure you want to setup IPTV Simple for the use with IPTV Manager? Any existing configuration of IPTV Simple will be overwritten." msgstr "Sunteți sigur/(ă) că vreți să configurați IPTV Simple pentru a fi utilizat cu IPTV Manager? Dacă există vreo configurație a IPTV Simple, aceasta va fi suprascrisă." @@ -40,8 +123,7 @@ msgctxt "#30706" msgid "This program isn't available to play." msgstr "Acest program nu este disponibil pentru redare." - -### SETTINGS +# SETTINGS msgctxt "#30800" msgid "Channels" msgstr "Canale" @@ -77,3 +159,23 @@ msgstr "Deschide setările IPTV Simple..." msgctxt "#30824" msgid "Automatically restart IPTV Simple to refresh the data" msgstr "Repornește automat IPTV Simple pentru a actualiza datele" + +msgctxt "#30880" +msgid "Expert" +msgstr "" + +msgctxt "#30881" +msgid "Logging" +msgstr "" + +msgctxt "#30882" +msgid "Enable debug logging" +msgstr "" + +msgctxt "#30883" +msgid "Install Kodi Logfile Uploader…" +msgstr "" + +msgctxt "#30884" +msgid "Open Kodi Logfile Uploader…" +msgstr "" diff --git a/resources/language/resource.language.ru_ru/strings.po b/resources/language/resource.language.ru_ru/strings.po index d27d364..5c66600 100644 --- a/resources/language/resource.language.ru_ru/strings.po +++ b/resources/language/resource.language.ru_ru/strings.po @@ -1,17 +1,100 @@ +# Kodi Media Center language file +# Addon Name: IPTV Manager +# Addon id: service.iptv.manager msgid "" msgstr "" +"Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ru\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -### CONTEXT MENU +# MENU +msgctxt "#30001" +msgid "Refresh channels and guide now…" +msgstr "" + +msgctxt "#30002" +msgid "IPTV Manager Settings…" +msgstr "" + +msgctxt "#30003" +msgid "Manage Sources…" +msgstr "" + +msgctxt "#30004" +msgid "Sources" +msgstr "" + +msgctxt "#30010" +msgid "Supported Add-ons" +msgstr "" + +msgctxt "#30011" +msgid "Custom Sources" +msgstr "" + +msgctxt "#30012" +msgid "Add Source…" +msgstr "" + +msgctxt "#30013" +msgid "Delete Source" +msgstr "" + +msgctxt "#30020" +msgid "Name" +msgstr "" + +msgctxt "#30021" +msgid "Modify the name of this source." +msgstr "" + +msgctxt "#30022" +msgid "Enabled" +msgstr "" + +msgctxt "#30023" +msgid "Enable or disable this source." +msgstr "" + +msgctxt "#30024" +msgid "Yes" +msgstr "" + +msgctxt "#30025" +msgid "No" +msgstr "" + +msgctxt "#30026" +msgid "Playlist" +msgstr "" + +msgctxt "#30027" +msgid "Select the channel data in M3U format for this source." +msgstr "" + +msgctxt "#30028" +msgid "EPG" +msgstr "" + +msgctxt "#30029" +msgid "Select the EPG data in XMLTV format for this source." +msgstr "" + +msgctxt "#30030" +msgid "Enter URL…" +msgstr "" + +msgctxt "#30031" +msgid "Browse for file…" +msgstr "" + +# CONTEXT MENU msgctxt "#30600" msgid "Play with IPTV Manager" msgstr "Воспроизвести в IPTV Manager" - -### MESSAGES +# MESSAGES msgctxt "#30700" msgid "Are you sure you want to setup IPTV Simple for the use with IPTV Manager? Any existing configuration of IPTV Simple will be overwritten." msgstr "Вы уверены, что хотите настроить IPTV Simple для работы с IPTV Manager? Существующие параметры IPTV Simple будут перезаписаны." @@ -40,8 +123,7 @@ msgctxt "#30706" msgid "This program isn't available to play." msgstr "" - -### SETTINGS +# SETTINGS msgctxt "#30800" msgid "Channels" msgstr "ТВ каналы" @@ -77,3 +159,23 @@ msgstr "Открыть настройки IPTV Simple…" msgctxt "#30824" msgid "Automatically restart IPTV Simple to refresh the data" msgstr "Автоматически перезапускать IPTV Simple при обновлении данных" + +msgctxt "#30880" +msgid "Expert" +msgstr "" + +msgctxt "#30881" +msgid "Logging" +msgstr "" + +msgctxt "#30882" +msgid "Enable debug logging" +msgstr "" + +msgctxt "#30883" +msgid "Install Kodi Logfile Uploader…" +msgstr "" + +msgctxt "#30884" +msgid "Open Kodi Logfile Uploader…" +msgstr "" diff --git a/resources/lib/addon.py b/resources/lib/addon.py new file mode 100644 index 0000000..48398a2 --- /dev/null +++ b/resources/lib/addon.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" Addon code """ + +from __future__ import absolute_import, division, unicode_literals + +import logging + +import routing + +from resources.lib import kodilogging + +routing = routing.Plugin() # pylint: disable=invalid-name + +_LOGGER = logging.getLogger(__name__) + + +@routing.route('/') +def show_main_menu(): + """ Show the main menu """ + from resources.lib.modules.menu import Menu + Menu().show_mainmenu() + + +@routing.route('/settings') +def show_settings(): + """ Show the sources menu """ + from resources.lib.modules.menu import Menu + Menu().show_settings() + + +@routing.route('/sources') +def show_sources(): + """ Show the sources menu """ + from resources.lib.modules.menu import Menu + Menu().show_sources() + + +@routing.route('/sources/add') +def add_source(): + """ Add a source """ + from resources.lib.modules.menu import Menu + Menu().add_source() + + +@routing.route('/sources/edit/') +@routing.route('/sources/edit//') +def edit_source(uuid, edit=None): + """ Edit a source """ + from resources.lib.modules.menu import Menu + Menu().edit_source(uuid, edit) + + +@routing.route('/sources/delete/') +def delete_source(uuid): + """ Delete a source """ + from resources.lib.modules.menu import Menu + Menu().delete_source(uuid) + + +@routing.route('/sources/refresh') +def refresh(): + """ Show the sources menu """ + from resources.lib.modules.menu import Menu + Menu().refresh() + + +@routing.route('/sources/enable/') +def enable_source(addon_id): + """ Show the sources menu """ + from resources.lib.modules.menu import Menu + Menu().enable_addon(addon_id) + + +@routing.route('/sources/disable/') +def disable_source(addon_id): + """ Show the sources menu """ + from resources.lib.modules.menu import Menu + Menu().disable_addon(addon_id) + + +@routing.route('/install') +def install(): + """ Setup IPTV Simple """ + from resources.lib.modules.menu import Menu + Menu().show_install() + + +@routing.route('/play') +def play(): + """ Play from Context Menu (used in Kodi 18) """ + from resources.lib.modules.contextmenu import ContextMenu + ContextMenu().play() + + +def run(params): + """ Run the routing plugin """ + kodilogging.config() + routing.run(params) diff --git a/resources/lib/functions.py b/resources/lib/functions.py deleted file mode 100644 index 560107a..0000000 --- a/resources/lib/functions.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -"""Functions code""" - -from __future__ import absolute_import, division, unicode_literals - -import logging - -from resources.lib import kodilogging, kodiutils -from resources.lib.modules.addon import Addon -from resources.lib.modules.contextmenu import ContextMenu -from resources.lib.modules.iptvsimple import IptvSimple - -_LOGGER = logging.getLogger(__name__) - - -def setup_iptv_simple(): - """Setup IPTV Simple""" - reply = kodiutils.yesno_dialog(message=kodiutils.localize(30700)) # Are you sure... - if reply: - if IptvSimple.setup(): - kodiutils.ok_dialog(message=kodiutils.localize(30701)) # The configuration of IPTV Simple is completed! - else: - kodiutils.ok_dialog(message=kodiutils.localize(30702)) # The configuration of IPTV Simple has failed! - - # Open settings again - kodiutils.open_settings() - - -def refresh(): - """Refresh the channels and EPG""" - Addon.refresh(True) - - # Open settings again - kodiutils.open_settings() - - -def play_from_contextmenu(): - """Play an item from the Context Menu in Kodi 18""" - stream = ContextMenu.get_direct_uri() - if stream is None: - kodiutils.ok_dialog(message=kodiutils.localize(30706)) - return - - _LOGGER.debug('Playing using direct URI: %s', stream) - kodiutils.execute_builtin('PlayMedia', stream) - - -def open_settings(): - """Open the settings for IPTV Manager""" - kodiutils.open_settings() - - -def run(args): - """Run the function""" - kodilogging.config() - - function = args[1] - function_map = { - 'setup-iptv-simple': setup_iptv_simple, - 'refresh': refresh, - 'play_from_contextmenu': play_from_contextmenu, - 'open_settings': open_settings, - } - try: - # TODO: allow to pass *args to the function so we can also pass arguments - _LOGGER.debug('Routing to function: %s', function) - function_map.get(function)() - except (TypeError, IndexError): - _LOGGER.error('Could not route to %s', function) - raise diff --git a/resources/lib/kodilogging.py b/resources/lib/kodilogging.py index c85dfa8..f4c470d 100644 --- a/resources/lib/kodilogging.py +++ b/resources/lib/kodilogging.py @@ -8,37 +8,51 @@ import xbmc import xbmcaddon +from resources.lib import kodiutils + +ADDON = xbmcaddon.Addon() + class KodiLogHandler(logging.StreamHandler): - """A log handler for Kodi""" + """ A log handler for Kodi """ def __init__(self): logging.StreamHandler.__init__(self) - addon_id = xbmcaddon.Addon().getAddonInfo("id") - formatter = logging.Formatter("[{}] [%(name)s] %(message)s".format(addon_id)) + formatter = logging.Formatter("[{}] [%(name)s] %(message)s".format(ADDON.getAddonInfo("id"))) self.setFormatter(formatter) + # xbmc.LOGNOTICE is deprecated in Kodi 19 Matrix + if kodiutils.kodi_version_major() > 18: + self.info_level = xbmc.LOGINFO + else: + self.info_level = xbmc.LOGNOTICE def emit(self, record): - """Emit a log message""" + """ Emit a log message """ levels = { logging.CRITICAL: xbmc.LOGFATAL, logging.ERROR: xbmc.LOGERROR, logging.WARNING: xbmc.LOGWARNING, - logging.INFO: xbmc.LOGINFO, + logging.INFO: self.info_level, logging.DEBUG: xbmc.LOGDEBUG, logging.NOTSET: xbmc.LOGNONE, } + + # Map DEBUG level to info_level if debug logging setting has been activated + # This is for troubleshooting only + if ADDON.getSetting('debug_logging') == 'true': + levels[logging.DEBUG] = self.info_level + try: xbmc.log(self.format(record), levels[record.levelno]) except UnicodeEncodeError: xbmc.log(self.format(record).encode('utf-8', 'ignore'), levels[record.levelno]) def flush(self): - """Flush the messages""" + """ Flush the messages """ def config(): - """Setup the logger with this handler""" + """ Setup the logger with this handler """ logger = logging.getLogger() + logger.setLevel(logging.DEBUG) # Make sure we pass all messages, Kodi will do some filtering itself. logger.addHandler(KodiLogHandler()) - logger.setLevel(logging.DEBUG) diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index a1aa913..29064a9 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -8,13 +8,58 @@ import xbmc import xbmcaddon import xbmcgui +import xbmcplugin import xbmcvfs ADDON = xbmcaddon.Addon() +SORT_METHODS = dict( + unsorted=xbmcplugin.SORT_METHOD_UNSORTED, + label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS, + title=xbmcplugin.SORT_METHOD_TITLE, + episode=xbmcplugin.SORT_METHOD_EPISODE, + duration=xbmcplugin.SORT_METHOD_DURATION, + year=xbmcplugin.SORT_METHOD_VIDEO_YEAR, + date=xbmcplugin.SORT_METHOD_DATE, +) +DEFAULT_SORT_METHODS = [ + 'unsorted', 'title' +] + _LOGGER = logging.getLogger(__name__) +class TitleItem: + """ This helper object holds all information to be used with Kodi xbmc's ListItem object """ + + def __init__(self, title, path=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None, context_menu=None, subtitles_path=None, + is_playable=False): + """ The constructor for the TitleItem class. + + :param str title: + :param str path: + :param dict art_dict: + :param dict info_dict: + :param dict prop_dict: + :param dict stream_dict: + :param list[tuple[str, str]] context_menu: + :param list[str] subtitles_path: + :param bool is_playable: + """ + self.title = title + self.path = path + self.art_dict = art_dict + self.info_dict = info_dict + self.stream_dict = stream_dict + self.prop_dict = prop_dict + self.context_menu = context_menu + self.subtitles_path = subtitles_path + self.is_playable = is_playable + + def __repr__(self): + return "%r" % self.__dict__ + + class SafeDict(dict): """A safe dictionary implementation that does not break down on missing keys""" @@ -68,6 +113,88 @@ def addon_profile(addon=None): return to_unicode(xbmc.translatePath(addon.getAddonInfo('profile'))) +def url_for(name, *args, **kwargs): + """Wrapper for routing.url_for() to lookup by name""" + import resources.lib.addon as addon + return addon.routing.url_for(getattr(addon, name), *args, **kwargs) + + +def show_listing(title_items, category=None, sort=None, content=None, cache=True, update_listing=False): + """Show a virtual directory in Kodi""" + from resources.lib.addon import routing + + if content: + # content is one of: files, songs, artists, albums, movies, tvshows, episodes, musicvideos, videos, images, games + xbmcplugin.setContent(routing.handle, content=content) + + # Jump through hoops to get a stable breadcrumbs implementation + category_label = '' + if category: + if not content: + category_label = '' + if isinstance(category, int): + category_label += localize(category) + else: + category_label += category + elif not content: + category_label = '' + + xbmcplugin.setPluginCategory(handle=routing.handle, category=category_label) + + # Add all sort methods to GUI (start with preferred) + if sort is None: + sort = DEFAULT_SORT_METHODS + elif not isinstance(sort, list): + sort = [sort] + DEFAULT_SORT_METHODS + + for key in sort: + xbmcplugin.addSortMethod(handle=routing.handle, sortMethod=SORT_METHODS[key]) + + # Add the listings + listing = [] + for title_item in title_items: + # Three options: + # - item is a virtual directory/folder (not playable, path) + # - item is a playable file (playable, path) + # - item is non-actionable item (not playable, no path) + is_folder = bool(not title_item.is_playable and title_item.path) + is_playable = bool(title_item.is_playable and title_item.path) + + list_item = xbmcgui.ListItem(label=title_item.title, path=title_item.path) + + if title_item.prop_dict: + list_item.setProperties(title_item.prop_dict) + list_item.setProperty(key='IsPlayable', value='true' if is_playable else 'false') + + list_item.setIsFolder(is_folder) + + if title_item.art_dict: + list_item.setArt(title_item.art_dict) + + if title_item.info_dict: + # type is one of: video, music, pictures, game + list_item.setInfo(type='video', infoLabels=title_item.info_dict) + + if title_item.stream_dict: + # type is one of: video, audio, subtitle + list_item.addStreamInfo('video', title_item.stream_dict) + + if title_item.context_menu: + list_item.addContextMenuItems(title_item.context_menu, ) + + is_folder = bool(not title_item.is_playable and title_item.path) + url = title_item.path if title_item.path else None + listing.append((url, list_item, is_folder)) + + succeeded = xbmcplugin.addDirectoryItems(routing.handle, listing, len(listing)) + xbmcplugin.endOfDirectory(routing.handle, succeeded, cacheToDisc=cache, updateListing=update_listing) + + +def input_dialog(heading='', message=''): + """Ask the user for a search string""" + return xbmcgui.Dialog().input(heading, defaultt=message) + + def ok_dialog(heading='', message=''): """Show Kodi's OK dialog""" if not heading: @@ -78,6 +205,16 @@ def ok_dialog(heading='', message=''): return xbmcgui.Dialog().ok(heading=heading, message=message) +def file_dialog(heading='', browse_type=1, default='', mask=''): + """Show Kodi's OK dialog""" + return xbmcgui.Dialog().browse(browse_type, heading, shares='', mask=mask, defaultt=default) + + +def show_context_menu(items): + """Show Kodi's OK dialog""" + return xbmcgui.Dialog().contextmenu(items) + + def yesno_dialog(heading='', message='', nolabel=None, yeslabel=None, autoclose=0): """Show Kodi's Yes/No dialog""" if not heading: @@ -260,6 +397,32 @@ def get_addon_info(key, addon=None): return to_unicode(addon.getAddonInfo(key)) +def container_refresh(url=None): + """Refresh the current container or (re)load a container by URL""" + if url: + _LOGGER.debug('Execute: Container.Refresh(%s)', url) + xbmc.executebuiltin('Container.Refresh({url})'.format(url=url)) + else: + _LOGGER.debug('Execute: Container.Refresh') + xbmc.executebuiltin('Container.Refresh') + + +def container_update(url): + """Update the current container while respecting the path history.""" + if url: + _LOGGER.debug('Execute: Container.Update(%s)', url) + xbmc.executebuiltin('Container.Update({url})'.format(url=url)) + else: + # URL is a mandatory argument for Container.Update, use Container.Refresh instead + container_refresh() + + +def end_of_directory(success=False): + """Close a virtual directory, required to avoid a waiting Kodi""" + from resources.lib.addon import routing + xbmcplugin.endOfDirectory(handle=routing.handle, succeeded=success, updateListing=False, cacheToDisc=False) + + def jsonrpc(*args, **kwargs): """Perform JSONRPC calls""" from json import dumps, loads diff --git a/resources/lib/modules/contextmenu.py b/resources/lib/modules/contextmenu.py index 8cf57a8..fdea31b 100644 --- a/resources/lib/modules/contextmenu.py +++ b/resources/lib/modules/contextmenu.py @@ -7,18 +7,30 @@ import re import sys +from resources.lib import kodiutils + _LOGGER = logging.getLogger(__name__) class ContextMenu: - """Helper class for PVR Context Menu handling""" + """ Helper class for PVR Context Menu handling (used in Kodi 18) """ def __init__(self): - """Initialise the Context Menu Module""" + """ Initialise object """ + + def play(self): + """ Play from Context Menu """ + stream = self.get_direct_uri() + if stream is None: + kodiutils.ok_dialog(message=kodiutils.localize(30706)) + return + + _LOGGER.debug('Playing using direct URI: %s', stream) + kodiutils.execute_builtin('PlayMedia', stream) @staticmethod def get_direct_uri(): - """Retrieve a direct URI from the selected ListItem.""" + """ Retrieve a direct URI from the selected ListItem. """ # We use a clever way / ugly hack (pick your choice) to hide the direct stream in Kodi 18. # Title [COLOR green]•[/COLOR][COLOR vod="plugin://plugin.video.example/play/whatever"][/COLOR] label = sys.listitem.getLabel() # pylint: disable=no-member diff --git a/resources/lib/modules/iptvsimple.py b/resources/lib/modules/iptvsimple.py index 83af90a..ee2bc6c 100644 --- a/resources/lib/modules/iptvsimple.py +++ b/resources/lib/modules/iptvsimple.py @@ -25,7 +25,42 @@ class IptvSimple: restart_required = False def __init__(self): - """Init""" + """Initialise object""" + + @classmethod + def _get_settings(cls): + """Return a dictionary with the required settings.""" + output_dir = kodiutils.addon_profile() + return { + 'm3uPathType': '0', # Local path + 'm3uPath': os.path.join(output_dir, IPTV_SIMPLE_PLAYLIST), + 'epgPathType': '0', # Local path + 'epgPath': os.path.join(output_dir, IPTV_SIMPLE_EPG), + 'epgCache': 'true', + 'epgTimeShift': '0', + 'logoPathType': '0', # Local path + 'logoPath': '/', + 'catchupEnabled': 'true', # Allow playback from the guide in Matrix + 'allChannelsCatchupMode': '1', # Allow to specify the vod mode per channel + 'catchupOnlyOnFinishedProgrammes': 'false', # Allow vod also on future programs + } + + @classmethod + def check(cls): + """Check if IPTV Simple is configured correctly.""" + try: + addon = kodiutils.get_addon(IPTV_SIMPLE_ID) + except Exception as exc: # pylint: disable=broad-except + _LOGGER.warning('Could not find IPTV Simple: %s', str(exc)) + return False + + # Validate IPTV Simple configuration + settings = cls._get_settings() + for key, value in settings.items(): + if value != addon.getSetting(key): + return False + + return True @classmethod def setup(cls): @@ -45,22 +80,9 @@ def setup(cls): cls._deactivate() # Configure IPTV Simple - output_dir = kodiutils.addon_profile() - - addon.setSetting('m3uPathType', '0') # Local path - addon.setSetting('m3uPath', os.path.join(output_dir, IPTV_SIMPLE_PLAYLIST)) - - addon.setSetting('epgPathType', '0') # Local path - addon.setSetting('epgPath', os.path.join(output_dir, IPTV_SIMPLE_EPG)) - addon.setSetting('epgCache', 'true') - addon.setSetting('epgTimeShift', '0') - - addon.setSetting('logoPathType', '0') # Local path - addon.setSetting('logoPath', '/') - - addon.setSetting('catchupEnabled', 'true') - addon.setSetting('allChannelsCatchupMode', '1') - addon.setSetting('catchupOnlyOnFinishedProgrammes', 'false') + settings = cls._get_settings() + for key, value in settings.items(): + addon.setSetting(key, value) # Activate IPTV Simple cls._activate() diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py new file mode 100644 index 0000000..ee49b78 --- /dev/null +++ b/resources/lib/modules/menu.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +""" Menu module """ + +from __future__ import absolute_import, division, unicode_literals + +import logging +from uuid import uuid4 + +from resources.lib import kodiutils +from resources.lib.kodiutils import TitleItem +from resources.lib.modules.iptvsimple import IptvSimple +from resources.lib.modules.sources import AddonSource, Sources, CustomSource + +_LOGGER = logging.getLogger(__name__) + + +class Menu: + """ Menu code """ + + def __init__(self): + """ Initialise object """ + + @staticmethod + def show_mainmenu(): + """ Show the main menu. """ + listing = [] + + if not IptvSimple.check(): + listing.append(TitleItem( + title=kodiutils.localize(30822), + path=kodiutils.url_for('install'), + art_dict=dict( + icon='DefaultAddonService.png', + ), + )) + + listing.append(TitleItem( + title=kodiutils.localize(30803), + path=kodiutils.url_for('refresh'), + art_dict=dict( + icon='DefaultAddonsUpdates.png', + ), + )) + + # listing.append(TitleItem( + # title='IPTV Manager Settings…', # TODO: translate + # path=kodiutils.url_for('show_settings'), + # art_dict=dict( + # icon='DefaultAddonService.png', + # ), + # info_dict=dict( + # plot='IPTV Manager Settings…', # TODO: translate + # ), + # )) + + listing.append(TitleItem( + title='Manage Sources…', # TODO: translate + path=kodiutils.url_for('show_sources'), + art_dict=dict( + icon='DefaultPlaylist.png', + ), + info_dict=dict( + plot='Manage Sources…', # TODO: translate + ), + )) + + kodiutils.show_listing(listing, sort=['unsorted']) + + @staticmethod + def show_install(): + """ Setup IPTV Simple """ + reply = kodiutils.yesno_dialog(message=kodiutils.localize(30700)) # Are you sure... + if reply: + if IptvSimple.setup(): + kodiutils.ok_dialog(message=kodiutils.localize(30701)) # The configuration of IPTV Simple is completed! + else: + kodiutils.ok_dialog(message=kodiutils.localize(30702)) # The configuration of IPTV Simple has failed! + + @staticmethod + def show_settings(): + """ Show the sources menu. """ + kodiutils.open_settings() + + @staticmethod + def refresh(): + """ Manually refresh to channels and epg. """ + kodiutils.end_of_directory() + Sources.refresh(True) + + @staticmethod + def show_sources(): + """ Show the sources menu. """ + listing = [] + listing.append(TitleItem( + title='[B]%s[/B]' % kodiutils.localize(30010), # Supported Add-ons + path=None, + art_dict=dict( + icon='empty.png', + ), + )) + for addon in AddonSource.detect_sources(): + if addon.enabled: + path = kodiutils.url_for('disable_source', addon_id=addon.addon_id) + else: + path = kodiutils.url_for('enable_source', addon_id=addon.addon_id) + + listing.append(TitleItem( + title=kodiutils.addon_name(addon.addon_obj), + path=path, + art_dict=dict( + icon='icons/infodialogs/enabled.png' if addon.enabled else 'icons/infodialogs/disable.png', + poster=kodiutils.addon_icon(addon.addon_obj), + ), + )) + + listing.append(TitleItem( + title='[B]%s[/B]' % kodiutils.localize(30011), # Custom Sources + path=None, + art_dict=dict( + icon='empty.png', + ), + )) + + for source in CustomSource.detect_sources().values(): + context_menu = [( + kodiutils.localize(30013), # Delete this Source + 'Container.Update(%s)' % + kodiutils.url_for('delete_source', uuid=source.uuid) + )] + + listing.append(TitleItem( + title=source.name, + path=kodiutils.url_for('edit_source', uuid=source.uuid), + art_dict=dict( + icon='icons/infodialogs/enabled.png' if source.enabled else 'icons/infodialogs/disable.png', + poster='DefaultAddonService.png', + ), + context_menu=context_menu, + )) + + listing.append(TitleItem( + title=kodiutils.localize(30012), # Add Source… + path=kodiutils.url_for('add_source'), + art_dict=dict( + icon='DefaultAddSource.png', + ), + )) + + kodiutils.show_listing(listing, category='Sources', sort=['unsorted']) + + @staticmethod + def enable_addon(addon_id): + """ Enable the specified source. """ + AddonSource.enable(addon_id) + + kodiutils.end_of_directory() + + @staticmethod + def disable_addon(addon_id): + """ Disable the specified source. """ + AddonSource.disable(addon_id) + + kodiutils.end_of_directory() + + @staticmethod + def add_source(): + """ Add a new source. """ + source = CustomSource(uuid=str(uuid4()), + name='Custom Source', # Default name + enabled=False) + source.save() + + # Go to edit page + Menu.edit_source(source.uuid) + + @staticmethod + def delete_source(uuid): + """ Add a new source. """ + sources = CustomSource.detect_sources() + source = sources.get(uuid) + source.delete() + + kodiutils.end_of_directory() + + @staticmethod + def edit_source(uuid, edit=None): + """ Edit a custom source. """ + sources = CustomSource.detect_sources() + source = sources.get(uuid) + + if source is None: + kodiutils.container_refresh(kodiutils.url_for('show_sources')) + return + + if edit == 'name': + name = kodiutils.input_dialog(heading='Enter name', message=source.name) + if name: + source.name = name + source.save() + + elif edit == 'enabled': + source.enabled = not source.enabled + source.save() + + elif edit == 'playlist': + res = kodiutils.show_context_menu([kodiutils.localize(30030), # Enter URL + kodiutils.localize(30031)]) # Browse for file + if res == -1: # user has cancelled + pass + elif res == 0: + # Enter URL + url = kodiutils.input_dialog(heading=kodiutils.localize(30030), # Enter URL + message=source.playlist_uri if source.playlist_type == CustomSource.TYPE_URL else '') + if url: + source.playlist_uri = url + source.playlist_type = CustomSource.TYPE_URL + source.save() + elif res == 1: + # Browse for file... + filename = kodiutils.file_dialog(kodiutils.localize(30031), mask='.m3u|.m3u8', # Browse for file + default=source.playlist_uri if source.playlist_type == CustomSource.TYPE_FILE else '') + if filename: + source.playlist_uri = filename + source.playlist_type = CustomSource.TYPE_FILE + source.save() + + elif edit == 'guide': + res = kodiutils.show_context_menu([kodiutils.localize(30030), # Enter URL + kodiutils.localize(30031)]) # Browse for file + if res == -1: # user has cancelled + pass + elif res == 0: + # Enter URL + url = kodiutils.input_dialog(heading=kodiutils.localize(30030), # Enter URL + message=source.epg_uri if source.epg_type == CustomSource.TYPE_URL else '') + if url: + source.epg_uri = url + source.epg_type = CustomSource.TYPE_URL + source.save() + elif res == 1: + # Browse for file... + filename = kodiutils.file_dialog(kodiutils.localize(30031), mask='.xml', # Browse for file + default=source.epg_uri if source.epg_type == CustomSource.TYPE_FILE else '') + if filename: + source.epg_uri = filename + source.epg_type = CustomSource.TYPE_FILE + source.save() + + listing = [ + TitleItem( + title='[B]%s:[/B] %s' % (kodiutils.localize(30020), source.name), + path=kodiutils.url_for('edit_source', uuid=source.uuid, edit='name'), + info_dict=dict( + plot=kodiutils.localize(30021), + ), + art_dict=dict( + icon='empty.png', + ), + ), + TitleItem( + title='[B]%s:[/B] %s' % (kodiutils.localize(30022), # Enabled + kodiutils.localize(30024) if source.enabled else kodiutils.localize(30025)), # Yes, No + path=kodiutils.url_for('edit_source', uuid=source.uuid, edit='enabled'), + info_dict=dict( + plot=kodiutils.localize(30023), + ), + art_dict=dict( + icon='empty.png', + ), + ), + TitleItem( + title='[B]%s:[/B] %s' % (kodiutils.localize(30026), source.playlist_uri), + path=kodiutils.url_for('edit_source', uuid=source.uuid, edit='playlist'), + info_dict=dict( + plot=kodiutils.localize(30027), + ), + art_dict=dict( + icon='empty.png', + ), + ), + TitleItem( + title='[B]%s:[/B] %s' % (kodiutils.localize(30028), source.epg_uri), + path=kodiutils.url_for('edit_source', uuid=source.uuid, edit='guide'), + info_dict=dict( + plot=kodiutils.localize(30029), + ), + art_dict=dict( + icon='empty.png', + ), + ) + ] + + kodiutils.show_listing(listing, category=30004, sort=['unsorted'], update_listing=edit is not None) diff --git a/resources/lib/modules/addon.py b/resources/lib/modules/sources.py similarity index 72% rename from resources/lib/modules/addon.py rename to resources/lib/modules/sources.py index 20f0ac9..7d5ded2 100644 --- a/resources/lib/modules/addon.py +++ b/resources/lib/modules/sources.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Addon Module""" +"""Sources Module""" from __future__ import absolute_import, division, unicode_literals @@ -9,6 +9,7 @@ import re import socket import time +from collections import OrderedDict from resources.lib import kodiutils from resources.lib.modules.iptvsimple import IptvSimple @@ -34,17 +35,10 @@ def update_qs(url, **params): return urlunparse(url_parts) -class Addon: - """Helper class for Addon communication""" - - def __init__(self, addon_id, addon_obj, channels_uri, epg_uri): - self.addon_id = addon_id - self.addon_obj = addon_obj - self.channels_uri = channels_uri - self.epg_uri = epg_uri +class Sources: + """Helper class for Source updating""" - addon = kodiutils.get_addon(addon_id) - self.addon_path = kodiutils.addon_path(addon) + SOURCES_FILE = 'sources.json' @classmethod def refresh(cls, show_progress=False): @@ -57,8 +51,12 @@ def refresh(cls, show_progress=False): else: progress = None - addons = cls.detect_iptv_addons() + addons = AddonSource.detect_sources() for index, addon in enumerate(addons): + # Skip Add-ons that have IPTV Manager support disabled + if not addon.enabled: + continue + _LOGGER.info('Updating IPTV data for %s...', addon.addon_id) if progress: @@ -105,8 +103,22 @@ def refresh(cls, show_progress=False): if show_progress: progress.close() + +class AddonSource: + """Helper class for Addon communication""" + + def __init__(self, addon_id, addon_obj, enabled, channels_uri, epg_uri): + self.addon_id = addon_id + self.addon_obj = addon_obj + self.enabled = enabled + self.channels_uri = channels_uri + self.epg_uri = epg_uri + + addon = kodiutils.get_addon(addon_id) + self.addon_path = kodiutils.addon_path(addon) + @staticmethod - def detect_iptv_addons(): + def detect_sources(): """Find add-ons that provide IPTV channel data""" result = kodiutils.jsonrpc(method="Addons.GetAddons", params={'installed': True, 'enabled': True, 'type': 'xbmc.python.pluginsource'}) @@ -116,18 +128,31 @@ def detect_iptv_addons(): addon = kodiutils.get_addon(row['addonid']) # Check if add-on supports IPTV Manager - if addon.getSetting('iptv.enabled') != 'true': + if not addon.getSetting('iptv.enabled'): continue - addons.append(Addon( + addons.append(AddonSource( addon_id=row['addonid'], addon_obj=addon, + enabled=addon.getSetting('iptv.enabled') == 'true', channels_uri=addon.getSetting('iptv.channels_uri'), epg_uri=addon.getSetting('iptv.epg_uri'), )) return addons + @staticmethod + def enable(addon_id): + """Enable this source""" + addon = kodiutils.get_addon(addon_id) + addon.setSetting('iptv.enabled', 'true') + + @staticmethod + def disable(addon_id): + """Disable this source""" + addon = kodiutils.get_addon(addon_id) + addon.setSetting('iptv.enabled', 'false') + def get_channels(self): """Get channel data from this add-on""" _LOGGER.info('Requesting channels from %s...', self.channels_uri) @@ -275,3 +300,81 @@ def _wait_for_data(self, sock, timeout=10): # Close our socket _LOGGER.debug('Closing socket on port %s', sock.getsockname()[1]) sock.close() + + +class CustomSource: + """ Helper class for a Source. """ + + TYPE_URL = 1 + TYPE_FILE = 2 + + def __init__(self, uuid, name, enabled, playlist_uri=None, playlist_type=None, epg_uri=None, epg_type=None): + self.uuid = uuid + self.name = name + self.enabled = enabled + self.playlist_uri = playlist_uri + self.playlist_type = playlist_type + self.epg_uri = epg_uri + self.epg_type = epg_type + + @staticmethod + def detect_sources(): + """Load our sources that provide custom channel data""" + try: + with open(os.path.join(kodiutils.addon_profile(), Sources.SOURCES_FILE), 'r') as fdesc: + result = json.loads(fdesc.read()) # TODO: keep order + except (IOError, TypeError, ValueError): + result = {} + + sources = OrderedDict() + for source in result.values(): + sources[source.get('uuid')] = CustomSource( + uuid=source.get('uuid'), + name=source.get('name'), + enabled=source.get('enabled'), + playlist_uri=source.get('playlist_uri'), + playlist_type=source.get('playlist_type'), + epg_uri=source.get('epg_uri'), + epg_type=source.get('epg_type'), + ) + + return sources + + def save(self): + """Save this source""" + try: + with open(os.path.join(kodiutils.addon_profile(), Sources.SOURCES_FILE), 'r') as fdesc: + sources = json.loads(fdesc.read()) + except (IOError, TypeError, ValueError): + sources = {} + + sources[self.uuid] = self.__dict__ + + with open(os.path.join(kodiutils.addon_profile(), Sources.SOURCES_FILE), 'w') as fdesc: + json.dump(sources, fdesc) + + def delete(self): + """Delete this source""" + try: + with open(os.path.join(kodiutils.addon_profile(), Sources.SOURCES_FILE), 'r') as fdesc: + sources = json.loads(fdesc.read()) + except (IOError, TypeError, ValueError): + sources = {} + + sources.pop(self.uuid) + + with open(os.path.join(kodiutils.addon_profile(), Sources.SOURCES_FILE), 'w') as fdesc: + json.dump(sources, fdesc) + + @staticmethod + def get_channels(): + """Get channel data from this source""" + + # TODO + return [] + + @staticmethod + def get_epg(): + """Get epg data from this source""" + # TODO + return [] diff --git a/resources/lib/service.py b/resources/lib/service.py index 004cfb0..47ee834 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -9,8 +9,8 @@ from xbmc import Monitor from resources.lib import kodilogging, kodiutils -from resources.lib.modules.addon import Addon from resources.lib.modules.iptvsimple import IptvSimple +from resources.lib.modules.sources import Sources _LOGGER = logging.getLogger(__name__) @@ -30,7 +30,7 @@ def run(self): while not self.abortRequested(): # Check if we need to do an update if self._is_refresh_required(): - Addon.refresh() + Sources.refresh() # Check if IPTV Simple needs to be restarted if IptvSimple.restart_required: diff --git a/resources/settings.xml b/resources/settings.xml index 4c99ece..52cd550 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -4,12 +4,18 @@ - + - + + + + + + + diff --git a/service.py b/service_entry.py similarity index 100% rename from service.py rename to service_entry.py diff --git a/tests/test_integration.py b/tests/test_integration.py index 85660ec..29f11ad 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals import os +import sys import time import unittest import lxml.etree @@ -15,14 +16,15 @@ from xbmcgui import ListItem from resources.lib import kodiutils -from resources.lib.modules.addon import Addon +from resources.lib.modules.contextmenu import ContextMenu +from resources.lib.modules.sources import Sources class IntegrationTest(unittest.TestCase): """Integration Tests""" def test_refresh(self): - """Test the refreshing of data""" + """Test the refreshing of data.""" m3u_path = 'tests/home/userdata/addon_data/service.iptv.manager/playlist.m3u8' epg_path = 'tests/home/userdata/addon_data/service.iptv.manager/epg.xml' @@ -33,7 +35,7 @@ def test_refresh(self): # Do the refresh with patch('xbmcgui.DialogProgress.iscanceled', return_value=False): - Addon.refresh(True) + Sources.refresh(True) # Check that the files now exist for path in [m3u_path, epg_path]: @@ -59,14 +61,13 @@ def test_refresh(self): self.assertIsNotNone(xml.find('./channel[@id="één.be"]')) self.assertIsNotNone(xml.find('./channel[@id="raw1.com"]')) - # Now, try playing something from the Guide - import sys + def test_play_from_guide(self): + """Play something from the guide.""" sys.listitem = ListItem(label='Example Show [COLOR green]•[/COLOR][COLOR vod="plugin://plugin.video.example/play/something"][/COLOR]', path='pvr://guide/0006/2020-05-23 11:35:00.epg') # Try to play it - from resources.lib.functions import play_from_contextmenu - play_from_contextmenu() + ContextMenu().play() # Check that something is playing player = xbmc.Player() diff --git a/tests/test_iptvsimple.py b/tests/test_iptvsimple.py index 72fcc2f..6fde622 100644 --- a/tests/test_iptvsimple.py +++ b/tests/test_iptvsimple.py @@ -15,7 +15,9 @@ class IptvSimpleTest(unittest.TestCase): def test_setup(self): """Test the setup of IPTV Simple (this will be mocked)""" - self.assertTrue(IptvSimple.setup()) + self.assertFalse(IptvSimple.check()) # Configuration will be incorrect + self.assertTrue(IptvSimple.setup()) # Setup configuration + self.assertTrue(IptvSimple.check()) # Configuration will be correct def test_restart(self): """Test the restart of IPTV Simple (this will be mocked)""" diff --git a/tests/test_sources.py b/tests/test_sources.py new file mode 100644 index 0000000..8ce6ac6 --- /dev/null +++ b/tests/test_sources.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# pylint: disable=invalid-name,missing-docstring,no-self-use + +from __future__ import absolute_import, division, print_function, unicode_literals + +import os +import unittest +from uuid import uuid4 + +from mock import patch + +from resources.lib import kodiutils +from resources.lib.modules.sources import CustomSource, Sources + +TEST_PLAYLIST = """ +#EXTM3U +#EXTINF:-1 tvg-name="Test 1" tvg-id="test1.com" tvg-logo="https://example.com/test1.png" tvg-chno="1" group-title="Test IPTV Addon" catchup="vod",Test 1 +plugin://plugin.video.test/play/live +""" + +TEST_EPG = """ + + + + + + Test 1 + Test 1 description + + +""" + + +class SourcesTest(unittest.TestCase): + + def test_create(self): + # Clean sources + filename = os.path.join(kodiutils.addon_profile(), Sources.SOURCES_FILE) + if os.path.exists(filename): + os.unlink(filename) + + key = str(uuid4()) + + # Create new source + source = CustomSource(uuid=key, + name='Custom Source', + enabled=False) + source.save() + + # Check that we can find this source + sources = CustomSource.detect_sources() + self.assertIn(key, sources.keys()) + self.assertEqual(sources.get(key).enabled, False) + + # Update source + source.enabled = True + source.save() + + # Check that we can find this source + sources = CustomSource.detect_sources() + self.assertIn(key, sources.keys()) + self.assertEqual(sources.get(key).enabled, True) + + def test_fetch(self): + + def mocked_requests_get(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + if args[0].endsWith('m3u'): + return MockResponse(TEST_PLAYLIST, 200) + elif args[0].endsWith('xml'): + return MockResponse(TEST_EPG, 200) + + return MockResponse(None, 404) + + with patch('requests.get', side_effect=mocked_requests_get): + source = CustomSource(uuid=str(uuid4()), + name='Test Source', + enabled=False, + playlist_uri='https://example.com/playlist.m3u', + playlist_type=CustomSource.TYPE_URL, + epg_uri='https://example.com/playlist.m3u', + epg_type=CustomSource.TYPE_URL, + ) + + channels = source.get_channels() + + print(channels) + + + epg = source.get_epg() + + print(epg) + + exit() + + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/update_translations.py b/tests/update_translations.py new file mode 100755 index 0000000..efc8a07 --- /dev/null +++ b/tests/update_translations.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pylint: disable=missing-docstring,no-self-use,wrong-import-order,wrong-import-position,invalid-name + +import sys + +import polib + +# Load po-files +translated = polib.pofile(sys.argv[1], wrapwidth=0) +english = polib.pofile(sys.argv[2], wrapwidth=0) + +for entry in english: + # Find a translation + translation = translated.find(entry.msgctxt, 'msgctxt') + + if translation and entry.msgid == translation.msgid: + entry.msgstr = translation.msgstr + +english.metadata = translated.metadata + +if sys.platform.startswith('win'): + # On Windows save the file keeping the Linux return character + with open(sys.argv[1], 'wb') as _file: + content = str(english).encode('utf-8') + content = content.replace(b'\r\n', b'\n') + _file.write(content) +else: + # Save it now over the translation + english.save(sys.argv[1])