#!/usr/bin/env python
### jack - extract audio from a CD and MP3ify it using 3rd party software
### Copyright (C) 1999, 2000  Arne Zellentin <arne@unix-ag.org>

### 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

### If you want to comment on this program, contact me: arne@unix-ag.org ###
### Visit the homepage: http://www.home.unix-ag.org/arne/jack/

### see CHANGELOG for recent changes in this program
### see TODO if you want to see what needs to be implemented

prog_version = "2.99.0"
prog_name = "jack"
DEBUG = 0

from string import atoi, atof, split, find, replace, upper, strip, rstrip
from os import nice, remove, popen, fdopen, waitpid, WNOHANG, execlp, execvp
from os import stat, environ, uname, fork, rename, system, kill, chdir, mkdir
from os import rmdir, path, execv
import os, sys, posix, string
from sys import argv, exit, stdin
from time import time, sleep, localtime, strftime
from stat import ST_SIZE
from array import array
from sndhdr import whathdr
from urllib import urlopen, quote_plus
from select import select
from fcntl import fcntl, ioctl
import FCNTL
from termios import tcgetattr, tcsetattr
import TERMIOS
import signal
import pty
import wave
import types
import traceback
from pprint import pprint
from ID3 import ID3
from jack_TOC import TOC
from jack_TOCentry import TOCentry
from jack_CDTime import CDTime
from jack_misc import multi_replace

desperately_seeking_curses = 0

try:
    import curses
    curses_enable = 1
    if not 'newpad' in dir(curses) or not 'resizeterm' in dir(curses):
        print "You need the alternate (n)curses module, install the supplied one!"
        curses_enable = 0
except ImportError:
    curses_enable = 0

if DEBUG: curses_enable = 0

if curses_enable:
    tiocgwinsz_found = 0
    try:
        from IOCTLS import TIOCGWINSZ
        tiocgwinsz_found = 1
    except ImportError:
        try:
            from TERMIOS import TIOCGWINSZ
            tiocgwinsz_found = 1
        except ImportError:
            pass
    if not tiocgwinsz_found:
        if 'desperately_seeking_curses' in dir() and desperately_seeking_curses:
            TIOCGWINSZ = 0x5413 # linux, ix86. Anyone else?
        else:
            print "Warning: could not find a module which exports TIOCGWINSZ."
            print "       : Use Tools/scripts/h2py.py from the Python source distribution to"
            print "       : convert /usr/include/asm/ioctls.h to IOCTLS.py and install it."
            print "       : If you're hardcore, try setting desperately_seeking_curses = 1"
            print "       : in jack. No. Don't."
            sleep(1)
            curses_enable = 0

try:
    from os import statvfs
    statvfs_found = 1
except ImportError:
    print "Info: os.statvfs not found, using builtin (base on df)"
    statvfs_found = 0

prefs_file = environ['HOME'] + "/.jackrc"

### don't edit things here (unless administering), run jack once and edit ###
### your prefs_file instead.                                              ###

prefs = """
## edit these defaults ##
my_mail = "@"           # needed for e-mail submissions, please set
ripper = "cdparanoia"   # use which program to rip: cdparanoia, tosha, cdda2wav,                        # dagrab (untested)
cd_device = "/dev/cdrom"    # use which device for ripping
                            # note: cdda2wav needs something like "/dev/sgb"
keep_free = 5*2**20     # stop if less than keep_free bytes are free
                        # don't set this to zero as mp3 size prediction is
                        # always a bit below actual sizes

#   %n: track number
#   %a: artist
#   %t: track title
#   %l: album title

rename_fmt = "%n.%a (%l) - %t"# MP3s are renamed to
                                # "01.Artist (Album) - Tracktitle.mp3"
rename_fmt_va = "%n.%a - %t"  # ditto for Various Artists CDs
                                # note that a '%' in freedb data may lead to
                                # problems - if it does, please mail me so I
                                # can think of a better way of doing this
rename_num = "%02i"             # numbering format, used for %n above, e.g.
                                # %02i with leading '0': 01, 02, ... , 11, ...
rename_dir = 1                  # rename directory as well:
dir_template = "%a/%l"          # if create_dirs is set and freedb data is
                                # available, files are put in
                                # base_dir + "/<artist>/<albumname>".
unusable_chars = ""             # put chars which can't be used in filenames
                                # here and their
replacement_chars = ""          # replacements here. replacement_chars is
                                # automagically stretched to match
                                # unusable_chars' length using the last char
                                # as fill

# example 1: replace all " " by "_":
# unusable_chars = " "
# replacement_chars = "_"

# example 2: replace umlauts by an alternate representation and kill some
#             special characters:
# unusable_chars = "äöüÄÖÜß?*^()[]{}"
# replacement_chars = ["ae", "oe", "ue", "Ae", "Oe", "Ue", "ss", ""]

recurse_dirs = 2                # search how deep for workdir
#searchdirs = ["/opt/mp3", "."] # where to start when searching for workdir
searchdirs = [os.curdir]        # the default is the current directory (".")
                                # if recurse_dirs > 0
base_dir = os.curdir            # where to create the subdir(s) (".")

update_interval = 1.0           # update status screen every ... seconds
max_load = 10                   # only start new encoders when load < max_load

## xtermset related stuff - obsoleted by the curses mode
xtermset_enable = 0             # disable if you don't have xtermset installed
restore_xterm_width = 0         # so you can still read all output
default_width = 80              # your xterm's geometry
default_height = 24             # these are autodetected when using curses
show_time = 0       # display track length instead of "track"
show_names = 0      # set this to 1 to display freedb track names instead
                    # "track_01", ... This will not fit in 80x24 term's

## more defaults, these can be changed via argv ##
encoder = "lame"    # use which encoder (bladeenc, lame, gogo, l3enc, mp3enc)
                    # NEW: xing - completely untested
bitrate = 160       # default bitrate
encoders = 1        # encode how many mp3s in parallel
vbr = 0             # use variable bitrate for encoders which support it
otf = 0             # on-the-fly operation with supported helper apps
create_dirs = 1     # create (and name if applicable) subdir(s)
force = 0           # general Override
reorder = 0         # reorder tracks to save space while encoding
keep_wavs = 0       # keep encoded WAVs
only_dae = 0        # only rip, do not code, implies keep_wavs
read_ahead = 99     # number of tracks to read in advance
nice_value = 12     # what nice level (priority) encoders get
recheck_space = 1   # yes we want to react to disc space dropping.
overwrite = 0       # overwrite existing mp3s
various = 0         # assume CD has various artists, 0 = auto
various_swap = 0    # exchange artist and title for broken freedb entrys
remove_files = 0    # remove jack.* files when done
silent_mode = 0     # silent-mode means: no output to screen

## Things to do when finished
exec_when_done = 0  # execute below commands when finished, default: no
                    # this is command line controllable
exec_rip_done = "eject " + cd_device # eject the CD when ripping is finished
exec_no_err = "wavplay /usr/local/audio/allok.wav"  # play sound when finished
exec_err = "wavplay /usr/local/audio/error.wav"     # this is played when an
                                                    # error occured

                        # there are two ways of freedb operation:
query_on_start = 0      # do query when starting, rename files when finished
query_when_ready = 0    # do query when done with encoding, rename files
freedb_submit = 0       # use this to submit freedb file (after editing)
freedb_mailsubmit = 0   # submit entry by e-mail
freedb_server = "freedb.freedb.org"      # which freedb server to use
freedb_mail = "freedb-submit@freedb.org" # where to send e-mail submission to
freedb_dir = ""         # change this to something like "/var/spool/cddb" and
                        # all queries will be done in this (local) directory
                        # later I will allow failed local queries to be
                        # done via network

## no need to go below this line ##
swap_byteorder = 1  # swap byteorder when reading from image
todo_exit = 0       # print what would be done and exit
space_from_argv = 0 # use currently free discspace (or override via argv)
check_toc = 0       # compare toc-file with cd-toc and exit
undo_rename = 0     # undo file renaming and exit
dont_work = 0       # use this to avoid actual work, good for query only
tlist = []          # tracks to encode, empty = use all tracks

## Misc stuff ##
name = "track_%02i" # filename template (before renaming)
rippers = 1         # not implemented: rip in parallel
toc_prog = ripper   # use which program to read cd's toc
                    # ripper means same as for ripping

## FREEDB stuff starts here: ##

read_freedb_file = 0    # read freedb file, e.g. to
freedb_rename = 0       #                    rename tracks accordingly
set_id3tag = 0          #                    and to set id3 tag info
gen_freedb_form = 1     # yes, generate a freedb submission template
if environ.has_key('USER'):
  username = environ['USER']
elif environ.has_key('LOGNAME'):
  username = environ['LOGNAME'] # this and
hostname = uname()[1]           # that is required for freedb query

# better not touch these: #
if environ.has_key("OSTYPE"):   # some things can be done faster on Linux
  Linux = environ['OSTYPE'] == "Linux"
else:
  Linux = 0
if Linux:
  loadavg = "get_sysload_linux_proc()"  # this is eval()'d to get the sysload
else:
  loadavg = "-1"    # tell me how to get sysload on non-Linux systems!
                    # preferably without calling uptime
image_file = ""     # normal operation: DAE from CD, not from image
toc_file = prog_name + ".toc"   # if noexistent, create on startup
def_toc = prog_name + ".toc"    # cache toc here
freedb_form_file = prog_name + ".freedb"    # name of submission template
out_file = prog_name + ".out"   # in silent-mode, stdout goes here
err_file = prog_name + ".err"   # in silent-mode, stderr goes here
progress_file = prog_name + ".progress"     # subprocess output is cached here
progr_sep = "/|\\\\"    # field seperator in progress_file

# supported helper apps
helpers = {}
helpers['builtin'] = {}
helpers['builtin']['status_blocksize'] = 160

helpers['mp3enc'] = {}
helpers['mp3enc']['type'] = "encoder"
helpers['mp3enc']['cmd'] = "mp3enc -v -qual 9 -br %r -if %i -of %o"
helpers['mp3enc']['otf-cmd'] = "mp3enc -v -qual 9 -br %r -sti -be -of %o"
helpers['mp3enc']['status_blocksize'] = 99
helpers['mp3enc']['bitrate_factor'] = 1000
helpers['mp3enc']['status_start'] = "%"
helpers['mp3enc']['percent_fkt'] = \"\"\"
s = split(i['buf'], '\\\\015')
if len(s) >= 4:
  s = s[-2]
  if find(s, "%") >= 0:
    y = split(s, " ", 3)
    percent = atof(y[0]) / (i['track'][LEN] * CDDA_BLOCKSIZE / 2) * 100.0
  else:
    percent = 0
\"\"\"

helpers['l3enc'] = {}
helpers['l3enc']['type'] = "encoder"
helpers['l3enc']['cmd'] = "l3enc -hq -br %r %i %o"
helpers['l3enc']['status_blocksize'] = 99
helpers['l3enc']['bitrate_factor'] = 1000
helpers['l3enc']['status_start'] = "%"
helpers['l3enc']['percent_fkt'] = \"\"\"
s = split(i['buf'], '\\\\015')
if len(s) >= 2: s = s[-2]
if len(s) == 1: s = s[0]
if find(s, "%") >= 0:
  y = split(s, " / ")
  y0 = split(y[0])[-1]
  y1 = split(y[1])[0]
  percent=atof(y0) / atof(y1) * 100.0
else:
  percent = 0
\"\"\"

helpers['lame'] = {}
helpers['lame']['type'] = "encoder"
helpers['lame']['cmd'] = "lame -h -b %r %i %o"
helpers['lame']['vbr-cmd'] = "lame -v -V 4 --nohist -h %i %o"
helpers['lame']['otf-cmd'] = "lame -h -b %r - %o"
helpers['lame']['vbr-otf-cmd'] = "lame -v -V 4 --nohist -h - %o"

# from lame:     -V n    quality setting for VBR.  default n=4
#                        0=high quality,bigger files. 9=smaller files

helpers['lame']['status_blocksize'] = 160
helpers['lame']['bitrate_factor'] = 1
helpers['lame']['status_start'] = "%"
helpers['lame']['percent_fkt'] = \"\"\"
s = split(i['buf'], '\\\\015')
if len(s) >= 2: s=s[-2]
if len(s) == 1: s=s[0]
if find(s, "%") >= 0:       # status reporting starts here
  y = split(s, "/")
  y1 = split(y[1], "(")[0]
  percent = atof(y[0]) / atof(y1) * 100.0
elif find(s, "Frame:") >= 0:    # older versions, like 3.13
  y = split(s, "/")
  y0 = split(y[0], "[")[-1]
  y1 = split(y[1], "]")[0]
  percent = atof(y0) / atof(y1) * 100.0
else:
  percent = 0
\"\"\"

helpers['gogo'] = {}
helpers['gogo']['type'] = "encoder"
helpers['gogo']['cmd'] = "gogo %i %o -b %r"
helpers['gogo']['status_blocksize'] = 160
helpers['gogo']['bitrate_factor'] = 1
helpers['gogo']['status_start'] = "%"
helpers['gogo']['percent_fkt'] = \"\"\"
s = split(i['buf'], '\\\\015')
if len(s) >= 2: s=s[-2]
if len(s) == 1: s=s[0]
if find(s, "%") >= 0:		# status reporting starts here
  y = split(s, "/")
  y0 = split(y[0], "{")[-1]
  y1 = split(y[1], "}")[0]
  percent = atof(y0) / atof(y1) * 100.0
else:
  percent = 0
\"\"\"

helpers['bladeenc'] = {}
helpers['bladeenc']['type'] = "encoder"
helpers['bladeenc']['cmd'] = "bladeenc %i %o -br %r"
helpers['bladeenc']['status_blocksize'] = 180
helpers['bladeenc']['bitrate_factor'] = 1000
helpers['bladeenc']['status_start'] = "%"
helpers['bladeenc']['percent_fkt'] = \"\"\"
s = split(i['buf'], '\\\\015')
if len(s) >= 2: s=s[-2]
if find(s, "Status:") != -1:
  y = split(s[8:])
  percent = atof(split(y[0], '%')[0])
else:
  percent = 0
\"\"\"

helpers['cdparanoia'] = {}
helpers['cdparanoia']['type'] = "ripper"
helpers['cdparanoia']['cmd'] = "cdparanoia -d %d %n %o"
helpers['cdparanoia']['otf-cmd'] = "cdparanoia -e -d %d %n -R -"
helpers['cdparanoia']['status_blocksize'] = 500
helpers['cdparanoia']['status_start'] = "%"
helpers['cdparanoia']['status_fkt'] = \"\"\"
new_status = split(i['buf'], '\\\\015')[-2][17:68]
\"\"\"
helpers['cdparanoia']['otf-status_fkt'] = \"\"\"
buf = i['buf']
tmp = split(buf, "\\\\012")
new_status = ""
if len(tmp) >= 2:
    tmp = split(tmp[-2], " @ ")
    if tmp[0] == "##: -2 [wrote]":
        percent = (atof(tmp[1]) - (i['track'][START] * CDDA_BLOCKSIZE / 2.0)) / (i['track'][LEN] * CDDA_BLOCKSIZE / 2.0) * 100.0
        new_status = "[otf - reading, %2i%%]" % percent
\"\"\"
helpers['cdparanoia']['final_status_fkt'] = \"\"\"
for tmp in split(exited_proc['buf'], '\\\\015'):
  if find(tmp, "PROGRESS") != -1:
    last_status = tmp
final_status = ("%4.1fx" % speed) + last_status[16:48] + "]"
\"\"\"
helpers['cdparanoia']['otf-final_status_fkt'] = \"\"\"
final_status = "[otf - done]"
\"\"\"
helpers['cdparanoia']['toc'] = 1
helpers['cdparanoia']['toc_cmd'] = "cdparanoia -d %d -Q 2>&1"
helpers['cdparanoia']['toc_fkt'] = \"\"\"
while l:
  l = rstrip(l)
  if l and l[0:5] == "TOTAL":
    start = 0
  if l and l == '=' * (len(l)):
    start = 1
  elif l and start:
    l = split(l, '.', 1)
    num = atoi(l[0])
    l = split(l[1])
    erg.append([num, atoi(l[0]), atoi(l[2]), l[4] == 'OK', l[5] == 'yes', atoi(l[6]), 1, bitrate, name % num])
  l = p.readline()
\"\"\"

helpers['cdda2wav'] = {}
helpers['cdda2wav']['type'] = "ripper"
helpers['cdda2wav']['cmd'] = "cdda2wav -H -v 1 -D %d -O wav -t %n %o"
helpers['cdda2wav']['status_blocksize'] = 200
helpers['cdda2wav']['status_start'] = "percent_done:"
helpers['cdda2wav']['status_fkt'] = \"\"\"
x = split(i['buf'], '\\\\015')[-2]
if find(x, '%') != -1:
  new_status = "ripping: " + strip(split(i['buf'], '\\\\015')[-2])
else:
  new_status = "waiting..."
\"\"\"
helpers['cdda2wav']['final_status_fkt'] = \"\"\"
final_status = ("%5.1f" % speed) + "x [DAE done]"
\"\"\"
helpers['cdda2wav']['toc'] = 1
helpers['cdda2wav']['toc_cmd'] = "cdda2wav -D %d -J -v 1 2>&1"
helpers['cdda2wav']['toc_fkt'] = \"\"\"
while l:
  l = strip(l)
  if l and l[0:11] == "Album title":
    start = 1
  elif l and start:
    l = split(l)
    if l[0] == "Leadout:":
      start = 0
    else:
      num = atoi(l[0][1:3])
      if l[6] == "stereo":
        channels = 2
      elif l[6] == "mono":
        channels = 1
      else:
        channels = 0
      t_start = atoi(l[1])
      msf = split(l[2], ":")
      sf = split(msf[1], ".")
      t_length = atoi(sf[1]) + atoi(sf[0]) * 75 + atoi(msf[0]) * 60 * 75
      erg.append([num, t_length, t_start, l[5] == "copyallowed", l[4] != "linear", channels, 1, bitrate, name % num])
  l = p.readline()
\"\"\"

helpers['dagrab'] = {}
helpers['dagrab']['type'] = "ripper"
helpers['dagrab']['cmd'] = "dagrab -d %d -f %o %n"
helpers['dagrab']['status_blocksize'] = 100
helpers['dagrab']['status_start'] = "total:"
helpers['dagrab']['status_fkt'] = \"\"\"
x = split(i['buf'], '\\\\015')[-2]
if find(x, 'total:') != -1:
  new_status = strip(split(i['buf'], '\\\\015')[-2])
else:
  new_status = "waiting..."
\"\"\"
helpers['dagrab']['final_status_fkt'] = \"\"\"
final_status = ("%5.1f" % speed) + "x [DAE done]"
\"\"\"
helpers['dagrab']['toc'] = 1
helpers['dagrab']['toc_cmd'] = "dagrab -d %d -i 2>&1"
helpers['dagrab']['toc_fkt'] = \"\"\"
while l:
  l = strip(l)
  if l and l[0:5] == "track":
    start = 1
  elif l and start:
    l = split(l)
    if l[3] == "leadout":
      start = 0
    else:
      num = atoi(l[0])
      channels = 2
      copy = 0
      pre = 0
      t_start = atoi(l[1]) - 150
      t_length = atoi(l[2])
      erg.append([num, t_length, t_start, copy, pre, channels, 1, bitrate, name % num])
  l = p.readline()
\"\"\"

helpers['tosha'] = {}
helpers['tosha']['type'] = "ripper"
helpers['tosha']['cmd'] = "tosha -d %d -f wav -t %n -o %o"
helpers['tosha']['status_blocksize'] = 100
helpers['tosha']['status_start'] = "total:"
helpers['tosha']['status_fkt'] = \"\"\"
x = split(i['buf'], '\\\\015')[-2]
if find(x, 'total:') != -1:
  new_status = strip(split(i['buf'], '\\\\015')[-2])
else:
  new_status = "waiting..."
\"\"\"
helpers['tosha']['final_status_fkt'] = \"\"\"
final_status = ("%5.1f" % speed) + "x [DAE done]"
\"\"\"
helpers['tosha']['toc'] = 1
helpers['tosha']['toc_cmd'] = "tosha -d %d -iq 2>&1"
helpers['tosha']['toc_fkt'] = \"\"\"
while l:
  l = rstrip(l)
  if l:
    l = split(l)
    num = atoi(l[0])
    erg.append([num, 1 + atoi(l[3]) - atoi(l[2]), atoi(l[2]), 0, 0, 2, 1, bitrate, name % num])
  l = p.readline()
\"\"\"

# xing  definitions kindly provided by Sebastian Weber
helpers['xing'] = {}
helpers['xing']['type'] = "encoder"
helpers['xing']['cmd'] = "xingmp3enc -B %r %i %o"
helpers['xing']['vbr-cmd'] = "xingmp3enc -V 100 %i %o"
helpers['xing']['otf-cmd'] = "xingmp3enc -b %r -- %o"
helpers['xing']['vbr-otf-cmd'] = "xingmp3enc -V 100 -- %o"
helpers['xing']['status_blocksize'] = 160
helpers['xing']['bitrate_factor'] = 1
helpers['xing']['status_start'] = "%"
helpers['xing']['percent_fkt'] = \"\"\"
s = split(i['buf'], '\\\\015')
if len(s) >= 2: s=s[-2]
if find(s, "ETA:") != -1:
y = strip(split(s, '%')[0])
if len(y) == 0:
percent = 0
else:
percent = atof(y)
else:
percent = 0
\"\"\"
"""

# read the defaults
exec(prefs)
if path.exists(prefs_file):
    execfile(prefs_file)
    try:
        if jackrc_version != prog_version:
            print "Warning: Your " + path.basename(prefs_file) + "'s version (" + jackrc_version + ") doesn't match this version"
            print "       : of jack (" + prog_version + ")."
            print "       : make sure that nothing important is missing! (sleeping 5s)"
            bla, bla, bla = select([sys.stdin], [], [], 5)
            sys.stdin.flush()
    except NameError:
        print "Warning: Your " + path.basename(prefs_file) + "'s version (unknown, too old) doesn't match"
        print "       : this version of jack (" + prog_version + ")."
        print "       : make sure that nothing important is missing! (sleeping 5s)"
        bla, bla, bla = select([sys.stdin], [], [], 5)
    sys.stdin.flush()

else:
    if not environ.has_key('http_proxy'):
        print "Warning: http_proxy is not set, you probably want to set it."
    print 'Info: no preferences "' + prefs_file + '" found, creating a new one...'
    f = open(prefs_file, "w")
    f.write("# preferences for " + prog_name + "-" + prog_version + "\n")
    f.write("# remove this file to get a new one filled with the defaults.\n")
    f.write("# This file is parsed as python code, it's easy to break things.\n\n")
    f.write("jackrc_version = \"" + prog_version + "\"\n\n")
    f.write(prefs)
    f.close()
    print "    : done. now edit this file and start " + prog_name + " again."
    exit()

# operating tracks... modes that is.
multi_mode = 0      # try to query freedb for all dirs in searchdirs
                        # which have no freedb data

# globals
dae_queue = []      # This stores the tracks to rip
enc_queue = []      # WAVs go here to get some codin'
enc_running = 0     # what is going on?
dae_running = 0     # what is going on?
enc_status = {}     # status messages are stored here
enc_cache = {}      # sometimes status messages are stored here
dae_status = {}     # status messages are stored here
children = []       # subprocess info
dir_created = None  # dirs are only renamed if we have created them
claim_dir = 0       # use (i.e. rename) the current dir. ARGV
scan_dirs = None    # to be overridden by argv
upd_progress = 0    # regenerate progress file if "lost"
progress_changed = 0# nothing written to progress, yet
global_error = 0    # remember if something went wrong
global_options = None   # a status line
global_discname = None  # a status line
special_line = None # a status line
bottom_line = None  # a status line
extra_lines = None  # number of status lines actually displayed
printable_names = None  # these are displayed for the track names
names_available = 0 # freedb info is available
all_tracks_todo_sorted = [] # well...
xterm_geom_changed = 0      # only restore xterm size if it was changed
width = default_width       # if we need to restore xterm
height = default_height     # if we need to restore xterm
old_tc = tcgetattr(stdin.fileno())  # terminal attributes

# curses related globals
curses_init = 0             # initscr has been called
max_y = max_x = None            # screen dimensions
stdscr = status_pad = usage_win = None  # screen objects
curses_sighandler = None        # not used
map_track_num = None            # list track number -> line number
pad_x = pad_y = pad_start_y = pad_start_x = pad_end_y = pad_end_x = None
pad_height = pad_width = None
pad_disp_start_y = pad_disp_start_x = 0
usage_win_y = usage_win_x = 0
usage_win_height, usage_win_width = 7, 49

# track representation format
# [ track#, len, start, copy, pre, ch, unused, bitrate, filename ]
NUM, LEN, START, COPY, PRE, CH, RIP, RATE, NAME = 0, 1, 2, 3, 4, 5, 6, 7, 8
# LEN and START is in cdda-blocks:
CDDA_BLOCKSIZE = 2352
# more constants
CDDA_BLOCKS_PER_SECOND = 75
MSF_OFFSET = 150
CHILD = 0
CDDA_MAXTRACKS = 100
STDIN_FILENO = 0
STDOUT_FILENO = 1
STDERR_FILENO = 2

# compile exec strings #XXX doesn't work anymore
#for h in helpers.keys():
#    for i in helpers[h].keys():
#        if i[-4:] == "_fkt":
#            pass
#            helpers[h][i] = compile(helpers[h][i], '<string>', 'exec')

#################################################################### functions


def local_freedb(id, freedb_dir, outfile = "/tmp/testfilefreedb"):
    "Use file from local freedb directory"
    # Moritz Moeller-Herrmann kindly provided this functionality.
    DEBUG=0
    if not path.isdir(freedb_dir):
        print "Error: freedb directory not found"
        sys.exit(1)
    if not os.access(freedb_dir, 5):
        print "Error: freedb directory access not permitted"
        sys.exit(1)
    cat=[] # category listing
    for entry in os.listdir(freedb_dir):
        if path.isdir(path.join(freedb_dir, entry)):
            cat.append(path.join(freedb_dir, entry))
    if DEBUG: print cat
    for musicdir in cat:
        for m in os.listdir(musicdir):
            if m == id:
                idfile = path.join(musicdir, id)
                inf = open (idfile, "r")
                outf = open (outfile, "w")
                buf = inf.readline()
                while buf:
                    buf = replace(buf, "\n", "")    # we need trailing spaces
                    if DEBUG: print buf
                    if buf != ".":
                        outf.write(buf + "\n")
                    buf = inf.readline()
                inf.close()
                outf.close()
                return 0
    sys.exit("No local matching freedb entry found")
    return 1

def df_statvfs(fs = "."):
    "returns free space on a filesystem (in bytes)"
    (f_bsize, f_frsize, f_blocks, f_bfree, f_bavail, f_files, f_ffree, f_favail, f_flag, f_namemax) = statvfs(fs)
    return long(f_bavail) * long(f_bsize)

def df_df(fs = ".", blocksize = 1024):
    "Uses real df to determine avail. discspace. Not very portable."
    p = popen("df " + fs)
    s = split(rstrip(p.readline()))
    for i in range(len(s)):
        if s[i] == "Available":
            s = split(rstrip(p.readline()))
            return atoi(s[i]) * long(blocksize) - long(keep_free)
    p.close()

def mp3format(file, warn = 1, max_skip = 100000):
    "get mp3 info. Transformed from mp3info (c) Thorvald Natvig"
    mode_names = ["stereo", "j-stereo", "dual-ch", "single-ch", "multi-ch"]
    layer_names = ["I", "II", "III"]
    version_names = ["MPEG-1", "MPEG-2 LSF", "MPEG-2.5"]
    version_nums = ["1", "2", "2.5"]
    bitrates = \
        [[[ 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],\
            [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],\
            [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]],\
         [[0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],\
            [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],\
            [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]],\
         [[0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],\
            [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],\
            [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]]]
    s_freq = \
        [[44100, 48000, 32000, 0],\
         [22050, 24000, 16000, 0],\
         [11025, 8000, 8000, 0]]

    f = open(file, "r")
    buffer = f.read(4)
    tmp = ((ord(buffer[0]) << 4) & 0xFF0) | ((ord(buffer[1]) >> 4) & 0xE)
    ok = 1
    x={}
    if tmp != 0xFFE:
        ok = 0
        if warn:
            print "Warning: MP3 file \"" + file + "\""
            print "       : doesn't start with MP3 header, skipping..."
        for i in range(max_skip):
             buffer = (buffer + f.read(1))[-4:]
             if buffer == "RIFF":
                 if warn: print "       : ... RIFF header..."
             tmp = ((ord(buffer[0]) << 4) & 0xFF0) | ((ord(buffer[1]) >> 4) & 0xE)
             if tmp == 0xFFE:
                 ok = 1
                 if warn: print "Warning: MP3 header found at offset", i
                 break
    if not ok:
        if warn: print "Warning: no MP3 header found in the first", max_skip, "bytes of file\n       : \"" + file + "\"."
    else:
        switch = (ord(buffer[1]) >> 3 & 0x3)
        if switch == 3: version = 0
        elif switch == 2: version = 1
        elif switch == 0: version = 2
        else:
            ok = 0
        if ok:
            x['lay'] = 4 - ((ord(buffer[1]) >> 1) & 0x3)
            x['error_protection'] = not (ord(buffer[1]) & 0x1)
            x['bitrate_index'] = (ord(buffer[2]) >> 4) & 0x0F
            x['sampling_frequency'] = (ord(buffer[2]) >> 2) & 0x3
            x['padding'] = (ord(buffer[2]) >> 1) & 0x01
            x['extension'] = ord(buffer[2]) & 0x01
            x['mode'] = (ord(buffer[3]) >> 6) & 0x3
            x['mode_ext'] = (ord(buffer[3]) >> 4) & 0x03
            x['copyright'] = (ord(buffer[3]) >> 3) & 0x01
            x['original'] = (ord(buffer[3]) >> 2) & 0x1
            x['emphasis'] = (ord(buffer[3])) & 0x3
            x['mode_name'] = mode_names[x['mode']]
            x['layer_name'] = layer_names[x['lay'] - 1]
            x['version_name'] = version_names[version]
            x['version_num'] = version_nums[version]
            x['bitrate'] = bitrates[version][x['lay'] - 1][x['bitrate_index']]
            x['sfreq'] = s_freq[version][x['sampling_frequency']]
            x['length'] = filesize(file) * 8.0 / (x['bitrate'] * 1000)
            #stereo = (mode == MPG_MD_MONO) ? 1 : 2;
    return x

def get_sysload_linux_proc():
    "extract sysload from /proc/loadavg, linux only (?)"
    f = open("/proc/loadavg", "r")
    loadavg = atof(split(f.readline())[0])
    return loadavg

def center_line(str, fill = " ", fill_sep = " ", fill_r = "", width = 80):
    "return str centered, filled with fill chars"
    if curses_enable:
        width = max_x
    free = width - len(str)
    if free >= 2:
        if not fill_r:
            fill_r = fill
        length = len(fill)
        left = free / 2
        right = free / 2 + (free % 2)
        left_c = fill * (left / length) + fill_sep * (left % length)
        right_c = fill_sep * (right % length) + fill_r * (right / length)
        return left_c + str + right_c
    else:
     return str

def pprint_i(num, fmt = "%i%s", scale = 2.0**10, max = 4):
    "return a string describing an int in a more human-readable way"
    c = ""
    change = 0
    for i in ("K", "M", "G", "T"):
        if abs(num) >= scale:
            c = i
            num = num / scale
            change = 1
    if change:
        #num = num + 0.5
        if num > 999:
            return fmt % (num, c)
        elif num >= 100:
            return replace(fmt, "%i", "%s") % (`num`[:3], c)
        else:
            return replace(fmt, "%i", "%s") % (`num`[:4], c)
    else:
        return fmt % (num, c)

def gettoc():
    "Returns track list"
    p = popen(replace(helpers[toc_prog]['toc_cmd'], "%d", cd_device))
    start = 0
    erg = []
    l = p.readline()
    exec(helpers[toc_prog]['toc_fkt'])
    if p.close():
        print "Error: could not read CD's TOC."
        if cd_device:
            try:
                f = open(cd_device, "r")
            except IOError:
                print "     : no read permission for " + cd_device + "."
            else:
                print "     : maybe " + ripper + " is not installed?"
        else:
            print "      : try setting cd_device to your CD device, e.g. /dev/cdrom"
        exit()
    else:
        return erg

def guesstoc(names):
    "Return track list based on guessed lengths"
    num = 1
    start = 0
    erg = []
    for i in names:
        x = mp3format(i)
        if not x:
            print "Error: could not get MP3 info for file \"" + i + "\"."
            exit()
        blocks = int(x['length'] * CDDA_BLOCKS_PER_SECOND + 0.5)
                            # NUM, LEN,        START, COPY, PRE, CH, RIP, RATE                , NAME
        erg.append([num, blocks, start, 0     , 0    , 2 , 1    , x['bitrate'], replace(path.basename(i), ".mp3", "")])
        progress(num, "dae", "[simulated]")
        progress(num, "enc", `x['bitrate']`, "[simulated]")
        progress(num, "ren", name % num + "-->" + replace(path.basename(i), ".mp3", ""))
        num = num + 1
        start = start + blocks
    return erg

#XXX will be moved to jack_convert
def timestrtoblocks(str):
    "convert mm:ss:ff to blocks"
    str = split(str, ":")
    blocks = atoi(str[2])
    blocks = blocks + atoi(str[1]) * CDDA_BLOCKS_PER_SECOND
    blocks = blocks + atoi(str[0]) * 60 * CDDA_BLOCKS_PER_SECOND
    return blocks

B_MM, B_SS, B_FF = 0, 1, 2
def blockstomsf(blocks):
    "convert blocks to mm, ss, ff"
    mm = blocks / 60 / CDDA_BLOCKS_PER_SECOND
    blocks = blocks - mm * 60 * CDDA_BLOCKS_PER_SECOND
    ss = blocks / CDDA_BLOCKS_PER_SECOND
    ff = blocks % CDDA_BLOCKS_PER_SECOND
    return mm, ss, ff, blocks

def starts_with(str, with):
    "checks whether str starts with with"
    return str[0:len(with)] == with

## #XXX the following will be used if all references to it have been updated.
## meanwhile the wrapper below is used.

def real_cdrdao_gettoc(tocfile):     # get toc from cdrdao-style toc-file
    "returns TOC object, needs name of toc-file to read"
    toc = TOC()

    f = open(tocfile, "r")

    tocpath, tocfiledummy = path.split(tocfile)

# a virtual track 0 is introduced which gets all of track 1s pregap.
# it is removed later if it is too small to contain anything interesting.

    actual_track = TOCentry()
    actual_track.number = 0
    actual_track.type = "audio"
    actual_track.channels = 2
    actual_track.media = "image"
    actual_track.start = 0
    actual_track.length = 0
    actual_track.rip = 1
    actual_track.bitrate = bitrate
    actual_track.image_name = ""
    actual_track.rip_name = name % 0

## tocfile data is read in line by line.

    num = 0
    while 1:
        line = f.readline()
        if not line:
            toc.append(actual_track)
            break
        line = strip(line)

## everytime we encounter "TRACK" we increment num and append the actual
## track to the toc.
        
        if starts_with(line, "TRACK "):
            num = num + 1
            new_track = TOCentry()
            new_track.number = num
            if actual_track:
                toc.append(actual_track)
            actual_track = new_track
            actual_track.rip = 1
            actual_track.bitrate = bitrate
            actual_track.start = toc.end_pos
            if line == "TRACK AUDIO":
                actual_track.type = "audio"
            else:
                actual_track.type = "other" # we don't care

## check the various track flags.
## FOUR_CHANNEL_AUDIO is not supported.
## we have to check for this before ripping. later. much later.

        elif line == "NO COPY":
            actual_track.copy = 0
        elif line == "COPY":
            actual_track.copy = 1
        elif line == "NO PRE_EMPHASIS":
            actual_track.preemphasis = 0
        elif line == "PRE_EMPHASIS":
            actual_track.preemphasis = 1
        elif line == "TWO_CHANNEL_AUDIO":
            actual_track.channels = 2
        elif line == "FOUR_CHANNEL_AUDIO":
            actual_track.channels = 4

## example: FILE "data.wav" 08:54:22 04:45:53

        elif starts_with(line, "FILE "):
            filename, start, length = split(line[5:])

## chop off filenames '"'s, convert time string to blocks(int), update info.

            filename = filename[1:-1]
            actual_track.length = CDTime(length).blocks

            actual_track.image_name = path.join(tocpath, filename)
            actual_track.rip_name = name % num

## example: START 00:01:53. This means the actual track starts 1:53s _after_
## the start given by the FILE statement. This so-called pregap needs to be
## added to the length of the previous track, added to the start of the
## actual track and subtracted from its length. This is done automagically
## by setting the pregap attribute.

        elif starts_with(line, "START "):
            actual_track.pregap = CDTime(split(line)[1]).blocks

    f.close()
    return toc


def cdrdao_gettoc(tocfile):     # get toc from cdrdao-style toc-file
    "just a wrapper for real_cdrdao_gettoc."
    toc = real_cdrdao_gettoc(tocfile)
    tracks = toc.export()
    track1_pregap = tracks[0][1]
    multi_file_mode = not toc.same_image()
    use_filename = toc.image_file
    if DEBUG: print "t1_pre:", track1_pregap, "multi:", multi_file_mode,
    if DEBUG: print "toc=", tracks
    return tracks[1:], use_filename, multi_file_mode, track1_pregap


#XXX remove asap
def xxcdrdao_gettoc(tocfile):     # get toc from cdrdao-style toc-file
    "Returns track list and datafile name, needs name of toc-file"
    ### this only supports machine-generated tocfiles as done by cdrdao 1.1.3
    tracks = []
    num = 0
    pregap = 0
    track1_pregap = 0
    f = open(tocfile, "r")
    buf = f.readline()
    multi_file_mode = 0
    while buf:
        buf = rstrip(buf)
        if buf == "TRACK AUDIO":
            if num > 0:
                if pregap:
                    if tracks:
                        tracks[-1][LEN] = tracks[-1][LEN] + pregap
                tracks.append([num, length, start + track1_pregap, copy, pre, ch, 1, bitrate, this_filename])
                pregap = 0
            num = num + 1
        elif buf == "NO COPY":
            copy = 0
        elif buf == "COPY":
            copy = 1
        elif buf == "NO PRE_EMPHASIS":
            pre = 0
        elif buf == "PRE_EMPHASIS":
            pre = 1
        elif buf == "TWO_CHANNEL_AUDIO":
            ch = 2
        elif buf == "FOUR_CHANNEL_AUDIO":
            ch = 4              # this is not supported.
            print "Error: can't do FOUR_CHANNEL_AUDIO, exiting."
            exit()
#
#        elif buf[0:8]=="SILENCE " and num==1 : # this only works for
#                        # automatically (i.e. cdrdao-) generated toc-files
#            track1_pregap=timestrtoblocks(buf[8:])
        elif buf[0:4] == "FILE":
            buf = "", '"' + split(buf, '"', 2)[1] + '"', split(buf)[-2], split(buf)[-1]
            if num == 1:
                filename = buf[1]
                use_filename = filename[1:-1]
                this_filename = use_filename[:-4]
                if not path.exists(use_filename):
                    toc_base = path.basename(tocfile)
                    use_filename = tocfile[:len(tocfile) - len(toc_base)] + filename[1:-1]
                    if not path.exists(use_filename):
                        use_filename = filename[1:-1]
                    this_filename = name % num
                track_1_filename = use_filename
            else:
                if filename != buf[1] or multi_file_mode:
                    filename = buf[1]
                    if multi_file_mode == 0:
                        if track_1_filename[-4:] != ".wav":
                            print 'Error: only ".wav"-files are allowed, aborting.'
                            exit()
                        else:
                            track_1_filename = track_1_filename[:-4]
                    multi_file_mode = 1
                    this_filename = buf[1][1:-1]
                    if this_filename[-4:] != ".wav":
                        print 'Error: only ".wav"-files are allowed, aborting.'
                        exit()
                    else:
                        this_filename = this_filename[:-4]
                    tracks[0][NAME] = track_1_filename
                    if not path.exists(this_filename + ".wav"):
                        toc_base = path.basename(tocfile)
                        this_filename = tocfile[:(len(tocfile)-len(toc_base))] + buf[1][1:-1]
                        if not path.exists(this_filename + ".wav"):
                            this_filename = filename[1:-5]
                else:
                    this_filename = name % num
            if buf[2] == "0":
                start = 0
            else:
                if multi_file_mode:
                    if start != 0:
                        print "Error: in multi-file-mode, only zero start-offsets are allowed"
                        exit()
                else:
                    start = timestrtoblocks(buf[2])
            length = timestrtoblocks(buf[3])
            if num == 1:
                global_offset = length
            if multi_file_mode:
                start = global_offset
                global_offset = global_offset + length
        elif buf[0:6] == "START ":
            if multi_file_mode:
                print "Error: pregap is unsupported in multi-file-mode"
                exit()
            pregap = timestrtoblocks(buf[6:])
            if num == 1:
                track1_pregap=pregap
#
#                pregap = timestrtoblocks(buf[6:]) - track1_pregap
            start = start + pregap
            length = length - pregap
        buf = f.readline()
    if tracks:
        tracks[-1][LEN] = tracks[-1][LEN] + pregap
    tracks.append([num, length, start + track1_pregap, copy, pre, ch, 1, bitrate, this_filename])
    return tracks, use_filename, multi_file_mode, track1_pregap

##XXX this will be moved to jack_convert
def msftostr(msf):
    "convert msf format to readable string"
    return "%02i" % msf[B_MM]+":"+"%02i" % msf[B_SS]+":"+"%02i" % msf[B_FF]

def cdrdao_puttoc(tocfile, tracks, id):     # put toc to cdrdao toc-file
    "writes toc-file from tracks"
    f = open(tocfile, "w")
    f.write("CD_DA\n\n")
    f.write("// DB-ID=" + id + "\n\n")
    for i in tracks:
        f.write("// Track " + `i[NUM]` + "\n")      # comments are cool
        f.write("TRACK AUDIO\n")
        if i[COPY]:
            f.write("COPY\n")
        else:
            f.write("NO COPY\n")
        if i[PRE]:
            f.write("PRE_EMPHASIS\n")
        else:
            f.write("NO PRE_EMPHASIS\n")
        if i[CH] == 2:
            f.write("TWO_CHANNEL_AUDIO\n")
        elif i[CH] == 4:
            f.write("FOUR_CHANNEL_AUDIO\n")
        else:
            print "Error: illegal TOC: channels=" + i[CH] + ", aborting."
            exit()
        f.write('FILE "' + i[NAME] + '.wav" 0 ')
        x = i[LEN]
        if i[NUM] == 1:         # add pregap to track, virtually
            x = x + i[START]
        x = blockstomsf(x)
        f.write("%02i" % x[B_MM] + ":" + "%02i" % x[B_SS] + ":" + "%02i" % x[B_FF] + "\n")
        if i[NUM]==1 and i[START] != 0:
            f.write("START "+msftostr(blockstomsf(i[START]))+"\n")
        f.write("\n")

MP3, WAV, BOTH, PEAK, AT, CDR, BLOCKS = 0, 1, 2, 3, 4, 5, 6
def tracksize(list, dont_dae = [], blocksize = 1024):
    "Calculates all kind of sizes for a track or a list of tracks."
    if list and type(list[0]) == types.IntType:
        list = ((list, ))
    peak, at, blocks = 0, 0, 0
    mp3size = wavsize = cdrsize = 0
    for track in list:
        blocks = blocks + track[LEN]
        mp3size = mp3size + track[LEN] / CDDA_BLOCKS_PER_SECOND * track[RATE] * 1000 / 8
        # mp3s can be a bit shorter, if someone knows a better way of guessing
        # mp3 filesizes, please let me know.
        count_thiswav = 1
        for i in dont_dae:
            if i[NUM] == track[NUM]:
                count_thiwav = 0
        thiscdrsize = track[LEN] * CDDA_BLOCKSIZE * count_thiswav
        wavsize = wavsize + thiscdrsize + 44
        cdrsize = cdrsize + thiscdrsize
        now = mp3size + thiscdrsize + 44
        if now>peak:
            at = track[NUM]
            peak = now
    return mp3size, wavsize, mp3size + wavsize, peak, at, cdrsize, blocks

def freedb_sum(n):
    "belongs to freedb_id"
    ret = 0
    while n>0:
        ret = ret + (n % 10)
        n = n / 10
    return ret

def freedb_id(tracks):
    "calculate freedb (aka CDDB) disc-id"
    cdtoc = []
    if not tracks:
        return 0
    for i in tracks:
        cdtoc.append(blockstomsf(i[START] + MSF_OFFSET))
    cdtoc.append(blockstomsf(tracks[-1][START] + tracks[-1][LEN]))

    n = t = 0
    for i in tracks:
        n = n + freedb_sum((i[START] + MSF_OFFSET) / CDDA_BLOCKS_PER_SECOND)
    t = (tracks[-1][START] + tracks[-1][LEN]) / CDDA_BLOCKS_PER_SECOND - tracks[0][START] / CDDA_BLOCKS_PER_SECOND

    return "%08x" % ((n % 0xff) << 24 | (t << 8) | (len(tracks)))

def freedb_template(tracks):
    "generate a freedb submission template"
    f = open(freedb_form_file, "w")
    f.write("# xmcd CD database file\n#\n# Track frame offsets:\n")
    for i in tracks:
        f.write("# " + `i[START] + MSF_OFFSET` + "\n")
    f.write("#\n# Disc length: " + `(MSF_OFFSET + tracks[-1][START] + tracks[-1][LEN]) / CDDA_BLOCKS_PER_SECOND`)
    f.write(" seconds\n#\n# Revision: 0\n")
    f.write("# Submitted via: " + prog_name + " " + prog_version + "\n#\n")
    f.write("DISCID=" + freedb_id(tracks) + "\n")
    f.write("DTITLE=\n")
    for i in tracks:
        f.write("TTITLE" + `i[NUM]-1` + "=\n")
    f.write("EXTD=\n")
    for i in tracks:
        f.write("EXTT" + `i[NUM]-1` + "=\n")
    f.write("PLAYORDER=\n")

def freedb_query(id, tracks, file):
    if freedb_dir:
        return local_freedb(id, freedb_dir, file) # use local database (if any)

    qs = "cmd=cddb query " + id + " " + `len(tracks)` + " " # query string
    for i in tracks:
        qs = qs + `i[START] + MSF_OFFSET` + " "
    qs = qs + `(MSF_OFFSET + tracks[-1][START] + tracks[-1][LEN]) / CDDA_BLOCKS_PER_SECOND`
    hello = "hello=" + username + " " + hostname + " " + prog_name + " " + prog_version
    qs = quote_plus(qs + "&" + hello + "&proto=3", "=&")
    url = "http://" + freedb_server + "/~cddb/cddb.cgi?" + qs
    f = urlopen(url)
    buf = f.readline()
    if buf and buf[0:1] == "2":
        if buf[0:3] == "211": # Found inexact matches, list follows
            print "Found inexact matches choose one:"
            num = 1
            matches = []
            buf = f.readline()
            while buf:
                buf = rstrip(buf)
                if buf != ".":
                    print "%2i" % num + ".) " + buf
                    matches.append(buf)
                    num = num + 1
                buf = f.readline()
            x = -1
            while x < 0 or x > num - 1:
                x = atoi(raw_input(" 0.) none of the above: "))
            if not x:
                print "ok, aborting."
                exit()

            buf = matches[x-1]
            buf = split(buf, " ", 2)
            freedb_cat = buf[0]
            id = buf[1]
            err = 0

        elif buf[0:3] == "200":
            buf = split(buf)
            freedb_cat = buf[1]
        elif buf[0:3] == "202":
            print "Error: ", buf, f.read()
            print "how about trying another --server?"
            exit()
        else:
            print "Error: ", buf, f.read()
            print "     : don't know what to do, aborting."
            exit()

        cmd = "cmd=cddb read " + freedb_cat + " " + id
        url = "http://" + freedb_server + "/~cddb/cddb.cgi?" + quote_plus(cmd + "&" + hello + "&proto=3", "=&")
        f = urlopen(url)
        buf = f.readline()
        if buf and buf[0:3] == "210": # entry follows
            if path.exists(file):
                rename(file, file + ".bak")
            of = open(file, "w")
            buf = f.readline()
            while buf:
                buf = rstrip(buf)
                if buf != ".":
                    of.write(buf + "\n")
                buf = f.readline()
            of.close()
            err = 0
        else:
            print rstrip(buf)
            print f.read()
            print "Error: could not query freedb entry."
            err = 1
        f.close()
    else:
        print rstrip(buf)
        print f.read()
        print "Error: could not check freedb category."
        err = 2
    f.close()
    return err

def freedb_names(id, tracks, name, various, verb = 0, warn = 1):
    "returns error, [(artist, albumname), (track_01-artist, track_01-name), ...], id"
    error = 0
    tracks_on_cd = tracks[-1][NUM]
    freedb = {}
    f = open(name, "r") # first the freedb info is read in...
    while 1:
        line = f.readline()
        if not line:
            break
        line = replace(line, "\n", "")  # cannot use rstrip, we need trailing
                                        # spaces
        for i in ["DISCID", "DTITLE", "TTITLE"]:
            if line[0:len(i)] == i:
                buf = line
                if find(buf, "=") != -1:
                    buf = split(buf, "=", 1)
                    if buf[1]:
                        if freedb.has_key(buf[0]):
                            if buf[0] == "DISCID":
                                freedb[buf[0]] = freedb[buf[0]] + ',' + buf[1]
                            else:
                                freedb[buf[0]] = freedb[buf[0]] + buf[1]
                        else:
                            freedb[buf[0]] = buf[1]

    for i in tracks:    # check that info is there for all tracks
        if not freedb.has_key("TTITLE%i" % (i[NUM] - 1)):   # -1 because freedb starts at 0
            error = 1
            if verb: print "Error: No freedb info for track %02i (\"TTITLE%i\")" % (i[NUM], i[NUM] - 1)
            freedb["TTITLE%i" % (i[NUM] - 1)] = "[not set]"

    for i in freedb.keys():# check that there is no extra info
        if i[0:6] == "TTITLE":
            if atoi(i[6:]) > tracks_on_cd - 1:
                error = 2
                if verb: print "Error: extra freedb info for track %02i (\"%s\"), cd has only %02i tracks." % (atoi(i[6:]) + 1, i, tracks_on_cd)

    if not freedb.has_key("DTITLE"):
        error = 3
        if verb: print "Error: freedb entry doesn't contain disc title info (\"DTITLE\")."
        freedb['DTITLE'] = "[not set]"

    if not freedb.has_key("DISCID"):
        error = 4
        if verb: print "Error: freedb entry doesn't contain disc id info (\"DISCID\")."
        read_id = "00000000"
    else:
        read_id = freedb['DISCID']
        read_ids = split(freedb['DISCID'], ",")
        id_matched = 0
        for i in read_ids:
            if i == id:
                id_matched = 1
        if not id_matched and warn:
            print "Warning: calculated id (" + id + ") and id from freedb file"
            print "       : (", read_ids, ")"
            print "       : do not match, hopefully due to inexact match."
        for i in read_ids:
            for j in i:
                if j not in "0123456789abcdef":
                    if verb: print "Error: the disc's id is not 8-digit hex (\"DISCID\")."
                    error = 5
            if len(i) != 8:
                if verb: print "Error: the disc's id is not 8-digit hex (\"DISCID\")."
                error = 5

    dtitle = freedb['DTITLE']
    dtitle = replace(dtitle, " / ", "/")    # kill superflous slashes
    dtitle = replace(dtitle, "/ ", "/")
    dtitle = replace(dtitle, " /", "/")
    dtitle = replace(dtitle, "(unknown disc title)", "(unknown artist)/(unknown disc title)") # yukk!
    if not dtitle:
        dtitle = "(unknown artist)/(unknown disc title)"
    if find(dtitle,"/") == -1:
        dtitle = "(unknown artist)/" + dtitle
    names = [split(dtitle,"/",1)]
    if names[0][0] == "(unknown artist)":
        if verb: print "Error: the disc's title must be set to \"artist / title\" (\"DTITLE\")."
        error = 6

    if upper(names[0][0]) in ("VARIOUS", "VARIOUS ARTISTS", "SAMPLER"):
        if not various:
            various = 1
        elif various == 2:
            various = 0

    sep = ""
    for i in range(tracks_on_cd):
        buf = freedb['TTITLE'+`i`]
        buf = replace(buf, " / ", "/")                # kill superflous slashes
        buf = replace(buf, "/ ", "/")
        buf = replace(buf, " /", "/")
        if various:
            if find(buf, " - ") != -1:
                if sep and sep != "-":
                    if verb: print "Error: artist and title are not seperated in a consitent way."
                    error = 8
                sep = "-"
                buf = split(buf, " - ", 1)
                buf[0] = replace(buf[0], "/", " - ")
                buf[1] = replace(buf[1], "/", " - ")
                if various_swap:
                    buf = [buf[1], buf[0]]
                names.append(buf)
            elif find(buf, "/") != -1:  # uncommon but seen
                if sep and sep != "/":
                    if verb: print "Error: artist and title are not seperated in a consitent way."
                    error = 8
                sep = "/"
                buf = split(buf, "/", 1)
                if various_swap:
                    buf = [buf[1], buf[0]]
                names.append(buf)
            else:
                if verb: print 'Error: could not seperate artist and title in "' + buf + '".'
                error = 7
        else:
            names.append(["", replace(buf, "/", " - ")])

    # clean up a bit:
    for i in names:
        for j in [0, 1]:
            if i[j]:
                i[j] = strip(i[j])
                while find(i[j], "    ") != -1:
                    i[j] = replace(i[j], "    ", " ")
                while i[j][0] == '"' and i[j][-1] == '"':
                    i[j] = i[j][1:-1]
                while i[j][0] == '"' and find(i[j][1:], '"') != -1:
                    i[j] = replace(i[j][1:], '"', '', 1)
    return error, names, read_id

def do_freedb_submit(file, id):
    from httplib import *
    hello = "hello=" + username + " " + hostname + " " + prog_name + " " + prog_version
    print "Info: querying categories..."
    url = "http://" + freedb_server + "/~cddb/cddb.cgi?" + quote_plus("cmd=cddb lscat" + "&" + hello + "&proto=3", "=&")
    f = urlopen(url)
    buf = f.readline()
    if buf[0:3] == "500":
        print "Info: LSCAT failed, using builtin categories..."
        print "choose a category:"
        cat = ["blues", "classical", "country", "data", "folk", "jazz", "misc", "newage", "reggae", "rock", "soundtrack"]
        num = 1
        for i in cat:
            print "%2i" % num + ".) " + cat[num - 1]
            num = num + 1
        
    elif buf[0:3] == "210":
        print "choose a category:"
        num = 1
        cat = []
        buf = f.readline()
        while buf:
            buf = rstrip(buf)
            if buf != ".":
                print "%2i" % num + ".) " + buf
                cat.append(buf)
                num = num + 1
            buf = f.readline()
        f.close()
    else:
        print "Error: LSCAT failed: " + rstrip(buf)
        print f.read()
        exit()
    x = -1
    while x < 0 or x > num - 1:
        x = atoi(raw_input(" 0.) none of the above: "))
    if not x:
        print "ok, aborting."
        exit()
    cat = cat[x - 1]
    print "OK, using `" + cat + "'."
    email = my_mail
    print "Your e-mail address is needed to send error messages to you."
    x = raw_input("enter your e-mail-address [" + email + "]: ")
    if x:
        email = x
    selector = '/~cddb/submit.cgi'
    proxy = ""
    if environ.has_key('http_proxy'):
        proxy = environ['http_proxy']
        def splittype(url):
            import re
            _typeprog = re.compile('^([^/:] + ):')
            match = _typeprog.match(url)
            if match:
                    scheme = match.group(1)
                    return scheme, url[len(scheme) + 1:]
            return None, url

        def splithost(url):
            import re
            _hostprog = re.compile('^//([^/]+)(.*)$')
            match = _hostprog.match(url) 
            if match: return match.group(1, 2)
            return None, url

        type, proxy = splittype(proxy)
        host, selector2 = splithost(proxy)
        h = HTTP(host)
        h.putrequest('POST', 'http://' + freedb_server + selector)
    else:
        h = HTTP(freedb_server)
        h.putrequest('POST', '/~cddb/submit.cgi')
    h.putheader('Category', cat)
    h.putheader('Discid', id)
    h.putheader('User-Email', email)
    h.putheader('Submit-Mode', 'test')
    h.putheader('Charset', 'ISO-8859-1')
    h.putheader('X-Cddbd-Note', 'Problems submitting with ' + prog_name + '? - RTFS(ource)!')
    h.putheader('Content-Length', `filesize(file)`)
    h.endheaders()
    f = open(file, "r")
    h.send(f.read())
    f.close()

    err, msg, headers = h.getreply()
    f = h.getfile()
    if proxy:
        if err != 200:
            print "Error: proxy: " + `err` + " " + msg 
            print f.read()
            exit()
        else:
            buf = f.readline()
            err, msg = buf[0:3], buf[4:]
            
    # lets see if it worked:
    if err == 404:
        print "This server doesn't seem to support database submission via http."
        print "consider submitting via mail (" + progname + " -m). full error:\n"
    print err, msg
    print f.read()

def do_freedb_mailsubmit(file, id):
    cat = ["blues", "classical", "country", "data", "folk", "jazz", "misc", "newage", "reggae", "rock", "soundtrack"]
    num = 1
    
    print "choose a category:"
    for i in cat:
        print "%2i" % num + ".) " + cat[num - 1]
        num = num + 1
    x = -1
    while x < 0 or x > num - 1:
        x = atoi(raw_input(" 0.) none of the above: "))
    if not x:
        print "ok, aborting."
        exit()
    cat = cat[x - 1]
    print "OK, using `" + cat + "'."
    if find(my_mail, "@") >= 1 and len(my_mail) > 3:
        return system("( echo 'To: " + freedb_mail + "'; echo From: '" + my_mail + "'; echo 'Subject: cddb " + cat + " " + id + "' ; cat '" + file + "' ) | sendmail -t")
    else:
        print "please set your e-mail address. aborting..."

def progress(track, what, data, data2 = None):
    "append a line to the progress file"
    global progress_changed

    if type(track) == types.IntType:
        first = "%02i" % track
    elif type(track) == types.StringType:
        first = track
    else:
        print "Error: illegal progress entry, exit."
        exit()
    progress_changed = 1
    f = open(progress_file, "a")
    f.write(first + progr_sep + what + progr_sep + data)
    if data2:
        f.write(progr_sep + data2)
    f.write("\n")
    f.close()

def progress_error(s):
    progress("all", "error", s)

###################################################################### workers 

def start_new_process(args, nice_val = 0):
    "start a new process in a pty and renice it"
    data = {}
    data['start_time'] = time()
    pid, master_fd = pty.fork()
    if pid == CHILD:
        if nice:
            nice(nice_val)
        execvp(args[0], args)
    else:
        data['pid'] = pid
        if Linux:
            fcntl(master_fd, FCNTL.F_SETFL, FCNTL.O_NONBLOCK)
        data['fd'] = master_fd
        data['file'] = fdopen(master_fd)
        data['cmd'] = args
        data['buf'] = ""
        data['otf'] = 0
        data['percent'] = 0
        data['elapsed'] = 0
        return data

def start_new_ripper(track, ripper):
    "start a new DAE process"
    helper = helpers[ripper]
    cmd = split(helper['cmd'])
    args = []
    for i in cmd:
        if i == "%n": args.append(`track[NUM]`)
        elif i == "%o": args.append(track[NAME] + ".wav")
        elif i == "%d": args.append(cd_device)
        else: args.append(i)
    data = start_new_process(args)
    data['type'] = "ripper"
    data['prog'] = ripper
    data['track'] = track
    return data

def start_new_encoder(track, encoder):
    "start a new encoeder process"
    helper = helpers[encoder]
    if vbr:
        cmd = split(helper['vbr-cmd'])
    else:
        cmd = split(helper['cmd'])
    args = []
    for i in cmd:
        if i == "%r": args.append(`track[RATE] * helper['bitrate_factor']`)
        elif i == "%i": args.append(track[NAME] + ".wav")
        elif i == "%o": args.append(track[NAME] + ".mp3")
        else: args.append(i)
    data = start_new_process(args, nice_value)
    data['type'] = "encoder"
    data['prog'] = encoder
    data['track'] = track
    return data

def start_new_otf(track, ripper, encoder):
    "start a new ripper/encoder pair for on-the-fly encoding"
    data = {}
    data['rip'] = {}
    data['enc'] = {}
    data['rip']['otf'] = 1
    data['enc']['otf'] = 1
    enc_in, rip_out = os.pipe()
    data['rip']['fd'], rip_err = os.pipe()
    data['enc']['fd'], enc_err = os.pipe()
    args = []
    for i in split(helpers[ripper]['otf-cmd']):
        if i == "%n": args.append(`track[NUM]`)
        elif i == "%d": args.append(cd_device)
        else: args.append(i)
    data['rip']['start_time'] = time()
    pid = os.fork()
    if pid == CHILD:
        os.dup2(rip_out, STDOUT_FILENO)
        os.dup2(rip_err, STDERR_FILENO)
        os.close(rip_out)
        os.close(rip_err)
        execvp(args[0], args)
        # child won't see anything below...
    os.close(rip_out)
    os.close(rip_err)
    data['rip']['pid'] = pid
    data['rip']['cmd'] = helpers[ripper]['otf-cmd']
    data['rip']['buf'] = ""
    data['rip']['percent'] = 0
    data['rip']['elapsed'] = 0
    data['rip']['type'] = "ripper"
    data['rip']['prog'] = ripper
    data['rip']['track'] = track
    if vbr:
        cmd = split(helpers[encoder]['vbr-otf-cmd'])
    else:
        cmd = split(helpers[encoder]['otf-cmd'])
    args = []
    for i in cmd:
        if i == "%r": args.append(`track[RATE] * helpers[encoder]['bitrate_factor']`)
        elif i == "%o": args.append(track[NAME] + ".mp3")
        elif i == "%d": args.append(cd_device)
        else: args.append(i)
    data['enc']['start_time'] = time()
    pid = os.fork()
    if pid == CHILD:
        if nice:
            nice(nice_value)
        os.dup2(enc_in, STDIN_FILENO)
        os.dup2(enc_err, STDERR_FILENO)
        os.close(enc_in)
        os.close(enc_err)
        execvp(args[0], args)
        # child won't see anything below...
    os.close(enc_in)
    os.close(enc_err)
    data['enc']['pid'] = pid
    data['enc']['otf-pid'] = data['rip']['pid']
    data['enc']['cmd'] = helpers[encoder]['otf-cmd']
    data['enc']['buf'] = ""
    data['enc']['percent'] = 0
    data['enc']['elapsed'] = 0
    data['enc']['type'] = "encoder"
    data['enc']['prog'] = encoder
    data['enc']['track'] = track
    data['rip']['otf-pid'] = data['enc']['pid']

    if Linux:
        fcntl(data['rip']['fd'], FCNTL.F_SETFL, FCNTL.O_NONBLOCK)
        fcntl(data['enc']['fd'], FCNTL.F_SETFL, FCNTL.O_NONBLOCK)
    data['rip']['file'] = fdopen(data['rip']['fd'])
    data['enc']['file'] = fdopen(data['enc']['fd'])
    return data

def ripread(track, offset = 0):
    "rip one track from an image file."
    data = {}
    start_time = time()
    pid, master_fd = pty.fork() # this could also be done with a pipe, anyone?
    if pid == CHILD:

# FIXME: all this offset stuff has to go, track0 support has to come.

        print ":fAE: waiting for status report..."
        sys.stdout.flush()
        hdr = whathdr(image_file)
        my_swap_byteorder = swap_byteorder
        my_offset = offset
        if hdr:

## I guess most people use cdparanoia 1- (instead of 0- if applicable)
## for image creation, so for a wav file use:

            image_offset = -offset

        else:
            if upper(image_file)[-4:] == ".CDR":
                hdr = ('cdr', 44100, 2, -1, 16) # Unknown header, assuming cdr
#
## assume old cdrdao which started at track 1, not at block 0
                image_offset = -offset

            elif upper(image_file)[-4:] == ".BIN":
                hdr = ('bin', 44100, 2, -1, 16) # Unknown header, assuming bin
#
## assume new cdrdao which starts at block 0, byteorder is reversed.
                my_swap_byteorder = not my_swap_byteorder
                image_offset = 0

        expected_filesize = tracksize(all_tracks)[CDR] + CDDA_BLOCKSIZE * offset
        if hdr[0] == 'wav':
            expected_filesize = expected_filesize + 44
        if filesize(image_file) != expected_filesize:
            print "Error: image file size mismatch, aborted."
            progress_error( "Error: image file size mismatch, aborted. %i != %i"% (filesize(image_file), expected_filesize))
            posix._exit(1)
        elif hdr[0] == 'wav' and (hdr[1], hdr[2], hdr[4]) != (44100, 2, 16):
            print "Error: unsupported WAV, need CDDA_fmt, aborted."
            progress_error( "Error: unsupported WAV, need CDDA_fmt, aborted.")
            posix._exit(2)
        elif hdr[0] not in ('wav', 'cdr', 'bin'):
            print "Error: unsupported: " + hdr[0] + ", aborted."
            progress_error( "Error: unsupported: " + hdr[0] + ", aborted.")
            posix._exit(3)
        else:
#
## set up output wav file:
#
            f = open(image_file, 'r')
            wav = wave.open(track[NAME] + ".wav", 'w')
            wav.setnchannels(2)
            wav.setsampwidth(2)
            wav.setframerate(44100)
            wav.setnframes(0)
            wav.setcomptype('NONE', 'not compressed')
#
## calculate (and seek to) position in image file
#
            track_start = (track[START] + image_offset) * CDDA_BLOCKSIZE
            if hdr[0] == 'wav':
                track_start = track_start + 44
            f.seek(track_start)
#
## copy / convert the stuff
#
            for i in range(0, track[LEN]):
                buf = array("h")
                buf.fromfile(f, 1176) # CDDA_BLOCKSIZE / 2
                if not my_swap_byteorder:  # this is inverted as WAVE swabs them anyway.
                    buf.byteswap()
                wav.writeframesraw(buf.tostring())
                if i % 1000 == 0:
                    print ":fAE: Block " + `i` + "/" + `track[LEN]` + " (%2i%%)" % (i * 100 / track[LEN])
                    sys.stdout.flush()
            wav.close()
            f.close()

            stop_time = time()
            read_speed = track[LEN] / CDDA_BLOCKS_PER_SECOND / ( stop_time - start_time )
            if hdr[0] == 'wav':
                print "[" + "%.0fx" % read_speed + "] [-read from image-]"
            elif hdr[0] == 'cdr':
                print "[" + "%.0fx" % read_speed + "] [cdr-WARNING, check byteorder !]"
            sys.stdout.flush()
            posix._exit(0)
    else:
        data['start_time'] = start_time
        data['pid'] = pid
        data['fd'] = master_fd
        data['file'] = fdopen(master_fd)
        data['cmd'] = ""
        data['buf'] = ""
        data['type'] = "image_reader"
        data['prog'] = "builtin"
        data['track'] = track
        data['percent'] = 0
        data['otf'] = 0
        data['elapsed'] = 0
    return data

######################################################################### utils

def all_paths(p):
    "return all path leading to and including p"
    if type(p) == types.StringType:
        p = split_dirname(p)
    all = []
    x = ""
    for i in p:
        x = path.join(x, i)
        all.append(x)
    return all

def check_path(p1, p2):
    "check if p1 and p2 are equal or sub/supersets"
    if type(p1) == types.StringType:
        p1 = split_dirname(p1)
    if type(p2) == types.StringType:
        p2 = split_dirname(p2)
    for i in p1, p2:
        if type(i) != types.ListType:
            print "Error: invalid type for check_path", i
            exit()
    if len(p1) > len(p2):   # make sure p1 is shorter or as long as p2
        p1, p2 = p2, p1
    ok = 1
    for i in range(1, len(p1) + 1):
        if p1[-i] != p2[-i]:
            ok = 0
    return ok

def rename_path(old, new):
    "this is complicated."
    cwd = os.getcwd()
    cwds = split_dirname(cwd)
    if type(old) == types.StringType:
        old = split_dirname(old)
    if type(new) == types.StringType:
        new = split_dirname(new)
    for i in old, new, cwds:
        if type(i) != types.ListType:
            print "Error: invalid type for rename_path:", i
            exit()
    for i in old:
        chdir(os.pardir)
    for i in new[:-1]:
        if not path.exists(i):
            mkdir(i)
        if path.isdir(i):
            chdir(i)
        else:
            print "Error: could not create or change to " + i + " from " + os.getcwd()
            exit()
    rename(cwd, new[-1])
    chdir(new[-1])
                                               # now remove empty "orphan" dirs

    old_dirs = all_paths(cwds)
    old_dirs.reverse()
    for i in old_dirs[:len(old)][1:]:
        try:
            rmdir(i)
        except OSError:
            pass

def cmp_toc(x, y):
    "compare two track's length"
    x, y = x[LEN], y[LEN]
    if x>y: return 1
    elif x == y: return 0
    elif x<y: return -1

def cmp_toc_cd(x, y):
    "compare the relevant parts of two TOCs"
    ok = 1
    if len(x) == len(y):
        for i in range(len(x)):
            for j in NUM, LEN, START, COPY, CH:
                if x[i][j] != y[i][j]:
                    ok = 0
    else:
        ok = 0
    return ok

def filesize(name):
    return stat(name)[ST_SIZE]

def yes(what):
    if what:
        return "yes"
    else:
        return "no"

def sig_handler(sig, frame):
    "signal handler and general cleanup procedure"
    if frame < 0:
        exit_code = frame
    else:
        exit_code = 0
    if not curses_enable:
        tcsetattr(stdin.fileno(), TERMIOS.TCSADRAIN, old_tc)
    elif curses_init:
        curses.endwin()
    if sig:
        exit_code = 2
        print "Info: signal", sig, "caught, exiting."
    for i in children:
        exit_code = 1
        if not silent_mode:
            print "Info: killing " + i['type'] + " (pid " + `i['pid']` + ")"
        kill(i['pid'], signal.SIGTERM)
        i['file'].close()
    if xtermset_enable and xterm_geom_changed:
        new_height = max(default_height, height)
        if restore_xterm_width:
            new_width = default_width
        else:
            new_width = max(default_width, width)
        signal.signal(signal.SIGWINCH, signal.SIG_IGN)
        system("xtermset -fg black -bg white -restore -geom "+ `new_width`+ "x" + `new_height`)
    if exit_code and silent_mode:
        progress("all", "err", "abnormal exit (code %i), check %s and %s" % (exit_code, err_file, out_file))
    sys.exit(exit_code)
#/ end of sig_handler /#

def sig_winch_handler(sig, frame):
    global staus_pad, stdscr, usage_win
    global pad_y, pad_x, pad_start_y, pad_start_x, pad_end_y, pad_end_x
    global max_y, max_x, usage_win_y, usage_win_x

    signal.signal(signal.SIGWINCH, signal.SIG_IGN)
    if type(curses_sighandler) == types.FunctionType:
        curses_sighandler(sig, frame)

    # we will have to do an ioctl which will return a
    # struct winsize {
    #         unsigned short ws_row;
    #         unsigned short ws_col;
    #         unsigned short ws_xpixel;
    #         unsigned short ws_ypixel;
    # };
    # (according to _I386_TERMIOS_H, /usr/include/asm/termios.h)

    winsize = array("H")
    data = " " * (winsize.itemsize * 4)
    data = ioctl(sys.stdout.fileno(), TIOCGWINSZ, data)
    # unpack the data, I hope this is portable:
    winsize.fromstring(data)
    new_y, new_x, xpixel, ypixel = winsize.tolist()

    old_y, old_x = stdscr.getmaxyx()
    curses.resizeterm(new_y, new_x)
    if sig == signal.SIGWINCH:
        stdscr.getkey()         # resizeterm ungetch a 'KEY_RESIZE'
    max_y, max_x = stdscr.getmaxyx()
    pad_y, pad_x = pad_disp_start_y, pad_disp_start_x
    pad_start_y, pad_start_x = extra_lines - 1, 0
    pad_end_y, pad_end_x = min(extra_lines - 1 + pad_height, max_y - 2), min(max_x, pad_width) - 1
    pad_missing_y = max(pad_height - (max_y - extra_lines), 0)
    pad_missing_x = max(pad_width - max_x, 0)
    if pad_missing_y >= pad_height:
        pad_start_y = 0
    stdscr.clear()
    status_pad.clear()
    scroll_keys = ""
    if pad_disp_start_x:
        scroll_keys = scroll_keys + "h"
    else:
        scroll_keys = scroll_keys + " "
    if pad_missing_y and not pad_disp_start_y > pad_missing_y - 1:
        scroll_keys = scroll_keys + "j"
    else:
        scroll_keys = scroll_keys + " "
    if pad_disp_start_y:
        scroll_keys = scroll_keys + "k"
    else:
        scroll_keys = scroll_keys + " "
    if pad_missing_x and not pad_disp_start_x > pad_missing_x - 1:
        scroll_keys = scroll_keys + "l"
    else:
        scroll_keys = scroll_keys + " "
    if extra_lines < max_y:
        if global_discname:
            stdscr.addstr(0, 0, (center_line(global_options + " [" + scroll_keys + "]", fill = " ", width = width))[:max_x], curses.A_REVERSE)
            stdscr.addstr(1, 0, center_line(global_discname, fill = "- ", fill_r = " -", width = width)[:max_x], curses.A_REVERSE)

        else:
            stdscr.addstr(0, 0, (global_options + " " * (max_x - len(global_options) - (0 + 4)) + scroll_keys)[:max_x], curses.A_REVERSE)

        if special_line:
            stdscr.addstr(2, 0, center_line(special_line, fill = " ", width = width)[:max_x], curses.A_REVERSE)

        if bottom_line:
            stdscr.addstr(max_y - 1, 0, (bottom_line + " " * (max_x - len(bottom_line) - 1 ))[:max_x - 1], curses.A_REVERSE)

        stdscr.refresh()

        usage_win_y, usage_win_x = max_y - usage_win_height - 2, (max_x - usage_win_width) / 2
        if usage_win_y > extra_lines and usage_win_x > 0 and max_y > extra_lines + 2 + usage_win_height and max_x > usage_win_width:
            del usage_win
            usage_win = curses.newwin(usage_win_height, usage_win_width, usage_win_y, usage_win_x)
            usage_win.box()
            usage_win.addstr(1, 2, "* * * " + prog_name + " " + prog_version + " (C)2000 Arne Zellentin * * *")
            usage_win.addstr(2, 2, "use cursor keys or hjkl to scroll status info")
            usage_win.addstr(3, 2, "press P to disable/continue ripping,")
            usage_win.addstr(4, 2, "      E to pause/continue all encoders or")
            usage_win.addstr(5, 2, "      R to pause/continue all rippers.")
            usage_win.refresh()

        for i in all_tracks_todo_sorted:
            status_pad.addstr(map_track_num[i[NUM]], 0, printable_names[i[NUM]] + ": " + dae_status[i[NUM]] + " " + enc_status[i[NUM]])

        status_pad.refresh(pad_y, pad_x, pad_start_y, pad_start_x, pad_end_y, pad_end_x)
    signal.signal(signal.SIGWINCH, sig_winch_handler)
    return
#/ end of sig_winch_handler(sig, frame) /#

def exit(why = 0):
    "call my own cleanum fkt. and exit"
    if why:
        why = 0 - why
    sig_handler(0, why)

def mkdirname(names, template):
    "generate mkdir-able directory name(s)"
    dirs = split(template, os.sep)  # this is not really clean
    if dirs[0] == "":
        dirs[0] = os.sep
        
    dirs2 = []
    for i in dirs:
        replace_list = (("%a", names[0][0]), ("%l", names[0][1]))
        x = multi_replace(i, replace_list)
        for char_i in range(len(unusable_chars)):
            x = replace(x, unusable_chars[char_i], replacement_chars[char_i])
        dirs2.append(x)
    name = ""
    for i in dirs2:
        name = path.join(name, i)
    return dirs2, name

def split_dirname(name):
    "split path in components"
    names = []
    while 1:
        base, sub = path.split(name)
        if not base or base == os.sep:
            names.append(path.join(base, sub))
            break
        names.append(sub)
        name = base
    names.reverse()
    return names

def split_path(path, num):
    "split given path in num parts"
    new_path = []
    for i in range(1, num):
        base, end = path.split(path)
        path = base
        new_path.append(end)
    new_path.append(base)
    new_path.reverse()

def interpret_db_file(all_tracks, freedb_form_file, various, verb, dirs = 0, warn = None):
    "read freedb file and rename dir(s)"
    global names_available, claim_dir, dir_created
    freedb_rename = 0
    if warn == None:
        err, track_names, id = freedb_names(freedb_id(all_tracks), all_tracks, freedb_form_file, various, verb = verb)
    else:
        err, track_names, id = freedb_names(freedb_id(all_tracks), all_tracks, freedb_form_file, various, verb = verb, warn = warn)
    if (not err) and dirs:
        freedb_rename = 1

# The user wants us to use the current dir, unconditionally.

        if claim_dir:
            dir_created = split_dirname(os.getcwd())[-1]
            progress("all", "mkdir", dir_created)
            claim_dir = 0

        if rename_dir and dir_created:
            new_dirs, new_dir = mkdirname(track_names, dir_template)
            old_dir = os.getcwd()
            old_dirs = split_dirname(old_dir)
            dirs_created = split_dirname(dir_created)

# only do the following if we are where we think we are and the dir has to be
# renamed.

            if check_path(dirs_created, old_dirs) and not check_path(dirs_created, new_dirs):
                rename_path(dirs_created, new_dirs)
                print "Info: cwd now", os.getcwd()
                progress("all", 'ren', dir_created + "-->" + new_dir)
    if not err:
        names_available = 1
    else:
        freedb_rename = 0
    return err, track_names, freedb_rename
#/ end of interpret_db_file /#

def enc_stat_upd(num, string):
    enc_status[num] = string
    if curses_enable:
        status_pad.addstr(map_track_num[num], 0, printable_names[num] + ": " + dae_status[num] + " " + enc_status[num])
        status_pad.clrtoeol()

def dae_stat_upd(num, string):
    dae_status[num] = string
    if curses_enable:
        status_pad.addstr(map_track_num[num], 0, printable_names[num] + ": " + dae_status[num] + " " + enc_status[num])
        status_pad.clrtoeol()

def print_status(form = 'normal'):
    for i in all_tracks_todo_sorted:
        if form != 'normal':
            print name % i[NUM] + ": " + dae_status[i[NUM]], enc_status[i[NUM]]
        else:
            print printable_names[i[NUM]] + ": " + dae_status[i[NUM]], enc_status[i[NUM]]

##############################################################################
###################### M A I N ###############################################
##############################################################################

if statvfs_found:   # which df() to use, checked at the very beginning 
    df = df_statvfs
else:
    df = df_df

if len(replacement_chars) == 0:
    replacement_chars = [""]
while len(unusable_chars) > len(replacement_chars): # stretch rep._chars
    if type(replacement_chars) == types.ListType:
        replacement_chars.append(replacement_chars[-1])
    elif type(replacement_chars) == types.StringType:
        replacement_chars = replacement_chars + replacement_chars[-1]
    else:
        print "Error: unsupported type: " + `type(replacement_chars[-1])`
        exit(1)

space_set_from_argv = 0
argv_bitrate = 0
if len(my_mail) <= 3:
    my_mail = username + "@" + hostname

#### Parse argv
i = 1
tracks = ""
guess_mp3s = []

# say hello...
print "This is ", prog_name, prog_version, "(C) 2000  Arne Zellentin <arne@unix-ag.org>"

while i<len(argv):
    if argv[i] in ("-t", "--tracks"):
        tracks = argv[i + 1]
        i = i + 1
    elif argv[i] in ("-b", "--bitrate"):
        argv_bitrate = 1
        bitrate = atoi(argv[i + 1])
        i = i + 1
    elif argv[i] in ("-F", "--from-image"):
        image_file = argv[i + 1]
        read_ahead = 0          # we do not want to waste discspace
        i = i + 1
    elif argv[i] in ("-f", "--from-tocfile"):
        toc_file = argv[i + 1]
        read_ahead = 0          # we do not want to waste discspace
        i = i + 1
    elif argv[i] in ("-e", "--encoders"):
        encoders = atoi(argv[i + 1])
        i = i + 1
    elif argv[i] in ("-E", "--encoder-name"):
        encoder = argv[i + 1]
        i = i + 1
    elif argv[i] in ("-n", "--nice"):
        nice_value = atoi(argv[i + 1])
        i = i + 1
    elif argv[i] in ("-l", "--max-load"):
        max_load = atof(argv[i + 1])
        i = i + 1
    elif argv[i] in ("-g", "--guess-toc"):
        i = i + 1
        while len(argv[i:]):
            if argv[i] == ";":
                break
            else:
                guess_mp3s.append(argv[i])
            i = i + 1
    elif argv[i] in ("-x", "--exec"):
        exec_when_done = 1
    elif argv[i] in ("-s", "--space"):  # in bytes, overrides autodetection
        space_from_argv = atoi(argv[i + 1])
        space_set_from_argv = 1
        i = i + 1
    elif argv[i] in ("-a", "--read-ahead"):
        read_ahead = atoi(argv[i + 1])
        i = i + 1
    elif argv[i] == "--server":
        freedb_server = argv[i + 1]
        i = i + 1
    elif argv[i] == "--device":
        cd_device = argv[i + 1]
        i = i + 1
    elif argv[i] == "--submit":
        freedb_submit = 1
    elif argv[i] in ("-m", "--mail-submit"):
        freedb_mailsubmit = 1
    elif argv[i] in ("-M", "--mail-address"):
        freedb_mail = argv[i + 1]
        i = i + 1
    elif argv[i] == "--my-email":
        my_mail = argv[i + 1]
        i = i + 1
    elif argv[i] in ("-s", "--swab"):
        swap_byteorder = 1
    elif argv[i] in ("-r", "--reorder"):
        reorder = 1
    elif argv[i] in ("-k", "--keep-wavs"):
        keep_wavs = 1
    elif argv[i] in ("-O", "--only-dae"):
        only_dae = 1
    elif argv[i] in ("-o", "--overwrite"):
        overwrite = 1
    elif argv[i] in ("-c", "--check-toc"):
        check_toc = 1
    elif argv[i] in ("-v", "--vbr"):
        vbr = 1
    elif argv[i] =="--silent-mode":
        silent_mode = 1
    elif argv[i] =="--force":
        force = 1
    elif argv[i] =="--various":
        various = 1
    elif argv[i] =="--various-swap":
        various_swap = 1
    elif argv[i] =="--no-various":
        various = 2
    elif argv[i] =="--remove":
        remove_files = 1
    elif argv[i] =="--upd-progress":
        upd_progress = 1
    elif argv[i] in ("-q", "--query"):
        query_when_ready = 1
        read_freedb_file = 1
        set_id3tag = 1
    elif argv[i] in ("-Q", "--query-now"):
        query_on_start = 1
        set_id3tag = 1
    elif argv[i] in ("", "--multi-mode"):
        multi_mode = 1
    elif argv[i] in ("", "--scan-dirs"):
        scan_dirs = atoi(argv[i + 1])
        i = i + 1
    elif argv[i] in ("-d", "--dont-work"):
        dont_work = 1
    elif argv[i] in ("-D", "--create-dir"):
        create_dirs = 1
        rename_dir = 1
    elif argv[i] in ("-R", "--rename-only"):
        read_freedb_file = 1
        set_id3tag = 1
    elif argv[i] in ("-C", "--claim-dir"):
        claim_dir = 1
    elif argv[i] in ("-u", "--undo-rename"):
        undo_rename = 1
    elif argv[i] == "--todo":
        todo_exit = 1
    elif argv[i] == "--otf":
        otf = 1
    else:
        if not argv[i] in ("-h", "-help", "--help"):
            print "unknown option", argv[i]
        print "usage:", prog_name, "[option]..."
        print "Options marked \"*\" need a string argument; \"+\" marks integer argument."
        print "  -t, --tracks:      *which tracks to process (e.g. 1, 3, 5-9, 12-)"
        print "  -b, --bitrate:     +MP3 bitrate (in kbit/s, default is " + `bitrate` + ")"
        print "  -v, --vbr:          generate variable bitrate MP3s (" + yes(vbr) + ")"
        print "  -o, --overwrite:    overwrite existing WAVs and MP3s (" + yes(overwrite) + ")"
        print "  -O, --only-dae:     only produce WAVs, implies -k (" + yes(only_dae) + ")"
        print "  -F, --from-image:  *read audio data from image file"
        print "  -f, --from-tocfile:*read toc from file"
        print "  -S, --swab:         swap byteorder from image file (" + yes(swap_byteorder) + ")"
        print "  -e, --encoders:    +encode how many MP3s in parallel (" + `encoders` + ")"
        print "  -E, --encoder-name:*use which encoder [bladeenc, lame, l3enc, mp3enc] (" + encoder + ")"
        print "  -n, --nice:        +nice-level of encoders (" + `nice_value` + ")"
        print "  -l, --max-load:    +only start new encoders if load < (" + `max_load` + ") + #encoders"
        print "  -a, --read-ahead:  +read how many WAVs in advance (" + `read_ahead` + ")"
        print "  -r, --reorder:      optimize track-order for discspace (" + yes(reorder) + ")"
        print "  -s, --space:       +force usable discspace, in bytes (auto)"
        print "  -k, --keep-wavs:    do not delete WAVs after encoding them (" + yes(keep_wavs) + ")"
        print "  -c, --check-toc:    compare toc-file and cd-toc, then exit (" + yes(check_toc) + ")"
        print "  -q, --query:        do freedb query when all is done (" + yes(query_when_ready) + ")"
        print "  -Q, --query-now:    do freedb query when starting (" + yes(query_on_start) + ")"
        print "  -d, --dont-work:    don't do DAE or encoding (" + yes(dont_work) + ")"
        print "  -D, --create-dirs:  create subdir for files (" + yes(create_dirs) + ")"
        print "  -R, --rename-only:  rename according to freedb file, eg. after editing it (" + yes(read_freedb_file and set_id3tag) + ")"
        print "  -C, --claim-dir:    rename the dir even if it was not created by jack (" + yes(claim_dir) + ")"
        print "  -u, --undo-rename:  undo file renaming and exit (" + yes(undo_rename) + ")"
        print "  -g, --guess-toc:   *guess TOC from files (until terminating \";\") (" + yes(guess_mp3s) + ")"
        print "  -x, --exec:         run predefined command when finished (" + yes(exec_when_done) + ")"
        print '  -m, --mail-submit:  submit by e-mail - needs sendmail! (' + yes(freedb_mailsubmit) + ")"
        print "  -M, --mail-address:*submit database entry by e-mail (" + freedb_mail + ")"
        print "      --my-email:    *submission report is sent here (" + my_mail + ")"
        print "      --submit:       http-submit freedb file to server and exit (" + yes(freedb_submit) + ")"
        print "      --server:      *use freedb server (" + freedb_server + ")"
        print "      --silent-mode:  be quiet (no screen output) (" + yes(silent_mode) + ")"
        print "      --device:      *use which cdrom device (" + cd_device + ")"
        print "      --scan-dirs:   +scan in cwd n dir levels deep, e.g. 0 to disable"
        print "      --force:        don't ask. (" + yes(force) + ")"
        print "      --remove:       remove jack.* file when done (" + yes(remove_files) + ")"
        print "      --upd-progress: re-generate progress file if \"lost\" (" + yes(upd_progress) + ")"
        print "      --various:      assume CD has various artists (" + "auto" * (various == 0) + "yes" * (various == 1) + ")"
        print "      --various-swap: exchange artist and title (" + yes(various_swap) + ")"
        print "      --no-various:   force no various artists, override autodetection (" + yes(various == 2) + ")"
        print "      --todo:         print what would be done and exit (" + yes(todo_exit) + ")"
        print "      --otf:          on-the-fly encoding *experimental* (" + yes(otf) + ")"
        print
        print "    When a freedb query is done, files are renamed and tagged accordingly."

        if not curses_enable:
            print """

    While Jack is running, press q or Q to quit,
                               p or P to disable ripping (you need the CD drive)
                               p or P (again) or c or C to resume,
                               e or E to pause/continue all encoders and
                               r or R to pause/continue all rippers.
"""
        exit()
    i = i + 1

if silent_mode:
    curses_enable = 0
    xtermset_enable = 0
    out_f = open(out_file, "a")
    err_f = open(err_file, "a")
    os.dup2(out_f.fileno(), STDOUT_FILENO)
    out_f.close()
    os.dup2(err_f.fileno(), STDERR_FILENO)
    err_f.close()
    signal.signal(signal.SIGTTOU, signal.SIG_IGN)

# check for option conflicts:
if otf and only_dae:
    print "Warning: disabling on-the-fly operation because we're doing DAE only."
    otf = 0
if otf and keep_wavs:
    print "Warning: disabling on-the-fly operation because we want to keep the wavs."
    otf = 0
if otf and image_file:
    print "Warning: disabling on-the-fly operation as we're reading from image."
    otf = 0
if otf:
    for i in (ripper, encoder):
        if not helpers[i].has_key('otf-cmd'):
            print "Error: can't do on-the-fly because " + helpers[i]['type'] + " " + i + " doesn't support it."
            exit()
if vbr and not helpers[encoder].has_key('vbr-cmd'):
    print "Error: can't do VBR because " + encoder + " doesn't support it."
    exit()

if query_on_start and query_when_ready:
    print "Error: it doesn't make sense to query now _and_ when finished."
    exit()

### interpret options

### (1) search for a dir containing a toc-file or do the multi-mode

tries = 0
while (not path.exists(toc_file)) or multi_mode:
    tries = tries + 1
    if tries > 2:
        break
    if guess_mp3s:
        all_tracks = guesstoc(guess_mp3s)
    else:
        if multi_mode:
            all_tracks = []
        else:
            all_tracks = gettoc()

        if scan_dirs != None:
            recurse_dirs = scan_dirs
            dirs = [os.getcwd()]
        else:
            dirs = searchdirs
        while recurse_dirs > 0:
            recurse_dirs = recurse_dirs - 1
            new_dirs = []
            for i in dirs:
                if not i in new_dirs:
                    new_dirs.append(i)
                subdir = os.listdir(i)
                for j in subdir:
                    dir = path.join(i,j)
                    if path.isdir(dir) and not dir in new_dirs:
                        new_dirs.append(dir)
            dirs = new_dirs
        possible_dirs = []  # dirs matching inserted CD
        jack_dirs = []      # dirs containing toc_file
        for i in dirs:
            if path.exists(path.join(i, toc_file)):
                jack_dirs.append(i)
                file_toc, my_image_file, my_multi_file_mode, my_track1_offset = cdrdao_gettoc(path.join(i, toc_file))
                if freedb_id(all_tracks) == freedb_id(file_toc):
                    possible_dirs.append(i)

        if multi_mode:
            unique_dirs = []
            for i in range(len(jack_dirs)):
                found = 0
                for j in range(i + 1,len(jack_dirs)):
                    if path.samefile(jack_dirs[i], jack_dirs[j]):
                        found = 1
                if not found:
                    unique_dirs.append(jack_dirs[i])
            for i in unique_dirs:
                all_tracks, new_image_file, multi_file_mode, track1_offset = cdrdao_gettoc(path.join(i, toc_file))
                err, track_names, id = freedb_names(freedb_id(all_tracks), all_tracks,  path.join(i, freedb_form_file), various, verb = 0, warn = 0)
                if err or force:# this means freedb data is not there yet
                    print "Info: matching dir found:", i
                    chdir(i)
                    ch_args = argv
                    for killarg in ('--force', '--multi-mode'):
                        if killarg in ch_args:
                            ch_args.remove(killarg)
                    pid = os.fork()
                    if pid == CHILD:
                        execvp(ch_args[0], ch_args)
                    else:
                        respid, res = waitpid(pid, 0)
            sys.exit()

        unique_dirs = []
        for i in range(len(possible_dirs)):
            found = 0
            for j in range(i + 1,len(possible_dirs)):
                if path.samefile(possible_dirs[i], possible_dirs[j]):
                    found = 1
            if not found:
                unique_dirs.append(possible_dirs[i])
                print "Info: matching dir found:", possible_dirs[i]
        if len(unique_dirs) > 1:
            print "Error: found more than one workdir, change to the correct one."
            exit()
        elif len(unique_dirs) == 1:
            chdir(unique_dirs[0])
        else:
            if create_dirs:
                chdir(base_dir)
                dir_name = prog_name + "-" + freedb_id(all_tracks)
                if not path.exists(dir_name) and not path.isdir(dir_name):
                    mkdir(dir_name)
                chdir(dir_name)
                dir_created = dir_name
                progress("all", "mkdir", dir_created)

    if not multi_mode:
        if not path.exists(toc_file):
            cdrdao_puttoc(toc_file, all_tracks, freedb_id(all_tracks))
            freedb_template(all_tracks) # generate freedb form if tocfile is created
        if not path.exists(freedb_form_file):
            freedb_template(all_tracks)
    else:
        break

# now we are set to go as we know we are in the right dir

### (2) check toc (operation mode)

if check_toc:
    cd_toc = gettoc()
    if path.exists(toc_file):
        file_toc, new_image_file, multi_file_mode, track1_offset = cdrdao_gettoc(toc_file)
    else:
        print "no toc-file named " + toc_file + " found, exiting."
        exit()
    if cmp(cd_toc, file_toc) == 0:
        print 'Yes, toc-file ("' + toc_file + '") matches inserted CD.'
    else:
        print 'No, toc-file ("' + toc_file + '") *DOES NOT* match inserted CD.'
    exit()

### (3) read and interpret toc_file

if path.exists(toc_file):
    all_tracks, new_image_file, multi_file_mode, track1_offset = cdrdao_gettoc(toc_file)
    if not path.exists(def_toc):
        cdrdao_puttoc(def_toc, all_tracks, freedb_id(all_tracks))
    if not image_file:
        image_file = new_image_file
    if multi_file_mode:
        image_file = ""
    if freedb_submit:               # freedb submit
        err, track_names, id = freedb_names(freedb_id(all_tracks), all_tracks, freedb_form_file, various)
        if not err:
            do_freedb_submit(freedb_form_file, id)
        else:
            print "Error: invalid freedb file."
        exit()
    elif freedb_mailsubmit:
        err, track_names, id = freedb_names(freedb_id(all_tracks), all_tracks, freedb_form_file, various)
        if not err:
            do_freedb_mailsubmit(freedb_form_file, id)
        else:
            print "Error: invalid freedb file."
        exit()
        
if not path.exists(freedb_form_file):
    freedb_template(all_tracks) # ... or if it has been deleted.

### (4) Parse tracks from argv, generate todo

tracknum = {}
for i in all_tracks:
    tracknum[i[NUM]] = i

if not tracks:
    todo = []
    for i in all_tracks:
        todo.append(i)

else:       # example: "1,2,4-8,12-" ->  [ 1,2,4,5,6,7,8,12,13,...,n ]
    tlist = []
    tracks = split(tracks, ",")
    for k in tracks:
        if find(k, '-') >= 0:
            k = split(k, '-')
            if k[1]:
                upper_limit = atoi(k[1])
            else:
                upper_limit = len(all_tracks)
            for j in range(atoi(k[0]), upper_limit + 1):
                tlist.append(j)
        else:
            tlist.append(atoi(k))
    tlist.sort()
    todo = []
    if len(tlist) == 1:
        todo.append(all_tracks[tlist[0] - 1])
    else:
        for k in range(0, len(tlist)):
            if tlist[k] != tlist[k - 1]:
                todo.append(all_tracks[tlist[k]-1])

for i in todo:
    all_tracks_todo_sorted.append(i)

### init status
for i in todo:
    dae_status[i[NUM]] = ""
    enc_status[i[NUM]] = ""
    enc_cache[i[NUM]] = ""

### (5) read progress info into status

status = {}
for i in all_tracks:
    num = i[NUM]
    status[num] = {}
    status[num]['dae'] = status[num]['enc'] = status[num]['ren'] = []
    status[num]['names'] = [i[NAME],]

status['all'] = {}
status['all']['mkdir'] = status['all']['names'] = [[],]
status['all']['dae'] = status['all']['enc'] = status['all']['ren'] = []

### (6) update progress file at user's request (operation mode)

if upd_progress:
    for i in todo:
        num = i[NUM]
        if not status[num]['dae']:
            if path.exists(i[NAME] + ".wav"):
                status[num]['dae'] = "[simulated]"
                progress(num, "dae", status[num]['dae'])
        if not status[num]['enc']:
            if path.exists(i[NAME] + ".mp3"):
                x = mp3format(i[NAME] + ".mp3")
                status[num]['enc'] = `x['bitrate']` + progr_sep + "[simulated]"
                progress(num, "enc", status[num]['enc'])

if path.exists(progress_file):
    f = open(progress_file, "r")
    while 1:
        buf = f.readline()
        if not buf:
            break
        buf = split(strip(buf), progr_sep, 3)
        try:
            num = atoi(buf[0])
        except ValueError:
            num = buf[0]
        if buf[1] == 'undo':        # this needs special treatment as
                                    # the correct sequence is important

            status[num]['ren'].append(('Undo',))
        elif buf[1] == 'ren':
            status[num][buf[1]].append(buf[2:])
        else:
            status[num][buf[1]] = buf[2:]
    f.close()

# names for 'all' can't be initialized earlier...
status['all']['names'] = [status['all']['mkdir'][-1],]

                                    # extract names from renaming
for i in status.keys():
    for j in status[i]['ren']:
        if j == ('Undo',):
            if len(status[i]['names']) > 1:
                del status[i]['names'][-1]
            else:
                print "Error: more undos than renames, exit."
                sys.exit()
        else:
            names = split(j[0], '-->', 1)
            if status[i]['names'][-1] == names[0]:
                status[i]['names'].append(names[1])
        if type(i) == types.IntType:
            tracknum[i][NAME] = status[i]['names'][-1]
    del status[i]['ren']

status_all = status['all']  # status info for the whole CD is treated seperately
del status['all']

                                    # now clean up a little

for i in status.keys():
    if len(status[i]['dae']) > 1 or len(status[i]['enc']) > 2:
        print "Error: malformed progress file, exit."
        sys.exit()
    if len(status[i]['enc']) == 2:
        tracknum[i][RATE] = atoi(status[i]['enc'][0])
        status[i]['enc'] = status[i]['enc'][1]
    elif status[i]['enc'] and len(status[i]['enc']) == 1:
        tracknum[i][RATE] = bitrate
    if status[i]['dae'] and len(status[i]['dae']) == 1:
        status[i]['dae'] = status[i]['dae'][0]

                                    # extract status from read progress data

for i in status.keys():
    if status[i]['dae']:
        dae_status[i] = status[i]['dae']
    if status[i]['enc']:
        enc_status[i] = status[i]['enc']

dir_created = status_all['names'][-1]

### (7) do query on start

if query_on_start:
    print "Info: Querying..."
    if freedb_query(freedb_id(all_tracks), all_tracks, freedb_form_file):
        exit()
    err, track_names, freedb_rename = interpret_db_file(all_tracks, freedb_form_file, various, verb = query_on_start, dirs = 1)
    if err:
        print "Error: query on start failed to give a good freedb file, aborting."
        exit()
else:
    err, track_names, freedb_rename = interpret_db_file(all_tracks, freedb_form_file, various, verb = query_on_start, warn = query_on_start)


### (8) undo renaming (operation mode)

if undo_rename:
    maxnames = max(map(lambda x: len(x['names']), status.values()))
    if len(status_all['names']) >= maxnames:
        dir_too = 1
    else:
        dir_too = 0
    maxnames = max(maxnames, len(status_all['names']))
    if maxnames > 1:

                                    # undo dir renaming

        cwd = os.getcwd()
        if dir_created and check_path(dir_created, cwd) and dir_too:
            new_name, old_name = status_all['names'][-2:]
            rename_path(old_name, new_name)    # this changes cwd!
            print "Info: cwd now", os.getcwd()
            progress("all", "undo", "dir")

        else:
            maxnames = max(map(lambda x: len(x['names']), status.values()))

                                    # undo file renaming
        for i in todo:
            if maxnames < 2:
                break
            act_names = status[i[NUM]]['names']
            if len(act_names) == maxnames:
                for j in ('.mp3', '.wav'):
                    new_name, old_name = act_names[-2:]
                    new_name, old_name = new_name + j, old_name + j
                    if not path.exists(old_name):
                        if j == '.mp3':
                                    print 'NOT renaming "' + old_name + '": it doesn\'t exist.'
                    else:
                        if path.exists(new_name):
                            print 'NOT renaming "' + old_name + '" to "' + new_name + '" because dest. exists.'
                        else:
                            progress(i[NUM], "undo", "-")
                            rename(old_name, new_name)
    else:
        print "Info: nothing to do."
    sys.exit()

#### Reorder if told so
if reorder:
    todo.sort(cmp_toc)
    todo.reverse()

#### check how much bytes we can burn
if space_set_from_argv:
    space = raw_space = space_from_argv
else:
    space = raw_space = df()

#### check what is already there
wavs_todo = []
mp3s_todo = []
remove_q = []
for track in todo:
    wavs_todo.append(track)
    mp3s_todo.append(track)
wavs_ready = [] # this is not really needed
mp3s_ready = [] # we need this later to remove the tracks from wavs_todo
                # and to prevent re-coding

for track in todo:
    mp3 = track[NAME] + ".mp3"
    if path.exists(mp3):
        if overwrite:
            space = space + filesize(mp3)
            remove_q.append(mp3)
            enc_status[track[NUM]] = "will o/w MP3."
        elif not force and not enc_status[track[NUM]]:
            space = space + filesize(mp3)
            remove_q.append(mp3)
            enc_status[track[NUM]] = "no MP3 enc run."
        elif filesize(mp3) <= tracksize(track)[MP3] * 0.99: # found by trial'n'err
            space = space + filesize(mp3)
            remove_q.append(mp3)
            enc_status[track[NUM]] = "MP3 too small by " + pprint_i(tracksize(track)[MP3] - filesize(mp3)) + "."
        elif not vbr and filesize(mp3) >= tracksize(track)[MP3] * 1.05: # found by trial'n'err
            space = space + filesize(mp3)
            remove_q.append(mp3)
            enc_status[track[NUM]] = "MP3 too large by " + pprint_i(filesize(mp3) - tracksize(track)[MP3]) + "."
        else:
            mp3s_todo.remove(track)
            mp3s_ready.append(track)
    else:
        if enc_status[track[NUM]]:
            enc_status[track[NUM]] = "[MP3 lost, doing again]"

for track in todo:
    wav = track[NAME] + ".wav"
    if path.exists(wav):
        if overwrite:
            space = space + filesize(wav)
            remove_q.append(wav)
            dae_status[track[NUM]] = "Existing WAV will be overwritten."
        elif filesize(wav) == tracksize(track)[WAV] and dae_status[track[NUM]]:
            wavs_todo.remove(track)
            wavs_ready.append(track)
        else:
            space = space + filesize(wav)
            remove_q.append(wav)
            dae_status[track[NUM]] = "Existing WAV was not complete."
            if enc_status[track[NUM]] == "[MP3 lost, doing again]":
                enc_status[track[NUM]] = ""
    else:
        if dae_status[track[NUM]]:
            if enc_status[track[NUM]] == "[MP3 lost, doing again]":
                dae_status[track[NUM]] = "[WAV+MP3 lost, doing again]"
                enc_status[track[NUM]] = ""
            elif keep_wavs or track not in mp3s_ready:
                dae_status[track[NUM]] = "[WAV lost, doing again]"

if only_dae: keep_wavs = 1

if not keep_wavs:
    for track in todo:
        if track in mp3s_ready and track in wavs_todo:
            wavs_todo.remove(track)

if reorder:
    mp3s_todo.sort(cmp_toc)

for track in wavs_todo:
    dae_queue.append(track) # copy track to dae + code in queue
    if track in mp3s_todo:
        mp3s_todo.remove(track) # remove mp3s which are not there yet

if only_dae:            # if only_dae MP3s are not generated _at_all_.
    mp3s_todo = []

# now mp3s_todo contains the tracks where the wavs only need to be coded and
# wavs_todo lists those tracks which need to be dae'd end enc'd. The dae_queue
# has been filled from wavs_todo (todo is superflous now). The main-loop
# will handle the tracks in mp3s_todo.

if todo_exit:           # print what needs to be done, then exit
    for i in all_tracks:
        print "%02i" % i[NUM],
        if i in todo:
            print "*",
        else:
            print "-",
        if i in wavs_todo:
            print ":DAE:",
            if dae_status[i[NUM]] != "[simulated]": print dae_status[i[NUM]],
            if not only_dae:
                print ":ENC:",
                if enc_status[i[NUM]] != "[simulated]": print enc_status[i[NUM]],
        if i in mp3s_todo:
            print ":ENC:",
            if enc_status[i[NUM]] != "[simulated]": print enc_status[i[NUM]],
        print
    exit()

# overwrite cached bitrates from argv
if argv_bitrate:
    for i in wavs_todo:
        i[RATE] = bitrate
    for i in mp3s_todo:
        i[RATE] = bitrate

# check free space
will_work = 1
freeable_space = 0
if keep_wavs:
    space_needed = tracksize(wavs_todo)[BOTH]
elif otf:
    space_needed = tracksize(wavs_todo)[MP3]
else:
    space_needed = tracksize(wavs_todo)[PEAK]
if only_dae:
    space_needed = tracksize(wavs_todo)[WAV]
else:
    for i in mp3s_todo:
        if space + freeable_space>tracksize(i)[MP3]:
            if not keep_wavs:
                freeable_space = freeable_space + tracksize(i)[WAV] - tracksize(i)[MP3]
        else:
            will_work = 0
            space_needed = tracksize(i)[MP3] - space + freeable_space # this is quite dirty
            break

if (space + freeable_space<space_needed or not will_work) and not dont_work:
    if reorder:
        print "insufficient discspace (%sBytes needed), free %sBytes." % (pprint_i(space_needed - freeable_space, "%i %s"), pprint_i(space_needed - freeable_space - raw_space, "%i %s"))
    else:
        print "insufficient discspace (%sBytes needed), try --reorder or free %sBytes" % (pprint_i(space_needed - freeable_space, "%i %s"), pprint_i(space_needed - freeable_space - raw_space, "%i %s"))
    exit()

max_load = max_load + encoders

if not dont_work and dae_queue:     # check if inserted cd matches toc.
    if not image_file:
        all_tracks_on_cd = gettoc()
        if not force and not cmp_toc_cd(all_tracks, all_tracks_on_cd):
            print "Error: you did not insert the right cd, aborting."
            exit()

if remove_q and not force and not dont_work:
    if silent_mode:
        print "remove these files before going on:"
        for i in remove_q:
            print i
        print "### . ###"
        exit(3)
    print "/\\" * 40
    for i in remove_q:
        print i
    x = raw_input("These files will be deleted, continue? ") + "x"
    if x[0] != "y":
        exit()
if not dont_work:
    for i in remove_q: # remove files (delayed so we can sanity check toc)
        remove(i)

# bail out now if told so
if dont_work:
    exit()

signal.signal(signal.SIGTERM, sig_handler)  # install signal handlers
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGQUIT, sig_handler)
signal.signal(signal.SIGHUP, sig_handler)

if not curses_enable and (wavs_todo or mp3s_todo):
    new = tcgetattr(stdin.fileno())     # set terminal attributes
    new[3] = new[3] & ~TERMIOS.ECHO
    new[3] = new[3] & ~TERMIOS.ICANON
    tcsetattr(stdin.fileno(), TERMIOS.TCSADRAIN, new)


             #\                                                 /#
#########> real work starts here <#############################################
             #/                                                 \#

def main_loop(mp3s_todo, wavs_todo, space, dae_queue, enc_queue, enc_running, dae_running):
    global status_pad, global_options, map_track_num, printable_names
    global pad_y, pad_x, pad_start_y, pad_start_x, pad_end_y, pad_end_x
    global pad_height, pad_width, global_discname, special_line
    global max_y, max_x, extra_lines, pad_disp_start_y, pad_disp_start_x
    global default_height, default_width, xterm_geom_changed
    global children, global_error, bottom_line, width, height, helpers
    global usage_win

    actual_load = -2    # this is always smaller than max_load
    waiting_load = 0    # are we waiting for the load to drop?
    waiting_space = 0   # are we waiting for disc space to be freed?
    space_waiting = 0   # how much space _running_ subprocesses will consume
    space_adjust = 0    # by how much space has been modified
    blocked = 0     # we _try_ do detect deadlocks
    cycles = 0      # it's sort of a timer
    last_update = 0 # screen updates are done once per second
    pause = 0       # no new encoders are started if pause==1
    flags = "[   ]" # runtime controllable flags
    rotate="/-\\|"
    rotate_ball=" .o0O0o."
    rot_cycle = len(rotate)
    rot_ball_cycle = len(rotate_ball)
    rot_count = 0
    global_blocks = tracksize(wavs_todo)[BLOCKS] + tracksize(mp3s_todo)[BLOCKS]
    global_start = time()
    global_done = 0 
    first_encoder = 1
    had_special = 0

    printable_names=[]
    for i in range(CDDA_MAXTRACKS):
        printable_names.append("")

    if names_available and show_names:
        max_name_len = max(map(lambda x: len(track_names[x[NUM]][1]), todo))
        max_name_len = len("01 ") + max_name_len
    else:
        max_name_len = max(map(lambda x: len(x[NAME]), todo))
    if show_time:
        max_name_len = max_name_len + 6

    for i in todo:
        if show_time:
            len_tmp = i[LEN] / CDDA_BLOCKS_PER_SECOND
            len_tmp = ("%02i:%02i") % (len_tmp / 60, len_tmp % 60)

        if names_available and show_names:
            if show_time:
                tmp = "%02i %5s " % (i[NUM], len_tmp) + track_names[i[NUM]][1]
            else:
                tmp = "%02i " % i[NUM] + track_names[i[NUM]][1]
            printable_names[i[NUM]] = tmp + "." * (max_name_len - len(tmp))
        else:
            if show_time:
                printable_names[i[NUM]] = i[NAME] + " " + len_tmp + "." * (max_name_len - len(i[NAME]) - 6)
            else:
                printable_names[i[NUM]] = i[NAME] + "." * (max_name_len - len(i[NAME]))

    if wavs_todo or mp3s_todo:          # i.e. there is work to be done
        # how big should the screen be?
        width = 80 - len("track_00") + max_name_len
        height = len(all_tracks_todo_sorted) + 3
        if curses_enable:
            default_height, default_width = stdscr.getmaxyx()
            height = height - 1
        if names_available:
            height = height + 1

        if curses_enable:
            pad_height, pad_width = len(all_tracks_todo_sorted), width
            status_pad = curses.newpad(pad_height, pad_width)
            usage_win = curses.newwin(usage_win_height, usage_win_width, 0, 0)
            map_track_num = {}
            for i in range(len(all_tracks_todo_sorted)):
                map_track_num[all_tracks_todo_sorted[i][NUM]] = i

        # adjust screen size with xtermset
        if xtermset_enable:
            system("xtermset -geom " + `width` + "x" + `height`)
            xterm_geom_changed = 1

        if curses_enable:
            max_y, max_x = stdscr.getmaxyx()

        global_options = "Options: bitrate=" + `bitrate` + " vbr" * vbr + " reorder" * reorder + " read-ahead=" + `read_ahead` + " keep-wavs" * keep_wavs + " -+- press Q to quit"
        extra_lines = 2
        if names_available:
            extra_lines = extra_lines + 1
            if curses_enable:
                global_discname = track_names[0][0] + " - " + track_names[0][1]
            else:
                global_options = center_line(track_names[0][0] + " - " + track_names[0][1], fill = "- ", fill_r = " -", width = width) + "\n" + center_line(global_options, fill = " ", fill_r = " ", width = width)
        smile = " :-)"

        if curses_enable:
            sig_winch_handler(None, None)

                           #####################
                             ### MAIN LOOP ###
                           #####################

    while mp3s_todo or enc_queue or dae_queue or enc_running or dae_running:

                            # feed in the WAVs which have been there from the start

        if mp3s_todo and tracksize(mp3s_todo[0])[MP3] < space:
            waiting_space = 0
            enc_queue.append(mp3s_todo[0])
            space = space - tracksize(mp3s_todo[0])[MP3]
            enc_stat_upd(mp3s_todo[0][NUM], "waiting for encoder.")
            mp3s_todo = mp3s_todo[1:]

                                                    # start new DAE subprocess

        elif (len(enc_queue) + enc_running) < (read_ahead + encoders) and dae_queue and dae_running < rippers and ((tracksize(dae_queue[0])[BOTH] < space) or (only_dae and tracksize(dae_queue[0])[WAV] < space) or (otf and tracksize(dae_queue[0])[MP3] < space)):
            waiting_space = 0
            this_is_ok = 1
            if pause:
                this_is_ok = 0
                dae_stat_upd(dae_queue[0][NUM], "Paused. Press 'c' to continue.")
            elif not image_file:
                all_tracks_on_cd = gettoc()
                if not cmp_toc_cd(all_tracks, all_tracks_on_cd):
                    while dae_queue:
                        track = dae_queue[0]
                        dae_queue = dae_queue[1:]
                        dae_stat_upd(track[NUM], "Wrong disc - aborting this track")
                    global_error = global_error + 1
                    this_is_ok = 0
            if this_is_ok:
                if only_dae:
                    space_waiting = space_waiting + tracksize(dae_queue[0])[WAV]
                    space = space - tracksize(dae_queue[0])[WAV]
                elif otf:
                    space_waiting = space_waiting + tracksize(dae_queue[0])[MP3]
                    space = space - tracksize(dae_queue[0])[MP3]
                else:
                    space_waiting = space_waiting + tracksize(dae_queue[0])[BOTH]
                    space = space - tracksize(dae_queue[0])[BOTH]
                dae_running = dae_running + 1
                track = dae_queue[0]
                dae_queue = dae_queue[1:]
                if otf and not image_file:
                    #XXX image_reader can't do otf at the moment.
                    dae_stat_upd(track[NUM], ":DAE: waiting for status report...")
                    if encoder == "lame":
                        enc_stat_upd(track[NUM], "[no otf status for lame]")
                    elif encoder == "gogo":
                        enc_stat_upd(track[NUM], "[no otf status for gogo]")
                    else:
                        enc_stat_upd(track[NUM], "waiting for encoder.")
                    enc_running = enc_running + 1
                    if first_encoder:
                        first_encoder = 0
                        global_start = time()
                    data = start_new_otf(track, ripper, encoder)
                    children.append(data['rip'])
                    children.append(data['enc'])
                else:
                    if enc_status[track[NUM]]:
                        enc_cache[track[NUM]] = enc_status[track[NUM]]
                        enc_stat_upd(track[NUM], "[...]")
                    dae_stat_upd(track[NUM], ":DAE: waiting for status report...")
                    if image_file:
                        children.append(ripread(track, track1_offset))
                    else:
                        children.append(start_new_ripper(track, ripper))

                                            # start new encoder subprocess

        if enc_queue and enc_running < encoders:
            if tracksize(enc_queue[0])[MP3] <= space + space_waiting:
                waiting_space = 0
                actual_load = eval(loadavg)
                if actual_load < max_load:
                    waiting_load = 0
                    enc_running = enc_running + 1
                    track = enc_queue[0]
                    enc_queue = enc_queue[1:]
                    enc_stat_upd(track[NUM], 'waiting for encoder status...')
                    children.append(start_new_encoder(track, encoder))
                    if first_encoder:
                        first_encoder = 0
                        global_start = time()
                else:
                    waiting_load = 1

                                            # check for subprocess output

        readfd=[stdin.fileno()]
        for i in children:
            readfd.append(i['fd'])
        try:
            rfd, wfd, xfd = select(readfd, [], [], update_interval)
        except:
            rfd, wfd, xfd = [], [], []
            if curses_enable:
                sig_winch_handler(None, None)

                                            # check for keyboard commands

        if stdin.fileno() in rfd:
            last_update = last_update - update_interval
            if curses_enable:
                cmd = stdscr.getkey()
            else:
                cmd = stdin.read(1)
            stdin.flush()
            if upper(cmd) == "Q":
                exit()
            elif not pause and upper(cmd) == "P":
                pause = 1
                flags = flags[:1] + "P" + flags[2:]
            elif upper(cmd) == "C" or pause and upper(cmd) == "P":
                pause = 0
                flags = flags[:1] + " " + flags[2:]
            elif not flags[3] == "e" and upper(cmd) == "E":
                for i in children:
                    if i['type'] == "encoder":
                        kill(i['pid'], signal.SIGSTOP)
                        flags = flags[:3] + "e" + flags[4:]
            elif flags[3] == "e" and upper(cmd) == "E":
                for i in children:
                    if i['type'] == "encoder":
                        kill(i['pid'], signal.SIGCONT)
                        flags = flags[:3] + " " + flags[4:]
            elif not flags[2] == "r" and upper(cmd) == "R":
                for i in children:
                    if i['type'] == "ripper":
                        kill(i['pid'], signal.SIGSTOP)
                        flags = flags[:2] + "r" + flags[3:]
            elif flags[2] == "r" and upper(cmd) == "R":
                for i in children:
                    if i['type'] == "ripper":
                        kill(i['pid'], signal.SIGCONT)
                        flags = flags[:2] + " " + flags[3:]
            elif upper(cmd) == "U":
                cycles = 29     # do periodic stuff _now_
            elif curses_enable:
                if cmd in ("j", 'KEY_DOWN') and pad_disp_start_y < pad_height - 1:
                    pad_disp_start_y = pad_disp_start_y + 1
                elif cmd in ("k", 'KEY_UP') and pad_disp_start_y > 0:
                    pad_disp_start_y = pad_disp_start_y - 1
                elif cmd in ("l", 'KEY_RIGHT') and pad_disp_start_x < pad_width - 1:
                    pad_disp_start_x = pad_disp_start_x + 1
                elif cmd in ("h", 'KEY_LEFT') and pad_disp_start_x > 0:
                    pad_disp_start_x = pad_disp_start_x - 1
                elif cmd == 'KEY_RESIZE':
                    continue
                else:
                    last_update = time()
                sig_winch_handler(None, None)
            else:
                last_update = time()

                                            # read from file with activity

        for i in children:
            if i['fd'] in rfd:
                if Linux and i['type'] != "image_reader":
                    try:
                        x = i['file'].read()
                    except IOError:
                        pass
                else:
                    read_chars = 0
                    x = ""
                    while read_chars < helpers[i['prog']]['status_blocksize']:
                        try:
                            x = x + i['file'].read(1)
                        except IOError:
                            break
                        read_chars = read_chars + 1
                        try:
                            rfd2, wfd2, xfd2 = select([i['fd']], [], [], 0.0)
                        except:
                            rfd2, wfd2, xfd2 = [], [], []
                            if curses_enable:
                                sig_winch_handler(None, None)
                        if i['fd'] not in rfd2:
                            break
                # put read data into child's buffer
                i['buf'] = (i['buf'] + x)[-helpers[i['prog']]['status_blocksize']:]

                                            # check for exiting child processes

        if children:
            respid, res = waitpid(-1, WNOHANG)
            if respid != 0:
                last_update = last_update - update_interval # ensure info is printed
                new_ch = []
                exited_proc = []
                for i in children:
                    if i['pid'] == respid:
                        if exited_proc != []:
                            print "Error: pid " + `respid` + " found at multiple child processes."
                            exit()
                        exited_proc = i
                    else:
                        new_ch.append(i)
                if not exited_proc:
                    print "Error: unknown process (" + `respid` + ") has exited."
                    exit()
                children = new_ch
                x = ""
                try:
                    x = exited_proc['file'].read()
                except IOError:
                    pass
                except ValueError:
                    pass
                exited_proc['buf'] = (exited_proc['buf'] + x)[-helpers[exited_proc['prog']]['status_blocksize']:]
                exited_proc['file'].close()

                global_error = global_error + res
                track = exited_proc['track']
                num = track[NUM]
                stop_time = time()
                speed = ( track[LEN] / float(CDDA_BLOCKS_PER_SECOND)) / ( stop_time - exited_proc['start_time'] )

                if exited_proc['type'] == "ripper" or exited_proc['type'] == "image_reader":
                    dae_running = dae_running - 1
                    if exec_when_done and exited_proc['type'] == "ripper" and dae_running == 0 and len(dae_queue) == 0:
                        system(exec_rip_done)
                    if not res:
                        if not exited_proc['otf']:
                            if path.exists(track[NAME] + ".wav"):
                                if tracksize(track)[WAV] != filesize(track[NAME] + ".wav"):
                                    res = 242
                                    dae_stat_upd(num, strip(split(exited_proc['buf'], "\n")[-2]))
                            else:
                                dae_stat_upd(num, strip(split(exited_proc['buf'], "\n")[-2]))
                                res = 243
                            global_error = global_error + res
                    if res:
                        if path.exists(track[NAME] + ".wav"):
                            remove(track[NAME] + ".wav")
                            space = space + tracksize(track)[WAV]
                            if otf:
                                kill(exited_proc['otf-pid'], signal.SIGTERM)
                                if path.exists(track[NAME] + ".mp3"):
                                    remove(track[NAME] + ".mp3")
                                space = space + tracksize(track)[MP3]
                            if not otf and not only_dae and track not in mp3s_ready:
                                space = space + tracksize(track)[MP3]
                            dae_stat_upd(num, 'DAE failed with status ' + `res` + ", wav removed.")
                    else:
                        if exited_proc['type'] == "image_reader":
                            dae_stat_upd(num, strip(split(exited_proc['buf'], "\n")[-2]))
                        else:
                            if exited_proc['otf'] and helpers[exited_proc['prog']].has_key('otf-final_status_fkt'):
                                exec(helpers[exited_proc['prog']]['otf-final_status_fkt'])
                            else:
                                exec(helpers[exited_proc['prog']]['final_status_fkt'])
                            dae_stat_upd(num, final_status)
                        if enc_cache[num]:
                            enc_stat_upd(num, enc_cache[num])
                            enc_cache[num] = ""
                        progress(num, "dae", dae_status[num])
                        if not otf and not only_dae and track not in mp3s_ready:
                            if waiting_space:
                                mp3s_todo.append(track)
                                space = space + tracksize(track)[MP3]
                            else:
                                enc_stat_upd(num, 'waiting for encoder.')
                                enc_queue.append(track)
                    space_waiting = space_waiting - tracksize(track)[WAV]

                elif exited_proc['type'] == "encoder":
                    enc_running = enc_running - 1
                    if not res and tracksize(track)[MP3] * 0.99 > filesize(track[NAME] + ".mp3"):
                        res = 242
                        global_error = global_error + res
                    if res:
                        global_blocks = global_blocks - exited_proc['track'][LEN]
                        global_start = global_start + exited_proc['elapsed'] / (enc_running + 1)
                        if global_start > time():
                            global_start = time()
                        if path.exists(track[NAME] + ".mp3"):
                            # mp3enc doesn't report errors when out of discspace...
                            remove(track[NAME] + ".mp3")
                        space = space + tracksize(track)[MP3]
                        enc_stat_upd(num, 'coding failed, err#' + `res`)
                    else:
                        global_done = global_done + exited_proc['track'][LEN]
                        enc_stat_upd(num, "[coding @" + '%1.2f' % speed + "x done, mp3 OK]")
                        if not otf and not keep_wavs:
                            remove(track[NAME] + ".wav")
                            space = space + tracksize(track)[WAV]
                        progress(num, "enc", `track[RATE]`, enc_status[num])

                else:
                    print "Error: child process of unknown type (" + exited_proc['type'] + ") exited."
                    exit()
                if global_error:
                    smile = " :-["

        if last_update + update_interval <= time():
            last_update = time()

                                                # interpret subprocess output

            for i in children:
                if i['type'] == "ripper":
                    if len(i['buf']) == helpers[i['prog']]['status_blocksize']:
                        if i['otf'] and helpers[i['prog']].has_key('otf-status_fkt'):
                            exec(helpers[i['prog']]['otf-status_fkt'])
                        else:
                            exec(helpers[i['prog']]['status_fkt'])
                        if new_status:
                            dae_stat_upd(i['track'][NUM], ":DAE: " + new_status)
        
                elif i['type'] == "encoder":
                    if len(i['buf']) == helpers[i['prog']]['status_blocksize']:
                        exec(helpers[i['prog']]['percent_fkt'])
                        i['percent'] = percent
                        if percent > 0:
                            elapsed = time() - i['start_time']
                            i['elapsed'] = elapsed
                            speed = ((track[LEN] / float(CDDA_BLOCKS_PER_SECOND)) * ( percent / 100 )) / elapsed
                            eta = (100 - percent) * elapsed / percent
                            eta_ms = "%02i:%02i" % (eta / 60, eta % 60)
                            enc_stat_upd(i['track'][NUM], '%2i%% done, ETA:%6s, %5.2fx' % (percent, eta_ms, speed))
        
                elif i['type'] == "image_reader":
                    line = split(i['buf'], "\n")
                    if len(line) >= 2:
                        line = strip(line[-2])
                        dae_stat_upd(i['track'][NUM], line)
                        if line[:5] == "Error":
                            global_error = global_error + 1
        
                else:
                    print "Error: unknown subprocess type \"" + i['type'] + "\"."
                    exit()

            cycles = cycles + 1
            if cycles % 30 == 0:
                if recheck_space and not space_set_from_argv:
                    actual_space = df()
                    if space_adjust:
                        diff = actual_space - space
                        if diff > space_adjust:
                            space = space + space_adjust
                            space_adjust = 0
                            waiting_space = 0
                        else:
                            space = space + diff
                            space_adjust = space_adjust - diff
                    else:
                        if actual_space < space:
                            space_adjust = space - actual_space
                            space = actual_space

            if space_adjust and enc_running == 0 and dae_running == 0:
                waiting_space = waiting_space + 1
            if not waiting_space >= 2 and not waiting_load and enc_running == 0 and dae_running == 0:
                blocked = blocked + 1
            else:
                blocked = 0

            total_done = global_done
            for i in children: 
                total_done = total_done + (i['percent'] / 100) * i['track'][LEN]
            elapsed = time() - global_start
            if global_blocks > 0:
                percent = total_done / global_blocks
            else:
                percent = 0
            if percent > 0 and elapsed > 40:
                eta = ((1 - percent) * elapsed / percent)
                eta_hms = " ETA=%i:%02i:%02i" % (eta / 3600, (eta % 3600) / 60, eta % 60)
            else:
                eta_hms = ""

            if strip(flags[1:-1]):
                print_flags = " " + flags
            else:
                print_flags = ""
            if dae_running:
                rot = rotate_ball[rot_count % rot_ball_cycle]
            else:
                rot = rotate[rot_count % rot_cycle]
            rot_count = rot_count + 1

                                                            # print status

            if blocked > 2:
                special_line = " ...I feel blocked - quit with 'q' if you get bored... "
                if blocked > 5:
                    space = df() - keep_free
            elif waiting_load and waiting_space >= 2:
                special_line = " ...waiting for load (" + `actual_load` + ") < " + `max_load` + " and for " + pprint_i(space_adjust, "%i %sBytes") + " to be freed... "
            elif waiting_space >= 2:
                special_line = " ...waiting for " + pprint_i(space_adjust, "%i %sBytes") + " to be freed.... "
            elif waiting_load:
                special_line = " ...waiting for load (" + `actual_load` + ") to drop below " + `max_load`+"... "
            else:
                special_line = None

            bottom_line =  "(" + rot + ") " + "SPACE:" * (space_adjust != 0) + "space:" * (space_adjust == 0) + pprint_i(space, "%i%sB") + " waiting_WAVs:" + `len(enc_queue)` + " DAE:" + `rippers - dae_running` + "+" + `dae_running` + " ENC:" + `encoders - enc_running` + "+" + `enc_running` + eta_hms + " errors: " + `global_error` + smile + print_flags

            if curses_enable:
                if special_line and not had_special:
                    had_special = 1
                    extra_lines = extra_lines + 1
                    sig_winch_handler(None, None)
                elif had_special and not special_line:
                    had_special = 0
                    extra_lines = extra_lines - 1
                    sig_winch_handler(None, None)
                if 1 < max_y:
                    stdscr.addstr(max_y - 1, 0, (bottom_line + " " * (max_x - len(bottom_line)))[:max_x - 1], curses.A_REVERSE)
                    # why can't I put a char in the bootom right corner?
                    #stdscr.addch(max_y, max_x, "x")
                    status_pad.refresh(pad_y, pad_x, pad_start_y, pad_start_x, pad_end_y, pad_end_x)
                    stdscr.refresh()
            elif not silent_mode:
                print
                print
                if special_line:
                    print center_line(special_line, fill = "#", width = width)
                print global_options
                for i in all_tracks_todo_sorted:
                    print printable_names[i[NUM]] + ": " + dae_status[i[NUM]], enc_status[i[NUM]]
                print bottom_line

    # end of main loop #########################################################
    # if we came this far, all work is done. ###################################

try:
    if curses_enable and (wavs_todo or mp3s_todo):
        # Initialize curses
        stdscr = curses.initscr()
        curses_init = 1
        curses_sighandler = signal.signal(signal.SIGWINCH, signal.SIG_IGN)
        # Turn off echoing of keys, and enter cbreak mode,
        # where no buffering is performed on keyboard input
        curses.noecho() ; curses.cbreak()

        # In keypad mode, escape sequences for special keys
        # (like the cursor keys) will be interpreted and
        # a special value like curses.KEY_LEFT will be returned
        stdscr.keypad(1)
        stdscr.leaveok(0)
    main_loop(mp3s_todo, wavs_todo, space, dae_queue, enc_queue, enc_running, dae_running)
    if curses_init:
        # Set everything back to normal
        stdscr.keypad(0)
        #XXX stdscr.move(max_y - 1, 0)
        curses.echo() ; curses.nocbreak()
        curses.endwin()                 # Terminate curses
except SystemExit:
    if curses_init:
        stdscr.keypad(0)
        #XXX stdscr.move(max_y - 1, 0)
        curses.echo() ; curses.nocbreak()
        curses.endwin()
    print "--- Last status: ---------------------------------------------------------------"
    if max_x and max_x >= pad_width:
        print_status()
    else:
        print_status(form = 'short')
    sys.exit()
except:
    if curses_init:
        # In the event of an error, restore the terminal
        # to a sane state.
        stdscr.keypad(0)
        #XXX stdscr.move(max_y - 1, 0)
        curses.echo() ; curses.nocbreak()
        curses.endwin()
    traceback.print_exc()         
    exit()

if xtermset_enable and xterm_geom_changed:
    system("xtermset -fg \\#000000 -restore")
    for i in range(0, 255, 16):
        color = "#" + "%x" % i * 3
        system("xtermset -fg black -bg \\" + color)

if query_when_ready:
    print "Info: querying..."
    if freedb_query(freedb_id(all_tracks), all_tracks, freedb_form_file):
        exit()

if query_when_ready or read_freedb_file or query_on_start:
    err, track_names, freedb_rename = interpret_db_file(all_tracks, freedb_form_file, various, verb = 1, dirs = 1)
    if err:
        print "Error: could not read freedb file, aborting."
        exit()

if curses_enable:
    print "The final status was:"
    if max_x and max_x >= pad_width:
        print_status()
    else:
        print_status(form = 'short')

if names_available:
    a_artist = track_names[0][0]
    a_title = track_names[0][1]

if global_error:
    print "Error: aborting because of previous error(s) [%i]." % global_error
    if exec_when_done:
        system(exec_err)
    exit()

elif set_id3tag or freedb_rename:
    for i in all_tracks_todo_sorted:
        mp3name = i[NAME] + ".mp3"
        wavname = i[NAME] + ".wav"
        t_artist = track_names[i[NUM]][0]
        t_name = track_names[i[NUM]][1]
        t_comm = ""
        if set_id3tag:
            if len(t_name) > 30:
                if find(t_name, "(") != -1 and find(t_name, ")") != -1:
                    # we only use the last comment
                    t_comm = split(t_name, "(")[-1]
                    if t_comm[-1] == ")":
                        t_comm = t_comm[:-1]
                        if t_comm[-1] == " ":
                            t_comm = t_comm[:-1]
                        t_name2 = replace(t_name, " (" + t_comm + ") ", "")
                        t_name2 = replace(t_name2, " (" + t_comm + ")", "")
                        t_name2 = replace(t_name2, "(" + t_comm + ") ", "")
                        t_name2 = replace(t_name2, "(" + t_comm + ")", "")
                    else:
                        t_comm = ""
            id3 = ID3(mp3name)
            id3.album = a_title
            if t_comm:
                id3.comment = t_comm
                id3.title = t_name2
            else:
                id3.title = t_name
            if t_artist:
                id3.artist = t_artist
            else:
                id3.artist = a_artist
            id3.write()
        if freedb_rename:
            if t_artist:    # 'Various Artists'
                replacelist = (("%n", rename_num % i[NUM]), ("%a", t_artist), ("%t", t_name), ("%l", a_title))
                newname = multi_replace(rename_fmt_va, replacelist)
                
                #newname = replace(newname, "%", "%_jack_%")
                #newname = replace(newname, "%_jack_%a", t_artist)
                #newname = replace(newname, "%_jack_%t", t_name)
                #newname = replace(newname, "%_jack_%l", a_title)
            else:
                replacelist = (("%n", rename_num % i[NUM]), ("%a", a_artist), ("%t", t_name), ("%l", a_title))
                newname = multi_replace(rename_fmt, replacelist)
                #newname = replace(newname, "%", "%_jack_%")
                #newname = replace(newname, "%_jack_%a", a_artist)
                #newname = replace(newname, "%_jack_%t", t_name)
                #newname = replace(newname, "%_jack_%l", a_title)
            #newname = replace(newname, "%_jack_%", "%")
            #if find(newname, "%") != -1:
                #newname = newname % i[NUM]
            for char_i in range(len(unusable_chars)):
                newname = replace(newname, unusable_chars[char_i], replacement_chars[char_i])
            if i[NAME] != newname:
                ok = 1
                if path.exists(newname + ".mp3"):
                    ok = 0
                    print 'NOT renaming "' + mp3name + '" to "' + newname + ".mp3" + '" because dest. exists.'
                    if keep_wavs:
                        print 'NOT renaming "' + wavname + '" to "' + newname + ".wav" + '" because MP3 dest. exists.'
                elif keep_wavs and path.exists(newname + ".wav"):
                    ok = 0
                    print 'NOT renaming "' + wavname + '" to "' + newname + ".wav" + '" because dest. exists.'
                    print 'NOT renaming "' + mp3name + '" to "' + newname + ".mp3" + '" because WAV dest. exists.'
                if ok:
                    rename(mp3name, newname + ".mp3")
                    if keep_wavs:
                        rename(wavname, newname + ".wav")
                    progress(i[NUM], "ren", "%s-->%s" % (i[NAME], newname))
                elif silent_mode:
                    progress(i[NUM], "err", "while renaming track")

if not silent_mode:
    if names_available:
        print "Done with \"" + a_artist+ " - " + a_title + "\"."
    else:
        print "All done."

if progress_changed:
    progress("all", "done", strftime("%b %2d %H:%M:%S", localtime(time())))

if remove_files:
    for i in [progress_file, toc_file, def_toc, freedb_form_file, freedb_form_file+".bak"]:
        if path.exists(i):
            remove(i)

if exec_when_done:
    system(exec_no_err)

exit()      # call the cleanup function & exit



###############################################################################
##################################         ####################################
##################################  T H E  ####################################
##################################  E N D  ####################################
##################################         ####################################
###############################################################################
