#!/usr/bin/perl # mythexport-daemon v2.2.3 # By: John Baab # Email: rhpot1991@ubuntu.com # Purpose: daemon for exporting mythtv recordings into formats used by portable devices. # Requirements: perl and the DBI & DBD::mysql modules, MythTV perl bindings, AtomicParsley # # License: # # This Package 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 3 of the License, or (at your option) any later version. # # This package 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 package; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # On Debian & Ubuntu systems, a complete copy of the GPL can be found under # /usr/share/common-licenses/GPL-3, or (at your option) any later version use strict; use POSIX qw(setsid); use DBI; use DBD::mysql; use Config::Simple; use MythTV; use Proc::Daemon; use Proc::PID::File; use Log::Dispatch; use Log::Dispatch::File; use Date::Format; use File::Spec; use File::Copy; use XML::Writer; use IO::File; use lib '/usr/share/mythexport'; use lib '/usr/share/mythexport/configs'; &startDaemon; my $debug = ""; my $level = "notice"; foreach (@ARGV){ if ($_ =~ m/debug/) { $debug = 1; } } if($debug == 1){ $level = "debug"; } use constant LOG_DIR => '/var/log/mythtv/'; use constant LOG_FILE => 'mythexport.log'; our $HOSTNAME = `hostname`; chomp $HOSTNAME; my $log = new Log::Dispatch( callbacks => sub { my %h=@_; return Date::Format::time2str('%B %e %T', time)." ".$HOSTNAME." $0\[$$]: ".$h{message}."\n"; } ); $log->add( Log::Dispatch::File->new( name => 'file1', min_level => $level, mode => 'append', filename => File::Spec->catfile(LOG_DIR, LOG_FILE), ) ); $log->warning("Starting Processing: ".time()); open STDERR, '>>', File::Spec->catfile(LOG_DIR, LOG_FILE); my $connect = undef; my $myth = undef; my $tries = 5; while ($connect == undef && --$tries > 0) { eval { $myth = new MythTV(); # connect to database $connect = $myth->{'dbh'}; 1; } or do { logerror("Can't connect to MythTV: $@"); logdebug("Sleeping 5 seconds..."); sleep(5); }; } if ($connect == undef) { die "Couldn't connect to MythTV."; } my $keep_going = 1; $SIG{HUP} = sub { $log->warning("Caught SIGHUP: exiting gracefully"); $keep_going = 0; }; $SIG{INT} = sub { $log->warning("Caught SIGINT: exiting gracefully"); $keep_going = 0; }; $SIG{QUIT} = sub { $log->warning("Caught SIGQUIT: exiting gracefully"); $keep_going = 0; }; $SIG{TERM} = sub { $log->warning("Caught SIGTERM: exiting gracefully"); $keep_going = 0; }; sub startDaemon{ Proc::Daemon::Init; dienice("Already running!") if Proc::PID::File->running(dir => "/var/run/mythtv",name => "mythexport"); } sub dienice($) { my ($package, $filename, $line) = caller; $log->critical("$_[0] at line $line in $filename"); die $_[0]; } sub logerror($) { my ($package, $filename, $line) = caller; $log->critical("$_[0] at line $line in $filename"); } sub logdebug($){ my ($package, $filename, $line) = caller; $log->debug("$_[0] at line $line in $filename"); } sub getExportDir(){ # Set default values my $cfg = new Config::Simple(); $cfg->read('/etc/mythtv/mythexport/mythexport.cfg') || logerror("Cannot read config file: /etc/mythtv/mythexport/mythexport.cfg"); my $exportdir = $cfg->param("dir"); $exportdir =~ s/\/$//; return $exportdir; } sub createXML($){ my @params = split('&',$_[0]); my ($exportdir, @starttime, @chanid, $config, $sql_where) = ""; foreach (@params){ if ($_ =~ m/exportdir/) { $exportdir = (split(/\=/,$_))[1]; } elsif ($_ =~ m/starttime/) { @starttime = split('\|',(split(/\=/,$_))[1]); } elsif ($_ =~ m/chanid/) { @chanid = split('\|',(split(/\=/,$_))[1]); } elsif ($_ =~ m/config/) { $config = (split(/\=/,$_))[1]; } } # debugging logging logdebug("exportdir = $exportdir"); logdebug("starttime = @starttime"); logdebug("chanid = @chanid"); logdebug("config = $config"); $exportdir =~ s/\/$//; require "$config.pm"; my $object = new $config(); my $extension = $object->Extension; #test that the directory has the correct permissions -w $exportdir || logerror("ERROR: Directory $exportdir is not writeable.\n"); my $output = new IO::File(">$exportdir/mythimport.xml"); # create sql where clause foreach my $i (0..scalar(@chanid)-1) { $sql_where .= "(rec.chanid=\'$chanid[$i]\' and rec.starttime=\'$starttime[$i]\') or "; } $sql_where =~ s/\sor\s$//; print $output ""; my $writer = new XML::Writer(OUTPUT => $output); $writer->startTag("channel"); my $query = "SELECT rec.title, rec.subtitle, rec.description, pg.syndicatedepisodenumber, pg.showtype, rec.programid, rec.basename, rec.chanid, rec.starttime FROM recorded rec LEFT JOIN program pg ON pg.starttime = rec.starttime AND pg.chanid = rec.chanid WHERE $sql_where"; my $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute() || logerror("Unable to query mythexport table"); $log->notice("Creating xml file"); while ( my ($title,$subtitle,$description,$syndicatedepisodenumber,$showtype,$programid,$basename,$currentchanid,$currentstarttime) = $query_handle->fetchrow_array() ) { # FIND OUT THE CHANNEL NAME my $subquery = "SELECT callsign FROM channel WHERE chanid=?"; my $subquery_handle = $connect->prepare($subquery); $subquery_handle->execute($currentchanid) || logerror("ERROR: Unable to query settings table"); my $channame = $subquery_handle->fetchrow_array; # replace non-word characters in channame with dashes $channame =~ s/\W+/-/g; # replace non-word characters in title with underscores $title =~ s/\W+/_/g; # replace non-word characters in subtitle with underscores $subtitle =~ s/\W+/_/g; # Remove non alphanumeric chars from $starttime & $endtime $currentstarttime =~ s/[|^\W|\s|-|]//g; my $newfilename = $channame."-".$title."-".$subtitle."-".$currentstarttime; $newfilename =~ s/\..*?$//; $writer->startTag("item"); $writer->startTag("title"); $writer->characters("$title - $subtitle"); $writer->endTag("title"); $writer->startTag("description"); $writer->characters("$description"); $writer->endTag("description"); $writer->startTag("link"); $writer->characters("$newfilename.$extension"); $writer->endTag("link"); $writer->startTag("guid"); $writer->characters("$newfilename"); $writer->endTag("guid"); $writer->endTag("item"); $log->notice("Creating mysql dump"); export("starttime=$currentstarttime&chanid=$currentchanid&config=$config&otg=true"); } $writer->endTag("channel"); $writer->end(); $output->close(); copy("/usr/share/mythtv/mythimport.xslt","$exportdir/mythimport.xslt") || logerror("ERROR: Cannot copy /usr/share/mythtv/mythimport.xslt to $exportdir/mythimport.xslt"); } sub createSQL($){ my @params = split('&',$_[0]); my ($exportdir, @starttime, @chanid, $config, $sql_where) = ""; foreach (@params){ if ($_ =~ m/exportdir/) { $exportdir = (split(/\=/,$_))[1]; } elsif ($_ =~ m/starttime/) { @starttime = split('\|',(split(/\=/,$_))[1]); } elsif ($_ =~ m/chanid/) { @chanid = split('\|',(split(/\=/,$_))[1]); } elsif ($_ =~ m/config/) { $config = (split(/\=/,$_))[1]; } } # debugging logging logdebug("exportdir = $exportdir"); logdebug("starttime = @starttime"); logdebug("chanid = @chanid"); logdebug("config = $config"); $exportdir =~ s/\/$//; my $dbuser = $myth->{'db_user'}; my $dbpass = $myth->{'db_pass'}; my $dbhost = $myth->{'db_host'}; my $dbname = $myth->{'db_name'}; my $dbport = $myth->{'db_port'}; # debugging logging logdebug("dbuser = $dbuser"); logdebug("dbpass = $dbpass"); logdebug("dbhost = $dbhost"); logdebug("dbname = $dbname"); logdebug("dbport = $dbport"); #test that the directory has the correct permissions -w $exportdir || logerror("ERROR: Directory $exportdir is not writeable.\n"); # create sql where clause foreach my $i (0..scalar(@chanid)-1) { $sql_where .= "(chanid=\'$chanid[$i]\' and starttime=\'$starttime[$i]\') or "; } $sql_where =~ s/\sor\s$//; my $command = "mysqldump -h$dbhost -u$dbuser -p$dbpass -P$dbport $dbname recorded recordedseek recordedrating recordedprogram recordedmarkup recordedcredits --where=\"$sql_where\" --no-create-db --no-create-info > $exportdir/mythimport.sql 2>&1"; $log->notice("Creating mysql dump"); logdebug("mysqldump command = $command"); system($command) == 0 || logerror("ERROR: $command failed."); my $query = "SELECT basename FROM recorded where $sql_where"; my $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute() || logerror("Unable to query mythexport table"); my $schemaVer = $myth->backend_setting('DBSchemaVer'); #copy files while ( my $basename = $query_handle->fetchrow_array() ) { my $dir = 'UNKNOWN'; if ($schemaVer < 1171) { $dir = $myth->backend_setting('RecordFilePrefix'); } else { my $storagegroup = new MythTV::StorageGroup(); $dir = $storagegroup->FindRecordingDir($basename); } $log->notice("Copying $dir/$basename to $exportdir/$basename"); copy("$dir/$basename","$exportdir/$basename") || logerror("Unable to copy $dir/$basename to $exportdir/$basename"); } } sub export($){ my @params = split("&",$_[0]); my ($debug,$starttime,$chanid,$config,$deleteperiod,$otg,$podcastname) = ""; foreach (@params){ if ($_ =~ m/starttime/) { $starttime = (split(/\=/,$_))[1]; } elsif ($_ =~ m/chanid/) { $chanid = (split(/\=/,$_))[1]; } elsif ($_ =~ m/config/) { $config = (split(/\=/,$_))[1]; } elsif ($_ =~ m/deleteperiod/) { $deleteperiod = (split(/\=/,$_))[1]; } elsif ($_ =~ m/otg/) { $otg = (split(/\=/,$_))[1]; } elsif ($_ =~ m/podcastname/) { $podcastname = (split(/\=/,$_))[1]; } } my ($title, $subtitle, $description, $syndicatedepisodenumber, $showtype, $programid, $basename, $airdate) = ""; my $exportdir = getExportDir(); # debugging logging logdebug("exportdir = $exportdir"); logdebug("starttime = $starttime"); logdebug("chanid = $chanid"); logdebug("config = $config"); #test that the directory has the correct permissions -w $exportdir || logerror("ERROR: Directory $exportdir is not writeable.\n"); my $query = "SELECT rec.title, rec.subtitle, rec.description, pg.syndicatedepisodenumber, pg.showtype, rec.programid, rec.basename, rec.starttime FROM recorded rec LEFT JOIN program pg ON pg.starttime = rec.starttime AND pg.chanid = rec.chanid WHERE rec.chanid=? AND rec.starttime=?"; my $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute($chanid,$starttime) || logerror("ERROR: Cannot connect to database \n"); $query_handle->bind_columns(undef, \$title, \$subtitle, \$description, \$syndicatedepisodenumber, \$showtype, \$programid, \$basename, \$airdate); $query_handle->fetch(); my $schemaVer = $myth->backend_setting('DBSchemaVer'); # Storage Groups were added in DBSchemaVer 1171 # FIND WHERE THE RECORDINGS LIVE my $dir = 'UNKNOWN'; if ($schemaVer < 1171) { logdebug("Using compatibility mode"); $dir = $myth->backend_setting('RecordFilePrefix'); } else { logdebug("Going into new mode\n"); my $storagegroup = new MythTV::StorageGroup(); $dir = $storagegroup->FindRecordingDir($basename); } # FIND OUT THE CHANNEL NAME $query = "SELECT callsign FROM channel WHERE chanid=?"; $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute($chanid) || logerror("ERROR: Unable to query channel table"); my $channame = $query_handle->fetchrow_array; # replace non-word characters in channame with dashes $channame =~ s/\W+/-/g; # replace non-word characters in title with underscores my $title_old = $title; $title =~ s/\W+/_/g; # replace non-word characters in subtitle with underscores my $subtitle_old = $subtitle; $subtitle =~ s/\W+/_/g; # Remove non alphanumeric chars from $starttime & $endtime my $newstarttime = $starttime; $newstarttime =~ s/[|^\W|\s|-|]//g; my $filename = $dir."/".$basename; my $newfilename = $exportdir."/".$channame."-".$title."-".$subtitle."-".$newstarttime; $newfilename =~ s/\..*?$//; $syndicatedepisodenumber =~ m/^.*?(\d*)(\d{2})$/; my $seasonnumber = $1; my $episodenumber = $2; require "$config.pm"; # need the extension to calculate our file name my $object = new $config(); my $extension = $object->Extension(); # Trim any characters over 63, # it seems iTunes does not like files with lengths this long. my $x = 63 - $extension; $newfilename =~ s/^(.*\/(.{1,$x})).*$/$1/g; my $webfilename = "$2"; if ($debug) { print "\n\n Source filename:$filename \nDestination filename:$newfilename\n \n"; } # move to the export directory incase any logs get written chdir $exportdir; # create new object with our file names my $object = new $config($filename, $newfilename); $object->export(); my $returnCode = $object->checkOutput(); # only continue if encoding didn't fail if ($returnCode == 1){ if (!$debug){ # clean up any log files left behind by ffmpeg system "rm *.log"; system "rm *.mbtree"; } if($otg ne "true"){ # Save Data $query = "INSERT into mythexport VALUES(NULL,?,?,?,?,NOW(),DATE_ADD(NOW(),INTERVAL ? DAY),?,?)"; $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute("$webfilename$extension",$title_old,$subtitle,$description,$deleteperiod,$airdate,$podcastname) || logerror("ERROR: Unable to update table."); } } else{ logerror("ERROR: No resulting file from ffmpeg, most likely your ffmpeg failed. Enable debugging and test by hand."); } } sub delete($){ my $param = $_[0]; my $cfg = new Config::Simple(); $cfg->read('/etc/mythtv/mythexport/mythexport.cfg') || logerror("Cannot read config file: /etc/mythtv/mythexport/mythexport.cfg"); my $dir = $cfg->param("dir"); $dir =~ s/\/$//; my $delete_query = "SELECT file FROM mythexport where id=?"; my $delete_query_handle = $connect->prepare($delete_query); logdebug("query = $delete_query"); $delete_query_handle->execute($param) || logerror("ERROR: Unable to query mythexport table"); my $file = $delete_query_handle->fetchrow_array(); my $location = "$dir\/$file"; $log->notice("Deleting $file"); logdebug("file = $location"); unlink($location) || logerror("ERROR: Unable to find file: $location"); my $query = "delete from mythexport where id=?"; my $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute($param) || logerror("Unable to delete from mythexport table"); } # our infinite loop while($keep_going) { my $query = "select id, type, param from mythexport_job_queue order by id"; my $query_handle = $connect->prepare($query); logdebug("query = $query"); $query_handle->execute() || logerror("Unable to query mythexport_job_queue table"); while(my ($id, $type, $param) = $query_handle->fetchrow_array()){ if($type eq "delete"){ &delete($param); } elsif($type eq "export"){ &export($param); } elsif($type eq "otg-lightweight"){ createXML($param); } elsif($type eq "otg-full"){ createSQL($param); } else{ logerror("ERROR: job type unknown."); } my $job_query = "delete from mythexport_job_queue where id=?"; my $job_query_handle = $connect->prepare($job_query); logdebug("query = $job_query"); $job_query_handle->execute($id) || logerror("Unable to delete from mythexport_job_queue table"); # remove lock file so the userjob knows that it's work is done my $lockfile = getExportDir() . "/mythexport.$id"; if (-e $lockfile){ unlink($lockfile); } } # wait for 60 seconds sleep(60); } $log->warning("Stopping Processing: ".time());