small fixes in player backend
[panucci.git] / src / panucci / playlist.py
blobbdbe4a3c526db1ef4721925635a9740991a02825
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 position = self.get_seek_to()
77 self.set_position_duration(position, 0)
78 self.do_seek(position)
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 get_current_item(self):
185 return self.__queue.current_item
187 def quit(self):
188 self.__log.debug('quit() called.')
189 #self.notify('stop-requested', caller=self.quit)
190 self.stop()
191 if self.__queue.modified:
192 self.__log.info('Queue modified, saving temporary playlist')
193 self.save_temp_playlist()
195 ######################################
196 # Bookmark-related functions
197 ######################################
199 def __load_from_bookmark(self, item_id, bookmark):
200 new_pos = self.__queue.index(item_id)
201 same_pos = self.__queue.current_item_position == new_pos
202 if not same_pos:
203 self.__queue.current_item_position = new_pos
205 if bookmark is None:
206 seek_position = 0
207 else:
208 seek_position = bookmark.seek_position
209 self.__queue.current_item.seek_to = seek_position
211 if same_pos:
212 if self.playing or seek_position == 0:
213 self.do_seek(seek_position)
214 if not self.playing:
215 self.play()
217 return True
219 def load_from_bookmark_id(self, item_id=None, bookmark_id=None, set_seek_to=True):
220 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
221 if str(self.__queue.current_item) != item_id and not self.null:
222 #self.notify('stop-requested', caller=self.load_from_bookmark_id)
223 self.stop(False, set_seek_to)
225 if item is not None:
226 return self.__load_from_bookmark( str(item), bookmark )
227 else:
228 self.__log.warning(
229 'item_id=%s,bookmark_id=%s not found', item_id, bookmark_id )
230 return False
232 def find_resume_bookmark(self):
233 """ Find a resume bookmark in the queue """
234 for item in self.__queue:
235 for bookmark in item.bookmarks:
236 if bookmark.is_resume_position:
237 return str(item), str(bookmark)
238 else:
239 return None, None
241 def load_from_resume_bookmark(self):
242 item_id, bookmark_id = self.find_resume_bookmark()
243 if None in ( item_id, bookmark_id ):
244 self.__log.info('No resume bookmark found.')
245 return False
246 else:
247 return self.load_from_bookmark_id(item_id, bookmark_id, False)
249 def save_bookmark( self, bookmark_name, position ):
250 if self.__queue.current_item is not None:
251 self.__queue.current_item.save_bookmark( bookmark_name, position,
252 resume_pos=False )
253 self.notify( 'bookmark_added', str(self.__queue.current_item),
254 bookmark_name, position, caller=self.save_bookmark )
256 def update_bookmark(self, item_id, bookmark_id, name=None, seek_pos=None):
257 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
259 if item is None:
260 self.__log.warning('No such item id (%s)', item_id)
261 return False
263 if bookmark_id is not None and bookmark is None:
264 self.__log.warning('No such bookmark id (%s)', bookmark_id)
265 return False
267 if bookmark_id is None:
268 if name and item.title != name:
269 item.title = name
270 self.__queue.modified = True
271 if self.__queue.current_item == item:
272 self.notify( 'new-metadata-available',
273 caller=self.update_bookmark )
274 else:
275 bookmark.timestamp = time.time()
277 if name is not None:
278 bookmark.bookmark_name = name
280 if seek_pos is not None:
281 bookmark.seek_position = seek_pos
283 db.update_bookmark(bookmark)
285 return True
287 def update_bookmarks(self):
288 """ Updates the database entries for items that have been modified """
289 for item in self.__queue:
290 if item.is_modified:
291 self.__log.debug('Playlist Item "%s" is modified, updating bookmarks', item)
292 item.update_bookmarks()
293 item.is_modified = False
295 def remove_bookmark( self, item_id, bookmark_id ):
296 item = self.__queue.get_item(item_id)
298 if item is None:
299 self.__log.info('Cannot find item with id: %s', item_id)
300 return False
302 if bookmark_id is None:
303 if self.__queue.current_item_position == self.__queue.index(item):
304 is_end = self.end_of_playlist()
305 self.stop()
306 item.delete_bookmark(None)
307 self.__queue.remove(item)
308 if not is_end:
309 self.__queue.current_item_position = self.__queue.current_item_position
310 elif not self.is_empty:
311 self.__queue.current_item_position = 0
312 else:
313 self.reset_playlist()
314 else:
315 item.delete_bookmark(None)
316 self.__queue.remove(item)
317 else:
318 item.delete_bookmark(bookmark_id)
320 return True
322 def delete_all_bookmarks(self):
323 db.delete_all_bookmarks()
324 for item in self.__queue.get_items():
325 item.delete_bookmark(None)
327 def remove_resume_bookmarks(self):
328 item_id, bookmark_id = self.find_resume_bookmark()
330 if None in ( item_id, bookmark_id ):
331 return False
332 else:
333 return self.remove_bookmark( item_id, bookmark_id )
335 def add_bookmark_at_current_position( self, label=None ):
336 """ Adds a bookmark at the current position
337 Returns: (bookmark lable string, position in nanoseconds)
340 default_label, position = self.get_formatted_position()
341 label = default_label if label is None else label
342 self.save_bookmark( label, position )
343 self.__log.info('Added bookmark: %s - %d', label, position)
344 return label, position
346 def move_item( self, from_row, to_row ):
347 self.__log.info('Moving item from position %d to %d', from_row, to_row)
348 assert isinstance(from_row, int) and isinstance(to_row, int)
349 self.__queue.move_item(from_row, to_row)
351 #####################################
352 # Model-builder functions
353 #####################################
355 def get_item_by_id(self, item_id):
356 """ Gets a PlaylistItem from it's unique id """
358 item, bookmark = self.__queue.get_bookmark(item_id, None)
359 if item is None:
360 self.__log.warning('Cannot get item for id: %s', item_id)
362 return item
364 def get_playlist_item_ids(self):
365 """ Returns an iterator which yields a tuple which contains the
366 item's unique ID and a dict of interesting data (currently
367 just the title). """
369 for item in self.__queue:
370 yield str(item), { 'title' : item.title }
372 def get_bookmarks_from_item_id(self, item_id, include_resume_marks=False):
373 """ Returns an iterator which yields the following data regarding a
374 bookmark: ( bookmark id, a custom name, the seek position ) """
376 item = self.get_item_by_id( item_id )
377 if item is not None:
378 for bkmk in item.bookmarks:
379 if not bkmk.is_resume_position or include_resume_marks:
380 yield str(bkmk), bkmk.bookmark_name, bkmk.seek_position
382 ######################################
383 # File-related convenience functions
384 ######################################
386 def get_current_position(self):
387 """ Returns the saved position for the current
388 file or 0 if no file is available"""
389 if not self.is_empty:
390 return self.__queue.current_item.seek_to
391 else:
392 return 0
394 def get_current_filetype(self):
395 """ Returns the filetype of the current
396 file or None if no file is available """
398 if not self.is_empty:
399 return self.__queue.current_item.filetype
401 def get_file_metadata(self):
402 """ Return the metadata associated with the current FileObject """
403 if not self.is_empty:
404 return self.__queue.current_item.metadata
405 else:
406 return {}
408 def get_current_filepath(self):
409 if not self.is_empty:
410 return self.__queue.current_item.filepath
412 def get_recent_files(self, max_files=10):
413 files = db.get_latest_files()
415 if len(files) > max_files:
416 return files[:max_files]
417 else:
418 return files
420 ##################################
421 # File importing functions
422 ##################################
424 def load(self, filepath):
425 """ Detects filepath's filetype then loads it using
426 the appropriate loader function """
427 self.__log.debug('Attempting to load %s', filepath)
428 _play = self.is_empty or (self.end_of_playlist() and not self.get_position_duration()[0])
429 if self.__queue.is_empty():
430 _position = 0
431 else:
432 _position = len(self.__queue)
433 error = False
435 if os.path.isdir(filepath):
436 self.load_directory(filepath, True)
437 else:
438 parsers = {'m3u': playlistformat.M3U_Playlist, 'pls': playlistformat.PLS_Playlist}
439 extension = util.detect_filetype(filepath)
440 if parsers.has_key(extension): # importing a playlist
441 self.__log.info('Loading playlist file (%s)', extension)
442 parser = parsers[extension](filepath, self.__queue)
443 self.filepath = filepath
444 self._id = None
445 self.__queue.playlist_id = self.id
446 if parser.parse(filepath):
447 self.__queue = parser.get_queue()
448 self.__file_queued( filepath, True, False )
449 else:
450 return False
451 else: # importing a single file
452 error = not self.append(filepath, notify=False)
454 # if we let the queue emit a current_item_changed signal (which will
455 # happen if load_from_bookmark changes the current track), the player
456 # will start playing and ingore the resume point
457 if _play:
458 self.__queue.set_current_item_position(_position)
459 if _position == 0:
460 self.__queue.disable_notifications = True
461 self.load_from_resume_bookmark()
462 self.__queue.disable_notifications = False
463 self.__queue.modified = True
464 #self.notify( 'stop-requested', caller=self.load )
465 if not self.null:
466 self.stop(False, False)
467 self.new_track_loaded()
468 #self.notify( 'new-track-loaded', caller=self.load )
469 #self.notify( 'new-metadata-available', caller=self.load )
471 return not error
473 def load_last_played(self):
474 recent = self.get_recent_files(max_files=1)
475 if recent:
476 self.load(recent[0])
477 return bool(recent)
479 def __file_queued(self, filepath, successfull, notify):
480 if successfull:
481 self.notify( 'file_queued', filepath, successfull, notify,
482 caller=self.__file_queued )
484 return successfull
486 def append(self, filepath, notify=True):
487 self.__log.debug('Attempting to queue file: %s', filepath)
488 success = self.__queue.append(
489 playlistformat.PlaylistItem.create_by_filepath(filepath, filepath) )
490 return self.__file_queued( filepath, success, notify)
492 def insert(self, position, filepath ):
493 self.__log.debug(
494 'Attempting to insert %s at position %s', filepath, position )
495 return self.__file_queued( filepath, self.__queue.insert( position,
496 playlistformat.PlaylistItem.create_by_filepath(filepath, filepath)), True )
498 def load_directory(self, directory, append=False):
499 self.__log.debug('Attempting to load directory "%s"', directory)
501 if not os.path.isdir(directory):
502 self.__log.warning('"%s" is not a directory.', directory)
503 return False
505 if not append:
506 if self.notify( 'playlist-to-be-overwritten',
507 caller=self.load_directory ):
508 self.reset_playlist()
509 else:
510 self.__log.info('Directory load aborted by user.')
511 return False
513 self.filepath = panucci.PLAYLIST_FILE
514 self.__queue.playlist_id = self.id
516 items = []
517 potential_items = os.listdir(directory)
518 potential_items.sort()
520 for item in potential_items:
521 filepath = os.path.join( directory, item )
522 if os.path.isfile(filepath) and is_supported(filepath):
523 items.append(filepath)
525 items.sort()
526 for item in items:
527 self.append( item, notify=False )
529 if not append:
530 self.on_queue_current_item_changed()
532 return True
534 ##################################
535 # Playlist controls
536 ##################################
538 def set_seek_to(self, seek_to, seconds=False):
539 """Set the seek-to position for the current track"""
540 if seconds:
541 seek_to = (10**9) * seek_to
542 if not self.is_empty:
543 self.get_current_item().seek_to = seek_to
545 def set_seek_to_from_current(self):
546 self.set_seek_to(self.get_position_duration()[0])
548 def get_seek_to(self, reset=True):
549 """Get the seek-to position for the current track"""
550 seek_to = self.__queue.current_item.seek_to
551 if reset:
552 self.get_current_item().seek_to = 0
553 return seek_to
555 def skip(self, loop=True, skip_by=None, skip_to=None, set_seek_to=True):
556 """ Skip to another track in the playlist.
557 Use either skip_by or skip_to, skip_by has precedence.
558 skip_to: skip to a known playlist position
559 skip_by: skip by n number of episodes (positive or negative)
560 loop: loop if the track requested lays out of
561 the 0 to queue_length-1 boundary.
563 if not self.__queue:
564 return False
566 current_item_position = self.__queue.current_item_position
568 if skip_by is not None:
569 skip = current_item_position + skip_by
570 elif skip_to is not None:
571 skip = skip_to
572 else:
573 skip = 0
574 self.__log.warning('No skip method provided...')
576 if not 0 <= skip < self.queue_length:
577 #self.notify( 'end-of-playlist', loop, caller=self.skip )
579 if not loop:
580 self.__log.warning( "Can't skip to non-existant file w/o loop."
581 " (requested=%d, total=%d)", skip,
582 self.queue_length )
583 return False
584 else:
585 # If skip_by is given as an argument, we assume the user knows
586 # what they're doing. Ie. if the queue length is 5, current
587 # track is 3 and they pass skip_by=-9, then they'll end up
588 # at 4. On the other hand, if skip_to is passed then we skip
589 # back to 0 because in that case the user must enter a number
590 # from 0 to queue_length-1, anything else is an error.
591 if skip_by is not None:
592 skip %= self.queue_length
593 else:
594 skip = 0
596 #self.notify('stop-requested', caller=self.skip)
597 self.stop(False, set_seek_to)
598 self.__queue.current_item_position = skip
599 self.__log.debug( 'Skipping to file %d (%s)', skip,
600 self.__queue.current_item.filepath )
602 return True
604 def next(self, loop=False, set_seek_to=True):
605 """ Move the playlist to the next track.
606 False indicates end of playlist. """
607 return self.skip(loop, 1, None, set_seek_to)
609 def prev(self, set_seek_to=True):
610 """ Same as next() except moves to the previous track. """
611 return self.skip(False, -1, None, set_seek_to)
613 def last(self, set_seek_to=True):
614 """ Plays last file in queue. """
615 skip_to = len(self.__queue.get_items()) - 1
616 return self.skip(False, None, skip_to, set_seek_to)
618 def random(self, set_seek_to=True):
619 """ Plays random file in queue. """
620 skip_to = random.choice(range(len(self.__queue.get_items())))
621 return self.skip(False, None, skip_to, set_seek_to)
623 ##################################
624 # Player controls
625 ##################################
627 def play(self):
628 """ This gets called by the player to get
629 the last time the file was paused """
630 self.__player.play()
632 def pause(self):
633 """ Called whenever the player is paused """
634 #position = self.get_position_duration()[0]
635 #self.__queue.current_item.seek_to = position
636 self.__player.pause()
638 def play_pause_toggle(self):
639 if self.current_filepath:
640 self.__player.play_pause_toggle()
642 def stop(self, save_resume_point=True, set_seek_to=True):
643 """ This should be run when the program is closed
644 or if the user switches playlists """
645 position, duration = self.get_position_duration()
646 self.remove_resume_bookmarks()
647 self.on_player_stopped()
648 self.__player.on_stop_requested()
649 if not self.is_empty and save_resume_point:
650 self.get_current_item().save_bookmark(_('Auto Bookmark'), position, True)
651 if not self.is_empty and set_seek_to:
652 self.set_seek_to(position)
654 def on_player_eof(self):
655 play_mode = self.config.get("options", "play_mode")
656 if play_mode == "single":
657 if not self.config.getboolean("options", "stay_at_end"):
658 self.stop(set_seek_to=False)
659 self.notify('end-of-playlist', False, caller=self.on_player_eof)
660 elif play_mode == "random":
661 self.random(set_seek_to=False)
662 elif play_mode == "repeat":
663 self.next(loop=True, set_seek_to=False)
664 else:
665 if self.end_of_playlist():
666 if not self.config.getboolean("options", "stay_at_end"):
667 self.stop(set_seek_to=False)
668 else:
669 self.next(loop=False, set_seek_to=False)
671 def on_player_playing(self):
672 self.__player.on_playing(self.get_seek_to())
673 self.notify("playing", caller=self.on_player_playing)
675 def on_player_paused(self, position, duration):
676 self.notify("paused", position, duration, caller=self.on_player_paused)
678 def on_player_stopped(self):
679 self.notify("stopped", caller=self.on_player_stopped)
681 def do_seek(self, from_beginning=None, from_current=None, percent=None):
682 resp = None
683 if from_beginning is not None:
684 resp = self.__player.do_seek(from_beginning=from_beginning)
685 elif from_current is not None:
686 if not self.config.getboolean("options", "seek_back") or self.start_of_playlist() or from_current > 0:
687 resp = self.__player.do_seek(from_current=from_current)
688 else:
689 pos_int, dur_int = self.get_position_duration()
690 if pos_int + from_current >= 0:
691 resp = self.__player.do_seek(from_current=from_current)
692 else:
693 self.prev()
694 pos_int, dur_int = self.get_position_duration()
695 resp = self.__player.do_seek(from_beginning=dur_int+from_current)
696 elif percent is not None:
697 resp = self.__player.do_seek(percent=percent)
698 return resp
700 def get_position_duration(self):
701 return self.__player.get_position_duration()
703 def set_position_duration(self, position, duration):
704 self.__player.set_position_duration(position, duration)
706 @property
707 def playing(self):
708 return self.__player.playing
710 @property
711 def paused(self):
712 return self.__player.paused
714 @property
715 def stopped(self):
716 return self.__player.stopped
718 @property
719 def null(self):
720 return self.__player.null
722 @property
723 def seeking(self):
724 return self.__player.seeking
726 def new_track_loaded(self):
727 self.__player.on_new_track(self.current_filepath)
728 self.notify('new-track-loaded', caller=self.new_track_loaded)
729 self.notify( 'new-metadata-available', caller=self.new_track_loaded )
731 def reset_player(self):
732 self.__player.on_reset_playlist()
734 class Queue(list, ObservableService):
735 """ A Simple list of PlaylistItems """
737 signals = [ 'current_item_changed', ]
739 def __init__(self, playlist_id):
740 self.__log = logging.getLogger('panucci.playlist.Queue')
741 ObservableService.__init__(self, self.signals, self.__log)
743 self.playlist_id = playlist_id
744 self.modified = False # Has the queue been modified?
745 self.disable_notifications = False
746 self.__current_item_position = 0
747 # This is a hack and WILL BE REPLACED WITH SOMETHING BETTER.
748 # it's here to speed up the get_item function
749 self.__mapping_dict = {}
750 list.__init__(self)
752 def __get_current_item_position(self):
753 return self.__current_item_position
755 def __set__current_item_position(self, new_value):
757 # set the new position before notify()'ing
758 # or else we'll end up load the old file's metadata
759 old_value = self.__current_item_position
760 self.__current_item_position = new_value
762 if old_value != new_value:
763 self.__log.debug( 'Current item changed from %d to %d',
764 old_value, new_value )
765 if not self.disable_notifications:
766 self.notify( 'current_item_changed',
767 caller=self.__set__current_item_position )
768 else:
769 self.__log.debug( 'Current item reloaded')
770 if not self.disable_notifications:
771 self.notify( 'current_item_changed',
772 caller=self.__set__current_item_position )
774 current_item_position = property(
775 __get_current_item_position, __set__current_item_position )
777 def __count_dupe_items(self, subset, item):
778 # Count the number of duplicate items (by filepath only) in a list
779 tally = 0
780 for i in subset:
781 tally += int( i.filepath == item.filepath )
782 return tally
784 def __prep_item(self, item):
785 """ Do some error checking and other stuff that's
786 common to the insert and append functions """
788 assert isinstance( item, playlistformat.PlaylistItem )
789 item.playlist_id = self.playlist_id
791 if '://' in item.filepath or (os.path.isfile(item.filepath) and \
792 is_supported(item.filepath)):
793 self.modified = True
794 return True
795 else:
796 self.__log.warning(
797 'File not found or not supported: %s', item.filepath )
799 return False
801 @property
802 def current_item(self):
803 if len(self) > 0:
804 if self.current_item_position >= len(self):
805 self.__log.info( 'Current item position is greater '
806 'than queue length, resetting to 0.' )
807 self.current_item_position = 0
809 return self[self.current_item_position]
810 else:
811 self.__log.info('Queue is empty...')
813 def move_item(self, from_pos, to_pos):
814 old_current_item = self.current_item_position
816 temp = self[from_pos]
817 self.remove(str(temp))
818 self.insert(to_pos, temp)
820 if old_current_item == from_pos:
821 self.__current_item_position = to_pos
823 def clear(self):
824 """ Reset the the queue to a known state """
826 try:
827 items = self.__mapping_dict.values()
828 for item in items:
829 list.remove(self, item)
830 except:
831 pass
832 self[:] = []
833 self.playlist_id = None
834 self.modified = True
835 self.__current_item_position = 0
836 self.__mapping_dict = {}
838 def get_item(self, item_id):
839 return self.__mapping_dict.get(item_id)
841 def get_items(self):
842 return self.__mapping_dict.values()
844 def is_empty(self):
845 if self.__mapping_dict:
846 return False
847 else:
848 return True
850 def get_bookmark(self, item_id, bookmark_id):
851 item = self.get_item(item_id)
853 if item is None:
854 self.__log.warning(
855 'Item with id "%s" not found, scanning for item...', item_id )
857 for item_ in self:
858 if item_.bookmarks.count(bookmark_id):
859 item = item_
860 break
862 if item is None: return None, None
864 if item.get_bookmark(bookmark_id):
865 return item, item.get_bookmark(bookmark_id)
866 else:
867 return item, None
869 def set_new_playlist_id(self, id):
870 self.playlist_id = id
871 for item in self:
872 item.playlist_id = id
873 for bookmark in item.bookmarks:
874 bookmark.playlist_id = id
875 bookmark.save()
877 def insert(self, position, item):
878 if not self.__prep_item(item):
879 return False
881 item.duplicate_id = self[:position].count(item)
883 if self.__count_dupe_items(self[position:], item):
884 for i in self[position:]:
885 if i.filepath == item.filepath:
886 i.is_modified = True
887 i.duplicate_id += 1
889 # to be safe rebuild self.__mapping_dict
890 self.__mapping_dict = dict([(str(i),i) for i in self])
891 elif not self.__count_dupe_items(self[:position], item):
892 # there are no other items like this one so it's *safe* to load
893 # bookmarks without a potential conflict, but there's a good chance
894 # that there aren't any bookmarks to load (might be useful in the
895 # event of a crash)...
896 item.load_bookmarks()
898 if position <= self.current_item_position:
899 self.__current_item_position += 1
901 self.__mapping_dict[str(item)] = item
902 list.insert(self, position, item)
903 return True
905 def append(self, item):
906 if not self.__prep_item(item):
907 return False
909 item.playlist_id = self.playlist_id
910 item.duplicate_id = self.__count_dupe_items(self, item)
911 item.load_bookmarks()
913 self.__mapping_dict[str(item)] = item
914 list.append(self, item)
915 return True
917 def remove(self, item):
918 if self.count(item):
919 self.modified = True
921 if self.index(item) < self.current_item_position:
922 self.__current_item_position -= 1
924 del self.__mapping_dict[str(item)]
925 list.remove(self, item)
927 def extend(self, items):
928 self.__log.warning('FIXME: extend not supported yet...')
930 def pop(self, item):
931 self.__log.warning('FIXME: pop not supported yet...')
933 def set_current_item_position(self, position):
934 self.__current_item_position = position