Merge branch 'develop' into feature/commands-helper-tmp

Conflicts:
	mopidy/utils/log.py
This commit is contained in:
Thomas Adamcik 2013-11-14 23:36:27 +01:00
commit 37ba3ca01d
34 changed files with 975 additions and 103 deletions

View File

@ -28,3 +28,4 @@
- Pavol Babincak <scroolik@gmail.com>
- Javier Domingo <javierdo1@gmail.com>
- Lasse Bigum <lasse@bigum.org>
- David Eisner <david.eisner@oriel.oxon.org>

View File

@ -10,18 +10,61 @@ v0.17.0 (UNRELEASED)
**Core**
- The :class:`~mopidy.models.Track` model has grown fields for ``composers``,
``performers``, ``genre``, and ``comment``.
- The search field ``track`` has been renamed to ``track_name`` to avoid
confusion with ``track_no``. (Fixes: :issue:`535`)
- The signature of the tracklist's
:meth:`~mopidy.core.TracklistController.filter` and
:meth:`~mopidy.core.TracklistController.remove` methods have changed.
Previously, they expected e.g. ``tracklist.filter(tlid=17)``. Now, the value
must always be a list, e.g. ``tracklist.filter(tlid=[17])``. This change
allows you to get or remove multiple tracks with a single call, e.g.
``tracklist.remove(tlid=[1, 2, 7])``. This is especially useful for web
clients, as requests can be batched. This also brings the interface closer to
the library's :meth:`~mopidy.core.LibraryController.find_exact` and
:meth:`~mopidy.core.LibraryController.search` methods.
**Local backend**
- Library scanning has been switched back to custom code due to various issues
with GStreamer's built in scanner in 0.10. This also fixes the scanner
slowdown. (Fixes: :issue:`565`)
- When scanning, we no longer default the album artist to be the same as the
track artist. Album artist is now only populated if the scanned file got an
explicit album artist set.
- Library scanning has been switched back to custom code due to various issues
with GStreamer's built in scanner in 0.10. This also fixes the scanner slowdown.
(Fixes: :issue:`565`)
- Fix scanner so that mtime is respected when deciding which files can be skipped.
- The scanner will now extract multiple artists from files with multiple artist
tags.
- The scanner will now extract composers and performers, as well as genre,
bitrate, and comments. (Fixes: :issue:`577`)
- Fix scanner so that time of last modification is respected when deciding
which files can be skipped.
**MPD frontend**
- The MPD service is now published as a Zeroconf service if avahi-daemon is
running on the system. Some MPD clients will use this to present Mopidy as an
available server on the local network without needing any configuration. See
the :confval:`mpd/zeroconf` config value to change the service name or
disable the service. (Fixes: :issue:`39`)
- Add support for ``composer``, ``performer``, ``comment``, ``genre``, and
``performer``. These tags can be used with ``list ...``, ``search ...``, and
``find ...`` and their variants, and are supported in the ``any`` tag also
**HTTP frontend**
- The HTTP service is now published as a Zeroconf service if avahi-daemon is
running on the system. Some browsers will present HTTP Zeroconf services on
the local network as "local sites" bookmarks. See the
:confval:`http/zeroconf` config value to change the service name or disable
the service. (Fixes: :issue:`39`)
**Sub-commands**

View File

@ -59,6 +59,13 @@ Configuration values
Change this to have Mopidy serve e.g. files for your JavaScript client.
"/mopidy" will continue to work as usual even if you change this setting.
.. confval:: http/zeroconf
Name of the HTTP service when published through Zeroconf. The variables
``$hostname`` and ``$port`` can be used in the name.
Set to an empty string to disable Zeroconf for HTTP.
Usage
=====

View File

@ -34,7 +34,6 @@ Items on this list will probably not be supported in the near future.
- Crossfade is not supported
- Replay gain is not supported
- ``stats`` does not provide any statistics
- ``list`` does not support listing tracks by genre
- ``decoders`` does not provide information about available decoders
The following items are currently not supported, but should be added in the
@ -97,6 +96,13 @@ Configuration values
Number of seconds an MPD client can stay inactive before the connection is
closed by the server.
.. confval:: mpd/zeroconf
Name of the MPD service when published through Zeroconf. The variables
``$hostname`` and ``$port`` can be used in the name.
Set to an empty string to disable Zeroconf for MPD.
Usage
=====

View File

@ -121,6 +121,8 @@ def audio_data_to_track(data):
albumartist_kwargs = {}
album_kwargs = {}
artist_kwargs = {}
composer_kwargs = {}
performer_kwargs = {}
track_kwargs = {}
def _retrieve(source_key, target_key, target):
@ -131,6 +133,22 @@ def audio_data_to_track(data):
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
_retrieve(gst.TAG_ALBUM_VOLUME_COUNT, 'num_discs', album_kwargs)
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
_retrieve(gst.TAG_COMPOSER, 'name', composer_kwargs)
_retrieve(gst.TAG_PERFORMER, 'name', performer_kwargs)
_retrieve(gst.TAG_ALBUM_ARTIST, 'name', albumartist_kwargs)
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
_retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs)
_retrieve(gst.TAG_GENRE, 'genre', track_kwargs)
_retrieve(gst.TAG_BITRATE, 'bitrate', track_kwargs)
# Following keys don't seem to have TAG_* constant.
_retrieve('comment', 'comment', track_kwargs)
_retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs)
_retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs)
_retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs)
_retrieve(
'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
if gst.TAG_DATE in data and data[gst.TAG_DATE]:
date = data[gst.TAG_DATE]
@ -141,18 +159,6 @@ def audio_data_to_track(data):
else:
track_kwargs['date'] = date.isoformat()
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
_retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs)
# Following keys don't seem to have TAG_* constant.
_retrieve('album-artist', 'name', albumartist_kwargs)
_retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs)
_retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs)
_retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs)
_retrieve(
'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
if albumartist_kwargs:
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
@ -160,6 +166,28 @@ def audio_data_to_track(data):
track_kwargs['last_modified'] = int(data['mtime'])
track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND
track_kwargs['album'] = Album(**album_kwargs)
track_kwargs['artists'] = [Artist(**artist_kwargs)]
if ('name' in artist_kwargs
and not isinstance(artist_kwargs['name'], basestring)):
track_kwargs['artists'] = [Artist(name=artist)
for artist in artist_kwargs['name']]
else:
track_kwargs['artists'] = [Artist(**artist_kwargs)]
if ('name' in composer_kwargs
and not isinstance(composer_kwargs['name'], basestring)):
track_kwargs['composers'] = [Artist(name=artist)
for artist in composer_kwargs['name']]
else:
track_kwargs['composers'] = \
[Artist(**composer_kwargs)] if composer_kwargs else ''
if ('name' in performer_kwargs
and not isinstance(performer_kwargs['name'], basestring)):
track_kwargs['performers'] = [Artist(name=artist)
for artist in performer_kwargs['name']]
else:
track_kwargs['performers'] = \
[Artist(**performer_kwargs)] if performer_kwargs else ''
return Track(**track_kwargs)

View File

@ -79,15 +79,28 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
albumartist_filter = lambda t: any([
q == a.name
for a in getattr(t.album, 'artists', [])])
composer_filter = lambda t: any([
q == a.name
for a in getattr(t, 'composers', [])])
performer_filter = lambda t: any([
q == a.name
for a in getattr(t, 'performers', [])])
track_no_filter = lambda t: q == t.track_no
genre_filter = lambda t: t.genre and q == t.genre
date_filter = lambda t: q == t.date
comment_filter = lambda t: q == t.comment
any_filter = lambda t: (
uri_filter(t) or
track_name_filter(t) or
album_filter(t) or
artist_filter(t) or
albumartist_filter(t) or
date_filter(t))
composer_filter(t) or
performer_filter(t) or
track_no_filter(t) or
genre_filter(t) or
date_filter(t) or
comment_filter(t))
if field == 'uri':
result_tracks = filter(uri_filter, result_tracks)
@ -99,10 +112,18 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(artist_filter, result_tracks)
elif field == 'albumartist':
result_tracks = filter(albumartist_filter, result_tracks)
elif field == 'composer':
result_tracks = filter(composer_filter, result_tracks)
elif field == 'performer':
result_tracks = filter(performer_filter, result_tracks)
elif field == 'track_no':
result_tracks = filter(track_no_filter, result_tracks)
elif field == 'genre':
result_tracks = filter(genre_filter, result_tracks)
elif field == 'date':
result_tracks = filter(date_filter, result_tracks)
elif field == 'comment':
result_tracks = filter(comment_filter, result_tracks)
elif field == 'any':
result_tracks = filter(any_filter, result_tracks)
else:
@ -137,15 +158,28 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
albumartist_filter = lambda t: any([
q in a.name.lower()
for a in getattr(t.album, 'artists', [])])
composer_filter = lambda t: any([
q in a.name.lower()
for a in getattr(t, 'composers', [])])
performer_filter = lambda t: any([
q in a.name.lower()
for a in getattr(t, 'performers', [])])
track_no_filter = lambda t: q == t.track_no
genre_filter = lambda t: t.genre and q in t.genre.lower()
date_filter = lambda t: t.date and t.date.startswith(q)
comment_filter = lambda t: t.comment and q in t.comment.lower()
any_filter = lambda t: (
uri_filter(t) or
track_name_filter(t) or
album_filter(t) or
artist_filter(t) or
albumartist_filter(t) or
date_filter(t))
composer_filter(t) or
performer_filter(t) or
track_no_filter(t) or
genre_filter(t) or
date_filter(t) or
comment_filter(t))
if field == 'uri':
result_tracks = filter(uri_filter, result_tracks)
@ -157,10 +191,18 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(artist_filter, result_tracks)
elif field == 'albumartist':
result_tracks = filter(albumartist_filter, result_tracks)
elif field == 'composer':
result_tracks = filter(composer_filter, result_tracks)
elif field == 'performer':
result_tracks = filter(performer_filter, result_tracks)
elif field == 'track_no':
result_tracks = filter(track_no_filter, result_tracks)
elif field == 'genre':
result_tracks = filter(genre_filter, result_tracks)
elif field == 'date':
result_tracks = filter(date_filter, result_tracks)
elif field == 'comment':
result_tracks = filter(comment_filter, result_tracks)
elif field == 'any':
result_tracks = filter(any_filter, result_tracks)
else:

View File

@ -131,15 +131,27 @@ def _convert_mpd_data(data, tracks):
if 'albumartist' in data:
albumartist_kwargs['name'] = data['albumartist']
if 'composer' in data:
track_kwargs['composers'] = [Artist(name=data['composer'])]
if 'performer' in data:
track_kwargs['performers'] = [Artist(name=data['performer'])]
if 'album' in data:
album_kwargs['name'] = data['album']
if 'title' in data:
track_kwargs['name'] = data['title']
if 'genre' in data:
track_kwargs['genre'] = data['genre']
if 'date' in data:
track_kwargs['date'] = data['date']
if 'comment' in data:
track_kwargs['comment'] = data['comment']
if 'musicbrainz_trackid' in data:
track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid']

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
import collections
import logging
import random
@ -292,36 +293,51 @@ class TracklistController(object):
"""
Filter the tracklist by the given criterias.
A criteria consists of a model field to check and a list of values to
compare it against. If the model field matches one of the values, it
may be returned.
Only tracks that matches all the given criterias are returned.
Examples::
# Returns track with TLID 7 (tracklist ID)
filter({'tlid': 7})
filter(tlid=7)
# Returns tracks with TLIDs 1, 2, 3, or 4 (tracklist ID)
filter({'tlid': [1, 2, 3, 4]})
filter(tlid=[1, 2, 3, 4])
# Returns track with ID 1
filter({'id': 1})
filter(id=1)
# Returns track with IDs 1, 5, or 7
filter({'id': [1, 5, 7]})
filter(id=[1, 5, 7])
# Returns track with URI 'xyz'
filter({'uri': 'xyz'})
filter(uri='xyz')
# Returns track with URIs 'xyz' or 'abc'
filter({'uri': ['xyz', 'abc']})
filter(uri=['xyz', 'abc'])
# Returns track with ID 1 and URI 'xyz'
filter({'id': 1, 'uri': 'xyz'})
filter(id=1, uri='xyz')
# Returns tracks with ID 1 and URI 'xyz'
filter({'id': [1], 'uri': ['xyz']})
filter(id=[1], uri=['xyz'])
# Returns track with a matching ID (1, 3 or 6) and a matching URI
# ('xyz' or 'abc')
filter({'id': [1, 3, 6], 'uri': ['xyz', 'abc']})
filter(id=[1, 3, 6], uri=['xyz', 'abc'])
:param criteria: on or more criteria to match by
:type criteria: dict
:type criteria: dict, of (string, list) pairs
:rtype: list of :class:`mopidy.models.TlTrack`
"""
criteria = criteria or kwargs
matches = self._tl_tracks
for (key, value) in criteria.iteritems():
for (key, values) in criteria.iteritems():
if (not isinstance(values, collections.Iterable)
or isinstance(values, basestring)):
# Fail hard if anyone is using the <0.17 calling style
raise ValueError('Filter values must be iterable: %r' % values)
if key == 'tlid':
matches = filter(lambda ct: ct.tlid == value, matches)
matches = filter(lambda ct: ct.tlid in values, matches)
else:
matches = filter(
lambda ct: getattr(ct.track, key) == value, matches)
lambda ct: getattr(ct.track, key) in values, matches)
return matches
def move(self, start, end, to_position):
@ -435,7 +451,7 @@ class TracklistController(object):
"""Private method used by :class:`mopidy.core.PlaybackController`."""
if not self.consume:
return False
self.remove(tlid=tl_track.tlid)
self.remove(tlid=[tl_track.tlid])
return True
def _trigger_tracklist_changed(self):

View File

@ -21,6 +21,7 @@ class Extension(ext.Extension):
schema['hostname'] = config.Hostname()
schema['port'] = config.Port()
schema['static_dir'] = config.Path(optional=True)
schema['zeroconf'] = config.String(optional=True)
return schema
def validate_environment(self):

View File

@ -11,6 +11,7 @@ from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
from mopidy import models
from mopidy.core import CoreListener
from mopidy.utils import zeroconf
from . import ws
@ -22,6 +23,12 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
super(HttpFrontend, self).__init__()
self.config = config
self.core = core
self.hostname = config['http']['hostname']
self.port = config['http']['port']
self.zeroconf_name = config['http']['zeroconf']
self.zeroconf_service = None
self._setup_server()
self._setup_websocket_plugin()
app = self._create_app()
@ -30,8 +37,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
def _setup_server(self):
cherrypy.config.update({
'engine.autoreload_on': False,
'server.socket_host': self.config['http']['hostname'],
'server.socket_port': self.config['http']['port'],
'server.socket_host': self.hostname,
'server.socket_port': self.port,
})
def _setup_websocket_plugin(self):
@ -88,7 +95,21 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
cherrypy.engine.start()
logger.info('HTTP server running at %s', cherrypy.server.base())
if self.zeroconf_name:
self.zeroconf_service = zeroconf.Zeroconf(
stype='_http._tcp', name=self.zeroconf_name,
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.info('Registered HTTP with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.warning('Registering HTTP with Zeroconf failed.')
def on_stop(self):
if self.zeroconf_service:
self.zeroconf_service.unpublish()
logger.debug('Stopping HTTP server')
cherrypy.engine.exit()
logger.info('Stopped HTTP server')

View File

@ -3,6 +3,7 @@ enabled = true
hostname = 127.0.0.1
port = 6680
static_dir =
zeroconf = Mopidy HTTP server on $hostname
[loglevels]
cherrypy = warning

View File

@ -23,6 +23,7 @@ class Extension(ext.Extension):
schema['password'] = config.Secret(optional=True)
schema['max_connections'] = config.Integer(minimum=1)
schema['connection_timeout'] = config.Integer(minimum=1)
schema['zeroconf'] = config.String(optional=True)
return schema
def validate_environment(self):

View File

@ -7,7 +7,7 @@ import pykka
from mopidy.core import CoreListener
from mopidy.frontends.mpd import session
from mopidy.utils import encoding, network, process
from mopidy.utils import encoding, network, process, zeroconf
logger = logging.getLogger('mopidy.frontends.mpd')
@ -15,12 +15,16 @@ logger = logging.getLogger('mopidy.frontends.mpd')
class MpdFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(MpdFrontend, self).__init__()
hostname = network.format_hostname(config['mpd']['hostname'])
port = config['mpd']['port']
self.hostname = hostname
self.port = config['mpd']['port']
self.zeroconf_name = config['mpd']['zeroconf']
self.zeroconf_service = None
try:
network.Server(
hostname, port,
self.hostname, self.port,
protocol=session.MpdSession,
protocol_kwargs={
'config': config,
@ -34,9 +38,24 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
encoding.locale_decode(error))
sys.exit(1)
logger.info('MPD server running at [%s]:%s', hostname, port)
logger.info('MPD server running at [%s]:%s', self.hostname, self.port)
def on_start(self):
if self.zeroconf_name:
self.zeroconf_service = zeroconf.Zeroconf(
stype='_mpd._tcp', name=self.zeroconf_name,
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.info('Registered MPD with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.warning('Registering MPD with Zeroconf failed.')
def on_stop(self):
if self.zeroconf_service:
self.zeroconf_service.unpublish()
process.stop_actors_by_class(session.MpdSession)
def send_idle(self, subsystem):

View File

@ -5,3 +5,4 @@ port = 6600
password =
max_connections = 20
connection_timeout = 60
zeroconf = Mopidy MPD server on $hostname

View File

@ -76,7 +76,7 @@ def delete_range(context, start, end=None):
if not tl_tracks:
raise MpdArgError('Bad song index', command='delete')
for (tlid, _) in tl_tracks:
context.core.tracklist.remove(tlid=tlid)
context.core.tracklist.remove(tlid=[tlid])
@handle_request(r'^delete "(?P<songpos>\d+)"$')
@ -86,7 +86,7 @@ def delete_songpos(context, songpos):
songpos = int(songpos)
(tlid, _) = context.core.tracklist.slice(
songpos, songpos + 1).get()[0]
context.core.tracklist.remove(tlid=tlid)
context.core.tracklist.remove(tlid=[tlid])
except IndexError:
raise MpdArgError('Bad song index', command='delete')
@ -101,7 +101,7 @@ def deleteid(context, tlid):
Deletes the song ``SONGID`` from the playlist
"""
tlid = int(tlid)
tl_tracks = context.core.tracklist.remove(tlid=tlid).get()
tl_tracks = context.core.tracklist.remove(tlid=[tlid]).get()
if not tl_tracks:
raise MpdNoExistError('No such song', command='deleteid')
@ -157,7 +157,7 @@ def moveid(context, tlid, to):
"""
tlid = int(tlid)
to = int(to)
tl_tracks = context.core.tracklist.filter(tlid=tlid).get()
tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get()
if not tl_tracks:
raise MpdNoExistError('No such song', command='moveid')
position = context.core.tracklist.index(tl_tracks[0]).get()
@ -195,7 +195,7 @@ def playlistfind(context, tag, needle):
- does not add quotes around the tag.
"""
if tag == 'filename':
tl_tracks = context.core.tracklist.filter(uri=needle).get()
tl_tracks = context.core.tracklist.filter(uri=[needle]).get()
if not tl_tracks:
return None
position = context.core.tracklist.index(tl_tracks[0]).get()
@ -215,7 +215,7 @@ def playlistid(context, tlid=None):
"""
if tlid is not None:
tlid = int(tlid)
tl_tracks = context.core.tracklist.filter(tlid=tlid).get()
tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get()
if not tl_tracks:
raise MpdNoExistError('No such song', command='playlistid')
position = context.core.tracklist.index(tl_tracks[0]).get()
@ -380,8 +380,8 @@ def swapid(context, tlid1, tlid2):
"""
tlid1 = int(tlid1)
tlid2 = int(tlid2)
tl_tracks1 = context.core.tracklist.filter(tlid=tlid1).get()
tl_tracks2 = context.core.tracklist.filter(tlid=tlid2).get()
tl_tracks1 = context.core.tracklist.filter(tlid=[tlid1]).get()
tl_tracks2 = context.core.tracklist.filter(tlid=[tlid2]).get()
if not tl_tracks1 or not tl_tracks2:
raise MpdNoExistError('No such song', command='swapid')
position1 = context.core.tracklist.index(tl_tracks1[0]).get()

View File

@ -10,8 +10,9 @@ from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
QUERY_RE = (
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Aa]lbumartist|[Dd]ate|[Ff]ile|'
r'[Ff]ilename|[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$')
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Aa]lbumartist|[Cc]omment|'
r'[Cc]omposer|[Dd]ate|[Ff]ile|[Ff]ilename|[Gg]enre|[Pp]erformer|'
r'[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$')
def _get_field(field, search_results):
@ -100,7 +101,10 @@ def find(context, mpd_query):
return
results = context.core.library.find_exact(**query).get()
result_tracks = []
if 'artist' not in query and 'albumartist' not in query:
if ('artist' not in query and
'albumartist' not in query and
'composer' not in query and
'performer' not in query):
result_tracks += [_artist_as_track(a) for a in _get_artists(results)]
if 'album' not in query:
result_tracks += [_album_as_track(a) for a in _get_albums(results)]
@ -127,8 +131,9 @@ def findadd(context, mpd_query):
@handle_request(
r'^list "?(?P<field>([Aa]rtist|[Aa]lbumartist|[Aa]lbum|[Dd]ate|'
r'[Gg]enre))"?( (?P<mpd_query>.*))?$')
r'^list "?(?P<field>([Aa]rtist|[Aa]lbumartist|[Aa]lbum|[Cc]omposer|'
r'[Dd]ate|[Gg]enre|[Pp]erformer))"?'
r'( (?P<mpd_query>.*))?$')
def list_(context, field, mpd_query=None):
"""
*musicpd.org, music database section:*
@ -222,10 +227,14 @@ def list_(context, field, mpd_query=None):
return _list_albumartist(context, query)
elif field == 'album':
return _list_album(context, query)
elif field == 'composer':
return _list_composer(context, query)
elif field == 'performer':
return _list_performer(context, query)
elif field == 'date':
return _list_date(context, query)
elif field == 'genre':
pass # TODO We don't have genre in our internal data structures yet
return _list_genre(context, query)
def _list_artist(context, query):
@ -258,6 +267,26 @@ def _list_album(context, query):
return albums
def _list_composer(context, query):
composers = set()
results = context.core.library.find_exact(**query).get()
for track in _get_tracks(results):
for composer in track.composers:
if composer.name:
composers.add(('Composer', composer.name))
return composers
def _list_performer(context, query):
performers = set()
results = context.core.library.find_exact(**query).get()
for track in _get_tracks(results):
for performer in track.performers:
if performer.name:
performers.add(('Performer', performer.name))
return performers
def _list_date(context, query):
dates = set()
results = context.core.library.find_exact(**query).get()
@ -267,6 +296,15 @@ def _list_date(context, query):
return dates
def _list_genre(context, query):
genres = set()
results = context.core.library.find_exact(**query).get()
for track in _get_tracks(results):
if track.genre:
genres.add(('Genre', track.genre))
return genres
@handle_request(r'^listall$')
@handle_request(r'^listall "(?P<uri>[^"]+)"$')
def listall(context, uri=None):

View File

@ -151,7 +151,7 @@ def playid(context, tlid):
tlid = int(tlid)
if tlid == -1:
return _play_minus_one(context)
tl_tracks = context.core.tracklist.filter(tlid=tlid).get()
tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get()
if not tl_tracks:
raise MpdNoExistError('No such song', command='playid')
return context.core.playback.play(tl_tracks[0]).get()

View File

@ -37,8 +37,11 @@ def track_to_mpd_format(track, position=None):
('Artist', artists_to_mpd_format(track.artists)),
('Title', track.name or ''),
('Album', track.album and track.album.name or ''),
('Date', track.date or ''),
]
if track.date:
result.append(('Date', track.date))
if track.album is not None and track.album.num_tracks != 0:
result.append(('Track', '%d/%d' % (
track.track_no, track.album.num_tracks)))
@ -63,14 +66,31 @@ def track_to_mpd_format(track, position=None):
artists = filter(lambda a: a.musicbrainz_id is not None, track.artists)
if artists:
result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id))
if track.composers:
result.append(('Composer', artists_to_mpd_format(track.composers)))
if track.performers:
result.append(('Performer', artists_to_mpd_format(track.performers)))
if track.genre:
result.append(('Genre', track.genre))
if track.disc_no:
result.append(('Disc', track.disc_no))
if track.comment:
result.append(('Comment', track.comment))
if track.musicbrainz_id is not None:
result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id))
return result
MPD_KEY_ORDER = '''
key file Time Artist AlbumArtist Title Album Track Date MUSICBRAINZ_ALBUMID
MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime
key file Time Artist Album AlbumArtist Title Track Genre Date Composer
Performer Comment Disc MUSICBRAINZ_ALBUMID MUSICBRAINZ_ALBUMARTISTID
MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime
'''.split()
@ -165,7 +185,8 @@ def query_from_mpd_list_format(field, mpd_query):
key = tokens[0].lower()
value = tokens[1]
tokens = tokens[2:]
if key not in ('artist', 'album', 'albumartist', 'date', 'genre'):
if key not in ('artist', 'album', 'albumartist', 'composer',
'date', 'genre', 'performer'):
raise MpdArgError('not able to parse args', command='list')
if not value:
raise ValueError
@ -188,9 +209,13 @@ MPD_SEARCH_QUERY_RE = re.compile(r"""
[Aa]lbum
| [Aa]rtist
| [Aa]lbumartist
| [Cc]omment
| [Cc]omposer
| [Dd]ate
| [Ff]ile
| [Ff]ilename
| [Gg]enre
| [Pp]erformer
| [Tt]itle
| [Tt]rack
| [Aa]ny
@ -207,9 +232,13 @@ MPD_SEARCH_QUERY_PART_RE = re.compile(r"""
[Aa]lbum
| [Aa]rtist
| [Aa]lbumartist
| [Cc]omment
| [Cc]omposer
| [Dd]ate
| [Ff]ile
| [Ff]ilename
| [Gg]enre
| [Pp]erformer
| [Tt]itle
| [Tt]rack
| [Aa]ny

View File

@ -219,6 +219,12 @@ class Track(ImmutableObject):
:type artists: list of :class:`Artist`
:param album: track album
:type album: :class:`Album`
:param composers: track composers
:type composers: string
:param performers: track performers
:type performers: string
:param genre: track genre
:type genre: string
:param track_no: track number in album
:type track_no: integer
:param disc_no: disc number in album
@ -229,6 +235,8 @@ class Track(ImmutableObject):
:type length: integer
:param bitrate: bitrate in kbit/s
:type bitrate: integer
:param comment: track comment
:type comment: string
:param musicbrainz_id: MusicBrainz ID
:type musicbrainz_id: string
:param last_modified: Represents last modification time
@ -247,6 +255,15 @@ class Track(ImmutableObject):
#: The track :class:`Album`. Read-only.
album = None
#: A set of track composers. Read-only.
composers = frozenset()
#: A set of track performers`. Read-only.
performers = frozenset()
#: The track genre. Read-only.
genre = None
#: The track number in the album. Read-only.
track_no = 0
@ -262,6 +279,9 @@ class Track(ImmutableObject):
#: The track's bitrate in kbit/s. Read-only.
bitrate = None
#: The track comment. Read-only.
comment = None
#: The MusicBrainz ID of the track. Read-only.
musicbrainz_id = None
@ -272,6 +292,8 @@ class Track(ImmutableObject):
def __init__(self, *args, **kwargs):
self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
self.__dict__['composers'] = frozenset(kwargs.pop('composers', []))
self.__dict__['performers'] = frozenset(kwargs.pop('performers', []))
super(Track, self).__init__(*args, **kwargs)

View File

@ -38,9 +38,7 @@ def setup_logging(config, verbosity_level, save_debug_log):
if save_debug_log:
setup_debug_logging_to_file(config)
if hasattr(logging, 'captureWarnings'):
# New in Python 2.7
logging.captureWarnings(True)
logging.captureWarnings(True)
if config['logging']['config_file']:
logging.config.fileConfig(config['logging']['config_file'])

81
mopidy/utils/zeroconf.py Normal file
View File

@ -0,0 +1,81 @@
from __future__ import unicode_literals
import logging
import re
import socket
import string
logger = logging.getLogger('mopidy.utils.zerconf')
try:
import dbus
except ImportError:
dbus = None
_AVAHI_IF_UNSPEC = -1
_AVAHI_PROTO_UNSPEC = -1
_AVAHI_PUBLISHFLAGS_NONE = 0
def _filter_loopback_and_meta_addresses(host):
# TODO: see if we can find a cleaner way of handling this.
if re.search(r'(?<![.\d])(127|0)[.]', host):
return ''
return host
def _convert_text_to_dbus_bytes(text):
return [dbus.Byte(ord(c)) for c in text]
class Zeroconf(object):
"""Publish a network service with Zeroconf using Avahi."""
def __init__(self, name, port, stype=None, domain=None,
host=None, text=None):
self.group = None
self.stype = stype or '_http._tcp'
self.domain = domain or ''
self.port = port
self.text = text or []
self.host = _filter_loopback_and_meta_addresses(host or '')
template = string.Template(name)
self.name = template.safe_substitute(
hostname=self.host or socket.getfqdn(), port=self.port)
def publish(self):
if not dbus:
logger.debug('Zeroconf publish failed: dbus not installed.')
return False
try:
bus = dbus.SystemBus()
except dbus.exceptions.DBusException as e:
logger.debug('Zeroconf publish failed: %s', e)
return False
if not bus.name_has_owner('org.freedesktop.Avahi'):
logger.debug('Zeroconf publish failed: Avahi service not running.')
return False
server = dbus.Interface(bus.get_object('org.freedesktop.Avahi', '/'),
'org.freedesktop.Avahi.Server')
self.group = dbus.Interface(
bus.get_object('org.freedesktop.Avahi', server.EntryGroupNew()),
'org.freedesktop.Avahi.EntryGroup')
text = [_convert_text_to_dbus_bytes(t) for t in self.text]
self.group.AddService(_AVAHI_IF_UNSPEC, _AVAHI_PROTO_UNSPEC,
dbus.UInt32(_AVAHI_PUBLISHFLAGS_NONE),
self.name, self.stype, self.domain, self.host,
dbus.UInt16(self.port), text)
self.group.Commit()
return True
def unpublish(self):
if self.group:
self.group.Reset()
self.group = None

View File

@ -24,6 +24,8 @@ class TranslatorTest(unittest.TestCase):
'album': 'albumname',
'track-number': 1,
'artist': 'name',
'composer': 'composer',
'performer': 'performer',
'album-artist': 'albumartistname',
'title': 'trackname',
'track-count': 2,
@ -31,7 +33,9 @@ class TranslatorTest(unittest.TestCase):
'album-disc-count': 3,
'date': FakeGstDate(2006, 1, 1,),
'container-format': 'ID3 tag',
'genre': 'genre',
'duration': 4531000000,
'comment': 'comment',
'musicbrainz-trackid': 'mbtrackid',
'musicbrainz-albumid': 'mbalbumid',
'musicbrainz-artistid': 'mbartistid',
@ -46,11 +50,38 @@ class TranslatorTest(unittest.TestCase):
'musicbrainz_id': 'mbalbumid',
}
self.artist = {
self.artist_single = {
'name': 'name',
'musicbrainz_id': 'mbartistid',
}
self.artist_multiple = {
'name': ['name1', 'name2'],
'musicbrainz_id': 'mbartistid',
}
self.artist = self.artist_single
self.composer_single = {
'name': 'composer',
}
self.composer_multiple = {
'name': ['composer1', 'composer2'],
}
self.composer = self.composer_single
self.performer_single = {
'name': 'performer',
}
self.performer_multiple = {
'name': ['performer1', 'performer2'],
}
self.performer = self.performer_single
self.albumartist = {
'name': 'albumartistname',
'musicbrainz_id': 'mbalbumartistid',
@ -60,8 +91,10 @@ class TranslatorTest(unittest.TestCase):
'uri': 'uri',
'name': 'trackname',
'date': '2006-01-01',
'genre': 'genre',
'track_no': 1,
'disc_no': 2,
'comment': 'comment',
'length': 4531,
'musicbrainz_id': 'mbtrackid',
'last_modified': 1234,
@ -71,7 +104,30 @@ class TranslatorTest(unittest.TestCase):
if self.albumartist:
self.album['artists'] = [Artist(**self.albumartist)]
self.track['album'] = Album(**self.album)
self.track['artists'] = [Artist(**self.artist)]
if ('name' in self.artist
and not isinstance(self.artist['name'], basestring)):
self.track['artists'] = [Artist(name=artist)
for artist in self.artist['name']]
else:
self.track['artists'] = [Artist(**self.artist)]
if ('name' in self.composer
and not isinstance(self.composer['name'], basestring)):
self.track['composers'] = [Artist(name=artist)
for artist in self.composer['name']]
else:
self.track['composers'] = [Artist(**self.composer)] \
if self.composer else ''
if ('name' in self.performer
and not isinstance(self.performer['name'], basestring)):
self.track['performers'] = [Artist(name=artist)
for artist in self.performer['name']]
else:
self.track['performers'] = [Artist(**self.performer)] \
if self.performer else ''
return Track(**self.track)
def check(self):
@ -117,11 +173,37 @@ class TranslatorTest(unittest.TestCase):
del self.artist['name']
self.check()
def test_missing_composer_name(self):
del self.data['composer']
del self.composer['name']
self.check()
def test_multiple_track_composers(self):
self.data['composer'] = ['composer1', 'composer2']
self.composer = self.composer_multiple
self.check()
def test_multiple_track_performers(self):
self.data['performer'] = ['performer1', 'performer2']
self.performer = self.performer_multiple
self.check()
def test_missing_performer_name(self):
del self.data['performer']
del self.performer['name']
self.check()
def test_missing_artist_musicbrainz_id(self):
del self.data['musicbrainz-artistid']
del self.artist['musicbrainz_id']
self.check()
def test_multiple_track_artists(self):
self.data['artist'] = ['name1', 'name2']
self.data['musicbrainz-artistid'] = 'mbartistid'
self.artist = self.artist_multiple
self.check()
def test_missing_album_artist(self):
del self.data['album-artist']
del self.albumartist['name']
@ -132,6 +214,11 @@ class TranslatorTest(unittest.TestCase):
del self.albumartist['musicbrainz_id']
self.check()
def test_missing_genre(self):
del self.data['genre']
del self.track['genre']
self.check()
def test_missing_date(self):
del self.data['date']
del self.track['date']
@ -142,6 +229,11 @@ class TranslatorTest(unittest.TestCase):
del self.track['date']
self.check()
def test_missing_comment(self):
del self.data['comment']
del self.track['comment']
self.check()
class ScannerTest(unittest.TestCase):
def setUp(self):

View File

@ -20,6 +20,8 @@ class LocalLibraryProviderTest(unittest.TestCase):
Artist(name='artist2'),
Artist(name='artist3'),
Artist(name='artist4'),
Artist(name='artist5'),
Artist(name='artist6'),
]
albums = [
@ -45,7 +47,14 @@ class LocalLibraryProviderTest(unittest.TestCase):
Track(
uri='local:track:path4', name='track4',
artists=[artists[2]], album=albums[3],
date='2004', length=60000, track_no=4),
date='2004', length=60000, track_no=4,
comment='This is a fantastic track'),
Track(
uri='local:track:path5', name='track5', genre='genre1',
album=albums[3], length=4000, composers=[artists[4]]),
Track(
uri='local:track:path6', name='track6', genre='genre2',
album=albums[3], length=4000, performers=[artists[5]]),
]
config = {
@ -116,18 +125,30 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(albumartist=['unknown albumartist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(album=['unknown artist'])
result = self.library.find_exact(composer=['unknown composer'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(performer=['unknown performer'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(album=['unknown album'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(date=['1990'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(genre=['unknown genre'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(track_no=['9'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(track_no=['no_match'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(comment=['fake comment'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(uri=['fake uri'])
self.assertEqual(list(result[0].tracks), [])
@ -143,7 +164,7 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(uri=track_2_uri)
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_track(self):
def test_find_exact_track_name(self):
result = self.library.find_exact(track_name=['track1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -160,6 +181,20 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(artist=['artist3'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
def test_find_exact_composer(self):
result = self.library.find_exact(composer=['artist5'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
result = self.library.find_exact(composer=['artist6'])
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_performer(self):
result = self.library.find_exact(performer=['artist6'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
result = self.library.find_exact(performer=['artist5'])
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_album(self):
result = self.library.find_exact(album=['album1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -187,6 +222,13 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(track_no=['2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_genre(self):
result = self.library.find_exact(genre=['genre1'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
result = self.library.find_exact(genre=['genre2'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
def test_find_exact_date(self):
result = self.library.find_exact(date=['2001'])
self.assertEqual(list(result[0].tracks), [])
@ -197,6 +239,15 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(date=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_comment(self):
result = self.library.find_exact(
comment=['This is a fantastic track'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
result = self.library.find_exact(
comment=['This is a fantastic'])
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_any(self):
# Matches on track artist
result = self.library.find_exact(any=['artist1'])
@ -205,7 +256,7 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(any=['artist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
# Matches on track
# Matches on track name
result = self.library.find_exact(any=['track1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -221,10 +272,30 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(
list(result[0].tracks), [self.tracks[3], self.tracks[2]])
# Matches on track year
# Matches on track composer
result = self.library.find_exact(any=['artist5'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
# Matches on track performer
result = self.library.find_exact(any=['artist6'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
# Matches on track genre
result = self.library.find_exact(any=['genre1'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
result = self.library.find_exact(any=['genre2'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
# Matches on track date
result = self.library.find_exact(any=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
# Matches on track comment
result = self.library.find_exact(
any=['This is a fantastic track'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
# Matches on URI
result = self.library.find_exact(any=['local:track:path1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -243,15 +314,27 @@ class LocalLibraryProviderTest(unittest.TestCase):
test = lambda: self.library.find_exact(track_name=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(composer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(performer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track_no=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(genre=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(date=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(comment=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(any=[''])
self.assertRaises(LookupError, test)
@ -265,7 +348,13 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.search(albumartist=['unknown albumartist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown artist'])
result = self.library.search(composer=['unknown composer'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(performer=['unknown performer'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown album'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(track_no=['9'])
@ -274,9 +363,15 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.search(track_no=['no_match'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(genre=['unknown genre'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(date=['unknown date'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(comment=['unknown comment'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(uri=['unknown uri'])
self.assertEqual(list(result[0].tracks), [])
@ -290,7 +385,7 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.search(uri=['TH2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self):
def test_search_track_name(self):
result = self.library.search(track_name=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -317,6 +412,14 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.search(albumartist=['Tist3'])
self.assertEqual(list(result[0].tracks), [self.tracks[2]])
def test_search_composer(self):
result = self.library.search(composer=['Tist5'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
def test_search_performer(self):
result = self.library.search(performer=['Tist6'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
def test_search_album(self):
result = self.library.search(album=['Bum1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -324,6 +427,13 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.search(album=['Bum2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_genre(self):
result = self.library.search(genre=['Enre1'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
result = self.library.search(genre=['Enre2'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
def test_search_date(self):
result = self.library.search(date=['2001'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -344,11 +454,26 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.search(track_no=['2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_comment(self):
result = self.library.search(comment=['fantastic'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
result = self.library.search(comment=['antasti'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
def test_search_any(self):
# Matches on track artist
result = self.library.search(any=['Tist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
# Matches on track composer
result = self.library.search(any=['Tist5'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
# Matches on track performer
result = self.library.search(any=['Tist6'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
# Matches on track
result = self.library.search(any=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -365,6 +490,20 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(
list(result[0].tracks), [self.tracks[3], self.tracks[2]])
# Matches on track genre
result = self.library.search(any=['Enre1'])
self.assertEqual(list(result[0].tracks), self.tracks[4:5])
result = self.library.search(any=['Enre2'])
self.assertEqual(list(result[0].tracks), self.tracks[5:6])
# Matches on track comment
result = self.library.search(any=['fanta'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
result = self.library.search(any=['is a fan'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
# Matches on URI
result = self.library.search(any=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
@ -380,15 +519,27 @@ class LocalLibraryProviderTest(unittest.TestCase):
test = lambda: self.library.search(albumartist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(composer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(performer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(track_name=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(genre=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(date=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(comment=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(uri=[''])
self.assertRaises(LookupError, test)

View File

@ -71,34 +71,34 @@ class LocalTracklistProviderTest(unittest.TestCase):
def test_filter_by_tlid(self):
tl_track = self.controller.tl_tracks[1]
self.assertEqual(
[tl_track], self.controller.filter(tlid=tl_track.tlid))
[tl_track], self.controller.filter(tlid=[tl_track.tlid]))
@populate_tracklist
def test_filter_by_uri(self):
tl_track = self.controller.tl_tracks[1]
self.assertEqual(
[tl_track], self.controller.filter(uri=tl_track.track.uri))
[tl_track], self.controller.filter(uri=[tl_track.track.uri]))
@populate_tracklist
def test_filter_by_uri_returns_nothing_for_invalid_uri(self):
self.assertEqual([], self.controller.filter(uri='foobar'))
self.assertEqual([], self.controller.filter(uri=['foobar']))
def test_filter_by_uri_returns_single_match(self):
track = Track(uri='a')
self.controller.add([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.filter(uri='a')[0].track)
self.assertEqual(track, self.controller.filter(uri=['a'])[0].track)
def test_filter_by_uri_returns_multiple_matches(self):
track = Track(uri='a')
self.controller.add([Track(uri='z'), track, track])
tl_tracks = self.controller.filter(uri='a')
tl_tracks = self.controller.filter(uri=['a'])
self.assertEqual(track, tl_tracks[0].track)
self.assertEqual(track, tl_tracks[1].track)
def test_filter_by_uri_returns_nothing_if_no_match(self):
self.controller.playlist = Playlist(
tracks=[Track(uri='z'), Track(uri='y')])
self.assertEqual([], self.controller.filter(uri='a'))
tracks=[Track(uri=['z']), Track(uri=['y'])])
self.assertEqual([], self.controller.filter(uri=['a']))
def test_filter_by_multiple_criteria_returns_elements_matching_all(self):
track1 = Track(uri='a', name='x')
@ -106,18 +106,18 @@ class LocalTracklistProviderTest(unittest.TestCase):
track3 = Track(uri='b', name='y')
self.controller.add([track1, track2, track3])
self.assertEqual(
track1, self.controller.filter(uri='a', name='x')[0].track)
track1, self.controller.filter(uri=['a'], name=['x'])[0].track)
self.assertEqual(
track2, self.controller.filter(uri='b', name='x')[0].track)
track2, self.controller.filter(uri=['b'], name=['x'])[0].track)
self.assertEqual(
track3, self.controller.filter(uri='b', name='y')[0].track)
track3, self.controller.filter(uri=['b'], name=['y'])[0].track)
def test_filter_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track()
track2 = Track(uri='b')
track3 = Track()
self.controller.add([track1, track2, track3])
self.assertEqual(track2, self.controller.filter(uri='b')[0].track)
self.assertEqual(track2, self.controller.filter(uri=['b'])[0].track)
@populate_tracklist
def test_clear(self):
@ -227,17 +227,29 @@ class LocalTracklistProviderTest(unittest.TestCase):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=track1.uri)
self.controller.remove(uri=[track1.uri])
self.assertLess(version, self.controller.version)
self.assertNotIn(track1, self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
@populate_tracklist
def test_removing_track_that_does_not_exist_does_nothing(self):
self.controller.remove(uri='/nonexistant')
self.controller.remove(uri=['/nonexistant'])
def test_removing_from_empty_playlist_does_nothing(self):
self.controller.remove(uri='/nonexistant')
self.controller.remove(uri=['/nonexistant'])
@populate_tracklist
def test_remove_lists(self):
track0 = self.controller.tracks[0]
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=[track0.uri, track2.uri])
self.assertLess(version, self.controller.version)
self.assertNotIn(track0, self.controller.tracks)
self.assertNotIn(track2, self.controller.tracks)
self.assertEqual(track1, self.controller.tracks[0])
@populate_tracklist
def test_shuffle(self):

View File

@ -147,7 +147,9 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
album = Album(name='æøå', artists=artists)
track = Track(
uri='local:track:song1.mp3', name='æøå', artists=artists,
album=album, length=4000, last_modified=1272319626)
composers=artists, performers=artists, genre='æøå',
album=album, length=4000, last_modified=1272319626,
comment='æøå&^`ൂ㔶')
self.assertEqual(track, list(tracks)[0])

View File

@ -105,7 +105,7 @@ class BackendEventsTest(unittest.TestCase):
self.core.tracklist.add([Track(uri='dummy:a')]).get()
send.reset_mock()
self.core.tracklist.remove(uri='dummy:a').get()
self.core.tracklist.remove(uri=['dummy:a']).get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')

View File

@ -37,7 +37,7 @@ class TracklistTest(unittest.TestCase):
self.assertEqual(tl_tracks, self.core.tracklist.tl_tracks[-1:])
def test_remove_removes_tl_tracks_matching_query(self):
tl_tracks = self.core.tracklist.remove(name='foo')
tl_tracks = self.core.tracklist.remove(name=['foo'])
self.assertEqual(2, len(tl_tracks))
self.assertListEqual(self.tl_tracks[:2], tl_tracks)
@ -46,7 +46,7 @@ class TracklistTest(unittest.TestCase):
self.assertListEqual(self.tl_tracks[2:], self.core.tracklist.tl_tracks)
def test_remove_works_with_dict_instead_of_kwargs(self):
tl_tracks = self.core.tracklist.remove({'name': 'foo'})
tl_tracks = self.core.tracklist.remove({'name': ['foo']})
self.assertEqual(2, len(tl_tracks))
self.assertListEqual(self.tl_tracks[:2], tl_tracks)
@ -55,15 +55,21 @@ class TracklistTest(unittest.TestCase):
self.assertListEqual(self.tl_tracks[2:], self.core.tracklist.tl_tracks)
def test_filter_returns_tl_tracks_matching_query(self):
tl_tracks = self.core.tracklist.filter(name='foo')
tl_tracks = self.core.tracklist.filter(name=['foo'])
self.assertEqual(2, len(tl_tracks))
self.assertListEqual(self.tl_tracks[:2], tl_tracks)
def test_filter_works_with_dict_instead_of_kwargs(self):
tl_tracks = self.core.tracklist.filter({'name': 'foo'})
tl_tracks = self.core.tracklist.filter({'name': ['foo']})
self.assertEqual(2, len(tl_tracks))
self.assertListEqual(self.tl_tracks[:2], tl_tracks)
def test_filter_fails_if_values_isnt_iterable(self):
self.assertRaises(ValueError, self.core.tracklist.filter, tlid=3)
def test_filter_fails_if_values_is_a_string(self):
self.assertRaises(ValueError, self.core.tracklist.filter, uri='a')
# TODO Extract tracklist tests from the base backend tests

View File

@ -37,5 +37,20 @@ Title: track4
Album: album4
Date: 2004
Track: 4
Comment: This is a fantastic track
Time: 60
key: key5
file: /path5
Composer: artist5
Title: track5
Album: album4
Genre: genre1
Time: 4
key: key6
file: /path6
Performer: artist6
Title: track6
Album: album4
Genre: genre2
Time: 4
songList end

View File

@ -8,7 +8,11 @@ file: /song1.mp3
Time: 4
Artist: æøå
AlbumArtist: æøå
Composer: æøå
Performer: æøå
Title: æøå
Album: æøå
Genre: æøå
Comment: æøå&^`ൂ㔶
mtime: 1272319626
songList end

View File

@ -28,6 +28,7 @@ class HttpEventsTest(unittest.TestCase):
'hostname': '127.0.0.1',
'port': 6680,
'static_dir': None,
'zeroconf': '',
}
}
self.http = actor.HttpFrontend(config=config, core=mock.Mock())

View File

@ -261,6 +261,22 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
self.sendRequest('find albumartist "what"')
self.assertInResponse('OK')
def test_find_composer(self):
self.sendRequest('find "composer" "what"')
self.assertInResponse('OK')
def test_find_composer_without_quotes(self):
self.sendRequest('find composer "what"')
self.assertInResponse('OK')
def test_find_performer(self):
self.sendRequest('find "performer" "what"')
self.assertInResponse('OK')
def test_find_performer_without_quotes(self):
self.sendRequest('find performer "what"')
self.assertInResponse('OK')
def test_find_filename(self):
self.sendRequest('find "filename" "afilename"')
self.assertInResponse('OK')
@ -297,6 +313,14 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
self.sendRequest('find "track" ""')
self.assertInResponse('OK')
def test_find_genre(self):
self.sendRequest('find "genre" "what"')
self.assertInResponse('OK')
def test_find_genre_without_quotes(self):
self.sendRequest('find genre "what"')
self.assertInResponse('OK')
def test_find_date(self):
self.sendRequest('find "date" "2002-01-01"')
self.assertInResponse('OK')
@ -456,6 +480,135 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.sendRequest('list "albumartist"')
self.assertNotInResponse('Artist: ')
self.assertNotInResponse('Albumartist: ')
self.assertNotInResponse('Composer: ')
self.assertNotInResponse('Performer: ')
self.assertInResponse('OK')
### Composer
def test_list_composer_with_quotes(self):
self.sendRequest('list "composer"')
self.assertInResponse('OK')
def test_list_composer_without_quotes(self):
self.sendRequest('list composer')
self.assertInResponse('OK')
def test_list_composer_without_quotes_and_capitalized(self):
self.sendRequest('list Composer')
self.assertInResponse('OK')
def test_list_composer_with_query_of_one_token(self):
self.sendRequest('list "composer" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_composer_with_unknown_field_in_query_returns_ack(self):
self.sendRequest('list "composer" "foo" "bar"')
self.assertEqualResponse('ACK [2@0] {list} not able to parse args')
def test_list_composer_by_artist(self):
self.sendRequest('list "composer" "artist" "anartist"')
self.assertInResponse('OK')
def test_list_composer_by_album(self):
self.sendRequest('list "composer" "album" "analbum"')
self.assertInResponse('OK')
def test_list_composer_by_full_date(self):
self.sendRequest('list "composer" "date" "2001-01-01"')
self.assertInResponse('OK')
def test_list_composer_by_year(self):
self.sendRequest('list "composer" "date" "2001"')
self.assertInResponse('OK')
def test_list_composer_by_genre(self):
self.sendRequest('list "composer" "genre" "agenre"')
self.assertInResponse('OK')
def test_list_composer_by_artist_and_album(self):
self.sendRequest(
'list "composer" "artist" "anartist" "album" "analbum"')
self.assertInResponse('OK')
def test_list_composer_without_filter_value(self):
self.sendRequest('list "composer" "artist" ""')
self.assertInResponse('OK')
def test_list_composer_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(composers=[Artist(name='')])])
self.sendRequest('list "composer"')
self.assertNotInResponse('Artist: ')
self.assertNotInResponse('Albumartist: ')
self.assertNotInResponse('Composer: ')
self.assertNotInResponse('Performer: ')
self.assertInResponse('OK')
### Performer
def test_list_performer_with_quotes(self):
self.sendRequest('list "performer"')
self.assertInResponse('OK')
def test_list_performer_without_quotes(self):
self.sendRequest('list performer')
self.assertInResponse('OK')
def test_list_performer_without_quotes_and_capitalized(self):
self.sendRequest('list Albumartist')
self.assertInResponse('OK')
def test_list_performer_with_query_of_one_token(self):
self.sendRequest('list "performer" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_performer_with_unknown_field_in_query_returns_ack(self):
self.sendRequest('list "performer" "foo" "bar"')
self.assertEqualResponse('ACK [2@0] {list} not able to parse args')
def test_list_performer_by_artist(self):
self.sendRequest('list "performer" "artist" "anartist"')
self.assertInResponse('OK')
def test_list_performer_by_album(self):
self.sendRequest('list "performer" "album" "analbum"')
self.assertInResponse('OK')
def test_list_performer_by_full_date(self):
self.sendRequest('list "performer" "date" "2001-01-01"')
self.assertInResponse('OK')
def test_list_performer_by_year(self):
self.sendRequest('list "performer" "date" "2001"')
self.assertInResponse('OK')
def test_list_performer_by_genre(self):
self.sendRequest('list "performer" "genre" "agenre"')
self.assertInResponse('OK')
def test_list_performer_by_artist_and_album(self):
self.sendRequest(
'list "performer" "artist" "anartist" "album" "analbum"')
self.assertInResponse('OK')
def test_list_performer_without_filter_value(self):
self.sendRequest('list "performer" "artist" ""')
self.assertInResponse('OK')
def test_list_performer_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(performers=[Artist(name='')])])
self.sendRequest('list "performer"')
self.assertNotInResponse('Artist: ')
self.assertNotInResponse('Albumartist: ')
self.assertNotInResponse('Composer: ')
self.assertNotInResponse('Performer: ')
self.assertInResponse('OK')
### Album
@ -492,6 +645,14 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.sendRequest('list "album" "albumartist" "anartist"')
self.assertInResponse('OK')
def test_list_album_by_composer(self):
self.sendRequest('list "album" "composer" "anartist"')
self.assertInResponse('OK')
def test_list_album_by_performer(self):
self.sendRequest('list "album" "performer" "anartist"')
self.assertInResponse('OK')
def test_list_album_by_full_date(self):
self.sendRequest('list "album" "date" "2001-01-01"')
self.assertInResponse('OK')
@ -679,6 +840,30 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase):
self.sendRequest('search "albumartist" ""')
self.assertInResponse('OK')
def test_search_composer(self):
self.sendRequest('search "composer" "acomposer"')
self.assertInResponse('OK')
def test_search_composer_without_quotes(self):
self.sendRequest('search composer "acomposer"')
self.assertInResponse('OK')
def test_search_composer_without_filter_value(self):
self.sendRequest('search "composer" ""')
self.assertInResponse('OK')
def test_search_performer(self):
self.sendRequest('search "performer" "aperformer"')
self.assertInResponse('OK')
def test_search_performer_without_quotes(self):
self.sendRequest('search performer "aperformer"')
self.assertInResponse('OK')
def test_search_performer_without_filter_value(self):
self.sendRequest('search "performer" ""')
self.assertInResponse('OK')
def test_search_filename(self):
self.sendRequest('search "filename" "afilename"')
self.assertInResponse('OK')
@ -739,6 +924,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase):
self.sendRequest('search "track" ""')
self.assertInResponse('OK')
def test_search_genre(self):
self.sendRequest('search "genre" "agenre"')
self.assertInResponse('OK')
def test_search_genre_without_quotes(self):
self.sendRequest('search genre "agenre"')
self.assertInResponse('OK')
def test_search_genre_without_filter_value(self):
self.sendRequest('search "genre" ""')
self.assertInResponse('OK')
def test_search_date(self):
self.sendRequest('search "date" "2002-01-01"')
self.assertInResponse('OK')
@ -755,6 +952,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase):
self.sendRequest('search "date" ""')
self.assertInResponse('OK')
def test_search_comment(self):
self.sendRequest('search "comment" "acomment"')
self.assertInResponse('OK')
def test_search_comment_without_quotes(self):
self.sendRequest('search comment "acomment"')
self.assertInResponse('OK')
def test_search_comment_without_filter_value(self):
self.sendRequest('search "comment" ""')
self.assertInResponse('OK')
def test_search_else_should_fail(self):
self.sendRequest('search "sometype" "something"')
self.assertEqualResponse('ACK [2@0] {search} incorrect arguments')

View File

@ -21,7 +21,7 @@ class StatusHandlerTest(protocol.BaseTestCase):
self.assertInResponse('Title: ')
self.assertInResponse('Album: ')
self.assertInResponse('Track: 0')
self.assertInResponse('Date: ')
self.assertNotInResponse('Date: ')
self.assertInResponse('Pos: 0')
self.assertInResponse('Id: 0')
self.assertInResponse('OK')

View File

@ -17,7 +17,12 @@ class TrackMpdFormatTest(unittest.TestCase):
album=Album(name='an album', num_tracks=13,
artists=[Artist(name='an other artist')]),
track_no=7,
composers=[Artist(name='a composer')],
performers=[Artist(name='a performer')],
genre='a genre',
date=datetime.date(1977, 1, 1),
disc_no='1',
comment='a comment',
length=137000,
)
@ -36,8 +41,8 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assertIn(('Title', ''), result)
self.assertIn(('Album', ''), result)
self.assertIn(('Track', 0), result)
self.assertIn(('Date', ''), result)
self.assertEqual(len(result), 7)
self.assertNotIn(('Date', ''), result)
self.assertEqual(len(result), 6)
def test_track_to_mpd_format_with_position(self):
result = translator.track_to_mpd_format(Track(), position=1)
@ -62,11 +67,16 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assertIn(('Title', 'a name'), result)
self.assertIn(('Album', 'an album'), result)
self.assertIn(('AlbumArtist', 'an other artist'), result)
self.assertIn(('Composer', 'a composer'), result)
self.assertIn(('Performer', 'a performer'), result)
self.assertIn(('Genre', 'a genre'), result)
self.assertIn(('Track', '7/13'), result)
self.assertIn(('Date', datetime.date(1977, 1, 1)), result)
self.assertIn(('Disc', '1'), result)
self.assertIn(('Comment', 'a comment'), result)
self.assertIn(('Pos', 9), result)
self.assertIn(('Id', 122), result)
self.assertEqual(len(result), 10)
self.assertEqual(len(result), 15)
def test_track_to_mpd_format_musicbrainz_trackid(self):
track = self.track.copy(musicbrainz_id='foo')

View File

@ -450,12 +450,14 @@ class TrackTest(unittest.TestCase):
def test_repr_without_artists(self):
self.assertEquals(
"Track(artists=[], name=u'name', uri=u'uri')",
"Track(artists=[], composers=[], name=u'name', "
"performers=[], uri=u'uri')",
repr(Track(uri='uri', name='name')))
def test_repr_with_artists(self):
self.assertEquals(
"Track(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')",
"Track(artists=[Artist(name=u'foo')], composers=[], name=u'name', "
"performers=[], uri=u'uri')",
repr(Track(uri='uri', name='name', artists=[Artist(name='foo')])))
def test_serialize_without_artists(self):
@ -670,7 +672,8 @@ class TlTrackTest(unittest.TestCase):
def test_repr(self):
self.assertEquals(
"TlTrack(tlid=123, track=Track(artists=[], uri=u'uri'))",
"TlTrack(tlid=123, track=Track(artists=[], composers=[], "
"performers=[], uri=u'uri'))",
repr(TlTrack(tlid=123, track=Track(uri='uri'))))
def test_serialize(self):
@ -804,8 +807,8 @@ class PlaylistTest(unittest.TestCase):
def test_repr_with_tracks(self):
self.assertEquals(
"Playlist(name=u'name', tracks=[Track(artists=[], name=u'foo')], "
"uri=u'uri')",
"Playlist(name=u'name', tracks=[Track(artists=[], composers=[], "
"name=u'foo', performers=[])], uri=u'uri')",
repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')])))
def test_serialize_without_tracks(self):