From 8a3ca73d5267e664a385c93e36a9c779c1f6c20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 21 Aug 2020 14:56:15 +0200 Subject: [PATCH 1/5] flake8 --- .config/generate_xml.py | 5 ++++- context.py | 4 ++-- context_play.py | 4 ++-- default.py | 4 ++-- jellyfin_kodi/downloader.py | 1 + jellyfin_kodi/full_sync.py | 4 ++-- jellyfin_kodi/jellyfin/api.py | 2 +- jellyfin_kodi/jellyfin/connection_manager.py | 2 +- jellyfin_kodi/jellyfin/credentials.py | 2 +- jellyfin_kodi/jellyfin/http.py | 2 +- jellyfin_kodi/library.py | 4 ++-- jellyfin_kodi/objects/movies.py | 4 ++-- jellyfin_kodi/player.py | 4 ++-- service.py | 6 +++--- 14 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.config/generate_xml.py b/.config/generate_xml.py index a35b995e..da4ae367 100644 --- a/.config/generate_xml.py +++ b/.config/generate_xml.py @@ -1,9 +1,11 @@ import xml.etree.ElementTree as ET -import yaml import sys import os from datetime import datetime +import yaml + + def indent(elem, level=0): ''' Nicely formats output xml with newlines and spaces @@ -23,6 +25,7 @@ def indent(elem, level=0): if level and (not elem.tail or not elem.tail.strip()): elem.tail = i + try: py_version = sys.argv[1] except IndexError: diff --git a/context.py b/context.py index 35655d7d..88eea1b3 100644 --- a/context.py +++ b/context.py @@ -17,8 +17,8 @@ sys.path.insert(0, __base__) ################################################################################################# -from entrypoint import Context # noqa: F402 -from helper import LazyLogger # noqa: F402 +from entrypoint import Context # noqa: E402 +from helper import LazyLogger # noqa: E402 ################################################################################################# diff --git a/context_play.py b/context_play.py index 2788a23d..87d1638d 100644 --- a/context_play.py +++ b/context_play.py @@ -17,8 +17,8 @@ sys.path.insert(0, __base__) ################################################################################################# -from entrypoint import Context # noqa: F402 -from helper import LazyLogger # noqa: F402 +from entrypoint import Context # noqa: E402 +from helper import LazyLogger # noqa: E402 ################################################################################################# diff --git a/default.py b/default.py index a4eb60c4..998ff512 100644 --- a/default.py +++ b/default.py @@ -17,8 +17,8 @@ sys.path.insert(0, __base__) ################################################################################################# -from entrypoint import Events # noqa: F402 -from helper import LazyLogger # noqa: F402 +from entrypoint import Events # noqa: E402 +from helper import LazyLogger # noqa: E402 ################################################################################################# diff --git a/jellyfin_kodi/downloader.py b/jellyfin_kodi/downloader.py index 762a4afe..b2db586b 100644 --- a/jellyfin_kodi/downloader.py +++ b/jellyfin_kodi/downloader.py @@ -224,6 +224,7 @@ def get_library_items(library_id, item_type): return _get(url, params) + def get_albums_by_artist(artist_id, basic=False): params = { diff --git a/jellyfin_kodi/full_sync.py b/jellyfin_kodi/full_sync.py index cebbd565..7a3ca779 100644 --- a/jellyfin_kodi/full_sync.py +++ b/jellyfin_kodi/full_sync.py @@ -435,13 +435,13 @@ class FullSync(object): obj.artist(artist) # Get all albums for each artist - artist_albums = [ album for album in albums if artist_name in album.get('Artists') ] + artist_albums = [album for album in albums if artist_name in album.get('Artists')] for album in artist_albums: # Add album to database obj.album(album) album_id = album.get('Id') # Get all songs in each album - album_songs = [ song for song in songs if album_id == song.get('AlbumId') ] + album_songs = [song for song in songs if album_id == song.get('AlbumId')] for song in album_songs: dialog.update(percent, message="%s/%s/%s" % (artist_name, album['Name'][:7], song['Name'][:7])) diff --git a/jellyfin_kodi/jellyfin/api.py b/jellyfin_kodi/jellyfin/api.py index c6d5ab16..b51de3fa 100644 --- a/jellyfin_kodi/jellyfin/api.py +++ b/jellyfin_kodi/jellyfin/api.py @@ -435,7 +435,7 @@ class API(object): if response.status_code == 200: return response.json() else: - return { 'Status_Code': response.status_code } + return {'Status_Code': response.status_code} def get_public_info(self, server_address): response = self.send_request(server_address, "system/info/public") diff --git a/jellyfin_kodi/jellyfin/connection_manager.py b/jellyfin_kodi/jellyfin/connection_manager.py index 05ed79b4..3dabcccc 100644 --- a/jellyfin_kodi/jellyfin/connection_manager.py +++ b/jellyfin_kodi/jellyfin/connection_manager.py @@ -260,7 +260,7 @@ class ConnectionManager(object): } servers.append(info) - + return servers # TODO: Make IPv6 compatable diff --git a/jellyfin_kodi/jellyfin/credentials.py b/jellyfin_kodi/jellyfin/credentials.py index 42e708a9..661792a7 100644 --- a/jellyfin_kodi/jellyfin/credentials.py +++ b/jellyfin_kodi/jellyfin/credentials.py @@ -111,7 +111,7 @@ class Credentials(object): existing['ConnectServerId'] = server['ConnectServerId'] return existing - + servers.append(server) return server diff --git a/jellyfin_kodi/jellyfin/http.py b/jellyfin_kodi/jellyfin/http.py index ee3b10fa..67b3edb5 100644 --- a/jellyfin_kodi/jellyfin/http.py +++ b/jellyfin_kodi/jellyfin/http.py @@ -56,7 +56,7 @@ class HTTP(object): else: LOG.debug("Server address not set") - if '{UserId}'in string: + if '{UserId}' in string: if self.config.data.get('auth.user_id', None): string = string.replace("{UserId}", self.config.data['auth.user_id']) else: diff --git a/jellyfin_kodi/library.py b/jellyfin_kodi/library.py index 3818ee13..2fa040cc 100644 --- a/jellyfin_kodi/library.py +++ b/jellyfin_kodi/library.py @@ -376,7 +376,7 @@ class Library(threading.Thread): include = [] filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"] sync = get_sync() - whitelist = [ x.replace('Mixed:', "") for x in sync['Whitelist'] ] + whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] LOG.info("--[ retrieve changes ] %s", last_sync) # Get the item type of each synced library and build list of types to request @@ -395,7 +395,7 @@ class Library(threading.Thread): try: # Get list of updates from server for synced library types and populate work queues - result = self.server.jellyfin.get_sync_queue(last_sync, ",".join([ x for x in query_filter ])) + result = self.server.jellyfin.get_sync_queue(last_sync, ",".join([x for x in query_filter])) if result is None: return True diff --git a/jellyfin_kodi/objects/movies.py b/jellyfin_kodi/objects/movies.py index 6aa03081..9edb1c9e 100644 --- a/jellyfin_kodi/objects/movies.py +++ b/jellyfin_kodi/objects/movies.py @@ -182,13 +182,13 @@ class Movies(KodiDb): if validate_dvd_dir(obj['Path'] + obj['Filename']): obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/' obj['Filename'] = 'VIDEO_TS.IFO' - LOG.debug("DVD directry %s",obj['Path']) + LOG.debug("DVD directry %s", obj['Path']) '''check bluray directries and point it to ./BDMV/index.bdmv''' if validate_bluray_dir(obj['Path'] + obj['Filename']): obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/' obj['Filename'] = 'index.bdmv' - LOG.debug("Bluray directry %s",obj['Path']) + LOG.debug("Bluray directry %s", obj['Path']) else: obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] diff --git a/jellyfin_kodi/player.py b/jellyfin_kodi/player.py index 0772b57c..93f94eb8 100644 --- a/jellyfin_kodi/player.py +++ b/jellyfin_kodi/player.py @@ -160,9 +160,9 @@ class Player(xbmc.Player): def set_audio_subs(self, audio=None, subtitle=None): if audio: - audio=int(audio) + audio = int(audio) if subtitle: - subtitle=int(subtitle) + subtitle = int(subtitle) ''' Only for after playback started ''' diff --git a/service.py b/service.py index 353cc58a..4d0fb7b7 100644 --- a/service.py +++ b/service.py @@ -18,9 +18,9 @@ sys.path.insert(0, __base__) ################################################################################################# -from entrypoint import Service # noqa: F402 -from helper.utils import settings # noqa: F402 -from helper import LazyLogger # noqa: F402 +from entrypoint import Service # noqa: E402 +from helper.utils import settings # noqa: E402 +from helper import LazyLogger # noqa: E402 ################################################################################################# From ed96dc8ad5dfa98b21cb1eee235e792ce7a04153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 21 Aug 2020 15:09:34 +0200 Subject: [PATCH 2/5] Clean json returned from server for keys with None values Add testing --- .gitignore | 66 ++++++++++++++++++++++++++-- jellyfin_kodi/jellyfin/http.py | 3 +- jellyfin_kodi/jellyfin/utils.py | 43 ++++++++++++++++++ requirements-dev.txt | 13 ++++++ tests/__init__.py | 0 tests/test_clean_none_dict_values.py | 51 +++++++++++++++++++++ tox.ini | 16 +++++++ 7 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 jellyfin_kodi/jellyfin/utils.py create mode 100644 requirements-dev.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_clean_none_dict_values.py diff --git a/.gitignore b/.gitignore index 8e87645e..d7548765 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,72 @@ -*.pyo +# Byte-compiled / optimized / DLL files __pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + + + __local__/ machine_guid -/resources/media/Thumbs.db +Thumbs.db .idea/ .DS_Store .vscode/ pyinstrument/ -pyinstrument_cext.so + +# Now managed by templates addon.xml + +*.log diff --git a/jellyfin_kodi/jellyfin/http.py b/jellyfin_kodi/jellyfin/http.py index 67b3edb5..866ac7f8 100644 --- a/jellyfin_kodi/jellyfin/http.py +++ b/jellyfin_kodi/jellyfin/http.py @@ -11,6 +11,7 @@ from six import string_types, ensure_str from helper.utils import JsonDebugPrinter from helper import LazyLogger from helper.exceptions import HTTPException +from jellyfin.utils import clean_none_dict_values ################################################################################################# @@ -161,7 +162,7 @@ class HTTP(object): LOG.debug("---<[ http ][%s ms]", elapsed) LOG.debug(JsonDebugPrinter(response)) - return response + return clean_none_dict_values(response) except ValueError: return diff --git a/jellyfin_kodi/jellyfin/utils.py b/jellyfin_kodi/jellyfin/utils.py new file mode 100644 index 00000000..a2b8f572 --- /dev/null +++ b/jellyfin_kodi/jellyfin/utils.py @@ -0,0 +1,43 @@ +from six import string_types +from six.moves import collections_abc + + +def clean_none_dict_values(obj): + """ + Recursively remove keys with a value of None + """ + if not isinstance(obj, collections_abc.Iterable) or isinstance(obj, string_types): + return obj + + queue = [obj] + + while queue: + item = queue.pop() + + if isinstance(item, collections_abc.Mapping): + mutable = isinstance(item, collections_abc.MutableMapping) + remove = [] + + for key, value in item.items(): + if value is None and mutable: + remove.append(key) + + elif isinstance(value, string_types): + continue + + elif isinstance(value, collections_abc.Iterable): + queue.append(value) + + if mutable: + # Remove keys with None value + for key in remove: + item.pop(key) + + elif isinstance(item, collections_abc.Iterable): + for value in item: + if value is None or isinstance(value, string_types): + continue + elif isinstance(value, collections_abc.Iterable): + queue.append(value) + + return obj diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..265c7918 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,13 @@ +setuptools >= 44.1.1 # Old setuptools causes script.module.addon.signals to fail installing +six >= 1.13 +python-dateutil >= 2.8.1 +requests >= 2.22 +futures >= 2.2; python_version < '3.0' +git+https://github.com/oddstr13/Kodistubs@python3 # Kodistubs >= 18 +git+https://github.com/romanvm/kodi.six +git+https://github.com/ruuk/script.module.addon.signals + +pytest >= 4.6.11 +coverage >= 5.2 +flake8 >= 3.8 +flake8-import-order >= 0.18 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_clean_none_dict_values.py b/tests/test_clean_none_dict_values.py new file mode 100644 index 00000000..9c094fb1 --- /dev/null +++ b/tests/test_clean_none_dict_values.py @@ -0,0 +1,51 @@ +import sys + +import pytest + +sys.path.insert(0, 'jellyfin_kodi') + +from jellyfin.utils import clean_none_dict_values # noqa: E402 + + +@pytest.mark.parametrize("obj,expected", [ + (None, None), + ([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]), + ({'foo': None, 'bar': 123}, {'bar': 123}), + ({ + 'dict': { + 'empty': None, + 'string': "Hello, Woorld!", + }, + 'number': 123, + 'list': [ + None, + 123, + "foo", + { + 'empty': None, + 'number': 123, + 'string': "foo", + 'list': [], + 'dict': {}, + } + ] + }, { + 'dict': { + 'string': "Hello, Woorld!", + }, + 'number': 123, + 'list': [ + None, + 123, + "foo", + { + 'number': 123, + 'string': "foo", + 'list': [], + 'dict': {}, + } + ] + }), +]) +def test_clean_none_dict_values(obj, expected): + assert clean_none_dict_values(obj) == expected diff --git a/tox.ini b/tox.ini index 41ad1896..ae876e22 100644 --- a/tox.ini +++ b/tox.ini @@ -6,3 +6,19 @@ extend-ignore = I202 per-file-ignores = */__init__.py: F401 + +[pytest] +minversion = 4.6 +testpaths = + tests + +[coverage:run] +source = + jellyfin_kodi + context.py + context_play.py + default.py + service.py + .config/generate_xml.py +omit = tests/* +command_line = -m pytest From 8792dd22cee1e73a914815a208b0ee92629c6295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 21 Aug 2020 16:11:42 +0200 Subject: [PATCH 3/5] Add sonarcloud settings file --- .sonarcloud.properties | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 00000000..31506853 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,15 @@ +# Path to sources +#sonar.sources=. +#sonar.exclusions= +#sonar.inclusions= + +# Path to tests +sonar.tests=tests +#sonar.test.exclusions= +#sonar.test.inclusions= + +# Source encoding +#sonar.sourceEncoding=UTF-8 + +# Exclusions for copy-paste detection +#sonar.cpd.exclusions= From 767eef635cf1a5d1d917da8f754479d00994ef87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 21 Aug 2020 16:13:04 +0200 Subject: [PATCH 4/5] Sonarcloud exclude tests --- .sonarcloud.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 31506853..4c10ebc4 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,6 +1,6 @@ # Path to sources #sonar.sources=. -#sonar.exclusions= +sonar.exclusions=tests #sonar.inclusions= # Path to tests From ec92a1ad3482c8e0d7fc354e800ecc56bf309aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 21 Aug 2020 16:25:19 +0200 Subject: [PATCH 5/5] Revert "Add sonarcloud settings file" This reverts commits: 8792dd22cee1e73a914815a208b0ee92629c6295, 767eef635cf1a5d1d917da8f754479d00994ef87. --- .sonarcloud.properties | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties deleted file mode 100644 index 4c10ebc4..00000000 --- a/.sonarcloud.properties +++ /dev/null @@ -1,15 +0,0 @@ -# Path to sources -#sonar.sources=. -sonar.exclusions=tests -#sonar.inclusions= - -# Path to tests -sonar.tests=tests -#sonar.test.exclusions= -#sonar.test.inclusions= - -# Source encoding -#sonar.sourceEncoding=UTF-8 - -# Exclusions for copy-paste detection -#sonar.cpd.exclusions=