Improvements have been made, bugs have been squashed.
The code for the playlist widget has been moved out of mainwindow.py into
its own module. Most of the things this change broke have been fixed.

file:ea478e5ea4f740f325bce4b3e412420e19ef9b90 -> file:14277acd53b89c4bc1bdd5a306b2a8fd29ddec69
--- a/mp3togo/cluster.py
+++ b/mp3togo/cluster.py
@@ -114,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()
@@ -210,11 +210,14 @@ class Boss:
self.current = None
self.pid, self.fd1 = pty.fork()
if self.pid == 0:
+ args = ''
args = r_schema.replace('%h', host)
+ if args:
+ args += ' '
if self.opts.argv:
- args += " " + self.opts.argv[0] + " --cluster-slave True"
+ args += self.opts.argv[0] + " --cluster-slave True"
else:
- args += " mp3togo --cluster-slave True"
+ args += "mp3togo --cluster-slave True"
args = args.split()
os.execvp(args[0], args)
else:
file:b7f11b8968c704154095fd4be26c63e5db6ecef7 -> file:feca2d1f7d37286d4f03ad1d73bc454cd20d41d9
--- a/mp3togo/conf.py
+++ b/mp3togo/conf.py
@@ -260,6 +260,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)
file:8e3b396fb24e674ccedc0761bfc22ea1ffd42f7f -> file:0c4aa85b716afdc199903e38cf5c15c255db6337
--- 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
file:1def193ac427571c9322b9cb1d55a515c1f92bc1 -> file:559f5ef47c2fda934305c1b82d7db811774f6d22
--- a/mp3togo/gui/filewindow.py
+++ b/mp3togo/gui/filewindow.py
@@ -46,5 +46,8 @@ class AddFilesSelector:
result = self.dlg.run()
names = self.dlg.get_filenames()
self.dlg.destroy()
- return names
+ if result == gtk.RESPONSE_OK:
+ return names
+ else:
+ return None
file:2d7ce1408ed1fd450854c082dfcdabc3f57691c7 -> file:a66f241dcb40389168fbaceaf859288713a797f5
--- a/mp3togo/gui/mainwindow.py
+++ b/mp3togo/gui/mainwindow.py
@@ -44,8 +44,9 @@ 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: ['running', True, False, '/foo/bar/one.mp3'] ###
+### Playlist Model: ['status', True, False, '/foo/bar/one.mp3'] ###
STATUS = 0
ABLE = 1
SELECTED = 2
@@ -58,7 +59,6 @@ class MainWind:
def __init__(self, opts):
self.opts = opts
- self.list = filelist.FileList(opts)
self.pool = pool.Pool(opts)
self.fails = {}
self.gang = []
@@ -66,31 +66,8 @@ class MainWind:
self.wTree = gtk.glade.XML(self.gladexml, "MainWindow")
- # Create the playlist list widget
- self.playlistmodel = gtk.ListStore(str, bool, bool, str, str)
- self.playlistview = self.wTree.get_widget("PlayListList")
- column = gtk.TreeViewColumn('Done',
- gtk.CellRendererText(),
- text=STATUS)
- self.playlistview.append_column(column)
- toggle = gtk.CellRendererToggle()
- toggle.set_property('activatable', True)
- def cell_toggle(renderer, path, data):
- new = self.playlistmodel[path]
- new[SELECTED] = not new[SELECTED]
- self.playlistmodel[path] = new
- toggle.connect("toggled", cell_toggle, None)
- column = gtk.TreeViewColumn('X',
- toggle,
- activatable=ABLE,
- active=SELECTED)
- self.playlistview.append_column(column)
- column = gtk.TreeViewColumn('File',
- gtk.CellRendererText(),
- text=NAME)
- column.set_sort_column_id(FILE)
- self.playlistview.append_column(column)
- self.playlistview.set_model(self.playlistmodel)
+ view = self.wTree.get_widget("PlayListList")
+ self.list = playlist.PlayList(opts, view)
self.tips = gtk.Tooltips()
@@ -99,10 +76,9 @@ class MainWind:
"on_MainWindow_destroy": self.exit,
"on_PlaylistAddFiles": self.add_files,
"on_PlaylistImport": self.import_files,
- "on_PlaylistRemove": self.update_playlist,
- "on_SelectAll": self.select_all_playlist,
- "on_SelectNone": self.select_none_playlist,
- "on_SelectInvert": self.select_invert_playlist,
+ "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,
}
@@ -115,81 +91,20 @@ class MainWind:
if self.window:
self.window.show()
- def update_playlist(self, obj=None):
- # Remove selected files
- def match_selected(model, path, iter, data):
- if model.get_value(iter, SELECTED):
- data.append(path)
- paths = []
- self.playlistmodel.foreach(match_selected, paths)
- paths.reverse()
- for path in paths:
- iter = self.playlistmodel.get_iter(path)
- name = self.playlistmodel.get_value(iter, FILE)
- self.list.removefile(name)
- self.playlistmodel.remove(iter)
-
- # Add new files to the widget
- # Assumes new files are always added to the end of a FileList.
- files = self.list._list[:]
- files.reverse()
-
- # This is stupid:
- addfiles = []
- while files and files[0] not in [x[FILE] for x in self.playlistmodel]:
- addfiles.insert(0, files[0])
- del files[0]
- for f in addfiles:
- name = os.path.basename(f)
- self.playlistmodel.append(['',
- True,
- False,
- f,
- name])
-
- def start_file(self, fname):
- """Start work on a file"""
- for row in self.playlistmodel:
- if row[FILE] == fname:
- row[SELECTED] = False
- row[ABLE] = False
- row[STATUS] = ' ->'
-
- def unstart_file(self, fname):
- """File started but didn't finish. probably failed."""
- for row in self.playlistmodel:
- if row[FILE] == fname:
- row[ABLE] = True
- row[STATUS] = 'Failed'
-
- def finish_file(self, fname, elapsed="done"):
- """File is done"""
- for row in self.playlistmodel:
- if row[FILE] == fname:
- row[ABLE] = True
- row[STATUS] = elapsed
-
def select_all_playlist(self, obj=None):
"""Select all"""
- for row in self.playlistmodel:
- row[SELECTED] = True
+ self.list.select_all(True)
def select_none_playlist(self, obj=None):
"""Select None"""
- for row in self.playlistmodel:
- row[SELECTED] = False
-
- def select_invert_playlist(self, obj=None):
- """Invert selection"""
- for row in self.playlistmodel:
- row[SELECTED] = not row[SELECTED]
+ self.list.select_all(False)
def add_files(self, obj):
"""Callback to spawn fileselector"""
child = filewindow.AddFilesSelector(self.opts)
result = child.run()
- self.list.addfiles(result)
- self.update_playlist()
+ if result:
+ self.list.addfiles(result)
def import_files(self, obj):
"""callback to spawn file importer"""
@@ -203,7 +118,6 @@ class MainWind:
self.list.addAmarok()
except:
print "exception"
- self.update_playlist()
def convert(self, obj=None):
"""Start the conversion"""
@@ -222,8 +136,8 @@ class MainWind:
self.opts,
host[1])
self.gang.append({'boss': master,
- 'poll': master.poll(),
- 'file': ''})
+ 'poll': master.poll(),
+ 'file': ''})
def refresh_callback(self):
"""Monitor the workers"""
@@ -233,30 +147,35 @@ class MainWind:
status = slave['poll'].next()
print status
except StopIteration:
+ self.set_progress(0.0)
if slave['file']:
- self.finish_file(slave['file'])
+ self.list.finish(slave['file'])
del self.gang[self.gang.index(slave)]
break
if status[1] == 'Subtask Failed':
if slave['file']:
- self.unstart_file(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.finish_file(slave['file'], ts)
+ 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.finish_file(slave['file'])
+ self.list.finish(slave['file'])
slave['file'] = status[0]
- self.start_file(slave['file'])
+ #self.start_file(slave['file']) Boss calls pop()
self.set_fname(slave['file'])
break
# Just working
file:fdecc9728910716744e151cc921d746bde0390c3(new)
--- /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 <reallifesim@gmail.com>
+#
+# 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]
+
+
+
+
+
file:66cb3da093a1ffe4dfd654081c20ed4e1039d730 -> file:d34f497216a5aa53178acf101a31fa76d1fe09f4
--- a/mp3togo/main.py
+++ b/mp3togo/main.py
@@ -62,7 +62,7 @@ def main(argv):
# Hand off control for gui interface
if opts['gui']:
gui = mainwindow.start_gui(opts)
- return
+ return 0
# Cluster mode is handled in the cluster module
if opts['clusterslave']:
file:fe1ae65e85f74809d3e54fa96fa96d61f03e9dff -> file:d0677643dda65fa9d4d708668bbe3717801c1084
--- a/mp3togo/tags.py
+++ b/mp3togo/tags.py
@@ -205,7 +205,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
@@ -215,6 +217,8 @@ class Tags(UserDict.DictMixin):
'l': 'ALBUM',
'y': 'YEAR',
'g': 'GENRE_NAME'}
+ #'x': file extension
+ #'f': filename
#'z': Used for literal '%'
out = ""
@@ -228,6 +232,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:
file:c4ca309cf270602b0ecc8790ddd427120124cbf0 -> file:385ab9c3710428a7130b1abc9ebdb7e25f6bf3bf
--- 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)