resources/lib/qobuz/node/inode/main.py
'''
qobuz.node.inode.main
~~~~~~~~~~~~~~~~~~~~~
:part_of: kodi-qobuz
:copyright: (c) 2012-2018 by Joachim Basmaison, Cyril Leclerc
:license: GPLv3, see LICENSE for more details.
'''
import os
import sys
import urllib
from .context_menu import attach_context_menu
from .pagination import add_pagination
from .props import node_type_from_class, node_image_from_class
from .props import node_contenttype_from_class
from qobuz import config
from qobuz import exception
from qobuz.api import api
from qobuz.api.user import current as current_user
from qobuz.cache import cache
from qobuz.constants import Mode
from qobuz.debug import getLogger
from qobuz.gui.contextmenu import contextMenu
from qobuz.node import Flag, getNode, helper
from qobuz.renderer import renderer
from qobuz.storage import Storage
from qobuz.util import data as dataUtil
from qobuz.util import properties
from qobuz.util.converter import converter
from qobuz.util.random import randrange
logger = getLogger(__name__)
def get_property_helper(data, path, to):
try:
_path, value = properties.deep_get(data, path)
except KeyError:
return None
if value is None:
return None
return getattr(converter, to)(value)
class INode(object):
'''Our base node, every node must inherit or mimic is behaviour
Calling build_down on a node start the build process
- pre_build_down: Retrieve data (disk, internet...) and store
result in self.data
- _build_down: If pre_build_down return true, parse data
and populate our node with childs
The main build_down method is responsible for the logic flow
(recursive, depth, whiteFlag, blackFlag...)
'''
def __init__(self, parent=None, parameters=None, data=None):
'''Constructor
@param parent=None: Parent node if not None
@param parameters={}: dictionary
'''
self._content_type = None
self._data = None
self._label = None
self._nid = None
self._parent = None
self.data = data
self.parameters = {} if parameters is None else parameters
self.parent = parent
self.content_type = node_contenttype_from_class(
self.__class__) or 'files'
self.image = node_image_from_class(self.__class__)
self.nt = node_type_from_class(self.__class__)
self.childs = []
self.hasWidget = False
self.is_folder = True
self.label = None
self.label2 = None
self.limit = config.app.registry.get('pagination_limit', to='int')
self.mode = self.get_parameter('mode', to='int')
self.nid = self.get_parameter(
'nid', default=None) or self.get_property('id', default=None)
self.node_storage = None
self.offset = self.get_parameter('offset', default=0)
self.pagination_next = None
self.pagination_prev = None
self.user_storage = None
self.user_storage = None
def set_nid(self, value):
'''@setter nid'''
self._nid = value
def get_nid(self):
'''@getter nid'''
if self._data and 'id' in self._data:
return self._data['id']
return self._nid
nid = property(get_nid, set_nid)
def get_parent(self):
'''@getter parent'''
if self._parent is None:
return None
return self._parent
def set_parent(self, parent):
'''@setter parent'''
if parent is None:
self._parent = None
return
self._parent = parent
parent = property(get_parent, set_parent)
def get_content_type(self):
'''@getter content_type'''
return self._content_type
def set_content_type(self, kind):
'''@setter content_type'''
if kind not in [
'files', 'songs', 'artists', 'albums', 'movies', 'tvshows',
'episodes', 'musicvideos'
]:
raise exception.InvalidKind(kind)
self._content_type = kind
content_type = property(get_content_type, set_content_type)
def get_data(self):
return self._data
def set_data(self, value):
self._data = value
self.hook_post_data()
data = property(get_data, set_data)
def hook_post_data(self):
''' Called after node data is set '''
pass
def set_property(self, pathList, value):
if self.data is None:
raise KeyError('Node without data')
root = self.data
for part in pathList.split('/'):
if part not in root:
root[part] = {}
root = root[part]
root = value
def get_property(self, pathList, default=u'', to='raw'):
'''Property are just a easy way to access JSON data (self.data)
@param pathList: a string or a list of string, each string can be
a path like 'album/image/large'
@return: string (empty string when all fail or when there's no data)
* When passing array of string the method return the first
path returning data
Example:
image = self.get_property(['image/extralarge',
'image/mega',
'picture'])
'''
if self.data is None:
return default
if isinstance(pathList, basestring):
pathList = [pathList]
for path in pathList:
value = get_property_helper(self.data, path, to)
if value is not None:
return value
return default
def __getattr__(self, attr):
if attr.startswith('get_') and hasattr(self, 'propsMap'):
key = attr[4:]
if key in self.propsMap:
prop = self.propsMap.get(key)
default = prop.get('default') if hasattr(
prop, 'default') else None
if self.data is None:
return lambda *a, **ka: default
try:
_path, value = properties.get_mapped(
self.data,
self.propsMap,
key,
default=default)
return lambda *a, **ka: value
except KeyError:
pass
raise AttributeError(attr)
def __add_pagination(self, data):
return add_pagination(self, data)
def set_parameters(self, parameters):
'''Setting parameters property
@param parameters: Dictionary
'''
self.parameters = parameters
def set_parameter(self, name, value, quote=False, **ka):
'''Setting a parameter
@param name: parameter name
@param value: parameter value
@param quote=False: use urllib.quote_plus against return value when True
'''
if quote is True:
value = urllib.quote_plus(value)
self.parameters[name] = value
def get_parameter(self, name, default=None, to='raw'):
'''Getting parameter by name
@param name: parameter name
@param default: value set when parameter not found or value is None
@param to='raw': string, converter used
'''
if self.parameters is None:
return getattr(converter, to)(default)
if name not in self.parameters:
return getattr(converter, to)(default)
value = self.parameters[name]
if value is None:
return getattr(converter, to)(default)
return getattr(converter, to)(value)
def del_parameter(self, name):
'''Deleting parameter
@param name: parameter name
'''
if name not in self.parameters:
return False
del self.parameters[name]
return True
def make_url(self, **ka):
'''Generate URL to navigate between nodes
Nodes with custom parameters must override this method
@todo: Ugly need rewrite =]
'''
if 'mode' not in ka:
ka['mode'] = Mode.VIEW
if 'nt' not in ka:
ka['nt'] = self.nt
if 'nid' not in ka:
ka['nid'] = self.nid
if 'offset' not in ka:
ka['offset'] = self.offset
if 'asLocalUrl' in ka and not ka['asLocalUrl']:
del ka['asLocalUrl']
if 'offset' in ka and ka['offset'] == 0:
del ka['offset']
for name in ['qnt', 'qid', 'query', 'search-type', 'mode']:
if name in ka:
continue
value = self.get_parameter(name)
if value is None:
continue
ka[name] = self.get_parameter(name)
url = sys.argv[0] + '?'
for key in sorted(ka):
value = ka[key]
if value is None:
continue
value = str(value).strip()
if value == '':
continue
url += key + '=' + value + '&'
url = url[:-1]
return url
def makeListItem(self, **ka):
'''
Make Xbmc List Item
return a xbml list item
Class can overload this method
'''
from kodi_six import xbmcgui # pylint:disable=E0401
if 'url' not in ka:
ka['url'] = self.make_url()
if 'label' not in ka:
ka['label'] = self.get_label()
if 'label2' not in ka:
ka['label2'] = self.get_label()
if 'image' not in ka:
ka['image'] = self.get_image()
if 'asLocalUrl' in ka and not ka['asLocalUrl']:
del ka['asLocalUrl']
item = xbmcgui.ListItem(ka['label'], ka['label2'], ka['image'],
ka['image'], ka['url'])
ctxMenu = contextMenu()
self.attach_context_menu(item, ctxMenu)
item.addContextMenuItems(ctxMenu.getTuples(), ka['replaceItems'])
return item
def add_child(self, child):
child.parent = self
child.set_parameters(self.parameters)
self.childs.append(child)
return self
def get_childs(self):
return self.childs
def set_label(self, label):
self._label = label
return self
def get_label(self, default=None):
if self._label is None:
return default
return self._label
label = property(get_label, set_label)
def get_image(self):
if self.image:
return self.image
if self.parent:
return self.parent.get_image()
if self.data is not None:
for name in ['images300', 'images150', 'images']:
if name in self.data and len(self.data[name]) > 0:
return self.data[name][randrange(0,
len(self.data[name]))]
return self.get_property('image')
def set_image(self, image):
self.image = image
return self
def get_label2(self):
return self.label2
@classmethod
def render_nodes(cls,
nt,
parameters,
lvl=1,
whiteFlag=Flag.ALL,
blackFlag=Flag.TRACK & Flag.STOPBUILD):
render = renderer(nt, parameters)
render.depth = -1
render.whiteFlag = whiteFlag
render.blackFlag = blackFlag
render.asList = True
render.run()
return render
@classmethod
def fetch(cls, options=None):
'''When returning None we are not displaying directory content
'''
return {}
def populating(self, options=None):
options = options if options is not None else helper.TreeTraverseOpts()
data = {} if options.data is None else options.data
if options.lvl != -1 and options.lvl < 1:
return False
if self.nt & options.blackFlag != self.nt:
new_data = self.fetch(options)
if new_data is None:
return False
data.update(new_data)
self.data = data
self.__add_pagination(self.data)
self.populate(options)
new_options = options.clone()
if options.lvl != -1:
new_options.lvl -= 1
self.__add_pagination_node(options.xdir,
options.lvl,
options.whiteFlag)
for child in self.childs:
if (child.nt & options.whiteFlag == child.nt) and not (
options.xdir.add_node(child)):
logger.error('Could not add node')
continue
child.populating(new_options)
def populate(self, options=None):
'''Hook / _build_down:
This method is called by build_down, each object who
inherit from Inode can overide it. Lot of object
simply fetch data from qobuz (cached data)
'''
pass
def __add_pagination_node(self, Dir, lvl=1, whiteFlag=Flag.NODE):
"""Helper/Called by build_down to add special node when pagination is
required
"""
if self.pagination_next:
params = config.app.bootstrap.params
params['offset'] = self.pagination_next_offset
params['nid'] = self.nid
node = getNode(self.nt, params)
node.data = self.data
node.label = u'{label} [{next_offset} / {pagination_total}]'.format(
label=self.get_label(),
next_offset=self.pagination_next_offset,
pagination_total=self.pagination_total)
self.add_child(node)
def attach_context_menu(self, item, menu):
return attach_context_menu(self, item, menu)
def get_user_storage(self):
if self.user_storage is not None:
return self.user_storage
filename = os.path.join(cache.base_path,
'user-%s.local' % str(current_user.get_id()))
self.user_storage = Storage(filename)
return self.user_storage
@classmethod
def get_user_path(cls):
return os.path.join(cache.base_path)
@classmethod
def get_user_data(cls):
data = api.get('/user/login',
username=current_user.username,
password=current_user.password)
if not data:
return None
return data['user']
def get_class_name(self):
return self.__class__.__name__
def as_dict(self):
d = {}
for k in ['class_name', 'nid', 'parent']:
d[k] = getattr(self, 'get_%s' % k)()
return d
def __str__(self):
return '<{class_name} nid={nid}>'.format(**self.as_dict())
def get_node_storage_path(self):
return os.path.join(cache.base_path, self._get_node_storage_filename())
def get_node_storage(self):
if self.node_storage is not None:
return self.node_storage
self.node_storage = Storage(self.get_node_storage_path())
return self.node_storage
def remove_node_storage(self):
filename = self.get_node_storage_path()
if os.path.exists(filename):
os.unlink(filename)
def _get_node_storage_filename(self):
raise NotImplementedError(self)
def remove_playlist_storage(self):
path = os.path.join(cache.base_path, self._get_node_storage_filename())
if os.path.exists(path):
os.unlink(path)
def get_image_from_storage(self):
desired_size = config.app.registry.get('image_default_size',
default=None)
images = []
if self.nid is not None:
storage = self.get_node_storage()
if storage is not None:
images_len = 0
if 'image' not in storage:
images = dataUtil.list_image(
self.data, desired_size=desired_size)
images_len = len(images)
if images_len > 0:
storage['image'] = images
storage.sync()
else:
images = storage['image']
images_len = len(images)
if images_len > 0:
return images[randrange(0, images_len)]
else:
logger.error('Cannot get node storage')
return None
def count(self):
if self.data is None:
raise exception.NodeHasNoData(Flag.to_s(self.nt))
if not hasattr(self, '_count'):
raise exception.NodeHasNoCountMethod(Flag.to_s(self.nt))
return getattr(self, '_count')()