Move backend controllers to mopidy.core
This commit is contained in:
parent
0a0c7c59b7
commit
0559213da3
@ -1,12 +1,7 @@
|
|||||||
import logging
|
from .library import BaseLibraryProvider
|
||||||
|
from .playback import BasePlaybackProvider
|
||||||
|
from .stored_playlists import BaseStoredPlaylistsProvider
|
||||||
|
|
||||||
from .current_playlist import CurrentPlaylistController
|
|
||||||
from .library import LibraryController, BaseLibraryProvider
|
|
||||||
from .playback import PlaybackController, BasePlaybackProvider
|
|
||||||
from .stored_playlists import (StoredPlaylistsController,
|
|
||||||
BaseStoredPlaylistsProvider)
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
|
||||||
|
|
||||||
class Backend(object):
|
class Backend(object):
|
||||||
#: The current playlist controller. An instance of
|
#: The current playlist controller. An instance of
|
||||||
|
|||||||
@ -1,79 +1,3 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
|
||||||
|
|
||||||
class LibraryController(object):
|
|
||||||
"""
|
|
||||||
:param backend: backend the controller is a part of
|
|
||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
|
||||||
:param provider: provider the controller should use
|
|
||||||
:type provider: instance of :class:`BaseLibraryProvider`
|
|
||||||
"""
|
|
||||||
|
|
||||||
pykka_traversable = True
|
|
||||||
|
|
||||||
def __init__(self, backend, provider):
|
|
||||||
self.backend = backend
|
|
||||||
self.provider = provider
|
|
||||||
|
|
||||||
def find_exact(self, **query):
|
|
||||||
"""
|
|
||||||
Search the library for tracks where ``field`` is ``values``.
|
|
||||||
|
|
||||||
Examples::
|
|
||||||
|
|
||||||
# Returns results matching 'a'
|
|
||||||
find_exact(any=['a'])
|
|
||||||
# Returns results matching artist 'xyz'
|
|
||||||
find_exact(artist=['xyz'])
|
|
||||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
|
||||||
find_exact(any=['a', 'b'], artist=['xyz'])
|
|
||||||
|
|
||||||
:param query: one or more queries to search for
|
|
||||||
:type query: dict
|
|
||||||
:rtype: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
return self.provider.find_exact(**query)
|
|
||||||
|
|
||||||
def lookup(self, uri):
|
|
||||||
"""
|
|
||||||
Lookup track with given URI. Returns :class:`None` if not found.
|
|
||||||
|
|
||||||
:param uri: track URI
|
|
||||||
:type uri: string
|
|
||||||
:rtype: :class:`mopidy.models.Track` or :class:`None`
|
|
||||||
"""
|
|
||||||
return self.provider.lookup(uri)
|
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
|
||||||
"""
|
|
||||||
Refresh library. Limit to URI and below if an URI is given.
|
|
||||||
|
|
||||||
:param uri: directory or track URI
|
|
||||||
:type uri: string
|
|
||||||
"""
|
|
||||||
return self.provider.refresh(uri)
|
|
||||||
|
|
||||||
def search(self, **query):
|
|
||||||
"""
|
|
||||||
Search the library for tracks where ``field`` contains ``values``.
|
|
||||||
|
|
||||||
Examples::
|
|
||||||
|
|
||||||
# Returns results matching 'a'
|
|
||||||
search(any=['a'])
|
|
||||||
# Returns results matching artist 'xyz'
|
|
||||||
search(artist=['xyz'])
|
|
||||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
|
||||||
search(any=['a', 'b'], artist=['xyz'])
|
|
||||||
|
|
||||||
:param query: one or more queries to search for
|
|
||||||
:type query: dict
|
|
||||||
:rtype: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
return self.provider.search(**query)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLibraryProvider(object):
|
class BaseLibraryProvider(object):
|
||||||
"""
|
"""
|
||||||
:param backend: backend the controller is a part of
|
:param backend: backend the controller is a part of
|
||||||
|
|||||||
@ -1,550 +1,3 @@
|
|||||||
import logging
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
|
|
||||||
from mopidy.listeners import BackendListener
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
|
||||||
|
|
||||||
|
|
||||||
def option_wrapper(name, default):
|
|
||||||
def get_option(self):
|
|
||||||
return getattr(self, name, default)
|
|
||||||
def set_option(self, value):
|
|
||||||
if getattr(self, name, default) != value:
|
|
||||||
self._trigger_options_changed()
|
|
||||||
return setattr(self, name, value)
|
|
||||||
return property(get_option, set_option)
|
|
||||||
|
|
||||||
|
|
||||||
class PlaybackController(object):
|
|
||||||
"""
|
|
||||||
:param backend: the backend
|
|
||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
|
||||||
:param provider: provider the controller should use
|
|
||||||
:type provider: instance of :class:`BasePlaybackProvider`
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable = R0902
|
|
||||||
# Too many instance attributes
|
|
||||||
|
|
||||||
pykka_traversable = True
|
|
||||||
|
|
||||||
#: Constant representing the paused state.
|
|
||||||
PAUSED = u'paused'
|
|
||||||
|
|
||||||
#: Constant representing the playing state.
|
|
||||||
PLAYING = u'playing'
|
|
||||||
|
|
||||||
#: Constant representing the stopped state.
|
|
||||||
STOPPED = u'stopped'
|
|
||||||
|
|
||||||
#: :class:`True`
|
|
||||||
#: Tracks are removed from the playlist when they have been played.
|
|
||||||
#: :class:`False`
|
|
||||||
#: Tracks are not removed from the playlist.
|
|
||||||
consume = option_wrapper('_consume', False)
|
|
||||||
|
|
||||||
#: The currently playing or selected track.
|
|
||||||
#:
|
|
||||||
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
|
|
||||||
#: :class:`None`.
|
|
||||||
current_cp_track = None
|
|
||||||
|
|
||||||
#: :class:`True`
|
|
||||||
#: Tracks are selected at random from the playlist.
|
|
||||||
#: :class:`False`
|
|
||||||
#: Tracks are played in the order of the playlist.
|
|
||||||
random = option_wrapper('_random', False)
|
|
||||||
|
|
||||||
#: :class:`True`
|
|
||||||
#: The current playlist is played repeatedly. To repeat a single track,
|
|
||||||
#: select both :attr:`repeat` and :attr:`single`.
|
|
||||||
#: :class:`False`
|
|
||||||
#: The current playlist is played once.
|
|
||||||
repeat = option_wrapper('_repeat', False)
|
|
||||||
|
|
||||||
#: :class:`True`
|
|
||||||
#: Playback is stopped after current song, unless in :attr:`repeat`
|
|
||||||
#: mode.
|
|
||||||
#: :class:`False`
|
|
||||||
#: Playback continues after current song.
|
|
||||||
single = option_wrapper('_single', False)
|
|
||||||
|
|
||||||
def __init__(self, backend, provider):
|
|
||||||
self.backend = backend
|
|
||||||
self.provider = provider
|
|
||||||
self._state = self.STOPPED
|
|
||||||
self._shuffled = []
|
|
||||||
self._first_shuffle = True
|
|
||||||
self.play_time_accumulated = 0
|
|
||||||
self.play_time_started = None
|
|
||||||
|
|
||||||
def _get_cpid(self, cp_track):
|
|
||||||
if cp_track is None:
|
|
||||||
return None
|
|
||||||
return cp_track.cpid
|
|
||||||
|
|
||||||
def _get_track(self, cp_track):
|
|
||||||
if cp_track is None:
|
|
||||||
return None
|
|
||||||
return cp_track.track
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_cpid(self):
|
|
||||||
"""
|
|
||||||
The CPID (current playlist ID) of the currently playing or selected
|
|
||||||
track.
|
|
||||||
|
|
||||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
|
||||||
"""
|
|
||||||
return self._get_cpid(self.current_cp_track)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_track(self):
|
|
||||||
"""
|
|
||||||
The currently playing or selected :class:`mopidy.models.Track`.
|
|
||||||
|
|
||||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
|
||||||
"""
|
|
||||||
return self._get_track(self.current_cp_track)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_playlist_position(self):
|
|
||||||
"""
|
|
||||||
The position of the current track in the current playlist.
|
|
||||||
|
|
||||||
Read-only.
|
|
||||||
"""
|
|
||||||
if self.current_cp_track is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return self.backend.current_playlist.cp_tracks.index(
|
|
||||||
self.current_cp_track)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def track_at_eot(self):
|
|
||||||
"""
|
|
||||||
The track that will be played at the end of the current track.
|
|
||||||
|
|
||||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
|
||||||
:attr:`cp_track_at_eot` for convenience.
|
|
||||||
"""
|
|
||||||
return self._get_track(self.cp_track_at_eot)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cp_track_at_eot(self):
|
|
||||||
"""
|
|
||||||
The track that will be played at the end of the current track.
|
|
||||||
|
|
||||||
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
|
||||||
|
|
||||||
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:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.random and not self._shuffled:
|
|
||||||
if self.repeat or self._first_shuffle:
|
|
||||||
logger.debug('Shuffling tracks')
|
|
||||||
self._shuffled = cp_tracks
|
|
||||||
random.shuffle(self._shuffled)
|
|
||||||
self._first_shuffle = False
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
if self.repeat and not self.single:
|
|
||||||
return cp_tracks[
|
|
||||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
|
||||||
|
|
||||||
try:
|
|
||||||
return cp_tracks[self.current_playlist_position + 1]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def track_at_next(self):
|
|
||||||
"""
|
|
||||||
The track that will be played if calling :meth:`next()`.
|
|
||||||
|
|
||||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
|
||||||
:attr:`cp_track_at_next` for convenience.
|
|
||||||
"""
|
|
||||||
return self._get_track(self.cp_track_at_next)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cp_track_at_next(self):
|
|
||||||
"""
|
|
||||||
The track that will be played if calling :meth:`next()`.
|
|
||||||
|
|
||||||
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
|
||||||
|
|
||||||
For normal playback this is the next track in the playlist. If repeat
|
|
||||||
is enabled the next track can loop around the playlist. When random is
|
|
||||||
enabled this should be a random track, all tracks should be played once
|
|
||||||
before the list repeats.
|
|
||||||
"""
|
|
||||||
cp_tracks = self.backend.current_playlist.cp_tracks
|
|
||||||
|
|
||||||
if not cp_tracks:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.random and not self._shuffled:
|
|
||||||
if self.repeat or self._first_shuffle:
|
|
||||||
logger.debug('Shuffling tracks')
|
|
||||||
self._shuffled = cp_tracks
|
|
||||||
random.shuffle(self._shuffled)
|
|
||||||
self._first_shuffle = False
|
|
||||||
|
|
||||||
if self.random and self._shuffled:
|
|
||||||
return self._shuffled[0]
|
|
||||||
|
|
||||||
if self.current_cp_track is None:
|
|
||||||
return cp_tracks[0]
|
|
||||||
|
|
||||||
if self.repeat:
|
|
||||||
return cp_tracks[
|
|
||||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
|
||||||
|
|
||||||
try:
|
|
||||||
return cp_tracks[self.current_playlist_position + 1]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def track_at_previous(self):
|
|
||||||
"""
|
|
||||||
The track that will be played if calling :meth:`previous()`.
|
|
||||||
|
|
||||||
Read-only. A :class:`mopidy.models.Track` extracted from
|
|
||||||
:attr:`cp_track_at_previous` for convenience.
|
|
||||||
"""
|
|
||||||
return self._get_track(self.cp_track_at_previous)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cp_track_at_previous(self):
|
|
||||||
"""
|
|
||||||
The track that will be played if calling :meth:`previous()`.
|
|
||||||
|
|
||||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
|
||||||
|
|
||||||
For normal playback this is the previous track in the playlist. If
|
|
||||||
random and/or consume is enabled it should return the current track
|
|
||||||
instead.
|
|
||||||
"""
|
|
||||||
if self.repeat or self.consume or self.random:
|
|
||||||
return self.current_cp_track
|
|
||||||
|
|
||||||
if self.current_playlist_position in (None, 0):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.backend.current_playlist.cp_tracks[
|
|
||||||
self.current_playlist_position - 1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
"""
|
|
||||||
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
|
|
||||||
:attr:`STOPPED`.
|
|
||||||
|
|
||||||
Possible states and transitions:
|
|
||||||
|
|
||||||
.. digraph:: state_transitions
|
|
||||||
|
|
||||||
"STOPPED" -> "PLAYING" [ label="play" ]
|
|
||||||
"STOPPED" -> "PAUSED" [ label="pause" ]
|
|
||||||
"PLAYING" -> "STOPPED" [ label="stop" ]
|
|
||||||
"PLAYING" -> "PAUSED" [ label="pause" ]
|
|
||||||
"PLAYING" -> "PLAYING" [ label="play" ]
|
|
||||||
"PAUSED" -> "PLAYING" [ label="resume" ]
|
|
||||||
"PAUSED" -> "STOPPED" [ label="stop" ]
|
|
||||||
"""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@state.setter
|
|
||||||
def state(self, new_state):
|
|
||||||
(old_state, self._state) = (self.state, new_state)
|
|
||||||
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
|
|
||||||
|
|
||||||
self._trigger_playback_state_changed()
|
|
||||||
|
|
||||||
# FIXME play_time stuff assumes backend does not have a better way of
|
|
||||||
# handeling this stuff :/
|
|
||||||
if (old_state in (self.PLAYING, self.STOPPED)
|
|
||||||
and new_state == self.PLAYING):
|
|
||||||
self._play_time_start()
|
|
||||||
elif old_state == self.PLAYING and new_state == self.PAUSED:
|
|
||||||
self._play_time_pause()
|
|
||||||
elif old_state == self.PAUSED and new_state == self.PLAYING:
|
|
||||||
self._play_time_resume()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_position(self):
|
|
||||||
"""Time position in milliseconds."""
|
|
||||||
if self.state == self.PLAYING:
|
|
||||||
time_since_started = (self._current_wall_time -
|
|
||||||
self.play_time_started)
|
|
||||||
return self.play_time_accumulated + time_since_started
|
|
||||||
elif self.state == self.PAUSED:
|
|
||||||
return self.play_time_accumulated
|
|
||||||
elif self.state == self.STOPPED:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _play_time_start(self):
|
|
||||||
self.play_time_accumulated = 0
|
|
||||||
self.play_time_started = self._current_wall_time
|
|
||||||
|
|
||||||
def _play_time_pause(self):
|
|
||||||
time_since_started = self._current_wall_time - self.play_time_started
|
|
||||||
self.play_time_accumulated += time_since_started
|
|
||||||
|
|
||||||
def _play_time_resume(self):
|
|
||||||
self.play_time_started = self._current_wall_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _current_wall_time(self):
|
|
||||||
return int(time.time() * 1000)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def volume(self):
|
|
||||||
return self.provider.get_volume()
|
|
||||||
|
|
||||||
@volume.setter
|
|
||||||
def volume(self, volume):
|
|
||||||
self.provider.set_volume(volume)
|
|
||||||
|
|
||||||
def change_track(self, cp_track, on_error_step=1):
|
|
||||||
"""
|
|
||||||
Change to the given track, keeping the current playback state.
|
|
||||||
|
|
||||||
:param cp_track: track to change to
|
|
||||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
|
||||||
or :class:`None`
|
|
||||||
:param on_error_step: direction to step at play error, 1 for next
|
|
||||||
track (default), -1 for previous track
|
|
||||||
:type on_error_step: int, -1 or 1
|
|
||||||
|
|
||||||
"""
|
|
||||||
old_state = self.state
|
|
||||||
self.stop()
|
|
||||||
self.current_cp_track = cp_track
|
|
||||||
if old_state == self.PLAYING:
|
|
||||||
self.play(on_error_step=on_error_step)
|
|
||||||
elif old_state == self.PAUSED:
|
|
||||||
self.pause()
|
|
||||||
|
|
||||||
def on_end_of_track(self):
|
|
||||||
"""
|
|
||||||
Tell the playback controller that end of track is reached.
|
|
||||||
"""
|
|
||||||
if self.state == self.STOPPED:
|
|
||||||
return
|
|
||||||
|
|
||||||
original_cp_track = self.current_cp_track
|
|
||||||
|
|
||||||
if self.cp_track_at_eot:
|
|
||||||
self._trigger_track_playback_ended()
|
|
||||||
self.play(self.cp_track_at_eot)
|
|
||||||
else:
|
|
||||||
self.stop(clear_current_track=True)
|
|
||||||
|
|
||||||
if self.consume:
|
|
||||||
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
|
|
||||||
|
|
||||||
def on_current_playlist_change(self):
|
|
||||||
"""
|
|
||||||
Tell the playback controller that the current playlist has changed.
|
|
||||||
|
|
||||||
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
|
|
||||||
"""
|
|
||||||
self._first_shuffle = True
|
|
||||||
self._shuffled = []
|
|
||||||
|
|
||||||
if (not self.backend.current_playlist.cp_tracks or
|
|
||||||
self.current_cp_track not in
|
|
||||||
self.backend.current_playlist.cp_tracks):
|
|
||||||
self.stop(clear_current_track=True)
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
"""
|
|
||||||
Change to the next track.
|
|
||||||
|
|
||||||
The current playback state will be kept. If it was playing, playing
|
|
||||||
will continue. If it was paused, it will still be paused, etc.
|
|
||||||
"""
|
|
||||||
if self.cp_track_at_next:
|
|
||||||
self._trigger_track_playback_ended()
|
|
||||||
self.change_track(self.cp_track_at_next)
|
|
||||||
else:
|
|
||||||
self.stop(clear_current_track=True)
|
|
||||||
|
|
||||||
def pause(self):
|
|
||||||
"""Pause playback."""
|
|
||||||
if self.provider.pause():
|
|
||||||
self.state = self.PAUSED
|
|
||||||
self._trigger_track_playback_paused()
|
|
||||||
|
|
||||||
def play(self, cp_track=None, on_error_step=1):
|
|
||||||
"""
|
|
||||||
Play the given track, or if the given track is :class:`None`, play the
|
|
||||||
currently active track.
|
|
||||||
|
|
||||||
:param cp_track: track to play
|
|
||||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
|
||||||
or :class:`None`
|
|
||||||
:param on_error_step: direction to step at play error, 1 for next
|
|
||||||
track (default), -1 for previous track
|
|
||||||
:type on_error_step: int, -1 or 1
|
|
||||||
"""
|
|
||||||
|
|
||||||
if cp_track is not None:
|
|
||||||
assert cp_track in self.backend.current_playlist.cp_tracks
|
|
||||||
elif cp_track is None:
|
|
||||||
if self.state == self.PAUSED:
|
|
||||||
return self.resume()
|
|
||||||
elif self.current_cp_track is not None:
|
|
||||||
cp_track = self.current_cp_track
|
|
||||||
elif self.current_cp_track is None and on_error_step == 1:
|
|
||||||
cp_track = self.cp_track_at_next
|
|
||||||
elif self.current_cp_track is None and on_error_step == -1:
|
|
||||||
cp_track = self.cp_track_at_previous
|
|
||||||
|
|
||||||
if cp_track is not None:
|
|
||||||
self.current_cp_track = cp_track
|
|
||||||
self.state = self.PLAYING
|
|
||||||
if not self.provider.play(cp_track.track):
|
|
||||||
# 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:
|
|
||||||
self.previous()
|
|
||||||
|
|
||||||
if self.random and self.current_cp_track in self._shuffled:
|
|
||||||
self._shuffled.remove(self.current_cp_track)
|
|
||||||
|
|
||||||
self._trigger_track_playback_started()
|
|
||||||
|
|
||||||
def previous(self):
|
|
||||||
"""
|
|
||||||
Change to the previous track.
|
|
||||||
|
|
||||||
The current playback state will be kept. If it was playing, playing
|
|
||||||
will continue. If it was paused, it will still be paused, etc.
|
|
||||||
"""
|
|
||||||
self._trigger_track_playback_ended()
|
|
||||||
self.change_track(self.cp_track_at_previous, on_error_step=-1)
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
"""If paused, resume playing the current track."""
|
|
||||||
if self.state == self.PAUSED and self.provider.resume():
|
|
||||||
self.state = self.PLAYING
|
|
||||||
self._trigger_track_playback_resumed()
|
|
||||||
|
|
||||||
def seek(self, time_position):
|
|
||||||
"""
|
|
||||||
Seeks to time position given in milliseconds.
|
|
||||||
|
|
||||||
:param time_position: time position in milliseconds
|
|
||||||
:type time_position: int
|
|
||||||
:rtype: :class:`True` if successful, else :class:`False`
|
|
||||||
"""
|
|
||||||
if not self.backend.current_playlist.tracks:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.state == self.STOPPED:
|
|
||||||
self.play()
|
|
||||||
elif self.state == self.PAUSED:
|
|
||||||
self.resume()
|
|
||||||
|
|
||||||
if time_position < 0:
|
|
||||||
time_position = 0
|
|
||||||
elif time_position > self.current_track.length:
|
|
||||||
self.next()
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.play_time_started = self._current_wall_time
|
|
||||||
self.play_time_accumulated = time_position
|
|
||||||
|
|
||||||
success = self.provider.seek(time_position)
|
|
||||||
if success:
|
|
||||||
self._trigger_seeked()
|
|
||||||
return success
|
|
||||||
|
|
||||||
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:
|
|
||||||
if self.provider.stop():
|
|
||||||
self._trigger_track_playback_ended()
|
|
||||||
self.state = self.STOPPED
|
|
||||||
if clear_current_track:
|
|
||||||
self.current_cp_track = None
|
|
||||||
|
|
||||||
def _trigger_track_playback_paused(self):
|
|
||||||
logger.debug(u'Triggering track playback paused event')
|
|
||||||
if self.current_track is None:
|
|
||||||
return
|
|
||||||
BackendListener.send('track_playback_paused',
|
|
||||||
track=self.current_track,
|
|
||||||
time_position=self.time_position)
|
|
||||||
|
|
||||||
def _trigger_track_playback_resumed(self):
|
|
||||||
logger.debug(u'Triggering track playback resumed event')
|
|
||||||
if self.current_track is None:
|
|
||||||
return
|
|
||||||
BackendListener.send('track_playback_resumed',
|
|
||||||
track=self.current_track,
|
|
||||||
time_position=self.time_position)
|
|
||||||
|
|
||||||
def _trigger_track_playback_started(self):
|
|
||||||
logger.debug(u'Triggering track playback started event')
|
|
||||||
if self.current_track is None:
|
|
||||||
return
|
|
||||||
BackendListener.send('track_playback_started',
|
|
||||||
track=self.current_track)
|
|
||||||
|
|
||||||
def _trigger_track_playback_ended(self):
|
|
||||||
logger.debug(u'Triggering track playback ended event')
|
|
||||||
if self.current_track is None:
|
|
||||||
return
|
|
||||||
BackendListener.send('track_playback_ended',
|
|
||||||
track=self.current_track,
|
|
||||||
time_position=self.time_position)
|
|
||||||
|
|
||||||
def _trigger_playback_state_changed(self):
|
|
||||||
logger.debug(u'Triggering playback state change event')
|
|
||||||
BackendListener.send('playback_state_changed')
|
|
||||||
|
|
||||||
def _trigger_options_changed(self):
|
|
||||||
logger.debug(u'Triggering options changed event')
|
|
||||||
BackendListener.send('options_changed')
|
|
||||||
|
|
||||||
def _trigger_seeked(self):
|
|
||||||
logger.debug(u'Triggering seeked event')
|
|
||||||
BackendListener.send('seeked')
|
|
||||||
|
|
||||||
|
|
||||||
class BasePlaybackProvider(object):
|
class BasePlaybackProvider(object):
|
||||||
"""
|
"""
|
||||||
:param backend: the backend
|
:param backend: the backend
|
||||||
|
|||||||
@ -1,120 +1,4 @@
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
|
||||||
|
|
||||||
class StoredPlaylistsController(object):
|
|
||||||
"""
|
|
||||||
:param backend: backend the controller is a part of
|
|
||||||
:type backend: :class:`mopidy.backends.base.Backend`
|
|
||||||
:param provider: provider the controller should use
|
|
||||||
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
|
|
||||||
"""
|
|
||||||
|
|
||||||
pykka_traversable = True
|
|
||||||
|
|
||||||
def __init__(self, backend, provider):
|
|
||||||
self.backend = backend
|
|
||||||
self.provider = provider
|
|
||||||
|
|
||||||
@property
|
|
||||||
def playlists(self):
|
|
||||||
"""
|
|
||||||
Currently stored playlists.
|
|
||||||
|
|
||||||
Read/write. List of :class:`mopidy.models.Playlist`.
|
|
||||||
"""
|
|
||||||
return self.provider.playlists
|
|
||||||
|
|
||||||
@playlists.setter
|
|
||||||
def playlists(self, playlists):
|
|
||||||
self.provider.playlists = playlists
|
|
||||||
|
|
||||||
def create(self, name):
|
|
||||||
"""
|
|
||||||
Create a new playlist.
|
|
||||||
|
|
||||||
:param name: name of the new playlist
|
|
||||||
:type name: string
|
|
||||||
:rtype: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
return self.provider.create(name)
|
|
||||||
|
|
||||||
def delete(self, playlist):
|
|
||||||
"""
|
|
||||||
Delete playlist.
|
|
||||||
|
|
||||||
:param playlist: the playlist to delete
|
|
||||||
:type playlist: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
return self.provider.delete(playlist)
|
|
||||||
|
|
||||||
def get(self, **criteria):
|
|
||||||
"""
|
|
||||||
Get playlist by given criterias from the set of stored playlists.
|
|
||||||
|
|
||||||
Raises :exc:`LookupError` if a unique match is not found.
|
|
||||||
|
|
||||||
Examples::
|
|
||||||
|
|
||||||
get(name='a') # Returns track with name 'a'
|
|
||||||
get(uri='xyz') # Returns track with URI 'xyz'
|
|
||||||
get(name='a', uri='xyz') # Returns track with name 'a' and URI
|
|
||||||
# 'xyz'
|
|
||||||
|
|
||||||
:param criteria: one or more criteria to match by
|
|
||||||
:type criteria: dict
|
|
||||||
:rtype: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
matches = self.playlists
|
|
||||||
for (key, value) in criteria.iteritems():
|
|
||||||
matches = filter(lambda p: getattr(p, key) == value, matches)
|
|
||||||
if len(matches) == 1:
|
|
||||||
return matches[0]
|
|
||||||
criteria_string = ', '.join(
|
|
||||||
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
|
||||||
if len(matches) == 0:
|
|
||||||
raise LookupError('"%s" match no playlists' % criteria_string)
|
|
||||||
else:
|
|
||||||
raise LookupError('"%s" match multiple playlists' % criteria_string)
|
|
||||||
|
|
||||||
def lookup(self, uri):
|
|
||||||
"""
|
|
||||||
Lookup playlist with given URI in both the set of stored playlists and
|
|
||||||
in any other playlist sources.
|
|
||||||
|
|
||||||
:param uri: playlist URI
|
|
||||||
:type uri: string
|
|
||||||
:rtype: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
return self.provider.lookup(uri)
|
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
"""
|
|
||||||
Refresh the stored playlists in
|
|
||||||
:attr:`mopidy.backends.base.StoredPlaylistsController.playlists`.
|
|
||||||
"""
|
|
||||||
return self.provider.refresh()
|
|
||||||
|
|
||||||
def rename(self, playlist, new_name):
|
|
||||||
"""
|
|
||||||
Rename playlist.
|
|
||||||
|
|
||||||
:param playlist: the playlist
|
|
||||||
:type playlist: :class:`mopidy.models.Playlist`
|
|
||||||
:param new_name: the new name
|
|
||||||
:type new_name: string
|
|
||||||
"""
|
|
||||||
return self.provider.rename(playlist, new_name)
|
|
||||||
|
|
||||||
def save(self, playlist):
|
|
||||||
"""
|
|
||||||
Save the playlist to the set of stored playlists.
|
|
||||||
|
|
||||||
:param playlist: the playlist
|
|
||||||
:type playlist: :class:`mopidy.models.Playlist`
|
|
||||||
"""
|
|
||||||
return self.provider.save(playlist)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseStoredPlaylistsProvider(object):
|
class BaseStoredPlaylistsProvider(object):
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
from pykka.actor import ThreadingActor
|
from pykka.actor import ThreadingActor
|
||||||
|
|
||||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
from mopidy import core
|
||||||
PlaybackController, BasePlaybackProvider, LibraryController,
|
from mopidy.backends import base
|
||||||
BaseLibraryProvider, StoredPlaylistsController,
|
|
||||||
BaseStoredPlaylistsProvider)
|
|
||||||
from mopidy.models import Playlist
|
from mopidy.models import Playlist
|
||||||
|
|
||||||
|
|
||||||
class DummyBackend(ThreadingActor, Backend):
|
class DummyBackend(ThreadingActor, base.Backend):
|
||||||
"""
|
"""
|
||||||
A backend which implements the backend API in the simplest way possible.
|
A backend which implements the backend API in the simplest way possible.
|
||||||
Used in tests of the frontends.
|
Used in tests of the frontends.
|
||||||
@ -18,24 +16,24 @@ class DummyBackend(ThreadingActor, Backend):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DummyBackend, self).__init__(*args, **kwargs)
|
super(DummyBackend, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
self.current_playlist = core.CurrentPlaylistController(backend=self)
|
||||||
|
|
||||||
library_provider = DummyLibraryProvider(backend=self)
|
library_provider = DummyLibraryProvider(backend=self)
|
||||||
self.library = LibraryController(backend=self,
|
self.library = core.LibraryController(backend=self,
|
||||||
provider=library_provider)
|
provider=library_provider)
|
||||||
|
|
||||||
playback_provider = DummyPlaybackProvider(backend=self)
|
playback_provider = DummyPlaybackProvider(backend=self)
|
||||||
self.playback = PlaybackController(backend=self,
|
self.playback = core.PlaybackController(backend=self,
|
||||||
provider=playback_provider)
|
provider=playback_provider)
|
||||||
|
|
||||||
stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
|
stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
|
||||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
self.stored_playlists = core.StoredPlaylistsController(backend=self,
|
||||||
provider=stored_playlists_provider)
|
provider=stored_playlists_provider)
|
||||||
|
|
||||||
self.uri_schemes = [u'dummy']
|
self.uri_schemes = [u'dummy']
|
||||||
|
|
||||||
|
|
||||||
class DummyLibraryProvider(BaseLibraryProvider):
|
class DummyLibraryProvider(base.BaseLibraryProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||||
self.dummy_library = []
|
self.dummy_library = []
|
||||||
@ -55,7 +53,7 @@ class DummyLibraryProvider(BaseLibraryProvider):
|
|||||||
return Playlist()
|
return Playlist()
|
||||||
|
|
||||||
|
|
||||||
class DummyPlaybackProvider(BasePlaybackProvider):
|
class DummyPlaybackProvider(base.BasePlaybackProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
|
super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
|
||||||
self._volume = None
|
self._volume = None
|
||||||
@ -83,7 +81,7 @@ class DummyPlaybackProvider(BasePlaybackProvider):
|
|||||||
self._volume = volume
|
self._volume = volume
|
||||||
|
|
||||||
|
|
||||||
class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||||
def create(self, name):
|
def create(self, name):
|
||||||
playlist = Playlist(name=name)
|
playlist = Playlist(name=name)
|
||||||
self._playlists.append(playlist)
|
self._playlists.append(playlist)
|
||||||
|
|||||||
@ -7,11 +7,8 @@ import shutil
|
|||||||
from pykka.actor import ThreadingActor
|
from pykka.actor import ThreadingActor
|
||||||
from pykka.registry import ActorRegistry
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings, DATA_PATH
|
from mopidy import core, settings, DATA_PATH
|
||||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
from mopidy.backends import base
|
||||||
LibraryController, BaseLibraryProvider, PlaybackController,
|
|
||||||
BasePlaybackProvider, StoredPlaylistsController,
|
|
||||||
BaseStoredPlaylistsProvider)
|
|
||||||
from mopidy.models import Playlist, Track, Album
|
from mopidy.models import Playlist, Track, Album
|
||||||
from mopidy.gstreamer import GStreamer
|
from mopidy.gstreamer import GStreamer
|
||||||
|
|
||||||
@ -27,12 +24,10 @@ if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
|
|||||||
DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
|
DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
|
||||||
|
|
||||||
|
|
||||||
class LocalBackend(ThreadingActor, Backend):
|
class LocalBackend(ThreadingActor, base.Backend):
|
||||||
"""
|
"""
|
||||||
A backend for playing music from a local music archive.
|
A backend for playing music from a local music archive.
|
||||||
|
|
||||||
**Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local
|
|
||||||
|
|
||||||
**Dependencies:**
|
**Dependencies:**
|
||||||
|
|
||||||
- None
|
- None
|
||||||
@ -47,10 +42,10 @@ class LocalBackend(ThreadingActor, Backend):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LocalBackend, self).__init__(*args, **kwargs)
|
super(LocalBackend, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
self.current_playlist = core.CurrentPlaylistController(backend=self)
|
||||||
|
|
||||||
library_provider = LocalLibraryProvider(backend=self)
|
library_provider = LocalLibraryProvider(backend=self)
|
||||||
self.library = LibraryController(backend=self,
|
self.library = core.LibraryController(backend=self,
|
||||||
provider=library_provider)
|
provider=library_provider)
|
||||||
|
|
||||||
playback_provider = LocalPlaybackProvider(backend=self)
|
playback_provider = LocalPlaybackProvider(backend=self)
|
||||||
@ -58,7 +53,7 @@ class LocalBackend(ThreadingActor, Backend):
|
|||||||
provider=playback_provider)
|
provider=playback_provider)
|
||||||
|
|
||||||
stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
|
stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
|
||||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
self.stored_playlists = core.StoredPlaylistsController(backend=self,
|
||||||
provider=stored_playlists_provider)
|
provider=stored_playlists_provider)
|
||||||
|
|
||||||
self.uri_schemes = [u'file']
|
self.uri_schemes = [u'file']
|
||||||
@ -72,7 +67,7 @@ class LocalBackend(ThreadingActor, Backend):
|
|||||||
self.gstreamer = gstreamer_refs[0].proxy()
|
self.gstreamer = gstreamer_refs[0].proxy()
|
||||||
|
|
||||||
|
|
||||||
class LocalPlaybackController(PlaybackController):
|
class LocalPlaybackController(core.PlaybackController):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LocalPlaybackController, self).__init__(*args, **kwargs)
|
super(LocalPlaybackController, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -84,7 +79,7 @@ class LocalPlaybackController(PlaybackController):
|
|||||||
return self.backend.gstreamer.get_position().get()
|
return self.backend.gstreamer.get_position().get()
|
||||||
|
|
||||||
|
|
||||||
class LocalPlaybackProvider(BasePlaybackProvider):
|
class LocalPlaybackProvider(base.BasePlaybackProvider):
|
||||||
def pause(self):
|
def pause(self):
|
||||||
return self.backend.gstreamer.pause_playback().get()
|
return self.backend.gstreamer.pause_playback().get()
|
||||||
|
|
||||||
@ -109,7 +104,7 @@ class LocalPlaybackProvider(BasePlaybackProvider):
|
|||||||
self.backend.gstreamer.set_volume(volume).get()
|
self.backend.gstreamer.set_volume(volume).get()
|
||||||
|
|
||||||
|
|
||||||
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
|
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||||
self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH
|
self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH
|
||||||
@ -182,7 +177,7 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
|||||||
self._playlists.append(playlist)
|
self._playlists.append(playlist)
|
||||||
|
|
||||||
|
|
||||||
class LocalLibraryProvider(BaseLibraryProvider):
|
class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
||||||
self._uri_mapping = {}
|
self._uri_mapping = {}
|
||||||
|
|||||||
@ -3,16 +3,15 @@ import logging
|
|||||||
from pykka.actor import ThreadingActor
|
from pykka.actor import ThreadingActor
|
||||||
from pykka.registry import ActorRegistry
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import core, settings
|
||||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
from mopidy.backends import base
|
||||||
LibraryController, PlaybackController, StoredPlaylistsController)
|
|
||||||
from mopidy.gstreamer import GStreamer
|
from mopidy.gstreamer import GStreamer
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.spotify')
|
logger = logging.getLogger('mopidy.backends.spotify')
|
||||||
|
|
||||||
BITRATES = {96: 2, 160: 0, 320: 1}
|
BITRATES = {96: 2, 160: 0, 320: 1}
|
||||||
|
|
||||||
class SpotifyBackend(ThreadingActor, Backend):
|
class SpotifyBackend(ThreadingActor, base.Backend):
|
||||||
"""
|
"""
|
||||||
A backend for playing music from the `Spotify <http://www.spotify.com/>`_
|
A backend for playing music from the `Spotify <http://www.spotify.com/>`_
|
||||||
music streaming service. The backend uses the official `libspotify
|
music streaming service. The backend uses the official `libspotify
|
||||||
@ -51,19 +50,19 @@ class SpotifyBackend(ThreadingActor, Backend):
|
|||||||
|
|
||||||
super(SpotifyBackend, self).__init__(*args, **kwargs)
|
super(SpotifyBackend, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
self.current_playlist = core.CurrentPlaylistController(backend=self)
|
||||||
|
|
||||||
library_provider = SpotifyLibraryProvider(backend=self)
|
library_provider = SpotifyLibraryProvider(backend=self)
|
||||||
self.library = LibraryController(backend=self,
|
self.library = core.LibraryController(backend=self,
|
||||||
provider=library_provider)
|
provider=library_provider)
|
||||||
|
|
||||||
playback_provider = SpotifyPlaybackProvider(backend=self)
|
playback_provider = SpotifyPlaybackProvider(backend=self)
|
||||||
self.playback = PlaybackController(backend=self,
|
self.playback = core.PlaybackController(backend=self,
|
||||||
provider=playback_provider)
|
provider=playback_provider)
|
||||||
|
|
||||||
stored_playlists_provider = SpotifyStoredPlaylistsProvider(
|
stored_playlists_provider = SpotifyStoredPlaylistsProvider(
|
||||||
backend=self)
|
backend=self)
|
||||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
self.stored_playlists = core.StoredPlaylistsController(backend=self,
|
||||||
provider=stored_playlists_provider)
|
provider=stored_playlists_provider)
|
||||||
|
|
||||||
self.uri_schemes = [u'spotify']
|
self.uri_schemes = [u'spotify']
|
||||||
|
|||||||
4
mopidy/core/__init__.py
Normal file
4
mopidy/core/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .current_playlist import CurrentPlaylistController
|
||||||
|
from .library import LibraryController
|
||||||
|
from .playback import PlaybackController
|
||||||
|
from .stored_playlists import StoredPlaylistsController
|
||||||
@ -5,7 +5,9 @@ import random
|
|||||||
from mopidy.listeners import BackendListener
|
from mopidy.listeners import BackendListener
|
||||||
from mopidy.models import CpTrack
|
from mopidy.models import CpTrack
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.backends.base')
|
|
||||||
|
logger = logging.getLogger('mopidy.core')
|
||||||
|
|
||||||
|
|
||||||
class CurrentPlaylistController(object):
|
class CurrentPlaylistController(object):
|
||||||
"""
|
"""
|
||||||
70
mopidy/core/library.py
Normal file
70
mopidy/core/library.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
class LibraryController(object):
|
||||||
|
"""
|
||||||
|
:param backend: backend the controller is a part of
|
||||||
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
|
:param provider: provider the controller should use
|
||||||
|
:type provider: instance of :class:`BaseLibraryProvider`
|
||||||
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
|
def __init__(self, backend, provider):
|
||||||
|
self.backend = backend
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
def find_exact(self, **query):
|
||||||
|
"""
|
||||||
|
Search the library for tracks where ``field`` is ``values``.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
# Returns results matching 'a'
|
||||||
|
find_exact(any=['a'])
|
||||||
|
# Returns results matching artist 'xyz'
|
||||||
|
find_exact(artist=['xyz'])
|
||||||
|
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||||
|
find_exact(any=['a', 'b'], artist=['xyz'])
|
||||||
|
|
||||||
|
:param query: one or more queries to search for
|
||||||
|
:type query: dict
|
||||||
|
:rtype: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
return self.provider.find_exact(**query)
|
||||||
|
|
||||||
|
def lookup(self, uri):
|
||||||
|
"""
|
||||||
|
Lookup track with given URI. Returns :class:`None` if not found.
|
||||||
|
|
||||||
|
:param uri: track URI
|
||||||
|
:type uri: string
|
||||||
|
:rtype: :class:`mopidy.models.Track` or :class:`None`
|
||||||
|
"""
|
||||||
|
return self.provider.lookup(uri)
|
||||||
|
|
||||||
|
def refresh(self, uri=None):
|
||||||
|
"""
|
||||||
|
Refresh library. Limit to URI and below if an URI is given.
|
||||||
|
|
||||||
|
:param uri: directory or track URI
|
||||||
|
:type uri: string
|
||||||
|
"""
|
||||||
|
return self.provider.refresh(uri)
|
||||||
|
|
||||||
|
def search(self, **query):
|
||||||
|
"""
|
||||||
|
Search the library for tracks where ``field`` contains ``values``.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
# Returns results matching 'a'
|
||||||
|
search(any=['a'])
|
||||||
|
# Returns results matching artist 'xyz'
|
||||||
|
search(artist=['xyz'])
|
||||||
|
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||||
|
search(any=['a', 'b'], artist=['xyz'])
|
||||||
|
|
||||||
|
:param query: one or more queries to search for
|
||||||
|
:type query: dict
|
||||||
|
:rtype: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
return self.provider.search(**query)
|
||||||
548
mopidy/core/playback.py
Normal file
548
mopidy/core/playback.py
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mopidy.listeners import BackendListener
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('mopidy.backends.base')
|
||||||
|
|
||||||
|
|
||||||
|
def option_wrapper(name, default):
|
||||||
|
def get_option(self):
|
||||||
|
return getattr(self, name, default)
|
||||||
|
|
||||||
|
def set_option(self, value):
|
||||||
|
if getattr(self, name, default) != value:
|
||||||
|
self._trigger_options_changed()
|
||||||
|
return setattr(self, name, value)
|
||||||
|
|
||||||
|
return property(get_option, set_option)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaybackController(object):
|
||||||
|
"""
|
||||||
|
:param backend: the backend
|
||||||
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
|
:param provider: provider the controller should use
|
||||||
|
:type provider: instance of :class:`BasePlaybackProvider`
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable = R0902
|
||||||
|
# Too many instance attributes
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
|
#: Constant representing the paused state.
|
||||||
|
PAUSED = u'paused'
|
||||||
|
|
||||||
|
#: Constant representing the playing state.
|
||||||
|
PLAYING = u'playing'
|
||||||
|
|
||||||
|
#: Constant representing the stopped state.
|
||||||
|
STOPPED = u'stopped'
|
||||||
|
|
||||||
|
#: :class:`True`
|
||||||
|
#: Tracks are removed from the playlist when they have been played.
|
||||||
|
#: :class:`False`
|
||||||
|
#: Tracks are not removed from the playlist.
|
||||||
|
consume = option_wrapper('_consume', False)
|
||||||
|
|
||||||
|
#: The currently playing or selected track.
|
||||||
|
#:
|
||||||
|
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
|
||||||
|
#: :class:`None`.
|
||||||
|
current_cp_track = None
|
||||||
|
|
||||||
|
#: :class:`True`
|
||||||
|
#: Tracks are selected at random from the playlist.
|
||||||
|
#: :class:`False`
|
||||||
|
#: Tracks are played in the order of the playlist.
|
||||||
|
random = option_wrapper('_random', False)
|
||||||
|
|
||||||
|
#: :class:`True`
|
||||||
|
#: The current playlist is played repeatedly. To repeat a single track,
|
||||||
|
#: select both :attr:`repeat` and :attr:`single`.
|
||||||
|
#: :class:`False`
|
||||||
|
#: The current playlist is played once.
|
||||||
|
repeat = option_wrapper('_repeat', False)
|
||||||
|
|
||||||
|
#: :class:`True`
|
||||||
|
#: Playback is stopped after current song, unless in :attr:`repeat`
|
||||||
|
#: mode.
|
||||||
|
#: :class:`False`
|
||||||
|
#: Playback continues after current song.
|
||||||
|
single = option_wrapper('_single', False)
|
||||||
|
|
||||||
|
def __init__(self, backend, provider):
|
||||||
|
self.backend = backend
|
||||||
|
self.provider = provider
|
||||||
|
self._state = self.STOPPED
|
||||||
|
self._shuffled = []
|
||||||
|
self._first_shuffle = True
|
||||||
|
self.play_time_accumulated = 0
|
||||||
|
self.play_time_started = None
|
||||||
|
|
||||||
|
def _get_cpid(self, cp_track):
|
||||||
|
if cp_track is None:
|
||||||
|
return None
|
||||||
|
return cp_track.cpid
|
||||||
|
|
||||||
|
def _get_track(self, cp_track):
|
||||||
|
if cp_track is None:
|
||||||
|
return None
|
||||||
|
return cp_track.track
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_cpid(self):
|
||||||
|
"""
|
||||||
|
The CPID (current playlist ID) of the currently playing or selected
|
||||||
|
track.
|
||||||
|
|
||||||
|
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||||
|
"""
|
||||||
|
return self._get_cpid(self.current_cp_track)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_track(self):
|
||||||
|
"""
|
||||||
|
The currently playing or selected :class:`mopidy.models.Track`.
|
||||||
|
|
||||||
|
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||||
|
"""
|
||||||
|
return self._get_track(self.current_cp_track)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_playlist_position(self):
|
||||||
|
"""
|
||||||
|
The position of the current track in the current playlist.
|
||||||
|
|
||||||
|
Read-only.
|
||||||
|
"""
|
||||||
|
if self.current_cp_track is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return self.backend.current_playlist.cp_tracks.index(
|
||||||
|
self.current_cp_track)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def track_at_eot(self):
|
||||||
|
"""
|
||||||
|
The track that will be played at the end of the current track.
|
||||||
|
|
||||||
|
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||||
|
:attr:`cp_track_at_eot` for convenience.
|
||||||
|
"""
|
||||||
|
return self._get_track(self.cp_track_at_eot)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cp_track_at_eot(self):
|
||||||
|
"""
|
||||||
|
The track that will be played at the end of the current track.
|
||||||
|
|
||||||
|
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||||
|
|
||||||
|
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:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.random and not self._shuffled:
|
||||||
|
if self.repeat or self._first_shuffle:
|
||||||
|
logger.debug('Shuffling tracks')
|
||||||
|
self._shuffled = cp_tracks
|
||||||
|
random.shuffle(self._shuffled)
|
||||||
|
self._first_shuffle = False
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
if self.repeat and not self.single:
|
||||||
|
return cp_tracks[
|
||||||
|
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return cp_tracks[self.current_playlist_position + 1]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def track_at_next(self):
|
||||||
|
"""
|
||||||
|
The track that will be played if calling :meth:`next()`.
|
||||||
|
|
||||||
|
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||||
|
:attr:`cp_track_at_next` for convenience.
|
||||||
|
"""
|
||||||
|
return self._get_track(self.cp_track_at_next)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cp_track_at_next(self):
|
||||||
|
"""
|
||||||
|
The track that will be played if calling :meth:`next()`.
|
||||||
|
|
||||||
|
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||||
|
|
||||||
|
For normal playback this is the next track in the playlist. If repeat
|
||||||
|
is enabled the next track can loop around the playlist. When random is
|
||||||
|
enabled this should be a random track, all tracks should be played once
|
||||||
|
before the list repeats.
|
||||||
|
"""
|
||||||
|
cp_tracks = self.backend.current_playlist.cp_tracks
|
||||||
|
|
||||||
|
if not cp_tracks:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.random and not self._shuffled:
|
||||||
|
if self.repeat or self._first_shuffle:
|
||||||
|
logger.debug('Shuffling tracks')
|
||||||
|
self._shuffled = cp_tracks
|
||||||
|
random.shuffle(self._shuffled)
|
||||||
|
self._first_shuffle = False
|
||||||
|
|
||||||
|
if self.random and self._shuffled:
|
||||||
|
return self._shuffled[0]
|
||||||
|
|
||||||
|
if self.current_cp_track is None:
|
||||||
|
return cp_tracks[0]
|
||||||
|
|
||||||
|
if self.repeat:
|
||||||
|
return cp_tracks[
|
||||||
|
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return cp_tracks[self.current_playlist_position + 1]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def track_at_previous(self):
|
||||||
|
"""
|
||||||
|
The track that will be played if calling :meth:`previous()`.
|
||||||
|
|
||||||
|
Read-only. A :class:`mopidy.models.Track` extracted from
|
||||||
|
:attr:`cp_track_at_previous` for convenience.
|
||||||
|
"""
|
||||||
|
return self._get_track(self.cp_track_at_previous)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cp_track_at_previous(self):
|
||||||
|
"""
|
||||||
|
The track that will be played if calling :meth:`previous()`.
|
||||||
|
|
||||||
|
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||||
|
|
||||||
|
For normal playback this is the previous track in the playlist. If
|
||||||
|
random and/or consume is enabled it should return the current track
|
||||||
|
instead.
|
||||||
|
"""
|
||||||
|
if self.repeat or self.consume or self.random:
|
||||||
|
return self.current_cp_track
|
||||||
|
|
||||||
|
if self.current_playlist_position in (None, 0):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.backend.current_playlist.cp_tracks[
|
||||||
|
self.current_playlist_position - 1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""
|
||||||
|
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
|
||||||
|
:attr:`STOPPED`.
|
||||||
|
|
||||||
|
Possible states and transitions:
|
||||||
|
|
||||||
|
.. digraph:: state_transitions
|
||||||
|
|
||||||
|
"STOPPED" -> "PLAYING" [ label="play" ]
|
||||||
|
"STOPPED" -> "PAUSED" [ label="pause" ]
|
||||||
|
"PLAYING" -> "STOPPED" [ label="stop" ]
|
||||||
|
"PLAYING" -> "PAUSED" [ label="pause" ]
|
||||||
|
"PLAYING" -> "PLAYING" [ label="play" ]
|
||||||
|
"PAUSED" -> "PLAYING" [ label="resume" ]
|
||||||
|
"PAUSED" -> "STOPPED" [ label="stop" ]
|
||||||
|
"""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, new_state):
|
||||||
|
(old_state, self._state) = (self.state, new_state)
|
||||||
|
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
|
||||||
|
|
||||||
|
self._trigger_playback_state_changed()
|
||||||
|
|
||||||
|
# FIXME play_time stuff assumes backend does not have a better way of
|
||||||
|
# handeling this stuff :/
|
||||||
|
if (old_state in (self.PLAYING, self.STOPPED)
|
||||||
|
and new_state == self.PLAYING):
|
||||||
|
self._play_time_start()
|
||||||
|
elif old_state == self.PLAYING and new_state == self.PAUSED:
|
||||||
|
self._play_time_pause()
|
||||||
|
elif old_state == self.PAUSED and new_state == self.PLAYING:
|
||||||
|
self._play_time_resume()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_position(self):
|
||||||
|
"""Time position in milliseconds."""
|
||||||
|
if self.state == self.PLAYING:
|
||||||
|
time_since_started = (self._current_wall_time -
|
||||||
|
self.play_time_started)
|
||||||
|
return self.play_time_accumulated + time_since_started
|
||||||
|
elif self.state == self.PAUSED:
|
||||||
|
return self.play_time_accumulated
|
||||||
|
elif self.state == self.STOPPED:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _play_time_start(self):
|
||||||
|
self.play_time_accumulated = 0
|
||||||
|
self.play_time_started = self._current_wall_time
|
||||||
|
|
||||||
|
def _play_time_pause(self):
|
||||||
|
time_since_started = self._current_wall_time - self.play_time_started
|
||||||
|
self.play_time_accumulated += time_since_started
|
||||||
|
|
||||||
|
def _play_time_resume(self):
|
||||||
|
self.play_time_started = self._current_wall_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _current_wall_time(self):
|
||||||
|
return int(time.time() * 1000)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume(self):
|
||||||
|
return self.provider.get_volume()
|
||||||
|
|
||||||
|
@volume.setter
|
||||||
|
def volume(self, volume):
|
||||||
|
self.provider.set_volume(volume)
|
||||||
|
|
||||||
|
def change_track(self, cp_track, on_error_step=1):
|
||||||
|
"""
|
||||||
|
Change to the given track, keeping the current playback state.
|
||||||
|
|
||||||
|
:param cp_track: track to change to
|
||||||
|
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||||
|
or :class:`None`
|
||||||
|
:param on_error_step: direction to step at play error, 1 for next
|
||||||
|
track (default), -1 for previous track
|
||||||
|
:type on_error_step: int, -1 or 1
|
||||||
|
|
||||||
|
"""
|
||||||
|
old_state = self.state
|
||||||
|
self.stop()
|
||||||
|
self.current_cp_track = cp_track
|
||||||
|
if old_state == self.PLAYING:
|
||||||
|
self.play(on_error_step=on_error_step)
|
||||||
|
elif old_state == self.PAUSED:
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
def on_end_of_track(self):
|
||||||
|
"""
|
||||||
|
Tell the playback controller that end of track is reached.
|
||||||
|
"""
|
||||||
|
if self.state == self.STOPPED:
|
||||||
|
return
|
||||||
|
|
||||||
|
original_cp_track = self.current_cp_track
|
||||||
|
|
||||||
|
if self.cp_track_at_eot:
|
||||||
|
self._trigger_track_playback_ended()
|
||||||
|
self.play(self.cp_track_at_eot)
|
||||||
|
else:
|
||||||
|
self.stop(clear_current_track=True)
|
||||||
|
|
||||||
|
if self.consume:
|
||||||
|
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
|
||||||
|
|
||||||
|
def on_current_playlist_change(self):
|
||||||
|
"""
|
||||||
|
Tell the playback controller that the current playlist has changed.
|
||||||
|
|
||||||
|
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
|
||||||
|
"""
|
||||||
|
self._first_shuffle = True
|
||||||
|
self._shuffled = []
|
||||||
|
|
||||||
|
if (not self.backend.current_playlist.cp_tracks or
|
||||||
|
self.current_cp_track not in
|
||||||
|
self.backend.current_playlist.cp_tracks):
|
||||||
|
self.stop(clear_current_track=True)
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
"""
|
||||||
|
Change to the next track.
|
||||||
|
|
||||||
|
The current playback state will be kept. If it was playing, playing
|
||||||
|
will continue. If it was paused, it will still be paused, etc.
|
||||||
|
"""
|
||||||
|
if self.cp_track_at_next:
|
||||||
|
self._trigger_track_playback_ended()
|
||||||
|
self.change_track(self.cp_track_at_next)
|
||||||
|
else:
|
||||||
|
self.stop(clear_current_track=True)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
"""Pause playback."""
|
||||||
|
if self.provider.pause():
|
||||||
|
self.state = self.PAUSED
|
||||||
|
self._trigger_track_playback_paused()
|
||||||
|
|
||||||
|
def play(self, cp_track=None, on_error_step=1):
|
||||||
|
"""
|
||||||
|
Play the given track, or if the given track is :class:`None`, play the
|
||||||
|
currently active track.
|
||||||
|
|
||||||
|
:param cp_track: track to play
|
||||||
|
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||||
|
or :class:`None`
|
||||||
|
:param on_error_step: direction to step at play error, 1 for next
|
||||||
|
track (default), -1 for previous track
|
||||||
|
:type on_error_step: int, -1 or 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
if cp_track is not None:
|
||||||
|
assert cp_track in self.backend.current_playlist.cp_tracks
|
||||||
|
elif cp_track is None:
|
||||||
|
if self.state == self.PAUSED:
|
||||||
|
return self.resume()
|
||||||
|
elif self.current_cp_track is not None:
|
||||||
|
cp_track = self.current_cp_track
|
||||||
|
elif self.current_cp_track is None and on_error_step == 1:
|
||||||
|
cp_track = self.cp_track_at_next
|
||||||
|
elif self.current_cp_track is None and on_error_step == -1:
|
||||||
|
cp_track = self.cp_track_at_previous
|
||||||
|
|
||||||
|
if cp_track is not None:
|
||||||
|
self.current_cp_track = cp_track
|
||||||
|
self.state = self.PLAYING
|
||||||
|
if not self.provider.play(cp_track.track):
|
||||||
|
# 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:
|
||||||
|
self.previous()
|
||||||
|
|
||||||
|
if self.random and self.current_cp_track in self._shuffled:
|
||||||
|
self._shuffled.remove(self.current_cp_track)
|
||||||
|
|
||||||
|
self._trigger_track_playback_started()
|
||||||
|
|
||||||
|
def previous(self):
|
||||||
|
"""
|
||||||
|
Change to the previous track.
|
||||||
|
|
||||||
|
The current playback state will be kept. If it was playing, playing
|
||||||
|
will continue. If it was paused, it will still be paused, etc.
|
||||||
|
"""
|
||||||
|
self._trigger_track_playback_ended()
|
||||||
|
self.change_track(self.cp_track_at_previous, on_error_step=-1)
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
"""If paused, resume playing the current track."""
|
||||||
|
if self.state == self.PAUSED and self.provider.resume():
|
||||||
|
self.state = self.PLAYING
|
||||||
|
self._trigger_track_playback_resumed()
|
||||||
|
|
||||||
|
def seek(self, time_position):
|
||||||
|
"""
|
||||||
|
Seeks to time position given in milliseconds.
|
||||||
|
|
||||||
|
:param time_position: time position in milliseconds
|
||||||
|
:type time_position: int
|
||||||
|
:rtype: :class:`True` if successful, else :class:`False`
|
||||||
|
"""
|
||||||
|
if not self.backend.current_playlist.tracks:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.state == self.STOPPED:
|
||||||
|
self.play()
|
||||||
|
elif self.state == self.PAUSED:
|
||||||
|
self.resume()
|
||||||
|
|
||||||
|
if time_position < 0:
|
||||||
|
time_position = 0
|
||||||
|
elif time_position > self.current_track.length:
|
||||||
|
self.next()
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.play_time_started = self._current_wall_time
|
||||||
|
self.play_time_accumulated = time_position
|
||||||
|
|
||||||
|
success = self.provider.seek(time_position)
|
||||||
|
if success:
|
||||||
|
self._trigger_seeked()
|
||||||
|
return success
|
||||||
|
|
||||||
|
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:
|
||||||
|
if self.provider.stop():
|
||||||
|
self._trigger_track_playback_ended()
|
||||||
|
self.state = self.STOPPED
|
||||||
|
if clear_current_track:
|
||||||
|
self.current_cp_track = None
|
||||||
|
|
||||||
|
def _trigger_track_playback_paused(self):
|
||||||
|
logger.debug(u'Triggering track playback paused event')
|
||||||
|
if self.current_track is None:
|
||||||
|
return
|
||||||
|
BackendListener.send('track_playback_paused',
|
||||||
|
track=self.current_track,
|
||||||
|
time_position=self.time_position)
|
||||||
|
|
||||||
|
def _trigger_track_playback_resumed(self):
|
||||||
|
logger.debug(u'Triggering track playback resumed event')
|
||||||
|
if self.current_track is None:
|
||||||
|
return
|
||||||
|
BackendListener.send('track_playback_resumed',
|
||||||
|
track=self.current_track,
|
||||||
|
time_position=self.time_position)
|
||||||
|
|
||||||
|
def _trigger_track_playback_started(self):
|
||||||
|
logger.debug(u'Triggering track playback started event')
|
||||||
|
if self.current_track is None:
|
||||||
|
return
|
||||||
|
BackendListener.send('track_playback_started',
|
||||||
|
track=self.current_track)
|
||||||
|
|
||||||
|
def _trigger_track_playback_ended(self):
|
||||||
|
logger.debug(u'Triggering track playback ended event')
|
||||||
|
if self.current_track is None:
|
||||||
|
return
|
||||||
|
BackendListener.send('track_playback_ended',
|
||||||
|
track=self.current_track,
|
||||||
|
time_position=self.time_position)
|
||||||
|
|
||||||
|
def _trigger_playback_state_changed(self):
|
||||||
|
logger.debug(u'Triggering playback state change event')
|
||||||
|
BackendListener.send('playback_state_changed')
|
||||||
|
|
||||||
|
def _trigger_options_changed(self):
|
||||||
|
logger.debug(u'Triggering options changed event')
|
||||||
|
BackendListener.send('options_changed')
|
||||||
|
|
||||||
|
def _trigger_seeked(self):
|
||||||
|
logger.debug(u'Triggering seeked event')
|
||||||
|
BackendListener.send('seeked')
|
||||||
113
mopidy/core/stored_playlists.py
Normal file
113
mopidy/core/stored_playlists.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
class StoredPlaylistsController(object):
|
||||||
|
"""
|
||||||
|
:param backend: backend the controller is a part of
|
||||||
|
:type backend: :class:`mopidy.backends.base.Backend`
|
||||||
|
:param provider: provider the controller should use
|
||||||
|
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
|
||||||
|
"""
|
||||||
|
|
||||||
|
pykka_traversable = True
|
||||||
|
|
||||||
|
def __init__(self, backend, provider):
|
||||||
|
self.backend = backend
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playlists(self):
|
||||||
|
"""
|
||||||
|
Currently stored playlists.
|
||||||
|
|
||||||
|
Read/write. List of :class:`mopidy.models.Playlist`.
|
||||||
|
"""
|
||||||
|
return self.provider.playlists
|
||||||
|
|
||||||
|
@playlists.setter
|
||||||
|
def playlists(self, playlists):
|
||||||
|
self.provider.playlists = playlists
|
||||||
|
|
||||||
|
def create(self, name):
|
||||||
|
"""
|
||||||
|
Create a new playlist.
|
||||||
|
|
||||||
|
:param name: name of the new playlist
|
||||||
|
:type name: string
|
||||||
|
:rtype: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
return self.provider.create(name)
|
||||||
|
|
||||||
|
def delete(self, playlist):
|
||||||
|
"""
|
||||||
|
Delete playlist.
|
||||||
|
|
||||||
|
:param playlist: the playlist to delete
|
||||||
|
:type playlist: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
return self.provider.delete(playlist)
|
||||||
|
|
||||||
|
def get(self, **criteria):
|
||||||
|
"""
|
||||||
|
Get playlist by given criterias from the set of stored playlists.
|
||||||
|
|
||||||
|
Raises :exc:`LookupError` if a unique match is not found.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
get(name='a') # Returns track with name 'a'
|
||||||
|
get(uri='xyz') # Returns track with URI 'xyz'
|
||||||
|
get(name='a', uri='xyz') # Returns track with name 'a' and URI
|
||||||
|
# 'xyz'
|
||||||
|
|
||||||
|
:param criteria: one or more criteria to match by
|
||||||
|
:type criteria: dict
|
||||||
|
:rtype: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
matches = self.playlists
|
||||||
|
for (key, value) in criteria.iteritems():
|
||||||
|
matches = filter(lambda p: getattr(p, key) == value, matches)
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0]
|
||||||
|
criteria_string = ', '.join(
|
||||||
|
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
||||||
|
if len(matches) == 0:
|
||||||
|
raise LookupError('"%s" match no playlists' % criteria_string)
|
||||||
|
else:
|
||||||
|
raise LookupError('"%s" match multiple playlists'
|
||||||
|
% criteria_string)
|
||||||
|
|
||||||
|
def lookup(self, uri):
|
||||||
|
"""
|
||||||
|
Lookup playlist with given URI in both the set of stored playlists and
|
||||||
|
in any other playlist sources.
|
||||||
|
|
||||||
|
:param uri: playlist URI
|
||||||
|
:type uri: string
|
||||||
|
:rtype: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
return self.provider.lookup(uri)
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""
|
||||||
|
Refresh the stored playlists in
|
||||||
|
:attr:`mopidy.backends.base.StoredPlaylistsController.playlists`.
|
||||||
|
"""
|
||||||
|
return self.provider.refresh()
|
||||||
|
|
||||||
|
def rename(self, playlist, new_name):
|
||||||
|
"""
|
||||||
|
Rename playlist.
|
||||||
|
|
||||||
|
:param playlist: the playlist
|
||||||
|
:type playlist: :class:`mopidy.models.Playlist`
|
||||||
|
:param new_name: the new name
|
||||||
|
:type new_name: string
|
||||||
|
"""
|
||||||
|
return self.provider.rename(playlist, new_name)
|
||||||
|
|
||||||
|
def save(self, playlist):
|
||||||
|
"""
|
||||||
|
Save the playlist to the set of stored playlists.
|
||||||
|
|
||||||
|
:param playlist: the playlist
|
||||||
|
:type playlist: :class:`mopidy.models.Playlist`
|
||||||
|
"""
|
||||||
|
return self.provider.save(playlist)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from mopidy.backends.base import PlaybackController
|
from mopidy import core
|
||||||
from mopidy.frontends.mpd.protocol import handle_request
|
from mopidy.frontends.mpd.protocol import handle_request
|
||||||
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
||||||
MpdNotImplemented)
|
MpdNotImplemented)
|
||||||
@ -105,10 +105,10 @@ def pause(context, state=None):
|
|||||||
"""
|
"""
|
||||||
if state is None:
|
if state is None:
|
||||||
if (context.backend.playback.state.get() ==
|
if (context.backend.playback.state.get() ==
|
||||||
PlaybackController.PLAYING):
|
core.PlaybackController.PLAYING):
|
||||||
context.backend.playback.pause()
|
context.backend.playback.pause()
|
||||||
elif (context.backend.playback.state.get() ==
|
elif (context.backend.playback.state.get() ==
|
||||||
PlaybackController.PAUSED):
|
core.PlaybackController.PAUSED):
|
||||||
context.backend.playback.resume()
|
context.backend.playback.resume()
|
||||||
elif int(state):
|
elif int(state):
|
||||||
context.backend.playback.pause()
|
context.backend.playback.pause()
|
||||||
@ -185,9 +185,11 @@ def playpos(context, songpos):
|
|||||||
raise MpdArgError(u'Bad song index', command=u'play')
|
raise MpdArgError(u'Bad song index', command=u'play')
|
||||||
|
|
||||||
def _play_minus_one(context):
|
def _play_minus_one(context):
|
||||||
if (context.backend.playback.state.get() == PlaybackController.PLAYING):
|
if (context.backend.playback.state.get() ==
|
||||||
|
core.PlaybackController.PLAYING):
|
||||||
return # Nothing to do
|
return # Nothing to do
|
||||||
elif (context.backend.playback.state.get() == PlaybackController.PAUSED):
|
elif (context.backend.playback.state.get() ==
|
||||||
|
core.PlaybackController.PAUSED):
|
||||||
return context.backend.playback.resume().get()
|
return context.backend.playback.resume().get()
|
||||||
elif context.backend.playback.current_cp_track.get() is not None:
|
elif context.backend.playback.current_cp_track.get() is not None:
|
||||||
cp_track = context.backend.playback.current_cp_track.get()
|
cp_track = context.backend.playback.current_cp_track.get()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import pykka.future
|
import pykka.future
|
||||||
|
|
||||||
from mopidy.backends.base import PlaybackController
|
from mopidy import core
|
||||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||||
from mopidy.frontends.mpd.protocol import handle_request
|
from mopidy.frontends.mpd.protocol import handle_request
|
||||||
from mopidy.frontends.mpd.translator import track_to_mpd_format
|
from mopidy.frontends.mpd.translator import track_to_mpd_format
|
||||||
@ -194,8 +194,8 @@ def status(context):
|
|||||||
if futures['playback.current_cp_track'].get() is not None:
|
if futures['playback.current_cp_track'].get() is not None:
|
||||||
result.append(('song', _status_songpos(futures)))
|
result.append(('song', _status_songpos(futures)))
|
||||||
result.append(('songid', _status_songid(futures)))
|
result.append(('songid', _status_songid(futures)))
|
||||||
if futures['playback.state'].get() in (PlaybackController.PLAYING,
|
if futures['playback.state'].get() in (core.PlaybackController.PLAYING,
|
||||||
PlaybackController.PAUSED):
|
core.PlaybackController.PAUSED):
|
||||||
result.append(('time', _status_time(futures)))
|
result.append(('time', _status_time(futures)))
|
||||||
result.append(('elapsed', _status_time_elapsed(futures)))
|
result.append(('elapsed', _status_time_elapsed(futures)))
|
||||||
result.append(('bitrate', _status_bitrate(futures)))
|
result.append(('bitrate', _status_bitrate(futures)))
|
||||||
@ -239,11 +239,11 @@ def _status_songpos(futures):
|
|||||||
|
|
||||||
def _status_state(futures):
|
def _status_state(futures):
|
||||||
state = futures['playback.state'].get()
|
state = futures['playback.state'].get()
|
||||||
if state == PlaybackController.PLAYING:
|
if state == core.PlaybackController.PLAYING:
|
||||||
return u'play'
|
return u'play'
|
||||||
elif state == PlaybackController.STOPPED:
|
elif state == core.PlaybackController.STOPPED:
|
||||||
return u'stop'
|
return u'stop'
|
||||||
elif state == PlaybackController.PAUSED:
|
elif state == core.PlaybackController.PAUSED:
|
||||||
return u'pause'
|
return u'pause'
|
||||||
|
|
||||||
def _status_time(futures):
|
def _status_time(futures):
|
||||||
|
|||||||
@ -14,9 +14,8 @@ except ImportError as import_error:
|
|||||||
|
|
||||||
from pykka.registry import ActorRegistry
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import core, settings
|
||||||
from mopidy.backends.base import Backend
|
from mopidy.backends.base import Backend
|
||||||
from mopidy.backends.base.playback import PlaybackController
|
|
||||||
from mopidy.utils.process import exit_process
|
from mopidy.utils.process import exit_process
|
||||||
|
|
||||||
# Must be done before dbus.SessionBus() is called
|
# Must be done before dbus.SessionBus() is called
|
||||||
@ -198,11 +197,11 @@ class MprisObject(dbus.service.Object):
|
|||||||
logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
|
logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
|
||||||
return
|
return
|
||||||
state = self.backend.playback.state.get()
|
state = self.backend.playback.state.get()
|
||||||
if state == PlaybackController.PLAYING:
|
if state == core.PlaybackController.PLAYING:
|
||||||
self.backend.playback.pause().get()
|
self.backend.playback.pause().get()
|
||||||
elif state == PlaybackController.PAUSED:
|
elif state == core.PlaybackController.PAUSED:
|
||||||
self.backend.playback.resume().get()
|
self.backend.playback.resume().get()
|
||||||
elif state == PlaybackController.STOPPED:
|
elif state == core.PlaybackController.STOPPED:
|
||||||
self.backend.playback.play().get()
|
self.backend.playback.play().get()
|
||||||
|
|
||||||
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
@dbus.service.method(dbus_interface=PLAYER_IFACE)
|
||||||
@ -220,7 +219,7 @@ class MprisObject(dbus.service.Object):
|
|||||||
logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
|
logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
|
||||||
return
|
return
|
||||||
state = self.backend.playback.state.get()
|
state = self.backend.playback.state.get()
|
||||||
if state == PlaybackController.PAUSED:
|
if state == core.PlaybackController.PAUSED:
|
||||||
self.backend.playback.resume().get()
|
self.backend.playback.resume().get()
|
||||||
else:
|
else:
|
||||||
self.backend.playback.play().get()
|
self.backend.playback.play().get()
|
||||||
@ -287,11 +286,11 @@ class MprisObject(dbus.service.Object):
|
|||||||
|
|
||||||
def get_PlaybackStatus(self):
|
def get_PlaybackStatus(self):
|
||||||
state = self.backend.playback.state.get()
|
state = self.backend.playback.state.get()
|
||||||
if state == PlaybackController.PLAYING:
|
if state == core.PlaybackController.PLAYING:
|
||||||
return 'Playing'
|
return 'Playing'
|
||||||
elif state == PlaybackController.PAUSED:
|
elif state == core.PlaybackController.PAUSED:
|
||||||
return 'Paused'
|
return 'Paused'
|
||||||
elif state == PlaybackController.STOPPED:
|
elif state == core.PlaybackController.STOPPED:
|
||||||
return 'Stopped'
|
return 'Stopped'
|
||||||
|
|
||||||
def get_LoopStatus(self):
|
def get_LoopStatus(self):
|
||||||
|
|||||||
0
tests/core/__init__.py
Normal file
0
tests/core/__init__.py
Normal file
@ -1,12 +1,12 @@
|
|||||||
from mopidy.backends import base as backend
|
from mopidy import core
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
from tests.frontends.mpd import protocol
|
from tests.frontends.mpd import protocol
|
||||||
|
|
||||||
PAUSED = backend.PlaybackController.PAUSED
|
PAUSED = core.PlaybackController.PAUSED
|
||||||
PLAYING = backend.PlaybackController.PLAYING
|
PLAYING = core.PlaybackController.PLAYING
|
||||||
STOPPED = backend.PlaybackController.STOPPED
|
STOPPED = core.PlaybackController.STOPPED
|
||||||
|
|
||||||
|
|
||||||
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
|
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
from mopidy.backends import dummy as backend
|
from mopidy import core
|
||||||
|
from mopidy.backends import dummy
|
||||||
from mopidy.frontends.mpd import dispatcher
|
from mopidy.frontends.mpd import dispatcher
|
||||||
from mopidy.frontends.mpd.protocol import status
|
from mopidy.frontends.mpd.protocol import status
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
PAUSED = backend.PlaybackController.PAUSED
|
PAUSED = core.PlaybackController.PAUSED
|
||||||
PLAYING = backend.PlaybackController.PLAYING
|
PLAYING = core.PlaybackController.PLAYING
|
||||||
STOPPED = backend.PlaybackController.STOPPED
|
STOPPED = core.PlaybackController.STOPPED
|
||||||
|
|
||||||
# FIXME migrate to using protocol.BaseTestCase instead of status.stats
|
# FIXME migrate to using protocol.BaseTestCase instead of status.stats
|
||||||
# directly?
|
# directly?
|
||||||
@ -15,7 +16,7 @@ STOPPED = backend.PlaybackController.STOPPED
|
|||||||
|
|
||||||
class StatusHandlerTest(unittest.TestCase):
|
class StatusHandlerTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.backend = backend.DummyBackend.start().proxy()
|
self.backend = dummy.DummyBackend.start().proxy()
|
||||||
self.dispatcher = dispatcher.MpdDispatcher()
|
self.dispatcher = dispatcher.MpdDispatcher()
|
||||||
self.context = self.dispatcher.context
|
self.context = self.dispatcher.context
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,8 @@ import sys
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from mopidy import OptionalDependencyError
|
from mopidy import core, OptionalDependencyError
|
||||||
from mopidy.backends.dummy import DummyBackend
|
from mopidy.backends.dummy import DummyBackend
|
||||||
from mopidy.backends.base.playback import PlaybackController
|
|
||||||
from mopidy.models import Album, Artist, Track
|
from mopidy.models import Album, Artist, Track
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -14,9 +13,9 @@ except OptionalDependencyError:
|
|||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
PLAYING = PlaybackController.PLAYING
|
PLAYING = core.PlaybackController.PLAYING
|
||||||
PAUSED = PlaybackController.PAUSED
|
PAUSED = core.PlaybackController.PAUSED
|
||||||
STOPPED = PlaybackController.STOPPED
|
STOPPED = core.PlaybackController.STOPPED
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
|
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user