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/