From: Simeon Veldstra Date: Mon, 6 Nov 2006 01:15:18 +0000 (-0800) Subject: Merge with bugfix-0.5 X-Git-Tag: v0.5.9 X-Git-Url: http://puddle.ca/cgi-bin/gitweb.cgi?p=mp3togo;a=commitdiff;h=9c9c439d29c945f9fc14b3e2f1f58082eb9b1d03 Merge with bugfix-0.5 --- --- /dev/null +++ b/lib/mp3togo.glade @@ -0,0 +1,1120 @@ + + + + + + + Import Playlist + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 320 + 260 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + Import + True + GTK_RELIEF_NORMAL + True + 0 + + + + + + True + Cancel + True + GTK_RELIEF_NORMAL + True + 0 + + + + + + 0 + False + True + GTK_PACK_END + + + + + + True + False + 0 + + + + True + Import Playlist from Application + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + True + + + + + + True + False + 0 + + + + True + Import from XMMS session + True + GTK_RELIEF_NORMAL + True + False + False + True + + + + + 0 + False + False + + + + + + True + True + 1 + 0 + True + GTK_UPDATE_ALWAYS + False + False + 0 0 100 1 10 10 + + + 0 + False + False + GTK_PACK_END + + + + + 0 + False + False + + + + + + True + Import from amaroK + True + GTK_RELIEF_NORMAL + True + False + False + True + ImportXMMS + + + + + 0 + False + True + + + + + 12 + True + True + + + + + + + + mp3togo + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 620 + 400 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + + True + False + 0 + + + + True + False + 0 + + + + True + False + 0 + + + + 15 + True + mp3togo + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + True + 0 + + + 0 + True + True + + + + + + True + GTK_BUTTONBOX_DEFAULT_STYLE + 0 + + + + True + Settings + True + GTK_RELIEF_NORMAL + True + + + + + + + True + Quit + True + GTK_RELIEF_NORMAL + True + + + + + + 0 + False + False + + + + + 10 + True + True + + + + + + 2 + True + + + 0 + True + True + + + + + 0 + False + False + + + + + + True + True + + + + True + False + 0 + + + + True + False + 0 + + + + True + Playlist + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 5 + False + False + + + + + + True + False + 0 + + + + 2 + 300 + 300 + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_NONE + GTK_CORNER_TOP_LEFT + + + + 2 + True + True + False + True + False + True + False + False + True + + + + + 0 + True + True + + + + + 0 + True + True + + + + + 0 + True + True + + + + + + True + GTK_BUTTONBOX_START + 0 + + + + True + True + True + GTK_RELIEF_NORMAL + True + + + + + True + 0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + + + + True + False + 2 + + + + True + gtk-apply + 4 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + False + + + + + + True + All + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + + + + True + True + True + GTK_RELIEF_NORMAL + True + + + + + True + 0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + + + + True + False + 2 + + + + True + gtk-apply + 4 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + False + + + + + + True + None + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + + + + True + True + True + GTK_RELIEF_NORMAL + True + + + + + True + 0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + + + + True + False + 2 + + + + True + gtk-apply + 4 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + False + + + + + + True + Invert + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + + + 0 + True + True + + + + + + True + GTK_BUTTONBOX_START + 0 + + + + True + Remove Selected + True + GTK_RELIEF_NORMAL + True + + + + + + + True + Add Files + True + GTK_RELIEF_NORMAL + True + + + + + + + True + Import from App + True + GTK_RELIEF_NORMAL + True + + + + + + 0 + False + False + + + + + True + False + + + + + + True + False + 0 + + + + True + False + 0 + + + + True + Player + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 5 + False + False + + + + + + True + False + 0 + + + + True + Not implemented + False + False + GTK_JUSTIFY_CENTER + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + True + False + + + + + 0 + True + True + + + + + 0 + True + True + + + + + True + True + + + + + 0 + True + True + + + + + + 2 + True + + + 0 + False + True + + + + + + True + False + 0 + + + + True + True + GTK_POLICY_NEVER + GTK_POLICY_NEVER + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + 420 + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + + + 0 + False + False + + + + + + True + False + 0 + + + + True + False + 0 + + + + True + + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 5 + False + False + + + + + + 20 + True + GTK_PROGRESS_LEFT_TO_RIGHT + 0 + 0.10000000149 + PANGO_ELLIPSIZE_NONE + + + 0 + False + True + + + + + 0 + True + True + + + + + + 30 + True + GTK_BUTTONBOX_DEFAULT_STYLE + 5 + + + + True + gtk-convert + True + GTK_RELIEF_NORMAL + True + + + + + + + True + gtk-media-pause + True + GTK_RELIEF_NORMAL + True + + + + + + True + gtk-media-stop + True + GTK_RELIEF_NORMAL + True + + + + + 10 + False + False + + + + + 0 + False + False + + + + + 0 + False + True + + + + + + + + mp3togo settings + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 320 + 260 + True + True + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + Save + True + GTK_RELIEF_NORMAL + True + 0 + + + + + + True + Cancel + True + GTK_RELIEF_NORMAL + True + 0 + + + + + 0 + False + True + GTK_PACK_END + + + + + + True + False + 0 + + + + + + + + + + + + + + + 0 + True + True + + + + + + + + True + GTK_FILE_CHOOSER_ACTION_OPEN + True + True + False + False + Add Files or Playlists + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + + + + True + False + 24 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + True + True + True + True + gtk-open + True + GTK_RELIEF_NORMAL + True + -5 + + + + + 0 + False + True + GTK_PACK_END + + + + + + + --- /dev/null +++ b/lib/mp3togo.gladep @@ -0,0 +1,8 @@ + + + + + mp3togo + mp3togo + FALSE + --- a/mp3togo/__init__.py +++ b/mp3togo/__init__.py @@ -21,5 +21,5 @@ # __all__ = ('converter', 'main', 'options', 'setup') -version = '0.5.8' +version = '0.6.0' --- a/mp3togo/cluster.py +++ b/mp3togo/cluster.py @@ -97,23 +97,13 @@ def slave(opts): def initialize(ropts): """Integrate remote options into local opts""" - # of = file('/home/sim/temp/2go/cluster/opts-slave', 'w') -# for key in ropts.keys(): -# if opts.has_key(key): -# opts[key] = ropts[key] -# # of.write("%s: %s\n" % (key, opts[key])) -# else: -# return None - # of.close() - st = ropts['pickle'] - opts._d = cPickle.loads(st) - - opts['verbosity'] = 0 - of = file('/home/sim/temp/2go/cluster/opts-slave', 'w') - for key in opts.keys(): - of.write("%s: %s\n" % (key, opts[key])) - of.close() - return "SLAVE: ready" + try: + st = ropts['pickle'] + opts._d = cPickle.loads(st) + opts['verbosity'] = 0 + return "SLAVE: ready" + except: + return None def perform(rotps): """Run the work unit and watch progress""" @@ -124,7 +114,7 @@ def slave(opts): fc = cache.Cache(opts['usecache'], opts) else: fc = None - trk = track.Track(rotps['file'], opts, fc) + trk = track.Track(rotps['file'], opts, cooked=fc) pl = pool.Pool(opts) if pl.add_track(trk): tsk = trk.getfirst() @@ -220,9 +210,14 @@ class Boss: self.current = None self.pid, self.fd1 = pty.fork() if self.pid == 0: + args = '' args = r_schema.replace('%h', host) - args += " mp3togo --cluster-slave True" - #args += " test-mp3togo --cluster-slave True" + if args: + args += ' ' + if self.opts.argv: + args += self.opts.argv[0] + " --cluster-slave True" + else: + args += "mp3togo --cluster-slave True" args = args.split() os.execvp(args[0], args) else: @@ -267,6 +262,7 @@ class Boss: self.files.push(self.current, front=True) raise StopIteration yield ('Not Ready', 'Not Ready', 0) + # Roll back and try to start again self = Boss(yada, ...) continue self.rf.flush() for line in inp.split('\n'): @@ -289,12 +285,12 @@ class Boss: return self.wf.write('file=%s\n' % self.current) self.wf.write(EOT) - start = time.time() + self.start = time.time() percent = 0.0 if EOR.replace('\n', '') == line: if self.current: yield (self.current, 'Finished', - float(time.time() - start)) + float(time.time() - self.start)) next() break elif line.startswith('Progress:'): --- a/mp3togo/conf.py +++ b/mp3togo/conf.py @@ -75,11 +75,14 @@ class Options(options.Options): """Subclass options.Options and fill in application specific information.""" def __init__(self, argv=None, conffile=None, readconf=True): - options.Options.__init__(self, '~/.mp3togo') + self.argv = argv + options.Options.__init__(self, absfile('~/.mp3togo')) # Options to add: # ((name, short option, long option, default value, hook, help text), ...) self._set += [ + ('gui', 'X', 'gui-mode', False, None, + '''Start mp3togo GTK interface.'''), ('brwarning', 't', 'file-tag', '.2go', None, '''A string appended to the name of the file to indicate it has been recoded.'''), ('tempdir', 'w', 'work-dir', '/tmp', self._absfile, @@ -260,6 +263,8 @@ Description: def checkfile(self, name): """Verify the existence of an audio file""" name = name.replace('\n', '') + name = name.replace('\r', '') + name = name.replace('\f', '') if not os.path.exists(name): raise ErrorNoFile name = absfile(name) --- a/mp3togo/filelist.py +++ b/mp3togo/filelist.py @@ -24,13 +24,14 @@ import os import tempfile import threading -import mp3togo.conf as setup +import mp3togo.conf as conf class FileList: """A list of verified music files.""" def __init__(self, opts): + """Setup""" self._opts = opts self._list = [] self._i = 0 @@ -51,28 +52,30 @@ class FileList: return tuple(self._list) def addfiles(self, names, block=True): + """Add a list of files or playlists to the list""" if not self._lock.acquire(block) and not block: return False - if type(names) not in (type([]), type(())): names = (names,) - for name in names: - try: - if os.path.splitext(name)[1] in ('.m3u', '.pls'): - self._addplaylist(name) - continue - else: - name = self._opts.checkfile(name) - except: - continue - - if name not in self._list: - self._list.append(name) - + if os.path.splitext(name)[1] in ('.m3u', '.pls'): + self._addplaylist(name) + else: + self._addfile(name) self._lock.release() return True + def _addfile(self, name): + """Add one file""" + #All files must be added through this method for the subclass to work + try: + name = self._opts.checkfile(name) + if name not in self._list: + self._list.append(name) + return True + except: + return None + def addplaylist(self, lst, block=True): """Add a playlist.""" if not self._lock.acquire(block) and not block: @@ -86,40 +89,35 @@ class FileList: return ret def _addplaylist(self, lst): + """Internal use""" if not os.path.exists(lst): - raise setup.ErrorNoFile + raise conf.ErrorNoFile pl = file(lst) for line in pl: if line.startswith('/'): - try: - name = self._opts.checkfile(line) - except: - continue - self._list.append(name) + self._addfile(line) elif line.startswith("File") and line.find('=') > -1: name = line.strip().split('=', 1)[1] - try: - name = self._opts.checkfile(name) - except: - continue - self._list.append(name) + self._addfile(name) return True def addXMMS(self, session=0, block=True): + """Add playlist from running XMMS session""" if self._opts.mod['xmms']: import xmms else: - raise setup.ErrorNoXMMSControl + raise conf.ErrorNoXMMSControl if not xmms.control.is_running: - raise setup.ErrorXMMSNotRunning + raise conf.ErrorXMMSNotRunning res = [] for i in range(xmms.control.get_playlist_length(session)): - res.append(xmms.control.get_playlist_file(i)) + res.append(xmms.control.get_playlist_file(i, session)) # locks self in addfiles self.addfiles(res, block) def addAmarok(self, session=0, block=True): + """Add playlist from running amaroK session""" # Thanks Justus Pendleton if self._opts.bin['dcop']: m3u = tempfile.NamedTemporaryFile(suffix='.m3u', @@ -129,20 +127,24 @@ class FileList: 'dcop', 'amarok', 'playlist', 'saveM3u', m3u.name, '0') if exit_code != 0: - raise setup.ErrorAmarokNotRunning + raise conf.ErrorAmarokNotRunning self.addplaylist(m3u.name) #os.unlink(m3u.name) <- cleaned up automaticaly by tempfile def removefile(self, name, block=True): + """remove a file from the list""" if not self._lock.acquire(block) and not block: return False + self._removefile(name) + self._lock.release() + return True + def _removefile(self, name): + """Internal use""" + # All removals should use this while name in self._list: del self._list[self._list.index(name)] - self._lock.release() - return True - def pop(self, index=-1): """Pop off a value atomicly Use either this or the iterator access, not both.""" @@ -157,7 +159,7 @@ class FileList: """Put a file back in the list for another cluster node to try maybe.""" self._poplock.acquire() - if front: + if front: self._list.insert(0, value) else: self._list.append(value) @@ -167,18 +169,21 @@ class FileList: """To use the iterator access, you must promise to not modify the list.""" if not self._lock.locked(): - raise setup.ErrorUnlocked + raise conf.ErrorUnlocked for self._i in range(len(self._list)): yield self._list[self._i] def cur_file(self): + """Returns the index of the iterator""" return self._i def __len__(self): + """returns the length of the list""" return len(self._list) def __contains__(self, name): + """Membership test""" return name in self._list --- /dev/null +++ b/mp3togo/gui/__init__.py @@ -0,0 +1,23 @@ +# +# mp3togo Portable Music Manager +# +# PyGTK interface sub package +# +# Copyright 2006: Simeon Veldstra +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc. +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + --- /dev/null +++ b/mp3togo/gui/filewindow.py @@ -0,0 +1,53 @@ +# - gui/filewindow.py -- +# GTK user interface - File selection window +# +# This file is part of mp3togo +# +# (c) Simeon Veldstra 2006 +# +# This software is free. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You may redistribute this program under the terms of the +# GNU General Public Licence version 2 +# Available in this package or at http://www.fsf.org + + +try: + import pygtk + pygtk.require("2.0") + import gtk + import gtk.glade + import gobject +except: + print "GTK not present! mp3togo GTK GUI requires python-gtk2 and python-glade2" + sys.exit(1) + +import sys +import os + +import mp3togo.gui.gutil as gutil + +class AddFilesSelector: + """File and playlist selector""" + + def __init__(self, opts): + self.opts = opts + + def run(self): + gladefile = gutil.find_glade_xml(self.opts) + self.wTree = gtk.glade.XML(gladefile, "AddFilesSelector") + self.dlg = self.wTree.get_widget("AddFilesSelector") + + result = self.dlg.run() + names = self.dlg.get_filenames() + self.dlg.destroy() + if result == gtk.RESPONSE_OK: + return names + else: + return None + --- /dev/null +++ b/mp3togo/gui/gutil.py @@ -0,0 +1,35 @@ +# - gutil.py -- +# GTK user interface utility functions + +# This file is part of mp3togo + +# Convert audio files to play on a mp3 player +# +# (c) Simeon Veldstra 2006 +# +# This software is free. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You may redistribute this program under the terms of the +# GNU General Public Licence version 2 +# Available in this package or at http://www.fsf.org + +import os +import sys + +import mp3togo.conf as conf + +def find_glade_xml(opts): + """Return a path to the glade xml file.""" + if opts.argv and opts.argv[0] != "mp3togo": + xml = os.path.split(conf.__file__)[0] + xml = os.path.split(xml)[0] + xml = os.path.join(xml, 'lib/mp3togo.glade') + else: + xml = os.path.join(sys.prefix, 'lib/mp3togo/mp3togo.glade') + return xml + --- /dev/null +++ b/mp3togo/gui/importwindow.py @@ -0,0 +1,77 @@ +# - gui/importwindow.py -- +# GTK user interface - Import from jukebox application window +# +# This file is part of mp3togo +# +# (c) Simeon Veldstra 2006 +# +# This software is free. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You may redistribute this program under the terms of the +# GNU General Public Licence version 2 +# Available in this package or at http://www.fsf.org + + +try: + import pygtk + pygtk.require("2.0") + import gtk + import gtk.glade + import gobject +except: + print "GTK not present! mp3togo GTK GUI requires python-gtk2 and python-glade2" + sys.exit(1) + +import mp3togo.gui.gutil as gutil + + +class ImportDialog: + """Import playlists from jukebox apps.""" + + def __init__(self, opts): + self.current = "XMMS" + + gladefile = gutil.find_glade_xml(opts) + self.wTree = gtk.glade.XML(gladefile, "ImportDialog") + self.dlg = self.wTree.get_widget("ImportDialog") + + callbacks = { + "on_ImportXMMS_toggled": (self.change, "XMMS"), + "on_ImportAmarok_toggled": (self.change, "amaroK"), + "on_ImportCancel": (self.change, ""), + } + self.wTree.signal_autoconnect(callbacks) + + # Shade out jukeboxes that are not supported on this machine + if not opts.mod['xmms']: + if self.current.startswith('XMMS'): + self.current = '' + rbut = self.wTree.get_widget('ImportXMMS') + spinner = self.wTree.get_widget('XMMSSession') + rbut.set_active(False) + spinner.set_editable(False) + + if not opts.bin['dcop']: + rbut = self.wTree.get_widget('ImportAmarok') + rbut.set_active(False) + + def change(self, obj, arg): + self.current = arg + + def run(self): + """Show the Import from Jukebox dialog.""" + self.dlg.run() + name = self.current + if name == 'XMMS': + spinner = self.wTree.get_widget('XMMSSession') + name += ':' + str(int(spinner.get_value())) + self.dlg.destroy() + return name + + + --- /dev/null +++ b/mp3togo/gui/mainwindow.py @@ -0,0 +1,266 @@ +# - gui/mainwindow.py -- +# GTK user interface - Main application window +# +# This file is part of mp3togo +# +# (c) Simeon Veldstra 2006 +# +# This software is free. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You may redistribute this program under the terms of the +# GNU General Public Licence version 2 +# Available in this package or at http://www.fsf.org + + +try: + import pygtk + pygtk.require("2.0") + import gtk + import gtk.glade + import gobject +except: + print "GTK not present! mp3togo GTK GUI requires python-gtk2 and python-glade2" + sys.exit(1) + +import sys +import os +import time +import threading + +import mp3togo.util as util +import mp3togo.conf as conf +import mp3togo.filelist as filelist +import mp3togo.pool as pool +import mp3togo.track as track +import mp3togo.task as task +import mp3togo.cache as cache +import mp3togo.cluster as cluster + +import mp3togo.gui.gutil as gutil +import mp3togo.gui.filewindow as filewindow +import mp3togo.gui.importwindow as importwindow +import mp3togo.gui.playlist as playlist + +### Playlist Model: ['status', True, False, '/foo/bar/one.mp3'] ### +STATUS = 0 +ABLE = 1 +SELECTED = 2 +FILE = 3 +NAME = 4 + + +class MainWind: + """The GUI Front end""" + + def __init__(self, opts): + self.opts = opts + self.pool = pool.Pool(opts) + self.fails = {} + self.gang = [] + self.gladexml = gutil.find_glade_xml(opts) + + self.wTree = gtk.glade.XML(self.gladexml, "MainWindow") + + view = self.wTree.get_widget("PlayListList") + self.list = playlist.PlayList(opts, view) + + self.tips = gtk.Tooltips() + + callbacks = { + "on_Exit": self.exit, + "on_MainWindow_destroy": self.exit, + "on_PlaylistAddFiles": self.add_files, + "on_PlaylistImport": self.import_files, + "on_PlaylistRemove": self.list.remove_selected, + "on_SelectAll": (self.list.select_all, True), + "on_SelectNone": (self.list.select_all, False), + "on_Configure": self.configure, + "on_Start": self.convert, + } + self.wTree.signal_autoconnect(callbacks) + + # Set up worker processes: + self.workers = [('mp3togoGTK', '')] + + self.window = self.wTree.get_widget("MainWindow") + if self.window: + self.window.show() + + def select_all_playlist(self, obj=None): + """Select all""" + self.list.select_all(True) + + def select_none_playlist(self, obj=None): + """Select None""" + self.list.select_all(False) + + def add_files(self, obj): + """Callback to spawn fileselector""" + child = filewindow.AddFilesSelector(self.opts) + result = child.run() + if result: + self.list.addfiles(result) + + def import_files(self, obj): + """callback to spawn file importer""" + child = importwindow.ImportDialog(self.opts) + result = child.run() + try: + # Move this into the list class + if result.startswith('XMMS'): + self.list.addXMMS(session=int(result.split(':')[1])) + elif result.startswith('amaroK'): + self.list.addAmarok() + except: + print "exception" + + def convert(self, obj=None): + """Start the conversion""" + if not self.list._list: + return + # Set up cache + cooked = None + + # Check for sanity + pass + + for host in self.workers: + master = cluster.Boss(host[0], + self.list, + self.fails, + self.opts, + host[1]) + self.gang.append({'boss': master, + 'poll': master.poll(), + 'file': ''}) + + def refresh_callback(self): + """Monitor the workers""" + if self.gang: + for slave in self.gang[:]: + try: + status = slave['poll'].next() + print status + except StopIteration: + self.set_progress(0.0) + if slave['file']: + self.list.finish(slave['file']) + del self.gang[self.gang.index(slave)] + break + if status[1] == 'Subtask Failed': + if slave['file']: + self.list.push(slave['file']) + self.set_status('Task failed') + break + if status[1] == 'Not Ready': + # Boss.kill() and restart? + break + if status[1] == 'Finished': + self.set_progress(0.0) + if slave['file']: + ts = util.format_time(status[2]) + self.list.finish(slave['file'], ts) + self.set_status('Finished') + slave['file'] = '' + else: + if status[0] in self.list: + self.list.finish(status[0]) + break + if status[0] != slave['file']: + if slave['file']: + self.list.finish(slave['file']) + slave['file'] = status[0] + #self.start_file(slave['file']) Boss calls pop() + self.set_fname(slave['file']) + break + # Just working + self.set_status(status[1]) # Current action + self.set_progress(status[2] / 100.0) + self.window.show_all() + return True + else: + return True + + + #wrap up + # def exec_thread(): + # print "Bar" + # bar = self.wTree.get_widget("Progress") + # flabel = self.wTree.get_widget("CurrentFilename") + # while self.list: + # et = 0 + # active = self.list.pop(0) + # print active + # self.start_file(active) + # flabel.set_label(active) + # trk = track.Track(active, self.opts, cooked) + # if self.pool.add_track(trk): + # tsk = trk.getfirst() + # while tsk: + # self.set_status(tsk.name) + # tsk.run() + # while tsk.status() == task.RUNNING: + # pcent = tsk.output() + # try: + # pcent = float(pcent) / 100.0 + # except: + # pcent = 0.0 + # bar.set_fraction(pcent) + # et += tsk.elapsed_time() + # tsk.wait() + # tsk = tsk.next() + # self.finish_file(active, format_time(et)) + # else: + # self.set_status("Out of space") + # return + # self.set_status("Done") + # flabel.set_label("") + + # self.machine = threading.Thread(None, exec_thread) + # self.machine.start() + + def set_status(self, msg): + """Put text in the status label""" + self.status = msg + bar = self.wTree.get_widget("StatusText") + bar.set_label(msg) + bar.show() + + def set_fname(self, name): + """Set the current filename label""" + flabel = self.wTree.get_widget("CurrentFile") + #if len(name) > 60: + # self.tips.set_tip(flabel, name) + # name = name[-60:] + flabel.set_label(name) + flabel.show() + + def set_progress(self, percent): + """Update the progress bar""" + bar = self.wTree.get_widget("Progress") + bar.set_fraction(percent) + bar.show() + + def configure(self): + """Callback to display configure window""" + child = Configure(self.opts) + result = child.run() + print result + + def exit(self, obj): + """Exit, but don't leave a mess.""" + gtk.main_quit() + + +def start_gui(opts): + """Run the mp3togo PyGTK interface""" + gobject.threads_init() + gui = MainWind(opts) + gobject.idle_add(gui.refresh_callback) + gtk.main() + --- /dev/null +++ b/mp3togo/gui/playlist.py @@ -0,0 +1,233 @@ +# - gui/playlist.py -- +# GTK user interface - playlist widget +# +# This file is part of mp3togo +# +# (c) Simeon Veldstra 2006 +# +# This software is free. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You may redistribute this program under the terms of the +# GNU General Public Licence version 2 +# Available in this package or at http://www.fsf.org + +"""Manage the playlist widget""" + +try: + import pygtk + pygtk.require("2.0") + import gtk + import gtk.glade + import gobject +except: + print "GTK not present! mp3togo GTK GUI requires python-gtk2 and python-glade2" + sys.exit(1) + +import sys +import os + +import mp3togo.filelist as filelist +import mp3togo.tags as tags + +import mp3togo.gui.gutil as gutil + + +STATUS = 0 +ABLE = 1 +SELECT = 2 +NAME = 3 +FILE = 4 + + +class PlayList(filelist.FileList): + """manage a list of tracks""" + + def __init__(self, opts, view): + """Create a playlist + + opts is a conf.Options() instance + view is a gtk ListView widget""" + + filelist.FileList.__init__(self, opts) + self.view = view + # add option to conf for this: + self.format = "[%x] %a - %t" + # STATUS, ABLE, SELECT, NAME, FILE + self.model = gtk.ListStore(str, bool, bool, str, str) + + # Populate the ListStore widget + def cell_toggle(renderer, path, data): + new = self.model[path] + new[SELECT] = not new[SELECT] + self.model[path] = new + self._list[self.index(new[FILE])].selected = new[SELECT] + # Status Column + column = gtk.TreeViewColumn('Status', + gtk.CellRendererText(), + text=STATUS) + self.view.append_column(column) + # Select Column + toggle = gtk.CellRendererToggle() + toggle.set_property('activatable', True) + toggle.connect("toggled", cell_toggle, None) + column = gtk.TreeViewColumn('', + toggle, + activatable=ABLE, + active=SELECT) + self.view.append_column(column) + # Name Column + column = gtk.TreeViewColumn('Track', + gtk.CellRendererText(), + text=NAME) + self.view.append_column(column) + # Link data to widget + self.view.set_model(self.model) + + def run(self): + pass + + def __iter__(self): + """Iterate over a static list of waiting files""" + sl = self.tuple() + self._i = 0 + for i in range(len(sl)): + yield sl[i] + + def __contains__(self, name): + return name in [x.name for x in self._list] + + def index(self, name): + """Return the index of a file in the list + Throws IndexError if not present.""" + return filter(lambda x: x[1].name == name, + zip(range(len(self._list)), self._list))[0][0] + + def tuple(self): + """Returns a tuple containing all the unstarted files""" + return [x.name for x in self._list if x.state == 'ready'] + + def _addfile(self, name): + """Add a file to self""" + name = self._opts.checkfile(name) + if name not in self: + entry = ListEntry(name, self._opts) + self._list.append(entry) + self.model.append(entry.as_model(self.format)) + + def _removefile(self, name): + """Remove a file from the list""" + try: + i = self.index(name) + except IndexError: + return + del self._list[i] + del self.model[i] + + def pop(self, index=0): + """Pop a file off of the ready list and set its status + Raises IndexError like list.pop()""" + self._lock.acquire() + name = None + try: + name = self.tuple()[index] + i = self.index(name) + self._list[i].state = 'running' + self.model[i][STATUS] = ' ->' + self._list[i].selected = False + self.model[i][SELECT] = False + self._list[i].selectable = False + self.model[i][ABLE] = False + return name + finally: + self._lock.release() + + def finish(self, name, elapsed='done'): + """Wrap up a finished file""" + self._lock.acquire() + try: + i = self.index(name) + self._list[i].state = 'done' + self.model[i][STATUS] = elapsed + self._list[i].selectable = True + self.model[i][ABLE] = True + finally: + self._lock.release() + + def push(self, name): + """Put a file back in the ready list""" + self._lock.acquire() + try: + if name in self: + i = self.index(name) + self._list[i].state = 'ready' + self.model[i][STATUS] = ' ' + self._list[i].selectable = True + self.model[i][ABLE] = True + else: + self._addfile(name) + finally: + self._lock.release() + + def regenerate(self): + """Regenerate the model from the data""" + self._lock.acquire() + try: + self.model.clear() + for entry in self._list: + self.model.append(entry.as_model(self.format)) + finally: + self._lock.release() + + def select_all(self, obj, all): + """Select all if all=True none if all=False""" + self._lock.acquire() + try: + all = bool(all) + for i in range(len(self._list)): + self.model[i][SELECT] = all + self._list[i].selected = all + finally: + self._lock.release() + + def remove_selected(self, obj=None): + self._lock.acquire() + try: + names = [x.name for x in self._list if x.selected] + for name in names: + i = self.index(name) + del self._list[i] + del self.model[i] + finally: + self._lock.release() + + +class ListEntry: + """An item in a PlayList""" + + def __init__(self, name, opts): + """Create a list entry""" + self.name = name + self.tags = tags.Tags(name, opts) + self.tags.read() + self.state = 'ready' + self.flag = ' ' + self.selectable = True + self.selected = False + + def as_model(self, fmt): + """The list entry in a form suitable for the ListStore widget""" + return [self.flag, + self.selectable, + self.selected, + self.tags.format(fmt), + self.name] + + + + + --- a/mp3togo/main.py +++ b/mp3togo/main.py @@ -33,6 +33,7 @@ import termios import fcntl import mp3togo +import mp3togo.util as util import mp3togo.filelist as filelist import mp3togo.conf as conf import mp3togo.track as track @@ -41,6 +42,8 @@ import mp3togo.pool as pool import mp3togo.cache as cache import mp3togo.cluster as cluster +import mp3togo.gui.mainwindow as mainwindow + def fail(mesg='', code=1): if mesg: @@ -56,6 +59,11 @@ def main(argv): print conf.Options().usage() fail(str(msg)) + # Hand off control for gui interface + if opts['gui']: + gui = mainwindow.start_gui(opts) + return 0 + # Cluster mode is handled in the cluster module if opts['clusterslave']: cluster.slave(opts) @@ -207,7 +215,7 @@ def execute_sequential(playlist, opts, c break except IOError: pass tm = tsk.elapsed_time() - ts = format_time(tm) + ts = util.format_time(tm) tsk.wait() tryp("\r ") if tsk.status() == task.DONE: @@ -244,14 +252,14 @@ def execute_sequential(playlist, opts, c del trk tt = time.time() - track_start - ts = format_time(tt) + ts = util.format_time(tt) if ts: tryp("Total time this track: %s\n\n" % ts) else: tryp('\n') tm = time.time() - start_time - ts = format_time(tm) + ts = util.format_time(tm) if bad_ones: if bad_ones == 1: bad_str = ", 1 track failed." @@ -269,32 +277,5 @@ def execute_sequential(playlist, opts, c fcntl.fcntl(fd, fcntl.F_SETFL, oldflags) -def format_time(tm): - tm = int(tm) - if tm: - hr, min, sec = (tm/3600, (tm%3600)/60, (tm%3600)%60) - ts = "%ds " % sec - if min > 0: - ts = ("%dm " % min) + ts - if hr > 0: - ts = ("%dh " % hr) + ts - return ts[:-1] - else: - return '' - -def format_bytes(bytes): - k = 1024.0 - m = 1024 * k - g = 1024 * m - if 0 < bytes < k: - ret = str(int(bytes/k)) + " KB" - elif k <= bytes < m: - ret = str(int(bytes/k)) + " KB" - elif m <= bytes < g: - ret = str(int(bytes/m)) + " MB" - else: - ret = str(bytes) + " Bytes" - - if __name__ == "__main__": main(sys.argv) --- a/mp3togo/tags.py +++ b/mp3togo/tags.py @@ -242,7 +242,9 @@ class Tags(UserDict.DictMixin): %t Track title %l Album title %y Album release year - %g Album genre""" + %g Album genre + %f Track file name + %x File extension """ if not self._lock.acquire(block) and not block: return False @@ -252,6 +254,8 @@ class Tags(UserDict.DictMixin): 'l': 'ALBUM', 'y': 'YEAR', 'g': 'GENRE_NAME'} + #'x': file extension + #'f': filename #'z': Used for literal '%' out = "" @@ -265,6 +269,10 @@ class Tags(UserDict.DictMixin): code = fmt[0] and fmt[0][0] if code == 'z': fmt[0] = '%' + fmt[0][1:] + elif code == 'f': + fmt[0] = self._file + fmt[0][1:] + elif code == 'x': + fmt[0] = self._type + fmt[0][1:] elif code in esc.keys(): fmt[0] = self._tags.get(esc[code], ('',))[0] + fmt[0][1:] else: --- a/mp3togo/track.py +++ b/mp3togo/track.py @@ -44,7 +44,7 @@ DFLACFACTOR = 3 class Track: """Encapsulate the transformation of a file.""" - def __init__(self, filename, opts, cooked=None): + def __init__(self, filename, opts, cooked=None, intags=None): self.bytes = 0 self._filename = filename @@ -82,8 +82,11 @@ class Track: # Read the tags # Do this early so that 'treestructure' can access them - self.tags = tags.Tags(filename, opts) - self.tags.read() + if intags: + self.tags = intags + else: + self.tags = tags.Tags(filename, opts) + self.tags.read() # Names filetype = opts.getfiletype(filename) --- /dev/null +++ b/mp3togo/util.py @@ -0,0 +1,56 @@ +# - util.py - +# General utility functions +# This file is part of mp3togo +# +# (c) Simeon Veldstra 2006 +# +# This software is free. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You may redistribute this program under the terms of the +# GNU General Public Licence version 2 +# Available in this package or at http://www.fsf.org + +"""Utility functions for mp3togo""" + +import sys, os + + + + +def format_time(tm): + """Convert a time from floating point seconds to H:M:S""" + tm = int(tm) + if tm: + hr, min, sec = (tm/3600, (tm%3600)/60, (tm%3600)%60) + ts = "%ds " % sec + if min > 0: + ts = ("%dm " % min) + ts + if hr > 0: + if min == 0: + ts = "0m " + ts + ts = ("%dh " % hr) + ts + return ts[:-1] + else: + return '' + +def format_bytes(bytes): + """Express a size in human readable format""" + k = 1024.0 + m = 1024 * k + g = 1024 * m + if 0 < bytes < k: + ret = str(int(bytes/k)) + " KB" + elif k <= bytes < m: + ret = str(int(bytes/m)) + " MB" + elif m <= bytes < g: + ret = str(int(bytes/g)) + " GB" + else: + ret = str(bytes) + " Bytes" + return ret + + --- a/setup.py +++ b/setup.py @@ -72,9 +72,10 @@ use its components in another program. The package installs a command line executable named mp3togo in /usr/bin, for command line options, run `mp3togo --help`.""", - packages = ["mp3togo"], + packages = ["mp3togo", "mp3togo.gui"], #data_files = [('/usr/bin', ['bin/mp3togo'])], scripts = ['bin/mp3togo'], + data_files = [('lib/mp3togo', ['lib/mp3togo.glade'])], classifiers = [ 'Development Status :: 4 - Beta', 'Environment :: Console',