Version 0.3.0
New version released. Major code rewrite, the last of the
wall street code is redone.
Simeon Veldstra

file:d490afae2c1d63aa97b7b12d6c4d4ec39dcfedb2 -> file:a0d9e024c5cf3e2359bea08e6a79e9150239f82c
--- a/bin/mp3togo
+++ b/bin/mp3togo
@@ -18,7 +18,9 @@
# GNU General Public Licence version 2
# Available in this package or at http://www.fsf.org
+import sys
+
from mp3togo.main import main
-main()
+main(sys.argv)
file:b25fe4faa1eb1ed3cff62cd768602c2551f7dfbc -> file:a3bfa6f95101b92d740a17ec5d83e86cf7e9069c
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,13 @@
+mp3togo (0.3.0) unstable; urgency=low
+
+ * Rewrote program to use os.exec instead of os.system.
+ * New progress output format, pause and stop controls.
+ * Cleaned up, modularized, code.
+ * Built unit test suite.
+ * Added thread support.
+
+ -- Simeon Veldstra <reallifesim@gmail.com> Sun, 14 May 2006 14:27:56 -0700
+
mp3togo (0.2.1) unstable; urgency=low
* Initial Release.
file:09d1a04554e2c7b21694510747f017ed58d43f29 -> file:e259c8979c5c41c407589905c88fe0aefa838e71
--- a/debian/control
+++ b/debian/control
@@ -9,7 +9,7 @@ Package: mp3togo
Architecture: all
Depends: ${python:Depends}, python-pyvorbis, python-id3, mpg321, vorbis-tools
Recommends: flac, normalize-audio
-Suggests: lame, xmms-shell
+Suggests: lame, python-xmms
Description: A tool for loading music on to portable mp3 players
A tool for creating sets of mp3 (or ogg vorbis) files
from an archive of music in various formats and bitrates.
file:2aefe85dec816d48bd0ccb494578dafc49bab530 -> file:8c77441a752d69e4810cc549a26e737e3538fa2d
--- a/debian/mp3togo.1
+++ b/debian/mp3togo.1
@@ -134,4 +134,4 @@ License can be found in /usr/share/commo
.PP
mp3togo can be found online at http://puddle.ca/mp3togo
-.\" created by instant / docbook-to-man, Thu 27 Apr 2006, 00:37
+.\" created by instant / docbook-to-man, Sun 14 May 2006, 15:17
file:1a2403f6674162ce4330f384536557d8ddd33bf5 -> file:073b7b7235b2f0437541a4a0c7c4dc4c9bb3c247
--- a/mp3togo/__init__.py
+++ b/mp3togo/__init__.py
@@ -17,3 +17,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc.
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+
+# __all__ = ('converter', 'main', 'options', 'setup')
+
+version = '0.3.0'
+
file:79b6720a8fd5ea522996ac90be1d98347be4f0e5 -> file:32dea4e252772e38185ab9d4e42beef43646c1be
--- a/mp3togo/main.py
+++ b/mp3togo/main.py
@@ -28,8 +28,15 @@
import sys
import os
-import converter
-import setup
+import time
+import termios
+import fcntl
+
+import mp3togo.filelist as filelist
+import mp3togo.conf as conf
+import mp3togo.track as track
+import mp3togo.task as task
+import mp3togo.pool as pool
def fail(mesg, code=1):
@@ -37,70 +44,37 @@ def fail(mesg, code=1):
print >>sys.stderr, mesg
sys.exit(code)
-def main():
+def main(argv):
+
# Read in configuration
try:
- opts = setup.Options()
- opts.getconf(argv=sys.argv)
- except setup.Fail, msg:
+ opts = conf.Options(argv)
+ except conf.Fail, msg:
print setup.Options().usage()
fail(str(msg))
# Check for sanity
if not opts.bin[opts['encoder']]:
- opts.log(1, "Encoder binary '%s' not found in PATH! Aborting." % opts['encoder'])
+ opts.log(1, "Encoder binary '%s' not found in PATH! Quiting." % opts['encoder'])
fail('')
if not opts.bin['normalize-audio'] and not opts['nonormal']:
opts.log(1, "'normalize-audio' binary not found. Normalization disabled.")
opts['nonormal'] = True
+ if not os.path.isdir(opts['playerdir']):
+ opts.log(1, "target_dir does not exist")
+ fail('')
# Build a playlist
- playlist = []
- def readlist(list, playlist):
- try:
- plist = file(list, 'r')
- playlist += plist.readlines()
- plist.close()
- except:
- opts.log(1, "Can't open playlist: %s" % list)
+ playlist = filelist.FileList()
+
if opts['playlist']:
- readlist(opts['playlist'], playlist)
+ playlist.addplaylist(opts['playlist'])
+
if opts['arg_files']:
- playlist += opts['arg_files']
- if opts['readxmms'] and opts.bin['xmms-shell']:
- listname = "mp3togo-playlist." + str(os.getpid())
- listname = os.path.join(opts['tempdir'], listname)
- cmd = '%s -e "save %s"' % (opts.bin['xmms-shell'], listname)
- os.system(cmd)
- if os.path.exists(listname):
- readlist(listname, playlist)
- os.unlink(listname)
- elif opts['readxmms'] and not opts.bin['xmms-shell']:
- opts.log(1, "xmms-shell binary not found in path: aborting.")
- fail('')
- cleanlist = []
- for line in playlist:
- if line.startswith('#'): #.m3u files, comments
- continue
- if line[-1:] == "\n":
- line = line[:-1]
- if line[:4] == "File" and "=" in line: #XMMS .pls files
- line = "=".join(line.split("=")[1:])
- if not os.path.exists(line):
- opts.log(2, "\"%s\" is not a valid file" % line)
- continue
- cleanlist.append(line)
- playlist = cleanlist
+ playlist.addfiles(opts['arg_files'])
- # Check for a destination
- if not os.path.isdir(opts['playerdir']):
- opts.log(1, "target_dir does not exist")
- fail('')
-
- # Calculate available space
- fsp = os.statvfs(opts['playerdir']).f_bfree * os.statvfs(opts['playerdir']).f_frsize
- if fsp and (opts['maxsize'] == 0 or opts['maxsize'] > fsp):
- opts['maxsize'] = fsp
+ if opts['readxmms']:
+ playlist.addXMMS()
# Offer a hint
if len(playlist) == 0:
@@ -109,7 +83,155 @@ def main():
fail('')
# Start work
- converter.eatlist(opts, playlist)
+ execute_sequential(playlist, opts)
+
+
+def execute_sequential(playlist, opts):
+ """Run the conversions one at a time"""
+ print "mp3togo\n"
+ print "<space> or 'p' to pause, <esc> or 'q' to quit\n"
+ c = ''
+ start_time = time.time()
+ good_ones = 0
+ bad_ones = 0
+ playlist.run()
+
+ try:
+
+ fd = sys.stdin.fileno()
+ oldterm = termios.tcgetattr(fd)
+ newattr = termios.tcgetattr(fd)
+ newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, newattr)
+
+ oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
+
+ for name in playlist:
+ track_start = time.time()
+ print "(%d/%d) %s: " % (playlist.cur_file() + 1, len(playlist), name)
+ trk = track.Track(name, opts)
+ pl = pool.Pool(opts)
+ if pl.add_track(trk):
+ tsk = trk.getfirst()
+ try:
+ while tsk:
+ tsk.run()
+ if tsk.name in ('Start', 'Done', 'Cleaning'):
+ tsk.wait()
+ tsk = tsk.next()
+ continue
+ while tsk.status() == task.RUNNING:
+ #parse output
+ sys.stdout.write("\r")
+ pcent = tsk.output()
+ if 0 < pcent < 101:
+ bar = '#' * int(pcent/5.0) + ('_' * (20 - int(pcent/5.0)))
+ else:
+ bar = ""
+ pcent = 0
+ sys.stdout.write("%s: %3.d%% %s" % (tsk.name, int(pcent), bar))
+ # Get keyboard input
+ time.sleep(0.01)
+ for busy in range(5):
+ try:
+ c = sys.stdin.read(1)
+ break
+ except IOError: pass
+ if c in ('q', 'Q', chr(27)):
+ raise KeyboardInterrupt
+ elif c in (' ', 'p', 'P'):
+ if tsk.pause():
+ sys.stdout.write("\r <PAUSED> ")
+ while 1:
+ try:
+ c = sys.stdin.read(1)
+ tsk.run()
+ break
+ except IOError: pass
+ tm = tsk.elapsed_time()
+ ts = format_time(tm)
+ tsk.wait()
+ sys.stdout.write("\r ")
+ if tsk.status() == task.DONE:
+ if ts:
+ print "\r%s: Done. %s elapsed." % (tsk.name, ts)
+ else:
+ print "\r%s: Done." % tsk.name
+ tsk = tsk.next()
+ else:
+ print "\r%s: Failed. Cleaning up." % tsk.name
+ while tsk:
+ print "Undo: %s" % tsk.name
+ tsk = tsk.prev()
+ except KeyboardInterrupt:
+ tsk.stop()
+ print "Stopping..."
+ while tsk:
+ print "Undo: %s" % tsk.name
+ tsk = tsk.prev()
+ sys.exit(1)
+ pass
+ else:
+ opts.log(1, "Out of space. Exiting.")
+ break
+
+ pl.rm_track(trk)
+ if trk() == track.DONE:
+ good_ones += 1
+ else:
+ bad_ones += 1
+ tt = time.time() - track_start
+ ts = format_time(tt)
+ if ts:
+ print "Total time this track: %s\n" % ts
+ else:
+ print
+
+ tm = time.time() - start_time
+ ts = format_time(tm)
+ if bad_ones:
+ if bad_ones == 1:
+ bad_str = ", 1 track failed."
+ else:
+ bad_str = ", %d tracks failed."
+ else:
+ bad_str = "."
+ #bytes = format_bytes(pl.produced_bytes)
+ print "\n%d tracks done%s %s total time elapsed." % \
+ (good_ones, bad_str, ts)
+
+ finally:
+ # Restore the terminal:
+ termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
+ 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
+ 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()
+if __name__ == "__main__": main(sys.argv)
file:a708ccb1278a3da389d3df8196dd8571bfed2590 -> file:7757d29fc0be10e0a10d411f967a75c64c329817
--- a/mp3togo/options.py
+++ b/mp3togo/options.py
@@ -33,13 +33,14 @@
disable reading in config files, set readconf=False in
the constructor."""
-version = "0.2.1"
+
import sys
import os
import UserDict
import getopt
-
+#import dummy_threading as threading
+import threading
class Fail(Exception):
def __init__(self, msg):
@@ -55,8 +56,12 @@ makes it available to the running progra
interface.
"""
def __init__(self, config_file):
+ # The lock
+ self._mutex = threading.Lock()
+
# The name of the program
- self._name = ''
+ self.name = ''
+ self.version = ''
# The option dictionary
self._d = {}
@@ -85,19 +90,36 @@ interface.
self._nosave_options = ['saveconffile', 'conffile', 'arg_files', 'help']
+ def _lock(self):
+ self._mutex.acquire()
+
+ def _unlock(self):
+ self._mutex.release()
+
+ def _locked(self):
+ return self._mutex.locked()
+
def getconf(self, argv=None, conffile=None, readconf=True):
+ self._lock()
+
#Do any pre configuration chores
- self._pre_hook()
+ try:
+ self._pre_hook()
+ except:
+ self._unlock()
+ raise
self._readconf = readconf
#Default values:
+ self._unlock()
for i in self._set:
- self[i[0]] = i[3]
+ self[i[0]] = i[3] # hooks run
+ self._lock()
#Read comand line options
olist, loose = {}, []
if argv:
- self._name = os.path.basename(argv[0])
+ self.name = os.path.basename(argv[0])
argv = argv[1:]
sopts = ''
lopts = []
@@ -114,6 +136,7 @@ interface.
try:
olist, loose = getopt.getopt(argv, sopts, lopts)
except getopt.GetoptError:
+ self._unlock()
raise Fail, "Bad arguments."
if '--no-config-file' in olist:
self._readconf = False
@@ -122,7 +145,9 @@ interface.
if conffile:
self._d['conffile'] = conffile
oldconf = self._d['conffile']
+ self._unlock()
self.load_config_file()
+ self._lock()
#Sort out loose files from the command line
if loose:
@@ -132,9 +157,10 @@ interface.
absf = os.path.abspath(absf)
if os.path.exists(absf):
files.append(absf)
- self['arg_files'] = files
+ self._d['arg_files'] = files
#Process options.
+ self._unlock() # may execute a hook function
if olist:
for opt, val in olist:
opt = opt.strip('-')
@@ -156,22 +182,26 @@ interface.
break
#Offer help, if asked:
+ #unlocked
if self.opt('help'):
print self.usage()
sys.exit(0)
#Save the conf file and exit if so instructed:
+ #unlocked
if self.opt('saveconffile'):
- self.log(1, "Saving config to '%s'" % self['conffile'])
+ self.log(1, "Saving config to '%s'" % self._d['conffile'])
self.save_config_file()
self.log(1, "Exiting.")
sys.exit(0)
#Read in new config file if a new one has been specified:
+ #unlocked
if self.opt('conffile') != oldconf:
self.load_config_file()
#Check for binaries:
+ self._lock()
search = os.environ['PATH'].split(':')
for bin in self.bin.keys():
if not self.bin[bin]:
@@ -186,21 +216,26 @@ interface.
#Do any post configuration chores
self._post_hook()
+ self._unlock()
def save_config_file(self):
if self.opt('conffile') and self._readconf:
+ self._lock()
try:
- fp = file(self['conffile'], 'w')
- for key in self.keys():
+ fp = file(self._d['conffile'], 'w')
+ for key in self._d.keys():
if key in self._nosave_options:
continue
- fp.write("%s=%s\n" % (key, self[key]))
+ fp.write("%s=%s\n" % (key, self._d[key]))
fp.close()
except IOError:
self.log(1, "Failed to save config file")
+ self._unlock()
def load_config_file(self):
"""Load the config file specified by the 'conffile' option, if it exists."""
+ # All state access through __getitem__ and __setitem__
+ # Don't lock, except to directly manipulate state
if self.opt('conffile') and \
os.path.exists(os.path.expanduser(self['conffile'])) and self._readconf:
filename = os.path.expanduser(self['conffile'])
@@ -230,20 +265,28 @@ interface.
self[key] = False
else:
self[key] = True
+ else:
+ self.log(2, "Unknown entry in config file: %s" % key)
except:
self.log(1, "Error reading config file")
def opt(self, name):
+ self._lock()
if self._d.has_key(name):
- return self._d[name]
+ out = self._d[name]
else:
- return None
+ out = None
+ self._unlock()
+ return out
def __getitem__(self, name):
+ self._lock()
if self._d.has_key(name):
+ self._unlock()
return self._d[name]
else:
- raise IndexError
+ self._unlock()
+ raise KeyError
def __setitem__(self, name, value):
"""Set value if key exists and type is correct."""
@@ -252,14 +295,23 @@ interface.
if type(i[3]) != type(value):
raise ValueError
if i[4]:
- apply(i[4], [value])
+ try:
+ self._lock()
+ apply(i[4], [value])
+ finally:
+ self._unlock()
else:
+ self._lock()
self._d[name] = value
+ self._unlock()
return
raise IndexError
def keys(self):
- return self._d.keys()
+ self._lock()
+ out = self._d.keys()
+ self._unlock()
+ return out
def __delitem__(self, item):
"""conf items shouldn't be deleted. Object methods shouldn't
@@ -270,15 +322,16 @@ interface.
def log(self, level, msg):
"""Print a log message. Level must be at least 1. If the level
is below the current verbosity level, nothing happens."""
+ # Don't lock
if level < 1:
raise Fail, "log() called with insufficiently high level."
- if level <= self['verbosity']:
+ if level <= self._d['verbosity']:
print >>sys.stderr, msg
def usage(self):
"""Returns a pretty printed usage message with the available
command line options in a string."""
- msg = "%s version %s\n" % (self._name, version)
+ msg = "%s version %s\n" % (self.name, self.version)
msg += self._help_preamble
msg += "\nOptions:\n"
for option in self._set:
@@ -286,7 +339,7 @@ interface.
if option[3] is not False:
if option [1]:
line = " -%s %s" % (option[1], option[3])
- if option[1] and option[2]:
+ if option[2]:
line += ", "
if option[2]:
line += " --%s %s" % (option[2], option[3])
@@ -300,20 +353,23 @@ interface.
if option[2]:
line += " --%s" % option[2]
msg += line + "\n"
- if option[5]:
- desc = option[5].replace('\n', ' ').split()
- while len(desc):
- line = " "
- while 1:
- if len(line) + len(desc[0]) > 60:
- msg += line + "\n"
- break
- else:
- line += " " + desc[0]
- del desc[0]
- if len(desc) == 0:
- msg += line + "\n\n"
+ line = ""
+ if option[5]:
+ desc = option[5].replace('\n', ' ').split()
+ while len(desc):
+ line = " "
+ while 1:
+ if len(line) + len(desc[0]) > 60:
+ msg += line + "\n"
+ line = ""
break
+ else:
+ line += " " + desc[0]
+ del desc[0]
+ if len(desc) == 0:
+ msg += line + "\n\n"
+ line = ""
+ break
return msg
def manpage(self):
file:4ad7bfc70b8e2708f3c2b71cca6fd21bf3255848 -> file:bd05804c94c554cf979f16acb878bfe7748b396e
--- a/setup.py
+++ b/setup.py
@@ -22,8 +22,21 @@ import sys, os
from distutils.core import setup
+#Sort out the version number from the containing directory
release = os.path.basename(os.getcwd()).split('-')[1]
-
+fp = file("mp3togo/__init__.py", 'r')
+inlines = fp.readlines()
+fp.close()
+outlines = []
+for line in inlines:
+ if line.startswith('version = '):
+ outlines.append("version = '%s'\n" % release)
+ else:
+ outlines.append(line)
+fp = file("mp3togo/__init__.py", 'w')
+fp.writelines(outlines)
+fp.close()
+
# patch distutils if it can't cope with the "classifiers" or
# "download_url" keywords
if sys.version < '2.2.3':