enhanced seeking
[panucci.git] / src / panucci / playlist.py
blobde437aa94b787d4e0768ba4242bf663474bfe3f0
1 # -*- coding: utf-8 -*-
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2011 The Panucci Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
19 from __future__ import absolute_import
21 import time
22 import os
23 import logging
24 import random
26 import panucci
27 from panucci.dbsqlite import db
28 from panucci.services import ObservableService
29 from panucci.dbusinterface import interface
30 from panucci import playlistformat
31 from panucci import player
32 from panucci import util
34 def is_supported(uri):
35 filename, extension = os.path.splitext(uri)
36 if extension.startswith('.'):
37 extension = extension[1:]
39 return (extension.lower() in panucci.EXTENSIONS)
41 class Playlist(ObservableService):
42 signals = [ 'new-track-loaded', 'new-metadata-available', 'file_queued',
43 'bookmark_added', 'seek-requested', 'end-of-playlist',
44 'playlist-to-be-overwritten', 'stop-requested', 'reset-playlist',
45 'stopped', 'playing', 'paused' ]
47 def __init__(self, config):
48 self.__log = logging.getLogger('panucci.playlist.Playlist')
49 ObservableService.__init__(self, self.signals, self.__log)
50 self.config = config
51 self.filepath = panucci.PLAYLIST_FILE
52 self._id = None
54 self.__player = player.PanucciPlayer(self.config)
55 self.__player.register('eof', self.on_player_eof)
56 self.__player.register('playing', self.on_player_playing)
57 self.__player.register('paused', self.on_player_paused)
58 self.__player.register('stopped', self.on_player_stopped)
59 self.__queue = Queue(self.id)
60 self.__queue.register('current_item_changed', self.on_queue_current_item_changed)
62 # Register the d-bus interface only once we're ready
63 interface.register_playlist(self)
65 def init(self, filepath=None):
66 """ Start playing the current file in the playlist or a custom file.
67 This should be called by the UI once it has initialized.
69 Params: filepath is an optional filepath to the first file that
70 should be loaded/played
71 Returns: Nothing
72 """
74 if filepath is None or not self.load( filepath ):
75 self.load_last_played()
76 self.set_position_duration(self.get_current_position(), 0)
77 self.do_seek(self.get_current_position())
78 self.set_seek_to(0)
79 else:
80 self.play()
82 def reset_playlist(self):
83 """ Sets the playlist to a default "known" state """
85 #self.notify('stop-requested', caller=self.reset_playlist)
86 self.stop()
87 self.filepath = None
88 self._id = None
89 self.__queue.clear()
90 self.reset_player()
91 self.notify('reset-playlist', caller=self.reset_playlist)
93 @property
94 def id(self):
95 if self.filepath is None:
96 self.__log.warning("Can't get playlist id without having filepath")
97 elif self._id is None:
98 self._id = db.get_playlist_id( self.filepath, True, True )
100 return self._id
102 @property
103 def current_filepath(self):
104 """ Get the current file """
105 if not self.is_empty:
106 return self.__queue.current_item.filepath
108 @property
109 def queue_modified(self):
110 return self.__queue.modified
112 @property
113 def queue_length(self):
114 return len(self.__queue)
116 @property
117 def is_empty(self):
118 return not self.__queue
120 def print_queue_layout(self):
121 """ This helps with debugging ;) """
122 for item in self.__queue:
123 print str(item), item.playlist_reported_filepath
124 for bookmark in item.bookmarks:
125 print '\t', str(bookmark), bookmark.bookmark_filepath
127 def save_to_new_playlist(self, filepath, playlist_type='m3u'):
128 self.filepath = filepath
129 self._id = None
131 playlist = { 'm3u': playlistformat.M3U_Playlist, 'pls': playlistformat.PLS_Playlist }
132 if not playlist.has_key(playlist_type):
133 playlist_type = 'm3u' # use m3u by default
134 self.filepath += '.m3u'
136 playlist = playlist[playlist_type](self.filepath, self.__queue)
137 if not playlist.export_items( filepath ):
138 self.__log.error('Error exporting playlist to %s', self.filepath)
139 return False
141 # copy the bookmarks over to new playlist
142 db.remove_all_bookmarks(self.id)
143 self.__queue.set_new_playlist_id(self.id)
145 return True
147 def save_temp_playlist(self):
148 return self.save_to_new_playlist(panucci.PLAYLIST_FILE)
150 def on_queue_current_item_changed(self):
151 if os.path.isfile(self.__queue.current_item.filepath):
152 self.new_track_loaded()
153 #self.notify( 'new-track-loaded', caller=self.on_queue_current_item_changed )
154 #self.notify('new-metadata-available', caller=self.on_queue_current_item_changed)
155 else:
156 #self.player.notify("eof", caller=self.on_queue_current_item_changed)
157 self.on_player_eof()
159 def send_metadata(self):
160 self.notify( 'new-metadata-available', caller=self.send_metadata )
162 def start_of_playlist(self):
163 """Checks if the currently played item is the first"""
164 if self.__queue.current_item_position == 0:
165 return True
167 def end_of_playlist(self):
168 """Checks if the currently played item is the last"""
169 if len(self.__queue.get_items()) - 1 == self.__queue.current_item_position:
170 return True
172 def get_formatted_position(self, pos=None):
173 """ Gets the current position and converts it to a human-readable str.
175 Returns: (bookmark lable string, position in nanoseconds)
178 if pos is None:
179 (pos, dur) = self.get_position_duration()
181 text = util.convert_ns(pos)
182 return (text, pos)
184 def quit(self):
185 self.__log.debug('quit() called.')
186 #self.notify('stop-requested', caller=self.quit)
187 self.stop()
188 if self.__queue.modified:
189 self.__log.info('Queue modified, saving temporary playlist')
190 self.save_temp_playlist()
192 ######################################
193 # Bookmark-related functions
194 ######################################
196 def __load_from_bookmark(self, item_id, bookmark):
197 new_pos = self.__queue.index(item_id)
198 same_pos = self.__queue.current_item_position == new_pos
199 if not same_pos:
200 self.__queue.current_item_position = new_pos
202 if bookmark is None:
203 seek_position = 0
204 else:
205 seek_position = bookmark.seek_position
206 self.__queue.current_item.seek_to = seek_position
208 if same_pos:
209 if self.playing or seek_position == 0:
210 self.do_seek(seek_position)
211 if not self.playing:
212 self.play()
214 return True
216 def load_from_bookmark_id( self, item_id=None, bookmark_id=None ):
217 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
218 if str(self.__queue.current_item) != item_id:
219 #self.notify('stop-requested', caller=self.load_from_bookmark_id)
220 self.stop()
222 if item is not None:
223 return self.__load_from_bookmark( str(item), bookmark )
224 else:
225 self.__log.warning(
226 'item_id=%s,bookmark_id=%s not found', item_id, bookmark_id )
227 return False
229 def find_resume_bookmark(self):
230 """ Find a resume bookmark in the queue """
231 for item in self.__queue:
232 for bookmark in item.bookmarks:
233 if bookmark.is_resume_position:
234 return str(item), str(bookmark)
235 else:
236 return None, None
238 def load_from_resume_bookmark(self):
239 item_id, bookmark_id = self.find_resume_bookmark()
240 if None in ( item_id, bookmark_id ):
241 self.__log.info('No resume bookmark found.')
242 return False
243 else:
244 return self.load_from_bookmark_id( item_id, bookmark_id )
246 def save_bookmark( self, bookmark_name, position ):
247 if self.__queue.current_item is not None:
248 self.__queue.current_item.save_bookmark( bookmark_name, position,
249 resume_pos=False )
250 self.notify( 'bookmark_added', str(self.__queue.current_item),
251 bookmark_name, position, caller=self.save_bookmark )
253 def update_bookmark(self, item_id, bookmark_id, name=None, seek_pos=None):
254 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
256 if item is None:
257 self.__log.warning('No such item id (%s)', item_id)
258 return False
260 if bookmark_id is not None and bookmark is None:
261 self.__log.warning('No such bookmark id (%s)', bookmark_id)
262 return False
264 if bookmark_id is None:
265 if name and item.title != name:
266 item.title = name
267 self.__queue.modified = True
268 if self.__queue.current_item == item:
269 self.notify( 'new-metadata-available',
270 caller=self.update_bookmark )
271 else:
272 bookmark.timestamp = time.time()
274 if name is not None:
275 bookmark.bookmark_name = name
277 if seek_pos is not None:
278 bookmark.seek_position = seek_pos
280 db.update_bookmark(bookmark)
282 return True
284 def update_bookmarks(self):
285 """ Updates the database entries for items that have been modified """
286 for item in self.__queue:
287 if item.is_modified:
288 self.__log.debug(
289 'Playlist Item "%s" is modified, updating bookmarks', item)
290 item.update_bookmarks()
291 item.is_modified = False
293 def remove_bookmark( self, item_id, bookmark_id ):
294 item = self.__queue.get_item(item_id)
296 if item is None:
297 self.__log.info('Cannot find item with id: %s', item_id)
298 return False
300 if bookmark_id is None:
301 if self.__queue.current_item_position == self.__queue.index(item):
302 is_end = self.end_of_playlist()
303 self.stop()
304 item.delete_bookmark(None)
305 self.__queue.remove(item)
306 if not is_end:
307 self.__queue.current_item_position = self.__queue.current_item_position
308 elif not self.is_empty:
309 self.__queue.current_item_position = 0
310 else:
311 self.reset_playlist()
312 else:
313 item.delete_bookmark(None)
314 self.__queue.remove(item)
315 else:
316 item.delete_bookmark(bookmark_id)
318 return True
320 def delete_all_bookmarks(self):
321 db.delete_all_bookmarks()
322 for item in self.__queue.get_items():
323 item.delete_bookmark(None)
325 def remove_resume_bookmarks(self):
326 item_id, bookmark_id = self.find_resume_bookmark()
328 if None in ( item_id, bookmark_id ):
329 return False
330 else:
331 return self.remove_bookmark( item_id, bookmark_id )
333 def add_bookmark_at_current_position( self, label=None ):
334 """ Adds a bookmark at the current position
335 Returns: (bookmark lable string, position in nanoseconds)
338 default_label, position = self.get_formatted_position()
339 label = default_label if label is None else label
340 self.save_bookmark( label, position )
341 self.__log.info('Added bookmark: %s - %d', label, position)
342 return label, position
344 def move_item( self, from_row, to_row ):
345 self.__log.info('Moving item from position %d to %d', from_row, to_row)
346 assert isinstance(from_row, int) and isinstance(to_row, int)
347 self.__queue.move_item(from_row, to_row)
349 #####################################
350 # Model-builder functions
351 #####################################
353 def get_item_by_id(self, item_id):
354 """ Gets a PlaylistItem from it's unique id """
356 item, bookmark = self.__queue.get_bookmark(item_id, None)
357 if item is None:
358 self.__log.warning('Cannot get item for id: %s', item_id)
360 return item
362 def get_playlist_item_ids(self):
363 """ Returns an iterator which yields a tuple which contains the
364 item's unique ID and a dict of interesting data (currently
365 just the title). """
367 for item in self.__queue:
368 yield str(item), { 'title' : item.title }
370 def get_bookmarks_from_item_id(self, item_id, include_resume_marks=False):
371 """ Returns an iterator which yields the following data regarding a
372 bookmark: ( bookmark id, a custom name, the seek position ) """
374 item = self.get_item_by_id( item_id )
375 if item is not None:
376 for bkmk in item.bookmarks:
377 if not bkmk.is_resume_position or include_resume_marks:
378 yield str(bkmk), bkmk.bookmark_name, bkmk.seek_position
380 ######################################
381 # File-related convenience functions
382 ######################################
384 def get_current_position(self):
385 """ Returns the saved position for the current
386 file or 0 if no file is available"""
387 if not self.is_empty:
388 return self.__queue.current_item.seek_to
389 else:
390 return 0
392 def get_current_filetype(self):
393 """ Returns the filetype of the current
394 file or None if no file is available """
396 if not self.is_empty:
397 return self.__queue.current_item.filetype
399 def get_file_metadata(self):
400 """ Return the metadata associated with the current FileObject """
401 if not self.is_empty:
402 return self.__queue.current_item.metadata
403 else:
404 return {}
406 def get_current_filepath(self):
407 if not self.is_empty:
408 return self.__queue.current_item.filepath
410 def get_recent_files(self, max_files=10):
411 files = db.get_latest_files()
413 if len(files) > max_files:
414 return files[:max_files]
415 else:
416 return files
418 ##################################
419 # File importing functions
420 ##################################
422 def load(self, filepath):
423 """ Detects filepath's filetype then loads it using
424 the appropriate loader function """
425 self.__log.debug('Attempting to load %s', filepath)
426 _play = self.__queue.is_empty() or (self.end_of_playlist() and self.null)
427 if self.__queue.is_empty():
428 _position = 0
429 else:
430 _position = len(self.__queue)
431 error = False
433 if os.path.isdir(filepath):
434 self.load_directory(filepath, True)
435 else:
436 parsers = { 'm3u': playlistformat.M3U_Playlist, 'pls': playlistformat.PLS_Playlist }
437 extension = util.detect_filetype(filepath)
438 if parsers.has_key(extension): # importing a playlist
439 self.__log.info('Loading playlist file (%s)', extension)
440 parser = parsers[extension](filepath, self.__queue)
441 self.filepath = filepath
442 self._id = None
443 self.__queue.playlist_id = self.id
444 if parser.parse(filepath):
445 self.__queue = parser.get_queue()
446 self.__file_queued( filepath, True, False )
447 else:
448 return False
449 else: # importing a single file
450 error = not self.append(filepath, notify=False)
452 # if we let the queue emit a current_item_changed signal (which will
453 # happen if load_from_bookmark changes the current track), the player
454 # will start playing and ingore the resume point
455 if _play:
456 self.__queue.set_current_item_position(_position)
457 if _position == 0:
458 self.__queue.disable_notifications = True
459 self.load_from_resume_bookmark()
460 self.__queue.disable_notifications = False
461 self.__queue.modified = True
462 #self.notify( 'stop-requested', caller=self.load )
463 self.stop()
464 self.new_track_loaded()
465 #self.notify( 'new-track-loaded', caller=self.load )
466 #self.notify( 'new-metadata-available', caller=self.load )
468 return not error
470 def load_last_played(self):
471 recent = self.get_recent_files(max_files=1)
472 if recent:
473 self.load(recent[0])
474 return bool(recent)
476 def __file_queued(self, filepath, successfull, notify):
477 if successfull:
478 self.notify( 'file_queued', filepath, successfull, notify,
479 caller=self.__file_queued )
481 return successfull
483 def append(self, filepath, notify=True):
484 self.__log.debug('Attempting to queue file: %s', filepath)
485 success = self.__queue.append(
486 playlistformat.PlaylistItem.create_by_filepath(filepath, filepath) )
487 return self.__file_queued( filepath, success, notify)
489 def insert(self, position, filepath ):
490 self.__log.debug(
491 'Attempting to insert %s at position %s', filepath, position )
492 return self.__file_queued( filepath, self.__queue.insert( position,
493 playlistformat.PlaylistItem.create_by_filepath(filepath, filepath)), True )
495 def load_directory(self, directory, append=False):
496 self.__log.debug('Attempting to load directory "%s"', directory)
498 if not os.path.isdir(directory):
499 self.__log.warning('"%s" is not a directory.', directory)
500 return False
502 if not append:
503 if self.notify( 'playlist-to-be-overwritten',
504 caller=self.load_directory ):
505 self.reset_playlist()
506 else:
507 self.__log.info('Directory load aborted by user.')
508 return False
510 self.filepath = panucci.PLAYLIST_FILE
511 self.__queue.playlist_id = self.id
513 items = []
514 potential_items = os.listdir(directory)
515 potential_items.sort()
517 for item in potential_items:
518 filepath = os.path.join( directory, item )
519 if os.path.isfile(filepath) and is_supported(filepath):
520 items.append(filepath)
522 items.sort()
523 for item in items:
524 self.append( item, notify=False )
526 if not append:
527 self.on_queue_current_item_changed()
529 return True
531 ##################################
532 # Playlist controls
533 ##################################
535 def set_seek_to(self, position):
536 """Set the seek-to position for the current track"""
537 self.__queue.current_item.seek_to = (10**9) * position
539 def get_seek_to(self, reset=True):
540 """Get the seek-to position for the current track"""
541 seek_to = self.__queue.current_item.seek_to
542 if reset:
543 self.__queue.current_item.seek_to = 0
544 return seek_to
546 def skip(self, loop=True, skip_by=None, skip_to=None):
547 """ Skip to another track in the playlist.
548 Use either skip_by or skip_to, skip_by has precedence.
549 skip_to: skip to a known playlist position
550 skip_by: skip by n number of episodes (positive or negative)
551 loop: loop if the track requested lays out of
552 the 0 to queue_length-1 boundary.
554 if not self.__queue:
555 return False
557 current_item_position = self.__queue.current_item_position
559 if skip_by is not None:
560 skip = current_item_position + skip_by
561 elif skip_to is not None:
562 skip = skip_to
563 else:
564 skip = 0
565 self.__log.warning('No skip method provided...')
567 if not 0 <= skip < self.queue_length:
568 #self.notify( 'end-of-playlist', loop, caller=self.skip )
570 if not loop:
571 self.__log.warning( "Can't skip to non-existant file w/o loop."
572 " (requested=%d, total=%d)", skip,
573 self.queue_length )
574 return False
575 else:
576 # If skip_by is given as an argument, we assume the user knows
577 # what they're doing. Ie. if the queue length is 5, current
578 # track is 3 and they pass skip_by=-9, then they'll end up
579 # at 4. On the other hand, if skip_to is passed then we skip
580 # back to 0 because in that case the user must enter a number
581 # from 0 to queue_length-1, anything else is an error.
582 if skip_by is not None:
583 skip %= self.queue_length
584 else:
585 skip = 0
587 #self.notify('stop-requested', caller=self.skip)
588 self.stop()
589 self.__queue.current_item_position = skip
590 self.__log.debug( 'Skipping to file %d (%s)', skip,
591 self.__queue.current_item.filepath )
593 return True
595 def get_current_item(self):
596 return self.__queue.current_item
598 def next(self, loop=False):
599 """ Move the playlist to the next track.
600 False indicates end of playlist. """
601 return self.skip( loop, skip_by=1)
603 def prev(self):
604 """ Same as next() except moves to the previous track. """
605 return self.skip( loop=False, skip_by=-1 )
607 def last(self):
608 """ Plays last file in queue. """
609 skip_to = len(self.__queue.get_items()) - 1
610 return self.skip( False, None, skip_to )
612 def random(self):
613 """ Plays random file in queue. """
614 skip_to = random.choice(range(len(self.__queue.get_items())))
615 return self.skip( False, None, skip_to )
617 ##################################
618 # Player controls
619 ##################################
621 def play(self):
622 """ This gets called by the player to get
623 the last time the file was paused """
624 self.__player.play()
626 def pause(self):
627 """ Called whenever the player is paused """
628 #position = self.get_position_duration()[0]
629 #self.__queue.current_item.seek_to = position
630 self.__player.pause()
632 def play_pause_toggle(self):
633 if self.current_filepath:
634 self.__player.play_pause_toggle()
636 def stop(self, save_resume_point=True):
637 """ This should be run when the program is closed
638 or if the user switches playlists """
639 position = self.get_position_duration()[0]
640 self.remove_resume_bookmarks()
641 self.on_player_stopped()
642 self.__player.on_stop_requested()
643 if not self.is_empty and save_resume_point:
644 self.__queue.current_item.save_bookmark(_('Auto Bookmark'), position, True)
646 def on_player_eof(self):
647 if not self.config.getboolean("options", "stay_at_end"):
648 self.stop()
649 play_mode = self.config.get("options", "play_mode")
650 if play_mode == "single":
651 if not self.config.getboolean("options", "stay_at_end"):
652 self.notify('end-of-playlist', False, caller=self.on_player_eof)
653 elif play_mode == "random":
654 self.random()
655 elif play_mode == "repeat":
656 self.next(True)
657 else:
658 if self.end_of_playlist():
659 if not self.config.getboolean("options", "stay_at_end"):
660 self.next(False)
661 else:
662 self.next(False)
664 def on_player_playing(self):
665 self.__player.on_playing(self.get_seek_to())
666 self.notify("playing", caller=self.on_player_playing)
668 def on_player_paused(self, position, duration):
669 self.notify("paused", position, duration, caller=self.on_player_paused)
671 def on_player_stopped(self):
672 self.notify("stopped", caller=self.on_player_stopped)
674 def do_seek(self, from_beginning=None, from_current=None, percent=None):
675 resp = None
676 if from_beginning is not None:
677 resp = self.__player.do_seek(from_beginning=from_beginning)
678 elif from_current is not None:
679 if not self.config.getboolean("options", "seek_back") or self.start_of_playlist() or from_current > 0:
680 resp = self.__player.do_seek(from_current=from_current)
681 else:
682 pos_int, dur_int = self.get_position_duration()
683 if pos_int + from_current >= 0:
684 resp = self.__player.do_seek(from_current=from_current)
685 else:
686 self.prev()
687 pos_int, dur_int = self.get_position_duration()
688 resp = self.__player.do_seek(from_beginning=dur_int+from_current)
689 elif percent is not None:
690 resp = self.__player.do_seek(percent=percent)
691 return resp
693 def get_position_duration(self):
694 return self.__player.get_position_duration()
696 def set_position_duration(self, position, duration):
697 self.__player.set_position_duration(position, duration)
699 @property
700 def playing(self):
701 return self.__player.playing
703 @property
704 def paused(self):
705 return self.__player.paused
707 @property
708 def stopped(self):
709 return self.__player.stopped
711 @property
712 def null(self):
713 return self.__player.null
715 @property
716 def seeking(self):
717 return self.__player.seeking
719 def new_track_loaded(self):
720 self.__player.on_new_track(self.current_filepath)
721 self.notify('new-track-loaded', caller=self.new_track_loaded)
722 self.notify( 'new-metadata-available', caller=self.new_track_loaded )
724 def reset_player(self):
725 self.__player.on_reset_playlist()
727 class Queue(list, ObservableService):
728 """ A Simple list of PlaylistItems """
730 signals = [ 'current_item_changed', ]
732 def __init__(self, playlist_id):
733 self.__log = logging.getLogger('panucci.playlist.Queue')
734 ObservableService.__init__(self, self.signals, self.__log)
736 self.playlist_id = playlist_id
737 self.modified = False # Has the queue been modified?
738 self.disable_notifications = False
739 self.__current_item_position = 0
740 # This is a hack and WILL BE REPLACED WITH SOMETHING BETTER.
741 # it's here to speed up the get_item function
742 self.__mapping_dict = {}
743 list.__init__(self)
745 def __get_current_item_position(self):
746 return self.__current_item_position
748 def __set__current_item_position(self, new_value):
750 # set the new position before notify()'ing
751 # or else we'll end up load the old file's metadata
752 old_value = self.__current_item_position
753 self.__current_item_position = new_value
755 if old_value != new_value:
756 self.__log.debug( 'Current item changed from %d to %d',
757 old_value, new_value )
758 if not self.disable_notifications:
759 self.notify( 'current_item_changed',
760 caller=self.__set__current_item_position )
761 else:
762 self.__log.debug( 'Current item reloaded')
763 if not self.disable_notifications:
764 self.notify( 'current_item_changed',
765 caller=self.__set__current_item_position )
767 current_item_position = property(
768 __get_current_item_position, __set__current_item_position )
770 def __count_dupe_items(self, subset, item):
771 # Count the number of duplicate items (by filepath only) in a list
772 tally = 0
773 for i in subset:
774 tally += int( i.filepath == item.filepath )
775 return tally
777 def __prep_item(self, item):
778 """ Do some error checking and other stuff that's
779 common to the insert and append functions """
781 assert isinstance( item, playlistformat.PlaylistItem )
782 item.playlist_id = self.playlist_id
784 if '://' in item.filepath or (os.path.isfile(item.filepath) and \
785 is_supported(item.filepath)):
786 self.modified = True
787 return True
788 else:
789 self.__log.warning(
790 'File not found or not supported: %s', item.filepath )
792 return False
794 @property
795 def current_item(self):
796 if len(self) > 0:
797 if self.current_item_position >= len(self):
798 self.__log.info( 'Current item position is greater '
799 'than queue length, resetting to 0.' )
800 self.current_item_position = 0
802 return self[self.current_item_position]
803 else:
804 self.__log.info('Queue is empty...')
806 def move_item(self, from_pos, to_pos):
807 old_current_item = self.current_item_position
809 temp = self[from_pos]
810 self.remove(str(temp))
811 self.insert(to_pos, temp)
813 if old_current_item == from_pos:
814 self.__current_item_position = to_pos
816 def clear(self):
817 """ Reset the the queue to a known state """
819 try:
820 items = self.__mapping_dict.values()
821 for item in items:
822 list.remove(self, item)
823 except:
824 pass
825 self[:] = []
826 self.playlist_id = None
827 self.modified = True
828 self.__current_item_position = 0
829 self.__mapping_dict = {}
831 def get_item(self, item_id):
832 return self.__mapping_dict.get(item_id)
834 def get_items(self):
835 return self.__mapping_dict.values()
837 def is_empty(self):
838 if self.__mapping_dict:
839 return False
840 else:
841 return True
843 def get_bookmark(self, item_id, bookmark_id):
844 item = self.get_item(item_id)
846 if item is None:
847 self.__log.warning(
848 'Item with id "%s" not found, scanning for item...', item_id )
850 for item_ in self:
851 if item_.bookmarks.count(bookmark_id):
852 item = item_
853 break
855 if item is None: return None, None
857 if item.get_bookmark(bookmark_id):
858 return item, item.get_bookmark(bookmark_id)
859 else:
860 return item, None
862 def set_new_playlist_id(self, id):
863 self.playlist_id = id
864 for item in self:
865 item.playlist_id = id
866 for bookmark in item.bookmarks:
867 bookmark.playlist_id = id
868 bookmark.save()
870 def insert(self, position, item):
871 if not self.__prep_item(item):
872 return False
874 item.duplicate_id = self[:position].count(item)
876 if self.__count_dupe_items(self[position:], item):
877 for i in self[position:]:
878 if i.filepath == item.filepath:
879 i.is_modified = True
880 i.duplicate_id += 1
882 # to be safe rebuild self.__mapping_dict
883 self.__mapping_dict = dict([(str(i),i) for i in self])
884 elif not self.__count_dupe_items(self[:position], item):
885 # there are no other items like this one so it's *safe* to load
886 # bookmarks without a potential conflict, but there's a good chance
887 # that there aren't any bookmarks to load (might be useful in the
888 # event of a crash)...
889 item.load_bookmarks()
891 if position <= self.current_item_position:
892 self.__current_item_position += 1
894 self.__mapping_dict[str(item)] = item
895 list.insert(self, position, item)
896 return True
898 def append(self, item):
899 if not self.__prep_item(item):
900 return False
902 item.playlist_id = self.playlist_id
903 item.duplicate_id = self.__count_dupe_items(self, item)
904 item.load_bookmarks()
906 self.__mapping_dict[str(item)] = item
907 list.append(self, item)
908 return True
910 def remove(self, item):
911 if self.count(item):
912 self.modified = True
914 if self.index(item) < self.current_item_position:
915 self.__current_item_position -= 1
917 del self.__mapping_dict[str(item)]
918 list.remove(self, item)
920 def extend(self, items):
921 self.__log.warning('FIXME: extend not supported yet...')
923 def pop(self, item):
924 self.__log.warning('FIXME: pop not supported yet...')
926 def set_current_item_position(self, position):
927 self.__current_item_position = position