Project

General

Profile

RE: Sky Auto Channel Numbering ยป autobouquets.pl

autobouquets inc. channel tagging - Kelvin Middleton, 2015-07-13 10:51

 
1
#!/usr/bin/perl
2
# Perl version of AutoBouquets E2 28.2E by LraiZer for www.ukcvs.org
3
# modified to integrate with TVHeadend
4
# by Jonathan Kempson - [email protected]
5

    
6
use 5.010;
7
use strict;
8
use warnings;
9

    
10
use Data::Dumper;
11
use LWP::Simple qw(get);
12
use JSON;
13
use URI::Escape;
14
use List::Util qw(first);
15
use LWP::Simple qw($ua head);
16

    
17
# Required regional data/region here (use SD version if HD channel verification required)
18
my $data_sd   = "4097";
19
my $region_sd = "07";
20

    
21
# HD data/region here to be verified. Set to "" to skip HD channel verification
22
my $data_hd   = "";
23
my $region_hd = "";
24

    
25
# TVH configuration
26
my $tag_channels  = 1;
27
my $tvh_user      = "someuser";
28
my $tvh_pass      = "somepassword";
29
my $tvh_ip        = "someip";
30
my $tvh_proto     = "http";
31
my $tvh_port      = "9981";
32
my $tvh_tag       = "TV channels";
33
my $icons_enabled = 1;
34

    
35
# Change 'on' => 1 to 'on' => 0 if you don't want that channel group
36
my %chan_tags = (
37
    'Entertainment'         => { 'num' => 101,  'on' => 1 },
38
    'Lifestyle and Culture' => { 'num' => 240,  'on' => 1 },
39
    'Movies'                => { 'num' => 301,  'on' => 1 },
40
    'Music'                 => { 'num' => 350,  'on' => 1 },
41
    'Sports'                => { 'num' => 401,  'on' => 1 },
42
    'News'                  => { 'num' => 501,  'on' => 1 },
43
    'Documentaries'         => { 'num' => 520,  'on' => 1 },
44
    'Religious'             => { 'num' => 580,  'on' => 1 },
45
    'Kids'                  => { 'num' => 601,  'on' => 1 },
46
    'Shopping'              => { 'num' => 640,  'on' => 1 },
47
    'Sky Box Office'        => { 'num' => 700,  'on' => 1 },
48
    'International'         => { 'num' => 780,  'on' => 1 },
49
    'Gaming and Dating'     => { 'num' => 861,  'on' => 1 },
50
    'Specialist'            => { 'num' => 881,  'on' => 1 },
51
    'Adult'                 => { 'num' => 900,  'on' => 1 },
52
    'Other'                 => { 'num' => 950,  'on' => 1 },
53
    'Radio'                 => { 'num' => 3101, 'on' => 0 },
54
);
55

    
56
# Manual add channel, number:SID, for channels that may not be in your bouquet		
57
my @sid_adds  = ("131:10155");
58

    
59
say "Started";
60
my $tvh_url = $tvh_proto . "\:\/\/" . $tvh_user . "\:" . $tvh_pass . "\@" . $tvh_ip . "\:" . $tvh_port;
61

    
62
say "Get Channel Tags...";
63
my %chan_ids = tvhTagHash( $tvh_tag, \%chan_tags );
64
say "Done";
65

    
66
#my $f_dvb = "/tmp/abm_dvb.txt";
67
#my @dvb   = readFile($f_dvb);
68

    
69
# Run dvbsnoop
70
my @dvb = `dvbsnoop -nph -n 500 0x11`;
71
if ( scalar(@dvb) <= 1 ) { die "No data received. You must have root access?"; }
72

    
73
# Get SDT
74
say "Get SDT...";
75
my @sdt = serviceDescriptionTable( \@dvb );
76
say "Done";
77

    
78

    
79
# Get service lists
80
say "Get services";
81
my @services_sd    = populateServices($data_sd, $region_sd, @dvb, @sdt);
82
@services_sd = sort( uniqArray(@services_sd) );
83
say "Done";
84

    
85
my @merged_services;
86

    
87
if (($data_hd ne "") && ($region_hd ne "")) {
88
	my @services_hd    = populateServices($data_hd, $region_hd, @dvb, @sdt);
89
	@services_hd = sort( uniqArray(@services_hd) );
90
	my @services_hd_checked;
91
	$ua->timeout(10);
92
	my @tvh_services = tvhQuery("mpegts/service/grid?dir=ASC&limit=100000&sort=sid&start=0");
93
	foreach my $s_hd (@services_hd) {
94
		my $dupe = 0;
95
		foreach my $s_sd (@services_sd) {
96
			if ($s_hd eq $s_sd) { $dupe = 1; }
97
		}
98

    
99
		if ($dupe == 0) { 
100
			my @conv = split( " ", $s_hd );
101
			my $serv_type = tvhServiceType( hex( $conv[2] ), \@tvh_services );
102
		
103
			# dvb_servicetype seems to be HD channels? Use to ignore SD channels returned in HD bouquet.
104
			if ($serv_type == 25) {	    
105
				my $chan_num  = hex( $conv[0] );
106
				my $chan_uuid = tvhServiceID( hex( $conv[2] ), \@tvh_services );
107
				my $chan_name = lookupService( "$conv[2]", \@sdt );
108
				my $url = "$tvh_url/stream/service/$chan_uuid";
109
				print "Testing channel $chan_num $chan_name...";
110
				if (head($url)) { 
111
					push (@services_hd_checked, $s_hd);
112
					print "ok\n"; 
113
				} else { 
114
					print "fail\n";
115
				}
116
			}
117
		}
118

    
119
	}
120
	foreach my $s_sd (@services_sd) {
121
		my @conv = split( " ", $s_sd );
122
		my $chan_num  = hex( $conv[0] );
123
		my $replace = $s_sd;
124
		foreach my $s_hd (@services_hd_checked) {
125
				my @conv_hd = split( " ", $s_hd );
126
				my $chan_num_hd  = hex( $conv_hd[0] );
127
				if ($chan_num eq $chan_num_hd) { $replace = $s_hd; }
128
		}
129
		push (@merged_services, $replace);
130
	}
131
} else {
132
	@merged_services = @services_sd;
133
}
134

    
135
foreach my $add (@sid_adds) {
136
	my @arr = split(":",$add);
137
	$arr[0] = sprintf( "%04x", $arr[0] );
138
	$arr[1] = sprintf( "%04x", $arr[1] );
139
	push @merged_services, "$arr[0] 0000 $arr[1] 0000";
140
}
141
@merged_services = sort( uniqArray(@merged_services) );
142

    
143
# Populate channels in TVH
144
say "Populate channels in TVH...";
145
updateTVH( \@merged_services, $tvh_tag, $tag_channels );
146
say "Done";
147

    
148
exit;
149

    
150
################################################################################
151

    
152
sub populateServices {
153
    my ( $data, $region, $dvb, $sdt ) = @_;
154
	my (@svcs, @sections);
155
	
156
	$region    = sprintf( "%02x", $region );
157
		
158
	say "Process regional services first...";
159
	processServices( $data, $region, \@dvb, \@svcs, \@sdt );
160
	say "Done";
161

    
162
	say "Everything else...";
163

    
164
	if ( ( $region ne "33" ) && ( $region ne "32" ) ) {
165

    
166
		# Use different base services if region is Irish
167
		processServices( "4104", "21", \@dvb, \@svcs, \@sdt );
168
	}
169
	else {
170
		processServices( "4101", "01", \@dvb, \@svcs, \@sdt );
171
	}
172
	
173
	say "Done";
174
	return @svcs;
175
}
176

    
177
sub updateTVH {
178
    my ( $services, $tvh_tag, $tag_channels ) = @_;
179
    my @tvh_tags = tvhQuery("channeltag/list?enum=true");
180

    
181
    my $tag_id = tvhTagID($tvh_tag);
182

    
183
    # Get all services from tvh
184
    my @tvh_services = tvhQuery("mpegts/service/grid?dir=ASC&limit=100000&sort=sid&start=0");
185
    my @tvh_channels = tvhQuery("channel/grid?dir=ASC&limit=100000&sort=number&start=0");
186

    
187
    my @chan_list;
188
    my @sid_dupes;
189
		
190
    foreach my $line (@$services) {
191
		my @conv = split( " ", $line );
192

    
193
        my $chan_num  = hex( $conv[0] );
194
        my $chan_name = lookupService( "$conv[2]", \@sdt );
195
        my $chan_epg  = hex( $conv[1] );
196
        my $chan_logo = "http://tv.sky.com/logo/0/0/skychb" . $chan_epg . ".png";
197

    
198
						
199
        my ( @j_sid, @j_tag );
200
        my $chan_uuid = tvhServiceID( hex( $conv[2] ), \@tvh_services );
201
        push @j_sid, $chan_uuid;
202

    
203
        # Duplicate channels with different channel numbers seem to cause problems with EPG in XBMC (perhaps others)?
204
        # EPG sometimes isn't imported for channels which are assigned multiple channel numbers.
205
        # Skip channel if it has been seen earlier.
206
        my $skip = 0;
207
        if ( first { $_ eq $chan_uuid } @sid_dupes ) {
208
            $skip = 1;
209
        }
210
        push @sid_dupes, $chan_uuid;
211

    
212
        # Ignore useless channels 65535
213
        if ( $chan_num == 65535 ) { $skip = 1; }
214

    
215
        push @j_tag, $tag_id;
216

    
217
        # Add tag info if enabled
218
        if ( $tag_channels == 1 ) {
219
            if ( $chan_tags{ chanToTag($chan_num) }->{on} == 1 ) {
220
                push @j_tag, $chan_ids{ chanToTag($chan_num) };
221
            }
222
            else {
223
                $skip = 1;
224
            }
225
        }
226

    
227
        # Create hash of channel data to be added
228
        if ( $skip == 0 ) {
229
            my %chan_hash = (
230
                'name'         => $chan_name,
231
                'number'       => $chan_num,
232
                'services'     => \@j_sid,
233
                'tags'         => \@j_tag,
234
                'dvr_pre_time' => '0',
235
                'dvr_pst_time' => '0'
236
            );
237
            
238
            if ($icons_enabled !=0) {
239
            	$chan_hash{'icon'} = $chan_logo;
240
            }
241

    
242
            push @chan_list, $chan_num;
243
            tvhUpdateChannel( \@tvh_channels, $tag_id, \%chan_hash );
244
        }
245
    }
246

    
247
    # Remove orphan channels
248
    foreach my $chan (@tvh_channels) {
249
        my $tag_check = 0;
250
        foreach my $tag ( @{ $chan->{tags} } ) {
251
            if ( $tag eq $tag_id ) { $tag_check = 1; }
252
        }
253

    
254
        my $found = 0;
255
        for my $line (@chan_list) {
256
            if ( $chan->{number} eq $line ) { $found = 1; }
257
        }
258

    
259
        if ( ( $found == 0 ) && ( $tag_check == 1 ) ) {
260
            say "Removing orphaned channel $chan->{number} $chan->{name}";
261
            get( $tvh_url . "/api/idnode/delete?uuid=" . uri_escape( $chan->{uuid} ) );
262
        }
263
    }
264

    
265
}
266

    
267
sub tvhTagHash {
268
    my ( $tvh_tag, $chan_tags ) = @_;
269
    my @tvh_tags;
270
    push @tvh_tags, $tvh_tag;
271

    
272
    foreach my $ch ( sort keys %chan_tags ) {
273
        push @tvh_tags, $ch;
274
    }
275

    
276
    my %chan_ids;
277

    
278
    foreach my $ch (@tvh_tags) {
279
        my $tag_id = tvhTagID($ch);
280
        if ( !$tag_id ) {
281
            $tag_id = tvhCreateTag($ch);
282
        }
283
        $chan_ids{$ch} = $tag_id;
284
    }
285
    return %chan_ids;
286
}
287

    
288
sub chanToTag {
289
    my ( $chan, $chan_tags ) = @_;
290
    my $tag = "";
291
    foreach my $n ( sort { $chan_tags{$b}->{num} <=> $chan_tags{$a}->{num} } keys %chan_tags ) {
292
        if ( $chan >= $chan_tags{$n}->{num} ) {
293
            $tag = $n;
294
            last;
295
        }
296
    }
297
    return $tag;
298
}
299

    
300
sub tvhTagID {
301
    my ($tvh_tag) = @_;
302
    my @tvh_tags = tvhQuery("channeltag/list?enum=true");
303
    my $tag_id;
304

    
305
    foreach my $tag (@tvh_tags) {
306
        if ( $tag->{val} eq $tvh_tag ) { $tag_id = $tag->{key}; }
307
    }
308

    
309
    return $tag_id;
310
}
311

    
312
sub tvhCreateTag {
313
    my ($tag_name) = @_;
314
    my $post       = "{\"enabled\":true,\"name\":\"" . $tag_name . "\",\"internal\":false,\"icon\":\"\",\"titled_icon\":false,\"comment\":\"\"}";
315
	say "Adding channel tag $tag_name";
316
    get( $tvh_url."/api/channeltag/create?conf=".uri_escape($post)); 
317
    return tvhTagID($tag_name);
318
}
319

    
320
sub tvhUpdateChannel {
321
    my ( $tvh_channels, $tag_id, $post_json ) = @_;
322
    my $matches    = 0;
323
    my $chan_exist = "";
324

    
325
    for my $line (@$tvh_channels) {
326
        my $tag_check = 0;
327
        foreach my $tag ( @{ $line->{tags} } ) {
328
            if ( $tag eq $tag_id ) { $tag_check = 1; }
329
        }
330

    
331
        if ( $tag_check == 1 ) {
332
            if ( $line->{number} eq $post_json->{number} ) {
333
                $matches    = 1;
334
                $chan_exist = $line->{uuid};
335
                my $servs      = join( "", sort( @{ $line->{services} } ) );
336
                my $post_servs = join( "", sort( @{ $post_json->{services} } ) );
337

    
338
                my $tags      = join( "", sort( @{ $line->{tags} } ) );
339
                my $post_tags = join( "", sort( @{ $post_json->{tags} } ) );
340
                
341
                if ( $line->{name} ne $post_json->{name} ) { $matches = 0 }
342
                                
343
                if ($icons_enabled == 1) {
344
                	if ( $line->{icon} ne $post_json->{icon} ) { $matches = 0 }
345
                }
346
                
347
                if ( $servs        ne $post_servs )        { $matches = 0 }
348
                if ( $tags         ne $post_tags )         { $matches = 0 }
349
                last;
350
            }
351
        }
352
    }
353

    
354
    if ( $matches == 0 ) {
355
        if ( $chan_exist ne "" ) {
356
            say "Deleting old channel $post_json->{number}";
357
            get( $tvh_url . "/api/idnode/delete?uuid=" . uri_escape($chan_exist) );
358
        }
359
        say "Adding channel $post_json->{number} $post_json->{name}";
360
        get( $tvh_url . "/api/channel/create?conf=" . uri_escape( encode_json($post_json) ) );
361
    }
362
}
363

    
364
sub tvhQuery {
365
    my ($query) = @_;
366
    my $url    = $tvh_url . '/api/' . $query;
367
    my $return = from_json( get($url) );
368
    return @{ $return->{entries} };
369
}
370

    
371
sub tvhServiceID {
372
    my ( $search, $decoded ) = @_;
373
    my $uuid = "";
374
    foreach my $line (@$decoded) {
375
        if ( $line->{sid} eq $search ) { $uuid = $line->{uuid}; last; }
376
    }
377

    
378
    return $uuid;
379
}
380

    
381
sub tvhServiceType {
382
    my ( $search, $decoded ) = @_;
383
    my $uuid = "";
384
    foreach my $line (@$decoded) {
385
        if ( $line->{sid} eq $search ) { $uuid = $line->{dvb_servicetype}; last; }
386
    }
387

    
388
    return $uuid;
389
}
390

    
391
sub uniqArray {
392
    return keys %{ { map { $_ => 1 } @_ } };
393
}
394

    
395
sub processServices {
396
    my ( $data, $region, $dvb, $svcs, $sdt ) = @_;
397
    my @sections = findSections( $data, \@dvb );
398
    return streamData( $region, \@sections, $svcs, \@sdt );
399
}
400

    
401
sub streamData {
402
    my ( $region, $sections, $conv_data, $sdt ) = @_;
403
    my ( @data, $hex, $ts, $code, $pcode );
404

    
405
    # If there is already data in services array then turn on duplicate checking
406
    my $check_dupes;
407
    if ( scalar(@$conv_data) > 0 ) {
408
        $check_dupes = 1;
409
    }
410
    else {
411
        $check_dupes = 0;
412
    }
413

    
414
    foreach my $line (@$sections) {
415

    
416
        if ( $line =~ /^    Transport_stream_ID/ ) {
417
            ($ts) = $line =~ /\(.*x([^\)]+)/;
418
            $ts    = trim($ts);
419
            $pcode = -1;
420
            push( @data, $hex );
421
            $hex = $ts;
422
        }
423

    
424
        if ( $line =~ /^                 00/ ) {
425
            $line =~ s /(.{74}).*/${1}/s;
426
            $line =~ s/\h+/ /g;
427
            $line = trim($line);
428

    
429
            $code = hex( substr( $line, 0, 4 ) );
430
            $line = substr( $line, 5 );
431

    
432
            if ( $code <= $pcode ) {
433
                push( @data, $hex );
434
                $hex = $ts;
435
            }
436
            $pcode = $code;
437

    
438
            $hex = $hex . $line;
439
        }
440
    }
441
    push( @data, $hex );
442
    shift @data;
443

    
444
    filterServices( \@data, $region );
445

    
446
    for my $line (@data) {
447
        my $count      = 1;
448
        my $number     = 7;
449
        my $serv_count = length($line) / 27;
450
        my $code       = substr( $line, 0, 4 );
451
        while ( $count < $serv_count ) {
452
            my @s = split( " ", substr( $line, $number, 27 ) );
453
            my $out = $s[6] . $s[7] . " " . $s[4] . $s[5] . " " . $s[1] . $s[2] . " " . $code;
454

    
455
            my $found = 0;
456
            if ( $check_dupes eq 1 ) {
457
                my $d = substr( $out, 0, 4 );
458
                if ( grep( /^$d/i, @$conv_data ) ) { $found = 1; }
459
            }
460
            if ( $found == 0 ) {
461
                push( @$conv_data, $out );
462
            }
463
            
464
            $number = $number + 27;
465
            $count++;
466
        }
467
    }
468
    return @$conv_data;
469
}
470

    
471
sub filterServices {
472
    my ( $data, $region ) = @_;
473
    my $i = 0;
474
    do {
475
        if ( ( substr( @$data[$i], 8, 2 ) ne $region ) && ( substr( @$data[$i], 5, 2 ) ne "ff" ) ) {
476
            splice @$data, $i, 1;
477
        }
478
        else {
479
            $i++;
480
        }
481
    } until ( $i == scalar(@$data) );
482

    
483
    return @$data;
484
}
485

    
486
sub lookupService {
487
    my $search = shift;
488
    my $sdt    = shift;
489
    foreach my $line (@$sdt) {
490
        if ( $line =~ /^$search/ ) {
491
            $search = substr( $line, 10 );
492
            last;
493
        }
494
    }
495
    return $search;
496
}
497

    
498
sub findSections {
499

    
500
    # Extract relevant sections from dvbsnoop array based on $data bouquet info
501
    my ( $data, $dvb ) = @_;
502

    
503
    my @sections_ref = sectionsRef( $data, \@dvb );
504
    my @sections;
505

    
506
    my $sec_first = ( @$dvb[ $sections_ref[0] + 18 ] =~ /Section_number: (.*) \(/ )[0];
507
    my $sec_last  = ( @$dvb[ $sections_ref[0] + 19 ] =~ /Section_number: (.*) \(/ )[0];
508
    my $sec_cycle;
509

    
510
    # Set loop stop point to prevent repeat data
511
    if ( $sec_first == 0 ) {
512
        $sec_cycle = $sec_last;
513
    }
514
    else {
515
        $sec_cycle = $sec_first - 1;
516
    }
517

    
518
    foreach my $line (@sections_ref) {
519
        my $strt = $line;
520
        do {
521
            $strt++;
522
            push( @sections, @$dvb[$strt] );
523
        } while !( @$dvb[$strt] =~ /CRC/ );
524

    
525
        # sec_num = current sequence position
526
        my $sec_num = ( @$dvb[ $line + 18 ] =~ /Section_number: (.*) \(/ )[0];
527

    
528
        # If position in sequence matches stop point then exit
529
        if ( $sec_num == $sec_cycle ) { last; }
530
    }
531
    return @sections;
532
}
533

    
534
sub sectionsRef {
535

    
536
    # Extract positions of Bouquet_ID for $data in dvbsnoop array
537
    my ( $data, $dvb ) = @_;
538
    my $cnt = 0;
539
    my @sections;
540
    foreach my $line (@$dvb) {
541
        if ( $line =~ /Bouquet_ID: $data/ ) {
542
            push( @sections, ( $cnt - 14 ) . "\n" );
543
        }
544
        $cnt++;
545
    }
546
    return @sections;
547
}
548

    
549
sub serviceDescriptionTable {
550

    
551
    # Finds all service data from the dvbsnoop array
552
    my $dvb = shift;
553

    
554
    # Match any line in this array
555
    my $match = join( "|",
556
        qr/^Transport_Stream_ID:/,
557
        qr/^    Service_id:/,
558
        qr/^    Free_CA_mode:/,
559
        qr/^            service_provider_name:/,
560
        qr/^            Service_name:/,
561
        qr/^                 0000:  /,
562
    );
563

    
564
    # Do not include matching line if a second check matches any values here (not sure why?)
565
    my $no_match = join( "|", qr/^                 0000:  00/, qr/^                 0000:  ff/, qr/^                 0000:  1d/, );
566

    
567
    my @services;
568
    foreach my $line (@$dvb) {
569
        if ( ( $line =~ $match ) && !( $line =~ $no_match ) ) {
570
            push( @services, $line );
571
        }
572
    }
573
    return processSDT( \@services );
574
}
575

    
576
sub processSDT {
577
    my $services = shift;
578
    my @return;
579
    my ( $tsid, $sid, $sn, $ca, $spn );
580

    
581
    foreach my $line (@$services) {
582
        if ( $line =~ /Transport_Stream_ID/ ) {
583
            $tsid = get_hex($line);
584
        }
585

    
586
        if ( $line =~ /Service_id:/ ) {
587
            $sid = get_hex($line);
588
            $sn  = "";
589
        }
590

    
591
        if ( $line =~ /Free_CA_mode:/ ) {
592
            $ca = get_hex($line);
593
            if   ( $ca == 0 ) { $ca = "FTA"; }
594
            else              { $ca = "NDS"; }
595
        }
596
        if ( $line =~ /service_provider_name:/ ) {
597
            $spn = get_txt($line);
598
        }
599

    
600
        if ( $line =~ /Service_name:/ ) {
601
            $sn = get_txt($line);
602

    
603
            #push @return, "$sid:$tsid#$ca:$spn:$sn";
604
            push @return, "$sid:$tsid:$sn";
605
        }
606

    
607
        if ( $line =~ /0000:/ ) {
608
            if ( length($sn) == 0 ) {
609

    
610
                # Need a regexp here, this isn't nice :S
611
                $sn = trim( substr( $line, 70 ) );
612

    
613
                # push @return, "$sid:$tsid#$ca:BSkyB:$sn";
614
                push @return, "$sid:$tsid:$sn";
615
            }
616
        }
617
    }
618

    
619
    @return = sort( uniqArray(@return) );
620
    return @return;
621
}
622

    
623
sub get_txt {
624
    if ( $_[0] =~ /"(.+?)"/ ) { return $1; }
625
}
626

    
627
sub get_hex {
628
    my $return;
629
    ($return) = $_[0] =~ /\(.*x([^\)]+)/;
630
    $return = trim($return);
631
    return $return;
632
}
633

    
634
sub trim {
635
    ( my $s = $_[0] ) =~ s/^\s+|\s+$//g;
636
    return $s;
637
}
638

    
639
sub readFile {
640
    open( FH, $_[0] );
641
    my @buf = <FH>;
642
    close(FH);
643
    return @buf;
644
}
    (1-1/1)