Index: src/panucci/util.py =================================================================== --- src/panucci/util.py (revision 0) +++ src/panucci/util.py (revision 0) @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +import traceback + +def convert_ns(time_int): + time_int = time_int / 1000000000 + time_str = "" + if time_int >= 3600: + _hours = time_int / 3600 + time_int = time_int - (_hours * 3600) + time_str = str(_hours) + ":" + if time_int >= 600: + _mins = time_int / 60 + time_int = time_int - (_mins * 60) + time_str = time_str + str(_mins) + ":" + elif time_int >= 60: + _mins = time_int / 60 + time_int = time_int - (_mins * 60) + time_str = time_str + "0" + str(_mins) + ":" + else: + time_str = time_str + "00:" + if time_int > 9: + time_str = time_str + str(time_int) + else: + time_str = time_str + "0" + str(time_int) + + return time_str + +def detect_filetype( filepath ): + if len(filepath.split('.')) > 1: + filename,extension = [ i.lower() for i in filepath.rsplit('.',1) ] + else: + extension = None + return extension + +def pretty_filename( filename ): + filename, extension = os.path.basename(filename).rsplit('.',1) + return filename.replace('_', ' ') + + +logging_enabled = False + +def log( msg, *args, **kwargs ): + global logging_enabled + if not logging_enabled: + return + + if args: + msg = msg % args + + print msg + + if kwargs.get('exception') is not None: + traceback.print_exc() + Index: src/panucci/playlist.py =================================================================== --- src/panucci/playlist.py (revision 0) +++ src/panucci/playlist.py (revision 0) @@ -0,0 +1,413 @@ +#!/usr/bin/env python +# +# Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +import gobject, gtk +import time +import os.path +import hashlib +import mutagen + +import util +from util import log +from dbsqlite import db + +_ = lambda x: x + +class Playlist(object): + def __init__(self): + self.filename = None + + self._current_file = 0 + self.__current_fileobj = None + self.__filelist = [] + self.__bookmarks = {} + self.__bookmarks_model = None + + def insert( self, position, filepath ): + if os.path.exists(filepath): + self.__filelist.insert( position, filepath ) + else: + log('File cannot be found: %s' % filepath) + + def append( self, filepath ): + """ Append a file to the queue """ + self.insert( self.queue_length(), filepath ) + + def clear_filelist(self): + """ clears all the files in the filelist """ + self.__filelist = [] + self._current_file = 0 + + @property + def current_filepath(self): + """ Get the current file """ + if self.__filelist: + return self.__filelist[self._current_file] + + @property + def current_fileobj(self): + """ Get the FileObject of the current file """ + if self.__current_fileobj is None or ( + self.__current_fileobj.filepath != self.current_filepath ): + log('Cached FileObject is out of date, loading a new one') + self.__current_fileobj = FileObject( self.current_filepath ) + + return self.__current_fileobj + + def get_current_file_number(self): + return self._current_file + + def queue_length(self): + return len(self.__filelist) + + def is_empty(self): + """ Returns False if we're at the end of the + playlist or if no files have been loaded """ + return self.get_current_file_number() >= self.queue_length() + + def md5( self, string ): + """ Return the md5sum of 'string' """ + return hashlib.md5(string).hexdigest() + + ###################################### + # Bookmark-related functions + ###################################### + + def append_bookmark(self, bookmark): + self.__bookmarks[bookmark.id] = bookmark + + def load_from_bookmark( self, bkmk ): + if self.__bookmarks.has_key(bkmk): + bookmark = self.__bookmarks[bkmk] + elif isinstance( bkmk, Bookmark ): + bookmark = bkmk + else: + log('No such bookmark, type: %s' % type(bkmk)) + return + + self._current_file = bookmark.playlist_index + if self.current_fileobj is not None: + self.current_fileobj.seek_to = bookmark.seek_position + + def save_bookmark( self, bookmark_name, position ): + self.__save_bookmark( bookmark_name, position ) + + def __save_bookmark( self, bookmark_name, position, resume_pos=False ): + b = Bookmark() + b.playlist_filepath = self.filename + b.bookmark_name = bookmark_name + b.playlist_index = self.get_current_file_number() + b.seek_position = position + b.timestamp = time.time() + b.is_resume_position = resume_pos + b.id = db.save_bookmark(b) + self.append_bookmark(b) + + def generate_bookmark_model(self, include_resume_marks=False): + self.__bookmarks_model = gtk.ListStore( + gobject.TYPE_INT64, gobject.TYPE_STRING, gobject.TYPE_STRING ) + + self.__create_per_file_bookmarks() + + bookmarks = self.__bookmarks.values() + bookmarks.sort() + + for bookmark in bookmarks: + if not bookmark.is_resume_position or ( + bookmark.is_resume_position and include_resume_marks ): + + self.__bookmarks_model.append([ + bookmark.id, bookmark.bookmark_name, + '[%02d - %s] %s' % ( bookmark.playlist_index+1, + util.convert_ns(bookmark.seek_position), + os.path.basename(self.__filelist[bookmark.playlist_index]) + ) ]) + + def get_bookmark_model(self, include_resume_marks=False): + if self.__bookmarks_model is None: + log('Generating new bookmarks model') + self.generate_bookmark_model(include_resume_marks) + else: + log('Using cached bookmarks model') + + return self.__bookmarks_model + + def __create_per_file_bookmarks(self): + for n, filepath in enumerate(self.__filelist): + b = Bookmark() + b.id = -1*(n+1) + b.bookmark_name = '%s %d' % (_('File'), n+1) + b.playlist_index = n + self.append_bookmark(b) + + ###################################### + # File-related convenience functions + ###################################### + + def get_current_position(self): + """ Returns the saved position for the current + file or 0 if no file is available""" + if self.current_fileobj is not None: + return self.current_fileobj.pause_time + else: + return 0 + + def get_current_filetype(self): + """ Returns the filetype of the current + file or None if no file is available """ + + if self.current_fileobj is not None: + return self.current_fileobj.filetype + + def get_file_metadata(self): + """ Return the metadata associated with the current FileObject """ + if self.current_fileobj is not None: + return self.current_fileobj.get_metadata() + else: + return {} + + def get_current_filepath(self): + if self.current_fileobj is not None: + return self.current_fileobj.filepath + + ################################## + # File importing functions + ################################## + + def load(self, File): + """ Detects File's filetype then loads it using + the appropriate loader function """ + + self.filename = File + + extension = util.detect_filetype(File) + if extension == 'm3u': + self.m3u_importer(File) + elif extension == 'pls': + pass + else: + self.single_file_import(File) + + bookmarks = db.load_bookmarks( self.filename, + factory=Bookmark().load_from_dict ) + + self.__bookmarks = dict([ [b.id, b] for b in bookmarks ]) + + for id, bkmk in self.__bookmarks.iteritems(): + if bkmk.is_resume_position: + log('Found resume position, loading bookmark...') + self.load_from_bookmark( bkmk ) + break + + def m3u_importer( self, filename ): + """ Import an m3u playlist + TODO: make this actually support proper m3u playlists... """ + + f = open( filename, 'r' ) + files = f.read() + f.close() + + self.clear_filelist() + + files = files.splitlines() + for f in files: + if f.strip(): self.append(f.strip()) + + def single_file_import( self, filename ): + """ Add a single track to the playlist """ + self.append( filename ) + + ################################## + # Plalist controls + ################################## + + def play(self): + """ This gets called by the player to get + the last time the file was paused """ + db.remove_resume_bookmark( self.filename ) + return self.current_fileobj.play() + + def pause(self, position): + """ Called whenever the player is paused """ + self.current_fileobj.pause(position) + self.__save_bookmark( _('Auto Bookmark'), position, True ) + + def stop(self): + """ Caused when we reach the end of a file """ + db.remove_resume_bookmark( self.filename ) + self.current_fileobj.pause(0) + + def skip(self, skip_by=None, skip_to=None, dont_loop=False): + """ Skip to another track in the playlist. + Use either skip_by or skip_to, skip_by has precedence. + skip_to: skip to a known playlist position + skip_by: skip by n number of episodes (positive or negative) + dont_loop: applies only to skip_by, if we're skipping past + the last track loop back to the begining. + """ + if not self.__filelist: + return False + + if skip_by is not None: + if dont_loop: + skip = self._current_file + skip_by + else: + skip = ( self._current_file + skip_by ) % self.queue_length() + elif skip_to is not None: + skip = skip_to + else: + log('No skip method provided...') + + if not ( 0 <= skip < self.queue_length() ): + log('Can\'t skip to non-existant file. (requested=%d, total=%d)' % ( + skip, self.queue_length()) ) + return False + + self._current_file = skip + log('Skipping to file %d (%s)' % (skip, self.current_fileobj.filepath) ) + return True + + def next(self): + """ Move the playlist to the next track. + False indicates end of playlist. """ + return self.skip( skip_by=1, dont_loop=True ) + + def prev(self): + """ Same as next() except moves to the previous track. """ + return self.skip( skip_by=-1, dont_loop=True ) + + +class Bookmark(object): + def __init__(self): + self.id = 0 + self.playlist_filepath = '' + self.bookmark_name = '' + self.playlist_index = 0 + self.seek_position = 0 + self.timestamp = 0 + self.is_resume_position = False + + @staticmethod + def load_from_dict(bkmk_dict): + bkmkobj = Bookmark() + + for key,value in bkmk_dict.iteritems(): + if hasattr( bkmkobj, key ): + setattr( bkmkobj, key, value ) + else: + log('Attr: %s doesn\'t exist...' % key) + + return bkmkobj + + def __cmp__(self, b): + if self.playlist_index == b.playlist_index: + if self.seek_position == b.seek_position: + return 0 + else: + return -1 if self.seek_position < b.seek_position else 1 + else: + return -1 if self.playlist_index < b.playlist_index else 1 + + +class FileObject(object): + coverart_names = ['cover', 'cover.jpg'] + + def __init__(self, filepath): + self.filepath = filepath # the full path to the file + self.seek_to = 0 + + self.title = '' + self.artist = '' + self.album = '' + self.length = 0 + self.coverart = None + + self.__metadata_extracted = False + + def extract_metadata(self): + filetype = util.detect_filetype(self.filepath) + File = mutagen.File(self.filepath) + self.length = File.info.length + + if filetype == 'mp3': + for tag,value in File.iteritems(): + if tag == 'TIT2': self.title = str(value) + elif tag == 'TALB': self.album = str(value) + elif tag == 'TPE1': self.artist = str(value) + elif tag.startswith('APIC'): self.coverart = value.data + + elif filetype in ['ogg', 'flac']: + for tag,value in File.iteritems(): + if tag == 'title': self.title = str(value) + elif tag == 'album': self.album = str(value) + elif tag == 'artist': self.artist = str(value) + + else: + self.title = util.pretty_filename(self.filepath) + + if self.coverart is None: + self.coverart = self.__find_coverart() + + def __find_coverart(self): + """ Find coverart in the same directory as the filepath """ + directory = os.path.dirname(self.filepath) + for cover in self.coverart_names: + c = os.path.join( directory, cover ) + if os.path.isfile(c): + try: + f.open(c,'r') + binary_coverart = f.read() + f.close() + return binary_coverart + except: + pass + return None + + def get_metadata(self): + """ Returns a dict of metadata """ + + if not self.__metadata_extracted: + log('Extracting metadata for %s' % self.filepath) + self.extract_metadata() + self.__metadata_extracted = True + + metadata = { + 'title': self.title, + 'artist': self.artist, + 'album': self.album, + 'image': self.coverart, + } + + return metadata + + @property + def filetype(self): + return util.detect_filetype(self.filepath) + + def play(self): + return max(0, self.seek_to) + + def pause(self, position): + self.seek_to = position + Index: src/panucci/panucci.py =================================================================== --- src/panucci/panucci.py (revision 53) +++ src/panucci/panucci.py (working copy) @@ -40,12 +40,6 @@ pygst.require('0.10') import gst -try: - import gconf -except: - # on the tablet, it's probably in "gnome" - from gnome import gconf - import dbus import dbus.service import dbus.mainloop @@ -63,6 +57,11 @@ if running_on_tablet: log('Using GTK widgets, install "python2.5-hildon" for this to work properly.') +import util +from util import log +from simplegconf import gconf +from playlist import Playlist + about_name = 'Panucci' about_text = _('Resuming audiobook and podcast player') about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor'] @@ -73,20 +72,10 @@ short_seek = 10 long_seek = 60 -gconf_dir = '/apps/panucci' - coverart_names = [ 'cover', 'cover.jpg', 'cover.png' ] coverart_size = [240, 240] if running_on_tablet else [130, 130] -debug_override = False -def log( msg ): - """ A very simple log function (no log output is produced when - using the python optimization (-O, -OO) options) """ - global debug_override - if __debug__ or debug_override: - print msg - def open_link(d, url, data): webbrowser.open_new(url) @@ -121,67 +110,6 @@ widget.add(image) image.show() -class PositionManager(object): - def __init__(self, filename=None): - if filename is None: - filename = os.path.expanduser('~/.rmp-bookmarks') - self.filename = filename - - try: - # load the playback positions - f = open(self.filename, 'rb') - bookmarks_store = pickle.load(f) - f.close() - ### Migrating from old pickle format to new format - # is the store the new format? - if 'bookmarks' in bookmarks_store: - self.positions = bookmarks_store - else: - #try converting to the new format. - self.positions = {} - self.positions['bookmarks'] = [] - for k, v in bookmarks_store.iteritems(): - if k is not None: - url = k - self.positions[url] = { 'position' : - bookmarks_store[url]['position'] } - for label, position in bookmarks_store[url]['bookmarks']: - self.positions['bookmarks'].append( - (label, position, url)) - ### End of migration code - except: - # let's start out with a new dict - self.positions = {} - print self.positions - - def set_position(self, url, position): - if not url in self.positions: - self.positions[url] = {} - self.positions[url]['position'] = position - - def get_position(self, url): - if url in self.positions and 'position' in self.positions[url]: - return self.positions[url]['position'] - else: - return 0 - - def set_bookmarks(self, bookmarks): - self.positions['bookmarks'] = bookmarks - - def get_bookmarks(self): - if 'bookmarks' in self.positions: - return self.positions['bookmarks'] - else: - return [] - - def save(self): - # save the playback position dict - f = open(self.filename, 'wb') - pickle.dump(self.positions, f) - f.close() - -pm = PositionManager() - class BookmarksWindow(gtk.Window): def __init__(self, main): self.main = main @@ -194,32 +122,23 @@ self.vbox.set_spacing(5) self.treeview = gtk.TreeView() self.treeview.set_headers_visible(True) - self.model = gtk.ListStore(gobject.TYPE_STRING, - gobject.TYPE_STRING, - gobject.TYPE_UINT64, - gobject.TYPE_STRING) + self.model = main.playlist.get_bookmark_model() self.treeview.set_model(self.model) - ncol = gtk.TreeViewColumn('Name') + ncol = gtk.TreeViewColumn(_('Name')) ncell = gtk.CellRendererText() ncell.set_property('editable', True) ncell.connect('edited', self.label_edited) ncol.pack_start(ncell) - ncol.add_attribute(ncell, 'text', 0) + ncol.add_attribute(ncell, 'text', 1) - tcol = gtk.TreeViewColumn('Time') + tcol = gtk.TreeViewColumn(_('Position')) tcell = gtk.CellRendererText() tcol.pack_start(tcell) - tcol.add_attribute(tcell, 'text', 1) + tcol.add_attribute(tcell, 'text', 2) - fcol = gtk.TreeViewColumn('File') - fcell = gtk.CellRendererText() - fcol.pack_start(fcell) - fcol.add_attribute(fcell, 'text', 3) - self.treeview.append_column(ncol) self.treeview.append_column(tcol) - self.treeview.append_column(fcol) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) @@ -245,29 +164,23 @@ self.hbox.pack_start(self.close_button) self.vbox.pack_start(self.hbox, False, True) self.add(self.vbox) - - bookmarks = pm.get_bookmarks() - for label, pos, filename in bookmarks: - self.add_bookmark(label=label, pos=pos, url=filename) self.show_all() def close(self, w): bookmarks = [] for row in self.model: - bookmarks.append((row[0], row[2], row[3])) - pm.set_bookmarks(bookmarks) + bookmarks.append((row[0], row[2])) +# pm.set_bookmarks(self.main.filename, bookmarks) self.destroy() def label_edited(self, cellrenderer, path, new_text): self.model.set_value(self.model.get_iter(path), 0, new_text) - def add_bookmark(self, w=None, label=None, pos=None, url=None): + def add_bookmark(self, w=None, label=None, pos=None): (text, position) = self.main.get_position(pos) if label is None: label = text - if url is None: - url = self.main.filename - self.model.append([label, text, position, url]) + self.model.append([label, text, position]) def remove_bookmark(self, w): selection = self.treeview.get_selection() @@ -279,78 +192,26 @@ selection = self.treeview.get_selection() (model, iter) = selection.get_selected() if iter is not None: - pos = model.get_value(iter, 2) - url = model.get_value(iter, 3) - self.main.play_file(url) - ### not sure what to do about this sleep. - import time - time.sleep(.05) - ### if I remove it, the player does not seek. - self.main.do_seek(from_beginning=pos) + bookmark_id = model.get_value(iter, 0) + self.main.stop_playing() + self.main.playlist.load_from_bookmark( bookmark_id ) + self.main.start_playback() -class SimpleGConfClient(gconf.Client): - """ A simplified wrapper around gconf.Client - GConf docs: http://library.gnome.org/devel/gconf/stable/ - """ - - __type_mapping = { int: 'int', long: 'float', float: 'float', - str: 'string', bool: 'bool', list: 'list', } - - def __init__(self, directory): - """ directory is the base directory that we're working in """ - self.__directory = directory - gconf.Client.__init__(self) - - self.add_dir( self.__directory, gconf.CLIENT_PRELOAD_NONE ) - - def __get_manipulator_method( self, data_type, operation ): - """ data_type must be a vaild "type" - operation is either 'set' or 'get' """ - - if self.__type_mapping.has_key( data_type ): - method = operation + '_' + self.__type_mapping[data_type] - return getattr( self, method ) - else: - log('Data type "%s" is not supported.' % data_type) - return lambda x,y=None: None - - def sset( self, key, value ): - """ A simple set function, no type is required, it is determined - automatically. 'key' is relative to self.__directory """ - - return self.__get_manipulator_method(type(value), 'set')( - os.path.join(self.__directory, key), value ) - - def sget( self, key, data_type, default=None ): - """ A simple get function, type is required, default value is - optional, 'key' is relative to self.__directory """ - - if self.get( os.path.join(self.__directory, key) ) is None: - return default - else: - return self.__get_manipulator_method(data_type, 'get')( - os.path.join(self.__directory, key) ) - - def snotify( self, callback ): - """ Set a callback to watch self.__directory """ - return self.notify_add( self.__directory, callback ) - class GTK_Main(dbus.service.Object): def __init__(self, bus_name, filename=None): dbus.service.Object.__init__(self, object_path="/player", bus_name=bus_name) - self.gconf = SimpleGConfClient( gconf_dir ) + self.gconf = gconf self.gconf.snotify(self.gconf_key_changed) - self.filename = filename + self.playlist = Playlist() self.progress_timer_id = None self.volume_timer_id = None self.make_main_window() - self.has_coverart = False - self.has_id3_coverart = False self.playing = False + self.has_coverart = False if running_on_tablet: # Enable play/pause with headset button @@ -369,8 +230,8 @@ self.set_volume(self.gconf.sget('volume', float, 0.3)) self.time_format = gst.Format(gst.FORMAT_TIME) - if self.filename is not None: - self.play_file(self.filename) + if filename is not None: + self.play_file(filename) def make_main_window(self): import pango @@ -583,17 +444,13 @@ dialog.run() dialog.destroy() - def save_position(self): - (pos, dur) = self.player_get_position() - pm.set_position(self.filename, pos) - def get_position(self, pos=None): if pos is None: if self.playing: (pos, dur) = self.player_get_position() else: - pos = pm.get_position(self.filename) - text = self.convert_ns(pos) + pos = self.playlist.get_current_position() + text = util.convert_ns(pos) return (text, pos) def destroy(self, widget): @@ -704,21 +561,13 @@ def play_file(self, filename): self.stop_playing() - self.filename = os.path.abspath(filename) - pretty_filename = os.path.basename(self.filename).rsplit('.',1)[0].replace('_', ' ') - self.setup_player(self.filename) + self.playlist.load( os.path.abspath(filename) ) + if self.playlist.is_empty(): + return False - self.has_coverart = False - self.want_to_seek = True self.start_playback() - self.start_progress_timer() - # This is just in case the file contains no tags, - # at least we can display the filename - self.set_metadata({'title': pretty_filename}) - def open_file_callback(self, widget=None): - old_filename = self.filename filename = self.get_file_from_filechooser() if filename is not None: self.play_file(filename) @@ -731,24 +580,31 @@ if self.player is not None: self.player.set_state(gst.STATE_NULL) self.stop_progress_timer() self.title_label.set_size_request(-1,-1) - self.filename = None self.playing = False - self.has_coverart = False - self.has_id3_coverart = False self.reset_progress() self.set_controls_sensitivity(False) image(self.button, gtk.STOCK_OPEN, True) def start_playback(self): + self.want_to_seek = True self.set_controls_sensitivity(True) for widget in [ self.title_label, self.artist_label, self.album_label ]: widget.set_text('') widget.hide() self.cover_art.hide() + self.set_metadata(self.playlist.get_file_metadata()) + self.setup_player() self.start_stop(widget=None) + self.start_progress_timer() - def setup_player(self, filename): - if filename.lower().endswith('.ogg') and running_on_tablet: + def setup_player(self): + filetype = self.playlist.get_current_filetype() + filepath = self.playlist.get_current_filepath() + + if None in [ filetype, filepath ]: + return False + + if filetype.startswith('ogg') and running_on_tablet: log( 'Using OGG workaround, I hope this works...' ) self.player = gst.Pipeline('player') @@ -763,7 +619,7 @@ self.get_volume_level = lambda : self.__get_volume_level(self.__volume_control) self.set_volume_level = lambda x: self.__set_volume_level(x, self.__volume_control) - source.set_property( 'location', 'file://' + filename ) + source.set_property( 'location', 'file://' + filepath ) else: log( 'Using plain-old playbin.' ) @@ -774,7 +630,7 @@ self.get_volume_level = lambda : self.__get_volume_level(self.player, div) self.set_volume_level = lambda x: self.__set_volume_level(x, self.player, div) - self.player.set_property( 'uri', 'file://' + self.filename ) + self.player.set_property( 'uri', 'file://' + filepath ) bus = self.player.get_bus() bus.add_signal_watch() @@ -797,8 +653,8 @@ def set_progress_callback(self, time_elapsed, total_time): """ times must be in nanoseconds """ - time_string = "%s / %s" % ( self.convert_ns(time_elapsed), - self.convert_ns(total_time) ) + time_string = "%s / %s" % ( util.convert_ns(time_elapsed), + util.convert_ns(total_time) ) self.progress.set_text( time_string ) fraction = float(time_elapsed) / float(total_time) if total_time else 0 self.progress.set_fraction( fraction ) @@ -810,7 +666,7 @@ self.do_seek(percent=new_fraction) def start_stop(self, widget=None): - if self.filename is None or not os.path.exists(self.filename): + if self.playlist.is_empty(): self.open_file_callback() return @@ -818,11 +674,13 @@ if self.playing: self.start_progress_timer() + self.playlist.play() self.player.set_state(gst.STATE_PLAYING) image(self.button, 'media-playback-pause.png') else: self.stop_progress_timer() # This should save some power - self.save_position() + pos, dur = self.player_get_position() + self.playlist.pause(pos) self.player.set_state(gst.STATE_PAUSED) image(self.button, 'media-playback-start.png') @@ -836,7 +694,7 @@ position, duration = self.player_get_position() # if position and duration are 0 then player_get_position caught an - # exception. Therefore self.player isn't ready to be seeing. + # exception. Therefore self.player isn't ready to be seeking. if not ( position or duration ) or self.player is None: return False @@ -892,7 +750,10 @@ if t == gst.MESSAGE_EOS: self.stop_playing() - pm.set_position(self.filename, 0) + if self.playlist.next(): + self.start_playback() + else: + self.playlist.stop() elif t == gst.MESSAGE_ERROR: err, debug = message.parse_error() @@ -905,61 +766,38 @@ if self.want_to_seek: # This only gets called when the file is first loaded - self.do_seek(from_beginning=pm.get_position(self.filename)) + pause_time = self.playlist.play() + self.do_seek(from_beginning=pause_time) else: self.set_controls_sensitivity(True) - elif t == gst.MESSAGE_TAG: - keys = message.parse_tag().keys() - tags = dict([ (key, message.structure[key]) for key in keys ]) - self.set_metadata( tags ) - def set_coverart( self, pixbuf ): self.cover_art.set_from_pixbuf(pixbuf) self.cover_art.show() self.has_coverart = True - def set_coverart_from_dir( self, directory ): - for cover in coverart_names: - c = os.path.join( directory, cover ) - if os.path.isfile(c): - try: - pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(c, *coverart_size) - self.cover_art.set_from_pixbuf(pixbuf) - self.cover_art.show() - return True - except: - pass - return False - def set_metadata( self, tag_message ): tags = { 'title': self.title_label, 'artist': self.artist_label, 'album': self.album_label } - if tag_message.has_key('image') and not self.has_id3_coverart: + if tag_message.has_key('image') and tag_message['image'] is not None: value = tag_message['image'] - if isinstance( value, list ): - value = value[0] pbl = gtk.gdk.PixbufLoader() try: - pbl.write(value.data) + pbl.write(value) pbl.close() pixbuf = pbl.get_pixbuf().scale_simple( coverart_size[0], coverart_size[1], gtk.gdk.INTERP_BILINEAR ) self.set_coverart(pixbuf) - self.has_id3_coverart = True except: import traceback traceback.print_exc(file=sys.stdout) pbl.close() - if not self.has_coverart and self.filename is not None: - self.has_coverart = self.set_coverart_from_dir(os.path.dirname(self.filename)) - tag_vals = dict([ (i,'') for i in tags.keys()]) for tag,value in tag_message.iteritems(): - if tags.has_key(tag) and value.strip(): + if tags.has_key(tag) and value is not None and value.strip(): tags[tag].set_markup(''+value+'') tag_vals[tag] = value tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5) @@ -986,41 +824,12 @@ def bookmarks_callback(self, w): BookmarksWindow(self) - def convert_ns(self, time_int): - time_int = time_int / 1000000000 - time_str = "" - if time_int >= 3600: - _hours = time_int / 3600 - time_int = time_int - (_hours * 3600) - time_str = str(_hours) + ":" - if time_int >= 600: - _mins = time_int / 60 - time_int = time_int - (_mins * 60) - time_str = time_str + str(_mins) + ":" - elif time_int >= 60: - _mins = time_int / 60 - time_int = time_int - (_mins * 60) - time_str = time_str + "0" + str(_mins) + ":" - else: - time_str = time_str + "00:" - if time_int > 9: - time_str = time_str + str(time_int) - else: - time_str = time_str + "0" + str(time_int) - return time_str - - -def run(filename=None, debug=False): - global debug_override - debug_override = debug - +def run(filename=None): session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop()) bus_name = dbus.service.BusName('org.panucci', bus=session_bus) GTK_Main(bus_name, filename) gtk.main() - # save position manager data - pm.save() if __name__ == '__main__': log( 'WARNING: Use the "panucci" executable to run this program.' ) Index: src/panucci/simplegconf.py =================================================================== --- src/panucci/simplegconf.py (revision 0) +++ src/panucci/simplegconf.py (revision 0) @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +import os.path + +try: + import gconf +except: + # on the tablet, it's probably in "gnome" + from gnome import gconf + +gconf_dir = '/apps/panucci' + +class SimpleGConfClient(gconf.Client): + """ A simplified wrapper around gconf.Client + GConf docs: http://library.gnome.org/devel/gconf/stable/ + """ + + __type_mapping = { int: 'int', long: 'float', float: 'float', + str: 'string', bool: 'bool', list: 'list', } + + def __init__(self, directory): + """ directory is the base directory that we're working in """ + self.__directory = directory + gconf.Client.__init__(self) + + self.add_dir( self.__directory, gconf.CLIENT_PRELOAD_NONE ) + + def __get_manipulator_method( self, data_type, operation ): + """ data_type must be a vaild "type" + operation is either 'set' or 'get' """ + + if self.__type_mapping.has_key( data_type ): + method = operation + '_' + self.__type_mapping[data_type] + return getattr( self, method ) + else: + log('Data type "%s" is not supported.' % data_type) + return lambda x,y=None: None + + def sset( self, key, value ): + """ A simple set function, no type is required, it is determined + automatically. 'key' is relative to self.__directory """ + + return self.__get_manipulator_method(type(value), 'set')( + os.path.join(self.__directory, key), value ) + + def sget( self, key, data_type, default=None ): + """ A simple get function, type is required, default value is + optional, 'key' is relative to self.__directory """ + + if self.get( os.path.join(self.__directory, key) ) is None: + return default + else: + return self.__get_manipulator_method(data_type, 'get')( + os.path.join(self.__directory, key) ) + + def snotify( self, callback ): + """ Set a callback to watch self.__directory """ + return self.notify_add( self.__directory, callback ) + +gconf = SimpleGConfClient( gconf_dir ) + Index: src/panucci/dbsqlite.py =================================================================== --- src/panucci/dbsqlite.py (revision 0) +++ src/panucci/dbsqlite.py (revision 0) @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# +# Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# _Heavily_ inspired by gPodder's dbsqlite +# + +try: + from sqlite3 import dbapi2 as sqlite +except ImportError: + log( 'Error importing sqlite, FAIL!') + +import os.path +from simplegconf import gconf +from util import log + +class Storage(object): + def __init__(self, db_file): + """ db_file is the on-disk location of the database file """ + self.__db_file = db_file + self.__db = None + + @property + def db(self): + if self.__db is None: + self.__db = sqlite.connect(self.__db_file) + log('Connected to %s' % self.__db_file) + self.__check_schema() + return self.__db + + def cursor(self): + return self.db.cursor() + + def commit(self): + try: + log("COMMIT") + self.db.commit() + except ProgrammingError, e: + log('Error commiting changes: %s' % e) + + def __check_schema(self): + """ + Creates all necessary tables and indexes that don't exist. + """ + cursor = self.cursor() + + cursor.execute( + """ CREATE TABLE IF NOT EXISTS bookmarks ( + bookmark_id INTEGER PRIMARY KEY AUTOINCREMENT, + bookmark_name TEXT, + playlist_filepath TEXT, + playlist_index INTEGER, + seek_position INTEGER, + timestamp INTEGER, + is_resume_position INTEGER + ) """ ) + + cursor.close() + + def get_bookmarks(self, playlist_filepath): + cursor = self.cursor() + sql = 'SELECT * FROM bookmarks WHERE playlist_filepath = ?' + cursor.execute( sql, [playlist_filepath,] ) + bookmarks = cursor.fetchall() + cursor.close() + return bookmarks + + def bookmark_exists(self, playlist_filepath): + return self.get_bookmarks( playlist_filepath ) is not None + + def load_bookmarks(self, playlist_filepath, factory=None): + bkmks = self.get_bookmarks( playlist_filepath ) + + if bkmks is None: + return [] + + bkmk_list = [] + for bkmk in bkmks: + BKMK = { + 'playlist_filepath' : playlist_filepath, + 'id' : bkmk[0], + 'bookmark_name' : bkmk[1], + 'playlist_index' : bkmk[3], + 'seek_position' : bkmk[4], + 'timestamp' : bkmk[5], + 'is_resume_position': bool(bkmk[6]), + } + + if factory is not None: + BKMK = factory(BKMK) + + bkmk_list.append(BKMK) + + return bkmk_list + + def save_bookmark(self, bookmark): + if bookmark.id < 0: + log('Not saving bookmark with negative id (%d)' % bookmark.id) + return + + if bookmark.is_resume_position: + self.remove_resume_bookmark( bookmark.playlist_filepath ) + + log('Saving %s (%s)' % ( + bookmark.bookmark_name, bookmark.playlist_filepath )) + + cursor = self.cursor() + cursor.execute( + """ INSERT INTO bookmarks ( + bookmark_name, + playlist_filepath, + playlist_index, + seek_position, + timestamp, + is_resume_position + ) VALUES (?, ?, ?, ?, ?, ?) """, + + ( bookmark.bookmark_name, bookmark.playlist_filepath, + bookmark.playlist_index, bookmark.seek_position, + bookmark.timestamp, bookmark.is_resume_position )) + + r_id = self.__get__( 'SELECT last_insert_rowid()' ) + + cursor.close() + self.commit() + + return r_id[0] + + def remove_bookmark(self, bookmark_id): + log('Deleting bookmark by id: %s' % bookmark_id) + + cursor = self.cursor() + cursor.execute( + 'DELETE FROM bookmarks WHERE bookmark_id = ?', bookmark_id ) + + cursor.close() + self.commit() + + def remove_resume_bookmark(self, playlist_filepath): + log('Deleting resume bookmark for: %s' % playlist_filepath) + cursor = self.cursor() + + cursor.execute( + """ DELETE FROM bookmarks WHERE + playlist_filepath = ? AND + is_resume_position = 1 """, + + ( playlist_filepath, )) + + cursor.close() + self.commit() + + def __get__(self, sql, params=None): + """ Returns the first row of a query result """ + + cursor = self.cursor() + + if params is None: + cursor.execute(sql) + else: + if not isinstance( params, (list, tuple) ): + params = [ params, ] + + cursor.execute(sql, params) + + row = cursor.fetchone() + cursor.close() + + return row + +db = Storage(os.path.expanduser(gconf.sget('db_location', str, '~/.panucci.sqlite'))) + Index: bin/panucci =================================================================== --- bin/panucci (revision 53) +++ bin/panucci (working copy) @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.5 +#!/usr/bin/env python # # Copyright (c) 2008 The Panucci Audiobook and Podcast Player Project # @@ -47,12 +47,12 @@ if remote_object is None: from panucci import panucci + from panucci import util # This could eventually be made fancy with optparse - debug = '--debug' in sys.argv - panucci.run(filename=filename, debug=debug) + util.logging_enabled = '--debug' in sys.argv + panucci.run(filename=filename) else: - if filename is None: - remote_object.show_main_window(dbus_interface='org.panucci.interface') - else: + if filename is not None: remote_object.play_file(filename, dbus_interface='org.panucci.interface') + remote_object.show_main_window(dbus_interface='org.panucci.interface') Index: data/panucci.schemas =================================================================== --- data/panucci.schemas (revision 53) +++ data/panucci.schemas (working copy) @@ -37,5 +37,17 @@ + + /schemas/apps/panucci/db_location + /apps/panucci/db_location + panucci + string + ~/.panucci.sqlite + + Where Panucci's database is stored. + Where Panucci stores it's sqlite database of cached metadata and bookmark data. + + + Index: Makefile =================================================================== --- Makefile (revision 53) +++ Makefile (working copy) @@ -52,7 +52,7 @@ rm -rf dist test: - PYTHONPATH=src/ python bin/panucci + PYTHONPATH=src/ python bin/panucci --debug post-install: gtk-update-icon-cache -f -i $(PREFIX)/share/icons/hicolor/