#!/usr/bin/perl -w
# Last Updated: 2003.02.12 (xris)

    use strict;
    use Cwd;

# VCDImager XML Info:
# http://www.vcdimager.org/guides/general_xml_structure.html

#This program relies on pieces from the vcdimager, transcode and mpgtx packages
#
# vcdxminfo
# vcdxbuild
# mpgtx
# tcmplex

    #mpgtx -d -b file file.mpg
    #tcmplex -m s -i file-0.m2v -p file-0.mp2 -o output.mpg

    $|++;
    print "\n";

#Gather in the commandline options
    use Getopt::Long;
    my ($Help, $ChapterLength, $Title, $Remux, $ThisDisk, $TotalDisks);
    GetOptions('chapterlength=s'    => \$ChapterLength,
               'title|movie=s'      => \$Title,
               'remux|remultiplex'  => \$Remux,
               'help'               => \$Help);
    $Title ||= "Video_CD";
    $ChapterLength ||= 300;

# Show the help
    if ($Help || !@ARGV) {
        print <<EOF;
SVCD Chapterize version 0.1

requires:

    vcdimager >= 0.7.x
    tcmplex  (for --remux)

usage:

    chapterize [options] <directory>
    chapterize [options] <file.mpg> <file.mpg>

options:

    -c  --chapter=   Length of chapters in minutes (default: 5)
    -t  --title=     SVCD disk title
    -r  --remux=     Remultiplex mpeg files with tcmpled?
    -h  --help       Show this message

files:

    Run chapterize on a single mpeg file, or a list of files, and they will
    be combined into a single SVCD bin/cue pair, with chapter markers placed
    at the specified intervals.

    When run on a directory, chapterize expects files named according to a
    certain format:

    file<disk>.mpg    or    file<chapter>-<disk>.mpg

    Where <disk> is the disk number and <chapter> is the chapter number.
    When multiple disk numbers are used, chapterize will create multiple
    bin/cue pairs.  Different chapters of the same disk will obviously be
    placed into the same bin/cue pair, and as always, chapter markers will
    also be inserted at the specified intervals.

burning:

    cdrdao write [options] yourfile.cue

    See the cdrdao documentation for its options.

EOF
        exit;
    }


#Attempt to divine the volume numbers
    ($ThisDisk, $TotalDisks) = $Title =~ /\b(\d+)\s*of\s*(\d+)$/;
    $ThisDisk ||= 1;
    $TotalDisks ||= 1;

#Scan the passed-in arguments and figure out whether we're processing directories or files
    my (%Files, @Files, @Dirs, $fnum, $Parts);
    foreach my $path (@ARGV) {
        unless (-e $path) {
            print "File $path doesn't exist.\n";
            next;
        }
        if (-d $path) {
            push @Dirs, $path;
        }
        elsif (!@Dirs) {
            LoadFile($path, ++$fnum);
        }
    }

#We were asked to process a directory - this requires some checking
    if (@Dirs) {
        foreach my $dir (@Dirs) {
            $dir =~ s/\/+$//s;
            my ($dirname) = $dir =~ /([^\/]+)$/;
        #Clean up the title, and possibly autodetect it
            my $title = $Title;
            $title =~ s/[\s_]*\d+[\s_]*of[\s_]*\d+\s*$//si;
            if ($title eq 'Video_CD') {
                $title = $dirname;
                $title =~ s/\bSVCD(?:\-\w+)\b//si;
                $title =~ s/\bDVD(?:Rip)\b//si;
                $title =~ s/\b(?:LIMITED|INTERNAL)\b//s;
            }
            $title =~ s/[\.\s_]+/_/sgi;
        #Figure out which files are in this directory
            my %volumes;
            opendir(DIR, $dir) or die "Can't open directory $dir:  $!\n\n";
            foreach my $file (grep(/\.mpg$/, readdir DIR)) {
                $file =~ /(\d+)(?:[^\w\s](\d+))?\.mpg$/ or die "Unknown mpeg name format:  $file\n\n";
                my $vol = $1;
                #my $sect = $2;
            #Try to calculate the number of disks in this volume
                $TotalDisks = $vol if ($vol > $TotalDisks);
            #Take note of this file
                push @{$volumes{$vol}}, $file;
            }
            closedir DIR;
        #Process each file (or group of files)
            foreach my $vol (sort { $a <=> $b } keys %volumes) {
                undef @Files;
                undef %Files;
                undef $fnum;
                undef $Parts;
                foreach my $file (sort byVolNum @{$volumes{$vol}}) {
                    LoadFile("$dir/$file", ++$fnum);
                }
                $ThisDisk = $vol;
                $Title = "${title}_${vol}_of_$TotalDisks";
                &WriteXML($dir);
                &SaveBinCue($dir);
            }
        }
    }
#Just asked to process one disk worth of info - we've already gathered the necessary info
    else {
        &WriteXML;
        &SaveBinCue;
    }


    #####
    ## Beware:  Subroutines lurk below!
    #####

sub Remux {
    my $path = shift;
    my ($dir, $name, $basename) = $path =~ /^(.*?\/)?(([^\/]+?)(?:\.\w+)?)$/;
    $dir ||= '';    #suppress errors
    print "\n\nmpgtx -d -b \"$dir$basename\" \"$path\"\n\n";
    system("nice -n 19 mpgtx -d -b \"$dir$basename\" \"$path\"");
    print "\n\ntcmplex -m s -i \"$dir$basename-0.m2v\" -p \"$dir$basename-0.mp2\" -o \"$dir$basename-remux.mpg\"\n\n";
    system("nice -n 19 tcmplex -m s -i \"$dir$basename-0.m2v\" -p \"$dir$basename-0.mp2\" -o \"$dir$basename-remux.mpg\"");
#delete the intermediary files
    unlink "$dir$basename-0.m2v";
    unlink "$dir$basename-0.mp2";
#rename the old file and move the new one into its place
    rename $path, "$dir$basename.old.mpg";
    rename "$dir$basename-remux.mpg", $path;
}

sub SaveBinCue {
    my $dir = (shift or '');
#cd into $dir
    my $thisdir = getcwd;
    chdir $dir if ($dir);
    my $safename = $Files[0]->{basename};
        $safename =~ tr/a-zA-Z0-9/_/c;
    print "Writing bin/cue pair $dir$Files[0]->{basename}\n";
    system("nice -n 19 vcdxbuild -p -b \"$safename.bin\" -c \"$safename.cue\" \"$Files[0]->{basename}.xml\"");
    print "\n";
#and back out again
    chdir $thisdir if ($dir);
}

sub WriteXML {
    my $dir = (shift or '');
    $dir =~ s/\/*$/\// if ($dir);
    print "Writing XML file for \"$Title\" (vol. $ThisDisk of $TotalDisks)\n";
    open FILE, ">$dir$Files[0]->{basename}.xml" or die "Can't write to $dir$Files[0]->{basename}.xml:  $!\n\n";
    print FILE <<EOF;
<?xml version="1.0"?>
<!DOCTYPE videocd PUBLIC "-//GNU//DTD VideoCD//EN" "http://www.gnu.org/software/vcdimager/videocd.dtd">

<videocd class="svcd" version="1.0">
  <option name="relaxed aps" value="false"/>
  <info>
    <album-id></album-id>
    <volume-count>$ThisDisk</volume-count>
    <volume-number>$TotalDisks</volume-number>
    <restriction>0</restriction>
  </info>
  <pvd>
    <volume-id>$Title</volume-id>
    <system-id>CD-RTOS CD-BRIDGE</system-id>
    <application-id></application-id>
    <publisher-id></publisher-id>
  </pvd>
  <filesystem>
    <folder>
      <name>SEGMENT</name>
    </folder>
  </filesystem>
  <sequence-items>
EOF
    foreach my $file (@Files) {
        print FILE <<EOF;
    <sequence-item src="$file->{name}" id="Sequence-$file->{id}">
      <default-entry id="Chapter-$file->{id}-1"/>
EOF
        foreach my $chapter (@{$file->{chapters}}) {
            print FILE "      <entry id=\"Chapter-$file->{id}-$chapter->{id}\">$chapter->{entry}</entry>\n";
        }
        print FILE "    </sequence-item>\n";
    }
    print FILE <<EOF;
  </sequence-items>
  <pbc>
EOF
    foreach my $file (@Files) {
        my ($prev, $prevstr, $next, $nextstr);
    #Figure out some info about the previous sequence
        if ($file->{part} > 1) {
            $prev = $file->{part} - 1;
            #$prevstr = "Sequence-$file->{next}->{id} - 0:00:00.000 - $file->{next}->{name}";
        }
        else {
            $prev = 'END';
            $prevstr = 'SVCD BEGIN';
        }
    #And some info about the next sequence
        if ($file->{part} == $Parts) {
            $next = 'END';
            $nextstr = 'SVCD END';
        }
        else {
            if ($file->{chapters}[0]->{id}) {
                $next = $file->{chapters}[0]->{part};
                $nextstr = "Chapter-$file->{id}-$file->{chapters}[0]->{id} - $file->{chapters}[0]->{timecode} - $file->{name}";
            }
            elsif ($file->{next}) {
                $next = $file->{next}->{part};
                $nextstr = "Sequence-$file->{next}->{id} - 0:00:00.000 - $file->{next}->{name}";
            }
            else {
                die "Weird end of sequence count (file $file->{id}, part $file->{part} of $Parts)\n";
            }
        }
    #Then print it
        print FILE <<EOF;
    <selection id="Selection-$file->{part}">                <!-- Sequence-$file->{id} - 0:00:00.000 - $file->{name} -->
      <prev ref="Selection-$prev"/>                 <!-- Key Prev: $prevstr -->
      <next ref="Selection-$next"/>                 <!-- Key Next:  $nextstr -->
      <return ref="Selection-END"/>             <!-- Key Return: VideoCD END -->
      <timeout ref="Selection-END"/>            <!-- On Timeout: VideoCD END -->
      <wait>2</wait>
      <loop jump-timing="immediate">1</loop>
      <play-item ref="Sequence-$file->{id}"/>
    </selection>
EOF
    #Now we go through any chapters that are here
        foreach my $chapter (@{$file->{chapters}}) {
        #Figure out some info about the previous sequence
            if ($chapter->{last}) {
                $prev = $chapter->{last}->{part};
                $prevstr = "Chapter-$file->{id}-$chapter->{last}->{id} - $chapter->{last}->{timecode} - $file->{name}";
            }
            else {
                $prev = $file->{part};
                $prevstr = "Sequence-$file->{id} - 0:00:00.000 - $file->{name}";
            }
        #And some info about the next sequence
            if ($chapter->{part} == $Parts) {
                $next = 'END';
                $nextstr = 'SVCD END';
            }
            else {
                if ($chapter->{next}) {
                    $next = $chapter->{next}->{part};
                    $nextstr = "Chapter-$file->{id}-$chapter->{next}->{id} - $chapter->{next}->{timecode} - $file->{name}";
                }
                elsif ($file->{next}) {
                    $next = $file->{next}->{part};
                    $nextstr = "Sequence-$file->{next}->{id} - 0:00:00.000 - $file->{next}->{name}";
                }
                else {
                    die "Weird end of sequence count (chapter $file->{id}.$chapter->{id}, part $chapter->{part} of $Parts)\n";
                }
            }
        #Then print it
            print FILE <<EOF;
    <selection id="Selection-$chapter->{part}">                <!-- Chapter-$file->{id}-$chapter->{id} - $chapter->{timecode} - $file->{name} -->
      <prev ref="Selection-$prev"/>                 <!-- Key Prev: $prevstr -->
      <next ref="Selection-$next"/>                 <!-- Key Next: $nextstr -->
      <return ref="Selection-END"/>             <!-- Key Return: VideoCD END -->
      <timeout ref="Selection-END"/>            <!-- On Timeout: VideoCD END -->
      <wait>2</wait>
      <loop jump-timing="immediate">1</loop>
      <play-item ref="Chapter-$file->{id}-$chapter->{id}"/>
    </selection>
EOF
        }
    }
    print FILE <<EOF;
    <endlist id="Selection-END" rejected="true"/>
  </pbc>
</videocd>
EOF
    close FILE;
}

sub LoadFile {
    my $path = shift;
    my $fnum = shift;
#Already loaded this file
    return if ($Files{$path});
#Remux?
    Remux($path) if ($Remux);
#Create a record for this item
    $Parts++;
    my %file = ('path' => $path,
                'id'   => $fnum,
                'part' => $Parts);
    ($file{name}, $file{basename}) = $file{path} =~ /(([^\/]+?)(?:\.\w+)?)$/;
#Get info about the mpeg, so we can have more accurate timecodes
    print "Analyzing $path:\n";
    my $data = '';
    local $/ = "\r";
    open DATA, "vcdxminfo -pai \"$path\" |" or die "Can't run vcdxminfo on $path:  $!\n\n";
    while (<DATA>) {
    #Trap the progress meter
        if (s/#scan[^\r\n]+?:\s*([^\r\n]+)\s*//s) {
            print "    processed:  $1\r";
        }
    #Don't display xml sequences, but store them to be processed later
        if (/</) {
            $data .= $_;
        }
    #print out everything else (usually errors/warnings)
        elsif (/S/) {
            s/^\s*|\s*\n\s*/\n    /sg;
            print $_;
        }
    }
    close DATA;
    print "\n";
#Extract the info we need
    ($file{length}) = $data =~ /<playing-time>\s*(\S+?)\s*<\/playing-time>/i;
    ($file{framerate}) = $data =~ /<frame-rate>\s*(\S+?)\s*<\/frame-rate>/i;
#Calculate the chapter info
    my @offsets;
    if ($data =~ /<aps packet-no/) {
        my $last = my $lasti = 0;
        foreach my $i (sort { $a <=> $b } ($data =~ /<aps\s+packet-no="\S+?">\s*(\S+?)\s*<\/aps>/sg)) {
            last if ($i > $file{length});    #just in case
            if ($i >= $last + $ChapterLength) {
            #Determine whether this or the previous offset is closer to the desired timecode
                if ($i - $last - $ChapterLength >= $last + $ChapterLength - $lasti) {
                    push @offsets, $lasti;
                }
                else {
                    push @offsets, $i;
                }
                $last = @offsets * $ChapterLength;
            }
            $lasti = $i;
        }
    }
    else {
        my $i;
        push @offsets, $i while (($i += $ChapterLength) < $file{length});
    }
    my $cnum = 1;    #We start on chapter 2, since chapter 1 is covered by the sequence id
    my $last = undef;
    foreach my $offset (@offsets) {
        $cnum++;
        $Parts++;
        my %chapter = ('entry'      => $offset,
                       'id'         => $cnum,
                       'timecode' => secs2time($offset),
                       'part'       => $Parts);
        if ($last) {
            $file{last} = $last;
            $file{last}->{next} = \%chapter;
        }
        push @{$file{chapters}}, $last = \%chapter;
    }
#Load it onto the file list
    $file{last} = @Files ? $Files[$#Files] : undef;
    $file{last}->{next} = \%file if ($file{last});
    push @Files, $Files{$path} = \%file;
}

sub secs2time {
    my $seconds    = shift;
    my $hours = int($seconds / 3600);
    $seconds -= 3600 * $hours;
    my $minutes = int($seconds / 60);
    $seconds -= 60 * $minutes;
    return sprintf("%d:%02d:%06.3f", $hours, $minutes, $seconds);
}

sub byVolNum {
    my ($av, $as) = $a =~ /(\d+)(?:[^\w\s](\d+))?\.mpg$/;
    my ($bv, $bs) = $b =~ /(\d+)(?:[^\w\s](\d+))?\.mpg$/;
    $av <=> $bv or $as <=> $bs;
}
