Skip to content

Commit

Permalink
Fix #9: Move mapping helpers to translator module.
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed Aug 18, 2015
1 parent 5df8d6d commit d700e1d
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 178 deletions.
18 changes: 16 additions & 2 deletions mopidy_dleyna/dleyna.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import threading
import time

import dbus

Expand Down Expand Up @@ -152,10 +153,23 @@ def error_handler(e):
@classmethod
def __call_async(cls, func, *args, **kwargs):
future = cls.Future()
name = getattr(func, '_method_name', 'method')
t = time.time()

def error(e):
def reply_handler(value):
logger.debug('dLeyna %s reply after %.3fs', name, time.time() - t)
future.set(value)

def error_handler(e):
logger.debug('dLeyna %s error after %.3fs', name, time.time() - t)
future.set_exception(exc_info=(type(e), e, None))
func(*args, reply_handler=future.set, error_handler=error, **kwargs)

func(
*args,
reply_handler=reply_handler,
error_handler=error_handler,
**kwargs
)
return future

if __name__ == '__main__':
Expand Down
212 changes: 36 additions & 176 deletions mopidy_dleyna/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,17 @@

import collections
import logging
import os

from mopidy import backend
from mopidy.models import Album, Artist, Image, Ref, SearchResult, Track

import pykka

from uritools import uricompose, urisplit

from . import Extension
from . import Extension, translator

logger = logging.getLogger(__name__)


ALBUM_TYPE_EX = 'container.album.musicAlbum'

ARTIST_TYPE_EX = 'container.person.musicArtist'

BROWSE_FILTER = [
'DisplayName',
'Path',
Expand All @@ -43,14 +36,15 @@
'DisplayName',
'Duration',
'Genre',
'Parent',
'Path',
'RefPath',
'TrackNumber',
'Type',
'TypeEx',
]

LOOKUP_QUERY = 'Type = "music"'
LOOKUP_QUERY = 'Type = "music" or Type = "audio"'

QUERY_MAPPING = [{
'any': """
Expand Down Expand Up @@ -85,143 +79,15 @@
# 'comment'
}]

SCHEME = Extension.ext_name


def _quote(s):
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'


def _name_to_uri(path, type_ex, name):
root = os.path.dirname(path) # TODO: obj['Parent']?
query = 'TypeEx = %s and DisplayName = %s' % (type_ex, _quote(name))
return uricompose(SCHEME, path=root, query=query)


def _name_to_album(path, name):
return Album(name=name, uri=_name_to_uri(path, ALBUM_TYPE_EX, name))


def _name_to_artist(path, name):
return Artist(name=name, uri=_name_to_uri(path, ARTIST_TYPE_EX, name))


def _properties_to_ref(server, obj):
if 'RefPath' in obj:
assert obj['RefPath'].startswith(server['Path'])
path = obj['RefPath'][len(server['Path']):]
else:
assert obj['Path'].startswith(server['Path'])
path = obj['Path'][len(server['Path']):]
uri = uricompose(SCHEME, host=server['UDN'], path=path)
name = obj['DisplayName']
type_ex = obj.get('TypeEx', obj['Type'])
if type_ex == ALBUM_TYPE_EX:
return Ref.album(name=name, uri=uri)
elif type_ex == ARTIST_TYPE_EX:
return Ref.artist(name=name, uri=uri)
elif type_ex == 'music' or type_ex == 'audio':
return Ref.track(name=name, uri=uri)
elif obj['Type'] == 'container':
return Ref.directory(name=name, uri=uri)
else:
return None


def _properties_to_model(server, obj):
type_ex = obj.get('TypeEx', obj['Type'])
if type_ex == ALBUM_TYPE_EX:
return _properties_to_album(server, obj)
elif type_ex == ARTIST_TYPE_EX:
return _properties_to_artist(server, obj)
elif type_ex == 'music' or type_ex == 'audio':
return _properties_to_track(server, obj)
else:
return None


def _properties_to_album(server, obj):
if 'RefPath' in obj:
assert obj['RefPath'].startswith(server['Path'])
path = obj['RefPath'][len(server['Path']):]
else:
assert obj['Path'].startswith(server['Path'])
path = obj['Path'][len(server['Path']):]
if 'Creator' in obj:
artists = [_name_to_artist(path, obj['Creator'])]
else:
artists = None
if 'AlbumArtURL' in obj:
images = [obj['AlbumArtURL']]
else:
images = None
return Album(
uri=uricompose(SCHEME, host=server['UDN'], path=path),
name=obj['DisplayName'],
artists=artists,
images=images,
num_tracks=obj.get('ItemCount', obj.get('ChildCount')),
)


def _properties_to_artist(server, obj):
if 'RefPath' in obj:
assert obj['RefPath'].startswith(server['Path'])
path = obj['RefPath'][len(server['Path']):]
else:
assert obj['Path'].startswith(server['Path'])
path = obj['Path'][len(server['Path']):]
return Artist(
uri=uricompose(SCHEME, host=server['UDN'], path=path),
name=obj['DisplayName']
)


def _properties_to_track(server, obj):
if 'RefPath' in obj:
assert obj['RefPath'].startswith(server['Path'])
path = obj['RefPath'][len(server['Path']):]
else:
assert obj['Path'].startswith(server['Path'])
path = obj['Path'][len(server['Path']):]
if 'Album' in obj:
album = _name_to_album(path, obj['Album'])
if 'AlbumArtURL' in obj:
album = album.copy(images=[obj['AlbumArtURL']])
else:
album = None
if 'Artists' in obj:
artists = [_name_to_artist(path, name) for name in obj['Artists']]
elif 'Artist' in obj:
artists = [_name_to_artist(path, obj['Artists'])]
else:
artists = None
if 'Duration' in obj:
length = obj['Duration'] * 1000
else:
length = None
return Track(
uri=uricompose(SCHEME, host=server['UDN'], path=path),
name=obj['DisplayName'],
artists=artists,
album=album,
date=obj.get('Date'),
genre=obj.get('Genre'),
length=length,
track_no=obj.get('TrackNumber')
)


def _map(future, func):
wrapper = future.__class__()
wrapper.set_get_hook(lambda timeout: func(future.get(timeout)))
return wrapper


class dLeynaLibraryProvider(backend.LibraryProvider):

root_directory = Ref.directory(
uri=uricompose(SCHEME),
uri=uricompose(Extension.ext_name),
name='Digital Media Servers'
)

Expand All @@ -231,19 +97,21 @@ def browse(self, uri):
if uri == self.root_directory.uri:
for server in dleyna.servers().get():
name = server['FriendlyName']
uri = uricompose(SCHEME, host=server['UDN'])
uri = uricompose(Extension.ext_name, host=server['UDN'])
refs.append(Ref.directory(name=name, uri=uri))
else:
parts = urisplit(uri)
server = dleyna.server(parts.gethost()).get()
baseuri = uricompose('dleyna', server['UDN'])
path = server['Path'] + parts.getpath()
future = dleyna.children(path, filter=BROWSE_FILTER)
for obj in future.get():
ref = _properties_to_ref(server, obj)
if ref:
refs.append(ref)
try:
ref = translator.ref(baseuri, obj)
except ValueError as e:
logger.warn('Skipping dLeyna browse result: %s', e)
else:
logger.debug('Skipping dLeyna browse result %r', obj)
refs.append(ref)
return refs

def get_images(self, uris):
Expand Down Expand Up @@ -271,31 +139,23 @@ def get_images(self, uris):
results[urimap[obj['Path']]] = [image]
except KeyError:
logger.warn('Unexpected dLeyna result path: %s', obj['Path'])
logger.debug('images results: %r', results)
return results

def lookup(self, uri):
parts = urisplit(uri)
dleyna = self.backend.dleyna
server = dleyna.server(parts.gethost()).get()
path = server['Path'] + parts.getpath()
props = dleyna.properties(path).get()
type = props['Type']
uriparts = urisplit(uri)
server = dleyna.server(uriparts.gethost()).get()
baseuri = uricompose(Extension.ext_name, server['UDN'])
path = server['Path'] + uriparts.getpath()
properties = dleyna.properties(path).get()

tracks = []
if type == 'container':
if properties['Type'] == 'container':
future = dleyna.search(path, LOOKUP_QUERY, filter=SEARCH_FILTER)
for obj in future.get():
track = _properties_to_track(server, obj)
if track:
tracks.append(track)
else:
logger.debug('Skipping dLeyna lookup result %r', obj)
elif type == 'music' or type == 'audio':
tracks.append(_properties_to_track(server, props))
return [translator.track(baseuri, obj) for obj in future.get()]
elif properties['Type'] in ('music', 'audio'):
return [translator.track(baseuri, properties)]
else:
logger.warn('Invalid dLeyna type for %s: %s', uri, type)
return tracks
raise ValueError('Invalid type for %s: %s', uri, obj['Type'])

def refresh(self, uri=None):
logger.info('Refreshing dLeyna library')
Expand All @@ -315,26 +175,26 @@ def search(self, query=None, uris=None, exact=False):
query = '*'
logger.debug('dLeyna search query: %s', query)

# TODO: refactor this (no future.map from reply_handler?)
futures = []
for uri in uris or [self.root_directory.uri]:
if uri == self.root_directory.uri:
for server in self.backend.dleyna.servers().get():
uri = uricompose(SCHEME, host=server['UDN'])
uri = uricompose(Extension.ext_name, host=server['UDN'])
futures.append(self.__search(query, uri))
else:
futures.append(self.__search(query, uri))

results = collections.defaultdict(list)
for objs in pykka.get_all(futures):
for server, obj in objs:
model = _properties_to_model(server, obj)
if model:
results[type(model)].append(model)
for baseuri, objs in (f.get() for f in futures):
for obj in objs:
try:
model = translator.model(baseuri, obj)
except ValueError:
logger.warn('Skipping dLeyna search result %r', obj)
else:
logger.debug('Skipping dLeyna search result %r', obj)
results[type(model)].append(model)
return SearchResult(
uri=uricompose(SCHEME, query=query),
uri=uricompose(Extension.ext_name, query=query),
albums=results[Album],
artists=results[Artist],
tracks=results[Track]
Expand All @@ -352,14 +212,14 @@ def __lookup(self, server, paths, filter=['*'], limit=10):
futures.append(dleyna.search(root, query, 0, 0, filter))
else:
for path in paths:
# pykka.Future.map() doesn't work with Dictionary value
futures.append(_map(dleyna.properties(path), lambda x: [x]))
futures.append(dleyna.properties(path).apply(lambda x: [x]))
return futures

def __search(self, query, uri):
parts = urisplit(uri)
dleyna = self.backend.dleyna
server = dleyna.server(parts.gethost()).get()
path = server['Path'] + parts.getpath()
uriparts = urisplit(uri)
server = dleyna.server(uriparts.gethost()).get()
baseuri = uricompose(Extension.ext_name, server['UDN'])
path = server['Path'] + uriparts.getpath()
future = dleyna.search(path, query, filter=SEARCH_FILTER)
return future.map(lambda result: (server, result))
return future.apply(lambda result: (baseuri, result))

0 comments on commit d700e1d

Please sign in to comment.