Merge branch 'develop' into feature/commands-helper-tmp
Conflicts: mopidy/utils/log.py
This commit is contained in:
commit
37ba3ca01d
1
AUTHORS
1
AUTHORS
@ -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>
|
||||
|
||||
@ -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**
|
||||
|
||||
|
||||
@ -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
|
||||
=====
|
||||
|
||||
@ -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
|
||||
=====
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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']
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -3,6 +3,7 @@ enabled = true
|
||||
hostname = 127.0.0.1
|
||||
port = 6680
|
||||
static_dir =
|
||||
zeroconf = Mopidy HTTP server on $hostname
|
||||
|
||||
[loglevels]
|
||||
cherrypy = warning
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -5,3 +5,4 @@ port = 6600
|
||||
password =
|
||||
max_connections = 20
|
||||
connection_timeout = 60
|
||||
zeroconf = Mopidy MPD server on $hostname
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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
81
mopidy/utils/zeroconf.py
Normal 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
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -8,7 +8,11 @@ file: /song1.mp3
|
||||
Time: 4
|
||||
Artist: æøå
|
||||
AlbumArtist: æøå
|
||||
Composer: æøå
|
||||
Performer: æøå
|
||||
Title: æøå
|
||||
Album: æøå
|
||||
Genre: æøå
|
||||
Comment: æøå&^`ൂ㔶
|
||||
mtime: 1272319626
|
||||
songList end
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user