Transcode cron job

From MythTV Official Wiki
Jump to: navigation, search

Author Jim Goddard. Heavily based upon Robert Houghton's High Quality Transcode. Edits from Nigel Pearson's "Copy and transcode"
Description This script performs a high quality transcode for HD playback on PS3 and some Android devices over ethernet or wireless network. The transcoded file is added to the recorded table (use --nodelete or original file and database entry will be removed!). Settings produce a 3.5Mbps x264 file with 5.1 AAC audio if available, 2.0 AAC audio otherwise. This script does not automatically cut the commercials out of the orginal, but does re-commercial flag the new recording.

Output files are approximately 850Mb per 60 minutes (~80 size reduction for HD, ~50% reduction for SD).
Output files are named "Series_SXXEXX_Title.mp4" for archiving purposes.

This script is designed to be run as a cron job. It guarantees that it only runs one instance at a time, checks that no recordings are happening or will happen during the time it takes to transcode, and will select a random mpg file to transcode to mp4. I created it as I have about 6 years worth of recordings (7TB, and have only recently gotten a backend that can transcode in less than 6x realtime. These jobs take about 45 minutes per hour of HD or 20 minutes per hour of SD. It also has the ability to exclude a list of titles from being transcoded. I have a rudimentary "crop" option which will remove the fake letterboxing baked into many SD programs, this gets them close enough to 16:9 for mythtv to scale them better without chopping off the picture. Note that some Android devices have hardware acceleration only for certain H.264 profiles. If playback appears blocky and with poor frame rates, you probably are not getting hardware assist on the rendering. In that case, you should add a another -vpre setting after all others, with the name "baseline", to use the constrained baseline H.264 profile.

Supports


User script may be run as a cron entry, like:

*/20 *	* * *	root	/usr/local/bin/transcode_x264_cron.pl --random >> /var/log/transcode.log 2>> /var/log/transcode_errors.log

Or with more options:

transcode_x264_cron.pl -f <filename> | --random [ --crop ] [ --nodelete ] [ --debug ]

Requires ffmpeg compiled with --enable-libx264 and --enable-libfaac Fedora HOWTO Requires the DateTime and File::Random perl modules to be installed, and a valid .mythtv directory in the /root/ folder


Application-x-perl.png transcode_x264_cron.pl


#!/usr/bin/perl -w

# ============================================================================
# = NAME 
# x264_transcode_high.pl
#
# = PURPOSE
# Convert mpeg2 file from myth to h264 with aac audio.
#
# = USAGE
my $usage = 'Usage:
transcode_x264_cron.pl -j %JOBID% 
transcode_x264_cron.pl -f %FILE% 
transcode_x264_cron.pl -j %JOBID% | -f %FILE% [ --crop ]
transcode_x264_cron.pl --random [ --nodelete ] [ --debug ] # Select a random mpg file to transcode. 
';

# ============================================================================

use strict;
use MythTV;
use XML::Simple;
use File::Random qw/:all/;
use DateTime;

# What file are we copying/transcoding?
my $dir = undef;
my $file  = '';
my $jobid = -1;

# do nothing?
my $noexec = 0;

# extra console output?
#my $DEBUG = 1;
my $DEBUG = 0;

# Delete original file and database record?
my $DELETE = 1;

# some globals
my ($chanid, $command, $query, $ref, $starttime, $showtitle, $episodetitle);
my ($seasonnumber, $episodenumber, $episodedetails);
my ($newfilename, $newstarttime);
my $xmlparser = new XML::Simple;
my $xmlstring;
# globals for stream and resolution mapping
my ($output, $videostream, $audiostreamsurround, $audiostreamstereo, $framerate, $origwidth, $origheight);
# variables to see if myth is doing something
my $randint; #used in lock file
my $toosoon;
my @mythtvstatus;
my $statuscommand = "mythtv-status --noguide-data --noencoders --nodisk-space --noschedule-conflicts --noauto-expire --nototal-disk-space";
my $lockfile = "/tmp/transode_job_running";
my $nextrecordingdate;
my $startdate;
my $endtime;
my $enddate;
my $jobdelay = 45;
my $testfor;
my $index = 0;
my @titlesExclude = ( "Chuck","Eureka");
# transcode options
my $verbosity = "quiet";
my $docrop = 0; # Crop the video, assumes we have sd fake letterboxed video
my $dorandom = 0; # Select random mpg file to work on.
my $randtitle;
my $deinterlace = "-deinterlace"; # disabled if video is found to be progressive
#my $size = "720x720";
#for letterboxed, try scaling to 1280x1280 then crop to 1280:720
# trying this again without scaling, as does not seem to add quality
#my $size = "1280x1280";
# After all that, we really aren't going to resize SD.  We will downscale the HD, but later when we build the $command, we will NOT set size for SD recordings
# for HD1080i, we output to hd720 (looks just as good)
my $size = "hd720";
my $audiocodec = "libfaac";
my $audiobitrate = "160k";
my $audiofrequency = "48000";
my $audiochannels = 6; # changed to 2 if input carries no surround audio
my $audiostream = 1.0; # default audio channel
my $ftype = "mp4";
# Try a couple runs letting ffmpeg decide how many threads to use.
my $threads = 0;
#my $threads = 8;
#my $threads = 6;
my $nicevalue = 17; # don't hog the CPU
# try slow, medium is default
#ultrafast,superfast,veryfast,faster,fast,medium,slow,slower,veryslow,placebo
#my $videopreset = "medium"; # video preset to HQ settings for x264 encoder
my $videopreset = "faster"; # video preset to HQ settings for x264 encoder
my $videocodec = "libx264";
my $videobitrate = "1800k"; # target bitrate
# Crop was tested on fake letterboxed Eureka episodes recorded at 480x480
# This needs a lot more testing, especially since a lot of SD was recored at 534x480, or thereabouts.  Also I believe the height of the fakeletterbox varies.
# However, this did get the tested recordings close enough to 16:9 for myth to properly zoom them.  Should use the ffmpeg builtin variables.
#my $cropoptions = "crop=480:370:0:55";
my $cropoptions;
my $croppixels = 55;

my $mt = '';
my $db = '';

sub Reconnect()
{
    $mt = new MythTV();
    $db = $mt->{'dbh'};
}

# ============================================================================
sub Die($)
{
    print STDERR "@_\n";
    #remove our lockfile
    # But ONLY if it is this instance's lockfile!
    if ( -e $lockfile ) {
    	my $lockvalue = int(`cat $lockfile`);
	$DEBUG && print "$lockvalue\n";
    	if ( $lockvalue == $randint )
    	{
        	unlink $lockfile || print STDERR "Unable to remove lockfile! Remove lockfile manually, or next job will not run.\n";
    	}
    }
    exit -1;
}
# ============================================================================
sub ExitNoError($)
{
	print STDERR "@_\n";
	#remove our lockfile
	# But ONLY if it is this instance's lockfile!
	if ( -e $lockfile ) {
		my $lockvalue = int(`cat $lockfile`);
		$DEBUG && print "$lockvalue\n";
		if ( $lockvalue == $randint )
		{
			unlink $lockfile || print STDERR "Unable to remove lockfile! Remove lockfile manually, or next job will not run.\n";
		}
	}
	exit 0;
}

# Get a random number to use as the key for our lockfile
$randint = int(rand(100000));

print "Run started at " . DateTime->now(time_zone=>'local') . "\n" ;

#==========================
# First gatekeeping check, is our cron already running, because we only want to do one job at a time
if ( -e $lockfile )
{
    # We are currently running another instance of this script, bail now
    ExitNoError "Another job is running";
} else {
    # Ok, create our lockfile
    open LOCKFILE, ">",  $lockfile || Die "couldn't open lockfile";
    #while we are in here, lets grab the time
    print LOCKFILE $randint;
    close LOCKFILE;
}

# Before we check the command line options
# see if we are already recording or about to...
# will use the output from "mythtv-status"
# and parse it for "Recording Now:"
# if not actually recording, grab the first line after "Scheduled Recordings:"
# and check to see if that is within the next 45 minutes (about how long an HD recording takes to transcode).

@mythtvstatus = `$statuscommand`;
# See if we are currently recording.
$testfor = "Recording Now:";
# the grep thing from the interwebs isn't working, go old fashioned
$index = 0;
foreach (@mythtvstatus)
{
    $index++;
    #print $_;
    last if $_ =~ /$testfor/
}

if ( $index >= $#mythtvstatus )
{
   $DEBUG && print "Not recording right now (got to index: $index out of $#mythtvstatus), check next scheduled\n";
} else {
   ExitNoError "Currently Recording";
}
$index = 0;
$testfor = "Scheduled Recordings:";
foreach (@mythtvstatus)
{
    $index++;
    #print $_;
    last if $_ =~ /$testfor/
}

if ( $index >= $#mythtvstatus )
{
    $DEBUG && print "Error, no upcoming recordings!!\n";
} else {
    $DEBUG && print "Scheduled recordings start at index $index, so next line is what we want\n";
}
$DEBUG && print "Our first upcoming recording is:\n";
$DEBUG && print "$mythtvstatus[$index]\n";
#$index++;
#$DEBUG && print "$mythtvstatus[$index]\n";
#now grab the date out of that line
if ( $mythtvstatus[$index] =~ m/(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/ )
{
    $nextrecordingdate = DateTime-> new (
			year      => $1,
			month     => $2,
			day       => $3,
			hour      => $4,
			minute    => $5,
			second    => $6,
			time_zone => 'local'
			);
} else {
    $DEBUG && print "unsuccessful in pulling date from mythtv-status\n";
}

$DEBUG && print "Next recording starts at: $nextrecordingdate\n";

# ============================================================================
# Parse command-line arguments, check there is something to do:
#
if ( ! @ARGV )
{   Die "$usage"  }
Reconnect;

while ( @ARGV && $ARGV[0] =~ m/^-/ )
{
    my $arg = shift @ARGV;

    if ( $arg eq '-d' || $arg eq '--debug' )
    {   $DEBUG = 1  }
    elsif ( $arg eq '-n' || $arg eq '--noaction' )
    {   $noexec = 1  }
    elsif ( $arg eq '-j' || $arg eq '--jobid' )
    {   $jobid = shift @ARGV  }
    elsif ( $arg eq '-f' || $arg eq '--file' )
    {   $file = shift @ARGV  }
    elsif ( $arg eq '--crop' )
    {   $docrop = 1 }
    elsif ( $arg eq '--random' )
    {   $dorandom = 1 }
    elsif ( $arg eq '--nodelete' )
    {   $DELETE = 0 }
    elsif ( $arg eq '--debug' )
    {   $DEBUG = 1 }
    else
    {
        unshift @ARGV, $arg;
        last;
    }
}

# If we said random, overwrite $file
if ( $dorandom )
{
    $dir = "/var/lib/mythtv/recordings/";
    $file = random_file( -dir => $dir, -check => qr/\.mpg$/, -recursive => 0 );
	if ( $file =~ m/(\d+)_(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ )
	{
		$chanid = $1, $starttime = "$2-$3-$4 $5:$6:$7";
		$query = $db->prepare("SELECT title,endtime FROM recorded WHERE chanid=$chanid AND starttime='$starttime';");
		$query->execute || Die "Unable to query recorded table";
		( $randtitle, $endtime ) = $query->fetchrow_array;
		$query->finish;
	} else {
		$DEBUG && print "Could not retrieve title name\n";
	}
}

if ( ! $file && $jobid == -1 )
{
    Die "No file or job specified. $usage";
}

# ============================================================================
# If we were supplied a jobid, lookup chanid
# and starttime so that we can find the filename
#
if ( $jobid != -1 )
{
    $query = $db->prepare("SELECT chanid, starttime, endtime " .
                          "FROM jobqueue WHERE id=$jobid;");
    $query->execute || Die "Unable to query jobqueue table";
    $ref       = $query->fetchrow_hashref;
    $chanid    = $ref->{'chanid'};
    $starttime = $ref->{'starttime'};
    $endtime   = $ref->{'endtime'};
    $query->finish;

    if ( ! $chanid || ! $starttime )
    {   Die "Cannot find details for job $jobid"  }

    $query = $db->prepare("SELECT basename FROM recorded " .
                          "WHERE chanid=$chanid AND starttime='$starttime';");
    $query->execute || Die "Unable to query recorded table";
    ($file) = $query->fetchrow_array;
    $query->finish;

    if ( ! $file )
    {   Die "Cannot find recording for chan $chanid, starttime $starttime"  }

    if ( $DEBUG )
    {
        print "Job $jobid refers to recording chanid=$chanid,",
              " starttime=$starttime\n"
    }
}
else
{
    if ( $file =~ m/(\d+)_(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ )
    {   $chanid = $1, $starttime = "$2-$3-$4 $5:$6:$7"  }
    else
    {
        print "File $file has a strange name. Searching in recorded table\n";
        $query = $db->prepare("SELECT chanid, starttime, endtime " .
                              "FROM recorded WHERE basename='$file';");
        $query->execute || Die "Unable to query recorded table";
        ($chanid,$starttime,$endtime) = $query->fetchrow_array;
        $query->finish;

        if ( ! $chanid || ! $starttime )
        {   Die "Cannot find details for filename $file"  }
    }
}

# A commonly used SQL row selector:
my $whereChanAndStarttime = "WHERE chanid=$chanid AND starttime='$starttime'";

#=================
# Now we should have an endtime, if not, grab it.
# then figure out the duration of this program, divide it in two, that is our
# starting "toosoon" time.
if ( ! $endtime )
{

	$query = $db->prepare("SELECT endtime FROM recorded $whereChanAndStarttime;");
	$query->execute || print "Unable to query recorded table\n";
	$endtime = $query->fetchrow_array;
	$query->finish;
}

#================
# Now convert that to datetime so that we can get the duration of the selected title.
if ( $starttime =~ m/(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/ )
{
	$startdate = DateTime-> new (
		year      => $1,
		month     => $2,
		day       => $3,
		hour      => $4,
		minute    => $5,
		second    => $6,
		time_zone => 'local'
		);
}
if ( $endtime =~ m/(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/ )
{
	$enddate = DateTime-> new (
		year      => $1,
		month     => $2,
		day       => $3,
		hour      => $4,
		minute    => $5,
		second    => $6,
		time_zone => 'local'
		);
}
if ( ! $enddate ) {
	$jobdelay = 45; # anything under 3 hours should be done in this window.
} else {
	$jobdelay = ( $enddate - $startdate )->in_units( 'minutes' );
	$jobdelay = int( $jobdelay / 2);
	$DEBUG && print "Based on the length of this program, we should make sure we have $jobdelay minutes until the next recording starts\n";
}
$toosoon = DateTime->now(time_zone=>'local')->add( minutes => $jobdelay );
if ( $nextrecordingdate <= $toosoon )
{
    ExitNoError "Next recording is starting too soon. Need $jobdelay before next recording from now, next recording at $nextrecordingdate";
} else {
    $DEBUG && print "No recordings scheduled within our window, $toosoon.\n";
}
if ( $dorandom )
{
	if ( grep {$_ eq $randtitle} @titlesExclude )
	{
		ExitNoError "$randtitle is excluded from transcoding by cron. Excluded: @titlesExclude";
	} else {
		$DEBUG && print "Show title $randtitle is OK to transcode\n";
	}
}
# ============================================================================
# Find the directory that contains the recordings, check the file exists
#
$dir  = undef;
my $dirs = $mt->{'video_dirs'};

foreach my $d ( @$dirs )
{
	if ( ! -e $d )
	{   Die "Cannot find directory $dir that contains recordings"  }

	if ( -e "$d/$file" )
	{
		$dir = $d;
		last
	}
	else
	{   print "$d/$file does not exist\n"   }
}

if ( ! $dir )
{   Die "Cannot find recording"  }

# ============================================================================
# Get ffmpeg info
#
$audiostreamstereo   = "";
$audiostreamsurround = "";
$command = "ffmpeg -i $dir/$file ";
open(FF_info, "$command 2>&1 |");
while ( defined(my $line = <FF_info>) ) {
	chomp($line);
	if ( $line =~ /^\s*Stream.*#(\S\.\S).*:\sVideo.*\s(\d{3,4})x(\d{3,4}).*\s(\d+.*)\stbr/ )
	{
		$framerate = $4;
                $origwidth = $2;
		$origheight = $3;
		$videostream = $1;
		$DEBUG && print "original format: ${origwidth}x${origheight},fps $framerate\n";
                next;
	}
	if ( $line =~ /^\s*Stream.*#(\S\.\S).*:\sAudio.*stereo/ )
	{
		$audiostreamstereo = $1;
		next;
	}
	if ( $line =~ /^\s*Stream.*#(\S\.\S).*:\sAudio.*5.1/ )
	{
		$audiostreamsurround = $1;
		next;
	}
}
if ( $framerate <= 30.0 ) { $deinterlace = "-deinterlace" } elsif ( $framerate > 30 && $framerate <= 60 ) { $deinterlace = "" }

# ============================================================================
# First, generate a new filename,
#
$query = $db->prepare("SELECT title FROM recorded $whereChanAndStarttime;");
$query->execute || Die "Unable to query recorded table";
$showtitle = $query->fetchrow_array;
$query->finish;

$query = $db->prepare("SELECT subtitle FROM recorded $whereChanAndStarttime;");
$query->execute || Die "Unable to query recorded table";
$episodetitle = $query->fetchrow_array;
$query->finish;

if ( $episodetitle ne "" ) 
{
  $seasonnumber = "";
  $episodenumber = "";
  $xmlstring = `/usr/share/mythtv/metadata/Television/ttvdb.py -N "$showtitle" "$episodetitle"`;
  if ( $xmlstring ne "" ) {
    $episodedetails =$xmlparser->XMLin($xmlstring);
    $seasonnumber = $episodedetails->{item}->{season};
    $episodenumber = $episodedetails->{item}->{episode};
  }
}
my ($year,$month,$day,$hour,$mins,$secs) = split m/[- :]/, $starttime;
my $oldShortTime = sprintf "%04d%02d%02d",
                   $year, $month, $day;
my $iter = 0;

do {
  if ( $episodetitle eq "" || $seasonnumber eq "" || $episodenumber eq "" )
  {
    $newfilename = sprintf "%s_%s.%s.%s", $showtitle, $month, $day, $year;
  } else {
    $newfilename = sprintf "%s_S%0sE%0s_%s", $showtitle, $seasonnumber, $episodenumber, $episodetitle;
    #$newfilename = sprintf "%s_S%0sE%0s_%s_%s_%s", $showtitle, $seasonnumber, $episodenumber, $episodetitle, $videopreset, $videobitrate;
    #$newfilename = sprintf "%s_S%0sE%0s_%s_%s_%s_%s_cropped", $showtitle, $seasonnumber, $episodenumber, $episodetitle, $size, $videopreset, $videobitrate;
  }
  $newfilename =~ s/\;/   AND   /g;
  $newfilename =~ s/\&/   AND   /g;
  $newfilename =~ s/\s+/ /g;
  $newfilename =~ s/\s/_/g;
  $newfilename =~ s/:/_/g;
  $newfilename =~ s/__/_/g;
  $newfilename =~ s/\(//g;
  $newfilename =~ s/\)//g;
  $newfilename =~ s/'//g;
  $newfilename =~ s/\!//g;
  $newfilename =~ s/\///g;
  if ( $iter != "0" ) 
  {  $newfilename = sprintf "%s_%d%s", $newfilename, $iter, ".mp4"  } else { $newfilename = sprintf "%s%s", $newfilename, ".mp4" }
  $iter ++;
  $secs = $secs + $iter;
  $newstarttime = sprintf "%04d-%02d-%02d %02d:%02d:%02d",
                    $year, $month, $day, $hour, $mins, $secs;
} while  ( -e "$dir/$newfilename" );

$DEBUG && print "$dir/$newfilename seems unique\n";

# ============================================================================
# Now do the actual transcode
#
$audiochannels = 6;
$audiostream = $audiostreamsurround;
if ( $audiostreamsurround eq "" )
{
  $audiochannels = 2;
  $audiostream = $audiostreamstereo;
} 

$command = "nice -n $nicevalue ffmpeg -loglevel $verbosity -i $file";
$command = "$command -acodec $audiocodec";
$command = "$command -ar $audiofrequency";
$command = "$command -ac $audiochannels";
$command = "$command -ab $audiobitrate";
$command = "$command -async 1";
$command = "$command -copyts";
# Try just commenting out the resize
# On nth thought, going to give up on the padding and cropping, and just use original size for sd, it doesn't look better upscaled anyway, but we definitely don't want the video to get stretched. Most SD records at 480x480, some is 534x480.
if ( $origwidth eq '480' || $origheight eq '480' )
{
    # while we are here, pick a better quality for SD, it is small anyway
    $videopreset = "fast";
} else {
    $command = "$command -s $size";
}
$command = "$command -f $ftype";
$command = "$command -vcodec $videocodec";
$command = "$command -vpre $videopreset";
$command = "$command -b $videobitrate";
$command = "$command -threads $threads";
$command = "$command -level 31";
$command = "$command -map $videostream";
$command = "$command -map $audiostream";
$command = "$command $deinterlace";
if ( $docrop && $origwidth && $origheight )
{
        #$cropoptions = "crop=480:370:0:55";
        my $newheight = $origheight - ($croppixels * 2);
        $cropoptions = "$origwidth:$newheight:0:$croppixels";

        $command = "$command -vf $cropoptions";
}

$command = "$command $newfilename";

$DEBUG && print "Executing: $command\n";

chdir $dir;
system $command;

if ( ! -e "$dir/$newfilename" )
{   Die "Transcode failed\n"  }

# Now we want to run two more things, qt-fastart to optimize the file for streaming, which we can do now, and then once the db is updated, we want to re-commflag the recording
my $speedup = "/usr/local/bin/qtfaststart $newfilename";
#my $speedup = "/usr/local/bin/qtfaststart /home/monkey/testtranscode/$newfilename";
$DEBUG && print "Executing: $speedup\n";
chdir $dir;
system $speedup;

#make sure we have the correct permissions
$command = "chmod 664 $dir/$newfilename";
$DEBUG && print "Executing: $command\n";
system $command;
$command = "chown mythtv. $dir/$newfilename";
$DEBUG && print "Executing: $command\n";
system $command;

## Commenting out the entire db update section.

# ============================================================================
# Last, copy the existing recorded details with the new file name.
#
Reconnect;
$query = $db->prepare("SELECT * FROM recorded $whereChanAndStarttime;");
$query->execute ||  Die "Unable to query recorded table";
$ref = $query->fetchrow_hashref;
$query->finish;

$ref->{'starttime'} = $newstarttime;
$ref->{'basename'}  = $newfilename;
if ( $DEBUG && ! $noexec )
{
    print 'Old file size = ' . (-s "$dir/$file")        . "\n";
    print 'New file size = ' . (-s "$dir/$newfilename") . "\n";
}
$ref->{'filesize'}  = -s "$dir/$newfilename";

my $extra = 'Copy';


#
# The new recording file has no cutlist, so we don't insert that field
#
my @recKeys = grep(!/^cutlist$/, keys %$ref);

#
# Build up the SQL insert command:
#
$command = 'INSERT INTO recorded (' . join(',', @recKeys) . ') VALUES ("';
foreach my $key ( @recKeys )
{
    if (defined $ref->{$key})
    {   $command .= quotemeta($ref->{$key}) . '","'   }
    else
    {   chop $command; $command .= 'NULL,"'   }
}

chop $command; chop $command;  # remove trailing comma quote

$command .= ');';

if ( $DEBUG || $noexec )
{   print "# $command\n"  }

if ( ! $noexec )
{   $db->do($command)  || Die "Couldn't create new recording's record, but transcoded file exists $newfilename\n"   }

# ============================================================================

#$db->disconnect;

#just before we return we want to recommflag this file
$command = "nice -n $nicevalue mythcommflag -f $newfilename --nopercentage --quiet --method all";
$DEBUG && print "Executing: $command\n";
system $command;

if ( $DELETE ) 
{
	# Aboslutely lastly, delete the original recording and db entry
	$DEBUG && print "Deleting original file: $file\n";
	unlink ( $file ) || Die "Did not delete original file, leaving database record as well.";
	
	# Assuming the original file was deleted, go ahead and remove the original entry in the database as well.
	#DELETE FROM recorded WHERE basename='1647_20120319030000.mpg' LIMIT 1;
	$command = "DELETE FROM recorded WHERE basename='$file' LIMIT 1;";
	$DEBUG && print "$command\n";
	if ( ! $noexec )
	{   $db->do($command)  || Die "Couldn't delete old recording's record, but $file has been deleted, run find_orphans.py\n" }
}

$db->disconnect;

if ( -e $lockfile )
{
	unlink $lockfile || Die "Unable to remove lockfile.";
}

print "Run ended at " . DateTime->now(time_zone=>'local') . "\n" ;
1;