Move create_item_by_filepath to PlaylistItem as a static method
[panucci.git] / src / panucci / playlist.py
blob7ea71e3545ae1a0e7e259430c4c89da77dfbe327
1 #!/usr/bin/env python
3 # Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 import gobject, gtk
26 import time
27 import os.path
28 import os
29 import re
30 import logging
31 from hashlib import md5
32 from xml.sax.saxutils import escape
34 import util
35 from dbsqlite import db
36 from settings import settings
37 from simplegconf import gconf
39 _ = lambda x: x
41 class Playlist(object):
42 def __init__(self):
43 self.__log = logging.getLogger('panucci.playlist.Playlist')
45 self.filepath = None
46 self._id = None
47 self.__queue = Queue(self.id)
49 self.__bookmarks_model = None
50 self.__bookmarks_model_changed = True
52 @property
53 def id(self):
54 if self.filepath is None:
55 self.__log.warning("Can't get playlist id without having filepath")
56 elif self._id is None:
57 self._id = db.get_playlist_id( self.filepath, True, True )
59 return self._id
61 def reset_playlist(self):
62 """ clears all the files in the filelist """
63 self.__init__()
65 @property
66 def current_filepath(self):
67 """ Get the current file """
68 if self.__queue.current_item is not None:
69 return self.__queue.current_item.filepath
71 def get_queue_modified(self): return self.__queue.modified
72 def set_queue_modified(self, v): self.__queue.modified = v
73 queue_modified = property(get_queue_modified,set_queue_modified)
75 def queue_length(self):
76 return len(self.__queue)
78 def is_empty(self):
79 return not self.__queue
81 def print_queue_layout(self):
82 """ This helps with debugging ;) """
83 for item in self.__queue:
84 print str(item), item.reported_filepath
85 for bookmark in item.bookmarks:
86 print '\t', str(bookmark), bookmark.bookmark_filepath
88 def save_to_new_playlist(self, filepath, playlist_type='m3u'):
89 self.filepath = filepath
90 self._id = None
92 playlist = { 'm3u': M3U_Playlist, 'pls': PLS_Playlist }
93 if not playlist.has_key(playlist_type):
94 playlist_type = 'm3u' # use m3u by default
95 self.filepath += '.m3u'
97 playlist = playlist[playlist_type](self.filepath, self.id)
98 if not playlist.export_items( filepath, self.__queue ):
99 self.__log.error('Error exporting playlist to %s', self.filepath)
100 return False
102 # copy the bookmarks over to new playlist
103 db.remove_all_bookmarks(self.id)
104 self.__queue.set_new_playlist_id(self.id)
106 return True
108 def save_temp_playlist(self):
109 filepath = os.path.expanduser(settings.temp_playlist)
110 return self.save_to_new_playlist(filepath)
112 ######################################
113 # Bookmark-related functions
114 ######################################
116 def load_from_bookmark( self, item_id, bookmark_id ):
117 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
119 if item is None:
120 self.__log.info('Item with id "%s" not found', item_id)
121 return False
123 self.__queue.current_item_position = self.__queue.index(item_id)
125 if bookmark is None:
126 self.__queue.current_item.seek_to = 0
127 else:
128 self.__queue.current_item.seek_to = bookmark.seek_position
130 return True
132 def save_bookmark( self, bookmark_name, position ):
133 self.__queue.current_item.save_bookmark(
134 bookmark_name, position, resume_pos=False )
135 self.__bookmarks_model_changed = True
137 def update_bookmark(self, item_id, bookmark_id, name=None, seek_pos=None):
138 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
140 if item is None:
141 self.__log.warning('No such item id (%s)', item_id)
142 return False
144 if bookmark_id is not None and bookmark is None:
145 self.__log.warning('No such bookmark id (%s)', bookmark_id)
146 return False
148 if bookmark_id is None:
149 if name is not None:
150 item.title = name
151 else:
152 bookmark.timestamp = time.time()
154 if name is not None:
155 bookmark.bookmark_name = name
157 if seek_pos is not None:
158 bookmark.seek_position = seek_pos
160 db.update_bookmark(bookmark)
162 return True
164 def update_bookmarks(self):
165 """ Updates the database entries for items that have been modified """
166 for item in self.__queue:
167 if item.is_modified:
168 self.__log.debug(
169 'Playlist Item "%s" is modified, updating bookmarks', item)
170 item.update_bookmarks()
171 item.is_modified = False
173 def remove_bookmark( self, item_id, bookmark_id ):
174 item = self.__queue.get_item(item_id)
176 if item is None:
177 self.__log.info('Cannot find item with id: %s', item_id)
178 return False
180 if bookmark_id is None:
181 item.delete_bookmark(None)
182 self.__queue.remove(item_id)
183 else:
184 item.delete_bookmark(bookmark_id)
186 return True
188 def generate_bookmark_model(self, include_resume_marks=False):
189 self.__bookmarks_model = gtk.TreeStore(
190 # uid, name, position
191 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
193 for item in self.__queue:
194 title = util.pretty_filename(
195 item.filepath ) if item.title is None else item.title
196 row = [ str(item), title, None ]
197 parent = self.__bookmarks_model.append( None, row )
199 for bkmk in item.bookmarks:
200 if not bkmk.is_resume_position or include_resume_marks:
201 row = [ str(bkmk), bkmk.bookmark_name,
202 util.convert_ns(bkmk.seek_position) ]
203 self.__bookmarks_model.append( parent, row )
205 def get_bookmark_model(self, include_resume_marks=False):
206 if self.__bookmarks_model is None or self.__bookmarks_model_changed:
207 self.__log.debug('Generating new bookmarks model')
208 self.generate_bookmark_model(include_resume_marks)
209 self.__bookmarks_model_changed = False
210 else:
211 self.__log.debug('Using cached bookmarks model')
213 return self.__bookmarks_model
215 ######################################
216 # File-related convenience functions
217 ######################################
219 def get_current_position(self):
220 """ Returns the saved position for the current
221 file or 0 if no file is available"""
222 if self.__queue.current_item is not None:
223 return self.__queue.current_item.seek_to
224 else:
225 return 0
227 def get_current_filetype(self):
228 """ Returns the filetype of the current
229 file or None if no file is available """
231 if self.__queue.current_item is not None:
232 return self.__queue.current_item.filetype
234 def get_file_metadata(self):
235 """ Return the metadata associated with the current FileObject """
236 if self.__queue.current_item is not None:
237 return self.__queue.current_item.metadata
238 else:
239 return {}
241 def get_current_filepath(self):
242 if self.__queue.current_item is not None:
243 return self.__queue.current_item.filepath
245 def get_recent_files(self, max_files=10):
246 files = db.get_latest_files()
248 if len(files) > max_files:
249 return files[:max_files]
250 else:
251 return files
253 ##################################
254 # File importing functions
255 ##################################
257 def load(self, File):
258 """ Detects File's filetype then loads it using
259 the appropriate loader function """
260 self.__log.debug('Attempting to load %s', File)
262 error = False
263 self.reset_playlist()
264 self.filepath = File
266 parsers = { 'm3u': M3U_Playlist, 'pls': PLS_Playlist }
267 extension = util.detect_filetype(File)
268 if parsers.has_key(extension):
269 self.__log.info('Loading playlist file (%s)', extension)
270 parser = parsers[extension](self.filepath, self.id)
272 if parser.parse(File):
273 self.__queue = parser.get_queue()
274 else:
275 return False
276 else:
277 error = not self.single_file_import(File)
279 self.queue_modified = os.path.expanduser(
280 settings.temp_playlist ) == self.filepath
282 return not error
284 def single_file_import( self, filepath ):
285 """ Add a single track to the playlist """
286 return self.__queue.append(
287 PlaylistItem.create_by_filepath(filepath, filepath) )
289 ##################################
290 # Playlist controls
291 ##################################
293 def play(self):
294 """ This gets called by the player to get
295 the last time the file was paused """
296 return self.__queue.current_item.seek_to
298 def pause(self, position):
299 """ Called whenever the player is paused """
300 self.__queue.current_item.seek_to = position
301 self.__queue.current_item.save_bookmark(
302 _('Auto Bookmark'), position, True )
304 def stop(self):
305 """ Caused when we reach the end of a file """
306 # for the time being, don't remove resume points at EOF to make sure
307 # that the recent files list stays populated.
308 # db.remove_resume_bookmark( self.filepath )
309 self.pause(0)
311 def skip(self, skip_by=None, skip_to=None, dont_loop=False):
312 """ Skip to another track in the playlist.
313 Use either skip_by or skip_to, skip_by has precedence.
314 skip_to: skip to a known playlist position
315 skip_by: skip by n number of episodes (positive or negative)
316 dont_loop: applies only to skip_by, if we're skipping past
317 the last track loop back to the begining.
319 if not self.__queue:
320 return False
322 current_item = self.__queue.current_item_position
324 if skip_by is not None:
325 if dont_loop:
326 skip = current_item + skip_by
327 else:
328 skip = ( current_item + skip_by ) % self.queue_length()
329 elif skip_to is not None:
330 skip = skip_to
331 else:
332 self.__log.warning('No skip method provided...')
334 if not ( 0 <= skip < self.queue_length() ):
335 self.__log.warning(
336 'Can\'t skip to non-existant file. (requested=%d, total=%d)',
337 skip, self.queue_length() )
338 return False
340 self.__queue.current_item_position = skip
341 self.__log.debug('Skipping to file %d (%s)', skip,
342 self.__queue.current_item.filepath )
343 return True
345 def next(self):
346 """ Move the playlist to the next track.
347 False indicates end of playlist. """
348 return self.skip( skip_by=1, dont_loop=True )
350 def prev(self):
351 """ Same as next() except moves to the previous track. """
352 return self.skip( skip_by=-1, dont_loop=True )
355 class Queue(list):
356 """ A Simple list of PlaylistItems """
358 def __init__(self, playlist_id):
359 self.__log = logging.getLogger('panucci.playlist.Queue')
361 self.playlist_id = playlist_id
362 self.modified = False # Has the queue been modified?
363 self.current_item_position = 0
364 list.__init__(self)
366 def __count_dupe_items(self, subset, item):
367 """ Count the number of duplicate items (by filepath only) in a list """
368 tally = 0
369 for i in subset:
370 tally += int( i.filepath == item.filepath )
371 return tally
373 def __prep_item(self, item):
374 """ Do some error checking and other stuff that's
375 common to the insert and append functions """
377 assert isinstance( item, PlaylistItem )
378 item.playlist_id = self.playlist_id
379 s = os.path.isfile(item.filepath) and util.is_supported(item.filepath)
381 if s:
382 self.modified = True
383 else:
384 self.__log.warning(
385 'File not found or not supported: %s', item.filepath )
387 return s
389 @property
390 def current_item(self):
391 if self.current_item_position > len(self):
392 self.__log.info( 'Current item position is greater '
393 'than queue length, resetting to 0.' )
394 self.current_item_position = 0
395 else:
396 return self[self.current_item_position]
398 def get_item(self, item_id):
399 if self.count(item_id):
400 return self[self.index(item_id)]
402 def get_bookmark(self, item_id, bookmark_id):
403 item = self.get_item(item_id)
405 if item is not None:
406 if item.bookmarks.count(bookmark_id):
407 return item, item.bookmarks[item.bookmarks.index(bookmark_id)]
408 else:
409 return item, None
410 else:
411 return None, None
413 def set_new_playlist_id(self, id):
414 self.playlist_id = id
415 for item in self:
416 item.playlist_id = id
417 for bookmark in item.bookmarks:
418 bookmark.playlist_id = id
419 bookmark.save()
421 def insert(self, position, item):
422 if not self.__prep_item(item):
423 return False
425 item.duplicate_id = self[:position].count(item)
427 if self.__count_dupe_items(self[position:], item):
428 for i in self[position:]:
429 if i.filepath == item.filepath:
430 i.is_modified = True
431 i.duplicate_id += 1
432 elif not self.__count_dupe_items(self[:position], item):
433 # there are no other items like this one so it's *safe* to load
434 # bookmarks without a potential conflict, but there's a good chance
435 # that there aren't any bookmarks to load (might be useful in the
436 # event of a crash)...
437 item.load_bookmarks()
439 if position <= self.current_item_position:
440 self.current_item_position += 1
442 list.insert(self, position, item)
443 return True
445 def append(self, item):
446 if not self.__prep_item(item):
447 return False
449 item.duplicate_id = self.__count_dupe_items(self, item)
450 item.load_bookmarks()
452 list.append(self, item)
453 return True
455 def remove(self, item):
456 self.modified = True
457 list.remove(self, item)
459 def extend(self, items):
460 self.__log.warning('FIXME: extend not supported yet...')
462 def pop(self, item):
463 self.__log.warning('FIXME: pop not supported yet...')
465 class PlaylistItem(object):
466 """ A (hopefully) lightweight object to hold the bare minimum amount of
467 data about a single item in a playlist and it's bookmark objects. """
469 def __init__(self):
470 self.__log = logging.getLogger('panucci.playlist.PlaylistItem')
472 # metadata that's pulled from the playlist file (pls/extm3u)
473 self.reported_filepath = None
474 self.title = None
475 self.length = None
477 self.playlist_id = None
478 self.filepath = None
479 self.duplicate_id = 0
480 self.seek_to = 0
482 # a flag to determine whether the item's bookmarks need updating
483 # ( used for example, if the duplicate_id is changed )
484 self.is_modified = False
485 self.bookmarks = []
487 @staticmethod
488 def create_by_filepath(reported_filepath, filepath):
489 item = PlaylistItem()
490 item.reported_filepath = reported_filepath
491 item.filepath = filepath
492 return item
494 def __eq__(self, b):
495 if isinstance( b, PlaylistItem ):
496 return ( self.filepath == b.filepath and
497 self.duplicate_id == b.duplicate_id )
498 elif isinstance( b, str ):
499 return str(self) == b
500 else:
501 self.__log.warning('Unsupported comparison: %s', type(b))
502 return False
504 def __str__(self):
505 uid = self.filepath + str(self.duplicate_id)
506 return md5(uid).hexdigest()
508 @property
509 def metadata(self):
510 """ Metadata is only needed once, so fetch it on-the-fly
511 If needed this could easily be cached at the cost of wasting a
512 bunch of memory """
514 m = FileMetadata(self.filepath)
515 metadata = m.get_metadata()
516 del m # *hopefully* save some memory
517 return metadata
519 @property
520 def filetype(self):
521 return util.detect_filetype(self.filepath)
523 def load_bookmarks(self):
524 self.bookmarks = db.load_bookmarks(
525 factory = Bookmark().load_from_dict,
526 playlist_id = self.playlist_id,
527 bookmark_filepath = self.filepath,
528 playlist_duplicate_id = self.duplicate_id,
529 allow_resume_bookmarks = False )
531 def save_bookmark(self, name, position, resume_pos=False):
532 b = Bookmark()
533 b.playlist_id = self.playlist_id
534 b.bookmark_name = name
535 b.bookmark_filepath = self.filepath
536 b.seek_position = position
537 b.timestamp = time.time()
538 b.is_resume_position = resume_pos
539 b.playlist_duplicate_id = self.duplicate_id
540 b.save()
541 self.bookmarks.append(b)
543 def delete_bookmark(self, bookmark_id):
544 """ WARNING: if bookmark_id is None, ALL bookmarks will be deleted """
545 if bookmark_id is None:
546 self.__log.debug(
547 'Deleting all bookmarks for %s', self.reported_filepath )
548 for bkmk in self.bookmarks:
549 bkmk.delete()
550 else:
551 bkmk = self.bookmarks.index(bookmark_id)
552 if bkmk >= 0:
553 self.bookmarks[bkmk].delete()
554 else:
555 self.__log.info('Cannot find bookmark with id: %s',bookmark_id)
556 return False
557 return True
559 def update_bookmarks(self):
560 for bookmark in self.bookmarks:
561 bookmark.playlist_duplicate_id = self.duplicate_id
562 bookmark.bookmark_filepath = self.filepath
563 db.update_bookmark(bookmark)
565 class Bookmark(object):
566 """ A single bookmark, nothing more, nothing less. """
568 def __init__(self):
569 self.__log = logging.getLogger('panucci.playlist.Bookmark')
571 self.id = 0
572 self.playlist_id = None
573 self.bookmark_name = ''
574 self.bookmark_filepath = ''
575 self.seek_position = 0
576 self.timestamp = 0
577 self.is_resume_position = False
578 self.playlist_duplicate_id = 0
580 @staticmethod
581 def load_from_dict(bkmk_dict):
582 bkmkobj = Bookmark()
584 for key,value in bkmk_dict.iteritems():
585 if hasattr( bkmkobj, key ):
586 setattr( bkmkobj, key, value )
587 else:
588 self.__log.info('Attr: %s doesn\'t exist...', key)
590 return bkmkobj
592 def save(self):
593 self.id = db.save_bookmark(self)
594 return self.id
596 def delete(self):
597 return db.remove_bookmark(self.id)
599 def __eq__(self, b):
600 if isinstance(b, str):
601 return str(self) == b
602 else:
603 self.__log.warning('Unsupported comparison: %s', type(b))
604 return False
606 def __str__(self):
607 uid = self.bookmark_filepath
608 uid += str(self.playlist_duplicate_id)
609 uid += str(self.seek_position)
610 return md5(uid).hexdigest()
612 def __cmp__(self, b):
613 if self.bookmark_filepath == b.bookmark_filepath:
614 if self.seek_position == b.seek_position:
615 return 0
616 else:
617 return -1 if self.seek_position < b.seek_position else 1
618 else:
619 self.__log.info(
620 'Can\'t compare bookmarks from different files:\n\tself: %s'
621 '\n\tb: %s', self.bookmark_filepath, b.bookmark_filepath )
622 return 0
624 class FileMetadata(object):
625 """ A class to hold all information about the file that's currently being
626 played. Basically it takes care of metadata extraction... """
628 coverart_names = ['cover', 'cover.jpg', 'cover.png']
629 tag_mappings = {
630 'mp4': { '\xa9nam': 'title',
631 '\xa9ART': 'artist',
632 '\xa9alb': 'album',
633 'covr': 'coverart' },
634 'mp3': { 'TIT2': 'title',
635 'TPE1': 'artist',
636 'TALB': 'album',
637 'APIC': 'coverart' },
638 'ogg': { 'title': 'title',
639 'artist': 'artist',
640 'album': 'album' },
642 tag_mappings['m4a'] = tag_mappings['mp4']
643 tag_mappings['flac'] = tag_mappings['ogg']
645 def __init__(self, filepath):
646 self.__log = logging.getLogger('panucci.playlist.FileMetadata')
647 self.__filepath = filepath
649 self.title = ''
650 self.artist = ''
651 self.album = ''
652 self.length = 0
653 self.coverart = None
655 self.__metadata_extracted = False
657 def extract_metadata(self):
658 filetype = util.detect_filetype(self.__filepath)
660 if filetype == 'mp3':
661 import mutagen.mp3 as meta_parser
662 elif filetype == 'ogg':
663 import mutagen.oggvorbis as meta_parser
664 elif filetype == 'flac':
665 import mutagen.flac as meta_parser
666 elif filetype in ['mp4', 'm4a']:
667 import mutagen.mp4 as meta_parser
668 else:
669 self.__log.info(
670 'Extracting metadata not supported for %s files.', filetype )
671 return False
673 try:
674 metadata = meta_parser.Open(self.__filepath)
675 except Exception, e:
676 self.title = util.pretty_filename(self.__filepath)
677 self.__log.exception('Error running metadata parser...')
678 return False
680 self.length = metadata.info.length * 10**9
681 for tag,value in metadata.iteritems():
682 if tag.find(':') != -1: # hack for weirdly named coverart tags
683 tag = tag.split(':')[0]
685 if self.tag_mappings[filetype].has_key(tag):
686 if isinstance( value, list ):
687 if len(value):
688 # Here we could either join the list or just take one
689 # item. I chose the latter simply because some ogg
690 # files have several messed up titles...
691 value = value[0]
692 else:
693 continue
695 if self.tag_mappings[filetype][tag] != 'coverart':
696 try:
697 value = escape(str(value))
698 except Exception, e:
699 self.__log.exception(
700 'Could not convert tag (%s) to escaped string', tag )
701 else:
702 # some coverart classes store the image in the data
703 # attribute whereas others do not :S
704 if hasattr( value, 'data' ):
705 value = value.data
707 setattr( self, self.tag_mappings[filetype][tag], value )
709 if not str(self.title).strip():
710 self.title = util.pretty_filename(self.__filepath)
712 if self.coverart is None:
713 self.coverart = self.__find_coverart()
715 def __find_coverart(self):
716 """ Find coverart in the same directory as the filepath """
717 directory = os.path.dirname(self.__filepath)
718 for cover in self.coverart_names:
719 c = os.path.join( directory, cover )
720 if os.path.isfile(c):
721 try:
722 f.open(c,'r')
723 binary_coverart = f.read()
724 f.close()
725 return binary_coverart
726 except:
727 pass
728 return None
730 def get_metadata(self):
731 """ Returns a dict of metadata """
733 if not self.__metadata_extracted:
734 self.__log.debug('Extracting metadata for %s', self.__filepath)
735 self.extract_metadata()
736 self.__metadata_extracted = True
738 metadata = {
739 'title': self.title,
740 'artist': self.artist,
741 'album': self.album,
742 'image': self.coverart,
743 'length': self.length
746 return metadata
748 class PlaylistFile(object):
749 """ The base class for playlist file parsers/exporters,
750 this should not be used directly but instead subclassed. """
752 def __init__(self, filepath, id):
753 self.__log = logging.getLogger('panucci.playlist.PlaylistFile')
754 self._filepath = filepath
755 self._file = None
756 self._items = Queue(id)
758 def __open_file(self, filepath, mode):
759 if self._file is not None:
760 self.close_file()
762 try:
763 self._file = open( filepath, mode )
764 self._filepath = filepath
765 except Exception, e:
766 self._filepath = None
767 self._file = None
769 self.__log.exception( 'Error opening file: %s', filepath)
770 return False
772 return True
774 def __close_file(self):
775 error = False
777 if self._file is not None:
778 try:
779 self._file.close()
780 except Exception, e:
781 self.__log.exception( 'Error closing file: %s', self.filepath )
782 error = True
784 self._filepath = None
785 self._file = None
787 return not error
789 def get_absolute_filepath(self, item_filepath):
790 if item_filepath is None: return
792 if item_filepath.startswith('/'):
793 path = item_filepath
794 else:
795 path = os.path.join(os.path.dirname(self._filepath), item_filepath)
797 if os.path.exists( path ):
798 return path
800 def get_filelist(self):
801 return [ item.filepath for item in self._items ]
803 def get_filedicts(self):
804 dict_list = []
805 for item in self._items:
806 d = { 'title': item.title,
807 'length': item.length,
808 'filepath': item.filepath }
810 dict_list.append(d)
811 return dict_list
813 def get_queue(self):
814 return self._items
816 def export_items(self, filepath=None, playlist_items=None):
817 if filepath is not None:
818 self._filepath = filepath
820 if playlist_items is not None:
821 self._items = playlist_items
823 if self.__open_file(filepath, 'w'):
824 self.export_hook(self._items)
825 self.__close_file()
826 return True
827 else:
828 return False
830 def export_hook(self, playlist_items):
831 pass
833 def parse(self, filepath):
834 if self.__open_file( filepath, mode='r' ):
835 current_line = self._file.readline()
836 while current_line:
837 self.parse_line_hook( current_line.strip() )
838 current_line = self._file.readline()
839 self.__close_file()
840 self.parse_eof_hook()
841 else:
842 return False
843 return True
845 def parse_line_hook(self, line):
846 pass
848 def parse_eof_hook(self):
849 pass
851 def _add_playlist_item(self, item):
852 path = self.get_absolute_filepath(item.reported_filepath)
853 if path is not None and os.path.isfile(path):
854 item.filepath = path
855 self._items.append(item)
857 class M3U_Playlist(PlaylistFile):
858 """ An (extended) m3u parser/writer """
860 def __init__(self, *args):
861 self.__log = logging.getLogger('panucci.playlist.M3U_Playlist')
862 PlaylistFile.__init__( self, *args )
863 self.extended_m3u = False
864 self.current_item = PlaylistItem()
866 def parse_line_hook(self, line):
867 if line.startswith('#EXTM3U'):
868 self.extended_m3u = True
869 elif self.extended_m3u and line.startswith('#EXTINF'):
870 match = re.match('#EXTINF:([^,]+),(.*)', line)
871 if match is not None:
872 length, title = match.groups()
873 try: length = int(length)
874 except: pass
875 self.current_item.length = length
876 self.current_item.title = title
877 elif line.startswith('#'):
878 pass # skip comments
879 elif line:
880 path = self.get_absolute_filepath( line )
881 if path is not None:
882 if os.path.isfile( path ):
883 self.current_item.reported_filepath = line
884 self._add_playlist_item(self.current_item)
885 self.current_item = PlaylistItem()
886 elif os.path.isdir( path ):
887 files = os.listdir( path )
888 for file in files:
889 item = PlaylistItem()
890 item.reported_filepath = os.path.join(line, file)
891 self._add_playlist_item(item)
893 def export_hook(self, playlist_items):
894 self._file.write('#EXTM3U\n\n')
896 for item in playlist_items:
897 string = ''
898 if not ( item.length is None and item.title is None ):
899 length = -1 if item.length is None else item.length
900 title = '' if item.title is None else item.title
901 string += '#EXTINF:%d,%s\n' % ( length, title )
903 string += '%s\n' % item.filepath
904 self._file.write(string)
906 class PLS_Playlist(PlaylistFile):
907 """ A somewhat simple pls parser/writer """
909 def __init__(self, *args):
910 self.__log = logging.getLogger('panucci.playlist.PLS_Playlist')
911 PlaylistFile.__init__( self, *args )
912 self.current_item = PlaylistItem()
913 self.in_playlist_section = False
914 self.current_item_number = None
916 def __add_current_item(self):
917 self._add_playlist_item(self.current_item)
919 def parse_line_hook(self, line):
920 sect_regex = '\[([^\]]+)\]'
921 item_regex = '[^\d]+([\d]+)=(.*)'
923 if re.search(item_regex, line) is not None:
924 current = re.search(item_regex, line).group(1)
925 if self.current_item_number is None:
926 self.current_item_number = current
927 elif self.current_item_number != current:
928 self.__add_current_item()
930 self.current_item = PlaylistItem()
931 self.current_item_number = current
933 if re.search(sect_regex, line) is not None:
934 section = re.match(sect_regex, line).group(1).lower()
935 self.in_playlist_section = section == 'playlist'
936 elif not self.in_playlist_section:
937 pass # don't do anything if we're not in [playlist]
938 elif line.lower().startswith('file'):
939 self.current_item.reported_filepath = re.search(
940 item_regex, line).group(2)
941 elif line.lower().startswith('title'):
942 self.current_item.title = re.search(item_regex, line).group(2)
943 elif line.lower().startswith('length'):
944 try: length = int(re.search(item_regex, line).group(2))
945 except: pass
946 self.current_item.length = length
948 def parse_eof_hook(self):
949 self.__add_current_item()
951 def export_hook(self, playlist_items):
952 self._file.write('[playlist]\n')
953 self._file.write('NumberOfEntries=%d\n\n' % len(playlist_items))
955 for i,item in enumerate(playlist_items):
956 title = '' if item.title is None else item.title
957 length = -1 if item.length is None else item.length
958 self._file.write('File%d=%s\n' % (i+1, item.filepath))
959 self._file.write('Title%d=%s\n' % (i+1, title))
960 self._file.write('Length%d=%s\n\n' % (i+1, length))
962 self._file.write('Version=2\n')