Merge pull request #366 from oddstr13/pr-json-filter-1

Filter keys containing None values from dictionaries returned from the server
This commit is contained in:
mcarlton00 2020-08-21 17:43:52 -04:00 committed by GitHub
commit 4e2c8a0af3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 214 additions and 26 deletions

View file

@ -1,9 +1,11 @@
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import yaml
import sys import sys
import os import os
from datetime import datetime from datetime import datetime
import yaml
def indent(elem, level=0): def indent(elem, level=0):
''' '''
Nicely formats output xml with newlines and spaces 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()): if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i elem.tail = i
try: try:
py_version = sys.argv[1] py_version = sys.argv[1]
except IndexError: except IndexError:

66
.gitignore vendored
View file

@ -1,12 +1,72 @@
*.pyo # Byte-compiled / optimized / DLL files
__pycache__/ __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__/ __local__/
machine_guid machine_guid
/resources/media/Thumbs.db Thumbs.db
.idea/ .idea/
.DS_Store .DS_Store
.vscode/ .vscode/
pyinstrument/ pyinstrument/
pyinstrument_cext.so
# Now managed by templates
addon.xml addon.xml
*.log

View file

@ -17,8 +17,8 @@ sys.path.insert(0, __base__)
################################################################################################# #################################################################################################
from entrypoint import Context # noqa: F402 from entrypoint import Context # noqa: E402
from helper import LazyLogger # noqa: F402 from helper import LazyLogger # noqa: E402
################################################################################################# #################################################################################################

View file

@ -17,8 +17,8 @@ sys.path.insert(0, __base__)
################################################################################################# #################################################################################################
from entrypoint import Context # noqa: F402 from entrypoint import Context # noqa: E402
from helper import LazyLogger # noqa: F402 from helper import LazyLogger # noqa: E402
################################################################################################# #################################################################################################

View file

@ -17,8 +17,8 @@ sys.path.insert(0, __base__)
################################################################################################# #################################################################################################
from entrypoint import Events # noqa: F402 from entrypoint import Events # noqa: E402
from helper import LazyLogger # noqa: F402 from helper import LazyLogger # noqa: E402
################################################################################################# #################################################################################################

View file

@ -224,6 +224,7 @@ def get_library_items(library_id, item_type):
return _get(url, params) return _get(url, params)
def get_albums_by_artist(artist_id, basic=False): def get_albums_by_artist(artist_id, basic=False):
params = { params = {

View file

@ -435,13 +435,13 @@ class FullSync(object):
obj.artist(artist) obj.artist(artist)
# Get all albums for each 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: for album in artist_albums:
# Add album to database # Add album to database
obj.album(album) obj.album(album)
album_id = album.get('Id') album_id = album.get('Id')
# Get all songs in each album # 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: for song in album_songs:
dialog.update(percent, dialog.update(percent,
message="%s/%s/%s" % (artist_name, album['Name'][:7], song['Name'][:7])) message="%s/%s/%s" % (artist_name, album['Name'][:7], song['Name'][:7]))

View file

@ -435,7 +435,7 @@ class API(object):
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
return { 'Status_Code': response.status_code } return {'Status_Code': response.status_code}
def get_public_info(self, server_address): def get_public_info(self, server_address):
response = self.send_request(server_address, "system/info/public") response = self.send_request(server_address, "system/info/public")

View file

@ -11,6 +11,7 @@ from six import string_types, ensure_str
from helper.utils import JsonDebugPrinter from helper.utils import JsonDebugPrinter
from helper import LazyLogger from helper import LazyLogger
from helper.exceptions import HTTPException from helper.exceptions import HTTPException
from jellyfin.utils import clean_none_dict_values
################################################################################################# #################################################################################################
@ -56,7 +57,7 @@ class HTTP(object):
else: else:
LOG.debug("Server address not set") LOG.debug("Server address not set")
if '{UserId}'in string: if '{UserId}' in string:
if self.config.data.get('auth.user_id', None): if self.config.data.get('auth.user_id', None):
string = string.replace("{UserId}", self.config.data['auth.user_id']) string = string.replace("{UserId}", self.config.data['auth.user_id'])
else: else:
@ -161,7 +162,7 @@ class HTTP(object):
LOG.debug("---<[ http ][%s ms]", elapsed) LOG.debug("---<[ http ][%s ms]", elapsed)
LOG.debug(JsonDebugPrinter(response)) LOG.debug(JsonDebugPrinter(response))
return response return clean_none_dict_values(response)
except ValueError: except ValueError:
return return

View file

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

View file

@ -376,7 +376,7 @@ class Library(threading.Thread):
include = [] include = []
filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"] filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"]
sync = get_sync() 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) LOG.info("--[ retrieve changes ] %s", last_sync)
# Get the item type of each synced library and build list of types to request # Get the item type of each synced library and build list of types to request
@ -395,7 +395,7 @@ class Library(threading.Thread):
try: try:
# Get list of updates from server for synced library types and populate work queues # 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: if result is None:
return True return True

View file

@ -191,13 +191,13 @@ class Movies(KodiDb):
if validate_dvd_dir(obj['Path'] + obj['Filename']): if validate_dvd_dir(obj['Path'] + obj['Filename']):
obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/' obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/'
obj['Filename'] = 'VIDEO_TS.IFO' 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''' '''check bluray directries and point it to ./BDMV/index.bdmv'''
if validate_bluray_dir(obj['Path'] + obj['Filename']): if validate_bluray_dir(obj['Path'] + obj['Filename']):
obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/' obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/'
obj['Filename'] = 'index.bdmv' obj['Filename'] = 'index.bdmv'
LOG.debug("Bluray directry %s",obj['Path']) LOG.debug("Bluray directry %s", obj['Path'])
else: else:
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId']

View file

@ -160,9 +160,9 @@ class Player(xbmc.Player):
def set_audio_subs(self, audio=None, subtitle=None): def set_audio_subs(self, audio=None, subtitle=None):
if audio: if audio:
audio=int(audio) audio = int(audio)
if subtitle: if subtitle:
subtitle=int(subtitle) subtitle = int(subtitle)
''' Only for after playback started ''' Only for after playback started
''' '''

13
requirements-dev.txt Normal file
View file

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

View file

@ -18,9 +18,9 @@ sys.path.insert(0, __base__)
################################################################################################# #################################################################################################
from entrypoint import Service # noqa: F402 from entrypoint import Service # noqa: E402
from helper.utils import settings # noqa: F402 from helper.utils import settings # noqa: E402
from helper import LazyLogger # noqa: F402 from helper import LazyLogger # noqa: E402
################################################################################################# #################################################################################################

0
tests/__init__.py Normal file
View file

View file

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

16
tox.ini
View file

@ -6,3 +6,19 @@ extend-ignore =
I202 I202
per-file-ignores = per-file-ignores =
*/__init__.py: F401 */__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