3 # Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project
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.
31 from hashlib
import md5
32 from xml
.sax
.saxutils
import escape
35 from dbsqlite
import db
36 from settings
import settings
37 from simplegconf
import gconf
41 class Playlist(object):
43 self
.__log
= logging
.getLogger('panucci.playlist.Playlist')
47 self
.__queue
= Queue(self
.id)
49 self
.__bookmarks
_model
= None
50 self
.__bookmarks
_model
_changed
= True
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 )
61 def reset_playlist(self
):
62 """ clears all the files in the filelist """
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
)
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
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
)
102 # copy the bookmarks over to new playlist
103 db
.remove_all_bookmarks(self
.id)
104 self
.__queue
.set_new_playlist_id(self
.id)
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
)
120 self
.__log
.info('Item with id "%s" not found', item_id
)
123 self
.__queue
.current_item_position
= self
.__queue
.index(item_id
)
126 self
.__queue
.current_item
.seek_to
= 0
128 self
.__queue
.current_item
.seek_to
= bookmark
.seek_position
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
)
141 self
.__log
.warning('No such item id (%s)', item_id
)
144 if bookmark_id
is not None and bookmark
is None:
145 self
.__log
.warning('No such bookmark id (%s)', bookmark_id
)
148 if bookmark_id
is None:
152 bookmark
.timestamp
= time
.time()
155 bookmark
.bookmark_name
= name
157 if seek_pos
is not None:
158 bookmark
.seek_position
= seek_pos
160 db
.update_bookmark(bookmark
)
164 def update_bookmarks(self
):
165 """ Updates the database entries for items that have been modified """
166 for item
in self
.__queue
:
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
)
177 self
.__log
.info('Cannot find item with id: %s', item_id
)
180 if bookmark_id
is None:
181 item
.delete_bookmark(None)
182 self
.__queue
.remove(item_id
)
184 item
.delete_bookmark(bookmark_id
)
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
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
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
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
]
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
)
263 self
.reset_playlist()
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()
277 error
= not self
.single_file_import(File
)
279 self
.queue_modified
= os
.path
.expanduser(
280 settings
.temp_playlist
) == self
.filepath
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 ##################################
291 ##################################
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 )
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 )
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.
322 current_item
= self
.__queue
.current_item_position
324 if skip_by
is not None:
326 skip
= current_item
+ skip_by
328 skip
= ( current_item
+ skip_by
) % self
.queue_length()
329 elif skip_to
is not None:
332 self
.__log
.warning('No skip method provided...')
334 if not ( 0 <= skip
< self
.queue_length() ):
336 'Can\'t skip to non-existant file. (requested=%d, total=%d)',
337 skip
, self
.queue_length() )
340 self
.__queue
.current_item_position
= skip
341 self
.__log
.debug('Skipping to file %d (%s)', skip
,
342 self
.__queue
.current_item
.filepath
)
346 """ Move the playlist to the next track.
347 False indicates end of playlist. """
348 return self
.skip( skip_by
=1, dont_loop
=True )
351 """ Same as next() except moves to the previous track. """
352 return self
.skip( skip_by
=-1, dont_loop
=True )
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
366 def __count_dupe_items(self
, subset
, item
):
367 """ Count the number of duplicate items (by filepath only) in a list """
370 tally
+= int( i
.filepath
== item
.filepath
)
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
)
385 'File not found or not supported: %s', item
.filepath
)
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
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
)
406 if item
.bookmarks
.count(bookmark_id
):
407 return item
, item
.bookmarks
[item
.bookmarks
.index(bookmark_id
)]
413 def set_new_playlist_id(self
, id):
414 self
.playlist_id
= id
416 item
.playlist_id
= id
417 for bookmark
in item
.bookmarks
:
418 bookmark
.playlist_id
= id
421 def insert(self
, position
, item
):
422 if not self
.__prep
_item
(item
):
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
:
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
)
445 def append(self
, item
):
446 if not self
.__prep
_item
(item
):
449 item
.duplicate_id
= self
.__count
_dupe
_items
(self
, item
)
450 item
.load_bookmarks()
452 list.append(self
, item
)
455 def remove(self
, item
):
457 list.remove(self
, item
)
459 def extend(self
, items
):
460 self
.__log
.warning('FIXME: extend not supported yet...')
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. """
470 self
.__log
= logging
.getLogger('panucci.playlist.PlaylistItem')
472 # metadata that's pulled from the playlist file (pls/extm3u)
473 self
.reported_filepath
= None
477 self
.playlist_id
= None
479 self
.duplicate_id
= 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
488 def create_by_filepath(reported_filepath
, filepath
):
489 item
= PlaylistItem()
490 item
.reported_filepath
= reported_filepath
491 item
.filepath
= filepath
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
501 self
.__log
.warning('Unsupported comparison: %s', type(b
))
505 uid
= self
.filepath
+ str(self
.duplicate_id
)
506 return md5(uid
).hexdigest()
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
514 m
= FileMetadata(self
.filepath
)
515 metadata
= m
.get_metadata()
516 del m
# *hopefully* save some memory
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):
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
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:
547 'Deleting all bookmarks for %s', self
.reported_filepath
)
548 for bkmk
in self
.bookmarks
:
551 bkmk
= self
.bookmarks
.index(bookmark_id
)
553 self
.bookmarks
[bkmk
].delete()
555 self
.__log
.info('Cannot find bookmark with id: %s',bookmark_id
)
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. """
569 self
.__log
= logging
.getLogger('panucci.playlist.Bookmark')
572 self
.playlist_id
= None
573 self
.bookmark_name
= ''
574 self
.bookmark_filepath
= ''
575 self
.seek_position
= 0
577 self
.is_resume_position
= False
578 self
.playlist_duplicate_id
= 0
581 def load_from_dict(bkmk_dict
):
584 for key
,value
in bkmk_dict
.iteritems():
585 if hasattr( bkmkobj
, key
):
586 setattr( bkmkobj
, key
, value
)
588 self
.__log
.info('Attr: %s doesn\'t exist...', key
)
593 self
.id = db
.save_bookmark(self
)
597 return db
.remove_bookmark(self
.id)
600 if isinstance(b
, str):
601 return str(self
) == b
603 self
.__log
.warning('Unsupported comparison: %s', type(b
))
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
:
617 return -1 if self
.seek_position
< b
.seek_position
else 1
620 'Can\'t compare bookmarks from different files:\n\tself: %s'
621 '\n\tb: %s', self
.bookmark_filepath
, b
.bookmark_filepath
)
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']
630 'mp4': { '\xa9nam': 'title',
633 'covr': 'coverart' },
634 'mp3': { 'TIT2': 'title',
637 'APIC': 'coverart' },
638 'ogg': { 'title': 'title',
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
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
670 'Extracting metadata not supported for %s files.', filetype
)
674 metadata
= meta_parser
.Open(self
.__filepath
)
676 self
.title
= util
.pretty_filename(self
.__filepath
)
677 self
.__log
.exception('Error running metadata parser...')
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 ):
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...
695 if self
.tag_mappings
[filetype
][tag
] != 'coverart':
697 value
= escape(str(value
))
699 self
.__log
.exception(
700 'Could not convert tag (%s) to escaped string', tag
)
702 # some coverart classes store the image in the data
703 # attribute whereas others do not :S
704 if hasattr( 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
):
723 binary_coverart
= f
.read()
725 return binary_coverart
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
740 'artist': self
.artist
,
742 'image': self
.coverart
,
743 'length': self
.length
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
756 self
._items
= Queue(id)
758 def __open_file(self
, filepath
, mode
):
759 if self
._file
is not None:
763 self
._file
= open( filepath
, mode
)
764 self
._filepath
= filepath
766 self
._filepath
= None
769 self
.__log
.exception( 'Error opening file: %s', filepath
)
774 def __close_file(self
):
777 if self
._file
is not None:
781 self
.__log
.exception( 'Error closing file: %s', self
.filepath
)
784 self
._filepath
= None
789 def get_absolute_filepath(self
, item_filepath
):
790 if item_filepath
is None: return
792 if item_filepath
.startswith('/'):
795 path
= os
.path
.join(os
.path
.dirname(self
._filepath
), item_filepath
)
797 if os
.path
.exists( path
):
800 def get_filelist(self
):
801 return [ item
.filepath
for item
in self
._items
]
803 def get_filedicts(self
):
805 for item
in self
._items
:
806 d
= { 'title': item
.title
,
807 'length': item
.length
,
808 'filepath': item
.filepath
}
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
)
830 def export_hook(self
, playlist_items
):
833 def parse(self
, filepath
):
834 if self
.__open
_file
( filepath
, mode
='r' ):
835 current_line
= self
._file
.readline()
837 self
.parse_line_hook( current_line
.strip() )
838 current_line
= self
._file
.readline()
840 self
.parse_eof_hook()
845 def parse_line_hook(self
, line
):
848 def parse_eof_hook(self
):
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
):
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
)
875 self
.current_item
.length
= length
876 self
.current_item
.title
= title
877 elif line
.startswith('#'):
880 path
= self
.get_absolute_filepath( line
)
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
)
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
:
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))
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')