#!/usr/bin/env python
### jack - extract audio from a CD and MP3ify it using 3rd party software
### Copyright (C) 1999  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 ###

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

prog_version = "2.0.1"
prog_name = "jack"

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 path, execv
import os, sys
from sys import argv, exit, stdin
from time import time, sleep
from stat import ST_SIZE
from array import array
from sndhdr import whathdr
from urllib import urlopen, quote_plus
from select import select
from ID3 import ID3
from fcntl import fcntl
import FCNTL
from termios import tcgetattr, tcsetattr
import TERMIOS
import signal
import pty
import wave

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 or tosha
toc_prog = ripper	# use which program to read cd's toc
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

rename_fmt = "%02i.%a (%l) - %t"# MP3s are renamed to "01.Artist (Album) - Tracktitle.mp3"
rename_fmt_va = "%02i.%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_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>".
rename_underscore = " "		# set this to "_" if you want spaces
				# replaced by "_"

recurse_dirs = 2		# search how deep for workdir
#searchdirs = ["/master/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_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	#
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 (blade, lame, l3enc, mp3enc)
bitrate = 160		# default bitrate
encoders = 1		# encode how many mp3s in parallel
create_dirs = 0		# 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
remove_files = 0	# remove jack.* files when done

			# 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

## 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 df for discspace to use (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
upd_progress = 0	# regenerate progress file if "lost"

## Misc stuff ##
name = "track_%02i"	# filename template (before renaming)
rippers = 1		# not implemented: rip in parallel

## 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
username = environ['USER'] # 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!
image_file = ""		# normal operation: DAE from CD, not from image
toc_file = prog_name + ".toc"	# if noexistand, create on startup
def_toc = prog_name + ".toc"	# cache toc here
freedb_form_file = prog_name + ".freedb" # name of submission template
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'] = 100

helpers['mp3enc'] = {}
helpers['mp3enc']['type'] = "encoder"
helpers['mp3enc']['cmd'] = "mp3enc -v -qual 9 -br %r -if %i -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) >= 2: s = s[-2]
if len(s) == 1: s = s[0]
if find(s, "%") >= 0:
  y = split(s, " ", 3)
  percent = atof(y[0]) / atof(y[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']['status_blocksize'] = 99
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, "Frame:") >= 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['blade'] = {}
helpers['blade']['type'] = "encoder"
helpers['blade']['cmd'] = "bladeenc %i %o -br %r"
helpers['blade']['status_blocksize'] = 180
helpers['blade']['bitrate_factor'] = 1000
helpers['blade']['status_start'] = "%"
helpers['blade']['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 /dev/cdrom %n %o"
helpers['cdparanoia']['status_blocksize'] = 500
helpers['cdparanoia']['status_start'] = "%"
helpers['cdparanoia']['status_fkt'] = \"\"\"
new_status = split(i['buf'], '\015')[-2][17:68]
\"\"\"
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']['toc'] = 1
helpers['cdparanoia']['toc_cmd'] = "cdparanoia -d /dev/cdrom -Q 2>&1"
helpers['cdparanoia']['toc_fkt'] = \"\"\"
while l:
  l = rstrip(l)
  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] == 'yes', l[5] == 'yes', atoi(l[6]), 1, bitrate, name % num])
  l = p.readline()
\"\"\"

helpers['tosha'] = {}
helpers['tosha']['type'] = "ripper"
helpers['tosha']['cmd'] = "tosha -d /dev/cdrom -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 /dev/cdrom -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()
\"\"\"
"""

# read the defaults
exec(prefs)
if path.exists(prefs_file):
  execfile(prefs_file)
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(prefs)
  f.close()
  print "    : done. now edit this file and start " + prog_name + " again."
  exit()

# 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
global_error = 0	# remember if something went wrong
children = []		# subprocess info
old_tc = tcgetattr(stdin.fileno())	# terminal attributes
all_tracks_todo_sorted = []
flags = "[   ]"		# runtime controllable flags
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


# 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
CHILD = 0
CDDA_MAXTRACKS = 100

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

def 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 + "\"\n       : 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"
  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(helpers[toc_prog]['toc_cmd'])
  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 " + dae_prog + " 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 = []
  f = open(progress_file, "w")
  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", "")])
    f.write(`num` + progr_sep + "dae" + progr_sep + "[simulated]\n")
    f.write(`num` + progr_sep + "enc" + progr_sep + `x['bitrate']` + progr_sep +"[simulated]\n")
    f.write(`num` + progr_sep + "ren" + progr_sep + "track_%02i" % num + "-->" + replace(path.basename(i), ".mp3", "") + "\n")
    num = num + 1
    start = start + blocks
  f.close()
  return erg

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 cdrdao_gettoc(tocfile):		# get toc from cdrdao-style toc-file
  "Returns track list and datafile name, needs name of toc-file"
### warning: this only supports machine-generated tocfiles as done by cdrdao 1.1.0 ##
  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()
      if num == 1:
        pregap = timestrtoblocks(buf[6:]) - track1_pregap
      else:
        pregap = timestrtoblocks(buf[6:])
      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

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()
    if i[NUM]==1 and i[START]:
      f.write("SILENCE "+msftostr(blockstomsf(i[START]))+"\n")
    f.write('FILE "' + i[NAME] + '.wav" 0 ')
    x = blockstomsf(i[LEN])
    f.write("%02i" % x[B_MM] + ":" + "%02i" % x[B_SS] + ":" + "%02i" % x[B_FF] + "\n\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]) == type(1):	# how does this really work?
    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 = []
  for i in tracks:
    cdtoc.append(blockstomsf(i[START] + 150))
  cdtoc.append(blockstomsf(tracks[-1][START] + tracks[-1][LEN]))

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

  return "%x" % ((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] + 150` + "\n")
  f.write("#\n# Disc length: " + `(tracks[-1][START] + tracks[-1][LEN]) / CDDA_BLOCKS_PER_SECOND + 2`)
  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):
  qs = "cmd=cddb query " + id + " " + `len(tracks)` + " " # query string
  for i in tracks:
    qs = qs + `i[START] + 150` + " "
  qs = qs + `(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):
  "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...
  line = f.readline()
  while line:
    line = rstrip(line)
    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]):
              freedb[buf[0]] = freedb[buf[0]] + buf[1]
            else:
              freedb[buf[0]] = buf[1]
    line = f.readline()

  for i in tracks:	# check that info is there for all tracks
    if not freedb.has_key("TTITLE%i" % (i[NUM] - 1)):	# -1 bec 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:
      print "Warning: calculated id (" + id + ") and id from freedb file\n       : (", read_ids, ")\n       : 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]) == "VARIOUS ARTISTS":
    if not various:
      various = 1
    elif various == 2:
      various = 0

  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:
        buf = split(buf, " - ", 1)
        buf[0] = replace(buf[0], "/", " - ")
        buf[1] = replace(buf[1], "/", " - ")
        names.append(buf)
      elif find(buf, "/") != -1:	# uncommon but seen
        buf = split(buf, "/", 1)
        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:
    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..."

###################################################################### 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['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")
    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]
  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 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:
    print ":fAE: waiting for status report..."
    hdr = whathdr(image_file)
    if not hdr:
      hdr = ('cdr', 44100, 2, -1, 16)	# Unknown header, assuming cdr
    if (hdr[0] == 'cdr' and filesize(image_file) != tracksize(all_tracks)[CDR]) or (hdr[0] == 'wav' and filesize(image_file) != tracksize(all_tracks)[CDR] + 44):
      print "Error: image file size mismatch, aborted."
      exit(1)
    elif hdr[0] == 'wav' and (hdr[1], hdr[2], hdr[4]) != (44100, 2, 16):
      print "Error: unsupported WAV, need CDDA_fmt, aborted."
      exit(2)
    elif hdr[0] not in ('wav', 'cdr'):
      print "Error: unsupported: " + hdr[0] + ", aborted."
      exit(3)
    else:
      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')
      if hdr[0] == 'cdr':
        f.seek((track[START] - offset) * CDDA_BLOCKSIZE)
      elif hdr[0] == 'wav':
        f.seek((track[START] - offset) * CDDA_BLOCKSIZE + 44)
      for i in range(0, track[LEN]):
        buf = array("h")
        buf.fromfile(f, 1176) # CDDA_BLOCKSIZE / 2
        if not swap_byteorder:	# this is inverted as WAVE swabs them anyway.
          buf.byteswap()
        wav.writeframesraw(buf.tostring())
        if not i%1000:
          print ":fAE: Block " + `i` + "/" + `track[LEN]`
      wav.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!]"
      exit()

  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
  return data

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):
  if sig:
    print "Info: signal", sig, "caught, exiting."
  tcsetattr(stdin.fileno(), TERMIOS.TCSADRAIN, old_tc)
  for i in children:
    print "Info: killing " + i['type'] + " (pid " + `i['pid']` + ")"
    kill(i['pid'], signal.SIGTERM)				# make this portable!
    i['file'].close()
  if xtermset_enable and xterm_geom_changed:
    if len(all_tracks_todo_sorted) + 3 <= default_height:
      new_height = default_height
    else:
       new_height = height
    if restore_xterm_width:
      new_width = default_width
    else:
      new_width = width
    system("xtermset -fg black -bg white -restore -geom "+ `new_width`+ "x" + `new_height`)
  sys.exit()

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

def mkdirname(names, template, underscore):
  "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:
    x = replace(i, "%a", names[0][0])
    x = replace(x, "%l", names[0][1])
    x = replace(x, " ", underscore)
    dirs2.append(x)
  name = ""
  for i in dirs2:
    name = path.join(name, i)
  return dirs2, name

def interpret_db_file(all_tracks, freedb_form_file, various, verb):
  "read freedb file and rename dir(s)"
  names_available = 0
  freedb_rename = 0
  err, track_names, id = freedb_names(freedb_id(all_tracks), all_tracks, freedb_form_file, various, verb = verb)
  if not err:
    freedb_rename = 1
    if rename_dir:
      old_dir = os.getcwd()
      chdir(os.pardir)
      dir_names, dir_name = mkdirname(track_names, dir_template, rename_underscore)
      for i in dir_names[:-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(old_dir, dir_names[-1])
      chdir(dir_names[-1])
      print "Info: cwd now", os.getcwd()
    names_available = 1
  else:
    freedb_rename = 0
  return err, track_names, freedb_rename, names_available

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

space_set_from_argv = 0
argv_bitrate = 0
if find(my_mail, "@") <= 1:
  my_mail = username + "@" + hostname

#### Parse argv
i = 1
tracks = ""
guess_mp3s = []
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 ("-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] == "--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] =="--force":
    force = 1
  elif argv[i] =="--various":
    various = 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 ("-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 ("-u", "--undo-rename"):
    undo_rename = 1
  elif argv[i] == "--todo":
    todo_exit = 1
  else:
    if not argv[i] in ("-h", "-help", "--help"):
      print "unknown option", argv[i]
    print "This is ", prog_name, prog_version, "(C) 1999  Arne Zellentin <arne@unix-ag.org>"
    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 "  -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 [blade, 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(freedb_query) + ")"
    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 "  -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 '  -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:       submit freedb file to server and exit (" + yes(freedb_submit) + ")"
    print "      --server:      *use freedb server (" + freedb_server + ")"
    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 "      --no-various:   force no various artists, override autodetection (" + yes(various == 2) + ")"
    print "      --todo:         print what would be done and exit (" + yes(todo_exit) + ")"
    print """
  When a freedb query is done, files are renamed and tagged accordingly.

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

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()

if not path.exists(toc_file):
  if guess_mp3s:
    all_tracks = guesstoc(guess_mp3s)
  else:
    all_tracks = gettoc()

    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 = []
    for i in dirs:
      if path.exists(path.join(i, toc_file)):
        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)
    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)
        mkdir(dir_name)
        chdir(dir_name)
    
      cdrdao_puttoc(toc_file, all_tracks, freedb_id(all_tracks))
      freedb_template(all_tracks) # generate freedb form if tocfile is created..

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.

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, names_available = interpret_db_file(all_tracks, freedb_form_file, various, verb = query_on_start)
if err and query_on_start:
  print "Error: query on start failed to give a good freedb file, aborting."
  exit()

							# Parse tracks from argv

if not tracks:
  todo = []
  for i in all_tracks:
    todo.append(i)
else:
  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]] = ""

						# read progress info into status

status = {}
for i in todo:
  num = i[NUM]
  status[num] = {}
  status[num]['dae'] = status[num]['enc'] = status[num]['ren'] = status[num]['undo'] = ""
  status[num]['names'] = []
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, 2)
    status[atoi(buf[0])][buf[1]] = buf[2]
  f.close()


				# update progress file at user's request

if upd_progress:
  f = open(progress_file, "a")
  for i in todo:
    num = i[NUM]
    if not status[num]['dae']:
      if path.exists(i[NAME] + ".wav"):
        status[num]['dae'] = "[simulated]"
        f.write(`num` + progr_sep + "dae" + progr_sep + status[num]['dae'] + "\n")
    if not status[num]['enc']:
      if path.exists(i[NAME] + ".mp3"):
        x = mp3format(i[NAME] + ".mp3")
        status[num]['enc'] = `x['bitrate']` + progr_sep + "[simulated]"
        f.write(`num` + progr_sep + "enc" + progr_sep + status[num]['enc'] + "\n")
					# read status from prev. runs (if any)

if path.exists(progress_file):
  old_names=[]
  for i in range(CDDA_MAXTRACKS):
    old_names.append(0)
  f = open(progress_file, "r")
  x = f.readline()
  while x:
    x = rstrip(x)
    x = split(x, progr_sep)
    num = atoi(x[0])
    if x[1] == "dae":
      dae_status[num] = x[2]
    elif x[1] == "enc":
      enc_status[num] = x[-1]
      for i in todo:
        if i[NUM] == num:
          if len(x) == 4:	# in previous versions, bitrate was not cached
            i[RATE] = atoi(x[2])
          else:
            i[RATE] = bitrate
    elif x[1] == "ren":
      x = split(x[2], "-->", 1)
      old_name, new_name = x
      for i in todo:
        if i[NUM] == num and i[NAME] == old_name:
          i[NAME] = new_name
          if old_names[i[NUM]]:
            old_names[i[NUM]].append(new_name)
          else:
            old_names[i[NUM]] = [old_name, new_name]
    elif x[1] == "undo":
      for i in todo:
        if i[NUM] == num:
          i[NAME] = old_names[i[NUM]][-2]
          if len(old_names[i[NUM]]) <= 2:
            old_names[i[NUM]] = 0
          else:
            old_names[i[NUM]] = old_names[i[NUM]][:-1]
    x = f.readline()

								# undo renaming

  if undo_rename:
    f = open(progress_file, "a")
    for i in todo:
      if old_names[i[NUM]]:
        new_name, old_name = old_names[i[NUM]][-2:]
        new_name, old_name = new_name + ".mp3", old_name + ".mp3"
        if not path.exists(old_name):
          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:
            f.write(`i[NUM]` + progr_sep + "undo" + progr_sep + "\n")
            rename(old_name, new_name)
    f.close()
    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 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]
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:
  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)

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

max = 0
for i in todo:
  if names_available and show_names:
    if len(track_names[i[NUM]][1]) > max:
      max = len("01 " + track_names[i[NUM]][1])
  else:
    if len(i[NAME]) > max:
      max = len(i[NAME])
for i in todo:
  if names_available and show_names:
    tmp = "%02i " % i[NUM] + track_names[i[NUM]][1]
    printable_names[i[NUM]] = tmp + "." * (max - len(tmp))
  else:
    printable_names[i[NUM]] = i[NAME] + "." * (max - len(i[NAME]))

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

width = 80 - 8 + max
height = len(all_tracks_todo_sorted) + 3
if names_available:
  height = height + 1
if xtermset_enable and (wavs_todo or mp3s_todo):
  system("xtermset -geom " + `width` + "x" + `height`)
  xterm_geom_changed = 1

global_options = "Options: bitrate=" + `bitrate` + " reorder" * reorder + " read-ahead=" + `read_ahead` + " keep-wavs" * keep_wavs + " -+- press Q to quit"
if names_available:
  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 = " :-)"

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)

new = tcgetattr(stdin.fileno())			# set terminal attributes
new[3] = new[3] & ~TERMIOS.ECHO
new[3] = new[3] & ~TERMIOS.ICANON

if wavs_todo or mp3s_todo:
  tcsetattr(stdin.fileno(), TERMIOS.TCSADRAIN, new)


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

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
rotate="/-\\|"
rot_cycle = len(rotate)
rot_count = 0
global_done = 0	# 
global_blocks = tracksize(wavs_todo)[BLOCKS] + tracksize(mp3s_todo)[BLOCKS]
global_start = time()
first_encoder = 1


### 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_status[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)):
    waiting_space = 0
    this_is_ok = 1
    if pause:
      this_is_ok = 0
      dae_status[dae_queue[0][NUM]] = "Paused. Press 'c' to continue."
    if 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_status[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]
      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 enc_status[track[NUM]]:
        enc_cache[track[NUM]] = enc_status[track[NUM]]
        enc_status[track[NUM]] = " - MP3 done."
      dae_status[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_status[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'])
  rfd, wfd, xfd = select(readfd, [], [], update_interval)

						# check for keyboard commands

  if stdin.fileno() in rfd:
    last_update = last_update - update_interval
    cmd = stdin.read(1)
    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_

						# read from file with activity

  for i in children:
    if i['fd'] in rfd:
      if Linux:
        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
          rfd, wfd, xfd = select([i['fd']], [], [], 0.0)
          if i['fd'] not in rfd:
            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:
      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
      exited_proc['buf'] = (exited_proc['buf'] + x)[-helpers[exited_proc['prog']]['status_blocksize']:]
      exited_proc['file'].close()

      global_error = global_error + res
      if global_error:
        smile = " :-["
      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 res:
          if path.exists(track[NAME] + ".wav"):
            remove(track[NAME] + ".wav")
            space = space + tracksize(track)[WAV]
            if not only_dae and track not in mp3s_ready:
              space = space + tracksize(track)[MP3]
            dae_status[num] = 'DAE failed with status ' + `res` + ", wav removed."
        else:
          if exited_proc['type'] == "image_reader":
            pass
          else:
            exec(helpers[exited_proc['prog']]['final_status_fkt'])
            dae_status[num] = final_status
          if enc_cache[num]:
            enc_status[num] = enc_cache[num]
            enc_cache[num] = ""
          f = open(progress_file, "a")
          f.write(`num` + progr_sep + "dae" + progr_sep + dae_status[num] + "\n")
          f.close()
          if not only_dae and track not in mp3s_ready:
            if waiting_space:
              mp3s_todo.append(track)
              space = space + tracksize(track)[MP3]
            else:
              enc_status[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 res or tracksize(track)[MP3] * 0.99 > filesize(track[NAME] + ".mp3"):
          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_status[num] = 'coding failed, err#' + `res`
        else:
          global_done = global_done + exited_proc['track'][LEN]
          enc_status[num] = "[coding @" + '%1.2f' % speed + "x done, mp3 OK]"
          if not keep_wavs:
            remove(track[NAME] + ".wav")
            space = space + tracksize(track)[WAV]
          f = open(progress_file, "a")
          f.write(`num` + progr_sep + "enc" + progr_sep + `track[RATE]` + progr_sep + enc_status[num] + "\n")
          f.close()

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


  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']:
	  exec(helpers[i['prog']]['status_fkt'])
	  dae_status[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_status[i['track'][NUM]] = '%2i%% done, ETA:%6s, %5.2fx' % (percent, eta_ms, speed)
  
      elif i['type'] == "image_reader":
	line = strip(split(i['buf'], "\n")[-2])
	dae_status[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
    percent = total_done / global_blocks
    if percent > 0:
      eta = ((1 - percent) * elapsed / percent)
      eta_hms = " ETA=%i:%02i:%02i" % (eta / 3600, (eta % 3600) / 60, eta % 60)
      #XXX eta_hms = `percent`
    else:
      eta_hms = ""

    if strip(flags[1:-1]):
      print_flags = " " + flags
    else:
      print_flags = ""

    rot = rotate[rot_count % rot_cycle]
    rot_count = rot_count + 1

								# print status

    print
    print
    if blocked > 2:
      print center_line(" ...I feel blocked - quit with 'q' if you get bored... ", fill = "#")
      if blocked > 5:
        space = df() - keep_free
    elif waiting_load and waiting_space >= 2:
      print center_line(" ...waiting for load (" + `actual_load` + ") < " + `max_load` + " and for " + pprint_i(space_adjust, "%i %sBytes") + " to be freed... ", fill = "#")
    elif waiting_space >= 2:
      print center_line(" ...waiting for " + pprint_i(space_adjust, "%i %sBytes") + " to be freed.... ", fill = "#")
    elif waiting_load:
      print center_line(" ...waiting for load (" + `actual_load` + ") to drop below " + `max_load`+"... ", fill = "#")
    else:
      print global_options
    for i in all_tracks_todo_sorted:
      print printable_names[i[NUM]] + ": " + dae_status[i[NUM]], enc_status[i[NUM]]
    print "(" + rot + ") " + "SPACE:" * (space_adjust != 0) + "space:" * (space_adjust == 0) + pprint_i(space, "%i%sB") + " waiting_WAVs:" + `len(enc_queue)`, "idle: DAE*" + `rippers - dae_running` + " ENC*" + `encoders - enc_running` + eta_hms + " errors: " + `global_error` + smile + print_flags
  else:
    #sleep(1)
    dummy, dummy, dummy = select([sys.stdin.fileno()], [], [], update_interval)
  
# end of main loop ############################################################
# if we came this far, all work is done. ######################################

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 read_freedb_file:
  err, track_names, freedb_rename, names_available = interpret_db_file(all_tracks, freedb_form_file, various, verb = query_when_ready)
  if err and query_when_ready:
    print "Error: could not read freedb file, aborting."
    exit()

if global_error:
  print "Error: aborting because of previous error(s)."
  exit()
elif set_id3tag or freedb_rename:
  f = open(progress_file, "a")
  a_artist = track_names[0][0]
  a_title = track_names[0][1]
  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:
          t_comm = split(t_name, "(")[-1]	# we only use the last comment
          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'
        newname = rename_fmt_va
        newname = replace(newname, "%a", t_artist)
        newname = replace(newname, "%t", t_name)
        newname = replace(newname, "%l", a_title)
        if find(newname, "%") != -1:
          newname = newname % i[NUM]
        newname = replace(newname, " ", rename_underscore)
      else:
        newname = rename_fmt
        newname = replace(newname, "%a", a_artist)
        newname = replace(newname, "%t", t_name)
        newname = replace(newname, "%l", a_title)
        if find(newname, "%") != -1:
          newname = newname % i[NUM]
        newname = replace(newname, " ", rename_underscore)
      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")
          f.write(("%02i" + progr_sep + "ren" + progr_sep + "%s-->%s") % (i[NUM], i[NAME], newname + "\n"))
  f.close()
  print "Done with \"" + a_artist+ " - " + a_title + "\"."
else:
  print "All done."
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)
exit()
