#!/usr/bin/python

# rip_multi_cd.py
#       --copyright--                   Copyright 2009 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       January 10, 2009        bar
#       January 12, 2009        bar     run "eject" at the end
#                                       lower lame quality
#       January 29, 2009        bar     --never-skip=2 for cdparanoia
#       February 12, 2009       bar     leave a numeric space between cds (so we can inject a "this is the last disc msg")
#       February 13, 2009       bar     try to avoid double-ripping CDs
#                                       make lame more quiet
#                                       nocd
#                                       dupecd
#       February 14, 2009       bar     indir
#       February 21, 2009       bar     eject immediately after ripping, before converting to mp3
#       March 8, 2009           bar     strip silence from both ends of the wave files (because 99-track albums have too much silence on the shoulders)
#       August 4, 2009          bar     spin the strip silence logic to a separate script so that it won't eat memory
#                                       --span
#       October 6, 2009         bar     --ignore_errors
#       April 10, 2010          bar     update
#                                       --cdparanoia_options
#       April 18, 2010          bar     help cdparanoia find the cdrom
#       September 29, 2010      bar     better help str
#       November 29, 2011       bar     pyflake cleanup
#       --eodstamps--
##      \file
#
#
#       Shell-ish script to rip audio book cds so that they end up with MP3 files named in sequencial manner.
#
#       Requires:
#           cdparanoia
#           lame
#       Wants:
#           eject
#
#       I convert audio books to mp3 files with this:
#
#           cd  tmp
#           nd  BOOK_TITLE
#           python /home/alex/flight/tzpython/rip_multi_cd.py --cdparanoia "-d /dev/cdrom2" --prename BOOK_TITLE_ . --ignore_errors --lame -S
#
#
#       TODO:
#
#           --strip causes a big memory leak and I don't know why!
#
#           If ripping is going too slowly, auto-apply --ignore_errors
#
#           Run the mp3 conversions while ripping.
#
#           Use the TOC length * 2352 (+ 44) for the wav file length to detect wav files already ripped to pick up in the middle of a disc.
#
#

import  glob
import  multiprocessing
import  os
import  re
import  subprocess
import  sys
import  time

import  strip_wave_silence
import  TZKeyReady
import  tzlib


PRE_NAME        = "rmcd_"

LAME_OPTIONS    = "-V 9 -b 64 --nohist -S "                 # note: -S causes parellel conversions

nmp3_re         = re.compile(r"(\d+)\.mp3$", re.DOTALL + re.IGNORECASE)

fn_format       = "%04u.mp3"




def get_wave_file_names(indir, outdir) :
    if  indir   :
        tfls    = glob.glob(os.path.join(indir,  "*.wav"))
    else :
        tfls    = glob.glob(os.path.join(outdir, "track*.wav"))
        tfls   += glob.glob(os.path.join(outdir, "??.Track_*.wav"))
        tfls   += glob.glob(os.path.join(outdir, "*AudioTrack *.wav"))
    tfls.sort()

    return(tfls)



def run_lame(cmd, fn) :
    if  os.system(cmd) :
        print "lame failure on %s!" % fn
        sys.exit(104)
    try     :
        os.unlink(fn)
    except ( OSError, IOError ) :
        tzlib.print_exception()
        sys.exit(105)
    pass


def convert_to_mp3(indir, outdir, pre_name = None, lame_options = None, strip_silence = False) :
    pre_name            = pre_name or PRE_NAME
    if  lame_options   == None :
        lame_options    = LAME_OPTIONS

    tfls        = get_wave_file_names(indir, outdir)

    if  tfls :

        mfls    = glob.glob(os.path.join(outdir, pre_name + "*.mp3"))
        mfls.sort()

        n   = 1
        if  mfls :
            g   = nmp3_re.search(mfls[-1])
            if  not g :
                print "Unnumbered mp3 file in directory, %s\n%s\n----------!" % ( outdir, "\n".join(mfls) )
                sys.exit(103)
            n   = int(g.group(1)) + 2

        pa      = []
        for fn in tfls :
            ofn = os.path.join(outdir, (pre_name + (fn_format % n)))

            if  strip_silence :
                os.system("python %s %s" % ( strip_wave_silence.__file__, fn ) )

            if  sys.platform == 'win32' :
                cmd = 'lame %s "%s" "%s"'   % ( lame_options, fn.replace('"', r'\"'), ofn )
            else :
                cmd = 'lame %s  %s  "%s"'   % ( lame_options, re.sub(r"([^a-zA-Z0-9\/\.])", r"\\\1", fn), ofn )           # fn.replace(")", r"\)").replace("(", r"\(").replace("'", r"\'").replace('"', r'\"').replace(' ', r'\ ')

            print "cmd", cmd

            if  lame_options.find("-S") >= 0 :
                p           = multiprocessing.Process(target = run_lame, args = ( cmd, fn, ))
                p.daemon    = True
                p.start()
                pa.append(p)
            else    :
                run_lame(cmd, fn)

            n  += 1

        for p in pa :
            p.join()

        pass

    pass



def inject_last_cd(outdir, lame_options, file_name, where) :
    if  file_name :
        if  lame_options   == None :
            lame_options    = LAME_OPTIONS

        tfls    = [ fn for fn in glob.glob(os.path.join(outdir, "*.mp3")) if nmp3_re.search(fn) ]
        tfls.sort()

        while tfls :
            pre_name    = nmp3_re.sub("", tfls[0])
            na          = []

            while tfls :
                if  not tfls[0].startswith(pre_name) :
                    break
                na.append(int(nmp3_re.search(tfls.pop(0)).group(1)))

            if  na :
                w   = where
                while na and (w < 0) :
                    n       = na.pop() - 1
                    while na and (na[-1] == n) :
                        n   = na.pop() - 1
                    w  += 1
                if (w  >= 0) and (n >= 0) :
                    ofn = pre_name + (fn_format % n)
                    if  os.path.isfile(ofn) :
                        print "%s is already a file! BUG!" % ofn
                        sys.exit(105)

                    # print ofn, file_name

                    if  file_name.endswith(".wav") :
                        cmd = "lame %s '%s' '%s'" % ( lame_options, file_name, ofn )
                        if  os.system(cmd) :
                            print "lame failure on %s%s!" % ( pre_name, file_name )
                            sys.exit(104)
                        pass
                    else :
                        tzlib.write_whole_binary_file(ofn, tzlib.read_whole_binary_file(file_name))
                    pass
                pass
            pass
        pass
    pass


def file_name_ok_to_inject(file_name) :
    if  not file_name :
        return(True)

    return(file_name.endswith(".wav") or file_name.endswith(".mp3"))





def is_dupe_or_no_disk(dupe_file_name, ask, options = "") :
    p           = subprocess.Popen("cdparanoia -Q %s" % ( options or "" ), shell=True, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, close_fds = True)
    ( si, so )  = ( p.stdin, p.stdout )
    sos         = p.stdout.read()
    p.stdout.close()
    p.stdin.close()

    if  sos.find("Table of contents (audio tracks only):") > 0 :
        ds  = ""
        if  os.path.isfile(dupe_file_name) :
            ds  = tzlib.read_whole_text_file(dupe_file_name)

            if  ds.find(sos) >= 0 :
                if  not ask :
                    return(True)

                print "This disk appears to be already ripped."
                print "Do you want to rip it again (y/N)?",
                while True :
                    k   = TZKeyReady.key_ready()
                    if  k :
                        print
                        k   = k.lower()
                        if  k == 'y' :
                            print "OK. Here we go..."
                            sos = ""
                            break

                        if  (k == 'n') or (k == '\r') or (k == '\n') or (k == chr(3)) or (k == 'q') :
                            print "Then I will eject the disc."

                            return(True)

                        print "Well (y/N)?"

                    time.sleep(0.1)
                pass
            pass

        if  sos :
            ds += sos
            tzlib.write_whole_text_file(dupe_file_name, ds)

        return(False)

    return(-1)








if  __name__ == '__main__' :

    import  TZCommandLineAtFile


    help_str    = """
%s (options)

    I rip multiple CDs to sequentially numbered MP3 files.
    The MP3 numbering spans multiple CDs, so that CDs can be ripped in order and then a player will play them in order.

    To use:
        Make an empty directory.
        cd to the directory.
        Put the 1st/next CD in the CD drive and run me for each CD.
        Run me with the --lastcd option to inject a .wav file of your choice before the last CD.

    I convert all track*.wav files in the outdir to sequentially numbered .mp3 files (skipping a number between CDs).
    I erase the   track*.wav files as I do this.
    Then I rip whatever CD is in the CD drive.
    Then I do the track*.wav to mp3 conversion operation again.
    Then, if the rip took long enough (10 seconds), I eject the CD.

    I do the sequential numbering like this:
        prename + "%%04.mp3"
    I pick up on currently existing files to continue numbering beyond them, skipping a number.

    I can inject a .wav file in the numeric holes before the presumed last 2 CDs.
      Run me with the --lastcd and/or --2ndlastcd options once on a directory, though,
      as the holes will move back a CD for each of these options having been done.

Options:

    --indir     dir             Set the input directory.                (default: none (If set, read ./*.wav files)
    --outdir    dir             Set the output directory.               (default: .)
    --prename                   Set the output file prefix name.        (default: %s)
    --span      option          Set the cdparanoia 'span' option.       (default: none)
    --ignore_errors             Set cdparanoia to ignore errors.        (default: retry twice)
    --cdparanoia    "options"   Set cdparanoia options.                 (default: "")
    --lame      "options"       Set the lame options.                   (default: %s)
    --lastcd    wave_file       Set the "last CD" wave file.            (default: none)
    --2ndlastcd wave_file       Set the "second to last CD" wave file.  (default: none)
    --dupefile  file_name       Set the duplicate CD detection file.    (default: rip_multi_cd_discs.txt)
    --nocd                      Do not access the CD (only do wav to mp3 conversions).
    --listwaves                 Just list the .wav files in order.
    --strip                     Strip silence from ends of the .wav files.

    """ % ( os.path.basename(sys.argv[0]), PRE_NAME, LAME_OPTIONS )

    del(sys.argv[0])
    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)


    indir               = None
    outdir              = "."
    pre_name            = None
    lame_options        = None
    cdparanoia_options  = None
    last_cd             = None
    second_to_last_cd   = None
    dupe_file_name      = None
    do_cd               = True
    strip_silence       = False
    span                = ""
    ignore_errors       = False

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--help", "-h", "-?", "/?", "/h" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        print help_str
        sys.exit(254)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--indir", "--in", "--in_dir", "-i" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        indir           = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--outdir", "--out", "--out_dir", "-o" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        outdir          = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--prename", "--pre", "--pre_name", "-p" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        pre_name        = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--span", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        span            = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--ignore_errors", "--ignoreerrors", "--ignore-errors", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        ignore_errors   = True

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--lame", "--lameoptions", "--lame_options", "--lame-options", "-l" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        lame_options    = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--cdparanoia", "--cdparanoiaoptions", "--cdparanoia_options", "--cdparanoia-options", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        cdparanoia_options  = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--lastcd", "--last_cd", "-1" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        last_cd             = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--2ndlastcd", "--secondtolastcd", "--2ndtolastcd", "--2nd2lastcd", "--second2lastcd", "--2nd_last_cd", "--second_to_last_cd", "--2nd_to_last_cd", "--2nd_2_last_cd", "--second_2_last_cd", "--second", "-2" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        second_to_last_cd   = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--dupefile", "--dupe_file", "-d" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        dupe_file_name      = sys.argv.pop(oi)

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--nocd", "--no_cd", "-n" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        do_cd               = False

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--strip", "--stripsilence", "--strip_silence", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        strip_silence       = True



    while True :
        oi  = tzlib.array_find(sys.argv, [ "--listwaves", "--listwavs", "--list_waves", "--list_wavs", "--lw", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        tfls                = get_wave_file_names(indir, outdir)
        for fn in tfls :
            print fn
        sys.exit(0)



    if  not file_name_ok_to_inject(last_cd) :
        print "Injection file name must be .wav or .mp3: %s" % last_cd
        sys.exit(109)
    if  not file_name_ok_to_inject(second_to_last_cd) :
        print "Injection file name must be .wav or .mp3: %s" % second_to_last_cd
        sys.exit(109)


    if  not dupe_file_name  :
        dupe_file_name      = os.path.join(outdir, "rip_multi_cd_discs.txt")

    convert_to_mp3(indir, outdir, pre_name, lame_options, strip_silence)

    et  = bt    = 0

    ask = True
    while do_cd :
        d   = is_dupe_or_no_disk(dupe_file_name, ask, options = cdparanoia_options)
        if  d :
            if  d != True       :
                cdopts          = cdparanoia_options or ""
                if  cdopts.find("-d ") < 0 :
                    cd_names    = glob.glob("/dev/cdrom*")
                    for cdn in cd_names  :
                        cdopts  = "%s -d %s" % ( cdopts, cdn )
                        d       = is_dupe_or_no_disk(dupe_file_name, ask, options = cdopts)
                        if  not d :
                            cdparanoia_options  = cdopts
                            break
                        pass
                    pass
                pass
            if  d   :
                if  d == True   :
                    os.system("eject")
                break
            pass

        if  not d   :
            print "copts", cdparanoia_options
            bt      = tzlib.elapsed_time()
            if  not ignore_errors :
                os.system("cdparanoia -B --never-skip=2 %s %s" % ( span, cdparanoia_options or "" ) )
            else    :
                os.system("cdparanoia -B -Z             %s %s" % ( span, cdparanoia_options or "" ) )
            et      = tzlib.elapsed_time()
            if  et - bt > 10.0 :
                os.system("eject")
                et  = bt    = 0

            convert_to_mp3(indir, outdir, pre_name, lame_options, strip_silence)
        ask = False

    sz      = 0.0
    mfls    = glob.glob(os.path.join(outdir, (pre_name or PRE_NAME) + "*.mp3"))
    for fn in mfls :
        sz += os.path.getsize(fn)

    # print "Total megabytes so far: %.0f" % ( (sz + (1 << 20) - 1) / (1 << 20) )
    print   "Total megabytes so far: %.0f" % ( (sz +  1000000  - 1) /  1000000  )


    inject_last_cd(outdir, lame_options, second_to_last_cd, -2)
    inject_last_cd(outdir, lame_options, last_cd,           -1)

    if  et - bt > 10.0 :
        os.system("eject")

    pass


#
#
# eof

