Merge branch 'develop' of https://github.com/mopidy/mopidy into feature/extra_tags

This commit is contained in:
Lasse Bigum 2013-11-13 00:23:44 +01:00
commit 9ba5f6862f
21 changed files with 293 additions and 61 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

@ -13,15 +13,48 @@ v0.17.0 (UNRELEASED)
- 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.
- 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`)
**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`)
**MPD frontend**

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

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

@ -150,7 +150,13 @@ 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)):

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

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

@ -12,9 +12,8 @@ def setup_logging(config, verbosity_level, save_debug_log):
setup_console_logging(config, verbosity_level)
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

@ -50,11 +50,18 @@ 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',
}
@ -97,7 +104,13 @@ 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)):
@ -185,6 +198,12 @@ class TranslatorTest(unittest.TestCase):
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']

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

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

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