#!/usr/bin/perl # # This program 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 2 # of the License, or (at your option) any later version. # # This program 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 program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Or, point your browser to http://www.gnu.org/copyleft/gpl.html # # Parts of this code are taken from VDRAdmin-AM # (C) 2005 - 2008 by Andreas Mair use Socket; use POSIX; my (%CONFIG); $CONFIG{VDR_HOST} = "localhost"; # Name or IP address of VDR server $CONFIG{VDR_PORT} = 6419; # SVDRP port on VDR server (use 2001 pre - 1.7.15) $CONFIG{SERIES_TIMEOUT} = 30; # Expiry time of a series (days) $CONFIG{START_PADDING} = 1; # Padding when creating new timers without VPS $CONFIG{STOP_PADDING} = 3; $CONFIG{PRIORITY} = 99; # Recording priority and lifetime $CONFIG{LIFETIME} = 99; $CONFIG{LINKSDIR} = "/video/video"; # Directory holding links file $CONFIG{VPS} = 1; # 1 = control timer from EIT running status if ($CONFIG{VPS}) { $CONFIG{START_PADDING} = 0; $CONFIG{VPS} = 1; } my (@timers, @chans, @epg); my %links = {}; my $updates = 0; open_SVDRP(); get_channels(); get_epg(); get_timers(); get_links(); if (scalar (@timers)) { $updates = check_timers(); } $updates += check_links(); if (scalar (@timers)) { $updates += check_split_recordings(); if ($updates) { put_links(); @timers = (); get_timers(); } #show_links(); check_timer_clashes(); check_changed_events(); } close_SVDRP(); # Examine each timer and update the links file if necessary (manually-added timers) sub check_timers { my $count = 0; foreach my $timer (@timers) { my $channelid = $chans[$timer->{chan}] -> {id}; my $start_t = $timer->{tstart}; my $stop_t = $timer->{tstop}; foreach my $prog (@epg) { my $scrid = $prog->{scrid}; my $icrid = $prog->{icrid}; next if $scrid eq 'NULL'; if (($prog->{sid} eq $channelid) && ($start_t <= $prog->{st}) && ($stop_t >= $prog->{et})) { if (exists $links{$scrid}) { # Existing series if ($links{$scrid} !~ /$icrid/) { my ($last_t,$val) = split(';', $links{$scrid}); $links{$scrid} = "$start_t;$val:$icrid"; # New episode already added $count++; } } else { # New series added to timer $links{$scrid} = "$start_t;$icrid"; $count++; } } } } return $count; } # Check for new EPG entries for each series in the links file and create timers. # FIXME This algorithm fails if an item is part of two series and both are being # recorded (how can that happen?). sub check_links { my $count = 0; foreach my $prog (@epg) { my $scrid = $prog->{scrid}; my $icrid = $prog->{icrid}; next if $scrid eq 'NULL'; if (exists $links{$scrid}) { if ($links{$scrid} !~ /$icrid/) { # Have we already recorded this programme on a diferent series? my $title = get_title($prog->{sid},$prog->{st}); my $done = 0; for (values %links) { if (/$icrid/) { print STDOUT "Item $icrid ($title) already recorded!\n"; $done = 1; } } if (!$done) { my $fstart = strftime("%Y-%m-%d:%H%M", localtime($prog->{st}-$CONFIG{START_PADDING}*60)); my $fend = strftime("%H%M", localtime($prog->{et}+$CONFIG{STOP_PADDING}*60)); my $flag = 1 + 4*$CONFIG{VPS}; print STDOUT "New timer set for $scrid, \"$title\" at $fstart\n"; set_timer ("$flag:$prog->{sid}:$fstart:$fend:$CONFIG{PRIORITY}:$CONFIG{LIFETIME}:$title:"); my ($last_t,$val) = split(';', $links{$scrid}); $links{$scrid} = "$prog->{st};$val:$icrid"; $count++; } } } } return $count; } # Review the timer list for clashes. FIXME: What happens when there are multiple cards? sub check_timer_clashes { my ($ii, $jj); my (@tstart, @tstop); print STDOUT "Checking for timer clashes\n"; for ($ii = 0 ; $ii < @timers ; $ii++) { push @tstart, $timers[$ii]->{tstart}; push @tstop, $timers[$ii]->{tstop}; for ($jj = 0 ; $jj < $ii ; $jj++) { if (is_clash($ii, $jj)) { # What to do?? For now just report the collision my $start1 = $tstart[$ii]+$CONFIG{START_PADDING}*60; my $start2 = $tstart[$jj]+$CONFIG{START_PADDING}*60; my $ttl1 = get_title($chans[$timers[$ii]->{chan}]->{id}, $start1); my $ttl2 = get_title($chans[$timers[$jj]->{chan}]->{id}, $start2); my $crid1 = get_crid_for_timer($chans[$timers[$ii]->{chan}]->{id}, $start1); my $crid2 = get_crid_for_timer($chans[$timers[$jj]->{chan}]->{id}, $start2); my $when = localtime ($timers[$ii]->{tstart}); print STDOUT "Collision! $when\n$ttl1 ($crid1) <-> $ttl2 ($crid2)\n"; list_alternatives($crid1, $ttl1, $start1); list_alternatives($crid2, $ttl2, $start2); } } } sub is_clash { my ($i, $j) = @_; if (($tstart[$i] >= $tstart[$j] && $tstart[$i] < $tstop[$j]) || ($tstart[$j] >= $tstart[$i] && $tstart[$j] < $tstop[$i])) { # Timers collide in time. Check if the # Timers are on the same transponder my $t1 = $chans[$timers[$i]->{chan}] -> {transponder}; my $t2 = $chans[$timers[$j]->{chan}] -> {transponder}; if ($t1 eq $t2) { # print STDOUT "Multiple recordings on same transponder - OK\n"; return 0; } else { return 1; } } return 0; } sub list_alternatives { my ($crid, $ttl, $start) = @_; if (my @prog = events_for_crid($crid)) { print STDOUT "Alternatives for $ttl:\n"; foreach my $evt (@prog) { if ($evt->{st} != $start) { my $w = localtime($evt->{st}); my ($c) = channel_name_from_id($evt->{sid}) =~ /(.*?);/; print STDOUT "\t$c - $w\n"; } } } } } # Look for split events (eg a film with a news summary in the middle), and ensure # that all parts of the event are being recorded. # A split event has a # in the item CRID, for example /19778232#1 # FIXME this will likely fail if a split event is repeated or is part of a series. sub check_split_recordings { my $count = 0; my @splits; print STDOUT "Checking for split recordings\n"; foreach my $timer (@timers) { my $channelid = $chans[$timer->{chan}] -> {id}; my $start_t = $timer->{tstart}; my $stop_t = $timer->{tstop}; foreach my $prog (@epg) { if (($prog->{sid} eq $channelid) && ($start_t <= $prog->{st}) && ($stop_t >= $prog->{et})) { if ($prog->{icrid} =~ /\#/) { push @splits, "$prog->{icrid}:$prog->{st}:$channelid"; } } } } foreach $split (@splits) { my ($tcrid, $tstart, $tchan) = split(':', $split); foreach my $prog (@epg) { if (($prog->{icrid} eq $tcrid) && ($tchan eq $prog->{sid}) && ($prog->{st} != $tstart)) { my $got = 0; foreach $split2 (@splits) { my ($tcrid2, $tstart2, $tchan2) = split(':', $split2); if (($prog->{icrid} eq $tcrid2) && ($tchan2 eq $prog->{sid}) && ($prog->{st} == $tstart2)) { $got = 1; } } if (!$got) { print STDOUT "Unset timer for split event $tcrid found!\n"; my $fstart = strftime("%Y-%m-%d:%H%M", localtime($prog->{st}-$CONFIG{START_PADDING}*60)); my $fend = strftime("%H%M", localtime($prog->{et}+$CONFIG{STOP_PADDING}*60)); my $title = get_title($prog->{sid},$prog->{st}); my $flag = 1 + 4*$CONFIG{VPS}; print STDOUT "New timer set for \"$title\" at $fstart\n"; set_timer ("$flag:$prog->{sid}:$fstart:$fend:$CONFIG{PRIORITY}:$CONFIG{LIFETIME}:$title:"); $count++; } } } } return $count; } # Scan the timers list for events which have changed since the timer was # set. For now we just report the error. TODO: if the event has moved maybe # we should try to find the new one? sub check_changed_events { print STDOUT "Checking for changed timer events\n"; foreach my $timer (@timers) { my $channelid = $chans[$timer->{chan}] -> {id}; my $start_t = $timer->{tstart}; my $timer_title = $timer ->{title}; my $event_title = get_title ($channelid, $start_t); if ($timer_title ne $event_title) { print STDOUT "Event: $event_title <=> Timer: $timer_title\n"; } } } # Get a list of all occurrences of an event CRID. Return an array of EPG objects. sub events_for_crid { my ($crid) = @_; my @elist; foreach my $prog (@epg) { if ($prog->{icrid} eq $crid) { push (@elist, $prog); } } return @elist; } # Get the name of a channel given its ID. sub channel_name_from_id { my ($id) = @_; foreach my $chan (@chans) { if ($chan->{id} eq $id) { return $chan->{name}; } } return ('Unknown'); } # Read the timers from VDR sub get_timers { Send("LSTT"); while () { chomp; if (/^\d*([- ])\d* (.*)/) { my ($flag,$chan,$day,$start,$stop,$prio,$life,$title) = split(':', $2); my ($yy,$mm,$dd) = split('-', $day); my $starth = $start / 100; my $startm = $start % 100; my $stoph = $stop / 100; my $stopm = $stop % 100; my $tstart = mktime(0, $startm, $starth, $dd, $mm-1, $yy-1900, 0, 0, -1); if ($stoph < $starth) { # prog over midnight $dd++; } my $tstop = mktime(0, $stopm, $stoph, $dd, $mm-1, $yy-1900, 0, 0, -1); push (@timers, { flag => $flag, chan => $chan, tstart => $tstart, tstop => $tstop, title => $title }); } last if $1 ne "-"; } print STDOUT "Read ",scalar(@timers)," Timers\n"; } # Read the EPG from VDR (TVAnytime events only) sub get_epg { my ($sid,$id,$st,$et,$d); my $icrid = "NULL"; my $scrid = "NULL"; Send("LSTE"); while () { chomp; if (my ($flag,$type,$data) = /^215(.)(.) *(.*)$/) { if ($type eq 'C') { ($sid) = ($data =~ /^(.*?) /); } elsif ($type eq 'Y') { $icrid = $data; } elsif ($type eq 'Z') { $scrid = $data; } elsif ($type eq 'E') { ($id,$st,$d) = split(" ",$data); $et = $st + $d; } elsif ($type eq 'e') { if ($icrid ne "NULL") { push (@epg, { sid => $sid, st => $st, et => $et, id => $id, icrid => $icrid, scrid => $scrid }); $icrid = "NULL"; } $scrid = "NULL"; } last if $flag ne "-"; } } print STDOUT "Read ",scalar(@epg)," EPG lines\n"; } # Read the channels list from VDR. Channel numbers are 1-based not 0-based... sub get_channels { push (@chans, { id => '0-0-0-0', transponder => '0-0', name => 'Dummy' }); Send("LSTC"); while () { chomp; if (/^\d*([- ])\d* (.*)/) { my ($name,$f,$p,$t,$d4,$d5,$d6,$d7,$d8,$id1,$id2,$id3) = split(':', $2); push (@chans, { id => join('-', $t, $id2, $id3, $id1), transponder => join('-', $t, $f), name => $name }); } last if $1 ne "-"; } print STDOUT "Read ",scalar(@chans)," Channels\n"; } # Read the links file. sub get_links { if (open (LINKS,'<',"$CONFIG{LINKSDIR}/links.data")) { while () { chomp; my ($scrid,$icrids) = split(','); $links{$scrid} = $icrids; } close (LINKS); print STDOUT "Read ",scalar(keys(%links))," Links\n"; } else { print STDOUT "No links file found\n"; } } # Save the links file sub put_links { print STDOUT "Rewriting Links file\n"; open (LINKS,'>',"$CONFIG{LINKSDIR}/links.data.new") or die "Cannot open new links file\n"; while (my($link,$val) = each %links){ if ($val ne '') { my ($last_t,$entries) = split(';',$val); if (($last_t + ($CONFIG{SERIES_TIMEOUT} * 86640)) > time()) { print LINKS $link,',',$val,"\n"; } else { print STDOUT "Expiring series $link\n"; } } } close (LINKS); if (-e "$CONFIG{LINKSDIR}/links.data.old") { unlink "$CONFIG{LINKSDIR}/links.data.old"; } if (-e "$CONFIG{LINKSDIR}/links.data") { rename "$CONFIG{LINKSDIR}/links.data", "$CONFIG{LINKSDIR}/links.data.old"; } rename "$CONFIG{LINKSDIR}/links.data.new", "$CONFIG{LINKSDIR}/links.data"; print STDOUT "Wrote ",scalar(keys(%links))," Links\n"; } # Display the links sub show_links { while (my($link,$val) = each %links){ if ($val ne '') { my ($last_t,$entries) = split(';',$val); my $last = strftime("%Y-%m-%d:%H%M", localtime($last_t)); print STDOUT "$link\t$last\n"; } } } # Get the program title from EPG, given channel and start time. sub get_title { my $title = "TITLE"; my ($chan,$time) = @_; Send ("LSTE $chan at $time"); while () { chomp; if (/^215-T (.*)/) { $title = $1; } last if substr($_, 3, 1) ne "-"; } $title =~ s/:/|/g; return ($title); } # Get the item CRID for a timer entry. sub get_crid_for_timer { my $crid = "CRID"; my ($chan,$time) = @_; Send ("LSTE $chan at $time"); while () { chomp; if (/^215-Y (.*)/) { $crid = $1; } last if substr($_, 3, 1) ne "-"; } $crid =~ s/:/|/g; return ($crid); } # Set a new timer sub set_timer { my $string = shift; Send ("NEWT $string"); Receive(); } # SVDRP handling sub open_SVDRP { $SIG{ALRM} = sub { Error("timeout"); }; alarm($Timeout); $iaddr = inet_aton($CONFIG{VDR_HOST}) || Error("no host: $Dest"); $paddr = sockaddr_in($CONFIG{VDR_PORT}, $iaddr); $proto = getprotobyname('tcp'); socket(SOCK, PF_INET, SOCK_STREAM, $proto) || Error("socket: $!"); connect(SOCK, $paddr) || Error("connect: $!"); select(SOCK); $| = 1; Receive(); } sub close_SVDRP { print STDOUT "Closing connection\n"; Send("quit"); Receive(); close(SOCK) || Error("close: $!"); } sub Send { my $cmd = shift || Error("no command to send"); print SOCK "$cmd\r\n"; } sub Receive { while () { # print STDOUT $_; last if substr($_, 3, 1) ne "-"; } } sub Error { print STDERR "@_\n"; close(SOCK); exit 0; }