huge update to qml
[panucci.git] / src / panucci / playlist.py
blob5c3e0c92092b9a228f03980b6fa5fe917497709d
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 self.new_track_loaded()
153 def send_metadata(self):
154 self.notify( 'new-metadata-available', caller=self.send_metadata )
156 def start_of_playlist(self):
157 """Checks if the currently played item is the first"""
158 if self.__queue.current_item_position == 0:
159 return True
161 def end_of_playlist(self):
162 """Checks if the currently played item is the last"""
163 if len(self.__queue.get_items()) - 1 == self.__queue.current_item_position:
164 return True
166 def get_formatted_position(self, pos=None):
167 """ Gets the current position and converts it to a human-readable str.
169 Returns: (bookmark lable string, position in nanoseconds)
172 if pos is None:
173 (pos, dur) = self.get_position_duration()
175 text = util.convert_ns(pos)
176 return (text, pos)
178 def get_current_item(self):
179 return self.__queue.current_item
181 def quit(self):
182 self.__log.debug('quit() called.')
183 #self.notify('stop-requested', caller=self.quit)
184 self.stop()
185 if self.__queue.modified:
186 self.__log.info('Queue modified, saving temporary playlist')
187 self.save_temp_playlist()
189 ######################################
190 # Bookmark-related functions
191 ######################################
193 def __load_from_bookmark(self, item_id, bookmark):
194 new_pos = self.__queue.index(item_id)
195 same_pos = self.__queue.current_item_position == new_pos
196 if not same_pos:
197 self.__queue.current_item_position = new_pos
199 if bookmark is None:
200 seek_position = 0
201 else:
202 seek_position = bookmark.seek_position
203 self.__queue.current_item.seek_to = seek_position
205 if same_pos:
206 if self.playing or seek_position == 0:
207 self.do_seek(seek_position)
208 if not self.playing:
209 self.play()
211 return True
213 def load_from_bookmark_id(self, item_id=None, bookmark_id=None, set_seek_to=True):
214 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
215 if str(self.__queue.current_item) != item_id and not self.null:
216 #self.notify('stop-requested', caller=self.load_from_bookmark_id)
217 self.stop(False, set_seek_to)
219 if item is not None:
220 return self.__load_from_bookmark( str(item), bookmark )
221 else:
222 self.__log.warning(
223 'item_id=%s,bookmark_id=%s not found', item_id, bookmark_id )
224 return False
226 def find_resume_bookmark(self):
227 """ Find a resume bookmark in the queue """
228 for item in self.__queue:
229 for bookmark in item.bookmarks:
230 if bookmark.is_resume_position:
231 return str(item), str(bookmark)
232 else:
233 return None, None
235 def load_from_resume_bookmark(self):
236 item_id, bookmark_id = self.find_resume_bookmark()
237 if None in ( item_id, bookmark_id ):
238 self.__log.info('No resume bookmark found.')
239 return False
240 else:
241 return self.load_from_bookmark_id(item_id, bookmark_id, False)
243 def save_bookmark( self, bookmark_name, position ):
244 if self.__queue.current_item is not None:
245 self.__queue.current_item.save_bookmark( bookmark_name, position,
246 resume_pos=False )
247 self.notify( 'bookmark_added', str(self.__queue.current_item),
248 bookmark_name, position, caller=self.save_bookmark )
250 def update_bookmark(self, item_id, bookmark_id, name=None, seek_pos=None):
251 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
253 if item is None:
254 self.__log.warning('No such item id (%s)', item_id)
255 return False
257 if bookmark_id is not None and bookmark is None:
258 self.__log.warning('No such bookmark id (%s)', bookmark_id)
259 return False
261 if bookmark_id is None:
262 if name and item.title != name:
263 item.title = name
264 self.__queue.modified = True
265 if self.__queue.current_item == item:
266 self.notify( 'new-metadata-available',
267 caller=self.update_bookmark )
268 else:
269 bookmark.timestamp = time.time()
271 if name is not None:
272 bookmark.bookmark_name = name
274 if seek_pos is not None:
275 bookmark.seek_position = seek_pos
277 db.update_bookmark(bookmark)
279 return True
281 def update_bookmarks(self):
282 """ Updates the database entries for items that have been modified """
283 for item in self.__queue:
284 if item.is_modified:
285 self.__log.debug('Playlist Item "%s" is modified, updating bookmarks', item)
286 item.update_bookmarks()
287 item.is_modified = False
289 def remove_bookmark( self, item_id, bookmark_id ):
290 item = self.__queue.get_item(item_id)
292 if item is None:
293 self.__log.info('Cannot find item with id: %s', item_id)
294 return False
296 if not bookmark_id:
297 if self.__queue.current_item_position == self.__queue.index(item):
298 is_end = self.end_of_playlist()
299 self.stop()
300 item.delete_bookmark(None)
301 self.__queue.remove(item)
302 if not is_end:
303 self.__queue.current_item_position = self.__queue.current_item_position
304 elif not self.is_empty:
305 self.__queue.current_item_position = 0
306 else:
307 self.reset_playlist()
308 else:
309 item.delete_bookmark(None)
310 self.__queue.remove(item)
311 else:
312 item.delete_bookmark(bookmark_id)
314 return True
316 def delete_all_bookmarks(self):
317 db.delete_all_bookmarks()
318 for item in self.__queue.get_items():
319 item.delete_bookmark(None)
321 def remove_resume_bookmarks(self):
322 item_id, bookmark_id = self.find_resume_bookmark()
324 if None in ( item_id, bookmark_id ):
325 return False
326 else:
327 return self.remove_bookmark( item_id, bookmark_id )
329 def add_bookmark_at_current_position( self, label=None ):
330 """ Adds a bookmark at the current position
331 Returns: (bookmark lable string, position in nanoseconds)
334 default_label, position = self.get_formatted_position()
335 label = default_label if label is None else label
336 self.save_bookmark( label, position )
337 self.__log.info('Added bookmark: %s - %d', label, position)
338 return label, position
340 def move_item( self, from_row, to_row ):
341 self.__log.info('Moving item from position %d to %d', from_row, to_row)
342 assert isinstance(from_row, int) and isinstance(to_row, int)
343 self.__queue.move_item(from_row, to_row)
345 #####################################
346 # Model-builder functions
347 #####################################
349 def get_item_by_id(self, item_id):
350 """ Gets a PlaylistItem from it's unique id """
352 item, bookmark = self.__queue.get_bookmark(item_id, None)
353 if item is None:
354 self.__log.warning('Cannot get item for id: %s', item_id)
356 return item
358 def get_playlist_item_ids(self):
359 """ Returns an iterator which yields a tuple which contains the
360 item's unique ID and a dict of interesting data (currently
361 just the title). """
363 for item in self.__queue:
364 yield str(item), { 'title' : item.title }
366 def get_bookmarks_from_item_id(self, item_id, include_resume_marks=False):
367 """ Returns an iterator which yields the following data regarding a
368 bookmark: ( bookmark id, a custom name, the seek position ) """
370 item = self.get_item_by_id( item_id )
371 if item is not None:
372 for bkmk in item.bookmarks:
373 if not bkmk.is_resume_position or include_resume_marks:
374 yield str(bkmk), bkmk.bookmark_name, bkmk.seek_position
376 ######################################
377 # File-related convenience functions
378 ######################################
380 def get_current_position(self):
381 """ Returns the saved position for the current
382 file or 0 if no file is available"""
383 if not self.is_empty:
384 return self.__queue.current_item.seek_to
385 else:
386 return 0
388 def get_current_filetype(self):
389 """ Returns the filetype of the current
390 file or None if no file is available """
392 if not self.is_empty:
393 return self.__queue.current_item.filetype
395 def get_file_metadata(self):
396 """ Return the metadata associated with the current FileObject """
397 if not self.is_empty:
398 return self.__queue.current_item.metadata
399 else:
400 return {}
402 def get_current_filepath(self):
403 if not self.is_empty:
404 return self.__queue.current_item.filepath
406 def get_recent_files(self, max_files=10):
407 files = db.get_latest_files()
409 if len(files) > max_files:
410 return files[:max_files]
411 else:
412 return files
414 ##################################
415 # File importing functions
416 ##################################
418 def load(self, filepath, play=True):
419 """ Detects filepath's filetype then loads it using
420 the appropriate loader function """
421 self.__log.debug('Attempting to load %s', filepath)
422 _play = self.is_empty or (self.end_of_playlist() and not self.get_position_duration()[0])
423 if self.is_empty:
424 _position = 0
425 else:
426 _position = len(self.__queue)
427 error = False
429 if os.path.isdir(filepath):
430 self.load_directory(filepath, True)
431 else:
432 parsers = {'m3u': playlistformat.M3U_Playlist, 'pls': playlistformat.PLS_Playlist}
433 extension = util.detect_filetype(filepath)
434 if parsers.has_key(extension): # importing a playlist
435 self.__log.info('Loading playlist file (%s)', extension)
436 parser = parsers[extension](filepath, self.__queue)
437 self.filepath = filepath
438 self._id = None
439 self.__queue.playlist_id = self.id
440 if parser.parse(filepath):
441 self.__queue = parser.get_queue()
442 self.__file_queued( filepath, True, False )
443 else:
444 return False
445 else: # importing a single file
446 error = not self.append(filepath, notify=False)
448 # if we let the queue emit a current_item_changed signal (which will
449 # happen if load_from_bookmark changes the current track), the player
450 # will start playing and ingore the resume point
451 self.__queue.modified = True
452 if _play and not self.is_empty:
453 if not self.null:
454 self.stop(False, False)
455 if _position == 0:
456 self.__queue.disable_notifications = True
457 self.load_from_resume_bookmark()
458 self.__queue.disable_notifications = False
459 else:
460 self.__queue.set_current_item_position(_position)
461 #self.notify( 'stop-requested', caller=self.load )
462 self.new_track_loaded()
463 if play:
464 self.play()
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], False)
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, seek_to, seconds=False):
536 """Set the seek-to position for the current track"""
537 if seconds:
538 seek_to = (10**9) * seek_to
539 if not self.is_empty:
540 self.get_current_item().seek_to = seek_to
542 def set_seek_to_from_current(self):
543 self.set_seek_to(self.get_position_duration()[0])
545 def reset_all_seek_to(self):
546 self.set_seek_to(self.get_position_duration()[0])
547 for item in self.__queue.get_items():
548 item.seek_to = 0
550 def get_seek_to(self, reset=True):
551 """Get the seek-to position for the current track"""
552 if not self.is_empty:
553 seek_to = self.__queue.current_item.seek_to
554 if reset:
555 self.get_current_item().seek_to = 0
556 else:
557 seek_to = 0
558 return seek_to
560 def skip(self, loop=True, skip_by=None, skip_to=None, set_seek_to=True, play=True):
561 """ Skip to another track in the playlist.
562 Use either skip_by or skip_to, skip_by has precedence.
563 skip_to: skip to a known playlist position
564 skip_by: skip by n number of episodes (positive or negative)
565 loop: loop if the track requested lays out of
566 the 0 to queue_length-1 boundary.
568 if not self.__queue:
569 return False
571 current_item_position = self.__queue.current_item_position
573 if skip_by is not None:
574 skip = current_item_position + skip_by
575 elif skip_to is not None:
576 skip = skip_to
577 else:
578 skip = 0
579 self.__log.warning('No skip method provided...')
581 if not 0 <= skip < self.queue_length:
582 #self.notify( 'end-of-playlist', loop, caller=self.skip )
584 if not loop:
585 self.__log.warning( "Can't skip to non-existant file w/o loop."
586 " (requested=%d, total=%d)", skip,
587 self.queue_length )
588 return False
589 else:
590 # If skip_by is given as an argument, we assume the user knows
591 # what they're doing. Ie. if the queue length is 5, current
592 # track is 3 and they pass skip_by=-9, then they'll end up
593 # at 4. On the other hand, if skip_to is passed then we skip
594 # back to 0 because in that case the user must enter a number
595 # from 0 to queue_length-1, anything else is an error.
596 if skip_by is not None:
597 skip %= self.queue_length
598 else:
599 skip = 0
601 #self.notify('stop-requested', caller=self.skip)
602 self.stop(False, set_seek_to)
603 self.__queue.current_item_position = skip
604 self.__log.debug('Skipping to file %d (%s)', skip, self.__queue.current_item.filepath)
605 if play:
606 self.play()
608 return True
610 def next(self, loop=False, set_seek_to=True):
611 """ Move the playlist to the next track.
612 False indicates end of playlist. """
613 return self.skip(loop, 1, None, set_seek_to)
615 def prev(self, set_seek_to=True):
616 """ Same as next() except moves to the previous track. """
617 return self.skip(False, -1, None, set_seek_to)
619 def last(self, set_seek_to=True, play=True):
620 """ Plays last file in queue. """
621 skip_to = len(self.__queue.get_items()) - 1
622 return self.skip(False, None, skip_to, set_seek_to, play)
624 def random(self, set_seek_to=True):
625 """ Plays random file in queue. """
626 skip_to = random.choice(range(len(self.__queue.get_items())))
627 return self.skip(False, None, skip_to, set_seek_to)
629 ##################################
630 # Player controls
631 ##################################
633 def play(self):
634 """ This gets called by the player to get
635 the last time the file was paused """
636 self.__player.play()
638 def pause(self):
639 """ Called whenever the player is paused """
640 #position = self.get_position_duration()[0]
641 #self.__queue.current_item.seek_to = position
642 self.__player.pause()
644 def play_pause_toggle(self):
645 if self.current_filepath:
646 self.__player.play_pause_toggle()
648 def stop(self, save_resume_point=True, set_seek_to=True):
649 """ This should be run when the program is closed
650 or if the user switches playlists """
651 position, duration = self.get_position_duration()
652 self.remove_resume_bookmarks()
653 self.on_player_stopped()
654 self.__player.on_stop_requested()
655 if not self.is_empty and save_resume_point:
656 self.get_current_item().save_bookmark(_('Auto Bookmark'), position, True)
657 if not self.is_empty and set_seek_to and self.config.getboolean("options", "resume_all"):
658 self.set_seek_to(position)
660 def on_player_eof(self):
661 play_mode = self.config.get("options", "play_mode")
662 if play_mode == "single":
663 if not self.config.getboolean("options", "stay_at_end"):
664 self.stop(set_seek_to=False)
665 self.notify('end-of-playlist', False, caller=self.on_player_eof)
666 elif play_mode == "random":
667 self.random(set_seek_to=False)
668 elif play_mode == "repeat":
669 self.next(loop=True, set_seek_to=False)
670 else:
671 if self.end_of_playlist():
672 if not self.config.getboolean("options", "stay_at_end"):
673 self.stop(set_seek_to=False)
674 else:
675 self.next(loop=False, set_seek_to=False)
677 def on_player_playing(self):
678 self.__player.on_playing(self.get_seek_to())
679 self.notify("playing", caller=self.on_player_playing)
681 def on_player_paused(self, position, duration):
682 self.notify("paused", position, duration, caller=self.on_player_paused)
684 def on_player_stopped(self):
685 self.notify("stopped", caller=self.on_player_stopped)
687 def do_seek(self, from_beginning=None, from_current=None, percent=None):
688 resp = None
689 if from_beginning is not None:
690 resp = self.__player.do_seek(from_beginning=from_beginning)
691 elif from_current is not None:
692 if not self.config.getboolean("options", "seek_back") or self.start_of_playlist() or from_current > 0:
693 resp = self.__player.do_seek(from_current=from_current)
694 else:
695 pos_int, dur_int = self.get_position_duration()
696 if pos_int + from_current >= 0:
697 resp = self.__player.do_seek(from_current=from_current)
698 else:
699 self.prev()
700 pos_int, dur_int = self.get_position_duration()
701 resp = self.__player.do_seek(from_beginning=dur_int+from_current)
702 elif percent is not None:
703 resp = self.__player.do_seek(percent=percent)
704 return resp
706 def get_position_duration(self):
707 return self.__player.get_position_duration()
709 def set_position_duration(self, position, duration):
710 self.__player.set_position_duration(position, duration)
712 @property
713 def playing(self):
714 return self.__player.playing
716 @property
717 def paused(self):
718 return self.__player.paused
720 @property
721 def stopped(self):
722 return self.__player.stopped
724 @property
725 def null(self):
726 return self.__player.null
728 @property
729 def seeking(self):
730 return self.__player.seeking
732 def new_track_loaded(self):
733 if os.path.isfile(self.current_filepath):
734 self.__player.load_media(self.current_filepath)
735 self.notify('new-track-loaded', caller=self.new_track_loaded)
736 self.notify('new-metadata-available', caller=self.new_track_loaded)
737 elif not self.end_of_playlist():
738 self.on_player_eof()
740 def reset_player(self):
741 self.__player.on_reset_playlist()
743 class Queue(list, ObservableService):
744 """ A Simple list of PlaylistItems """
746 signals = [ 'current_item_changed', ]
748 def __init__(self, playlist_id):
749 self.__log = logging.getLogger('panucci.playlist.Queue')
750 ObservableService.__init__(self, self.signals, self.__log)
752 self.playlist_id = playlist_id
753 self.modified = False # Has the queue been modified?
754 self.disable_notifications = False
755 self.__current_item_position = 0
756 # This is a hack and WILL BE REPLACED WITH SOMETHING BETTER.
757 # it's here to speed up the get_item function
758 self.__mapping_dict = {}
759 list.__init__(self)
761 def __get_current_item_position(self):
762 return self.__current_item_position
764 def __set__current_item_position(self, new_value):
766 # set the new position before notify()'ing
767 # or else we'll end up load the old file's metadata
768 old_value = self.__current_item_position
769 self.__current_item_position = new_value
771 if old_value != new_value:
772 self.__log.debug( 'Current item changed from %d to %d',
773 old_value, new_value )
774 if not self.disable_notifications:
775 self.notify( 'current_item_changed',
776 caller=self.__set__current_item_position )
777 else:
778 self.__log.debug( 'Current item reloaded')
779 if not self.disable_notifications:
780 self.notify( 'current_item_changed',
781 caller=self.__set__current_item_position )
783 current_item_position = property(
784 __get_current_item_position, __set__current_item_position )
786 def __count_dupe_items(self, subset, item):
787 # Count the number of duplicate items (by filepath only) in a list
788 tally = 0
789 for i in subset:
790 tally += int( i.filepath == item.filepath )
791 return tally
793 def __prep_item(self, item):
794 """ Do some error checking and other stuff that's
795 common to the insert and append functions """
797 assert isinstance( item, playlistformat.PlaylistItem )
798 item.playlist_id = self.playlist_id
800 if '://' in item.filepath or (os.path.isfile(item.filepath) and \
801 is_supported(item.filepath)):
802 self.modified = True
803 return True
804 else:
805 self.__log.warning(
806 'File not found or not supported: %s', item.filepath )
808 return False
810 @property
811 def current_item(self):
812 if len(self) > 0:
813 if self.current_item_position >= len(self):
814 self.__log.info( 'Current item position is greater '
815 'than queue length, resetting to 0.' )
816 self.current_item_position = 0
818 return self[self.current_item_position]
819 else:
820 self.__log.info('Queue is empty...')
822 def move_item(self, from_pos, to_pos):
823 old_current_item = self.current_item_position
825 temp = self[from_pos]
826 self.remove(str(temp))
827 self.insert(to_pos, temp)
829 if old_current_item == from_pos:
830 self.__current_item_position = to_pos
832 def clear(self):
833 """ Reset the the queue to a known state """
835 try:
836 items = self.__mapping_dict.values()
837 for item in items:
838 list.remove(self, item)
839 except:
840 pass
841 self[:] = []
842 self.playlist_id = None
843 self.modified = True
844 self.__current_item_position = 0
845 self.__mapping_dict = {}
847 def get_item(self, item_id):
848 return self.__mapping_dict.get(item_id)
850 def get_items(self):
851 return self.__mapping_dict.values()
853 def is_empty(self):
854 if self.__mapping_dict:
855 return False
856 else:
857 return True
859 def get_bookmark(self, item_id, bookmark_id):
860 item = self.get_item(item_id)
862 if item is None:
863 self.__log.warning(
864 'Item with id "%s" not found, scanning for item...', item_id )
866 for item_ in self:
867 if item_.bookmarks.count(bookmark_id):
868 item = item_
869 break
871 if item is None: return None, None
873 if item.get_bookmark(bookmark_id):
874 return item, item.get_bookmark(bookmark_id)
875 else:
876 return item, None
878 def set_new_playlist_id(self, id):
879 self.playlist_id = id
880 for item in self:
881 item.playlist_id = id
882 for bookmark in item.bookmarks:
883 bookmark.playlist_id = id
884 bookmark.save()
886 def insert(self, position, item):
887 if not self.__prep_item(item):
888 return False
890 item.duplicate_id = self[:position].count(item)
892 if self.__count_dupe_items(self[position:], item):
893 for i in self[position:]:
894 if i.filepath == item.filepath:
895 i.is_modified = True
896 i.duplicate_id += 1
898 # to be safe rebuild self.__mapping_dict
899 self.__mapping_dict = dict([(str(i),i) for i in self])
900 elif not self.__count_dupe_items(self[:position], item):
901 # there are no other items like this one so it's *safe* to load
902 # bookmarks without a potential conflict, but there's a good chance
903 # that there aren't any bookmarks to load (might be useful in the
904 # event of a crash)...
905 item.load_bookmarks()
907 if position <= self.current_item_position:
908 self.__current_item_position += 1
910 self.__mapping_dict[str(item)] = item
911 list.insert(self, position, item)
912 return True
914 def append(self, item):
915 if not self.__prep_item(item):
916 return False
918 item.playlist_id = self.playlist_id
919 item.duplicate_id = self.__count_dupe_items(self, item)
920 item.load_bookmarks()
922 self.__mapping_dict[str(item)] = item
923 list.append(self, item)
924 return True
926 def remove(self, item):
927 if self.count(item):
928 self.modified = True
930 if self.index(item) < self.current_item_position:
931 self.__current_item_position -= 1
933 del self.__mapping_dict[str(item)]
934 list.remove(self, item)
936 def extend(self, items):
937 self.__log.warning('FIXME: extend not supported yet...')
939 def pop(self, item):
940 self.__log.warning('FIXME: pop not supported yet...')
942 def set_current_item_position(self, position):
943 self.__current_item_position = position