/usr/bin/tv_merge is in xmltv-util 0.5.67-0.1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | #!/usr/bin/perl -w
eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
if 0; # not running under some shell
=pod
=head1 NAME
tv_merge - Merge (combine) two XMLTV files.
=head1 SYNOPSIS
tv_merge -i FILE -m FILE -o FILE
=head1 DESCRIPTION
Read XMLTV listings from two files and merge them together.
Unlike tv_cat (which just joins files) this will update (add/replace/delete)
the origin XMLTV file with channels and programmes contained in the second
file.
It works with multiple channels, and will insert any new programmes
and delete any overlapping programmes.
B<IMPORTANT>
The input files must be pre-sorted into datetime within channel order
by using the "--by-channel" option to tv_sort
e.g. tv_sort --by-channel --output FILE FILE
All programmes must have start and stop times.
(Note: Programmes in the merged-in file I<replace> any in the master file,
i.e. data are not updated I<within> programmes)
This program uses XML::TreePP which doesn't write <DOCTYPE> definitions in the
output file. If you need to add a suitable <DOCTYPE> tag then use the -t parameter.
e.g. tv_merge -i FILE -m FILE -o FILE -t
=head1 EXAMPLE
Use C<tv_merge -i master.xml -m newadditions.xml -o newmaster.xml> to
merge all channels/programmes in F<newadditions.xml>.
=head1 SEE ALSO
L<xmltv(5)>
=head1 AUTHOR
Copyright Geoff Westcott, February 2013.
This code is distributed under the GNU General Public License v2 (GPLv2) .
=cut
#
# Inspired by xmltvmerger.py ( http://niels.dybdahl.dk/xmltvdk/index.php/Xmltvmerger.py ) with bug fixes and enhancements.
#
# IMPORTANT: the input files must be pre-sorted into datetime within channel order by using the "--by-channel" option to tv_sort
# e.g. tv_sort --by-channel --output FILE FILE
#
# Help: tv_merge -h
#
my $_version = '$Id: tv_merge,v 1.2 2015/07/12 00:46:37 knowledgejunkie Exp $';
use strict;
use XML::TreePP; # http://search.cpan.org/~kawasaki/XML-TreePP-0.41/lib/XML/TreePP.pm
use Date::Parse;
use POSIX qw(strftime);
use Getopt::Std;
use Data::Dumper;
# Process command line arguments
my %opts = (); # hash to store input args
getopts("dDhi:o:m:qt",\%opts); # load the args into %opts
my $input_file = ($opts{'i'}) ? $opts{'i'} : $ARGV[0]; # main file
my $merge_file = ($opts{'m'}) ? $opts{'m'} : $ARGV[1]; # file to merge in
my $outfile = ($opts{'o'}) ? $opts{'o'} : "merged.xml";
my $debug = ($opts{'d'}) ? $opts{'d'} : ""; # print out debugging when set to true (1)
my $debugmore = ($opts{'D'}) ? $opts{'D'} : ""; # print out debugging when set to true (1)
my $quiet_mode = ($opts{'q'}) ? $opts{'q'} : "";
my $doctype = ($opts{'t'}) ? $opts{'t'} : "";
if ((!%opts && ! -r $ARGV[0]) || $opts{'h'}) {
# Print usage message
usage();
}
# Parse the XMLTV file
my $tpp = XML::TreePP->new();
$tpp->set( force_array => [ 'channel', 'programme' ] ); # force array ref for some fields
$tpp->set( indent => 2 );
$tpp->set( first_out => [ 'channel', 'programme', 'title', 'sub-title', 'desc', 'credits', 'date', 'category', 'language', 'orig-language', 'length', 'icon', 'url', 'country', 'episode-num', 'video', 'audio', 'previously-shown', 'premiere', 'last-chance', 'new', 'subtitles', 'rating', 'star-rating', 'review' ] );
my $xmltv = $tpp->parsefile( $input_file );
if ($debugmore) { print Dumper($xmltv); }
my $xmltv_merge = $tpp->parsefile( $merge_file );
if ($debugmore) { print Dumper($xmltv_merge); }
my %xmltv_new_channels;
my @xmltv_merged_channels;
my @xmltv_merged_progs;
# Merge the channels
&merge_channels();
# Merge the programmes
&merge_programmes();
# format and output the data
&write_newxml();
###############################################################################
###############################################################################
sub merge_programmes {
# Process the XMLTV data structure
#
# Assumes all data has start and stop times (note: this isn't a requirement of XMLTV DTD)
#
# If the files contain >1 channel then they must be pre-sorted into datetime within channel
# e.g. tv_sort --by-channel --output FILE FILE
#
# Rules:
# 1. always use the new programme details (todo: bit a bit more clever with merging)
#
my $prog1 = $xmltv->{tv}->{programme};
my $prog2 = $xmltv_merge->{tv}->{programme};
if ($debugmore) { print Dumper("Incoming programmes :", $prog1 ); }
if ($debugmore) { print Dumper("Merging programmes :", $prog2 ); }
my ($p1_channel, $p1_start, $p1_stop) = ('', 0 , 0);
my ($p2_channel, $p2_start, $p2_stop) = ('', 0 , 0);
my $i = 0;
my ($thischannel, $lastchannel) = ('', '');
my $p2count = scalar (@{ $prog2 });
for my $p (@{ $prog1 }) {
$thischannel = $p->{-channel} if $thischannel eq '';
$p1_channel = $p->{-channel};
$p1_start = to_timestamp( $p->{-start} );
$p1_stop = to_timestamp( $p->{-stop} );
if ($p2_stop == -1) {
# 'new' file channel has changed
# copy any remaining 'old' records for this channel, then insert the new channel's programmes
if ($p1_channel eq $lastchannel) {
push @xmltv_merged_progs, $p; # copy the old record
next;
}
# old channel has changed = so we have finished copying old records and can now insert new channel
my $insertchannel = $p2_channel;
while ($p2_channel eq $insertchannel) {
push @xmltv_merged_progs, $prog2->[$i]; $i++;
if ($i < $p2count) {
$p2_channel = $prog2->[$i]->{-channel};
} else {
$p2_channel = ''; # no more new records, end the loop
}
}
$p2_stop = 0;
}
if ($p2_stop != 0 && $p1_start < $p2_stop && $p1_channel eq $p2_channel) {
# skip old recs until start time >= last new rec's stop time
if ($debug) { print "skippy $p1_start $p2_stop\n"; }
next;
}
if ($i < $p2count) {
$p2_channel = $prog2->[$i]->{-channel};
$p2_start = to_timestamp( $prog2->[$i]->{-start} );
$p2_stop = to_timestamp( $prog2->[$i]->{-stop} );
if ($debug) { print "$p1_start $p2_start || $p1_stop $p2_stop \n"; }
if ($p1_channel eq $p2_channel) {
$lastchannel = $thischannel; $thischannel = $p1_channel; # remember the 'current' channel
if ($p1_start == $p2_start) {
# either
# (a) progs identical - replace old with new
# (b) new prog is longer - replace old with new & delete all old progs until new stop time
# (c) new prog is shorter - insert new prog
#
push @xmltv_merged_progs, $prog2->[$i]; $i++;
next;
} elsif ($p1_start < $p2_start) {
# get here either when new schedule hasn't commenced yet, or when there is a gap in the new schedule
# keep current old prog
# unless this would cause an overlap
if ($p1_stop > $p2_start) {
# uh oh overlap - we could possibly set the stop time to equal new prog's start, but that
# is just making up schedules! On balance I think a hole is better than an overlap:
# skip this old prog
next;
}
$p2_stop = 0; # ensure we *don't* skip the next old record
} elsif ($p1_start > $p2_start) {
# get here when additional prog in new schedule that's not in the old
# insert new prog & keep current old prog
push @xmltv_merged_progs, $prog2->[$i]; $i++;
redo;
}
} else {
# channel has changed on one (or both) of the files
if ($p1_channel ne $thischannel) {
# 'old' file channel has changed
# copy any remaining 'new' records for this channel
while ($p2_channel eq $thischannel) {
push @xmltv_merged_progs, $prog2->[$i]; $i++;
if ($i < $p2count) {
$p2_channel = $prog2->[$i]->{-channel};
} else {
$p2_channel = ''; # no more new records, end the loop
}
}
$lastchannel = $thischannel; $thischannel = $p1_channel; # remember the new 'current' channel
redo;
} elsif ($p2_channel ne $thischannel) {
# 'new' file channel has changed
# copy any remaining 'old' records for this channel
# then, if this is a totally new channel, insert the new channel's programmes
if (defined $xmltv_new_channels{$p2_channel}) {
$p2_stop = -1;
redo;
} else {
$p2_stop = 0;
}
}
}
}
# copy the old record
push @xmltv_merged_progs, $p;
}
# no more old progs
# addend any remaining new progs
while ($i < $p2count) {
push @xmltv_merged_progs, $prog2->[$i];
$i++;
}
if ($debugmore) { print Dumper("Merged programmes :", @xmltv_merged_progs); }
}
sub merge_channels {
# Merge the channels in the two input files
#
# - always use the newer channel details
#
my $chan1 = $xmltv->{tv}->{channel};
my $chan2 = $xmltv_merge->{tv}->{channel};
if ($debugmore) { print Dumper("Incoming channels :", $chan1 ); }
if ($debugmore) { print Dumper("Merging channels :", $chan2 ); }
my ($c1_id);
my ($c2_id);
my %channels;
for my $c1 (@{ $chan1 }) {
$c1_id = $c1->{-id};
$channels{$c1_id} = $c1;
}
for my $c2 (@{ $chan2 }) {
$c2_id = $c2->{-id};
$xmltv_new_channels{$c2_id} = 1 if !defined $channels{$c2_id}; # array of new channels not in current file
$channels{$c2_id} = $c2;
}
foreach my $key ( keys %channels ) {
push @xmltv_merged_channels, $channels{$key};
}
if ($debugmore) { print Dumper("Merged channels :", @xmltv_merged_channels); }
}
sub to_timestamp {
my ($ts) = @_;
use DateTime::Format::Strptime;
my $format = DateTime::Format::Strptime->new(
pattern => '%Y%m%d%H%M%S %z', # "20130320060000 +0000"
time_zone => 'UTC', # (better than 'local' - e.g. handles DST)
on_error => 'croak',
);
return ($format->parse_datetime($ts))->epoch();
}
sub write_newxml {
# Write the output xml files
#
# create an xml container
my %xml = ();
$xml{'tv'}{'-generator-info-name'} = $xmltv->{tv}->{'-generator-info-name'} if $xmltv->{tv}->{'-generator-info-name'};
$xml{'tv'}{'-generator-info-url'} = $xmltv->{tv}->{'-generator-info-url'} if $xmltv->{tv}->{'-generator-info-url'};
$xml{'tv'}{'-source-info-name'} = $xmltv->{tv}->{'-source-info-name'} if $xmltv->{tv}->{'-source-info-name'};
$xml{'tv'}{'-source-info-url'} = $xmltv->{tv}->{'-source-info-url'} if $xmltv->{tv}->{'-source-info-url'};
# add the <channel> elements
$xml{'tv'}{'channel'} = \@xmltv_merged_channels;
# add the <programme> elements
$xml{'tv'}{'programme'} = \@xmltv_merged_progs;
#
my $ccount = scalar @{ $xml{'tv'}{'channel'} };
my $pcount = scalar @{ $xml{'tv'}{'programme'} };
if (!$quiet_mode) { print "Writing : $ccount channels $pcount programmes \n"; }
# write the output xml file
if ($debugmore) { print Dumper(\%xml); }
if ($doctype) {
# TreePP doesn't write a DOCTYPE - add one if the user requests it
my $xmlout = $tpp->write( \%xml, 'UTF-8' );
$xmlout =~ s/^(<\?xml.*>)/$1\n<!DOCTYPE tv SYSTEM "xmltv.dtd">\n/;
open OUT, "> $outfile" or die "Failed to open $outfile for writing";
printf OUT $xmlout;
close OUT;
} else {
$tpp->writefile( $outfile, \%xml, 'UTF-8' );
}
}
sub usage {
#
# print Usage message
#
my $filename = (split(/\//,$0))[-1];
print STDERR << "EOF";
Merge (combine) two XMLTV files
Files should be sorted into datetime within channel order
e.g. tv_sort --by-channel --output FILE FILE
Assumes all data has start and stop times
Usage: $filename [-dDh] -i input_xmltv_file -m merge_xmltv_file [-o output_file ]
-h : this (help) message
-d : print debugging messages
-D : print even more debugging messages
-q : quiet mode (no STDOUT messages)
-i file : input XMLTV file (or filename as first arg to script)
-m file : merge XMLTV file (or filename as second arg to script)
-o file : output file (default: merged.xml)
-t : add DOCTYPE to output file
example: $filename -i xmltv.xml -m xmltv_add.xml -o xmltv_new.xml
EOF
exit;
}
|