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.
--- 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:
--- 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)
--- 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
--- 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
--- 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
--- /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]
+
+
+
+
+
--- 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']:
--- 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:
--- 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)