Merge branch 'develop' of http://github.com/jodal/mopidy into develop

This commit is contained in:
Thomas Adamcik 2010-10-26 22:21:06 +02:00
commit 0371698706
31 changed files with 770 additions and 236 deletions

View File

@ -11,8 +11,9 @@ platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones.
To install Mopidy, check out
`the installation docs <http://www.mopidy.com/docs/master/installation/>`_.
* `Documentation <http://www.mopidy.com/docs/master/>`_
* `Documentation (latest release) <http://www.mopidy.com/docs/master/>`_
* `Documentation (development version) <http://www.mopidy.com/docs/develop/>`_
* `Source code <http://github.com/jodal/mopidy>`_
* `Issue tracker <http://github.com/jodal/mopidy/issues>`_
* IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
* `Download development snapshot <http://github.com/jodal/mopidy/tarball/develop#egg=mopidy-dev>`_

View File

@ -5,31 +5,82 @@ Changes
This change log is used to track all major changes to Mopidy.
0.2.0 (in development)
0.3.0 (in development)
======================
No description yet.
**Changes**
- None so far.
0.2.0 (2010-10-24)
==================
In Mopidy 0.2.0 we've added a `Last.fm <http://www.last.fm/>`_ scrobbling
support, which means that Mopidy now can submit meta data about the tracks you
play to your Last.fm profile. See :mod:`mopidy.frontends.lastfm` for
details on new dependencies and settings. If you use Mopidy's Last.fm support,
please join the `Mopidy group at Last.fm <http://www.last.fm/group/Mopidy>`_.
With the exception of the work on the Last.fm scrobbler, there has been a
couple of quiet months in the Mopidy camp. About the only thing going on, has
been stabilization work and bug fixing. All bugs reported on GitHub, plus some,
have been fixed in 0.2.0. Thus, we hope this will be a great release!
We've worked a bit on OS X support, but not all issues are completely solved
yet. :issue:`25` is the one that is currently blocking OS X support. Any help
solving it will be greatly appreciated!
Finally, please :ref:`update your pyspotify installation
<pyspotify_installation>` when upgrading to Mopidy 0.2.0. The latest pyspotify
got a fix for the segmentation fault that occurred when playing music and
searching at the same time, thanks to Valentin David.
**Important changes**
- Added a Last.fm scrobbler. See :mod:`mopidy.frontends.lastfm` for details.
**Changes**
- Simplify the default log format, :attr:`mopidy.settings.CONSOLE_LOG_FORMAT`.
From a user's point of view: Less noise, more information.
- Rename the :option:`--dump` command line option to
:option:`--save-debug-log`.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to
:attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose`
too.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
- Switched from using subprocesses to threads. This partly fixes the OS X
support. See :issue:`14` for details.
- Logging and command line options:
- Simplify the default log format,
:attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view:
Less noise, more information.
- Rename the :option:`--dump` command line option to
:option:`--save-debug-log`.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to
:attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose`
too.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
- MPD frontend:
- ``add ""`` and ``addid ""`` now behaves as expected.
- MPD command ``list`` now supports queries by artist, album name, and date,
as used by e.g. the Ario client. (Fixes: :issue:`20`)
- MPD command ``add ""`` and ``addid ""`` now behaves as expected. (Fixes
:issue:`16`)
- MPD command ``playid "-1"`` now correctly resumes playback if paused.
- Random mode:
- Fix wrong behavior on end of track and next after random mode has been
used. (Fixes: :issue:`18`)
- Fix infinite recursion loop crash on playback of non-playable tracks when
in random mode. (Fixes :issue:`17`)
- Fix assertion error that happened if one removed tracks from the current
playlist, while in random mode. (Fixes :issue:`22`)
- Switched from using subprocesses to threads. (Fixes: :issue:`14`)
- :mod:`mopidy.outputs.gstreamer`: Set ``caps`` on the ``appsrc`` bin before
use. This makes sound output work with GStreamer >= 0.10.29, which includes
the versions used in Ubuntu 10.10 and on OS X if using Homebrew. (Fixes:
:issue:`21`, :issue:`24`, contributes to :issue:`14`)
- Improved handling of uncaught exceptions in threads. The entire process
should now exit immediately.
0.1.0 (2010-08-23)

View File

@ -151,20 +151,25 @@ Then, to generate docs::
Creating releases
=================
1. Update changelog and commit it.
#. Update changelog and commit it.
2. Tag release::
#. Merge the release branch (``develop`` in the example) into master::
git tag -a -m "Release v0.1.0a0" v0.1.0a0
git checkout master
git merge --no-ff -m "Release v0.2.0" develop
3. Push to GitHub::
#. Tag the release::
git tag -a -m "Release v0.2.0" v0.2.0
#. Push to GitHub::
git push
git push --tags
4. Build package and upload to PyPI::
#. Build package and upload to PyPI::
rm MANIFEST # Will be regenerated by setup.py
python setup.py sdist upload
5. Spread the word.
#. Spread the word.

View File

@ -3,7 +3,7 @@ if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
def get_version():
return u'0.2.0a1'
return u'0.3.0'
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):

View File

@ -12,13 +12,10 @@ class BaseCurrentPlaylistController(object):
:type backend: :class:`BaseBackend`
"""
#: The current playlist version. Integer which is increased every time the
#: current playlist is changed. Is not reset before Mopidy is restarted.
version = 0
def __init__(self, backend):
self.backend = backend
self._cp_tracks = []
self._version = 0
def destroy(self):
"""Cleanup after component."""
@ -42,6 +39,19 @@ class BaseCurrentPlaylistController(object):
"""
return [ct[1] for ct in self._cp_tracks]
@property
def version(self):
"""
The current playlist version. Integer which is increased every time the
current playlist is changed. Is not reset before Mopidy is restarted.
"""
return self._version
@version.setter
def version(self, version):
self._version = version
self.backend.playback.on_current_playlist_change()
def add(self, track, at_position=None):
"""
Add the track to the end of, or at the given position in the current
@ -71,16 +81,13 @@ class BaseCurrentPlaylistController(object):
:param tracks: tracks to append
:type tracks: list of :class:`mopidy.models.Track`
"""
self.version += 1
for track in tracks:
self.add(track)
self.backend.playback.on_current_playlist_change()
def clear(self):
"""Clear the current playlist."""
self._cp_tracks = []
self.version += 1
self.backend.playback.on_current_playlist_change()
def get(self, **criteria):
"""
@ -146,7 +153,6 @@ class BaseCurrentPlaylistController(object):
to_position += 1
self._cp_tracks = new_cp_tracks
self.version += 1
self.backend.playback.on_current_playlist_change()
def remove(self, **criteria):
"""
@ -191,7 +197,6 @@ class BaseCurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
self.backend.playback.on_current_playlist_change()
def mpd_format(self, *args, **kwargs):
"""Not a part of the generic backend API."""

View File

@ -10,6 +10,9 @@ class BasePlaybackController(object):
:type backend: :class:`BaseBackend`
"""
# pylint: disable = R0902
# Too many instance attributes
#: Constant representing the paused state.
PAUSED = u'paused'
@ -130,6 +133,9 @@ class BasePlaybackController(object):
Not necessarily the same track as :attr:`cp_track_at_next`.
"""
# pylint: disable = R0911
# Too many return statements
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_tracks:
@ -142,17 +148,16 @@ class BasePlaybackController(object):
random.shuffle(self._shuffled)
self._first_shuffle = False
if self._shuffled:
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
return cp_tracks[0]
if self.repeat and self.single:
return cp_tracks[
(self.current_playlist_position) % len(cp_tracks)]
return cp_tracks[self.current_playlist_position]
if self.repeat:
if self.repeat and not self.single:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
@ -195,7 +200,7 @@ class BasePlaybackController(object):
random.shuffle(self._shuffled)
self._first_shuffle = False
if self._shuffled:
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
@ -315,11 +320,8 @@ class BasePlaybackController(object):
if self.cp_track_at_eot:
self._trigger_stopped_playing_event()
self.play(self.cp_track_at_eot)
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
else:
self.stop()
self.current_cp_track = None
self.stop(clear_current_track=True)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track[0])
@ -333,13 +335,10 @@ class BasePlaybackController(object):
self._first_shuffle = True
self._shuffled = []
if not self.backend.current_playlist.cp_tracks:
self.stop()
self.current_cp_track = None
elif (self.current_cp_track not in
if (not self.backend.current_playlist.cp_tracks or
self.current_cp_track not in
self.backend.current_playlist.cp_tracks):
self.current_cp_track = None
self.stop()
self.stop(clear_current_track=True)
def next(self):
"""Play the next track."""
@ -350,11 +349,7 @@ class BasePlaybackController(object):
self._trigger_stopped_playing_event()
self.play(self.cp_track_at_next)
else:
self.stop()
self.current_cp_track = None
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
@ -385,15 +380,21 @@ class BasePlaybackController(object):
if cp_track is not None:
assert cp_track in self.backend.current_playlist.cp_tracks
elif not self.current_cp_track:
if cp_track is None and self.current_cp_track is None:
cp_track = self.cp_track_at_next
if self.state == self.PAUSED and cp_track is None:
if cp_track is None and self.state == self.PAUSED:
self.resume()
elif cp_track is not None:
if cp_track is not None:
self.state = self.STOPPED
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self._play(cp_track[1]):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_track)
if on_error_step == 1:
self.next()
elif on_error_step == -1:
@ -477,13 +478,21 @@ class BasePlaybackController(object):
"""
raise NotImplementedError
def stop(self):
"""Stop playing."""
def stop(self, clear_current_track=False):
"""
Stop playing.
:param clear_current_track: whether to clear the current track _after_
stopping
:type clear_current_track: boolean
"""
if self.state == self.STOPPED:
return
self._trigger_stopped_playing_event()
if self._stop():
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
def _stop(self):
"""
@ -501,11 +510,12 @@ class BasePlaybackController(object):
For internal use only. Should be called by the backend directly after a
track has started playing.
"""
self.backend.core_queue.put({
'to': 'frontend',
'command': 'started_playing',
'track': self.current_track,
})
if self.current_track is not None:
self.backend.core_queue.put({
'to': 'frontend',
'command': 'started_playing',
'track': self.current_track,
})
def _trigger_stopped_playing_event(self):
"""
@ -515,9 +525,10 @@ class BasePlaybackController(object):
is stopped playing, e.g. at the next, previous, and stop actions and at
end-of-track.
"""
self.backend.core_queue.put({
'to': 'frontend',
'command': 'stopped_playing',
'track': self.current_track,
'stop_position': self.time_position,
})
if self.current_track is not None:
self.backend.core_queue.put({
'to': 'frontend',
'command': 'stopped_playing',
'track': self.current_track,
'stop_position': self.time_position,
})

View File

@ -44,16 +44,19 @@ class DummyLibraryController(BaseLibraryController):
class DummyPlaybackController(BasePlaybackController):
def _next(self, track):
return True
"""Pass None as track to force failure"""
return track is not None
def _pause(self):
return True
def _play(self, track):
return True
"""Pass None as track to force failure"""
return track is not None
def _previous(self, track):
return True
"""Pass None as track to force failure"""
return track is not None
def _resume(self):
return True

View File

@ -6,6 +6,7 @@ from spotify import Link, SpotifyError
from mopidy.backends.base import BaseLibraryController
from mopidy.backends.libspotify import ENCODING
from mopidy.backends.libspotify.translator import LibspotifyTranslator
from mopidy.models import Playlist
logger = logging.getLogger('mopidy.backends.libspotify.library')
@ -28,15 +29,27 @@ class LibspotifyLibraryController(BaseLibraryController):
pass # TODO
def search(self, **query):
if not query:
# Since we can't search for the entire Spotify library, we return
# all tracks in the stored playlists when the query is empty.
tracks = []
for playlist in self.backend.stored_playlists.playlists:
tracks += playlist.tracks
return Playlist(tracks=tracks)
spotify_query = []
for (field, values) in query.iteritems():
if field == u'track':
field = u'title'
if field == u'date':
field = u'year'
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
if field == u'track':
field = u'title'
if field == u'any':
spotify_query.append(value)
elif field == u'year':
value = int(value.split('-')[0]) # Extract year
spotify_query.append(u'%s:%d' % (field, value))
else:
spotify_query.append(u'%s:"%s"' % (field, value))
spotify_query = u' '.join(spotify_query)

View File

@ -11,6 +11,9 @@ from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.backends.libspotify.session_manager')
# pylint: disable = R0901
# LibspotifySessionManager: Too many ancestors (9/7)
class LibspotifySessionManager(SpotifySessionManager, BaseThread):
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
@ -19,12 +22,8 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def __init__(self, username, password, core_queue, output):
SpotifySessionManager.__init__(self, username, password)
BaseThread.__init__(self)
BaseThread.__init__(self, core_queue)
self.name = 'LibspotifySMThread'
# Run as a daemon thread, so Mopidy won't wait for this thread to exit
# before Mopidy exits.
self.daemon = True
self.core_queue = core_queue
self.output = output
self.connected = threading.Event()
self.session = None
@ -69,16 +68,21 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
# TODO Base caps_string on arguments
# pylint: disable = R0913
# Too many arguments (8/5)
assert sample_type == 0, u'Expects 16-bit signed integer samples'
capabilites = """
audio/x-raw-int,
endianness=(int)1234,
channels=(int)2,
channels=(int)%(channels)d,
width=(int)16,
depth=(int)16,
signed=True,
rate=(int)44100
"""
signed=(boolean)true,
rate=(int)%(sample_rate)d
""" % {
'sample_rate': sample_rate,
'channels': channels,
}
self.output.deliver_data(capabilites, bytes(frames))
def play_token_lost(self, session):
@ -97,7 +101,7 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def search(self, query, connection):
"""Search method used by Mopidy backend"""
def callback(results, userdata):
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
LibspotifyTranslator.to_mopidy_track(t)

View File

@ -5,7 +5,9 @@ import os
import shutil
from mopidy import settings
from mopidy.backends.base import *
from mopidy.backends.base import (BaseBackend, BaseLibraryController,
BaseStoredPlaylistsController, BaseCurrentPlaylistController,
BasePlaybackController)
from mopidy.models import Playlist, Track, Album
from mopidy.utils.process import pickle_connection

View File

@ -1,20 +1,22 @@
import logging
import multiprocessing
import optparse
import sys
from mopidy import get_version, settings, OptionalDependencyError
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import BaseProcess
from mopidy.utils.process import BaseThread
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
class CoreProcess(BaseProcess):
class CoreProcess(BaseThread):
def __init__(self):
super(CoreProcess, self).__init__(name='CoreProcess')
self.core_queue = multiprocessing.Queue()
super(CoreProcess, self).__init__(self.core_queue)
self.name = 'CoreProcess'
self.options = self.parse_options()
self.output = None
self.backend = None
@ -79,7 +81,9 @@ class CoreProcess(BaseProcess):
return frontends
def process_message(self, message):
if message.get('to') == 'output':
if message.get('to') == 'core':
self.process_message_to_core(message)
elif message.get('to') == 'output':
self.output.process_message(message)
elif message.get('to') == 'frontend':
for frontend in self.frontends:
@ -92,3 +96,12 @@ class CoreProcess(BaseProcess):
self.backend.stored_playlists.playlists = message['playlists']
else:
logger.warning(u'Cannot handle message: %s', message)
def process_message_to_core(self, message):
assert message['to'] == 'core', u'Message recipient must be "core".'
if message['command'] == 'exit':
if message['reason'] is not None:
logger.info(u'Exiting (%s)', message['reason'])
sys.exit(message['status'])
else:
logger.warning(u'Cannot handle message: %s', message)

View File

@ -5,9 +5,9 @@ import time
try:
import pylast
except ImportError as e:
except ImportError as import_error:
from mopidy import OptionalDependencyError
raise OptionalDependencyError(e)
raise OptionalDependencyError(import_error)
from mopidy import get_version, settings, SettingsError
from mopidy.frontends.base import BaseFrontend
@ -45,7 +45,7 @@ class LastfmFrontend(BaseFrontend):
def __init__(self, *args, **kwargs):
super(LastfmFrontend, self).__init__(*args, **kwargs)
(self.connection, other_end) = multiprocessing.Pipe()
self.thread = LastfmFrontendThread(other_end)
self.thread = LastfmFrontendThread(self.core_queue, other_end)
def start(self):
self.thread.start()
@ -58,10 +58,9 @@ class LastfmFrontend(BaseFrontend):
class LastfmFrontendThread(BaseThread):
def __init__(self, connection):
super(LastfmFrontendThread, self).__init__()
def __init__(self, core_queue, connection):
super(LastfmFrontendThread, self).__init__(core_queue)
self.name = u'LastfmFrontendThread'
self.daemon = True
self.connection = connection
self.lastfm = None
self.scrobbler = None
@ -84,7 +83,7 @@ class LastfmFrontendThread(BaseThread):
CLIENT_ID, CLIENT_VERSION)
logger.info(u'Connected to Last.fm')
except SettingsError as e:
logger.info(u'Last.fm scrobbler did not start.')
logger.info(u'Last.fm scrobbler not started')
logger.debug(u'Last.fm settings error: %s', e)
except (pylast.WSError, socket.error) as e:
logger.error(u'Last.fm connection error: %s', e)

View File

@ -5,9 +5,11 @@ from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
# Do not remove the following import. The protocol modules must be imported to
# get them registered as request handlers.
# pylint: disable = W0611
from mopidy.frontends.mpd.protocol import (audio_output, command_list,
connection, current_playlist, empty, music_db, playback, reflection,
status, stickers, stored_playlists)
# pylint: enable = W0611
from mopidy.utils import flatten
class MpdDispatcher(object):

View File

@ -1,7 +1,8 @@
import re
import shlex
from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
def _build_query(mpd_query):
"""
@ -81,13 +82,9 @@ def findadd(frontend, query):
# TODO Add result to current playlist
#result = frontend.find(query)
@handle_pattern(r'^list (?P<field>[Aa]rtist)$')
@handle_pattern(r'^list "(?P<field>[Aa]rtist)"$')
@handle_pattern(r'^list (?P<field>album( artist)?)'
'( "(?P<artist>[^"]+)")*$')
@handle_pattern(r'^list "(?P<field>album(" "artist)?)"'
'( "(?P<artist>[^"]+)")*$')
def list_(frontend, field, artist=None):
@handle_pattern(r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
'( (?P<mpd_query>.*))?$')
def list_(frontend, field, mpd_query=None):
"""
*musicpd.org, music database section:*
@ -101,22 +98,70 @@ def list_(frontend, field, artist=None):
This filters the result list by an artist.
*Clarifications:*
The musicpd.org documentation for ``list`` is far from complete. The
command also supports the following variant:
``list {TYPE} {QUERY}``
Where ``QUERY`` applies to all ``TYPE``. ``QUERY`` is one or more pairs
of a field name and a value. If the ``QUERY`` consists of more than one
pair, the pairs are AND-ed together to find the result. Examples of
valid queries and what they should return:
``list "artist" "artist" "ABBA"``
List artists where the artist name is "ABBA". Response::
Artist: ABBA
OK
``list "album" "artist" "ABBA"``
Lists albums where the artist name is "ABBA". Response::
Album: More ABBA Gold: More ABBA Hits
Album: Absolute More Christmas
Album: Gold: Greatest Hits
OK
``list "artist" "album" "Gold: Greatest Hits"``
Lists artists where the album name is "Gold: Greatest Hits".
Response::
Artist: ABBA
OK
``list "artist" "artist" "ABBA" "artist" "TLC"``
Lists artists where the artist name is "ABBA" *and* "TLC". Should
never match anything. Response::
OK
``list "date" "artist" "ABBA"``
Lists dates where artist name is "ABBA". Response::
Date:
Date: 1992
Date: 1993
OK
``list "date" "artist" "ABBA" "album" "Gold: Greatest Hits"``
Lists dates where artist name is "ABBA" and album name is "Gold:
Greatest Hits". Response::
Date: 1992
OK
``list "genre" "artist" "The Rolling Stones"``
Lists genres where artist name is "The Rolling Stones". Response::
Genre:
Genre: Rock
OK
*GMPC:*
- does not add quotes around the field argument.
- asks for "list artist" to get available artists and will not query
for artist/album information if this is not retrived
- asks for multiple fields, i.e.::
list album artist "an artist name"
returns the albums available for the asked artist::
list album artist "Tiesto"
Album: Radio Trance Vol 4-Promo-CD
Album: Ur A Tear in the Open CDR
Album: Simple Trance 2004 Step One
Album: In Concert 05-10-2003
*ncmpc:*
@ -124,31 +169,70 @@ def list_(frontend, field, artist=None):
- capitalizes the field argument.
"""
field = field.lower()
query = _list_build_query(field, mpd_query)
if field == u'artist':
return _list_artist(frontend)
elif field == u'album artist':
return _list_album_artist(frontend, artist)
# TODO More to implement
return _list_artist(frontend, query)
elif field == u'album':
return _list_album(frontend, query)
elif field == u'date':
return _list_date(frontend, query)
elif field == u'genre':
pass # TODO We don't have genre in our internal data structures yet
def _list_artist(frontend):
"""
Since we don't know exactly all available artists, we respond with
the artists we know for sure, which is all artists in our stored playlists.
"""
def _list_build_query(field, mpd_query):
"""Converts a ``list`` query to a Mopidy query."""
if mpd_query is None:
return {}
# shlex does not seem to be friends with unicode objects
tokens = shlex.split(mpd_query.encode('utf-8'))
tokens = [t.decode('utf-8') for t in tokens]
if len(tokens) == 1:
if field == u'album':
return {'artist': [tokens[0]]}
else:
raise MpdArgError(
u'should be "Album" for 3 arguments', command=u'list')
elif len(tokens) % 2 == 0:
query = {}
while tokens:
key = tokens[0].lower()
key = str(key) # Needed for kwargs keys on OS X and Windows
value = tokens[1]
tokens = tokens[2:]
if key not in (u'artist', u'album', u'date', u'genre'):
raise MpdArgError(u'not able to parse args', command=u'list')
if key in query:
query[key].append(value)
else:
query[key] = [value]
return query
else:
raise MpdArgError(u'not able to parse args', command=u'list')
def _list_artist(frontend, query):
artists = set()
for playlist in frontend.backend.stored_playlists.playlists:
for track in playlist.tracks:
for artist in track.artists:
artists.add((u'Artist', artist.name))
playlist = frontend.backend.library.find_exact(**query)
for track in playlist.tracks:
for artist in track.artists:
artists.add((u'Artist', artist.name))
return artists
def _list_album_artist(frontend, artist):
playlist = frontend.backend.library.find_exact(artist=[artist])
def _list_album(frontend, query):
albums = set()
playlist = frontend.backend.library.find_exact(**query)
for track in playlist.tracks:
albums.add((u'Album', track.album.name))
if track.album is not None:
albums.add((u'Album', track.album.name))
return albums
def _list_date(frontend, query):
dates = set()
playlist = frontend.backend.library.find_exact(**query)
for track in playlist.tracks:
if track.date is not None:
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
return dates
@handle_pattern(r'^listall "(?P<uri>[^"]+)"')
def listall(frontend, uri):
"""

View File

@ -138,6 +138,10 @@ def playid(frontend, cpid):
at the first track.
"""
cpid = int(cpid)
paused = (frontend.backend.playback.state ==
frontend.backend.playback.PAUSED)
if cpid == -1 and paused:
return frontend.backend.playback.resume()
try:
if cpid == -1:
cp_track = _get_cp_track_for_play_minus_one(frontend)

View File

@ -24,6 +24,9 @@ class MpdServer(asyncore.dispatcher):
try:
if socket.has_ipv6:
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
# Explicitly configure socket to work for both IPv4 and IPv6
self.socket.setsockopt(
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
else:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()

View File

@ -8,10 +8,8 @@ logger = logging.getLogger('mopidy.frontends.mpd.thread')
class MpdThread(BaseThread):
def __init__(self, core_queue):
super(MpdThread, self).__init__()
super(MpdThread, self).__init__(core_queue)
self.name = u'MpdThread'
self.daemon = True
self.core_queue = core_queue
def run_inside_try(self):
logger.debug(u'Starting MPD server thread')

View File

@ -4,7 +4,7 @@ from multiprocessing import Pipe
from mopidy import settings
from mopidy.mixers import BaseMixer
from mopidy.utils.process import BaseProcess
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.mixers.nad')
@ -40,7 +40,7 @@ class NadMixer(BaseMixer):
super(NadMixer, self).__init__(*args, **kwargs)
self._volume = None
self._pipe, other_end = Pipe()
NadTalker(pipe=other_end).start()
NadTalker(self.backend.core_queue, pipe=other_end).start()
def _get_volume(self):
return self._volume
@ -50,7 +50,7 @@ class NadMixer(BaseMixer):
self._pipe.send({'command': 'set_volume', 'volume': volume})
class NadTalker(BaseProcess):
class NadTalker(BaseThread):
"""
Independent process which does the communication with the NAD device.
@ -72,8 +72,9 @@ class NadTalker(BaseProcess):
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
_nad_volume = None
def __init__(self, pipe=None):
super(NadTalker, self).__init__(name='NadTalker')
def __init__(self, core_queue, pipe=None):
super(NadTalker, self).__init__(core_queue)
self.name = u'NadTalker'
self.pipe = pipe
self._device = None

View File

@ -5,6 +5,9 @@ class DummyOutput(BaseOutput):
Audio output used for testing.
"""
# pylint: disable = R0902
# Too many instance attributes (9/7)
#: For testing. :class:`True` if :meth:`start` has been called.
start_called = False

View File

@ -29,7 +29,7 @@ class GStreamerOutput(BaseOutput):
def __init__(self, *args, **kwargs):
super(GStreamerOutput, self).__init__(*args, **kwargs)
# Start a helper thread that can run the gobject.MainLoop
self.messages_thread = GStreamerMessagesThread()
self.messages_thread = GStreamerMessagesThread(self.core_queue)
# Start a helper thread that can process the output_queue
self.output_queue = multiprocessing.Queue()
@ -78,7 +78,8 @@ class GStreamerOutput(BaseOutput):
return self._send_recv({'command': 'get_position'})
def set_position(self, position):
return self._send_recv({'command': 'set_position', 'position': position})
return self._send_recv({'command': 'set_position',
'position': position})
def set_state(self, state):
return self._send_recv({'command': 'set_state', 'state': state})
@ -91,10 +92,9 @@ class GStreamerOutput(BaseOutput):
class GStreamerMessagesThread(BaseThread):
def __init__(self):
super(GStreamerMessagesThread, self).__init__()
def __init__(self, core_queue):
super(GStreamerMessagesThread, self).__init__(core_queue)
self.name = u'GStreamerMessagesThread'
self.daemon = True
def run_inside_try(self):
gobject.MainLoop().run()
@ -113,10 +113,8 @@ class GStreamerPlayerThread(BaseThread):
"""
def __init__(self, core_queue, output_queue):
super(GStreamerPlayerThread, self).__init__()
super(GStreamerPlayerThread, self).__init__(core_queue)
self.name = u'GStreamerPlayerThread'
self.daemon = True
self.core_queue = core_queue
self.output_queue = output_queue
self.gst_pipeline = None
@ -142,7 +140,16 @@ class GStreamerPlayerThread(BaseThread):
uri_bin.connect('pad-added', self.process_new_pad, pad)
self.gst_pipeline.add(uri_bin)
else:
app_src = gst.element_factory_make('appsrc', 'src')
app_src = gst.element_factory_make('appsrc', 'appsrc')
app_src_caps = gst.Caps("""
audio/x-raw-int,
endianness=(int)1234,
channels=(int)2,
width=(int)16,
depth=(int)16,
signed=(boolean)true,
rate=(int)44100""")
app_src.set_property('caps', app_src_caps)
self.gst_pipeline.add(app_src)
app_src.get_pad('src').link(pad)
@ -208,12 +215,12 @@ class GStreamerPlayerThread(BaseThread):
def deliver_data(self, caps_string, data):
"""Deliver audio data to be played"""
data_src = self.gst_pipeline.get_by_name('src')
app_src = self.gst_pipeline.get_by_name('appsrc')
caps = gst.caps_from_string(caps_string)
buffer_ = gst.Buffer(buffer(data))
buffer_.set_caps(caps)
data_src.set_property('caps', caps)
data_src.emit('push-buffer', buffer_)
app_src.set_property('caps', caps)
app_src.emit('push-buffer', buffer_)
def end_of_data_stream(self):
"""
@ -222,7 +229,7 @@ class GStreamerPlayerThread(BaseThread):
We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks.
"""
self.gst_pipeline.get_by_name('src').emit('end-of-stream')
self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream')
def set_state(self, state_name):
"""

View File

@ -3,7 +3,6 @@ import multiprocessing
import multiprocessing.dummy
from multiprocessing.reduction import reduce_connection
import pickle
import sys
from mopidy import SettingsError
@ -17,24 +16,27 @@ def unpickle_connection(pickled_connection):
(func, args) = pickle.loads(pickled_connection)
return func(*args)
class BaseProcess(multiprocessing.Process):
def __init__(self, core_queue):
super(BaseProcess, self).__init__()
self.core_queue = core_queue
def run(self):
logger.debug(u'%s: Starting process', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'%s: Interrupted by user', self.name)
sys.exit(0)
logger.info(u'Interrupted by user')
self.exit(0, u'Interrupted by user')
except SettingsError as e:
logger.error(e.message)
sys.exit(1)
self.exit(1, u'Settings error')
except ImportError as e:
logger.error(e)
sys.exit(1)
self.exit(2, u'Import error')
except Exception as e:
logger.exception(e)
raise e
self.exit(3, u'Unknown error')
def run_inside_try(self):
raise NotImplementedError
@ -42,27 +44,43 @@ class BaseProcess(multiprocessing.Process):
def destroy(self):
self.terminate()
def exit(self, status=0, reason=None):
self.core_queue.put({'to': 'core', 'command': 'exit',
'status': status, 'reason': reason})
self.destroy()
class BaseThread(multiprocessing.dummy.Process):
def __init__(self, core_queue):
super(BaseThread, self).__init__()
self.core_queue = core_queue
# No thread should block process from exiting
self.daemon = True
def run(self):
logger.debug(u'%s: Starting process', self.name)
logger.debug(u'%s: Starting thread', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'%s: Interrupted by user', self.name)
sys.exit(0)
logger.info(u'Interrupted by user')
self.exit(0, u'Interrupted by user')
except SettingsError as e:
logger.error(e.message)
sys.exit(1)
self.exit(1, u'Settings error')
except ImportError as e:
logger.error(e)
sys.exit(1)
self.exit(2, u'Import error')
except Exception as e:
logger.exception(e)
raise e
self.exit(3, u'Unknown error')
def run_inside_try(self):
raise NotImplementedError
def destroy(self):
pass
def exit(self, status=0, reason=None):
self.core_queue.put({'to': 'core', 'command': 'exit',
'status': status, 'reason': reason})
self.destroy()

View File

@ -23,7 +23,9 @@ class SettingsProxy(object):
if not os.path.isfile(settings_file):
return {}
sys.path.insert(0, dotdir)
# pylint: disable = F0401
import settings as local_settings_module
# pylint: enable = F0401
return self._get_settings_dict_from_module(local_settings_module)
def _get_settings_dict_from_module(self, module):

View File

@ -5,18 +5,19 @@
#
# C0103 - Invalid name "%s" (should match %s)
# C0111 - Missing docstring
# C0112 - Empty docstring
# E0102 - %s already defined line %s
# Does not understand @property getters and setters
# E0202 - An attribute inherited from %s hide this method
# Does not understand @property getters and setters
# E1101 - %s %r has no %r member
# Does not understand @property getters and setters
# R0201 - Method could be a function
# R0801 - Similar lines in %s files
# R0903 - Too few public methods (%s/%s)
# R0904 - Too many public methods (%s/%s)
# W0141 - Used builtin function %r
# R0921 - Abstract class not referenced
# W0141 - Used builtin function '%s'
# W0142 - Used * or ** magic
# W0401 - Wildcard import %s
# W0511 - TODO, FIXME and XXX in the code
# W0613 - Unused argument %r
#
disable-msg = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,W0141,W0142,W0401,W0511,W0613
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613

View File

@ -128,7 +128,7 @@ class BaseCurrentPlaylistControllerTest(object):
def test_append_does_not_reset_version(self):
version = self.controller.version
self.controller.append([])
self.assertEqual(self.controller.version, version + 1)
self.assertEqual(self.controller.version, version)
@populate_playlist
def test_append_preserves_playing_state(self):
@ -249,7 +249,12 @@ class BaseCurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
def test_version(self):
def test_version_does_not_change_when_appending_nothing(self):
version = self.controller.version
self.controller.append([])
self.assertEquals(version, self.controller.version)
def test_version_increases_when_appending_something(self):
version = self.controller.version
self.controller.append([Track()])
self.assert_(version < self.controller.version)

View File

@ -524,7 +524,7 @@ class BasePlaybackControllerTest(object):
wrapper.called = False
self.playback.on_current_playlist_change = wrapper
self.backend.current_playlist.append([])
self.backend.current_playlist.append([Track()])
self.assert_(wrapper.called)

View File

@ -135,7 +135,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_deleteid(self):
self.b.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 2)
result = self.h.handle_request(u'deleteid "2"')
result = self.h.handle_request(u'deleteid "1"')
self.assertEqual(len(self.b.current_playlist.tracks), 1)
self.assert_(u'OK' in result)
@ -193,7 +193,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'moveid "5" "2"')
result = self.h.handle_request(u'moveid "4" "2"')
self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
self.assertEqual(self.b.current_playlist.tracks[1].name, 'b')
self.assertEqual(self.b.current_playlist.tracks[2].name, 'e')
@ -229,7 +229,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
result = self.h.handle_request(
u'playlistfind filename "file:///exists"')
self.assert_(u'file: file:///exists' in result)
self.assert_(u'Id: 1' in result)
self.assert_(u'Id: 0' in result)
self.assert_(u'Pos: 0' in result)
self.assert_(u'OK' in result)
@ -242,11 +242,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_playlistid_with_songid(self):
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid "2"')
result = self.h.handle_request(u'playlistid "1"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Id: 1' not in result)
self.assert_(u'Id: 0' not in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Id: 2' in result)
self.assert_(u'Id: 1' in result)
self.assert_(u'OK' in result)
def test_playlistid_with_not_existing_songid_fails(self):
@ -429,7 +429,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'swapid "2" "5"')
result = self.h.handle_request(u'swapid "1" "4"')
self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
self.assertEqual(self.b.current_playlist.tracks[1].name, 'e')
self.assertEqual(self.b.current_playlist.tracks[2].name, 'c')

View File

@ -15,6 +15,59 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
self.assert_(u'playtime: 0' in result)
self.assert_(u'OK' in result)
def test_findadd(self):
result = self.h.handle_request(u'findadd "album" "what"')
self.assert_(u'OK' in result)
def test_listall(self):
result = self.h.handle_request(u'listall "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_listallinfo(self):
result = self.h.handle_request(u'listallinfo "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo')
listplaylists_result = self.h.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo ""')
listplaylists_result = self.h.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo "/"')
listplaylists_result = self.h.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_update_without_uri(self):
result = self.h.handle_request(u'update')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_update_with_uri(self):
result = self.h.handle_request(u'update "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_without_uri(self):
result = self.h.handle_request(u'rescan')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_with_uri(self):
result = self.h.handle_request(u'rescan "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
class MusicDatabaseFindTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_find_album(self):
result = self.h.handle_request(u'find "album" "what"')
self.assert_(u'OK' in result)
@ -48,11 +101,20 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
u'find album "album_what" artist "artist_what"')
self.assert_(u'OK' in result)
def test_findadd(self):
result = self.h.handle_request(u'findadd "album" "what"')
self.assert_(u'OK' in result)
def test_list_artist(self):
class MusicDatabaseListTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_list_foo_returns_ack(self):
result = self.h.handle_request(u'list "foo"')
self.assertEqual(result[0],
u'ACK [2@0] {list} incorrect arguments')
### Artist
def test_list_artist_with_quotes(self):
result = self.h.handle_request(u'list "artist"')
self.assert_(u'OK' in result)
@ -64,44 +126,177 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'list Artist')
self.assert_(u'OK' in result)
def test_list_artist_with_artist_should_fail(self):
def test_list_artist_with_query_of_one_token(self):
result = self.h.handle_request(u'list "artist" "anartist"')
self.assertEqual(result[0], u'ACK [2@0] {list} incorrect arguments')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_album_without_artist(self):
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
result = self.h.handle_request(u'list "artist" "foo" "bar"')
self.assertEqual(result[0],
u'ACK [2@0] {list} not able to parse args')
def test_list_artist_by_artist(self):
result = self.h.handle_request(u'list "artist" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_artist_by_album(self):
result = self.h.handle_request(u'list "artist" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_artist_by_full_date(self):
result = self.h.handle_request(u'list "artist" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_artist_by_year(self):
result = self.h.handle_request(u'list "artist" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_artist_by_genre(self):
result = self.h.handle_request(u'list "artist" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_artist_by_artist_and_album(self):
result = self.h.handle_request(
u'list "artist" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Album
def test_list_album_with_quotes(self):
result = self.h.handle_request(u'list "album"')
self.assert_(u'OK' in result)
def test_list_album_with_artist(self):
def test_list_album_without_quotes(self):
result = self.h.handle_request(u'list album')
self.assert_(u'OK' in result)
def test_list_album_without_quotes_and_capitalized(self):
result = self.h.handle_request(u'list Album')
self.assert_(u'OK' in result)
def test_list_album_with_artist_name(self):
result = self.h.handle_request(u'list "album" "anartist"')
self.assert_(u'OK' in result)
def test_list_album_artist_with_artist_without_quotes(self):
result = self.h.handle_request(u'list album artist "anartist"')
def test_list_album_by_artist(self):
result = self.h.handle_request(u'list "album" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_listall(self):
result = self.h.handle_request(u'listall "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_list_album_by_album(self):
result = self.h.handle_request(u'list "album" "album" "analbum"')
self.assert_(u'OK' in result)
def test_listallinfo(self):
result = self.h.handle_request(u'listallinfo "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_list_album_by_full_date(self):
result = self.h.handle_request(u'list "album" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo')
listplaylists_result = self.h.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_list_album_by_year(self):
result = self.h.handle_request(u'list "album" "date" "2001"')
self.assert_(u'OK' in result)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo ""')
listplaylists_result = self.h.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_list_album_by_genre(self):
result = self.h.handle_request(u'list "album" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
lsinfo_result = self.h.handle_request(u'lsinfo "/"')
listplaylists_result = self.h.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_list_album_by_artist_and_album(self):
result = self.h.handle_request(
u'list "album" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Date
def test_list_date_with_quotes(self):
result = self.h.handle_request(u'list "date"')
self.assert_(u'OK' in result)
def test_list_date_without_quotes(self):
result = self.h.handle_request(u'list date')
self.assert_(u'OK' in result)
def test_list_date_without_quotes_and_capitalized(self):
result = self.h.handle_request(u'list Date')
self.assert_(u'OK' in result)
def test_list_date_with_query_of_one_token(self):
result = self.h.handle_request(u'list "date" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_date_by_artist(self):
result = self.h.handle_request(u'list "date" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_date_by_album(self):
result = self.h.handle_request(u'list "date" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_date_by_full_date(self):
result = self.h.handle_request(u'list "date" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_date_by_year(self):
result = self.h.handle_request(u'list "date" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_date_by_genre(self):
result = self.h.handle_request(u'list "date" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_date_by_artist_and_album(self):
result = self.h.handle_request(
u'list "date" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Genre
def test_list_genre_with_quotes(self):
result = self.h.handle_request(u'list "genre"')
self.assert_(u'OK' in result)
def test_list_genre_without_quotes(self):
result = self.h.handle_request(u'list genre')
self.assert_(u'OK' in result)
def test_list_genre_without_quotes_and_capitalized(self):
result = self.h.handle_request(u'list Genre')
self.assert_(u'OK' in result)
def test_list_genre_with_query_of_one_token(self):
result = self.h.handle_request(u'list "genre" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_genre_by_artist(self):
result = self.h.handle_request(u'list "genre" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_genre_by_album(self):
result = self.h.handle_request(u'list "genre" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_genre_by_full_date(self):
result = self.h.handle_request(u'list "genre" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_genre_by_year(self):
result = self.h.handle_request(u'list "genre" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_genre_by_genre(self):
result = self.h.handle_request(u'list "genre" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_genre_by_artist_and_album(self):
result = self.h.handle_request(
u'list "genre" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
class MusicDatabaseSearchTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_search_album(self):
result = self.h.handle_request(u'search "album" "analbum"')
@ -147,22 +342,4 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'search "sometype" "something"')
self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
def test_update_without_uri(self):
result = self.h.handle_request(u'update')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_update_with_uri(self):
result = self.h.handle_request(u'update "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_without_uri(self):
result = self.h.handle_request(u'rescan')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_with_uri(self):
result = self.h.handle_request(u'rescan "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)

View File

@ -254,7 +254,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
def test_playid(self):
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "1"')
result = self.h.handle_request(u'playid "0"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
@ -285,6 +285,18 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
self.assertEqual(self.b.playback.current_track, None)
def test_playid_minus_one_resumes_if_paused(self):
self.b.current_playlist.append([Track(length=40000)])
self.b.playback.seek(30000)
self.assert_(self.b.playback.time_position >= 30000)
self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
self.b.playback.pause()
self.assertEquals(self.b.playback.PAUSED, self.b.playback.state)
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assert_(self.b.playback.time_position >= 30000)
def test_playid_which_does_not_exist(self):
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "12345"')
@ -310,7 +322,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
def test_seekid(self):
self.b.current_playlist.append([Track(length=40000)])
result = self.h.handle_request(u'seekid "1" "30"')
result = self.h.handle_request(u'seekid "0" "30"')
self.assert_(u'OK' in result)
self.assert_(self.b.playback.time_position >= 30000)
@ -318,8 +330,8 @@ class PlaybackControlHandlerTest(unittest.TestCase):
seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append(
[Track(length=40000), seek_track])
result = self.h.handle_request(u'seekid "2" "30"')
self.assertEqual(self.b.playback.current_cpid, 2)
result = self.h.handle_request(u'seekid "1" "30"')
self.assertEqual(self.b.playback.current_cpid, 1)
self.assertEqual(self.b.playback.current_track, seek_track)
def test_stop(self):

View File

@ -0,0 +1,110 @@
import random
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class IssueGH17RegressionTest(unittest.TestCase):
"""
The issue: http://github.com/jodal/mopidy/issues#issue/17
How to reproduce:
- Play a playlist where one track cannot be played
- Turn on random mode
- Press next until you get to the unplayable track
"""
def setUp(self):
self.backend = DummyBackend(mixer_class=DummyMixer)
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), None,
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
def test(self):
random.seed(1) # Playlist order: abcfde
self.mpd.handle_request(u'play')
self.assertEquals('a', self.backend.playback.current_track.uri)
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'next')
self.assertEquals('b', self.backend.playback.current_track.uri)
self.mpd.handle_request(u'next')
# Should now be at track 'c', but playback fails and it skips ahead
self.assertEquals('f', self.backend.playback.current_track.uri)
self.mpd.handle_request(u'next')
self.assertEquals('d', self.backend.playback.current_track.uri)
self.mpd.handle_request(u'next')
self.assertEquals('e', self.backend.playback.current_track.uri)
class IssueGH18RegressionTest(unittest.TestCase):
"""
The issue: http://github.com/jodal/mopidy/issues#issue/18
How to reproduce:
Play, random on, next, random off, next, next.
At this point it gives the same song over and over.
"""
def setUp(self):
self.backend = DummyBackend(mixer_class=DummyMixer)
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
def test(self):
random.seed(1)
self.mpd.handle_request(u'play')
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'next')
self.mpd.handle_request(u'random "0"')
self.mpd.handle_request(u'next')
self.mpd.handle_request(u'next')
cp_track_1 = self.backend.playback.current_cp_track
self.mpd.handle_request(u'next')
cp_track_2 = self.backend.playback.current_cp_track
self.mpd.handle_request(u'next')
cp_track_3 = self.backend.playback.current_cp_track
self.assertNotEqual(cp_track_1, cp_track_2)
self.assertNotEqual(cp_track_2, cp_track_3)
class IssueGH22RegressionTest(unittest.TestCase):
"""
The issue: http://github.com/jodal/mopidy/issues/#issue/22
How to reproduce:
Play, random on, remove all tracks from the current playlist (as in
"delete" each one, not "clear").
Alternatively: Play, random on, remove a random track from the current
playlist, press next until it crashes.
"""
def setUp(self):
self.backend = DummyBackend(mixer_class=DummyMixer)
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
def test(self):
random.seed(1)
self.mpd.handle_request(u'play')
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'deleteid "1"')
self.mpd.handle_request(u'deleteid "2"')
self.mpd.handle_request(u'deleteid "3"')
self.mpd.handle_request(u'deleteid "4"')
self.mpd.handle_request(u'deleteid "5"')
self.mpd.handle_request(u'deleteid "6"')
self.mpd.handle_request(u'status')

View File

@ -27,7 +27,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'Track: 0' in result)
self.assert_(u'Date: ' in result)
self.assert_(u'Pos: 0' in result)
self.assert_(u'Id: 1' in result)
self.assert_(u'Id: 0' in result)
self.assert_(u'OK' in result)
def test_currentsong_without_song(self):
@ -166,7 +166,7 @@ class StatusHandlerTest(unittest.TestCase):
self.b.playback.play()
result = dict(dispatcher.status.status(self.h))
self.assert_('songid' in result)
self.assertEqual(int(result['songid']), 1)
self.assertEqual(int(result['songid']), 0)
def test_status_method_when_playing_contains_time_with_no_length(self):
self.b.current_playlist.append([Track(length=None)])

View File

@ -12,7 +12,7 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.1.0a1') < SV('0.1.0a2'))
self.assert_(SV('0.1.0a2') < SV('0.1.0a3'))
self.assert_(SV('0.1.0a3') < SV('0.1.0'))
self.assert_(SV('0.1.0') < SV(get_version()))
self.assert_(SV(get_version()) < SV('0.2.0'))
self.assert_(SV('0.1.1') < SV('0.2.0'))
self.assert_(SV('0.2.0') < SV('1.0.0'))
self.assert_(SV('0.1.0') < SV('0.2.0'))
self.assert_(SV('0.1.0') < SV('1.0.0'))
self.assert_(SV('0.2.0') < SV(get_version()))
self.assert_(SV(get_version()) < SV('0.3.1'))