3 # This file is part of Panucci.
4 # Copyright (c) 2008-2009 The Panucci Audiobook and Podcast Player Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
26 from hashlib
import md5
27 from xml
.sax
.saxutils
import escape
30 from dbsqlite
import db
31 from settings
import settings
32 from simplegconf
import gconf
33 from services
import ObservableService
37 class Playlist(ObservableService
):
38 signals
= [ 'new_track', 'new_track_metadata', 'file_queued',
42 self
.__log
= logging
.getLogger('panucci.playlist.Playlist')
43 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
45 self
.__queue
= Queue(None)
46 self
.__queue
.register(
47 'current_item_changed', self
.on_queue_current_item_changed
)
51 def reset_playlist(self
):
52 """ Sets the playlist to a default "known" state """
57 self
.__bookmarks
_model
= None
58 self
.__bookmarks
_model
_changed
= True
62 if self
.filepath
is None:
63 self
.__log
.warning("Can't get playlist id without having filepath")
64 elif self
._id
is None:
65 self
._id
= db
.get_playlist_id( self
.filepath
, True, True )
70 def current_filepath(self
):
71 """ Get the current file """
73 return self
.__queue
.current_item
.filepath
76 def queue_modified(self
):
77 return self
.__queue
.modified
80 def queue_length(self
):
81 return len(self
.__queue
)
85 return not self
.__queue
87 def print_queue_layout(self
):
88 """ This helps with debugging ;) """
89 for item
in self
.__queue
:
90 print str(item
), item
.reported_filepath
91 for bookmark
in item
.bookmarks
:
92 print '\t', str(bookmark
), bookmark
.bookmark_filepath
94 def save_to_new_playlist(self
, filepath
, playlist_type
='m3u'):
95 self
.filepath
= filepath
98 playlist
= { 'm3u': M3U_Playlist
, 'pls': PLS_Playlist
}
99 if not playlist
.has_key(playlist_type
):
100 playlist_type
= 'm3u' # use m3u by default
101 self
.filepath
+= '.m3u'
103 playlist
= playlist
[playlist_type
](self
.filepath
, self
.__queue
)
104 if not playlist
.export_items( filepath
):
105 self
.__log
.error('Error exporting playlist to %s', self
.filepath
)
108 # copy the bookmarks over to new playlist
109 db
.remove_all_bookmarks(self
.id)
110 self
.__queue
.set_new_playlist_id(self
.id)
114 def save_temp_playlist(self
):
115 filepath
= os
.path
.expanduser(settings
.temp_playlist
)
116 return self
.save_to_new_playlist(filepath
)
118 def on_queue_current_item_changed(self
):
119 self
.send_new_metadata( self
.on_queue_current_item_changed
)
120 self
.notify( 'new_track', caller
=self
.on_queue_current_item_changed
)
122 def send_new_metadata(self
, caller
=None):
123 self
.notify( 'new_track_metadata', self
.get_file_metadata(),
124 caller
=caller
or self
.send_new_metadata
)
127 self
.__log
.debug('quit() called.')
128 if self
.__queue
.modified
:
129 self
.__log
.info('Queue modified, saving temporary playlist')
130 self
.save_temp_playlist()
132 ######################################
133 # Bookmark-related functions
134 ######################################
136 def __load_from_bookmark( self
, item_id
, bookmark
):
137 self
.__queue
.current_item_position
= self
.__queue
.index(item_id
)
140 self
.__queue
.current_item
.seek_to
= 0
142 self
.__queue
.current_item
.seek_to
= bookmark
.seek_position
146 def load_from_bookmark_id( self
, item_id
=None, bookmark_id
=None ):
147 item
, bookmark
= self
.__queue
.get_bookmark(item_id
, bookmark_id
)
150 return self
.__load
_from
_bookmark
( str(item
), bookmark
)
153 'item_id=%s,bookmark_id=%s not found', item_id
, bookmark_id
)
156 def find_resume_bookmark(self
):
157 """ Find a resume bookmark in the queue """
158 for item
in self
.__queue
:
159 for bookmark
in item
.bookmarks
:
160 if bookmark
.is_resume_position
:
161 return str(item
), str(bookmark
)
165 def load_from_resume_bookmark(self
):
166 item_id
, bookmark_id
= self
.find_resume_bookmark()
167 if None in ( item_id
, bookmark_id
):
168 self
.__log
.info('No resume bookmark found.')
171 return self
.load_from_bookmark_id( item_id
, bookmark_id
)
173 def save_bookmark( self
, bookmark_name
, position
):
174 self
.notify( 'bookmark_added', caller
=self
.save_bookmark
)
175 self
.__queue
.current_item
.save_bookmark(
176 bookmark_name
, position
, resume_pos
=False )
177 self
.__bookmarks
_model
_changed
= True
179 def update_bookmark(self
, item_id
, bookmark_id
, name
=None, seek_pos
=None):
180 item
, bookmark
= self
.__queue
.get_bookmark(item_id
, bookmark_id
)
183 self
.__log
.warning('No such item id (%s)', item_id
)
186 if bookmark_id
is not None and bookmark
is None:
187 self
.__log
.warning('No such bookmark id (%s)', bookmark_id
)
190 if bookmark_id
is None:
191 if name
and item
.title
!= name
:
193 self
.__queue
.modified
= True
195 bookmark
.timestamp
= time
.time()
198 bookmark
.bookmark_name
= name
200 if seek_pos
is not None:
201 bookmark
.seek_position
= seek_pos
203 db
.update_bookmark(bookmark
)
207 def update_bookmarks(self
):
208 """ Updates the database entries for items that have been modified """
209 for item
in self
.__queue
:
212 'Playlist Item "%s" is modified, updating bookmarks', item
)
213 item
.update_bookmarks()
214 item
.is_modified
= False
216 def remove_bookmark( self
, item_id
, bookmark_id
):
217 item
= self
.__queue
.get_item(item_id
)
220 self
.__log
.info('Cannot find item with id: %s', item_id
)
223 if bookmark_id
is None:
224 if self
.__queue
.current_item_position
== self
.__queue
.index(item_id
):
227 item
.delete_bookmark(None)
228 self
.__queue
.remove(item_id
)
230 item
.delete_bookmark(bookmark_id
)
234 def remove_resume_bookmarks(self
):
235 item_id
, bookmark_id
= self
.find_resume_bookmark()
237 if None in ( item_id
, bookmark_id
):
240 return self
.remove_bookmark( item_id
, bookmark_id
)
243 def generate_bookmark_model(self
, include_resume_marks
=False):
244 self
.__bookmarks
_model
= gtk
.TreeStore(
245 # uid, name, position
246 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
248 for item
in self
.__queue
:
249 title
= util
.pretty_filename(
250 item
.filepath
) if item
.title
is None else item
.title
251 row
= [ str(item
), title
, None ]
252 parent
= self
.__bookmarks
_model
.append( None, row
)
254 for bkmk
in item
.bookmarks
:
255 if not bkmk
.is_resume_position
or include_resume_marks
:
256 row
= [ str(bkmk
), bkmk
.bookmark_name
,
257 util
.convert_ns(bkmk
.seek_position
) ]
258 self
.__bookmarks
_model
.append( parent
, row
)
260 def get_bookmark_model(self
, include_resume_marks
=False):
261 if self
.__bookmarks
_model
is None or self
.__bookmarks
_model
_changed
:
262 self
.__log
.debug('Generating new bookmarks model')
263 self
.generate_bookmark_model(include_resume_marks
)
264 self
.__bookmarks
_model
_changed
= False
266 self
.__log
.debug('Using cached bookmarks model')
268 return self
.__bookmarks
_model
270 def move_item( self
, from_row
, to_row
):
271 self
.__log
.info('Moving item from position %d to %d', from_row
, to_row
)
272 assert isinstance(from_row
, int) and isinstance(to_row
, int)
273 self
.__queue
.move_item(from_row
, to_row
)
275 ######################################
276 # File-related convenience functions
277 ######################################
279 def get_current_position(self
):
280 """ Returns the saved position for the current
281 file or 0 if no file is available"""
282 if not self
.is_empty
:
283 return self
.__queue
.current_item
.seek_to
287 def get_current_filetype(self
):
288 """ Returns the filetype of the current
289 file or None if no file is available """
291 if not self
.is_empty
:
292 return self
.__queue
.current_item
.filetype
294 def get_file_metadata(self
):
295 """ Return the metadata associated with the current FileObject """
296 if not self
.is_empty
:
297 return self
.__queue
.current_item
.metadata
301 def get_current_filepath(self
):
302 if not self
.is_empty
:
303 return self
.__queue
.current_item
.filepath
305 def get_recent_files(self
, max_files
=10):
306 files
= db
.get_latest_files()
308 if len(files
) > max_files
:
309 return files
[:max_files
]
313 ##################################
314 # File importing functions
315 ##################################
317 def load(self
, filepath
):
318 """ Detects filepath's filetype then loads it using
319 the appropriate loader function """
320 self
.__log
.debug('Attempting to load %s', filepath
)
323 self
.reset_playlist()
324 self
.filepath
= filepath
325 self
.__queue
.playlist_id
= self
.id
327 parsers
= { 'm3u': M3U_Playlist
, 'pls': PLS_Playlist
}
328 extension
= util
.detect_filetype(filepath
)
329 if parsers
.has_key(extension
): # importing a playlist
330 self
.__log
.info('Loading playlist file (%s)', extension
)
331 parser
= parsers
[extension
](self
.filepath
, self
.__queue
)
333 if parser
.parse(filepath
):
334 self
.__queue
= parser
.get_queue()
335 self
.__file
_queued
( filepath
, True, False )
338 else: # importing a single file
339 error
= not self
.append(filepath
, notify
=False)
341 self
.__queue
.disable_notifications
= True
342 self
.load_from_resume_bookmark()
343 self
.__queue
.disable_notifications
= False
345 self
.__queue
.modified
= os
.path
.expanduser(
346 settings
.temp_playlist
) == self
.filepath
348 self
.send_new_metadata( caller
=self
.load
)
352 def load_last_played(self
):
353 recent
= self
.get_recent_files(max_files
=1)
359 def __file_queued(self
, filepath
, successfull
, notify
):
361 self
.__bookmarks
_model
_changed
= True
362 self
.notify( 'file_queued', filepath
, successfull
, notify
,
363 caller
=self
.__file
_queued
)
367 def append(self
, filepath
, notify
=True):
368 self
.__log
.debug('Attempting to queue file: %s', filepath
)
369 success
= self
.__queue
.append(
370 PlaylistItem
.create_by_filepath(filepath
, filepath
) )
372 return self
.__file
_queued
( filepath
, success
, notify
)
374 def insert(self
, position
, filepath
):
376 'Attempting to insert %s at position %s', filepath
, position
)
377 return self
.__file
_queued
( filepath
, self
.__queue
.insert( position
,
378 PlaylistItem
.create_by_filepath(filepath
, filepath
)), True )
380 def load_directory(self
, directory
, append
=False):
381 self
.__log
.debug('Attempting to load directory "%s"', directory
)
384 self
.reset_playlist()
386 if os
.path
.isdir(directory
):
387 self
.filepath
= settings
.temp_playlist
388 self
.__queue
.playlist_id
= self
.id
391 for item
in os
.listdir(directory
):
392 filepath
= os
.path
.join( directory
, item
)
393 if os
.path
.isfile(filepath
) and util
.is_supported(filepath
):
394 items
.append(filepath
)
398 self
.append( item
, notify
=False )
400 self
.__log
.warning('"%s" is not a directory.', directory
)
405 ##################################
407 ##################################
410 """ This gets called by the player to get
411 the last time the file was paused """
412 pos
= self
.__queue
.current_item
.seek_to
413 self
.__queue
.current_item
.seek_to
= 0
416 def pause(self
, position
):
417 """ Called whenever the player is paused """
418 self
.__queue
.current_item
.seek_to
= position
420 def stop(self
, position
, save_resume_point
=True):
421 """ This should be run when the program is closed
422 or if the user switches playlists """
424 self
.remove_resume_bookmarks()
425 if not self
.is_empty
and save_resume_point
:
426 self
.__queue
.current_item
.save_bookmark(
427 _('Auto Bookmark'), position
, True )
429 def skip(self
, skip_by
=None, skip_to
=None, dont_loop
=False):
430 """ Skip to another track in the playlist.
431 Use either skip_by or skip_to, skip_by has precedence.
432 skip_to: skip to a known playlist position
433 skip_by: skip by n number of episodes (positive or negative)
434 dont_loop: applies only to skip_by, if we're skipping past
435 the last track loop back to the begining.
440 current_item
= self
.__queue
.current_item_position
442 if skip_by
is not None:
444 skip
= current_item
+ skip_by
446 skip
= ( current_item
+ skip_by
) % self
.queue_length
447 elif skip_to
is not None:
450 self
.__log
.warning('No skip method provided...')
452 if not ( 0 <= skip
< self
.queue_length
):
454 'Can\'t skip to non-existant file. (requested=%d, total=%d)',
455 skip
, self
.queue_length
)
458 self
.__queue
.current_item_position
= skip
459 self
.__log
.debug('Skipping to file %d (%s)', skip
,
460 self
.__queue
.current_item
.filepath
)
465 """ Move the playlist to the next track.
466 False indicates end of playlist. """
467 return self
.skip( skip_by
=1, dont_loop
=True )
470 """ Same as next() except moves to the previous track. """
471 return self
.skip( skip_by
=-1, dont_loop
=True )
474 class Queue(list, ObservableService
):
475 """ A Simple list of PlaylistItems """
477 signals
= [ 'current_item_changed', ]
479 def __init__(self
, playlist_id
):
480 self
.__log
= logging
.getLogger('panucci.playlist.Queue')
481 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
483 self
.playlist_id
= playlist_id
484 self
.modified
= False # Has the queue been modified?
485 self
.disable_notifications
= False
486 self
.__current
_item
_position
= 0
489 def __get_current_item_position(self
):
490 return self
.__current
_item
_position
492 def __set__current_item_position(self
, new_value
):
494 # set the new position before notify()'ing
495 # or else we'll end up load the old file's metadata
496 old_value
= self
.__current
_item
_position
497 self
.__current
_item
_position
= new_value
499 if old_value
!= new_value
:
500 self
.__log
.debug( 'Current item changed from %d to %d',
501 old_value
, new_value
)
502 if not self
.disable_notifications
:
503 self
.notify( 'current_item_changed',
504 caller
=self
.__set
__current
_item
_position
)
506 current_item_position
= property(
507 __get_current_item_position
, __set__current_item_position
)
509 def __count_dupe_items(self
, subset
, item
):
510 # Count the number of duplicate items (by filepath only) in a list
513 tally
+= int( i
.filepath
== item
.filepath
)
516 def __prep_item(self
, item
):
517 """ Do some error checking and other stuff that's
518 common to the insert and append functions """
520 assert isinstance( item
, PlaylistItem
)
521 item
.playlist_id
= self
.playlist_id
523 if os
.path
.isfile(item
.filepath
) and util
.is_supported(item
.filepath
):
528 'File not found or not supported: %s', item
.filepath
)
533 def current_item(self
):
535 if self
.current_item_position
>= len(self
):
536 self
.__log
.info( 'Current item position is greater '
537 'than queue length, resetting to 0.' )
538 self
.current_item_position
= 0
540 return self
[self
.current_item_position
]
542 self
.__log
.info('Queue is empty...')
544 def move_item(self
, from_pos
, to_pos
):
545 old_current_item
= self
.current_item_position
547 temp
= self
[from_pos
]
548 self
.remove(str(temp
))
549 self
.insert(to_pos
, temp
)
551 if old_current_item
== from_pos
:
552 self
.__current
_item
_position
= to_pos
555 """ Reset the the queue to a known state """
558 self
.playlist_id
= None
559 self
.modified
= False
560 self
.__current
_item
_position
= 0
562 def get_item(self
, item_id
):
563 if self
.count(item_id
):
564 return self
[self
.index(item_id
)]
566 def get_bookmark(self
, item_id
, bookmark_id
):
567 item
= self
.get_item(item_id
)
571 'Item with id "%s" not found, scanning for item...', item_id
)
574 if item_
.bookmarks
.count(bookmark_id
):
578 if item
is None: return None, None
580 if item
.bookmarks
.count(bookmark_id
):
581 return item
, item
.bookmarks
[item
.bookmarks
.index(bookmark_id
)]
585 def set_new_playlist_id(self
, id):
586 self
.playlist_id
= id
588 item
.playlist_id
= id
589 for bookmark
in item
.bookmarks
:
590 bookmark
.playlist_id
= id
593 def insert(self
, position
, item
):
594 if not self
.__prep
_item
(item
):
597 item
.duplicate_id
= self
[:position
].count(item
)
599 if self
.__count
_dupe
_items
(self
[position
:], item
):
600 for i
in self
[position
:]:
601 if i
.filepath
== item
.filepath
:
604 elif not self
.__count
_dupe
_items
(self
[:position
], item
):
605 # there are no other items like this one so it's *safe* to load
606 # bookmarks without a potential conflict, but there's a good chance
607 # that there aren't any bookmarks to load (might be useful in the
608 # event of a crash)...
609 item
.load_bookmarks()
611 if position
<= self
.current_item_position
:
612 self
.__current
_item
_position
+= 1
614 list.insert(self
, position
, item
)
617 def append(self
, item
):
618 if not self
.__prep
_item
(item
):
621 item
.duplicate_id
= self
.__count
_dupe
_items
(self
, item
)
622 item
.load_bookmarks()
624 list.append(self
, item
)
627 def remove(self
, item
):
631 if self
.index(item
) < self
.current_item_position
:
632 self
.__current
_item
_position
-= 1
634 list.remove(self
, item
)
636 def extend(self
, items
):
637 self
.__log
.warning('FIXME: extend not supported yet...')
640 self
.__log
.warning('FIXME: pop not supported yet...')
642 class PlaylistItem(object):
643 """ A (hopefully) lightweight object to hold the bare minimum amount of
644 data about a single item in a playlist and it's bookmark objects. """
647 self
.__log
= logging
.getLogger('panucci.playlist.PlaylistItem')
649 # metadata that's pulled from the playlist file (pls/extm3u)
650 self
.reported_filepath
= None
654 self
.playlist_id
= None
656 self
.duplicate_id
= 0
659 # a flag to determine whether the item's bookmarks need updating
660 # ( used for example, if the duplicate_id is changed )
661 self
.is_modified
= False
665 def create_by_filepath(reported_filepath
, filepath
):
666 item
= PlaylistItem()
667 item
.reported_filepath
= reported_filepath
668 item
.filepath
= filepath
672 if isinstance( b
, PlaylistItem
):
673 return ( self
.filepath
== b
.filepath
and
674 self
.duplicate_id
== b
.duplicate_id
)
675 elif isinstance( b
, str ):
676 return str(self
) == b
678 self
.__log
.warning('Unsupported comparison: %s', type(b
))
682 uid
= self
.filepath
+ str(self
.duplicate_id
)
683 return md5(uid
).hexdigest()
687 """ Metadata is only needed once, so fetch it on-the-fly
688 If needed this could easily be cached at the cost of wasting a
691 m
= FileMetadata(self
.filepath
)
692 metadata
= m
.get_metadata()
693 del m
# *hopefully* save some memory
698 return util
.detect_filetype(self
.filepath
)
700 def load_bookmarks(self
):
701 self
.bookmarks
= db
.load_bookmarks(
702 factory
= Bookmark().load_from_dict
,
703 playlist_id
= self
.playlist_id
,
704 bookmark_filepath
= self
.filepath
,
705 playlist_duplicate_id
= self
.duplicate_id
,
706 request_resume_bookmark
= None )
708 def save_bookmark(self
, name
, position
, resume_pos
=False):
710 b
.playlist_id
= self
.playlist_id
711 b
.bookmark_name
= name
712 b
.bookmark_filepath
= self
.filepath
713 b
.seek_position
= position
714 b
.timestamp
= time
.time()
715 b
.is_resume_position
= resume_pos
716 b
.playlist_duplicate_id
= self
.duplicate_id
718 self
.bookmarks
.append(b
)
720 def delete_bookmark(self
, bookmark_id
):
721 """ WARNING: if bookmark_id is None, ALL bookmarks will be deleted """
722 if bookmark_id
is None:
724 'Deleting all bookmarks for %s', self
.reported_filepath
)
725 for bkmk
in self
.bookmarks
:
728 bkmk
= self
.bookmarks
.index(bookmark_id
)
730 self
.bookmarks
[bkmk
].delete()
731 self
.bookmarks
.remove(bookmark_id
)
733 self
.__log
.info('Cannot find bookmark with id: %s',bookmark_id
)
737 def update_bookmarks(self
):
738 for bookmark
in self
.bookmarks
:
739 bookmark
.playlist_duplicate_id
= self
.duplicate_id
740 bookmark
.bookmark_filepath
= self
.filepath
741 db
.update_bookmark(bookmark
)
743 class Bookmark(object):
744 """ A single bookmark, nothing more, nothing less. """
747 self
.__log
= logging
.getLogger('panucci.playlist.Bookmark')
750 self
.playlist_id
= None
751 self
.bookmark_name
= ''
752 self
.bookmark_filepath
= ''
753 self
.seek_position
= 0
755 self
.is_resume_position
= False
756 self
.playlist_duplicate_id
= 0
759 def load_from_dict(bkmk_dict
):
762 for key
,value
in bkmk_dict
.iteritems():
763 if hasattr( bkmkobj
, key
):
764 setattr( bkmkobj
, key
, value
)
766 self
.__log
.info('Attr: %s doesn\'t exist...', key
)
771 self
.id = db
.save_bookmark(self
)
775 return db
.remove_bookmark(self
.id)
778 if isinstance(b
, str):
779 return str(self
) == b
781 self
.__log
.warning('Unsupported comparison: %s', type(b
))
785 uid
= self
.bookmark_filepath
786 uid
+= str(self
.playlist_duplicate_id
)
787 uid
+= str(self
.seek_position
)
788 return md5(uid
).hexdigest()
790 def __cmp__(self
, b
):
791 if self
.bookmark_filepath
== b
.bookmark_filepath
:
792 if self
.seek_position
== b
.seek_position
:
795 return -1 if self
.seek_position
< b
.seek_position
else 1
798 'Can\'t compare bookmarks from different files:\n\tself: %s'
799 '\n\tb: %s', self
.bookmark_filepath
, b
.bookmark_filepath
)
802 class FileMetadata(object):
803 """ A class to hold all information about the file that's currently being
804 played. Basically it takes care of metadata extraction... """
806 coverart_names
= ['cover', 'cover.jpg', 'cover.png']
808 'mp4': { '\xa9nam': 'title',
811 'covr': 'coverart' },
812 'mp3': { 'TIT2': 'title',
815 'APIC': 'coverart' },
816 'ogg': { 'title': 'title',
820 tag_mappings
['m4a'] = tag_mappings
['mp4']
821 tag_mappings
['flac'] = tag_mappings
['ogg']
823 def __init__(self
, filepath
):
824 self
.__log
= logging
.getLogger('panucci.playlist.FileMetadata')
825 self
.__filepath
= filepath
833 self
.__metadata
_extracted
= False
835 def extract_metadata(self
):
836 filetype
= util
.detect_filetype(self
.__filepath
)
838 if filetype
== 'mp3':
839 import mutagen
.mp3
as meta_parser
840 elif filetype
== 'ogg':
841 import mutagen
.oggvorbis
as meta_parser
842 elif filetype
== 'flac':
843 import mutagen
.flac
as meta_parser
844 elif filetype
in ['mp4', 'm4a']:
845 import mutagen
.mp4
as meta_parser
848 'Extracting metadata not supported for %s files.', filetype
)
852 metadata
= meta_parser
.Open(self
.__filepath
)
854 self
.title
= util
.pretty_filename(self
.__filepath
)
855 self
.__log
.exception('Error running metadata parser...')
858 self
.length
= metadata
.info
.length
* 10**9
859 for tag
,value
in metadata
.iteritems():
860 if tag
.find(':') != -1: # hack for weirdly named coverart tags
861 tag
= tag
.split(':')[0]
863 if self
.tag_mappings
[filetype
].has_key(tag
):
864 if isinstance( value
, list ):
866 # Here we could either join the list or just take one
867 # item. I chose the latter simply because some ogg
868 # files have several messed up titles...
873 if self
.tag_mappings
[filetype
][tag
] != 'coverart':
875 value
= escape(str(value
))
877 self
.__log
.exception(
878 'Could not convert tag (%s) to escaped string', tag
)
880 # some coverart classes store the image in the data
881 # attribute whereas others do not :S
882 if hasattr( value
, 'data' ):
885 setattr( self
, self
.tag_mappings
[filetype
][tag
], value
)
887 if not str(self
.title
).strip():
888 self
.title
= util
.pretty_filename(self
.__filepath
)
890 if self
.coverart
is None:
891 self
.coverart
= self
.__find
_coverart
()
893 def __find_coverart(self
):
894 """ Find coverart in the same directory as the filepath """
895 directory
= os
.path
.dirname(self
.__filepath
)
896 for cover
in self
.coverart_names
:
897 c
= os
.path
.join( directory
, cover
)
898 if os
.path
.isfile(c
):
901 binary_coverart
= f
.read()
903 return binary_coverart
908 def get_metadata(self
):
909 """ Returns a dict of metadata """
911 if not self
.__metadata
_extracted
:
912 self
.__log
.debug('Extracting metadata for %s', self
.__filepath
)
913 self
.extract_metadata()
914 self
.__metadata
_extracted
= True
918 'artist': self
.artist
,
920 'image': self
.coverart
,
921 'length': self
.length
926 class PlaylistFile(object):
927 """ The base class for playlist file parsers/exporters,
928 this should not be used directly but instead subclassed. """
930 def __init__(self
, filepath
, queue
):
931 self
.__log
= logging
.getLogger('panucci.playlist.PlaylistFile')
932 self
._filepath
= filepath
936 def __open_file(self
, filepath
, mode
):
937 if self
._file
is not None:
941 self
._file
= open( filepath
, mode
)
942 self
._filepath
= filepath
944 self
._filepath
= None
947 self
.__log
.exception( 'Error opening file: %s', filepath
)
952 def __close_file(self
):
955 if self
._file
is not None:
959 self
.__log
.exception( 'Error closing file: %s', self
.filepath
)
962 self
._filepath
= None
967 def get_absolute_filepath(self
, item_filepath
):
968 if item_filepath
is None: return
970 if item_filepath
.startswith('/'):
973 path
= os
.path
.join(os
.path
.dirname(self
._filepath
), item_filepath
)
975 if os
.path
.exists( path
):
978 def get_filelist(self
):
979 return [ item
.filepath
for item
in self
._items
]
981 def get_filedicts(self
):
983 for item
in self
._items
:
984 d
= { 'title': item
.title
,
985 'length': item
.length
,
986 'filepath': item
.filepath
}
994 def export_items(self
, filepath
=None):
995 if filepath
is not None:
996 self
._filepath
= filepath
998 if self
.__open
_file
(filepath
, 'w'):
999 self
.export_hook(self
._items
)
1005 def export_hook(self
, playlist_items
):
1008 def parse(self
, filepath
):
1009 if self
.__open
_file
( filepath
, mode
='r' ):
1010 current_line
= self
._file
.readline()
1012 self
.parse_line_hook( current_line
.strip() )
1013 current_line
= self
._file
.readline()
1015 self
.parse_eof_hook()
1020 def parse_line_hook(self
, line
):
1023 def parse_eof_hook(self
):
1026 def _add_playlist_item(self
, item
):
1027 path
= self
.get_absolute_filepath(item
.reported_filepath
)
1028 if path
is not None and os
.path
.isfile(path
):
1029 item
.filepath
= path
1030 self
._items
.append(item
)
1032 class M3U_Playlist(PlaylistFile
):
1033 """ An (extended) m3u parser/writer """
1035 def __init__(self
, *args
):
1036 self
.__log
= logging
.getLogger('panucci.playlist.M3U_Playlist')
1037 PlaylistFile
.__init
__( self
, *args
)
1038 self
.extended_m3u
= False
1039 self
.current_item
= PlaylistItem()
1041 def parse_line_hook(self
, line
):
1042 if line
.startswith('#EXTM3U'):
1043 self
.extended_m3u
= True
1044 elif self
.extended_m3u
and line
.startswith('#EXTINF'):
1045 match
= re
.match('#EXTINF:([^,]+),(.*)', line
)
1046 if match
is not None:
1047 length
, title
= match
.groups()
1048 try: length
= int(length
)
1050 self
.current_item
.length
= length
1051 self
.current_item
.title
= title
1052 elif line
.startswith('#'):
1053 pass # skip comments
1055 path
= self
.get_absolute_filepath( line
)
1056 if path
is not None:
1057 if os
.path
.isfile( path
):
1058 self
.current_item
.reported_filepath
= line
1059 self
._add
_playlist
_item
(self
.current_item
)
1060 self
.current_item
= PlaylistItem()
1061 elif os
.path
.isdir( path
):
1062 files
= os
.listdir( path
)
1064 item
= PlaylistItem()
1065 item
.reported_filepath
= os
.path
.join(line
, file)
1066 self
._add
_playlist
_item
(item
)
1068 def export_hook(self
, playlist_items
):
1069 self
._file
.write('#EXTM3U\n\n')
1071 for item
in playlist_items
:
1073 if not ( item
.length
is None and item
.title
is None ):
1074 length
= -1 if item
.length
is None else item
.length
1075 title
= '' if item
.title
is None else item
.title
1076 string
+= '#EXTINF:%d,%s\n' % ( length
, title
)
1078 string
+= '%s\n' % item
.filepath
1079 self
._file
.write(string
)
1081 class PLS_Playlist(PlaylistFile
):
1082 """ A somewhat simple pls parser/writer """
1084 def __init__(self
, *args
):
1085 self
.__log
= logging
.getLogger('panucci.playlist.PLS_Playlist')
1086 PlaylistFile
.__init
__( self
, *args
)
1087 self
.current_item
= PlaylistItem()
1088 self
.in_playlist_section
= False
1089 self
.current_item_number
= None
1091 def __add_current_item(self
):
1092 self
._add
_playlist
_item
(self
.current_item
)
1094 def parse_line_hook(self
, line
):
1095 sect_regex
= '\[([^\]]+)\]'
1096 item_regex
= '[^\d]+([\d]+)=(.*)'
1098 if re
.search(item_regex
, line
) is not None:
1099 current
= re
.search(item_regex
, line
).group(1)
1100 if self
.current_item_number
is None:
1101 self
.current_item_number
= current
1102 elif self
.current_item_number
!= current
:
1103 self
.__add
_current
_item
()
1105 self
.current_item
= PlaylistItem()
1106 self
.current_item_number
= current
1108 if re
.search(sect_regex
, line
) is not None:
1109 section
= re
.match(sect_regex
, line
).group(1).lower()
1110 self
.in_playlist_section
= section
== 'playlist'
1111 elif not self
.in_playlist_section
:
1112 pass # don't do anything if we're not in [playlist]
1113 elif line
.lower().startswith('file'):
1114 self
.current_item
.reported_filepath
= re
.search(
1115 item_regex
, line
).group(2)
1116 elif line
.lower().startswith('title'):
1117 self
.current_item
.title
= re
.search(item_regex
, line
).group(2)
1118 elif line
.lower().startswith('length'):
1119 try: length
= int(re
.search(item_regex
, line
).group(2))
1121 self
.current_item
.length
= length
1123 def parse_eof_hook(self
):
1124 self
.__add
_current
_item
()
1126 def export_hook(self
, playlist_items
):
1127 self
._file
.write('[playlist]\n')
1128 self
._file
.write('NumberOfEntries=%d\n\n' % len(playlist_items
))
1130 for i
,item
in enumerate(playlist_items
):
1131 title
= '' if item
.title
is None else item
.title
1132 length
= -1 if item
.length
is None else item
.length
1133 self
._file
.write('File%d=%s\n' % (i
+1, item
.filepath
))
1134 self
._file
.write('Title%d=%s\n' % (i
+1, title
))
1135 self
._file
.write('Length%d=%s\n\n' % (i
+1, length
))
1137 self
._file
.write('Version=2\n')