From 4de27f39adf73c041791c10f75a017258892f956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sat, 30 Jan 2021 12:13:11 +0100 Subject: [PATCH] Rename Custom Source -> External Source --- .gitattributes | 2 + README.md | 1 + .../resource.language.el_gr/strings.po | 4 +- .../resource.language.en_gb/strings.po | 4 +- .../resource.language.hu_hu/strings.po | 4 +- .../resource.language.nl_nl/strings.po | 8 +-- .../resource.language.ro_ro/strings.po | 4 +- .../resource.language.ru_ru/strings.po | 4 +- resources/lib/modules/menu.py | 65 ++++++++++-------- resources/lib/modules/sources/__init__.py | 7 +- resources/lib/modules/sources/addon.py | 1 + .../sources/{custom.py => external.py} | 26 +++---- .../data/{custom_epg.xml => external_epg.xml} | 0 ...tom_playlist.m3u => external_playlist.m3u} | 0 ...aylist.m3u.gz => external_playlist.m3u.gz} | Bin tests/test_integration.py | 18 ++--- tests/test_sources.py | 56 +++++++-------- 17 files changed, 107 insertions(+), 97 deletions(-) rename resources/lib/modules/sources/{custom.py => external.py} (79%) rename tests/data/{custom_epg.xml => external_epg.xml} (100%) rename tests/data/{custom_playlist.m3u => external_playlist.m3u} (100%) rename tests/data/{custom_playlist.m3u.gz => external_playlist.m3u.gz} (100%) diff --git a/.gitattributes b/.gitattributes index cc63879..f52271b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,9 @@ +.env.example export-ignore .github/ export-ignore tests/ export-ignore .gitattributes export-ignore .gitignore export-ignore .pylintrc export-ignore +codecov.yml export-ignore Makefile export-ignore requirements.txt export-ignore diff --git a/README.md b/README.md index 0c7858d..b258aa8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ More information and documentation for developers can be found on our [Wiki page ## Features * Integrates Live TV Channels with EPG data in Kodi from supported IPTV Add-ons +* Supports external sources so you can specify your own `M3U` and `XMLTV` files to merge from a file or a http(s)://-url * Allows playback of past and future programs directly from the EPG ## Screenshots diff --git a/resources/language/resource.language.el_gr/strings.po b/resources/language/resource.language.el_gr/strings.po index 788e79e..b107853 100644 --- a/resources/language/resource.language.el_gr/strings.po +++ b/resources/language/resource.language.el_gr/strings.po @@ -34,11 +34,11 @@ msgid "Supported Add-ons" msgstr "" msgctxt "#30012" -msgid "Custom Sources" +msgid "External Sources" msgstr "" msgctxt "#30013" -msgid "Add Source…" +msgid "Add external source…" msgstr "" msgctxt "#30014" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index ac1f8a5..17f2cb7 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -34,11 +34,11 @@ msgid "Supported Add-ons" msgstr "" msgctxt "#30012" -msgid "Custom Sources" +msgid "External Sources" msgstr "" msgctxt "#30013" -msgid "Add Source…" +msgid "Add external source…" msgstr "" msgctxt "#30014" diff --git a/resources/language/resource.language.hu_hu/strings.po b/resources/language/resource.language.hu_hu/strings.po index 34cbad5..d44a3e8 100644 --- a/resources/language/resource.language.hu_hu/strings.po +++ b/resources/language/resource.language.hu_hu/strings.po @@ -34,11 +34,11 @@ msgid "Supported Add-ons" msgstr "" msgctxt "#30012" -msgid "Custom Sources" +msgid "External Sources" msgstr "" msgctxt "#30013" -msgid "Add Source…" +msgid "Add external source…" msgstr "" msgctxt "#30014" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 596edbd..6ca5a12 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -35,12 +35,12 @@ msgid "Supported Add-ons" msgstr "Ondersteunde Add-ons" msgctxt "#30012" -msgid "Custom Sources" -msgstr "Extra bronnen" +msgid "External Sources" +msgstr "Externe bronnen" msgctxt "#30013" -msgid "Add Source…" -msgstr "Bron toevoegen…" +msgid "Add external source…" +msgstr "Externe bron toevoegen…" msgctxt "#30014" msgid "Delete Source" diff --git a/resources/language/resource.language.ro_ro/strings.po b/resources/language/resource.language.ro_ro/strings.po index a14e49c..de26ff2 100644 --- a/resources/language/resource.language.ro_ro/strings.po +++ b/resources/language/resource.language.ro_ro/strings.po @@ -34,11 +34,11 @@ msgid "Supported Add-ons" msgstr "" msgctxt "#30012" -msgid "Custom Sources" +msgid "External Sources" msgstr "" msgctxt "#30013" -msgid "Add Source…" +msgid "Add external source…" msgstr "" msgctxt "#30014" diff --git a/resources/language/resource.language.ru_ru/strings.po b/resources/language/resource.language.ru_ru/strings.po index 202353c..559075d 100644 --- a/resources/language/resource.language.ru_ru/strings.po +++ b/resources/language/resource.language.ru_ru/strings.po @@ -34,11 +34,11 @@ msgid "Supported Add-ons" msgstr "" msgctxt "#30012" -msgid "Custom Sources" +msgid "External Sources" msgstr "" msgctxt "#30013" -msgid "Add Source…" +msgid "Add external source…" msgstr "" msgctxt "#30014" diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index 683ebdc..f64a201 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -11,7 +11,7 @@ from resources.lib.modules.iptvsimple import IptvSimple from resources.lib.modules.sources import Sources from resources.lib.modules.sources.addon import AddonSource -from resources.lib.modules.sources.custom import CustomSource +from resources.lib.modules.sources.external import ExternalSource _LOGGER = logging.getLogger(__name__) @@ -94,37 +94,42 @@ def refresh(): def show_sources(): """ Show the sources menu. """ listing = [] - listing.append(TitleItem( - title='[B]%s[/B]' % kodiutils.localize(30011), # 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) + addon_sources = AddonSource.detect_sources() + external_sources = ExternalSource.detect_sources() + + if addon_sources: listing.append(TitleItem( - title=kodiutils.addon_name(addon.addon_obj), - path=path, + title='[B]%s[/B]' % kodiutils.localize(30011), # Supported Add-ons + path=None, art_dict=dict( - icon='icons/infodialogs/enabled.png' if addon.enabled else 'icons/infodialogs/disable.png', - poster=kodiutils.addon_icon(addon.addon_obj), + icon='empty.png', ), )) + for addon in addon_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(30012), # Custom Sources + title='[B]%s[/B]' % kodiutils.localize(30012), # External Sources path=None, art_dict=dict( icon='empty.png', ), )) - for source in CustomSource.detect_sources(): + for source in external_sources: context_menu = [( kodiutils.localize(30014), # Delete this Source 'Container.Update(%s)' % @@ -142,7 +147,7 @@ def show_sources(): )) listing.append(TitleItem( - title=kodiutils.localize(30013), # Add Source… + title=kodiutils.localize(30013), # Add Source path=kodiutils.url_for('add_source'), art_dict=dict( icon='DefaultAddSource.png', @@ -168,9 +173,9 @@ def disable_addon_source(addon_id): @staticmethod def add_source(): """ Add a new source. """ - source = CustomSource(uuid=str(uuid4()), - name='Custom Source', # Default name - enabled=False) + source = ExternalSource(uuid=str(uuid4()), + name='External Source', # Default name + enabled=False) source.save() # Go to edit page @@ -179,7 +184,7 @@ def add_source(): @staticmethod def delete_source(uuid): """ Add a new source. """ - sources = CustomSource.detect_sources() + sources = ExternalSource.detect_sources() source = next(source for source in sources if source.uuid == uuid) source.delete() @@ -188,7 +193,7 @@ def delete_source(uuid): @staticmethod def edit_source(uuid, edit=None): """ Edit a custom source. """ - sources = CustomSource.detect_sources() + sources = ExternalSource.detect_sources() source = next(source for source in sources if source.uuid == uuid) if source is None: @@ -282,22 +287,22 @@ def _select_source(current_type, current_source, mask): if res == 0: # None new_source = None - new_type = CustomSource.TYPE_NONE + new_type = ExternalSource.TYPE_NONE elif res == 1: # Enter URL url = kodiutils.input_dialog(heading=kodiutils.localize(30030), # Enter URL - message=current_source if current_type == CustomSource.TYPE_URL else '') + message=current_source if current_type == ExternalSource.TYPE_URL else '') if url: new_source = url - new_type = CustomSource.TYPE_URL + new_type = ExternalSource.TYPE_URL elif res == 2: # Browse for file... filename = kodiutils.file_dialog(kodiutils.localize(30031), mask=mask, # Browse for file - default=current_source if current_type == CustomSource.TYPE_FILE else '') + default=current_source if current_type == ExternalSource.TYPE_FILE else '') if filename: new_source = filename - new_type = CustomSource.TYPE_FILE + new_type = ExternalSource.TYPE_FILE return new_type, new_source diff --git a/resources/lib/modules/sources/__init__.py b/resources/lib/modules/sources/__init__.py index 8b237d6..5e3e323 100644 --- a/resources/lib/modules/sources/__init__.py +++ b/resources/lib/modules/sources/__init__.py @@ -35,10 +35,10 @@ def refresh(cls, show_progress=False): from resources.lib.modules.sources.addon import AddonSource addon_sources = AddonSource.detect_sources() - from resources.lib.modules.sources.custom import CustomSource - custom_sources = CustomSource.detect_sources() + from resources.lib.modules.sources.external import ExternalSource + external_sources = ExternalSource.detect_sources() - sources = [source for source in addon_sources + custom_sources if source.enabled] + sources = [source for source in addon_sources + external_sources if source.enabled] for index, source in enumerate(sources): # Skip Add-ons that have IPTV Manager support disabled @@ -161,6 +161,7 @@ def _decompress_gz(data): return decompress(data).decode() except ImportError: # Python 2 from gzip import GzipFile + from StringIO import StringIO with GzipFile(fileobj=StringIO(data)) as fdesc: return fdesc.read().decode() diff --git a/resources/lib/modules/sources/addon.py b/resources/lib/modules/sources/addon.py index 7441419..8c15a8e 100644 --- a/resources/lib/modules/sources/addon.py +++ b/resources/lib/modules/sources/addon.py @@ -20,6 +20,7 @@ def update_qs(url, **params): from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse except ImportError: # Python 2 from urllib import urlencode + from urlparse import parse_qsl, urlparse, urlunparse url_parts = list(urlparse(url)) query = dict(parse_qsl(url_parts[4])) diff --git a/resources/lib/modules/sources/custom.py b/resources/lib/modules/sources/external.py similarity index 79% rename from resources/lib/modules/sources/custom.py rename to resources/lib/modules/sources/external.py index cc669a9..27a3e19 100644 --- a/resources/lib/modules/sources/custom.py +++ b/resources/lib/modules/sources/external.py @@ -13,8 +13,8 @@ _LOGGER = logging.getLogger(__name__) -class CustomSource(Source): - """ Defines a Custom source """ +class ExternalSource(Source): + """ Defines an External source """ SOURCES_FILE = 'sources.json' @@ -24,7 +24,7 @@ class CustomSource(Source): def __init__(self, uuid, name, enabled, playlist_uri=None, playlist_type=TYPE_NONE, epg_uri=None, epg_type=TYPE_NONE): """ Initialise object """ - super(CustomSource, self).__init__() + super(ExternalSource, self).__init__() self.uuid = uuid self.name = name self.enabled = enabled @@ -38,26 +38,26 @@ def __str__(self): @staticmethod def detect_sources(): - """ Load our sources that provide custom channel data + """ Load our sources that provide external channel data. - :rtype: list[CustomSource] + :rtype: list[ExternalSource] """ try: - with open(os.path.join(kodiutils.addon_profile(), CustomSource.SOURCES_FILE), 'r') as fdesc: + with open(os.path.join(kodiutils.addon_profile(), ExternalSource.SOURCES_FILE), 'r') as fdesc: result = json.loads(fdesc.read()) except (IOError, TypeError, ValueError): result = {} sources = [] for source in result.values(): - sources.append(CustomSource( + sources.append(ExternalSource( uuid=source.get('uuid'), name=source.get('name'), enabled=source.get('enabled'), playlist_uri=source.get('playlist_uri'), - playlist_type=source.get('playlist_type', CustomSource.TYPE_NONE), + playlist_type=source.get('playlist_type', ExternalSource.TYPE_NONE), epg_uri=source.get('epg_uri'), - epg_type=source.get('epg_type', CustomSource.TYPE_NONE), + epg_type=source.get('epg_type', ExternalSource.TYPE_NONE), )) return sources @@ -113,7 +113,7 @@ def save(self): if not os.path.exists(output_path): os.mkdir(output_path) - with open(os.path.join(output_path, CustomSource.SOURCES_FILE), 'r') as fdesc: + with open(os.path.join(output_path, ExternalSource.SOURCES_FILE), 'r') as fdesc: sources = json.loads(fdesc.read()) except (IOError, TypeError, ValueError): sources = {} @@ -121,14 +121,14 @@ def save(self): # Update the element with my uuid sources[self.uuid] = self.__dict__ - with open(os.path.join(output_path, CustomSource.SOURCES_FILE), 'w') as fdesc: + with open(os.path.join(output_path, ExternalSource.SOURCES_FILE), 'w') as fdesc: json.dump(sources, fdesc) def delete(self): """ Delete this source. """ output_path = kodiutils.addon_profile() try: - with open(os.path.join(output_path, CustomSource.SOURCES_FILE), 'r') as fdesc: + with open(os.path.join(output_path, ExternalSource.SOURCES_FILE), 'r') as fdesc: sources = json.loads(fdesc.read()) except (IOError, TypeError, ValueError): sources = {} @@ -136,5 +136,5 @@ def delete(self): # Remove the element with my uuid sources.pop(self.uuid) - with open(os.path.join(output_path, CustomSource.SOURCES_FILE), 'w') as fdesc: + with open(os.path.join(output_path, ExternalSource.SOURCES_FILE), 'w') as fdesc: json.dump(sources, fdesc) diff --git a/tests/data/custom_epg.xml b/tests/data/external_epg.xml similarity index 100% rename from tests/data/custom_epg.xml rename to tests/data/external_epg.xml diff --git a/tests/data/custom_playlist.m3u b/tests/data/external_playlist.m3u similarity index 100% rename from tests/data/custom_playlist.m3u rename to tests/data/external_playlist.m3u diff --git a/tests/data/custom_playlist.m3u.gz b/tests/data/external_playlist.m3u.gz similarity index 100% rename from tests/data/custom_playlist.m3u.gz rename to tests/data/external_playlist.m3u.gz diff --git a/tests/test_integration.py b/tests/test_integration.py index 2477d53..fdd6957 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -20,7 +20,7 @@ from resources.lib import kodiutils from resources.lib.modules.contextmenu import ContextMenu from resources.lib.modules.sources import Sources -from resources.lib.modules.sources.custom import CustomSource +from resources.lib.modules.sources.external import ExternalSource class IntegrationTest(unittest.TestCase): @@ -37,14 +37,14 @@ def test_refresh(self): if os.path.exists(path): os.unlink(path) - # Add a custom source - source = CustomSource(uuid=str(uuid4()), - name='Custom Source', - enabled=True, - playlist_type=CustomSource.TYPE_FILE, - playlist_uri=os.path.realpath('tests/data/custom_playlist.m3u'), - epg_type=CustomSource.TYPE_FILE, - epg_uri=os.path.realpath('tests/data/custom_epg.xml')) + # Add an external source + source = ExternalSource(uuid=str(uuid4()), + name='External Source', + enabled=True, + playlist_type=ExternalSource.TYPE_FILE, + playlist_uri=os.path.realpath('tests/data/external_playlist.m3u'), + epg_type=ExternalSource.TYPE_FILE, + epg_uri=os.path.realpath('tests/data/external_epg.xml')) source.save() # Do the refresh diff --git a/tests/test_sources.py b/tests/test_sources.py index 95598d5..7d192dd 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -13,27 +13,27 @@ from resources.lib import kodiutils from resources.lib.modules.sources import Source -from resources.lib.modules.sources.custom import CustomSource +from resources.lib.modules.sources.external import ExternalSource class SourcesTest(unittest.TestCase): def test_create(self): # Clean sources - filename = os.path.join(kodiutils.addon_profile(), CustomSource.SOURCES_FILE) + filename = os.path.join(kodiutils.addon_profile(), ExternalSource.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 = ExternalSource(uuid=key, + name='External Source', + enabled=False) source.save() # Check that we can find this source - sources = CustomSource.detect_sources() + sources = ExternalSource.detect_sources() self.assertIn(key, [source.uuid for source in sources]) self.assertEqual(next(source for source in sources if source.uuid == key).enabled, False) @@ -42,7 +42,7 @@ def test_create(self): source.save() # Check that we can find this source - sources = CustomSource.detect_sources() + sources = ExternalSource.detect_sources() self.assertIn(key, [source.uuid for source in sources]) self.assertEqual(next(source for source in sources if source.uuid == key).enabled, True) @@ -50,18 +50,18 @@ def test_create(self): source.delete() # Check that we can't find this source anymore - sources = CustomSource.detect_sources() + sources = ExternalSource.detect_sources() self.assertNotIn(key, [source.uuid for source in sources]) def test_fetch_none(self): - source = CustomSource( + source = ExternalSource( uuid=str(uuid4()), name='Test Source', enabled=True, playlist_uri=None, - playlist_type=CustomSource.TYPE_NONE, + playlist_type=ExternalSource.TYPE_NONE, epg_uri=None, - epg_type=CustomSource.TYPE_NONE, + epg_type=ExternalSource.TYPE_NONE, ) channels = source.get_channels() @@ -71,24 +71,24 @@ def test_fetch_none(self): self.assertEqual(epg, '') def test_fetch_file(self): - source = CustomSource( + source = ExternalSource( uuid=str(uuid4()), name='Test Source', enabled=True, - playlist_uri=os.path.realpath('tests/data/custom_playlist.m3u'), - playlist_type=CustomSource.TYPE_FILE, - epg_uri=os.path.realpath('tests/data/custom_epg.xml'), - epg_type=CustomSource.TYPE_FILE, + playlist_uri=os.path.realpath('tests/data/external_playlist.m3u'), + playlist_type=ExternalSource.TYPE_FILE, + epg_uri=os.path.realpath('tests/data/external_epg.xml'), + epg_type=ExternalSource.TYPE_FILE, ) - expected_channels = Source._extract_m3u(open('tests/data/custom_playlist.m3u', 'r').read()) - expected_epg = Source._extract_xmltv(open('tests/data/custom_epg.xml', 'r').read()) + expected_channels = Source._extract_m3u(open('tests/data/external_playlist.m3u', 'r').read()) + expected_epg = Source._extract_xmltv(open('tests/data/external_epg.xml', 'r').read()) # Test channels channels = source.get_channels() self.assertEqual(channels.replace('\r\n', '\n'), expected_channels) # Test channels (gzip) - source.playlist_uri = os.path.realpath('tests/data/custom_playlist.m3u.gz') + source.playlist_uri = os.path.realpath('tests/data/external_playlist.m3u.gz') channels = source.get_channels() self.assertEqual(channels.replace('\r\n', '\n'), expected_channels) @@ -101,11 +101,11 @@ def test_fetch_url(self): def request_callback(request): if request.url.endswith('m3u'): - data = open('tests/data/custom_playlist.m3u', 'rb').read() + data = open('tests/data/external_playlist.m3u', 'rb').read() return 200, {}, data if request.url.endswith('m3u.gz'): - data = open('tests/data/custom_playlist.m3u', 'rb').read() + data = open('tests/data/external_playlist.m3u', 'rb').read() try: # Python 3 from gzip import compress return 200, {}, compress(data) @@ -119,28 +119,28 @@ def request_callback(request): if request.url.endswith('m3u.bz2'): from bz2 import compress - data = open('tests/data/custom_playlist.m3u', 'rb').read() + data = open('tests/data/external_playlist.m3u', 'rb').read() return 200, {}, compress(data) if request.url.endswith('xml'): - data = open('tests/data/custom_epg.xml', 'rb').read() + data = open('tests/data/external_epg.xml', 'rb').read() return 200, {}, data return 404, {}, None responses.add_callback(responses.GET, re.compile('https://example.com/.*'), callback=request_callback) - source = CustomSource( + source = ExternalSource( uuid=str(uuid4()), name='Test Source', enabled=True, playlist_uri='https://example.com/playlist.m3u', - playlist_type=CustomSource.TYPE_URL, + playlist_type=ExternalSource.TYPE_URL, epg_uri='https://example.com/xmltv.xml', - epg_type=CustomSource.TYPE_URL, + epg_type=ExternalSource.TYPE_URL, ) - expected_channels = Source._extract_m3u(open('tests/data/custom_playlist.m3u', 'r').read()) - expected_epg = Source._extract_xmltv(open('tests/data/custom_epg.xml', 'r').read()) + expected_channels = Source._extract_m3u(open('tests/data/external_playlist.m3u', 'r').read()) + expected_epg = Source._extract_xmltv(open('tests/data/external_epg.xml', 'r').read()) # Test channels channels = source.get_channels()