#!/usr/bin/perl
#
# This script is a try of doing the job of the burn-plugin in a batch job.
#       the burn plugin is written by: lordjaxom at vdrportal dot de
#       this script is written by ...: geronimo at vdrportal dot de
# 
# COPYRIGHT:   (c) 2005-2006 Creative Commons by-nc-sa
#               some rights reserved. For details on copyright see:
#               http://creativecommons.org/licenses/by-nc-sa
#
# DISCLAIMER:   NO WARANTY AT ALL. Use at your own responsability.
#
# vi: ts=3
#      
# INSTALLATION:
#    just start the installer and follow the questions. 
# 
# DESCRIPTION: see README on a detailled description
#
# HISTORY:
# 30.04.2006 - added support for additional menue pages
#              added support for [re]authoring of already muxed sources.
#
# 25.04.2006 - added option --toolChain, which tells the external tools
#              with their location and version used.
#
# 18.04.2006 - changed checking of external tools.
#              changed cleanup for less disk usage
#
# 17.04.2006 - added burning capability to script
#
# 27.03.2006 - changed to use theme-plugins and 'new' xml-structure
#
# 15.02.2006 - vdr2iso now may act as a network server too
#
# 15.01.2006 - chapter-processing now works using a patched vdrsync
#
# 14.01.2006 - check for diskspace
#
# 07.01.2006 - patch for projectX to use marks.vdr for cutting
#
# 01.01.2006 - Support for info.vdr
#
# 01.01.2006 - XMLSimple ersetzt
#
# 25.12.2005 - completely rewritten to use GD instead of Imagemagick 
#                jobfile format changed to xml.
#
# 20.12.2005 - support for letterbox-menues (aspect: lb)               
#
# 19.12.2005 - support for languange preference               
#
# 18.12.2005 - projectX added as possible demuxer.   
#                added pipe-support, so the jobfile can be passed via stdin.
#
# 16.12.2005 - added checks for used tools and also checks on background images               
#                
# 15.12.2005 - feedback on jobfile-parsing added               
#                
# 14.12.2005 - first success on recordings with AC3 audio streams
#
# 13.12.2005 - first try               
# 
use strict;
use IO::Socket;
use Net::hostent;
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
use POSIX ":sys_wait_h";
use File::Basename;
use Data::Dumper;
use V2I::utils;
use V2I::Config;
use V2I::Job;

my @allowedClients = qw(django17 localhost);   # place your client names here

$main::testOnly = 1;
my $abortOnCutmarkNotFound = 1;
my $debug = 0;
my $doCutting = 1;
my $doMuxing  = 1;
my $doMenues  = 1;
my $doAuthor  = 1;
my $doMkISO   = 1;
my $doCleanup = 0;
my $doBurn    = 1;
my $myPort    = 7938;
my $firstTC   = 0;
my $sleepTime = 60;
my $logFN     = '/tmp/vdr2iso.log';
my $opMode    = 'unknown';
my $commands  = {
  HELO => \&cHello        
, ACCJ => \&cAcceptJob
, STOP => \&cStopSrv
};
my($client, $server, $hostinfo, $pid, $lastCheck, %knownHosts, $clientHost);
my($jobFN, $mainPid, $nice, $appConfig);

sub usage {
   my $rv = "\n\n".'vdr2iso.pl is a script to convert vdr recordings to DVD-iso-images.'."\n"
           .'Default-values are read from v2icfg.pm and may be changed by editing'."\n"
           .'that file or by giving the propiate commandline-option.'."\n\n";
   
   $rv .= 'Syntax: vdr2iso.pl [options] [REALLY] jobfile'."\n\n";
   $rv .= 'Known options for standalone-mode are this:'."\n";
   $rv .= '   --logfile=<file>   where should the logs from the worker goto.'."\n";
   $rv .= '   --base=<dir>       the root of all recordings.'."\n"
         .'                      may be different on editor- / vdr2iso-machines.'."\n";
   $rv .= '   --work=<dir>       directory for temporary stuff.'."\n";
   $rv .= '   --iso=<dir>        where to put the final iso-files.'."\n";
   $rv .= '   --keep             don\'t remove temporary files.'."\n";
   $rv .= '   --cleanup          remove temporary files.'."\n";
   $rv .= '   --demuxonly        process only cut- and demux-steps.'."\n";
   $rv .= '   --menuonly         don\'t convert, just create menu-images.'."\n";
   $rv .= '   --authoronly[=tc]  assume already muxed sources.'."\n";
   $rv .= '                      optionally add the timecode of the work-dir.'."\n";
   $rv .= '   --toolChain        print the external tools with their path and the used versions.'."\n";
   $rv .= '   --help or --?      the output you\'re reading'."\n";
   $rv .= '   REALLY             use this keyword do disable testmode.'."\n";

   $rv .= "\n\n".'Known options for server-mode are this:'."\n";
   $rv .= '   --server           switch to server-mode, where jobs are'."\n"
         .'                      accepted via network-connection.'."\n";
   $rv .= '   --logfile=<file>   where should the logs from the worker goto.'."\n";
   $rv .= '   --base=<dir>       the root of all recordings.'."\n"
         .'                      may be different on editor- / vdr2iso-machines.'."\n";
   $rv .= '   --work=<dir>       directory for temporary stuff.'."\n";
   $rv .= '   --iso=<dir>        where to put the final iso-files.'."\n";
   $rv .= '   --sleep=<secs>     sleeptime for worker in idle-mode.'."\n";
   $rv .= '   --keep             don\'t remove temporary files.'."\n";
   $rv .= '   --cleanup          remove temporary files.'."\n";
   $rv .= '   --port=<port>      public tcp-port in server-mode'."\n";

   return $rv;
}

sub processArgs {
   while(my $arg = shift){
         if($arg =~ /^--?([^=:]+)(?:[=:](.+))?$/){
            my $param = $1;
            my $value = $2;

            for($param){
               /^(?:\?|HELP)$/i and do { die usage."\n"; last; };
               /^port$/         and do { if($value =~ /(\d{1,4})/){ $myPort = $1; } last; };
               /^sleep/         and do { if($value =~ /(\d{1,4})/){ $sleepTime = $1; } last; };
               /^server/        and do { $opMode = 'server'; $main::testOnly = 0; last; };
               /^logfile/       and do { $logFN = $value; last; };
               /^base/          and do { $appConfig->getConfig->{'recBase'} = $value if -d $value; last; };
               /^work/          and do { $appConfig->getConfig->{'temp'} = $value; last; };
               /^iso/           and do { $appConfig->getConfig->{'iso'} = $value; last; };
               /^toolChain/     and do { $appConfig->printToolsUsed; exit; };
               /^cleanup/       and do { $doCleanup = 1; last; };
               /^keep/          and do { $doCleanup = 0; last; };
               /^demuxonly/     and do { $doCutting   = 1;
                                         $doMuxing    = 1;
                                         $doMenues    = 0;
                                         $doAuthor    = 0;
                                         $doMkISO     = 0;
                                         $doCleanup   = 0;
                                         $doBurn      = 0;
                                         last;
                                         };
               /^authoronly/    and do { $doCutting   = 0;
                                         $doMuxing    = 0;
                                         $doMenues    = 1;
                                         $doAuthor    = 1;
                                         $doMkISO     = 1;
                                         $doCleanup   = 0;
                                         $doBurn      = 0;
                                         $firstTC = $value if $value =~ /^\d+$/;
                                         last;
                                         };
               /^menuonly/      and do { $doCutting   = 0;
                                         $doMuxing    = 0;
                                         $doMenues    = 1;
                                         $doAuthor    = 0;
                                         $doMkISO     = 0;
                                         $doCleanup   = 0;
                                         $doBurn      = 0;
                                         last;
                                         };
               die 'unknown option! - '.usage()."\n"; 
               }
            }
         elsif($arg =~ /^REALLY$/){ $main::testOnly = 0; }
         else{ $jobFN = $arg; }
         }
   die('ERROR: need an existing jobfile in standalone-mode!'.usage()."\n") if not -s $jobFN and not $opMode eq 'server';
}

sub tellConfig {
   print(stderr "\n\n\n");     
   print(stderr '-------------------------------------'."\n");
   print(stderr ' vdr2iso starts with these settings:'."\n");
   print(stderr '-------------------------------------'."\n");
   print(stderr '   run-mode : '.($opMode eq 'server'?'server':'standalone').' '.($main::testOnly?'!!! TESTMODE !!!':'real worker!')."\n");
   print(stderr '   port ....: '.$myPort."\n");
   print(stderr '   sleeptime: '.$sleepTime."\n");
   print(stderr '   logfile .: '.$logFN."\n");
   print(stderr "\n");
   print(stderr '   base of recordings: '.$appConfig->getRecBase."\n");
   print(stderr '   directory for isos: '.$appConfig->getIsoDir."\n");
   print(stderr '   directory for temp: '.$appConfig->getWorkDir."\n");
   print(stderr "\n".'These are the actions to perform:'."\n");
   print(stderr '   - cut and demux'."\n") if $doCutting;
   print(stderr '   - remux'."\n") if $doMuxing;
   print(stderr '   - create DVD-Menues'."\n") if $doMenues;
   print(stderr '   - author disk'."\n") if $doAuthor;
   print(stderr '   - build ISO'."\n") if $doMkISO;
   print(stderr '   - cleanup'."\n") if $doCleanup;
   print(stderr '======================================'."\n");
   print(stderr "\n\n");
}

sub reaper {
   my $child;
   
   while((my $waitedpid = waitpid(-1,WNOHANG)) > 0){
         logmsg( "reaped $waitedpid" . ($? ? " with exit $?" : ''));
         }
   $SIG{CHLD} = \&reaper;
}

sub cHello { 
   my $args = shift; 
   my $client = shift;
   
   logmsg('cHello( '.$args.' ) ...'."\n"); 
   print($client 'OLEH '.$args);
}

sub cAcceptJob {
   my $args   = shift;
   my $client = shift;
   my $newJob = '';
   my $scratch;

   print($client 'JCCA '.$args."\n");
   while(my $line = <$client>){
         $newJob .= $line;
         print('client: <['.$line.']>'."\n");
         last if $line =~ /^<\/job>/;
         }
   print($client 'jobsize: '.length($newJob)."\n");
   while(not ($scratch = <$client>)){}
   if($scratch =~ /JCCA OK/i){
      print(NET_WTR $newJob);
      logmsg('added new job with size of #'.length($newJob).' Bytes');
      }
}

sub cStopSrv { 
   my $args   = shift; 
   my $client = shift;
   my $parent = shift; 
   
   logmsg('cStopSrv( '.$args.' ) ...'."\n");  

   kill("TERM", $parent); 
}

sub doClientCommand {
   my $pid = shift;     
   my $client = shift;
   $client->autoflush(1);
   my $cMsg;

   logmsg('doClientCommand()... parent has pid #'.$pid) if $debug;
   while(not ($cMsg = <$client>)){}
   if($cMsg =~ /^([\S]+)\s*(.+)?$/){
      my $cmd  = uc($1);
      my $args = $2;

      logmsg('client sent command<'.$cmd.'> with args['.$args.']') if $debug;     
      if(exists($commands->{$cmd})){ $commands->{$cmd}($args, $client, $pid); }
      else{ logmsg('Oups, unknown command!'); }
      }
   close $client;
}

sub logmsg { my $msg = "@_"; $msg =~ s/\n$//; system('logger \''.$0.' '.$$.': '.$msg.'\''); }

sub serverStub {
   foreach (@allowedClients){ $knownHosts{$_} = 1; }
   $server = IO::Socket::INET->new(Proto     => 'tcp'
                                 , LocalPort => $myPort
                                 , Listen    => SOMAXCONN
                                 , Reuse     => 1);
   die "can't setup server" unless $server;

   $SIG{CHLD} = \&reaper;
   logmsg('Server accepting clients at port #'.$myPort);
   while($client = $server->accept()){
         $hostinfo = gethostbyaddr($client->peeraddr);
         $clientHost = $hostinfo ? lc($hostinfo->name) : $client->peerhost;
         logmsg('Connect from '.$clientHost);
         if(exists($knownHosts{$clientHost}) and $knownHosts{$clientHost}){
            doClientCommand($mainPid, $client); 
            }
         else{
            logmsg('Access denied!');
            print($client 'Access denied!'."\n");
            }
         }
}

sub doWorker {
   if($logFN){
      my $logpath = dirname($logFN);
      my $name = basename($logFN);
      my $xt = 'log';
         
      if($name =~ /^(.+?)\.([^.]+)$/){
         $name = $1;
         $xt   = $2;
         }
      if(-s $logpath.'/'.$name.'-0.'.$xt){
         logmsg('start log-rotation for name<'.$name.'> and ext<'.$xt.'>') if $debug;
         for(my $i=6; $i >= 0; $i--){
            if(-s $logpath.'/'.$name.'-'.sprintf('%d', $i).'.'.$xt){ 
               system('mv \''.$logpath.'/'.$name.'-'.sprintf('%d', $i).'.'.$xt.'\' '
                        .'\''.$logpath.'/'.$name.'-'.sprintf('%d', $i+1).'.'.$xt.'\'');
               }
            }
         }
      elsif(not -d $logpath){
         system('mkdir -p \''.$logpath.'\'');     
         }
      print('try using logfile: <'.$logpath.'/'.$name.'-0.'.$xt.">\n");
      open(STDOUT, '>> '.$logpath.'/'.$name.'-0.'.$xt) or die('could not reopen STDOUT'."\n");
      open(STDERR, '>> '.$logpath.'/'.$name.'-0.'.$xt) or die('could not reopen STDERR'."\n");
      }
   if($opMode eq 'server'){
      while(1){
            my $rawJob = '';     
            my $job = ();

            while(my $line = <WRK_RDR>){
                  $rawJob .= $line;     
                  last if $line =~ /<\/job>/;
                  }
            if(length($rawJob)){
               print('receive: job had size of #'.length($rawJob)." Bytes\n");
               $job = V2I::Job->fromXml($rawJob, $appConfig);
   
               workoutJob($job);        
               }
            else{ sleep($sleepTime); }
            }
      }
   else{
      my $job = V2I::Job->load($jobFN, $appConfig);
      
      print('done loading Job.'."\n");
      workoutJob($job);
      }
}

sub doServer {
   pipe(WRK_RDR, NET_WTR);

   my $mainPid = $$;
   my $flags = fcntl(WRK_RDR, F_GETFL, 0) or die "Can't get flags for the socket: $!\n";
   
   $flags = fcntl(WRK_RDR, F_SETFL, $flags | O_NONBLOCK) or die "Can't set flags for the socket: $!\n";
   NET_WTR->autoflush(1);
   if(!defined($pid = fork)){}
   elsif($pid){
      close(WRK_RDR);     
      serverStub();     
      }
   else{
      close(NET_WTR);     
      doWorker();     
      }
}

sub enufSpace {
   my $work = shift;
   my $cmd = 'sh LANG="C" && df \''.$appConfig->getWorkDir.'\' \''.$appConfig->getIsoDir.'\' 2>/dev/null';
   my @needed = (15000000, 4800000);
   my $fp;
   my $rv = 1;

   $needed[0] = 10000000 if $doCleanup;
   print('enufSpace() - command: <'.$cmd.'>'."\n") if $debug;
   open($fp, $cmd.' |') or die('could not determine diskspace '.$!."\n");
   my $check = '';
   my $i = 0;
   while(my $line = <$fp>){
         next if $line =~ /^Filesystem/;
         $check .= $line;
         if($check =~ /([^\s]+)\s+(\d+)\s+(\d+)\s+(\d+)/){
            my $device = $1;
            my $total  = $2;
            my $used   = $3;
            my $free   = $4;
   
            print($device.' has size='.$total.', used='.$used.' and free='.$free."\n") if $debug;
            $rv = 0 if($free < $needed[$i]);
            $check = '';
            $i++;
            }
         else{
            die('Oups, pattern did not match?!?');
            $rv = 0;
            }
         }
   close($fp);

   return $rv;
}

sub prepare {
   my $disk = shift;
   my $work = shift;
   my $i = 0;
   my $cmd;

   foreach my $rec (@{$disk->{'recordings'}}){
           my $dest = $work.'/VDRSYNC.'.$i;
      
           $cmd = 'mkdir -p \''.$dest.'\'';
           print('command: <|'.$cmd."|>\n");
           system($cmd);
           $i++;
           }
}

sub cutAndDemux {
   my $disk = shift;
   my $work = shift;
   my $i = 0;
   my %cmds;

   $cmds{'projectX'} = $appConfig->nice.' '.$appConfig->workerCommand('java').' -jar '.$appConfig->workerCommand('projectX').' -out ';
   $cmds{'vdrsync'}  = $appConfig->nice.' '.$appConfig->workerCommand('vdrsync').' -cut -o '; 
   foreach my $rec (@{$disk->{'recordings'}}){
           my $path = $rec->{'path'};   
           my $cfg  = $rec->{'settings'};
           my $dest = $work.'/VDRSYNC.'.$i;
     
           if(not -d $path){
              print('cutAndDemux(): Oups, recording ['.$path.'] not found! - ABORT!'."\n");
              exit;
              }
           print("\n".'------------------------------------------------------------------------------'."\n");
           print('--- Cut and demux recording ['.$rec->{'title'}.']...'."\n");
           print('---       path of recording ['.$rec->{'path'}.']...'."\n") if $debug;
           print('------------------------------------------------------------------------------'."\n");
           my $cmd = $cmds{$cfg->{'demux'}};   
     
           die('unsupported demuxer: <'.$cfg->{'demux'}.">\n") if not "$cmd";
           $cmd .= ' \''.$dest.'\' ';
           
           if($cfg->{'demux'} =~ /projectX/){
              my $fnCutMarks = '';     
     
              if($cfg->{'cutMarks'}){
                 $fnCutMarks = $cfg->{'cutMarks'} if(-s $cfg->{'cutMarks'});
                 $fnCutMarks = $path.$cfg->{'cutMarks'} if(-s $path.$cfg->{'cutMarks'});
                 die('given cutmark<'.$cfg->{'cutMarks'}.'> NOT found and should ABORT.'."\n") if(not -s $fnCutMarks and $abortOnCutmarkNotFound);
                 }
              if(-s $fnCutMarks) { print('cutmarks-file found. Gonna use '.$fnCutMarks."\n"); }
              elsif(-s $path.'001.vdr[0].Xcl'){ $fnCutMarks = $path.'001.vdr[0].Xcl'; }
              elsif(-s $path.'marks.pjx')  { $fnCutMarks = $path.'marks.pjx'; }
              elsif(-s $path.'marks.vdr')  { 
                 my $fp;
                 my $cutMarks = 'CollectionPanel.CutMode=4'."\n";
     
                 if(open($fp, $path.'marks.vdr')){
                    while(my $line = <$fp>){
                          $cutMarks .= $line;     
                          }
                    close($fp);
                    }
                 if(open($fp, '> '.$path.'marks.vdr.pjx')){
                    print($fp $cutMarks);     
                    close($fp);     
                    $fnCutMarks = $path.'marks.vdr.pjx '; 
                    }
                 }
              $cmd .= ' -cut '.$fnCutMarks.' ' if "$fnCutMarks";
              for my $n (1 .. 999){
                 my $file = sprintf("%03d.vdr", $n);     
     
                 last if(not -s $path.$file);
                 $cmd .= ' '.$path.$file;
                 }
              }
           elsif($cfg->{'demux'} =~ /vdrsync/){
              my $fnCutMarks = '';
     
              if($cfg->{'cutMarks'}){
                 $fnCutMarks = $cfg->{'cutMarks'} if(-s $cfg->{'cutMarks'});
                 $fnCutMarks = $path.$cfg->{'cutMarks'} if(-s $path.$cfg->{'cutMarks'});
                 die('given cutmark<'.$cfg->{'cutMarks'}.'> NOT found and should ABORT.'."\n") if(not -s $fnCutMarks and $abortOnCutmarkNotFound);
              }
              if(-s $fnCutMarks) { print('cutmarks-file found. Gonna use '.$fnCutMarks."\n"); }
              if(getCmType($fnCutMarks) eq 'projectX'){
                 print('Oups, given projectX-cutmarks and active demuxer is vdrsync - have 2 convert cutmarks...'."\n");
                 my $fp;     
                 my $pcmRef = readIdx($path);
                 my $marks  = readMarks($path, $fnCutMarks, $pcmRef);

                 $fnCutMarks =~ s/\.pjx$/\.vdr/i;
                 writeMarks($fnCutMarks, $marks, $pcmRef);
              }
              $cmd .= ' -user-cutmarks=\''.$fnCutMarks.'\' ' if "$fnCutMarks" and -s $fnCutMarks;
              $cmd .= ' \''.$rec->{'path'}.'\' ';     
              }
           print('command: <|'.$cmd."|>\n");
           system($cmd) if not $main::testOnly;
           $i++;
           }
   print("\n");
}

sub muxRecordings {
   my $disk = shift;
   my $work = shift;
   my $i=0;

   foreach my $rec (@{$disk->{'recordings'}}){
           my $dest = $work.'/VDRSYNC.'.$i;
           my $cfg  = $rec->{'settings'};
           my($vid, $aud, $sum);

           print("\n".'------------------------------------------------------------------------------'."\n");
           print('--- mux recording ['.$rec->{'title'}.']...'."\n");
           print('------------------------------------------------------------------------------'."\n");
           my $cmd  = $appConfig->nice.' '.$appConfig->workerCommand('mplex').' -v 1 -S 0 -f 8 -V -o \''.$dest.'/movie%d.mpg\' ';

           if($cfg->{'demux'} =~ /projectX/){
              $vid = $dest.'/001.m2v' if -s $dest.'/001.m2v';
              $vid = $dest.'/001.mpv' if -s $dest.'/001.mpv';
              if(-s $dest.'/001['.$rec->{'lang'}.'].ac3'){
                 $aud = $dest.'/001['.$rec->{'lang'}.'].ac3';
                 }        
              elsif(-s $dest.'/001.ac3'){ $aud = $dest.'/001.ac3'; }
              else                      { $aud = $dest.'/001.mpa'; }
              }
           else{
              $vid = $dest.'/vdrsync.mpv';
              if(-s $dest.'/vdrsync.ac3'){ $aud = $dest.'/vdrsync.ac3';  }
              else                       { $aud = $dest.'/vdrsync0.mpa'; }
              }
           $cmd .= '\''.$vid.'\' \''.$aud.'\' ';
           print('muxRecordings() vid=<'.$vid.'>, aud=<'.$aud.">\n");
           $sum  = -s $vid;
           $sum += -s $aud;
           print("command: <|$cmd|>\n");
           system($cmd) if not $main::testOnly;

           if($doCleanup){
              my $sRes = 0;
              
              for my $n (1 .. 99){
                  last if not -s $dest.'/movie'.$n.'.mpg';
                  $sRes += -s $dest.'/movie'.$n.'.mpg';
                  }
              if($sRes){
                 print('muxRecordings(cleanup-check) -*- sizes: new='.$sRes.', old='.$sum."\n");
                 if($sRes >= $sum){
                    $cmd = 'rm -f \''.$vid.'\' \''.$aud.'\'';
                    print("command: <|$cmd|>\n");
                    system($cmd) if not $main::testOnly;
                    }
                 }
              }
           $i++;
           }
   print("\n");
}

sub createMenues {
   my $disk = shift;
   my $work = shift;
   my $i=0;
   my $tm = $appConfig->getThemeManager;

   $tm->getTheme($disk->{'theme'})->createDVDMenu($disk, [$work], $appConfig, 1);
}

sub createChapterString {
   my $chapLength = shift;     
   my $step = 0;
   my $rv = '0';
   my $i = 1;
   my $h = 0;
   my $m = 0;
   $chapLength = 5 if($chapLength < 1 or $chapLength > 10);
   while($step < 240){
         $step = $i++ * $chapLength;     
         $h = sprintf("%d", $step / 60);
         $m = sprintf("%02d", $step % 60);
         $rv .= ','.$m.':00' if $h lt 1;
         $rv .= ','.$h.':'.$m.':00' if $h ge 1;
         }
   return $rv;
}

sub authorDisk { 
   my $disk = shift;
   my $work = shift;
   my $cfg = $disk->{'settings'};
   my $cmd = 'mkdir \''.$work.'/DVDAUTHOR\'';
   my $tm = $appConfig->getThemeManager;
   my $i=0;

   print("command: <|$cmd|>\n");
   system($cmd);

   my $jfn = $work.'/dvd.xml';
   my $fpx;
   print("\n".'------------------------------------------------------------------------------'."\n");
   print('--- author dvd ['.$disk->{'title'}.']...'."\n");
   print('------------------------------------------------------------------------------'."\n");
   if(open($fpx, ">$jfn")){
      my $xBody = '';
      my $xVmgm = '  <vmgm>'."\n"
                 .'    <menus>'."\n"
                 .'      <video format="pal" aspect="'.$cfg->{'aspect'}.'"/>'."\n";

      $xVmgm .= $tm->getTheme($disk->{'theme'})->getDAMenuMain($disk, $work);

      $i = 0;
      foreach my $rec (@{$disk->{'recordings'}}){
              next if not $rec->{'title'};     
              my $chapters = createChapterString($rec->{'settings'}{'chapLen'});
              my $source = $work.'/VDRSYNC.'.$i.'/';
              
              print('create titleset for rec ['.$rec->{'title'}."]\n");     
     
              $xBody .= '  <titleset>'."\n";
              $xBody .= '    <menus>'."\n";
              $xBody .= '      <video format="pal" aspect="'.$rec->{'settings'}{'aspect'}.'"/>'."\n";
              for my $page (0 .. 99){
                 last if not -s $source.'menu-'.$page.'.mpg';
                 my $subMenue = $tm->getTheme($disk->{'theme'})->getDAMenuSub($rec, $source, $i, $page);
                 
                 $xBody .= $subMenue;
                 }
              $xBody .= '    </menus>'."\n";
              $xBody .= '    <titles>'."\n";
              $xBody .= '      <video format="pal" aspect="'.$rec->{'settings'}{'aspect'}.'"/>'."\n";
              $xBody .= '      <pgc>'."\n";
              $xBody .= '        <vob file="'.$source.'movie1.mpg"'."\n";
              $xBody .= '             chapters="'.$chapters.'"/>'."\n";
              $xBody .= '        <post>call vmgm menu;</post>'."\n";
              $xBody .= '      </pgc>'."\n";
              $xBody .= '    </titles>'."\n";
              $xBody .= '  </titleset>'."\n";
              $i++;
              } 
      if($disk->{'special'}{'title'}){
         my $source = $work.'/VDRSYNC.'.$i.'/';

         $xVmgm .= $tm->getTheme($disk->{'theme'})->getDASpecMainMenu($disk->{'special'}, $source, $i);

         $i = 0;
         foreach my $spec (@{$disk->{'special'}{'pages'}}){
                 next if not $spec->{'title'};
                 $xBody .= '  <titleset>'."\n";
                 $xBody .= '    <menus>'."\n";
                 $xBody .= '      <video format="pal" aspect="'.$disk->{'special'}{'settings'}{'aspect'}.'"/>'."\n";
                 for my $page (0 .. 99){
                    last if not -s $source.'menu'.$i.'-'.$page.'.mpg';
                    $xBody .= $tm->getTheme($disk->{'theme'})->getDASpecSubMenu($spec, $source, $i, $page);
                    } 
                 $xBody .= '    </menus>'."\n";
                 $xBody .= '    <titles>'."\n";
                 $xBody .= '      <video format="pal" aspect="'.$disk->{'special'}{'settings'}{'aspect'}.'"/>'."\n";
                 $xBody .= '      <pgc>'."\n";
                 $xBody .= '        <vob file="'.$appConfig->getAppAddonsDir.'/dummy.mpg"/>'."\n";
                 $xBody .= '        <post>call vmgm menu;</post>'."\n";
                 $xBody .= '      </pgc>'."\n";
                 $xBody .= '    </titles>'."\n";
                 $xBody .= '  </titleset>'."\n";
                 $i++;
                 } 
         }
      $xVmgm .= '    </menus>'."\n".'  </vmgm>'."\n";

      print($fpx '<dvdauthor dest="'.$work.'/DVDAUTHOR">'."\n");
      print($fpx $xVmgm.$xBody.'</dvdauthor>'."\n");
      close($fpx);
      }   
   $cmd = $appConfig->nice.' '.$appConfig->workerCommand('dvdauthor').' -x \''.$work.'/dvd.xml\'';
   print("command: <|$cmd|>\n");
   system($cmd) if not $main::testOnly;
}

sub createISO {
   my $disk = shift;
   my $work = shift;
   my $ts   = shift;

   print("\n".'------------------------------------------------------------------------------'."\n");
   print('--- create iso ['.$disk>{'title'}.']...'."\n");
   print('------------------------------------------------------------------------------'."\n");
   my $title = substr($disk->{title},0,30);

   $title =~ s/'/\\'/g;
   my $cmd = $appConfig->nice.' '.$appConfig->workerCommand('mkisofs').' -V $\''.$title
            .'\' -dvd-video \''.$work.'/DVDAUTHOR\' > \''.$appConfig->getIsoDir.'/job-'.$ts.'.iso\' ';
   print("command: <|$cmd|>\n");
   system($cmd) if not $main::testOnly;
}

sub cleanUP {
   my $disk = shift;
   my $work = shift;
   my $ts   = shift;
   my $i=0;

   print("\n".'------------------------------------------------------------------------------'."\n");
   print('--- cleanup ['.$disk>{'title'}.']...'."\n");
   print('------------------------------------------------------------------------------'."\n");
   if(-s $appConfig->getIsoDir.'/job-'.$ts.'.iso'){
      my $cmd = 'rm -rf \''.$work.'\'';

      print("command: <|$cmd|>\n");
      system($cmd) if not $main::testOnly;
      } 
   else{
      print('Oups, no iso found (should be ['.$appConfig->getIsoDir.'/job-'.$ts.'.iso]) - cleanup ABORTED.'."\n");     
      }     
}

sub burnMedia {
   my $ts = shift;
   my $iso = $appConfig->getIsoDir.'/job-'.$ts.'.iso';
   my @mi = $appConfig->checkMedia($appConfig->getBurnDev);
   
   if($mi[0])    { print('No media found. Skipped burning disk.'."\n"); return; }
   if($mi[1])    { print('Media is not a DVD. Skipped burning disk.'."\n"); return; }
   if(not $mi[2]){ print('Disk is not empty. Skipped burning disk.'."\n"); return; } 
   my $fs = $mi[3] * $mi[4] * 1024;
   
   if($fs < -s $iso){
      print('Sorry, but iso seems to be too BIG for the actual disk! Skipped burning disk.'."\n");
      return;
      }
   my $cmd = $appConfig->workerCommand('growisofs').' -dvd-compat ';
   
   $cmd .= ' -overburn ' if -s $iso > 4700000000;
   $cmd .= ' -Z '.$appConfig->getBurnDev.'=\''.$iso.'\'';
   
   print("command: <|$cmd|>\n");
   system($cmd) if not $main::testOnly;
}

sub checkCommands {
   my $job = shift;
   my $wantCutting = shift;
   my $wantMuxing = shift;
   my $wantMenues = shift;
   my $wantAuthoring = shift;
   my $wantISO = shift;
   my $wantCleanup = shift;
   my $wantBurn = shift;
   my $cmd;
   
   if($wantCutting){
      my @udm = $job->usedDemuxers;
      
      $wantCutting = 0 if $#udm < 0;
      foreach (@udm){
              for($_){
                 /vdrsync/    and do { $cmd = $appConfig->workerCommand('vdrsync');
                                       $wantCutting = 0 if not -x $cmd;
                                       print('check command <'.$cmd.">\n");
                                       print('command vdrsync not found on this system and vdrsync is used in job!'."\n");
                                       last;
                                       };
                 /projectX/   and do { $cmd = $appConfig->workerCommand('java');
                                       print('check command <'.$cmd.">\n");
                                       $wantCutting = 0 if not -x $cmd;
                                       $cmd = $appConfig->workerCommand('projectX');
                                       print('check command <'.$cmd.">\n");
                                       $wantCutting = 0 if not -s $cmd;
                                       print('command projectX not found on this system and projectX is used in job!'."\n") if not $wantCutting;
                                       last;
                                       };
                 }
              }
      if($wantCutting != 1){
         print('Disabling subsequent steps!.'."\n\n");
         $wantMuxing = 0;
         $wantAuthoring = 0;
         $wantISO = 0;
         $wantCleanup = 0;
         $wantBurn = 0;
         }
      }
   if($wantMuxing){
      $cmd = $appConfig->workerCommand('mplex');
      print('check command <'.$cmd.">\n");
      if(not -x $cmd){
         print('command mplex not found on this system and muxing was enabled for processing.'."\n"
              .'Disabling subsequent steps! '."\n\n");
         $doMuxing = 0;
         $doAuthor = 0;
         $doMkISO = 0;
         $doCleanup = 0;
         $doBurn = 0;
         $wantMuxing = 0;
         $wantAuthoring = 0;
         $wantISO = 0;
         $wantCleanup = 0;
         $wantBurn = 0;
         }
      }
   if($wantMenues){
      foreach (qw(convert ppmtoy4m mpeg2enc spumux pngquant mplex)){
              $cmd = $appConfig->workerCommand($_);
              if(not -x $cmd){
                 print('check command <'.$cmd.">\n");
                 print('command '.$_.' not found on this system and menue-creation was enabled for processing.'."\n"
                      .'Disabling subsequent steps! '."\n\n");
                 $doAuthor = 0;
                 $doMkISO = 0;
                 $doCleanup = 0;
                 $doBurn = 0;
                 $wantAuthoring = 0;
                 $wantISO = 0;
                 $wantCleanup = 0;
                 $wantBurn = 0;
                 last;
                 }
              }
      }
   if($wantAuthoring){
      $cmd = $appConfig->workerCommand('dvdauthor');
      if(not -x $cmd){
         print('check command <'.$cmd.">\n");
         print('command dvdauthor not found on this system and dvd-authoring was enabled for processing.'."\n"
              .'Disabling subsequent steps! '."\n\n");
         $doAuthor = 0;
         $doMkISO = 0;
         $doCleanup = 0;
         $doBurn = 0;
         $wantAuthoring = 0;
         $wantISO = 0;
         $wantCleanup = 0;
         $wantBurn = 0;
         }
      }
   if($wantISO){
      $cmd = $appConfig->workerCommand('mkisofs');
      if(not -x $cmd){
         print('check command <'.$cmd.">\n");
         print('command mkiosfs not found on this system and iso-creation was enabled for processing.'."\n"
              .'Disabling subsequent steps! '."\n\n");
         $doMkISO = 0;
         $doCleanup = 0;
         $doBurn = 0;
         $wantISO = 0;
         $wantCleanup = 0;
         $wantBurn = 0;
         }
      }
   if($wantCleanup){
      $wantCleanup = 0 if $main::testOnly;
      }
   if($wantBurn){
      foreach (qw(checkMedia growisofs)){
              $cmd = $appConfig->workerCommand($_);
              if(not -x $cmd){
                 print('check command <'.$cmd.">\n");
                 print('command '.$_.' not found on this system and burning was enabled for processing.'."\n"
                      .'Disabled burning! '."\n\n");
                 $doBurn = 0;
                 $wantBurn = 0;
                 last;
                 }
              }
      }
   return ($wantCutting, $wantMuxing, $wantMenues, $wantAuthoring, $wantISO, $wantCleanup, $wantBurn);
}

sub workoutJob {
   my $job = shift;
   my $defaultValues = $job->{'defaultvalues'};
   my @validSteps = checkCommands($job, $doCutting, $doMuxing, $doMenues, $doAuthor, $doMkISO, $doCleanup, $doBurn);

   foreach my $disk (@{$job->getDisks}){
          if(not ref($disk->{'recordings'}) eq 'ARRAY'){
             print('workoutJob(): Oups! no recordings or invalid data structure!'."\n");
             next;
             }
          if($#{$disk->{'recordings'}} < 0){
             print('workoutJob(): Oups! no recordings!'."\n");
             next;
             }
          my $tsStart  = time();
          
          if($firstTC){
             $tsStart = $firstTC;
             if(-d $appConfig->getWorkDir.'/.v2i.'.$tsStart){
                my $work = $appConfig->getWorkDir.'/.v2i.'.$tsStart;
                my $cmd  = 'rm -f '.$work.'/*.png'; 

                print('firstTC given. command: ['.$cmd."]\n") if $debug;                             
                system($cmd);
                for my $i (0 .. 99){
                    last if not -d $work.'/VDRSYNC.'.$i;
                    $cmd = 'rm -f '.$work.'/VDRSYNC.'.$i.'/*.png';
                    print('firstTC given. command: ['.$cmd."]\n") if $debug;                             
                    system($cmd);
                    }
                }
             $firstTC = 0;
             }
          print("\n\n".'############################ START disk PROCESSING ###########################'."\n");
          print('work on disk with title: '.$disk->{'title'}."\n");     
          print('      workdir-timestamp: '.$tsStart."\n\n");     
          my $scratch = $appConfig->getWorkDir.'/.v2i.'.$tsStart;
   
          if(not enufSpace($scratch)){
             print(stderr "\n".$appConfig->getLangToken('nospace')."\n\n");
             logmsg($appConfig->getLangToken('nospace'));
             exit;
             }     
          prepare(      $disk, $scratch);
          cutAndDemux(  $disk, $scratch) if $validSteps[0];
          muxRecordings($disk, $scratch) if $validSteps[1];
          createMenues( $disk, $scratch) if $validSteps[2];
          authorDisk(   $disk, $scratch) if $validSteps[3];
          createISO(    $disk, $scratch, $tsStart) if $validSteps[4];
          cleanUP(      $disk, $scratch, $tsStart) if $validSteps[5];
          burnMedia(    $tsStart) if $validSteps[6];
          }
}

#
# start of main
#
$appConfig = V2I::Config->new(-iniFN=>'');
processArgs(@ARGV);
$appConfig->loadColors;
tellConfig;
if($opMode eq 'server'){ while(1){ doServer(); }}
else{ doWorker(); }
