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.
+
+
+