Merge branch 'develop' into feature/http-frontend

This commit is contained in:
Stein Magnus Jodal 2012-11-18 09:45:44 +01:00
commit 7e3fba0155
43 changed files with 1089 additions and 310 deletions

View File

@ -1,48 +1,5 @@
#!/usr/bin/env python #! /usr/bin/env python
from __future__ import unicode_literals if __name__ == '__main__':
from mopidy.scanner import main
import sys main()
import logging
from mopidy import settings
from mopidy.utils.log import setup_console_logging, setup_root_logger
from mopidy.scanner import Scanner, translator
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
setup_root_logger()
setup_console_logging(2)
tracks = []
def store(data):
track = translator(data)
tracks.append(track)
logging.debug('Added %s', track.uri)
def debug(uri, error, debug):
logging.error('Failed %s: %s - %s', uri, error, debug)
logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH)
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
try:
scanner.start()
except KeyboardInterrupt:
scanner.stop()
logging.info('Done')
for a in tracks_to_tag_cache_format(tracks):
if len(a) == 1:
print ('%s' % a).encode('utf-8')
else:
print ('%s: %s' % a).encode('utf-8')

View File

@ -33,6 +33,13 @@ Library provider
:members: :members:
Backend listener
================
.. autoclass:: mopidy.backends.listener.BackendListener
:members:
.. _backend-implementations: .. _backend-implementations:
Backend implementations Backend implementations

View File

@ -56,23 +56,29 @@ backends:
dummy/mocked lower layers easier than with the old variant, where dummy/mocked lower layers easier than with the old variant, where
dependencies where looked up in Pykka's actor registry. dependencies where looked up in Pykka's actor registry.
- The stored playlists part of the core API has been revised to be more focused - Renamed "current playlist" to "tracklist" everywhere, including the core API
around the playlist URI, and some redundant functionality has been removed: used by frontends.
- :attr:`mopidy.core.StoredPlaylistsController.playlists` no longer supports - Renamed "stored playlists" to "playlists" everywhere, including the core API
used by frontends.
- The playlists part of the core API has been revised to be more focused around
the playlist URI, and some redundant functionality has been removed:
- :attr:`mopidy.core.PlaylistsController.playlists` no longer supports
assignment to it. The `playlists` property on the backend layer still does, assignment to it. The `playlists` property on the backend layer still does,
and all functionality is maintained by assigning to the playlists and all functionality is maintained by assigning to the playlists
collections at the backend level. collections at the backend level.
- :meth:`mopidy.core.StoredPlaylistsController.delete` now accepts an URI, - :meth:`mopidy.core.PlaylistsController.delete` now accepts an URI, and not
and not a playlist object. a playlist object.
- :meth:`mopidy.core.StoredPlaylistsController.save` now returns the saved - :meth:`mopidy.core.PlaylistsController.save` now returns the saved
playlist. The returned playlist may differ from the saved playlist, and playlist. The returned playlist may differ from the saved playlist, and
should thus be used instead of the playlist passed to ``save()``. should thus be used instead of the playlist passed to ``save()``.
- :meth:`mopidy.core.StoredPlaylistsController.rename` has been removed, - :meth:`mopidy.core.PlaylistsController.rename` has been removed, since
since renaming can be done with ``save()``. renaming can be done with ``save()``.
**Changes** **Changes**
@ -105,11 +111,22 @@ backends:
- The Spotify backend now returns the track if you search for the Spotify track - The Spotify backend now returns the track if you search for the Spotify track
URI. (Fixes: :issue:`233`) URI. (Fixes: :issue:`233`)
- Renamed "current playlist" to "tracklist" everywhere, including the core API - :meth:`mopidy.core.TracklistController.append` now returns a list of the
used by frontends. :class:`mopidy.models.TlTrack` instances that was added to the tracklist.
This makes it easier to start playing one of the tracks that was just
appended to the tracklist.
- Renamed "stored playlists" to "playlists" everywhere, including the core API - When the tracklist is changed, we now trigger the new
used by frontends. :meth:`mopidy.core.CoreListener.tracklist_changed` event. Previously we
triggered :meth:`mopidy.core.CoreListener.playlist_changed`, which is
intended for stored playlists, not the tracklist.
- The event :meth:`mopidy.core.CoreListener.playlist_changed` has been changed
to include the playlist that was changed.
- The MPRIS playlists interface is now supported by our MPRIS frontend. This
means that you now can select playlists to queue and play from the Ubuntu
Sound Menu.
**Bug fixes** **Bug fixes**
@ -122,6 +139,10 @@ backends:
- MPD no longer lowercases search queries. This broke e.g. search by URI, where - MPD no longer lowercases search queries. This broke e.g. search by URI, where
casing may be essential. casing may be essential.
- :issue:`236`: The ``mopidy-scan`` command failed to include tags from ALAC
files (Apple lossless) because it didn't support multiple tag messages from
GStreamer per track it scanned.
v0.8.1 (2012-10-30) v0.8.1 (2012-10-30)
=================== ===================

View File

@ -9,8 +9,8 @@ Specification. It's a spec that describes a standard D-Bus interface for making
media players available to other applications on the same system. media players available to other applications on the same system.
Mopidy's :ref:`MPRIS frontend <mpris-frontend>` currently implements all Mopidy's :ref:`MPRIS frontend <mpris-frontend>` currently implements all
required parts of the MPRIS spec, but not the optional playlist interface. For required parts of the MPRIS spec, plus the optional playlist interface. It does
tracking the development of the playlist interface, see :issue:`229`. not implement the optional tracklist interface.
.. _ubuntu-sound-menu: .. _ubuntu-sound-menu:

View File

@ -3,3 +3,4 @@ from __future__ import unicode_literals
# flake8: noqa # flake8: noqa
from .actor import Audio from .actor import Audio
from .listener import AudioListener from .listener import AudioListener
from .constants import PlaybackState

View File

@ -13,6 +13,7 @@ from mopidy import settings
from mopidy.utils import process from mopidy.utils import process
from . import mixers from . import mixers
from .constants import PlaybackState
from .listener import AudioListener from .listener import AudioListener
logger = logging.getLogger('mopidy.audio') logger = logging.getLogger('mopidy.audio')
@ -29,9 +30,11 @@ class Audio(pykka.ThreadingActor):
- :attr:`mopidy.settings.OUTPUT` - :attr:`mopidy.settings.OUTPUT`
- :attr:`mopidy.settings.MIXER` - :attr:`mopidy.settings.MIXER`
- :attr:`mopidy.settings.MIXER_TRACK` - :attr:`mopidy.settings.MIXER_TRACK`
""" """
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
state = PlaybackState.STOPPED
def __init__(self): def __init__(self):
super(Audio, self).__init__() super(Audio, self).__init__()
@ -39,8 +42,11 @@ class Audio(pykka.ThreadingActor):
self._mixer = None self._mixer = None
self._mixer_track = None self._mixer_track = None
self._software_mixing = False self._software_mixing = False
self._appsrc = None
self._message_processor_set_up = False self._notify_source_signal_id = None
self._about_to_finish_id = None
self._message_signal_id = None
def on_start(self): def on_start(self):
try: try:
@ -63,7 +69,13 @@ class Audio(pykka.ThreadingActor):
fakesink = gst.element_factory_make('fakesink') fakesink = gst.element_factory_make('fakesink')
self._playbin.set_property('video-sink', fakesink) self._playbin.set_property('video-sink', fakesink)
self._playbin.connect('notify::source', self._on_new_source) self._about_to_finish_id = self._playbin.connect(
'about-to-finish', self._on_about_to_finish)
self._notify_source_signal_id = self._playbin.connect(
'notify::source', self._on_new_source)
def _on_about_to_finish(self, element):
self._appsrc = None
def _on_new_source(self, element, pad): def _on_new_source(self, element, pad):
uri = element.get_property('uri') uri = element.get_property('uri')
@ -77,8 +89,15 @@ class Audio(pykka.ThreadingActor):
b'rate=(int)44100') b'rate=(int)44100')
source = element.get_property('source') source = element.get_property('source')
source.set_property('caps', default_caps) source.set_property('caps', default_caps)
source.set_property('format', b'time') # Gstreamer does not like unicode
self._appsrc = source
def _teardown_playbin(self): def _teardown_playbin(self):
if self._about_to_finish_id:
self._playbin.disconnect(self._about_to_finish_id)
if self._notify_source_signal_id:
self._playbin.disconnect(self._notify_source_signal_id)
self._playbin.set_state(gst.STATE_NULL) self._playbin.set_state(gst.STATE_NULL)
def _setup_output(self): def _setup_output(self):
@ -151,17 +170,21 @@ class Audio(pykka.ThreadingActor):
def _setup_message_processor(self): def _setup_message_processor(self):
bus = self._playbin.get_bus() bus = self._playbin.get_bus()
bus.add_signal_watch() bus.add_signal_watch()
bus.connect('message', self._on_message) self._message_signal_id = bus.connect('message', self._on_message)
self._message_processor_set_up = True
def _teardown_message_processor(self): def _teardown_message_processor(self):
if self._message_processor_set_up: if self._message_signal_id:
bus = self._playbin.get_bus() bus = self._playbin.get_bus()
bus.disconnect(self._message_signal_id)
bus.remove_signal_watch() bus.remove_signal_watch()
def _on_message(self, bus, message): def _on_message(self, bus, message):
if message.type == gst.MESSAGE_EOS: if (message.type == gst.MESSAGE_STATE_CHANGED
self._trigger_reached_end_of_stream_event() and message.src == self._playbin):
old_state, new_state, pending_state = message.parse_state_changed()
self._on_playbin_state_changed(old_state, new_state, pending_state)
elif message.type == gst.MESSAGE_EOS:
self._on_end_of_stream()
elif message.type == gst.MESSAGE_ERROR: elif message.type == gst.MESSAGE_ERROR:
error, debug = message.parse_error() error, debug = message.parse_error()
logger.error('%s %s', error, debug) logger.error('%s %s', error, debug)
@ -170,8 +193,37 @@ class Audio(pykka.ThreadingActor):
error, debug = message.parse_warning() error, debug = message.parse_warning()
logger.warning('%s %s', error, debug) logger.warning('%s %s', error, debug)
def _trigger_reached_end_of_stream_event(self): def _on_playbin_state_changed(self, old_state, new_state, pending_state):
logger.debug('Triggering reached end of stream event') if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL:
# XXX: We're not called on the last state change when going down to
# NULL, so we rewrite the second to last call to get the expected
# behavior.
new_state = gst.STATE_NULL
pending_state = gst.STATE_VOID_PENDING
if pending_state != gst.STATE_VOID_PENDING:
return # Ignore intermediate state changes
if new_state == gst.STATE_READY:
return # Ignore READY state as it's GStreamer specific
if new_state == gst.STATE_PLAYING:
new_state = PlaybackState.PLAYING
elif new_state == gst.STATE_PAUSED:
new_state = PlaybackState.PAUSED
elif new_state == gst.STATE_NULL:
new_state = PlaybackState.STOPPED
old_state, self.state = self.state, new_state
logger.debug(
'Triggering event: state_changed(old_state=%s, new_state=%s)',
old_state, new_state)
AudioListener.send('state_changed',
old_state=old_state, new_state=new_state)
def _on_end_of_stream(self):
logger.debug('Triggering reached_end_of_stream event')
AudioListener.send('reached_end_of_stream') AudioListener.send('reached_end_of_stream')
def set_uri(self, uri): def set_uri(self, uri):
@ -185,23 +237,21 @@ class Audio(pykka.ThreadingActor):
""" """
self._playbin.set_property('uri', uri) self._playbin.set_property('uri', uri)
def emit_data(self, capabilities, data): def emit_data(self, buffer_):
""" """
Call this to deliver raw audio data to be played. Call this to deliver raw audio data to be played.
Note that the uri must be set to ``appsrc://`` for this to work. Note that the uri must be set to ``appsrc://`` for this to work.
:param capabilities: a GStreamer capabilities string Returns true if data was delivered.
:type capabilities: string
:param data: raw audio data to be played
"""
caps = gst.caps_from_string(capabilities)
buffer_ = gst.Buffer(buffer(data))
buffer_.set_caps(caps)
source = self._playbin.get_property('source') :param buffer_: buffer to pass to appsrc
source.set_property('caps', caps) :type buffer_: :class:`gst.Buffer`
source.emit('push-buffer', buffer_) :rtype: boolean
"""
if not self._appsrc:
return False
return self._appsrc.emit('push-buffer', buffer_) == gst.FLOW_OK
def emit_end_of_stream(self): def emit_end_of_stream(self):
""" """

16
mopidy/audio/constants.py Normal file
View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
class PlaybackState(object):
"""
Enum of playback states.
"""
#: Constant representing the paused state.
PAUSED = 'paused'
#: Constant representing the playing state.
PLAYING = 'playing'
#: Constant representing the stopped state.
STOPPED = 'stopped'

View File

@ -28,3 +28,18 @@ class AudioListener(object):
*MAY* be implemented by actor. *MAY* be implemented by actor.
""" """
pass pass
def state_changed(self, old_state, new_state):
"""
Called after the playback state have changed.
Will be called for both immediate and async state changes in GStreamer.
*MAY* be implemented by actor.
:param old_state: the state before the change
:type old_state: string from :class:`mopidy.core.PlaybackState` field
:param new_state: the state after the change
:type new_state: string from :class:`mopidy.core.PlaybackState` field
"""
pass

View File

@ -82,22 +82,30 @@ class DummyPlaybackProvider(base.BasePlaybackProvider):
class DummyPlaylistsProvider(base.BasePlaylistsProvider): class DummyPlaylistsProvider(base.BasePlaylistsProvider):
def create(self, name): def create(self, name):
playlist = Playlist(name=name) playlist = Playlist(name=name, uri='dummy:%s' % name)
self._playlists.append(playlist) self._playlists.append(playlist)
return playlist return playlist
def delete(self, playlist): def delete(self, uri):
self._playlists.remove(playlist) playlist = self.lookup(uri)
if playlist:
self._playlists.remove(playlist)
def lookup(self, uri): def lookup(self, uri):
return filter(lambda p: p.uri == uri, self._playlists) for playlist in self._playlists:
if playlist.uri == uri:
return playlist
def refresh(self): def refresh(self):
pass pass
def rename(self, playlist, new_name):
self._playlists[self._playlists.index(playlist)] = \
playlist.copy(name=new_name)
def save(self, playlist): def save(self, playlist):
self._playlists.append(playlist) old_playlist = self.lookup(playlist.uri)
if old_playlist is not None:
index = self._playlists.index(old_playlist)
self._playlists[index] = playlist
else:
self._playlists.append(playlist)
return playlist

View File

@ -0,0 +1,32 @@
from __future__ import unicode_literals
import pykka
class BackendListener(object):
"""
Marker interface for recipients of events sent by the backend actors.
Any Pykka actor that mixes in this class will receive calls to the methods
defined here when the corresponding events happen in the core actor. This
interface is used both for looking up what actors to notify of the events,
and for providing default implementations for those listeners that are not
interested in all events.
Normally, only the Core actor should mix in this class.
"""
@staticmethod
def send(event, **kwargs):
"""Helper to allow calling of backend listener events"""
listeners = pykka.ActorRegistry.get_by_class(BackendListener)
for listener in listeners:
getattr(listener.proxy(), event)(**kwargs)
def playlists_loaded(self):
"""
Called when playlists are loaded or refreshed.
*MAY* be implemented by actor.
"""
pass

View File

@ -6,7 +6,7 @@ import os
import shutil import shutil
from mopidy import settings from mopidy import settings
from mopidy.backends import base from mopidy.backends import base, listener
from mopidy.models import Playlist from mopidy.models import Playlist
from mopidy.utils import formatting, path from mopidy.utils import formatting, path
@ -63,6 +63,7 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
playlists.append(playlist) playlists.append(playlist)
self.playlists = playlists self.playlists = playlists
listener.BackendListener.send('playlists_loaded')
def save(self, playlist): def save(self, playlist):
assert playlist.uri, 'Cannot save playlist without URI' assert playlist.uri, 'Cannot save playlist without URI'

View File

@ -52,12 +52,8 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
return self.seek(time_position) return self.seek(time_position)
def seek(self, time_position): def seek(self, time_position):
self.audio.prepare_change()
self.backend.spotify.session.seek(time_position) self.backend.spotify.session.seek(time_position)
self.audio.start_playback()
self._timer.seek(time_position) self._timer.seek(time_position)
return True return True
def stop(self): def stop(self):

View File

@ -11,7 +11,9 @@ class SpotifyPlaylistsProvider(base.BasePlaylistsProvider):
pass # TODO pass # TODO
def lookup(self, uri): def lookup(self, uri):
pass # TODO for playlist in self._playlists:
if playlist.uri == uri:
return playlist
def refresh(self): def refresh(self):
pass # TODO pass # TODO

View File

@ -1,5 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import logging import logging
import os import os
import threading import threading
@ -7,6 +11,7 @@ import threading
from spotify.manager import SpotifySessionManager as PyspotifySessionManager from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from mopidy import settings from mopidy import settings
from mopidy.backends.listener import BackendListener
from mopidy.models import Playlist from mopidy.models import Playlist
from mopidy.utils import process, versioning from mopidy.utils import process, versioning
@ -108,8 +113,13 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate, 'sample_rate': sample_rate,
'channels': channels, 'channels': channels,
} }
self.audio.emit_data(capabilites, bytes(frames)) buffer_ = gst.Buffer(bytes(frames))
return num_frames buffer_.set_caps(gst.caps_from_string(capabilites))
if self.audio.emit_data(buffer_).get():
return num_frames
else:
return 0
def play_token_lost(self, session): def play_token_lost(self, session):
"""Callback used by pyspotify""" """Callback used by pyspotify"""
@ -146,6 +156,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
playlists = filter(None, playlists) playlists = filter(None, playlists)
self.backend.playlists.playlists = playlists self.backend.playlists.playlists = playlists
logger.info('Loaded %d Spotify playlist(s)', len(playlists)) logger.info('Loaded %d Spotify playlist(s)', len(playlists))
BackendListener.send('playlists_loaded')
def search(self, query, queue): def search(self, query, queue):
"""Search method used by Mopidy backend""" """Search method used by Mopidy backend"""

View File

@ -4,15 +4,17 @@ import itertools
import pykka import pykka
from mopidy.audio import AudioListener from mopidy.audio import AudioListener, PlaybackState
from mopidy.backends.listener import BackendListener
from .library import LibraryController from .library import LibraryController
from .listener import CoreListener
from .playback import PlaybackController from .playback import PlaybackController
from .playlists import PlaylistsController from .playlists import PlaylistsController
from .tracklist import TracklistController from .tracklist import TracklistController
class Core(pykka.ThreadingActor, AudioListener): class Core(pykka.ThreadingActor, AudioListener, BackendListener):
#: The library controller. An instance of #: The library controller. An instance of
# :class:`mopidy.core.LibraryController`. # :class:`mopidy.core.LibraryController`.
library = None library = None
@ -55,6 +57,22 @@ class Core(pykka.ThreadingActor, AudioListener):
def reached_end_of_stream(self): def reached_end_of_stream(self):
self.playback.on_end_of_track() self.playback.on_end_of_track()
def state_changed(self, old_state, new_state):
# XXX: This is a temporary fix for issue #232 while we wait for a more
# permanent solution with the implementation of issue #234. When the
# Spotify play token is lost, the Spotify backend pauses audio
# playback, but mopidy.core doesn't know this, so we need to update
# mopidy.core's state to match the actual state in mopidy.audio. If we
# don't do this, clients will think that we're still playing.
if (new_state == PlaybackState.PAUSED
and self.playback.state != PlaybackState.PAUSED):
self.playback.state = new_state
self.playback._trigger_track_playback_paused()
def playlists_loaded(self):
# Forward event from backend to frontends
CoreListener.send('playlists_loaded')
class Backends(list): class Backends(list):
def __init__(self, backends): def __init__(self, backends):
@ -66,8 +84,8 @@ class Backends(list):
# the X_by_uri_scheme dicts below. # the X_by_uri_scheme dicts below.
self.with_library = [b for b in backends if b.has_library().get()] self.with_library = [b for b in backends if b.has_library().get()]
self.with_playback = [b for b in backends if b.has_playback().get()] self.with_playback = [b for b in backends if b.has_playback().get()]
self.with_playlists = [b for b in backends self.with_playlists = [
if b.has_playlists().get()] b for b in backends if b.has_playlists().get()]
self.by_uri_scheme = {} self.by_uri_scheme = {}
for backend in backends: for backend in backends:

View File

@ -84,11 +84,30 @@ class CoreListener(object):
""" """
pass pass
def playlist_changed(self): def tracklist_changed(self):
"""
Called whenever the tracklist is changed.
*MAY* be implemented by actor.
"""
pass
def playlists_loaded(self):
"""
Called when playlists are loaded or refreshed.
*MAY* be implemented by actor.
"""
pass
def playlist_changed(self, playlist):
""" """
Called whenever a playlist is changed. Called whenever a playlist is changed.
*MAY* be implemented by actor. *MAY* be implemented by actor.
:param playlist: the changed playlist
:type playlist: :class:`mopidy.models.Playlist`
""" """
pass pass

View File

@ -4,6 +4,8 @@ import logging
import random import random
import urlparse import urlparse
from mopidy.audio import PlaybackState
from . import listener from . import listener
@ -24,21 +26,6 @@ def option_wrapper(name, default):
return property(get_option, set_option) return property(get_option, set_option)
class PlaybackState(object):
"""
Enum of playback states.
"""
#: Constant representing the paused state.
PAUSED = 'paused'
#: Constant representing the playing state.
PLAYING = 'playing'
#: Constant representing the stopped state.
STOPPED = 'stopped'
class PlaybackController(object): class PlaybackController(object):
# pylint: disable = R0902 # pylint: disable = R0902
# Too many instance attributes # Too many instance attributes

View File

@ -5,6 +5,8 @@ import urlparse
import pykka import pykka
from . import listener
class PlaylistsController(object): class PlaylistsController(object):
pykka_traversable = True pykka_traversable = True
@ -20,8 +22,8 @@ class PlaylistsController(object):
Read-only. List of :class:`mopidy.models.Playlist`. Read-only. List of :class:`mopidy.models.Playlist`.
""" """
futures = [b.playlists.playlists futures = [
for b in self.backends.with_playlists] b.playlists.playlists for b in self.backends.with_playlists]
results = pykka.get_all(futures) results = pykka.get_all(futures)
return list(itertools.chain(*results)) return list(itertools.chain(*results))
@ -47,7 +49,9 @@ class PlaylistsController(object):
backend = self.backends.by_uri_scheme[uri_scheme] backend = self.backends.by_uri_scheme[uri_scheme]
else: else:
backend = self.backends.with_playlists[0] backend = self.backends.with_playlists[0]
return backend.playlists.create(name).get() playlist = backend.playlists.create(name).get()
listener.CoreListener.send('playlist_changed', playlist=playlist)
return playlist
def delete(self, uri): def delete(self, uri):
""" """
@ -125,14 +129,16 @@ class PlaylistsController(object):
:type uri_scheme: string :type uri_scheme: string
""" """
if uri_scheme is None: if uri_scheme is None:
futures = [b.playlists.refresh() futures = [
for b in self.backends.with_playlists] b.playlists.refresh() for b in self.backends.with_playlists]
pykka.get_all(futures) pykka.get_all(futures)
listener.CoreListener.send('playlists_loaded')
else: else:
backend = self.backends.with_playlists_by_uri_scheme.get( backend = self.backends.with_playlists_by_uri_scheme.get(
uri_scheme, None) uri_scheme, None)
if backend: if backend:
backend.playlists.refresh().get() backend.playlists.refresh().get()
listener.CoreListener.send('playlists_loaded')
def save(self, playlist): def save(self, playlist):
""" """
@ -162,4 +168,6 @@ class PlaylistsController(object):
backend = self.backends.with_playlists_by_uri_scheme.get( backend = self.backends.with_playlists_by_uri_scheme.get(
uri_scheme, None) uri_scheme, None)
if backend: if backend:
return backend.playlists.save(playlist).get() playlist = backend.playlists.save(playlist).get()
listener.CoreListener.send('playlist_changed', playlist=playlist)
return playlist

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from copy import copy
import logging import logging
import random import random
@ -16,8 +15,8 @@ class TracklistController(object):
pykka_traversable = True pykka_traversable = True
def __init__(self, core): def __init__(self, core):
self.core = core self._core = core
self.tlid = 0 self._next_tlid = 0
self._tl_tracks = [] self._tl_tracks = []
self._version = 0 self._version = 0
@ -28,12 +27,12 @@ class TracklistController(object):
Read-only. Read-only.
""" """
return [copy(tl_track) for tl_track in self._tl_tracks] return self._tl_tracks[:]
@property @property
def tracks(self): def tracks(self):
""" """
List of :class:`mopidy.models.Track` in the current playlist. List of :class:`mopidy.models.Track` in the tracklist.
Read-only. Read-only.
""" """
@ -42,78 +41,91 @@ class TracklistController(object):
@property @property
def length(self): def length(self):
""" """
Length of the current playlist. Length of the tracklist.
""" """
return len(self._tl_tracks) return len(self._tl_tracks)
@property @property
def version(self): def version(self):
""" """
The current playlist version. Integer which is increased every time the The tracklist version. Integer which is increased every time the
current playlist is changed. Is not reset before Mopidy is restarted. tracklist is changed. Is not reset before Mopidy is restarted.
""" """
return self._version return self._version
@version.setter # noqa @version.setter # noqa
def version(self, version): def version(self, version):
self._version = version self._version = version
self.core.playback.on_tracklist_change() self._core.playback.on_tracklist_change()
self._trigger_playlist_changed() self._trigger_tracklist_changed()
def add(self, track, at_position=None, increase_version=True): def add(self, track, at_position=None, increase_version=True):
""" """
Add the track to the end of, or at the given position in the current Add the track to the end of, or at the given position in the tracklist.
playlist.
Triggers the :method:`mopidy.core.CoreListener.tracklist_changed`
event.
:param track: track to add :param track: track to add
:type track: :class:`mopidy.models.Track` :type track: :class:`mopidy.models.Track`
:param at_position: position in current playlist to add track :param at_position: position in tracklist to add track
:type at_position: int or :class:`None` :type at_position: int or :class:`None`
:param increase_version: if the playlist version should be increased :param increase_version: if the tracklist version should be increased
:type increase_version: :class:`True` or :class:`False` :type increase_version: :class:`True` or :class:`False`
:rtype: two-tuple of (TLID integer, :class:`mopidy.models.Track`) that :rtype: two-tuple of (TLID integer, :class:`mopidy.models.Track`) that
was added to the current playlist playlist was added to the tracklist
""" """
assert at_position <= len(self._tl_tracks), \ assert at_position <= len(self._tl_tracks), \
'at_position can not be greater than playlist length' 'at_position can not be greater than tracklist length'
tl_track = TlTrack(self.tlid, track) tl_track = TlTrack(self._next_tlid, track)
if at_position is not None: if at_position is not None:
self._tl_tracks.insert(at_position, tl_track) self._tl_tracks.insert(at_position, tl_track)
else: else:
self._tl_tracks.append(tl_track) self._tl_tracks.append(tl_track)
if increase_version: if increase_version:
self.version += 1 self.version += 1
self.tlid += 1 self._next_tlid += 1
return tl_track return tl_track
def append(self, tracks): def append(self, tracks):
""" """
Append the given tracks to the current playlist. Append the given tracks to the tracklist.
Triggers the :method:`mopidy.core.CoreListener.tracklist_changed`
event.
:param tracks: tracks to append :param tracks: tracks to append
:type tracks: list of :class:`mopidy.models.Track` :type tracks: list of :class:`mopidy.models.Track`
:rtype: list of class:`mopidy.models.TlTrack`
""" """
tl_tracks = []
for track in tracks: for track in tracks:
self.add(track, increase_version=False) tl_tracks.append(self.add(track, increase_version=False))
if tracks: if tracks:
self.version += 1 self.version += 1
return tl_tracks
def clear(self): def clear(self):
"""Clear the current playlist.""" """
Clear the tracklist.
Triggers the :method:`mopidy.core.CoreListener.tracklist_changed`
event.
"""
self._tl_tracks = [] self._tl_tracks = []
self.version += 1 self.version += 1
def get(self, **criteria): def get(self, **criteria):
""" """
Get track by given criterias from current playlist. Get track by given criterias from tracklist.
Raises :exc:`LookupError` if a unique match is not found. Raises :exc:`LookupError` if a unique match is not found.
Examples:: Examples::
get(tlid=7) # Returns track with TLID 7 get(tlid=7) # Returns track with TLID 7 (tracklist ID)
# (current playlist ID)
get(id=1) # Returns track with ID 1 get(id=1) # Returns track with ID 1
get(uri='xyz') # Returns track with URI 'xyz' get(uri='xyz') # Returns track with URI 'xyz'
get(id=1, uri='xyz') # Returns track with ID 1 and URI 'xyz' get(id=1, uri='xyz') # Returns track with ID 1 and URI 'xyz'
@ -141,7 +153,7 @@ class TracklistController(object):
def index(self, tl_track): def index(self, tl_track):
""" """
Get index of the given (TLID integer, :class:`mopidy.models.Track`) Get index of the given (TLID integer, :class:`mopidy.models.Track`)
two-tuple in the current playlist. two-tuple in the tracklist.
Raises :exc:`ValueError` if not found. Raises :exc:`ValueError` if not found.
@ -155,6 +167,9 @@ class TracklistController(object):
""" """
Move the tracks in the slice ``[start:end]`` to ``to_position``. Move the tracks in the slice ``[start:end]`` to ``to_position``.
Triggers the :method:`mopidy.core.CoreListener.tracklist_changed`
event.
:param start: position of first track to move :param start: position of first track to move
:type start: int :type start: int
:param end: position after last track to move :param end: position after last track to move
@ -170,10 +185,10 @@ class TracklistController(object):
assert start < end, 'start must be smaller than end' assert start < end, 'start must be smaller than end'
assert start >= 0, 'start must be at least zero' assert start >= 0, 'start must be at least zero'
assert end <= len(tl_tracks), \ assert end <= len(tl_tracks), \
'end can not be larger than playlist length' 'end can not be larger than tracklist length'
assert to_position >= 0, 'to_position must be at least zero' assert to_position >= 0, 'to_position must be at least zero'
assert to_position <= len(tl_tracks), \ assert to_position <= len(tl_tracks), \
'to_position can not be larger than playlist length' 'to_position can not be larger than tracklist length'
new_tl_tracks = tl_tracks[:start] + tl_tracks[end:] new_tl_tracks = tl_tracks[:start] + tl_tracks[end:]
for tl_track in tl_tracks[start:end]: for tl_track in tl_tracks[start:end]:
@ -184,10 +199,13 @@ class TracklistController(object):
def remove(self, **criteria): def remove(self, **criteria):
""" """
Remove the track from the current playlist. Remove the track from the tracklist.
Uses :meth:`get()` to lookup the track to remove. Uses :meth:`get()` to lookup the track to remove.
Triggers the :method:`mopidy.core.CoreListener.tracklist_changed`
event.
:param criteria: on or more criteria to match by :param criteria: on or more criteria to match by
:type criteria: dict :type criteria: dict
""" """
@ -198,9 +216,12 @@ class TracklistController(object):
def shuffle(self, start=None, end=None): def shuffle(self, start=None, end=None):
""" """
Shuffles the entire playlist. If ``start`` and ``end`` is given only Shuffles the entire tracklist. If ``start`` and ``end`` is given only
shuffles the slice ``[start:end]``. shuffles the slice ``[start:end]``.
Triggers the :method:`mopidy.core.CoreListener.tracklist_changed`
event.
:param start: position of first track to shuffle :param start: position of first track to shuffle
:type start: int or :class:`None` :type start: int or :class:`None`
:param end: position after last track to shuffle :param end: position after last track to shuffle
@ -216,7 +237,7 @@ class TracklistController(object):
if end is not None: if end is not None:
assert end <= len(tl_tracks), 'end can not be larger than ' + \ assert end <= len(tl_tracks), 'end can not be larger than ' + \
'playlist length' 'tracklist length'
before = tl_tracks[:start or 0] before = tl_tracks[:start or 0]
shuffled = tl_tracks[start:end] shuffled = tl_tracks[start:end]
@ -227,8 +248,8 @@ class TracklistController(object):
def slice(self, start, end): def slice(self, start, end):
""" """
Returns a slice of the current playlist, limited by the given Returns a slice of the tracklist, limited by the given start and end
start and end positions. positions.
:param start: position of first track to include in slice :param start: position of first track to include in slice
:type start: int :type start: int
@ -236,8 +257,8 @@ class TracklistController(object):
:type end: int :type end: int
:rtype: two-tuple of (TLID integer, :class:`mopidy.models.Track`) :rtype: two-tuple of (TLID integer, :class:`mopidy.models.Track`)
""" """
return [copy(tl_track) for tl_track in self._tl_tracks[start:end]] return self._tl_tracks[start:end]
def _trigger_playlist_changed(self): def _trigger_tracklist_changed(self):
logger.debug('Triggering playlist changed event') logger.debug('Triggering event: tracklist_changed()')
listener.CoreListener.send('playlist_changed') listener.CoreListener.send('tracklist_changed')

View File

@ -43,7 +43,7 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
def playback_state_changed(self, old_state, new_state): def playback_state_changed(self, old_state, new_state):
self.send_idle('player') self.send_idle('player')
def playlist_changed(self): def tracklist_changed(self):
self.send_idle('playlist') self.send_idle('playlist')
def options_changed(self): def options_changed(self):

View File

@ -112,9 +112,7 @@ def list_(context, field, mpd_query=None):
``artist``, ``date``, or ``genre``. ``artist``, ``date``, or ``genre``.
``ARTIST`` is an optional parameter when type is ``album``, ``ARTIST`` is an optional parameter when type is ``album``,
``date``, or ``genre``. ``date``, or ``genre``. This filters the result list by an artist.
This filters the result list by an artist.
*Clarifications:* *Clarifications:*

View File

@ -57,35 +57,48 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
self.indicate_server.show() self.indicate_server.show()
logger.debug('Startup notification sent') logger.debug('Startup notification sent')
def _emit_properties_changed(self, *changed_properties): def _emit_properties_changed(self, interface, changed_properties):
if self.mpris_object is None: if self.mpris_object is None:
return return
props_with_new_values = [ props_with_new_values = [
(p, self.mpris_object.Get(objects.PLAYER_IFACE, p)) (p, self.mpris_object.Get(interface, p))
for p in changed_properties] for p in changed_properties]
self.mpris_object.PropertiesChanged( self.mpris_object.PropertiesChanged(
objects.PLAYER_IFACE, dict(props_with_new_values), []) interface, dict(props_with_new_values), [])
def track_playback_paused(self, track, time_position): def track_playback_paused(self, track, time_position):
logger.debug('Received track playback paused event') logger.debug('Received track_playback_paused event')
self._emit_properties_changed('PlaybackStatus') self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus'])
def track_playback_resumed(self, track, time_position): def track_playback_resumed(self, track, time_position):
logger.debug('Received track playback resumed event') logger.debug('Received track_playback_resumed event')
self._emit_properties_changed('PlaybackStatus') self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus'])
def track_playback_started(self, track): def track_playback_started(self, track):
logger.debug('Received track playback started event') logger.debug('Received track_playback_started event')
self._emit_properties_changed('PlaybackStatus', 'Metadata') self._emit_properties_changed(
objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata'])
def track_playback_ended(self, track, time_position): def track_playback_ended(self, track, time_position):
logger.debug('Received track playback ended event') logger.debug('Received track_playback_ended event')
self._emit_properties_changed('PlaybackStatus', 'Metadata') self._emit_properties_changed(
objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata'])
def volume_changed(self): def volume_changed(self):
logger.debug('Received volume changed event') logger.debug('Received volume_changed event')
self._emit_properties_changed('Volume') self._emit_properties_changed(objects.PLAYER_IFACE, ['Volume'])
def seeked(self, time_position_in_ms): def seeked(self, time_position_in_ms):
logger.debug('Received seeked event') logger.debug('Received seeked event')
self.mpris_object.Seeked(time_position_in_ms * 1000) self.mpris_object.Seeked(time_position_in_ms * 1000)
def playlists_loaded(self):
logger.debug('Received playlists_loaded event')
self._emit_properties_changed(
objects.PLAYLISTS_IFACE, ['PlaylistCount'])
def playlist_changed(self, playlist):
logger.debug('Received playlist_changed event')
playlist_id = self.mpris_object.get_playlist_id(playlist.uri)
playlist = (playlist_id, playlist.name, '')
self.mpris_object.PlaylistChanged(playlist)

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64
import logging import logging
import os import os
@ -27,10 +28,11 @@ BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
OBJECT_PATH = '/org/mpris/MediaPlayer2' OBJECT_PATH = '/org/mpris/MediaPlayer2'
ROOT_IFACE = 'org.mpris.MediaPlayer2' ROOT_IFACE = 'org.mpris.MediaPlayer2'
PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player' PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player'
PLAYLISTS_IFACE = 'org.mpris.MediaPlayer2.Playlists'
class MprisObject(dbus.service.Object): class MprisObject(dbus.service.Object):
"""Implements http://www.mpris.org/2.1/spec/""" """Implements http://www.mpris.org/2.2/spec/"""
properties = None properties = None
@ -39,6 +41,7 @@ class MprisObject(dbus.service.Object):
self.properties = { self.properties = {
ROOT_IFACE: self._get_root_iface_properties(), ROOT_IFACE: self._get_root_iface_properties(),
PLAYER_IFACE: self._get_player_iface_properties(), PLAYER_IFACE: self._get_player_iface_properties(),
PLAYLISTS_IFACE: self._get_playlists_iface_properties(),
} }
bus_name = self._connect_to_dbus() bus_name = self._connect_to_dbus()
dbus.service.Object.__init__(self, bus_name, OBJECT_PATH) dbus.service.Object.__init__(self, bus_name, OBJECT_PATH)
@ -46,6 +49,8 @@ class MprisObject(dbus.service.Object):
def _get_root_iface_properties(self): def _get_root_iface_properties(self):
return { return {
'CanQuit': (True, None), 'CanQuit': (True, None),
'Fullscreen': (False, None),
'CanSetFullscreen': (False, None),
'CanRaise': (False, None), 'CanRaise': (False, None),
# NOTE Change if adding optional track list support # NOTE Change if adding optional track list support
'HasTrackList': (False, None), 'HasTrackList': (False, None),
@ -76,6 +81,13 @@ class MprisObject(dbus.service.Object):
'CanControl': (self.get_CanControl, None), 'CanControl': (self.get_CanControl, None),
} }
def _get_playlists_iface_properties(self):
return {
'PlaylistCount': (self.get_PlaylistCount, None),
'Orderings': (self.get_Orderings, None),
'ActivePlaylist': (self.get_ActivePlaylist, None),
}
def _connect_to_dbus(self): def _connect_to_dbus(self):
logger.debug('Connecting to D-Bus...') logger.debug('Connecting to D-Bus...')
mainloop = dbus.mainloop.glib.DBusGMainLoop() mainloop = dbus.mainloop.glib.DBusGMainLoop()
@ -84,10 +96,22 @@ class MprisObject(dbus.service.Object):
logger.info('Connected to D-Bus') logger.info('Connected to D-Bus')
return bus_name return bus_name
def _get_track_id(self, tl_track): def get_playlist_id(self, playlist_uri):
# Only A-Za-z0-9_ is allowed, which is 63 chars, so we can't use
# base64. Luckily, D-Bus does not limit the length of object paths.
# Since base32 pads trailing bytes with "=" chars, we need to replace
# them with an allowed character such as "_".
encoded_uri = base64.b32encode(playlist_uri).replace('=', '_')
return '/com/mopidy/playlist/%s' % encoded_uri
def get_playlist_uri(self, playlist_id):
encoded_uri = playlist_id.split('/')[-1].replace('_', '=')
return base64.b32decode(encoded_uri)
def get_track_id(self, tl_track):
return '/com/mopidy/track/%d' % tl_track.tlid return '/com/mopidy/track/%d' % tl_track.tlid
def _get_tlid(self, track_id): def get_track_tlid(self, track_id):
assert track_id.startswith('/com/mopidy/track/') assert track_id.startswith('/com/mopidy/track/')
return track_id.split('/')[-1] return track_id.split('/')[-1]
@ -237,7 +261,7 @@ class MprisObject(dbus.service.Object):
current_tl_track = self.core.playback.current_tl_track.get() current_tl_track = self.core.playback.current_tl_track.get()
if current_tl_track is None: if current_tl_track is None:
return return
if track_id != self._get_track_id(current_tl_track): if track_id != self.get_track_id(current_tl_track):
return return
if position < 0: if position < 0:
return return
@ -335,7 +359,7 @@ class MprisObject(dbus.service.Object):
return {'mpris:trackid': ''} return {'mpris:trackid': ''}
else: else:
(_, track) = current_tl_track (_, track) = current_tl_track
metadata = {'mpris:trackid': self._get_track_id(current_tl_track)} metadata = {'mpris:trackid': self.get_track_id(current_tl_track)}
if track.length: if track.length:
metadata['mpris:length'] = track.length * 1000 metadata['mpris:length'] = track.length * 1000
if track.uri: if track.uri:
@ -418,3 +442,58 @@ class MprisObject(dbus.service.Object):
def get_CanControl(self): def get_CanControl(self):
# NOTE This could be a setting for the end user to change. # NOTE This could be a setting for the end user to change.
return True return True
### Playlists interface methods
@dbus.service.method(dbus_interface=PLAYLISTS_IFACE)
def ActivatePlaylist(self, playlist_id):
logger.debug(
'%s.ActivatePlaylist(%r) called', PLAYLISTS_IFACE, playlist_id)
playlist_uri = self.get_playlist_uri(playlist_id)
playlist = self.core.playlists.lookup(playlist_uri).get()
if playlist and playlist.tracks:
tl_tracks = self.core.tracklist.append(playlist.tracks).get()
self.core.playback.play(tl_tracks[0])
@dbus.service.method(dbus_interface=PLAYLISTS_IFACE)
def GetPlaylists(self, index, max_count, order, reverse):
logger.debug(
'%s.GetPlaylists(%r, %r, %r, %r) called',
PLAYLISTS_IFACE, index, max_count, order, reverse)
playlists = self.core.playlists.playlists.get()
if order == 'Alphabetical':
playlists.sort(key=lambda p: p.name, reverse=reverse)
elif order == 'Modified':
playlists.sort(key=lambda p: p.last_modified, reverse=reverse)
elif order == 'User' and reverse:
playlists.reverse()
slice_end = index + max_count
playlists = playlists[index:slice_end]
results = [
(self.get_playlist_id(p.uri), p.name, '')
for p in playlists]
return dbus.Array(results, signature='(oss)')
### Playlists interface signals
@dbus.service.signal(dbus_interface=PLAYLISTS_IFACE, signature='(oss)')
def PlaylistChanged(self, playlist):
logger.debug('%s.PlaylistChanged signaled', PLAYLISTS_IFACE)
# Do nothing, as just calling the method is enough to emit the signal.
### Playlists interface properties
def get_PlaylistCount(self):
return len(self.core.playlists.playlists.get())
def get_Orderings(self):
return [
'Alphabetical', # Order by playlist.name
'Modified', # Order by playlist.last_modified
'User', # Don't change order
]
def get_ActivePlaylist(self):
playlist_is_valid = False
playlist = ('/', 'None', '')
return (playlist_is_valid, playlist)

View File

@ -1,7 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import namedtuple
class ImmutableObject(object): class ImmutableObject(object):
""" """
@ -151,9 +149,6 @@ class Album(ImmutableObject):
super(Album, self).__init__(*args, **kwargs) super(Album, self).__init__(*args, **kwargs)
TlTrack = namedtuple('TlTrack', ['tlid', 'track'])
class Track(ImmutableObject): class Track(ImmutableObject):
""" """
:param uri: track URI :param uri: track URI
@ -208,6 +203,44 @@ class Track(ImmutableObject):
super(Track, self).__init__(*args, **kwargs) super(Track, self).__init__(*args, **kwargs)
class TlTrack(ImmutableObject):
"""
A tracklist track. Wraps a regular track and it's tracklist ID.
The use of :class:`TlTrack` allows the same track to appear multiple times
in the tracklist.
This class also accepts it's parameters as positional arguments. Both
arguments must be provided, and they must appear in the order they are
listed here.
This class also supports iteration, so your extract its values like this::
(tlid, track) = tl_track
:param tlid: tracklist ID
:type tlid: int
:param track: the track
:type track: :class:`Track`
"""
#: The tracklist ID. Read-only.
tlid = None
#: The track. Read-only.
track = None
def __init__(self, *args, **kwargs):
if len(args) == 2 and len(kwargs) == 0:
kwargs['tlid'] = args[0]
kwargs['track'] = args[1]
args = []
super(TlTrack, self).__init__(*args, **kwargs)
def __iter__(self):
return iter([self.tlid, self.track])
class Playlist(ImmutableObject): class Playlist(ImmutableObject):
""" """
:param uri: playlist URI :param uri: playlist URI

View File

@ -1,5 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging
import datetime
import gobject import gobject
gobject.threads_init() gobject.threads_init()
@ -7,10 +10,40 @@ import pygst
pygst.require('0.10') pygst.require('0.10')
import gst import gst
import datetime from mopidy import settings
from mopidy.frontends.mpd import translator as mpd_translator
from mopidy.utils.path import path_to_uri, find_files
from mopidy.models import Track, Artist, Album from mopidy.models import Track, Artist, Album
from mopidy.utils import log, path
def main():
log.setup_root_logger()
log.setup_console_logging(2)
tracks = []
def store(data):
track = translator(data)
tracks.append(track)
logging.debug('Added %s', track.uri)
def debug(uri, error, debug):
logging.error('Failed %s: %s - %s', uri, error, debug)
logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH)
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
try:
scanner.start()
except KeyboardInterrupt:
scanner.stop()
logging.info('Done')
for row in mpd_translator.tracks_to_tag_cache_format(tracks):
if len(row) == 1:
print ('%s' % row).encode('utf-8')
else:
print ('%s: %s' % row).encode('utf-8')
def translator(data): def translator(data):
@ -56,51 +89,70 @@ def translator(data):
class Scanner(object): class Scanner(object):
def __init__(self, folder, data_callback, error_callback=None): def __init__(self, folder, data_callback, error_callback=None):
self.files = find_files(folder) self.data = {}
self.files = path.find_files(folder)
self.data_callback = data_callback self.data_callback = data_callback
self.error_callback = error_callback self.error_callback = error_callback
self.loop = gobject.MainLoop() self.loop = gobject.MainLoop()
fakesink = gst.element_factory_make('fakesink') self.fakesink = gst.element_factory_make('fakesink')
self.fakesink.set_property('signal-handoffs', True)
self.fakesink.connect('handoff', self.process_handoff)
self.uribin = gst.element_factory_make('uridecodebin') self.uribin = gst.element_factory_make('uridecodebin')
self.uribin.set_property('caps', gst.Caps(b'audio/x-raw-int')) self.uribin.set_property('caps', gst.Caps(b'audio/x-raw-int'))
self.uribin.connect( self.uribin.connect('pad-added', self.process_new_pad)
'pad-added', self.process_new_pad, fakesink.get_pad('sink'))
self.pipe = gst.element_factory_make('pipeline') self.pipe = gst.element_factory_make('pipeline')
self.pipe.add(self.uribin) self.pipe.add(self.uribin)
self.pipe.add(fakesink) self.pipe.add(self.fakesink)
bus = self.pipe.get_bus() bus = self.pipe.get_bus()
bus.add_signal_watch() bus.add_signal_watch()
bus.connect('message::application', self.process_application)
bus.connect('message::tag', self.process_tags) bus.connect('message::tag', self.process_tags)
bus.connect('message::error', self.process_error) bus.connect('message::error', self.process_error)
def process_new_pad(self, source, pad, target_pad): def process_handoff(self, fakesink, buffer_, pad):
pad.link(target_pad) # When this function is called the first buffer has reached the end of
# the pipeline, and we can continue with the next track. Since we're
# in another thread, we send a message back to the main thread using
# the bus.
structure = gst.Structure('handoff')
message = gst.message_new_application(fakesink, structure)
bus = self.pipe.get_bus()
bus.post(message)
def process_new_pad(self, source, pad):
pad.link(self.fakesink.get_pad('sink'))
def process_application(self, bus, message):
if message.src != self.fakesink:
return
if message.structure.get_name() != 'handoff':
return
self.data['uri'] = unicode(self.uribin.get_property('uri'))
self.data[gst.TAG_DURATION] = self.get_duration()
try:
self.data_callback(self.data)
self.next_uri()
except KeyboardInterrupt:
self.stop()
def process_tags(self, bus, message): def process_tags(self, bus, message):
taglist = message.parse_tag() taglist = message.parse_tag()
data = {
'uri': unicode(self.uribin.get_property('uri')),
gst.TAG_DURATION: self.get_duration(),
}
for key in taglist.keys(): for key in taglist.keys():
# XXX: For some crazy reason some wma files spit out lists here, # XXX: For some crazy reason some wma files spit out lists here,
# not sure if this is due to better data in headers or wma being # not sure if this is due to better data in headers or wma being
# stupid. So ugly hack for now :/ # stupid. So ugly hack for now :/
if type(taglist[key]) is list: if type(taglist[key]) is list:
data[key] = taglist[key][0] self.data[key] = taglist[key][0]
else: else:
data[key] = taglist[key] self.data[key] = taglist[key]
try:
self.data_callback(data)
self.next_uri()
except KeyboardInterrupt:
self.stop()
def process_error(self, bus, message): def process_error(self, bus, message):
if self.error_callback: if self.error_callback:
@ -118,14 +170,15 @@ class Scanner(object):
return None return None
def next_uri(self): def next_uri(self):
self.data = {}
try: try:
uri = path_to_uri(self.files.next()) uri = path.path_to_uri(self.files.next())
except StopIteration: except StopIteration:
self.stop() self.stop()
return False return False
self.pipe.set_state(gst.STATE_NULL) self.pipe.set_state(gst.STATE_NULL)
self.uribin.set_property('uri', uri) self.uribin.set_property('uri', uri)
self.pipe.set_state(gst.STATE_PAUSED) self.pipe.set_state(gst.STATE_PLAYING)
return True return True
def start(self): def start(self):

View File

@ -135,15 +135,9 @@ def _gstreamer_check_elements():
def pykka_info(): def pykka_info():
if hasattr(pykka, '__version__'):
# Pykka >= 0.14
version = pykka.__version__
else:
# Pykka < 0.14
version = pykka.get_version()
return { return {
'name': 'Pykka', 'name': 'Pykka',
'version': version, 'version': pykka.__version__,
'path': pykka.__file__, 'path': pykka.__file__,
} }

View File

@ -140,7 +140,7 @@ class Connection(object):
self.timeout = timeout self.timeout = timeout
self.send_lock = threading.Lock() self.send_lock = threading.Lock()
self.send_buffer = '' self.send_buffer = b''
self.stopping = False self.stopping = False
@ -193,7 +193,7 @@ class Connection(object):
if e.errno in (errno.EWOULDBLOCK, errno.EINTR): if e.errno in (errno.EWOULDBLOCK, errno.EINTR):
return data return data
self.stop('Unexpected client error: %s' % e) self.stop('Unexpected client error: %s' % e)
return '' return b''
def enable_timeout(self): def enable_timeout(self):
"""Reactivate timeout mechanism.""" """Reactivate timeout mechanism."""

0
tests/audio/__init__.py Normal file
View File

View File

@ -1,5 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
from mopidy import audio, settings from mopidy import audio, settings
from mopidy.utils.path import path_to_uri from mopidy.utils.path import path_to_uri
@ -63,3 +67,48 @@ class AudioTest(unittest.TestCase):
@unittest.SkipTest @unittest.SkipTest
def test_invalid_output_raises_error(self): def test_invalid_output_raises_error(self):
pass # TODO pass # TODO
class AudioStateTest(unittest.TestCase):
def setUp(self):
self.audio = audio.Audio()
def test_state_starts_as_stopped(self):
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
def test_state_does_not_change_when_in_gst_ready_state(self):
self.audio._on_playbin_state_changed(
gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
def test_state_changes_from_stopped_to_playing_on_play(self):
self.audio._on_playbin_state_changed(
gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING)
self.audio._on_playbin_state_changed(
gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING)
self.audio._on_playbin_state_changed(
gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state)
def test_state_changes_from_playing_to_paused_on_pause(self):
self.audio.state = audio.PlaybackState.PLAYING
self.audio._on_playbin_state_changed(
gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state)
def test_state_changes_from_playing_to_stopped_on_stop(self):
self.audio.state = audio.PlaybackState.PLAYING
self.audio._on_playbin_state_changed(
gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL)
self.audio._on_playbin_state_changed(
gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL)
# We never get the following call, so the logic must work without it
#self.audio._on_playbin_state_changed(
# gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)

View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
from mopidy import audio
from tests import unittest
class AudioListenerTest(unittest.TestCase):
def setUp(self):
self.listener = audio.AudioListener()
def test_listener_has_default_impl_for_reached_end_of_stream(self):
self.listener.reached_end_of_stream()
def test_listener_has_default_impl_for_state_changed(self):
self.listener.state_changed(None, None)

View File

@ -0,0 +1,23 @@
from __future__ import unicode_literals
import mock
import pykka
from mopidy import core, audio
from mopidy.backends import listener
@mock.patch.object(listener.BackendListener, 'send')
class BackendEventsTest(object):
def setUp(self):
self.audio = mock.Mock(spec=audio.Audio)
self.backend = self.backend_class.start(audio=audio).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
send.reset_mock()
self.core.playlists.refresh().get()
self.assertEqual(send.call_args[0][0], 'playlists_loaded')

View File

@ -88,7 +88,7 @@ class TracklistControllerTest(object):
def test_get_by_uri_returns_unique_match(self): def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a') track = Track(uri='a')
self.controller.append([Track(uri='z'), track, Track(uri='y')]) self.controller.append([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.get(uri='a')[1]) self.assertEqual(track, self.controller.get(uri='a').track)
def test_get_by_uri_raises_error_if_multiple_matches(self): def test_get_by_uri_raises_error_if_multiple_matches(self):
track = Track(uri='a') track = Track(uri='a')
@ -113,16 +113,16 @@ class TracklistControllerTest(object):
track2 = Track(uri='b', name='x') track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y') track3 = Track(uri='b', name='y')
self.controller.append([track1, track2, track3]) self.controller.append([track1, track2, track3])
self.assertEqual(track1, self.controller.get(uri='a', name='x')[1]) self.assertEqual(track1, self.controller.get(uri='a', name='x').track)
self.assertEqual(track2, self.controller.get(uri='b', name='x')[1]) self.assertEqual(track2, self.controller.get(uri='b', name='x').track)
self.assertEqual(track3, self.controller.get(uri='b', name='y')[1]) self.assertEqual(track3, self.controller.get(uri='b', name='y').track)
def test_get_by_criteria_that_is_not_present_in_all_elements(self): def test_get_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track() track1 = Track()
track2 = Track(uri='b') track2 = Track(uri='b')
track3 = Track() track3 = Track()
self.controller.append([track1, track2, track3]) self.controller.append([track1, track2, track3])
self.assertEqual(track2, self.controller.get(uri='b')[1]) self.assertEqual(track2, self.controller.get(uri='b').track)
def test_append_appends_to_the_tracklist(self): def test_append_appends_to_the_tracklist(self):
self.controller.append([Track(uri='a'), Track(uri='b')]) self.controller.append([Track(uri='a'), Track(uri='b')])
@ -153,10 +153,13 @@ class TracklistControllerTest(object):
self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
@populate_playlist
def test_append_returns_the_tl_tracks_that_was_added(self):
tl_tracks = self.controller.append(self.controller.tracks[1:2])
self.assertEqual(tl_tracks[0].track, self.controller.tracks[1])
def test_index_returns_index_of_track(self): def test_index_returns_index_of_track(self):
tl_tracks = [] tl_tracks = self.controller.append(self.tracks)
for track in self.tracks:
tl_tracks.append(self.controller.add(track))
self.assertEquals(0, self.controller.index(tl_tracks[0])) self.assertEquals(0, self.controller.index(tl_tracks[0]))
self.assertEquals(1, self.controller.index(tl_tracks[1])) self.assertEquals(1, self.controller.index(tl_tracks[1]))
self.assertEquals(2, self.controller.index(tl_tracks[2])) self.assertEquals(2, self.controller.index(tl_tracks[2]))

View File

@ -1,56 +0,0 @@
from __future__ import unicode_literals
import mock
import pykka
from mopidy import audio, core
from mopidy.backends import dummy
from mopidy.models import Track
from tests import unittest
@mock.patch.object(core.CoreListener, 'send')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.audio = mock.Mock(spec=audio.Audio)
self.backend = dummy.DummyBackend.start(audio=audio).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_pause_sends_track_playback_paused_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
self.core.playback.play().get()
send.reset_mock()
self.core.playback.pause().get()
self.assertEqual(send.call_args[0][0], 'track_playback_paused')
def test_resume_sends_track_playback_resumed(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
self.core.playback.play()
self.core.playback.pause().get()
send.reset_mock()
self.core.playback.resume().get()
self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
def test_play_sends_track_playback_started_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
send.reset_mock()
self.core.playback.play().get()
self.assertEqual(send.call_args[0][0], 'track_playback_started')
def test_stop_sends_track_playback_ended_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
self.core.playback.play().get()
send.reset_mock()
self.core.playback.stop().get()
self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
def test_seek_sends_seeked_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a', length=40000))
self.core.playback.play().get()
send.reset_mock()
self.core.playback.seek(1000).get()
self.assertEqual(send.call_args[0][0], 'seeked')

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from mopidy.backends.listener import BackendListener
from tests import unittest
class CoreListenerTest(unittest.TestCase):
def setUp(self):
self.listener = BackendListener()
def test_listener_has_default_impl_for_playlists_loaded(self):
self.listener.playlists_loaded()

View File

@ -0,0 +1,8 @@
from mopidy.backends.local import LocalBackend
from tests import unittest
from tests.backends.base import events
class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase):
backend_class = LocalBackend

124
tests/core/events_test.py Normal file
View File

@ -0,0 +1,124 @@
from __future__ import unicode_literals
import mock
import pykka
from mopidy import audio, core
from mopidy.backends import dummy
from mopidy.models import Track
from tests import unittest
@mock.patch.object(core.CoreListener, 'send')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.audio = mock.Mock(spec=audio.Audio)
self.backend = dummy.DummyBackend.start(audio=audio).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_backends_playlists_loaded_forwards_event_to_frontends(self, send):
send.reset_mock()
self.core.playlists_loaded().get()
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
def test_pause_sends_track_playback_paused_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
self.core.playback.play().get()
send.reset_mock()
self.core.playback.pause().get()
self.assertEqual(send.call_args[0][0], 'track_playback_paused')
def test_resume_sends_track_playback_resumed(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
self.core.playback.play()
self.core.playback.pause().get()
send.reset_mock()
self.core.playback.resume().get()
self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
def test_play_sends_track_playback_started_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
send.reset_mock()
self.core.playback.play().get()
self.assertEqual(send.call_args[0][0], 'track_playback_started')
def test_stop_sends_track_playback_ended_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a'))
self.core.playback.play().get()
send.reset_mock()
self.core.playback.stop().get()
self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
def test_seek_sends_seeked_event(self, send):
self.core.tracklist.add(Track(uri='dummy:a', length=40000))
self.core.playback.play().get()
send.reset_mock()
self.core.playback.seek(1000).get()
self.assertEqual(send.call_args[0][0], 'seeked')
def test_tracklist_add_sends_tracklist_changed_event(self, send):
send.reset_mock()
self.core.tracklist.add(Track(uri='dummy:a')).get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
def test_tracklist_append_sends_tracklist_changed_event(self, send):
send.reset_mock()
self.core.tracklist.append([Track(uri='dummy:a')]).get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
def test_tracklist_clear_sends_tracklist_changed_event(self, send):
self.core.tracklist.append([Track(uri='dummy:a')]).get()
send.reset_mock()
self.core.tracklist.clear().get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
def test_tracklist_move_sends_tracklist_changed_event(self, send):
self.core.tracklist.append(
[Track(uri='dummy:a'), Track(uri='dummy:b')]).get()
send.reset_mock()
self.core.tracklist.move(0, 1, 1).get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
def test_tracklist_remove_sends_tracklist_changed_event(self, send):
self.core.tracklist.append([Track(uri='dummy:a')]).get()
send.reset_mock()
self.core.tracklist.remove(uri='dummy:a').get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
def test_tracklist_shuffle_sends_tracklist_changed_event(self, send):
self.core.tracklist.append(
[Track(uri='dummy:a'), Track(uri='dummy:b')]).get()
send.reset_mock()
self.core.tracklist.shuffle().get()
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
send.reset_mock()
self.core.playlists.refresh().get()
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
def test_playlists_refresh_uri_sends_playlists_loaded_event(self, send):
send.reset_mock()
self.core.playlists.refresh(uri_scheme='dummy').get()
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
def test_playlists_create_sends_playlist_changed_event(self, send):
send.reset_mock()
self.core.playlists.create('foo').get()
self.assertEqual(send.call_args[0][0], 'playlist_changed')
@unittest.SkipTest
def test_playlists_delete_sends_playlist_deleted_event(self, send):
# TODO We should probably add a playlist_deleted event
pass
def test_playlists_save_sends_playlist_changed_event(self, send):
playlist = self.core.playlists.create('foo').get()
send.reset_mock()
playlist = playlist.copy(name='bar')
self.core.playlists.save(playlist).get()
self.assertEqual(send.call_args[0][0], 'playlist_changed')

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mopidy.core import CoreListener, PlaybackState from mopidy.core import CoreListener, PlaybackState
from mopidy.models import Track from mopidy.models import Playlist, Track
from tests import unittest from tests import unittest
@ -26,8 +26,14 @@ class CoreListenerTest(unittest.TestCase):
self.listener.playback_state_changed( self.listener.playback_state_changed(
PlaybackState.STOPPED, PlaybackState.PLAYING) PlaybackState.STOPPED, PlaybackState.PLAYING)
def test_listener_has_default_impl_for_tracklist_changed(self):
self.listener.tracklist_changed()
def test_listener_has_default_impl_for_playlists_loaded(self):
self.listener.playlists_loaded()
def test_listener_has_default_impl_for_playlist_changed(self): def test_listener_has_default_impl_for_playlist_changed(self):
self.listener.playlist_changed() self.listener.playlist_changed(Playlist())
def test_listener_has_default_impl_for_options_changed(self): def test_listener_has_default_impl_for_options_changed(self):
self.listener.options_changed() self.listener.options_changed()

View File

@ -41,7 +41,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.assertEqual(len(self.core.tracklist.tracks.get()), 6) self.assertEqual(len(self.core.tracklist.tracks.get()), 6)
self.assertEqual(self.core.tracklist.tracks.get()[5], needle) self.assertEqual(self.core.tracklist.tracks.get()[5], needle)
self.assertInResponse( self.assertInResponse(
'Id: %d' % self.core.tracklist.tl_tracks.get()[5][0]) 'Id: %d' % self.core.tracklist.tl_tracks.get()[5].tlid)
self.assertInResponse('OK') self.assertInResponse('OK')
def test_addid_with_empty_uri_acks(self): def test_addid_with_empty_uri_acks(self):
@ -60,7 +60,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.assertEqual(len(self.core.tracklist.tracks.get()), 6) self.assertEqual(len(self.core.tracklist.tracks.get()), 6)
self.assertEqual(self.core.tracklist.tracks.get()[3], needle) self.assertEqual(self.core.tracklist.tracks.get()[3], needle)
self.assertInResponse( self.assertInResponse(
'Id: %d' % self.core.tracklist.tl_tracks.get()[3][0]) 'Id: %d' % self.core.tracklist.tl_tracks.get()[3].tlid)
self.assertInResponse('OK') self.assertInResponse('OK')
def test_addid_with_songpos_out_of_bounds_should_ack(self): def test_addid_with_songpos_out_of_bounds_should_ack(self):
@ -94,7 +94,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
self.sendRequest( self.sendRequest(
'delete "%d"' % self.core.tracklist.tl_tracks.get()[2][0]) 'delete "%d"' % self.core.tracklist.tl_tracks.get()[2].tlid)
self.assertEqual(len(self.core.tracklist.tracks.get()), 4) self.assertEqual(len(self.core.tracklist.tracks.get()), 4)
self.assertInResponse('OK') self.assertInResponse('OK')
@ -424,11 +424,11 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
self.sendRequest('plchangesposid "0"') self.sendRequest('plchangesposid "0"')
tl_tracks = self.core.tracklist.tl_tracks.get() tl_tracks = self.core.tracklist.tl_tracks.get()
self.assertInResponse('cpos: 0') self.assertInResponse('cpos: 0')
self.assertInResponse('Id: %d' % tl_tracks[0][0]) self.assertInResponse('Id: %d' % tl_tracks[0].tlid)
self.assertInResponse('cpos: 2') self.assertInResponse('cpos: 2')
self.assertInResponse('Id: %d' % tl_tracks[1][0]) self.assertInResponse('Id: %d' % tl_tracks[1].tlid)
self.assertInResponse('cpos: 2') self.assertInResponse('cpos: 2')
self.assertInResponse('Id: %d' % tl_tracks[2][0]) self.assertInResponse('Id: %d' % tl_tracks[2].tlid)
self.assertInResponse('OK') self.assertInResponse('OK')
def test_shuffle_without_range(self): def test_shuffle_without_range(self):

View File

@ -5,7 +5,7 @@ import sys
import mock import mock
from mopidy.exceptions import OptionalDependencyError from mopidy.exceptions import OptionalDependencyError
from mopidy.models import Track from mopidy.models import Playlist, Track
try: try:
from mopidy.frontends.mpris import MprisFrontend, objects from mopidy.frontends.mpris import MprisFrontend, objects
@ -75,3 +75,19 @@ class BackendEventsTest(unittest.TestCase):
def test_seeked_event_causes_mpris_seeked_event(self): def test_seeked_event_causes_mpris_seeked_event(self):
self.mpris_frontend.seeked(31000) self.mpris_frontend.seeked(31000)
self.mpris_object.Seeked.assert_called_with(31000000) self.mpris_object.Seeked.assert_called_with(31000000)
def test_playlists_loaded_event_changes_playlist_count(self):
self.mpris_object.Get.return_value = 17
self.mpris_frontend.playlists_loaded()
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYLISTS_IFACE, 'PlaylistCount'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYLISTS_IFACE, {'PlaylistCount': 17}, [])
def test_playlist_changed_event_causes_mpris_playlist_changed_event(self):
self.mpris_object.get_playlist_id.return_value = 'id-for-dummy:foo'
playlist = Playlist(uri='dummy:foo', name='foo')
self.mpris_frontend.playlist_changed(playlist)
self.mpris_object.PlaylistChanged.assert_called_with(
('id-for-dummy:foo', 'foo', ''))

View File

@ -0,0 +1,171 @@
from __future__ import unicode_literals
import datetime
import sys
import mock
import pykka
from mopidy import core, exceptions
from mopidy.audio import PlaybackState
from mopidy.backends import dummy
from mopidy.models import Track
try:
from mopidy.frontends.mpris import objects
except exceptions.OptionalDependencyError:
pass
from tests import unittest
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class PlayerInterfaceTest(unittest.TestCase):
def setUp(self):
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.DummyBackend.start(audio=None).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
self.mpris = objects.MprisObject(core=self.core)
foo = self.core.playlists.create('foo').get()
foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0))
foo = self.core.playlists.save(foo).get()
bar = self.core.playlists.create('bar').get()
bar = bar.copy(last_modified=datetime.datetime(2012, 2, 1, 6, 0, 0))
bar = self.core.playlists.save(bar).get()
baz = self.core.playlists.create('baz').get()
baz = baz.copy(last_modified=datetime.datetime(2012, 1, 1, 6, 0, 0))
baz = self.core.playlists.save(baz).get()
self.playlist = baz
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_activate_playlist_appends_tracks_to_tracklist(self):
self.core.tracklist.append([
Track(uri='dummy:old-a'),
Track(uri='dummy:old-b'),
])
self.playlist = self.playlist.copy(tracks=[
Track(uri='dummy:baz-a'),
Track(uri='dummy:baz-b'),
Track(uri='dummy:baz-c'),
])
self.playlist = self.core.playlists.save(self.playlist).get()
self.assertEqual(2, self.core.tracklist.length.get())
playlists = self.mpris.GetPlaylists(0, 100, 'User', False)
playlist_id = playlists[2][0]
self.mpris.ActivatePlaylist(playlist_id)
self.assertEqual(5, self.core.tracklist.length.get())
self.assertEqual(
PlaybackState.PLAYING, self.core.playback.state.get())
self.assertEqual(
self.playlist.tracks[0], self.core.playback.current_track.get())
def test_activate_empty_playlist_is_harmless(self):
self.assertEqual(0, self.core.tracklist.length.get())
playlists = self.mpris.GetPlaylists(0, 100, 'User', False)
playlist_id = playlists[2][0]
self.mpris.ActivatePlaylist(playlist_id)
self.assertEqual(0, self.core.tracklist.length.get())
self.assertEqual(
PlaybackState.STOPPED, self.core.playback.state.get())
self.assertIsNone(self.core.playback.current_track.get())
def test_get_playlists_in_alphabetical_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', False)
self.assertEqual(3, len(result))
self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC4Q_', result[0][0])
self.assertEqual('bar', result[0][1])
self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC6Q_', result[1][0])
self.assertEqual('baz', result[1][1])
self.assertEqual('/com/mopidy/playlist/MR2W23LZHJTG63Y_', result[2][0])
self.assertEqual('foo', result[2][1])
def test_get_playlists_in_reverse_alphabetical_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', True)
self.assertEqual(3, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('baz', result[1][1])
self.assertEqual('bar', result[2][1])
def test_get_playlists_in_modified_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Modified', False)
self.assertEqual(3, len(result))
self.assertEqual('baz', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('foo', result[2][1])
def test_get_playlists_in_reverse_modified_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Modified', True)
self.assertEqual(3, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('baz', result[2][1])
def test_get_playlists_in_user_order(self):
result = self.mpris.GetPlaylists(0, 100, 'User', False)
self.assertEqual(3, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('baz', result[2][1])
def test_get_playlists_in_reverse_user_order(self):
result = self.mpris.GetPlaylists(0, 100, 'User', True)
self.assertEqual(3, len(result))
self.assertEqual('baz', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('foo', result[2][1])
def test_get_playlists_slice_on_start_of_list(self):
result = self.mpris.GetPlaylists(0, 2, 'User', False)
self.assertEqual(2, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('bar', result[1][1])
def test_get_playlists_slice_later_in_list(self):
result = self.mpris.GetPlaylists(2, 2, 'User', False)
self.assertEqual(1, len(result))
self.assertEqual('baz', result[0][1])
def test_get_playlist_count_returns_number_of_playlists(self):
result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'PlaylistCount')
self.assertEqual(3, result)
def test_get_orderings_includes_alpha_modified_and_user(self):
result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'Orderings')
self.assertIn('Alphabetical', result)
self.assertNotIn('Created', result)
self.assertIn('Modified', result)
self.assertNotIn('Played', result)
self.assertIn('User', result)
def test_get_active_playlist_does_not_return_a_playlist(self):
result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'ActivePlaylist')
valid, playlist = result
playlist_id, playlist_name, playlist_icon_uri = playlist
self.assertEqual(False, valid)
self.assertEqual('/', playlist_id)
self.assertEqual('None', playlist_name)
self.assertEqual('', playlist_icon_uri)

View File

@ -31,6 +31,18 @@ class RootInterfaceTest(unittest.TestCase):
def test_constructor_connects_to_dbus(self): def test_constructor_connects_to_dbus(self):
self.assert_(self.mpris._connect_to_dbus.called) self.assert_(self.mpris._connect_to_dbus.called)
def test_fullscreen_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'Fullscreen')
self.assertFalse(result)
def test_setting_fullscreen_fails_and_returns_none(self):
result = self.mpris.Set(objects.ROOT_IFACE, 'Fullscreen', 'True')
self.assertIsNone(result)
def test_can_set_fullscreen_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanSetFullscreen')
self.assertFalse(result)
def test_can_raise_returns_false(self): def test_can_raise_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise') result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise')
self.assertFalse(result) self.assertFalse(result)
@ -64,7 +76,7 @@ class RootInterfaceTest(unittest.TestCase):
self.assertEquals(result, 'foo') self.assertEquals(result, 'foo')
settings.runtime.clear() settings.runtime.clear()
def test_supported_uri_schemes_is_empty(self): def test_supported_uri_schemes_includes_backend_uri_schemes(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes') result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes')
self.assertEquals(len(result), 1) self.assertEquals(len(result), 1)
self.assertEquals(result[0], 'dummy') self.assertEquals(result[0], 'dummy')

View File

@ -314,21 +314,6 @@ class AlbumTest(unittest.TestCase):
self.assertNotEqual(hash(album1), hash(album2)) self.assertNotEqual(hash(album1), hash(album2))
class TlTrackTest(unittest.TestCase):
def setUp(self):
self.tlid = 123
self.track = Track()
self.tl_track = TlTrack(self.tlid, self.track)
def test_tl_track_can_be_accessed_as_a_tuple(self):
self.assertEqual(self.tlid, self.tl_track[0])
self.assertEqual(self.track, self.tl_track[1])
def test_tl_track_can_be_accessed_by_attribute_names(self):
self.assertEqual(self.tlid, self.tl_track.tlid)
self.assertEqual(self.track, self.tl_track.track)
class TrackTest(unittest.TestCase): class TrackTest(unittest.TestCase):
def test_uri(self): def test_uri(self):
uri = 'an_uri' uri = 'an_uri'
@ -567,6 +552,75 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(hash(track1), hash(track2)) self.assertNotEqual(hash(track1), hash(track2))
class TlTrackTest(unittest.TestCase):
def test_tlid(self):
tlid = 123
tl_track = TlTrack(tlid=tlid)
self.assertEqual(tl_track.tlid, tlid)
self.assertRaises(AttributeError, setattr, tl_track, 'tlid', None)
def test_track(self):
track = Track()
tl_track = TlTrack(track=track)
self.assertEqual(tl_track.track, track)
self.assertRaises(AttributeError, setattr, tl_track, 'track', None)
def test_invalid_kwarg(self):
test = lambda: TlTrack(foo='baz')
self.assertRaises(TypeError, test)
def test_positional_args(self):
tlid = 123
track = Track()
tl_track = TlTrack(tlid, track)
self.assertEqual(tl_track.tlid, tlid)
self.assertEqual(tl_track.track, track)
def test_iteration(self):
tlid = 123
track = Track()
tl_track = TlTrack(tlid, track)
(tlid2, track2) = tl_track
self.assertEqual(tlid2, tlid)
self.assertEqual(track2, track)
def test_repr(self):
self.assertEquals(
"TlTrack(tlid=123, track=Track(artists=[], uri=u'uri'))",
repr(TlTrack(tlid=123, track=Track(uri='uri'))))
def test_serialize(self):
self.assertDictEqual(
{'tlid': 123, 'track': {'uri': 'uri', 'name': 'name'}},
TlTrack(tlid=123, track=Track(uri='uri', name='name')).serialize())
def test_eq(self):
tlid = 123
track = Track()
tl_track1 = TlTrack(tlid=tlid, track=track)
tl_track2 = TlTrack(tlid=tlid, track=track)
self.assertEqual(tl_track1, tl_track2)
self.assertEqual(hash(tl_track1), hash(tl_track2))
def test_eq_none(self):
self.assertNotEqual(TlTrack(), None)
def test_eq_other(self):
self.assertNotEqual(TlTrack(), 'other')
def test_ne_tlid(self):
tl_track1 = TlTrack(tlid=123)
tl_track2 = TlTrack(tlid=321)
self.assertNotEqual(tl_track1, tl_track2)
self.assertNotEqual(hash(tl_track1), hash(tl_track2))
def test_ne_track(self):
tl_track1 = TlTrack(track=Track(uri='a'))
tl_track2 = TlTrack(track=Track(uri='b'))
self.assertNotEqual(tl_track1, tl_track2)
self.assertNotEqual(hash(tl_track1), hash(tl_track2))
class PlaylistTest(unittest.TestCase): class PlaylistTest(unittest.TestCase):
def test_uri(self): def test_uri(self):
uri = 'an_uri' uri = 'an_uri'