Index: src/panucci/playlist.py =================================================================== --- src/panucci/playlist.py (revision 0) +++ src/panucci/playlist.py (revision 0) @@ -0,0 +1,349 @@ +#!/usr/bin/env python + +import time +import os.path +import hashlib +import mutagen + +from dbsqlite import db + +def log(x): print x +_ = lambda x: x + +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('_', ' ') + +class Playlist(object): + def __init__(self): + self.filename = None + self.playlist_name = _('Individual Tracks') + self.single_file = True + + # The object's unique identifier + self.hash = None + self._current_file = 0 + self.__current_fileobj = None + self.__filelist = [] + + 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, 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() + + def save( self ): + """ Save current Playlist to the db """ + db.save_playlist( self ) + db.commit() + + ###################################### + # 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 """ + + extension = detect_filetype(File) + if extension == 'm3u': + self.m3u_importer(File) + elif extension == 'pls': + pass + else: + self.single_file_import(File) + + def m3u_importer( self, filename ): + """ Import an m3u playlist + TODO: make this actually support proper m3u playlists... """ + self.filename = filename + self.hash = self.md5(self.filename) + + f = open( filename, 'r' ) + files = f.read() + f.close() + + self.clear_filelist() + loaded_from_db = False + if db.playlist_exists(self): + log('Found playlist instance in db, loading...') + loaded_from_db = self.load_from_db( self.hash ) + + if not loaded_from_db: + self.playlist_name = pretty_filename( self.filename ) + self.single_file = False + + 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 """ + # we don't have to worry about adding a hash because there + # isn't really a point to saving a one-file playlist + self.__init__() + self.append( filename ) + + def load_from_db(self, hash_value): + """ Loads the current playlist based a playlist hash_value """ + playlist = db.load_playlist( hash_value ) + if playlist is not None: + for key,value in playlist.iteritems(): + if hasattr( self, key ): + setattr( self, key, value ) + else: + log('Playlist object doesn\'t have %s attribute' % key) + return True + else: + return False + + ################################## + # Plalist controls + ################################## + + def play(self): + """ This gets called by the player to get + the last time the file was paused """ + return self.current_fileobj.play() + + def pause(self, position): + """ Called whenever the player is paused """ + current_file = self.current_fileobj + current_file.pause(position) + + def stop(self): + """ Caused when we reach the end of a file """ + 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 FileObject(object): + coverart_names = ['cover', 'cover.jpg'] + + def __init__(self, playlist, filepath): + self.last_listened_time = 0 # time is secs since the file was played + self.play_count = 0 # number of times this file has been played + self.pause_time = 0 # if the file was paused, that time in ns + self.filepath = filepath # the full path to the file + self.playlist = playlist # the Playlist object that this belongs to + self._uid = None # a unique object identifier + + # cached file metadata + self.title = None + self.artist = None + self.album = None + self.length = None + self.coverart = None + + if self.load_from_db(): + log('Loaded file from db (%s)' % self.filepath) + else: + self.extract_metadata() + self.save() + + def extract_metadata(self): + filetype = 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 = 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 """ + + metadata = { + 'title': self.title, + 'artist': self.artist, + 'album': self.album, + 'image': self.coverart, + } + + return metadata + + @property + def filetype(self): + return detect_filetype(self.filepath) + + @property + def uid(self): + """ A unique FileObject identifier """ + if self._uid is None: + self._uid = self.playlist.md5(self.filepath) + + return self._uid + + def play(self): + self.last_listened_time = time.time() + return max(0, self.pause_time) + + def pause(self, position): + """ position = nanoseconds into the file """ + self.pause_time = position + self.save() + + def load_from_db( self ): + """ load a FileObject from the db """ + if not db.file_exists(self): + return False + + f = db.load_file(self.uid) + for key,value in f.iteritems(): + if hasattr( self, key ): + setattr( self, key, value ) + else: + log('Can\'t set unknown value: %s' % key) + + return True + + def save( self, dont_commit=False ): + """ Save the FileObject to the db """ + db.save_file( self ) + if not dont_commit: + db.commit() + Index: src/panucci/panucci.py =================================================================== --- src/panucci/panucci.py (revision 49) +++ src/panucci/panucci.py (working copy) @@ -18,12 +18,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 @@ -41,6 +35,9 @@ if running_on_tablet: log('Using GTK widgets, install "python2.5-hildon" for this to work properly.') +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'] @@ -51,8 +48,6 @@ 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] @@ -99,53 +94,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') - self.positions = pickle.load(f) - f.close() - except: - # let's start out with a new dict - 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, url, bookmarks): - if not url in self.positions: - self.positions[url] = {} - - self.positions[url]['bookmarks'] = bookmarks - - def get_bookmarks(self, url): - if url in self.positions and 'bookmarks' in self.positions[url]: - return self.positions[url]['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 @@ -201,15 +149,15 @@ self.hbox.pack_start(self.close_button) self.vbox.pack_start(self.hbox, False, True) self.add(self.vbox) - for label, pos in pm.get_bookmarks(self.main.filename): - self.add_bookmark(label=label, pos=pos) +# for label, pos in pm.get_bookmarks(self.main.filename): +# self.add_bookmark(label=label, pos=pos) self.show_all() def close(self, w): bookmarks = [] for row in self.model: bookmarks.append((row[0], row[2])) - pm.set_bookmarks(self.main.filename, bookmarks) +# pm.set_bookmarks(self.main.filename, bookmarks) self.destroy() def label_edited(self, cellrenderer, path, new_text): @@ -234,69 +182,21 @@ pos = model.get_value(iter, 2) self.main.do_seek(from_beginning=pos) -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 @@ -315,8 +215,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 @@ -529,16 +429,12 @@ 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) + pos = self.playlist.get_current_position() text = self.convert_ns(pos) return (text, pos) @@ -650,21 +546,15 @@ 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) @@ -674,13 +564,11 @@ if self.playing: self.start_stop(widget=None) + self.playlist.save() 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) @@ -691,10 +579,18 @@ 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) - 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') @@ -709,7 +605,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.' ) @@ -720,7 +616,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() @@ -756,7 +652,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 @@ -764,11 +660,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') @@ -782,7 +680,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 @@ -838,7 +736,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() @@ -849,63 +750,41 @@ if ( message.src == self.player and message.structure['new-state'] == gst.STATE_PLAYING ): + #if not self.want_to_seek: 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) @@ -966,7 +845,7 @@ GTK_Main(bus_name, filename) gtk.main() # save position manager data - pm.save() + #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,61 @@ +#!/usr/bin/env python + +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,247 @@ +#!/usr/bin/env python +# _Heavily_ "inspired" by gPodder's dbsqlite + +def log(x): print x + +try: + from sqlite3 import dbapi2 as sqlite +except ImportError: + log( 'Error importing sqlite, FAIL!') + +import os.path +from simplegconf import gconf + +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 playlists ( + id TEXT PRIMARY KEY, + current_file INTEGER, + playlist_name TEXT, + single_file INTEGER + ) """ ) + + cursor.execute( + """ CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY, + last_listened_time INTEGER, + play_count INTEGER, + pause_time INTEGER, + filepath TEXT, + track_title TEXT, + track_artist TEXT, + track_album TEXT, + track_length INTEGER, + track_coverart BLOB + ) """ ) + + cursor.close() + + def playlist_exists(self, playlistobj): + get = [ 'SELECT * FROM playlists WHERE id = ?', playlistobj.hash ] + return self.__get__(*get) is not None + + def save_playlist(self, playlistobj): + """ Save a valid playlist object to the db """ + + if playlistobj.hash is None: + log('Not saving playlist without a hash.') + return + + p = playlistobj + playlist_exists = self.playlist_exists(p) + log('Saving %s (%s)' % (p.filename, p.hash)) + cursor = self.cursor() + + if playlist_exists: + cursor.execute( + """ UPDATE playlists SET + current_file = ?, + playlist_name = ?, + single_file = ? + WHERE id = ? + """, ( p.get_current_file_number(), p.hash, p.playlist_name, p.single_file )) + else: + cursor.execute( + """ INSERT INTO playlists ( + id, + current_file, + playlist_name, + single_file + ) VALUES (?, ?, ?, ?) + """, ( p.hash, p.get_current_file_number(), p.playlist_name, p.single_file )) + + cursor.close() + + def load_playlist(self, hash_value): + """ Returns a dict of playlist object attributes from + the db or None if the playlist isn't found """ + + log('Loading playlist: %s' % hash_value) + + row = self.__get__( + """ SELECT + current_file, + playlist_name, + single_file + FROM playlists WHERE id=? + """, hash_value ) + + if row is None: + return + + playlist = { + '_current_file': row[0], + 'playlist_name': row[1], + 'single_file': bool(row[2]), + 'hash': hash_value, + } + + return playlist + + def file_exists(self, fileobj): + get = [ 'SELECT id FROM files WHERE id = ?', fileobj.uid ] + return self.__get__(*get) is not None + + def save_file(self, fileobj): + """ Save a valid file to the db """ + + if fileobj.uid is None: + log('Not saving file without a uid.') + return + + f = fileobj + log('Saving %s' % f.filepath) + file_exists = self.file_exists(f) + cursor = self.cursor() + + if f.coverart is None: + coverart = None + else: + coverart = sqlite.Binary(f.coverart) + + if file_exists: + cursor.execute( + """ UPDATE files SET + last_listened_time = ?, + play_count = ?, + pause_time = ?, + filepath = ?, + track_title = ?, + track_artist = ?, + track_album = ?, + track_length = ?, + track_coverart = ? + WHERE id = ? + """, ( f.last_listened_time, f.play_count, f.pause_time, + f.filepath, f.title, f.artist, f.album, f.length, + coverart, f.uid )) + else: + cursor.execute( + """ INSERT INTO files ( + id, + last_listened_time, + play_count, + pause_time, + filepath, + track_title, + track_artist, + track_album, + track_length, + track_coverart + ) VALUES (?,?,?,?,?,?,?,?,?,?) + """, ( f.uid, f.last_listened_time, f.play_count, f.pause_time, + f.filepath, f.title, f.artist, f.album, f.length, + coverart )) + + cursor.close() + + def load_file(self, uid, factory=None): + log('Getting track %s' % uid) + + row = self.__get__( + """ SELECT + last_listened_time, + play_count, + pause_time, + filepath, + track_title, + track_artist, + track_album, + track_length, + track_coverart + FROM files WHERE id=? + """, uid ) + + if row is None: + return None + + File = { + 'last_listened_time': row[0], + 'play_count': row[1], + 'pause_time': row[2], + 'filepath': row[3], + 'title': row[4], + 'artist': row[5], + 'album': row[6], + 'length': row[7], + 'coverart': row[8], + } + + if factory is not None: + File = factory(File) + + return File + + def __get__(self, sql, params=None): + """ + Returns the first cell of a query result, useful for COUNT()s. + """ + 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() + + if row is None: + return None + else: + return row + +db = Storage(os.path.expanduser(gconf.sget('db_location', str, '~/.panucci.sqlite'))) + Index: bin/panucci =================================================================== --- bin/panucci (revision 49) +++ bin/panucci (working copy) @@ -28,8 +28,7 @@ debug = '--debug' in sys.argv panucci.run(filename=filename, debug=debug) 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 49) +++ 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. + + +