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.
30 from hashlib
import md5
31 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):
46 self
.__queue
= Queue(self
.id)
48 self
.__bookmarks
_model
= None
49 self
.__bookmarks
_model
_changed
= True
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 )
60 def reset_playlist(self
):
61 """ clears all the files in the filelist """
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
)
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
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
)
101 # copy the bookmarks over to new playlist
102 db
.remove_all_bookmarks(self
.id)
103 self
.__queue
.set_new_playlist_id(self
.id)
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
)
119 log('Item with id "%s" not found' % item_id
)
122 self
.__queue
.current_item_position
= self
.__queue
.index(item_id
)
125 self
.__queue
.current_item
.seek_to
= 0
127 self
.__queue
.current_item
.seek_to
= bookmark
.seek_position
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
)
140 log('No such item id (%s)' % item_id
)
143 if bookmark_id
is not None and bookmark
is None:
144 log('No such bookmark id (%s)' % bookmark_id
)
147 if bookmark_id
is None:
151 bookmark
.timestamp
= time
.time()
154 bookmark
.bookmark_name
= name
156 if seek_pos
is not None:
157 bookmark
.seek_position
= seek_pos
159 db
.update_bookmark(bookmark
)
163 def update_bookmarks(self
):
164 """ Updates the database entries for items that have been modified """
165 for item
in self
.__queue
:
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
)
175 log('Cannot find item with id: %s' % item_id
)
178 if bookmark_id
is None:
179 item
.delete_bookmark(None)
180 self
.__queue
.remove(item_id
)
182 item
.delete_bookmark(bookmark_id
)
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
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
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
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
]
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
)
261 self
.reset_playlist()
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()
275 error
= not self
.single_file_import(File
)
277 self
.queue_modified
= os
.path
.expanduser(
278 settings
.temp_playlist
) == self
.filepath
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 ##################################
290 ##################################
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 )
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 )
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.
321 current_item
= self
.__queue
.current_item_position
323 if skip_by
is not None:
325 skip
= current_item
+ skip_by
327 skip
= ( current_item
+ skip_by
) % self
.queue_length()
328 elif skip_to
is not None:
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()) )
338 self
.__queue
.current_item_position
= skip
339 log('Skipping to file %d (%s)' % (
340 skip
, self
.__queue
.current_item
.filepath
))
344 """ Move the playlist to the next track.
345 False indicates end of playlist. """
346 return self
.skip( skip_by
=1, dont_loop
=True )
349 """ Same as next() except moves to the previous track. """
350 return self
.skip( skip_by
=-1, dont_loop
=True )
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
362 def create_item_by_filepath(self
, reported_filepath
, filepath
):
363 item
= PlaylistItem()
364 item
.reported_filepath
= reported_filepath
365 item
.filepath
= filepath
368 def __count_dupe_items(self
, subset
, item
):
369 """ Count the number of duplicate items (by filepath only) in a list """
372 tally
+= int( i
.filepath
== item
.filepath
)
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
)
386 log('File not found or not supported: %s' % item
.filepath
)
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
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
)]
408 def set_new_playlist_id(self
, id):
409 self
.playlist_id
= id
411 item
.playlist_id
= id
412 for bookmark
in item
.bookmarks
:
413 bookmark
.playlist_id
= id
416 def insert(self
, position
, item
):
417 if not self
.__prep
_item
(item
):
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
:
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
)
440 def append(self
, item
):
441 if not self
.__prep
_item
(item
):
444 item
.duplicate_id
= self
.__count
_dupe
_items
(self
, item
)
445 item
.load_bookmarks()
447 list.append(self
, item
)
450 def remove(self
, item
):
452 list.remove(self
, item
)
454 def extend(self
, items
):
455 log('FIXME: extend not supported yet...')
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. """
465 # metadata that's pulled from the playlist file (pls/extm3u)
466 self
.reported_filepath
= None
470 self
.playlist_id
= None
472 self
.duplicate_id
= 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
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
487 log('[PlaylistItem] Unsupported comparison...')
491 uid
= self
.filepath
+ str(self
.duplicate_id
)
492 return md5(uid
).hexdigest()
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
500 m
= FileMetadata(self
.filepath
)
501 metadata
= m
.get_metadata()
502 del m
# *hopefully* save some memory
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):
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
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
:
536 bkmk
= self
.bookmarks
.index(bookmark_id
)
538 self
.bookmarks
[bkmk
].delete()
540 log('Cannot find bookmark with id: %s' % bookmark_id
)
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. """
555 self
.playlist_id
= None
556 self
.bookmark_name
= ''
557 self
.bookmark_filepath
= ''
558 self
.seek_position
= 0
560 self
.is_resume_position
= False
561 self
.playlist_duplicate_id
= 0
564 def load_from_dict(bkmk_dict
):
567 for key
,value
in bkmk_dict
.iteritems():
568 if hasattr( bkmkobj
, key
):
569 setattr( bkmkobj
, key
, value
)
571 log('Attr: %s doesn\'t exist...' % key
)
576 self
.id = db
.save_bookmark(self
)
580 return db
.remove_bookmark(self
.id)
583 if isinstance(b
, str):
584 return str(self
) == b
586 log('[Bookmark] Unsupported comparison...')
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
:
600 return -1 if self
.seek_position
< b
.seek_position
else 1
602 log('Can\'t compare bookmarks from different files...')
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']
611 'mp4': { '\xa9nam': 'title',
614 'covr': 'coverart' },
615 'mp3': { 'TIT2': 'title',
618 'APIC': 'coverart' },
619 'ogg': { 'title': 'title',
623 tag_mappings
['m4a'] = tag_mappings
['mp4']
624 tag_mappings
['flac'] = tag_mappings
['ogg']
626 def __init__(self
, filepath
):
627 self
.filepath
= filepath
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
649 log('Extracting metadata not supported for %s files.' % filetype
)
653 metadata
= meta_parser
.Open(self
.filepath
)
655 self
.title
= util
.pretty_filename(self
.filepath
)
656 log('Error running metadata parser...', exception
=e
)
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 ):
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...
674 if self
.tag_mappings
[filetype
][tag
] != 'coverart':
676 value
= escape(str(value
))
678 log( 'Could not convert tag (%s) to escaped string' % tag
,
681 # some coverart classes store the image in the data
682 # attribute whereas others do not :S
683 if hasattr( 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
):
702 binary_coverart
= f
.read()
704 return binary_coverart
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
719 'artist': self
.artist
,
721 'image': self
.coverart
,
722 'length': self
.length
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
734 self
._items
= Queue(id)
736 def __open_file(self
, filepath
, mode
):
737 if self
._file
is not None:
741 self
._file
= open( filepath
, mode
)
742 self
._filepath
= filepath
744 self
._filepath
= None
747 log( 'Error opening file: %s' % filepath
, exception
=e
)
752 def __close_file(self
):
755 if self
._file
is not None:
759 log( 'Error closing file: %s' % self
.filepath
, exception
=e
)
762 self
._filepath
= None
767 def get_absolute_filepath(self
, item_filepath
):
768 if item_filepath
is None: return
770 if item_filepath
.startswith('/'):
773 path
= os
.path
.join(os
.path
.dirname(self
._filepath
), item_filepath
)
775 if os
.path
.exists( path
):
778 def get_filelist(self
):
779 return [ item
.filepath
for item
in self
._items
]
781 def get_filedicts(self
):
783 for item
in self
._items
:
784 d
= { 'title': item
.title
,
785 'length': item
.length
,
786 'filepath': item
.filepath
}
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
)
808 def export_hook(self
, playlist_items
):
811 def parse(self
, filepath
):
812 if self
.__open
_file
( filepath
, mode
='r' ):
813 current_line
= self
._file
.readline()
815 self
.parse_line_hook( current_line
.strip() )
816 current_line
= self
._file
.readline()
818 self
.parse_eof_hook()
823 def parse_line_hook(self
, line
):
826 def parse_eof_hook(self
):
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
):
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
)
852 self
.current_item
.length
= length
853 self
.current_item
.title
= title
854 elif line
.startswith('#'):
857 path
= self
.get_absolute_filepath( line
)
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
)
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
:
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))
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')