Make playlist exporting somewhat work
[panucci.git] / src / panucci / playlist.py
blob1f34a103f01f5f77a1ca244b5bb4cb5931d05234
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 from hashlib import md5
31 from xml.sax.saxutils import escape
33 import util
34 from util import log
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.filepath = None
45 self._id = None
46 self.__queue = Queue(self.id)
48 self.__bookmarks_model = None
49 self.__bookmarks_model_changed = True
51 @property
52 def id(self):
53 if self.filepath is None:
54 log('Can\'t get playlist id without having filepath')
55 elif self._id is None:
56 self._id = db.get_playlist_id( self.filepath, True, True )
58 return self._id
60 def reset_playlist(self):
61 """ clears all the files in the filelist """
62 self.__init__()
64 @property
65 def current_filepath(self):
66 """ Get the current file """
67 if self.__queue.current_item is not None:
68 return self.__queue.current_item.filepath
70 def get_queue_modified(self): return self.__queue.modified
71 def set_queue_modified(self, v): self.__queue.modified = v
72 queue_modified = property(get_queue_modified,set_queue_modified)
74 def queue_length(self):
75 return len(self.__queue)
77 def is_empty(self):
78 return not self.__queue
80 def print_queue_layout(self):
81 """ This helps with debugging ;) """
82 for item in self.__queue:
83 print str(item), item.reported_filepath
84 for bookmark in item.bookmarks:
85 print '\t', str(bookmark), bookmark.bookmark_filepath
87 def save_to_new_playlist(self, filepath, playlist_type='m3u'):
88 self.filepath = filepath
89 self._id = None
91 playlist = { 'm3u': M3U_Playlist, 'pls': PLS_Playlist }
92 if not playlist.has_key(playlist_type):
93 playlist_type = 'm3u' # use m3u by default
94 self.filepath += '.m3u'
96 playlist = playlist[playlist_type](self.filepath, self.id)
97 if not playlist.export_items( filepath, self.__queue ):
98 log('Error exporting playlist to %s' % self.filepath)
99 return False
101 # copy the bookmarks over to new playlist
102 db.remove_all_bookmarks(self.id)
103 self.__queue.set_new_playlist_id(self.id)
105 return True
107 def save_temp_playlist(self):
108 filepath = os.path.expanduser(settings.temp_playlist)
109 return self.save_to_new_playlist(filepath)
111 ######################################
112 # Bookmark-related functions
113 ######################################
115 def load_from_bookmark( self, item_id, bookmark_id ):
116 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
118 if item is None:
119 log('Item with id "%s" not found' % item_id)
120 return False
122 self.__queue.current_item_position = self.__queue.index(item_id)
124 if bookmark is None:
125 self.__queue.current_item.seek_to = 0
126 else:
127 self.__queue.current_item.seek_to = bookmark.seek_position
129 return True
131 def save_bookmark( self, bookmark_name, position ):
132 self.__queue.current_item.save_bookmark(
133 bookmark_name, position, resume_pos=False )
134 self.__bookmarks_model_changed = True
136 def update_bookmark(self, item_id, bookmark_id, name=None, seek_pos=None):
137 item, bookmark = self.__queue.get_bookmark(item_id, bookmark_id)
139 if item is None:
140 log('No such item id (%s)' % item_id)
141 return False
143 if bookmark_id is not None and bookmark is None:
144 log('No such bookmark id (%s)' % bookmark_id)
145 return False
147 if bookmark_id is None:
148 if name is not None:
149 item.title = name
150 else:
151 bookmark.timestamp = time.time()
153 if name is not None:
154 bookmark.bookmark_name = name
156 if seek_pos is not None:
157 bookmark.seek_position = seek_pos
159 db.update_bookmark(bookmark)
161 return True
163 def update_bookmarks(self):
164 """ Updates the database entries for items that have been modified """
165 for item in self.__queue:
166 if item.is_modified:
167 log('Playlist Item "%s" is modified, updating bookmarks'%item)
168 item.update_bookmarks()
169 item.is_modified = False
171 def remove_bookmark( self, item_id, bookmark_id ):
172 item = self.__queue.get_item(item_id)
174 if item is None:
175 log('Cannot find item with id: %s' % item_id)
176 return False
178 if bookmark_id is None:
179 item.delete_bookmark(None)
180 self.__queue.remove(item_id)
181 else:
182 item.delete_bookmark(bookmark_id)
184 return True
186 def generate_bookmark_model(self, include_resume_marks=False):
187 self.__bookmarks_model = gtk.TreeStore(
188 # uid, name, position
189 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
191 for item in self.__queue:
192 title = util.pretty_filename(
193 item.filepath ) if item.title is None else item.title
194 row = [ str(item), title, None ]
195 parent = self.__bookmarks_model.append( None, row )
197 for bkmk in item.bookmarks:
198 if not bkmk.is_resume_position or include_resume_marks:
199 row = [ str(bkmk), bkmk.bookmark_name,
200 util.convert_ns(bkmk.seek_position) ]
201 self.__bookmarks_model.append( parent, row )
203 def get_bookmark_model(self, include_resume_marks=False):
204 if self.__bookmarks_model is None or self.__bookmarks_model_changed:
205 log('Generating new bookmarks model')
206 self.generate_bookmark_model(include_resume_marks)
207 self.__bookmarks_model_changed = False
208 else:
209 log('Using cached bookmarks model')
211 return self.__bookmarks_model
213 ######################################
214 # File-related convenience functions
215 ######################################
217 def get_current_position(self):
218 """ Returns the saved position for the current
219 file or 0 if no file is available"""
220 if self.__queue.current_item is not None:
221 return self.__queue.current_item.seek_to
222 else:
223 return 0
225 def get_current_filetype(self):
226 """ Returns the filetype of the current
227 file or None if no file is available """
229 if self.__queue.current_item is not None:
230 return self.__queue.current_item.filetype
232 def get_file_metadata(self):
233 """ Return the metadata associated with the current FileObject """
234 if self.__queue.current_item is not None:
235 return self.__queue.current_item.metadata
236 else:
237 return {}
239 def get_current_filepath(self):
240 if self.__queue.current_item is not None:
241 return self.__queue.current_item.filepath
243 def get_recent_files(self, max_files=10):
244 files = db.get_latest_files()
246 if len(files) > max_files:
247 return files[:max_files]
248 else:
249 return files
251 ##################################
252 # File importing functions
253 ##################################
255 def load(self, File):
256 """ Detects File's filetype then loads it using
257 the appropriate loader function """
258 log('Attempting to load %s' % File)
260 error = False
261 self.reset_playlist()
262 self.filepath = File
264 parsers = { 'm3u': M3U_Playlist, 'pls': PLS_Playlist }
265 extension = util.detect_filetype(File)
266 if parsers.has_key(extension):
267 log('Loading playlist file (%s)' % extension)
268 parser = parsers[extension](self.filepath, self.id)
270 if parser.parse(File):
271 self.__queue = parser.get_queue()
272 else:
273 return False
274 else:
275 error = not self.single_file_import(File)
277 self.queue_modified = os.path.expanduser(
278 settings.temp_playlist ) == self.filepath
280 return not error
282 def single_file_import( self, filepath ):
283 """ Add a single track to the playlist """
284 self.filepath = filepath
285 return self.__queue.append(
286 self.__queue.create_item_by_filepath(filepath, filepath) )
288 ##################################
289 # Playlist controls
290 ##################################
292 def play(self):
293 """ This gets called by the player to get
294 the last time the file was paused """
295 return self.__queue.current_item.seek_to
297 def pause(self, position):
298 """ Called whenever the player is paused """
299 self.__queue.current_item.seek_to = position
300 self.__queue.current_item.save_bookmark(
301 _('Auto Bookmark'), position, True )
303 def stop(self):
304 """ Caused when we reach the end of a file """
305 # for the time being, don't remove resume points at EOF to make sure
306 # that the recent files list stays populated.
307 # db.remove_resume_bookmark( self.filepath )
308 self.pause(0)
310 def skip(self, skip_by=None, skip_to=None, dont_loop=False):
311 """ Skip to another track in the playlist.
312 Use either skip_by or skip_to, skip_by has precedence.
313 skip_to: skip to a known playlist position
314 skip_by: skip by n number of episodes (positive or negative)
315 dont_loop: applies only to skip_by, if we're skipping past
316 the last track loop back to the begining.
318 if not self.__queue:
319 return False
321 current_item = self.__queue.current_item_position
323 if skip_by is not None:
324 if dont_loop:
325 skip = current_item + skip_by
326 else:
327 skip = ( current_item + skip_by ) % self.queue_length()
328 elif skip_to is not None:
329 skip = skip_to
330 else:
331 log('No skip method provided...')
333 if not ( 0 <= skip < self.queue_length() ):
334 log('Can\'t skip to non-existant file. (requested=%d, total=%d)' % (
335 skip, self.queue_length()) )
336 return False
338 self.__queue.current_item_position = skip
339 log('Skipping to file %d (%s)' % (
340 skip, self.__queue.current_item.filepath ))
341 return True
343 def next(self):
344 """ Move the playlist to the next track.
345 False indicates end of playlist. """
346 return self.skip( skip_by=1, dont_loop=True )
348 def prev(self):
349 """ Same as next() except moves to the previous track. """
350 return self.skip( skip_by=-1, dont_loop=True )
353 class Queue(list):
354 """ A Simple list of PlaylistItems """
356 def __init__(self, playlist_id):
357 self.playlist_id = playlist_id
358 self.modified = False # Has the queue been modified?
359 self.current_item_position = 0
360 list.__init__(self)
362 def create_item_by_filepath(self, reported_filepath, filepath):
363 item = PlaylistItem()
364 item.reported_filepath = reported_filepath
365 item.filepath = filepath
366 return item
368 def __count_dupe_items(self, subset, item):
369 """ Count the number of duplicate items (by filepath only) in a list """
370 tally = 0
371 for i in subset:
372 tally += int( i.filepath == item.filepath )
373 return tally
375 def __prep_item(self, item):
376 """ Do some error checking and other stuff that's
377 common to the insert and append functions """
379 assert isinstance( item, PlaylistItem )
380 item.playlist_id = self.playlist_id
381 s = os.path.isfile(item.filepath) and util.is_supported(item.filepath)
383 if s:
384 self.modified = True
385 else:
386 log('File not found or not supported: %s' % item.filepath)
388 return s
390 @property
391 def current_item(self):
392 if self.current_item_position > len(self):
393 log('Current item position is greater 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)
404 if item is not None and item.bookmarks.count(bookmark_id):
405 return item, item.bookmarks[item.bookmarks.index(bookmark_id)]
406 return None, None
408 def set_new_playlist_id(self, id):
409 self.playlist_id = id
410 for item in self:
411 item.playlist_id = id
412 for bookmark in item.bookmarks:
413 bookmark.playlist_id = id
414 bookmark.save()
416 def insert(self, position, item):
417 if not self.__prep_item(item):
418 return False
420 item.duplicate_id = self[:position].count(item)
422 if self.__count_dupe_items(self[position:], item):
423 for i in self[position:]:
424 if i.filepath == item.filepath:
425 i.is_modified = True
426 i.duplicate_id += 1
427 elif not self.__count_dupe_items(self[:position], item):
428 # there are no other items like this one so it's *safe* to load
429 # bookmarks without a potential conflict, but there's a good chance
430 # that there aren't any bookmarks to load (might be useful in the
431 # event of a crash)...
432 item.load_bookmarks()
434 if position <= self.current_item_position:
435 self.current_item_position += 1
437 list.insert(self, position, item)
438 return True
440 def append(self, item):
441 if not self.__prep_item(item):
442 return False
444 item.duplicate_id = self.__count_dupe_items(self, item)
445 item.load_bookmarks()
447 list.append(self, item)
448 return True
450 def remove(self, item):
451 self.modified = True
452 list.remove(self, item)
454 def extend(self, items):
455 log('FIXME: extend not supported yet...')
457 def pop(self, item):
458 log('FIXME: pop not supported yet...')
460 class PlaylistItem(object):
461 """ A (hopefully) lightweight object to hold the bare minimum amount of
462 data about a single item in a playlist and it's bookmark objects. """
464 def __init__(self):
465 # metadata that's pulled from the playlist file (pls/extm3u)
466 self.reported_filepath = None
467 self.title = None
468 self.length = None
470 self.playlist_id = None
471 self.filepath = None
472 self.duplicate_id = 0
473 self.seek_to = 0
475 # a flag to determine whether the item's bookmarks need updating
476 # ( used for example, if the duplicate_id is changed )
477 self.is_modified = False
478 self.bookmarks = []
480 def __eq__(self, b):
481 if isinstance( b, PlaylistItem ):
482 return ( self.filepath == b.filepath and
483 self.duplicate_id == b.duplicate_id )
484 elif isinstance( b, str ):
485 return str(self) == b
486 else:
487 log('[PlaylistItem] Unsupported comparison...')
488 return False
490 def __str__(self):
491 uid = self.filepath + str(self.duplicate_id)
492 return md5(uid).hexdigest()
494 @property
495 def metadata(self):
496 """ Metadata is only needed once, so fetch it on-the-fly
497 If needed this could easily be cached at the cost of wasting a
498 bunch of memory """
500 m = FileMetadata(self.filepath)
501 metadata = m.get_metadata()
502 del m # *hopefully* save some memory
503 return metadata
505 @property
506 def filetype(self):
507 return util.detect_filetype(self.filepath)
509 def load_bookmarks(self):
510 self.bookmarks = db.load_bookmarks(
511 factory = Bookmark().load_from_dict,
512 playlist_id = self.playlist_id,
513 bookmark_filepath = self.reported_filepath,
514 playlist_duplicate_id = self.duplicate_id,
515 allow_resume_bookmarks = False )
517 def save_bookmark(self, name, position, resume_pos=False):
518 b = Bookmark()
519 b.playlist_id = self.playlist_id
520 b.bookmark_name = name
521 b.bookmark_filepath = self.reported_filepath
522 b.seek_position = position
523 b.timestamp = time.time()
524 b.is_resume_position = resume_pos
525 b.playlist_duplicate_id = self.duplicate_id
526 b.save()
527 self.bookmarks.append(b)
529 def delete_bookmark(self, bookmark_id):
530 """ WARNING: if bookmark_id is None, ALL bookmarks will be deleted """
531 if bookmark_id is None:
532 log('Deleting all bookmarks for %s' % self.reported_filepath)
533 for bkmk in self.bookmarks:
534 bkmk.delete()
535 else:
536 bkmk = self.bookmarks.index(bookmark_id)
537 if bkmk >= 0:
538 self.bookmarks[bkmk].delete()
539 else:
540 log('Cannot find bookmark with id: %s' % bookmark_id)
541 return False
542 return True
544 def update_bookmarks(self):
545 for bookmark in self.bookmarks:
546 bookmark.playlist_duplicate_id = self.duplicate_id
547 bookmark.bookmark_filepath = self.reported_filepath
548 db.update_bookmark(bookmark)
550 class Bookmark(object):
551 """ A single bookmark, nothing more, nothing less. """
553 def __init__(self):
554 self.id = 0
555 self.playlist_id = None
556 self.bookmark_name = ''
557 self.bookmark_filepath = ''
558 self.seek_position = 0
559 self.timestamp = 0
560 self.is_resume_position = False
561 self.playlist_duplicate_id = 0
563 @staticmethod
564 def load_from_dict(bkmk_dict):
565 bkmkobj = Bookmark()
567 for key,value in bkmk_dict.iteritems():
568 if hasattr( bkmkobj, key ):
569 setattr( bkmkobj, key, value )
570 else:
571 log('Attr: %s doesn\'t exist...' % key)
573 return bkmkobj
575 def save(self):
576 self.id = db.save_bookmark(self)
577 return self.id
579 def delete(self):
580 return db.remove_bookmark(self.id)
582 def __eq__(self, b):
583 if isinstance(b, str):
584 return str(self) == b
585 else:
586 log('[Bookmark] Unsupported comparison...')
587 return False
589 def __str__(self):
590 uid = self.bookmark_filepath
591 uid += str(self.playlist_duplicate_id)
592 uid += str(self.seek_position)
593 return md5(uid).hexdigest()
595 def __cmp__(self, b):
596 if self.bookmark_filepath == b.bookmark_filepath:
597 if self.seek_position == b.seek_position:
598 return 0
599 else:
600 return -1 if self.seek_position < b.seek_position else 1
601 else:
602 log('Can\'t compare bookmarks from different files...')
603 return 0
605 class FileMetadata(object):
606 """ A class to hold all information about the file that's currently being
607 played. Basically it takes care of metadata extraction... """
609 coverart_names = ['cover', 'cover.jpg', 'cover.png']
610 tag_mappings = {
611 'mp4': { '\xa9nam': 'title',
612 '\xa9ART': 'artist',
613 '\xa9alb': 'album',
614 'covr': 'coverart' },
615 'mp3': { 'TIT2': 'title',
616 'TPE1': 'artist',
617 'TALB': 'album',
618 'APIC': 'coverart' },
619 'ogg': { 'title': 'title',
620 'artist': 'artist',
621 'album': 'album' },
623 tag_mappings['m4a'] = tag_mappings['mp4']
624 tag_mappings['flac'] = tag_mappings['ogg']
626 def __init__(self, filepath):
627 self.filepath = filepath
629 self.title = ''
630 self.artist = ''
631 self.album = ''
632 self.length = 0
633 self.coverart = None
635 self.__metadata_extracted = False
637 def extract_metadata(self):
638 filetype = util.detect_filetype(self.filepath)
640 if filetype == 'mp3':
641 import mutagen.mp3 as meta_parser
642 elif filetype == 'ogg':
643 import mutagen.oggvorbis as meta_parser
644 elif filetype == 'flac':
645 import mutagen.flac as meta_parser
646 elif filetype in ['mp4', 'm4a']:
647 import mutagen.mp4 as meta_parser
648 else:
649 log('Extracting metadata not supported for %s files.' % filetype)
650 return False
652 try:
653 metadata = meta_parser.Open(self.filepath)
654 except Exception, e:
655 self.title = util.pretty_filename(self.filepath)
656 log('Error running metadata parser...', exception=e)
657 return False
659 self.length = metadata.info.length * 10**9
660 for tag,value in metadata.iteritems():
661 if tag.find(':') != -1: # hack for weirdly named coverart tags
662 tag = tag.split(':')[0]
664 if self.tag_mappings[filetype].has_key(tag):
665 if isinstance( value, list ):
666 if len(value):
667 # Here we could either join the list or just take one
668 # item. I chose the latter simply because some ogg
669 # files have several messed up titles...
670 value = value[0]
671 else:
672 continue
674 if self.tag_mappings[filetype][tag] != 'coverart':
675 try:
676 value = escape(str(value))
677 except Exception, e:
678 log( 'Could not convert tag (%s) to escaped string' % tag,
679 exception=e )
680 else:
681 # some coverart classes store the image in the data
682 # attribute whereas others do not :S
683 if hasattr( value, 'data' ):
684 value = value.data
686 setattr( self, self.tag_mappings[filetype][tag], value )
688 if not str(self.title).strip():
689 self.title = util.pretty_filename(self.filepath)
691 if self.coverart is None:
692 self.coverart = self.__find_coverart()
694 def __find_coverart(self):
695 """ Find coverart in the same directory as the filepath """
696 directory = os.path.dirname(self.filepath)
697 for cover in self.coverart_names:
698 c = os.path.join( directory, cover )
699 if os.path.isfile(c):
700 try:
701 f.open(c,'r')
702 binary_coverart = f.read()
703 f.close()
704 return binary_coverart
705 except:
706 pass
707 return None
709 def get_metadata(self):
710 """ Returns a dict of metadata """
712 if not self.__metadata_extracted:
713 log('Extracting metadata for %s' % self.filepath)
714 self.extract_metadata()
715 self.__metadata_extracted = True
717 metadata = {
718 'title': self.title,
719 'artist': self.artist,
720 'album': self.album,
721 'image': self.coverart,
722 'length': self.length
725 return metadata
727 class PlaylistFile(object):
728 """ The base class for playlist file parsers/exporters,
729 this should not be used directly but instead subclassed. """
731 def __init__(self, filepath, id):
732 self._filepath = filepath
733 self._file = None
734 self._items = Queue(id)
736 def __open_file(self, filepath, mode):
737 if self._file is not None:
738 self.close_file()
740 try:
741 self._file = open( filepath, mode )
742 self._filepath = filepath
743 except Exception, e:
744 self._filepath = None
745 self._file = None
747 log( 'Error opening file: %s' % filepath, exception=e )
748 return False
750 return True
752 def __close_file(self):
753 error = False
755 if self._file is not None:
756 try:
757 self._file.close()
758 except Exception, e:
759 log( 'Error closing file: %s' % self.filepath, exception=e )
760 error = True
762 self._filepath = None
763 self._file = None
765 return not error
767 def get_absolute_filepath(self, item_filepath):
768 if item_filepath is None: return
770 if item_filepath.startswith('/'):
771 path = item_filepath
772 else:
773 path = os.path.join(os.path.dirname(self._filepath), item_filepath)
775 if os.path.exists( path ):
776 return path
778 def get_filelist(self):
779 return [ item.filepath for item in self._items ]
781 def get_filedicts(self):
782 dict_list = []
783 for item in self._items:
784 d = { 'title': item.title,
785 'length': item.length,
786 'filepath': item.filepath }
788 dict_list.append(d)
789 return dict_list
791 def get_queue(self):
792 return self._items
794 def export_items(self, filepath=None, playlist_items=None):
795 if filepath is not None:
796 self._filepath = filepath
798 if playlist_items is not None:
799 self._items = playlist_items
801 if self.__open_file(filepath, 'w'):
802 self.export_hook(self._items)
803 self.__close_file()
804 return True
805 else:
806 return False
808 def export_hook(self, playlist_items):
809 pass
811 def parse(self, filepath):
812 if self.__open_file( filepath, mode='r' ):
813 current_line = self._file.readline()
814 while current_line:
815 self.parse_line_hook( current_line.strip() )
816 current_line = self._file.readline()
817 self.__close_file()
818 self.parse_eof_hook()
819 else:
820 return False
821 return True
823 def parse_line_hook(self, line):
824 pass
826 def parse_eof_hook(self):
827 pass
829 def _add_playlist_item(self, item):
830 path = self.get_absolute_filepath(item.reported_filepath)
831 if path is not None and os.path.isfile(path):
832 item.filepath = path
833 self._items.append(item)
835 class M3U_Playlist(PlaylistFile):
836 """ An (extended) m3u parser/writer """
838 def __init__(self, *args):
839 PlaylistFile.__init__( self, *args )
840 self.extended_m3u = False
841 self.current_item = PlaylistItem()
843 def parse_line_hook(self, line):
844 if line.startswith('#EXTM3U'):
845 self.extended_m3u = True
846 elif self.extended_m3u and line.startswith('#EXTINF'):
847 match = re.match('#EXTINF:([^,]+),(.*)', line)
848 if match is not None:
849 length, title = match.groups()
850 try: length = int(length)
851 except: pass
852 self.current_item.length = length
853 self.current_item.title = title
854 elif line.startswith('#'):
855 pass # skip comments
856 elif line:
857 path = self.get_absolute_filepath( line )
858 if path is not None:
859 if os.path.isfile( path ):
860 self.current_item.reported_filepath = line
861 self._add_playlist_item(self.current_item)
862 self.current_item = PlaylistItem()
863 elif os.path.isdir( path ):
864 files = os.listdir( path )
865 for file in files:
866 item = PlaylistItem()
867 item.reported_filepath = os.path.join(line, file)
868 self._add_playlist_item(item)
870 def export_hook(self, playlist_items):
871 self._file.write('#EXTM3U\n\n')
873 for item in playlist_items:
874 string = ''
875 if not ( item.length is None and item.title is None ):
876 length = -1 if item.length is None else item.length
877 title = '' if item.title is None else item.title
878 string += '#EXTINF:%d,%s\n' % ( length, title )
880 string += '%s\n' % item.reported_filepath
881 self._file.write(string)
883 class PLS_Playlist(PlaylistFile):
884 """ A somewhat simple pls parser/writer """
886 def __init__(self, *args):
887 PlaylistFile.__init__( self, *args )
888 self.current_item = PlaylistItem()
889 self.in_playlist_section = False
890 self.current_item_number = None
892 def __add_current_item(self):
893 self._add_playlist_item(self.current_item)
895 def parse_line_hook(self, line):
896 sect_regex = '\[([^\]]+)\]'
897 item_regex = '[^\d]+([\d]+)=(.*)'
899 if re.search(item_regex, line) is not None:
900 current = re.search(item_regex, line).group(1)
901 if self.current_item_number is None:
902 self.current_item_number = current
903 elif self.current_item_number != current:
904 self.__add_current_item()
906 self.current_item = PlaylistItem()
907 self.current_item_number = current
909 if re.search(sect_regex, line) is not None:
910 section = re.match(sect_regex, line).group(1).lower()
911 self.in_playlist_section = section == 'playlist'
912 elif not self.in_playlist_section:
913 pass # don't do anything if we're not in [playlist]
914 elif line.lower().startswith('file'):
915 self.current_item.reported_filepath = re.search(
916 item_regex, line).group(2)
917 elif line.lower().startswith('title'):
918 self.current_item.title = re.search(item_regex, line).group(2)
919 elif line.lower().startswith('length'):
920 try: length = int(re.search(item_regex, line).group(2))
921 except: pass
922 self.current_item.length = length
924 def parse_eof_hook(self):
925 self.__add_current_item()
927 def export_hook(self, playlist_items):
928 self._file.write('[playlist]\n')
929 self._file.write('NumberOfEntries=%d\n\n' % len(playlist_items))
931 for i,item in enumerate(playlist_items):
932 title = '' if item.title is None else item.title
933 length = -1 if item.length is None else item.length
934 self._file.write('File%d=%s\n' % (i+1, item.reported_filepath))
935 self._file.write('Title%d=%s\n' % (i+1, title))
936 self._file.write('Length%d=%s\n\n' % (i+1, length))
938 self._file.write('Version=2\n')