# Copyright (C) 2008-2010 Adam Olsen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
#
# The developers of the Exaile media player hereby grant permission
# for non-GPL compatible GStreamer and Exaile plugins to be used and
# distributed together with GStreamer and Exaile. This permission is
# above and beyond the permissions granted by the GPL license by which
# Exaile is covered. If you modify this code, you may extend this
# exception to your version of the code, but you are not obligated to
# do so. If you do not wish to do so, delete this exception statement
# from your version.
import logging
import pickle
from xl import common, event, playlist, settings
logger = logging.getLogger(__name__)
[docs]class PlayQueue(playlist.Playlist):
"""
Manages the queue of songs to be played
The content of the queue are processed before processing
the content of the assigned playlist.
When the remove_item_when_played option is enabled, the queue
removes items from itself as they are played.
When not enabled, the queue acts like a regular playlist, and
moves the position as tracks are played.
In this mode, when a new track is queued, the position is set
to play that track, and play will continue with that track
until the queue is exhausted, and then the assigned playlist
will be continued.
TODO: Queue needs to be threadsafe!
"""
def __init__(self, player, name, location=None):
playlist.Playlist.__init__(self, name=name)
self.__queue_has_tracks_val = False
self.__current_playlist = self # this should never be None
self.player = player
# hack for making docs work
if player is not None:
player.queue = self
if location is not None:
self.load_from_location(location)
event.add_callback(self._on_option_set, '%s_option_set' % name)
self.__opt_remove_item_when_played = '%s/remove_item_when_played' % name
self.__opt_remove_item_after_played = '%s/remove_item_after_played' % name
self.__opt_disable_new_track_when_playing = (
'%s/disable_new_track_when_playing' % name
)
self.__opt_enqueue_begins_playback = '%s/enqueue_begins_playback' % name
self._on_option_set(None, settings, self.__opt_remove_item_when_played)
self._on_option_set(None, settings, self.__opt_remove_item_after_played)
self._on_option_set(None, settings, self.__opt_disable_new_track_when_playing)
def _on_option_set(self, evtype, settings, option):
if option == self.__opt_remove_item_when_played:
self.__remove_item_on_playback = settings.get_option(option, True)
if len(self):
self.__queue_has_tracks = True
if option == self.__opt_remove_item_after_played:
self.__remove_item_after_playback = settings.get_option(option, True)
elif option == self.__opt_disable_new_track_when_playing:
self.__disable_new_track_when_playing = settings.get_option(option, False)
[docs] def set_current_playlist(self, playlist):
"""
Sets the playlist to be processed in the queue
:param playlist: the playlist to process
:type playlist: :class:`xl.playlist.Playlist`
.. note:: The following :doc:`events </xl/event>` will be emitted by this method:
* `queue_current_playlist_changed`: indicates that the queue playlist has been changed
"""
if playlist is self.__current_playlist:
return
elif playlist is None:
playlist = self
if playlist is self:
self.__queue_has_tracks = True
self.__current_playlist = playlist
event.log_event('queue_current_playlist_changed', self, playlist)
#: The playlist currently processed in the queue
current_playlist = property(
lambda self: self.__current_playlist, set_current_playlist
)
[docs] def get_next(self):
"""
Retrieves the next track that will be played. Does not
actually set the position. When you call next(), it should
return the same track.
This exists to support retrieving a track before it actually
needs to be played, such as for pre-buffering.
:returns: the next track to be played
:rtype: :class:`xl.trax.Track` or None
"""
if self.__queue_has_tracks and len(self):
if self.__remove_item_on_playback:
track = self._calculate_next_track()
if track is None:
# pop the last track
self.pop(0)
return track
else:
return playlist.Playlist.get_next(self)
elif self.current_playlist is not self:
return self.current_playlist.get_next()
else:
return None
[docs] def next(self, autoplay=True, track=None):
"""
Goes to the next track, either in the queue, or in the current
playlist. If a track is passed in, that track is played
:param autoplay: play the track in addition to returning it
:type autoplay: bool
:param track: if passed, play this track
:type track: :class:`xl.trax.Track`
.. note:: The following :doc:`events </xl/event>` will be emitted by this method:
* `playback_playlist_end`: indicates that the end of the queue has been reached
"""
if track is None:
if self.__queue_has_tracks:
if self.__remove_item_on_playback:
if self.__remove_item_after_playback:
track = self._calculate_next_track()
try:
self.pop(self.current_position)
except IndexError:
pass
self.current_position = 0
else:
track = self.pop(0)
self.current_position = -1
else:
track = super().next()
# reached the end of the internal queue, don't repeat
if track is None:
self.__queue_has_tracks = False
else:
# otherwise set current playlist to queue
self.player.queue.current_playlist = self
if track is None and self.current_playlist is not self:
track = self.current_playlist.next()
if autoplay:
self.player.play(track)
if not track:
event.log_event("playback_playlist_end", self, self.current_playlist)
return track
[docs] def prev(self):
"""
Goes to the previous track
"""
track = None
if self.player.current:
if self.player.get_time() < 5:
if self.__queue_has_tracks and not self.__remove_item_on_playback:
position = self.current_position - 1
if position < 0:
position = 0 if len(self) else -1
self.current_position = position
track = self.current
elif self.current_playlist is not self:
track = self.current_playlist.prev()
if track is None:
track = self.player.current
else:
track = self.current
self.player.play(track)
return track
[docs] def get_current(self):
"""
Gets the current track
:returns: the current track
:type: :class:`xl.trax.Track`
"""
if self.player.current and self.current_position > 0:
current = self.player.current
else:
current = playlist.Playlist.get_current(self)
if current is None and self.current_playlist is not self:
current = self.current_playlist.get_current()
return current
[docs] def is_play_enabled(self):
''':returns: True when calling play() will have no effect'''
return not (self.player.is_playing() and self.__disable_new_track_when_playing)
[docs] def play(self, track=None):
"""
Starts queue processing with the given
track preceding the queue content
:param track: the track to play
:type track: :class:`xl.trax.Track`
"""
if self.player.is_playing():
if not track or self.__disable_new_track_when_playing:
return
if not track:
track = self.current
if track:
self.player.play(track)
if self.__remove_item_on_playback and not self.__remove_item_after_playback:
try:
del self[self.index(track)]
except ValueError:
pass
else:
self.next()
[docs] def queue_length(self):
"""
Returns the number of tracks left to play in the queue's
internal playlist.
"""
if self.__remove_item_on_playback:
return len(self)
else:
if not self.__queue_has_tracks:
return -1
else:
return len(self) - (self.current_position + 1)
def __set_queue_has_tracks(self, value):
if value != self.__queue_has_tracks_val:
oldpos = self.current_position
self.__queue_has_tracks_val = value
event.log_event(
"playlist_current_position_changed",
self,
(self.current_position, oldpos),
)
# Internal value indicating whether the internal queue has tracks left to play
__queue_has_tracks = property(
lambda self: self.__queue_has_tracks_val, __set_queue_has_tracks
)
def __setitem__(self, i, value):
"""
Overrides the playlist.Playlist list API.
Allows us to ensure that when a track is added to an empty queue,
we play it. Or not, depending on what the user wants.
"""
old_len = playlist.Playlist.__len__(self)
playlist.Playlist.__setitem__(self, i, value)
# if nothing is queued, queue this track up
if self.current_position == -1:
if isinstance(i, slice):
self.current_position = i.indices(len(self))[0] - 1
else:
self.current_position = i - 1
self.__queue_has_tracks = True
if (
old_len == 0
and settings.get_option('queue/enqueue_begins_playback', True)
and old_len < playlist.Playlist.__len__(self)
):
self.play()
def _save_player_state(self, location):
state = {}
state['state'] = self.player.get_state()
state['position'] = self.player.get_time()
with open(location, 'wb') as f:
pickle.dump(state, f, protocol=2)
@common.threaded
def _restore_player_state(self, location):
if not settings.get_option("%s/resume_playback" % self.player._name, True):
return
try:
with open(location, 'rb') as f:
state = pickle.load(f)
except Exception:
return
for req in ['state', 'position']:
if req not in state:
return
self._do_restore_player_state(state)
@common.idle_add()
def _do_restore_player_state(self, state):
if state['state'] in ['playing', 'paused']:
start_at = None
if state['position'] is not None:
start_at = state['position']
paused = state['state'] == 'paused' or settings.get_option(
"%s/resume_paused" % self.player._name, False
)
self.player.play(
self.current_playlist.get_current(), start_at=start_at, paused=paused
)
def _calculate_next_track(self):
player_track = self.player.current
try:
real_position = self.index(player_track)
except:
real_position = -1
if real_position == 0:
if len(self) > 1:
return self[1]
elif len(self) > 0:
return self[0]
return None