From 8a3ca73d5267e664a385c93e36a9c779c1f6c20f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= <oddstr13@openshell.no>
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?= <oddstr13@openshell.no>
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?= <oddstr13@openshell.no>
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?= <oddstr13@openshell.no>
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?= <oddstr13@openshell.no>
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=