File Coverage

blib/lib/TV/Anytime.pm
Criterion Covered Total %
statement 40 42 95.2
branch n/a
condition n/a
subroutine 14 14 100.0
pod n/a
total 54 56 96.4


line stmt bran cond sub pod time code
1             package TV::Anytime;
2 1     1   11513 use strict;
  1         3  
  1         43  
3 1     1   7 use warnings;
  1         3  
  1         33  
4 1     1   2825 use DateTime;
  1         191905  
  1         37  
5 1     1   1007 use DateTime::Format::ISO8601;
  1         43210  
  1         78  
6 1     1   1128 use DateTime::Format::Duration;
  1         5661  
  1         53  
7 1     1   953 use File::Find::Rule;
  1         9066  
  1         9  
8 1     1   57 use List::Util;
  1         2  
  1         67  
9 1     1   841 use Path::Class;
  1         50329  
  1         80  
10 1     1   561 use TV::Anytime::Event;
  1         6  
  1         12  
11 1     1   825 use TV::Anytime::Genre;
  1         4  
  1         10  
12 1     1   665 use TV::Anytime::Group;
  1         3  
  1         10  
13 1     1   693 use TV::Anytime::Program;
  1         3  
  1         10  
14 1     1   1362 use TV::Anytime::Service;
  1         3  
  1         10  
15 1     1   654 use XML::LibXML;
  0            
  0            
16             use XML::LibXML::XPathContext;
17             use base 'Class::Accessor::Chained::Fast';
18             __PACKAGE__->mk_accessors(qw(directory));
19             our $VERSION = '0.31';
20              
21             sub new {
22             my $class = shift;
23             my $directory = shift;
24             die "$directory not a directory" unless -d $directory;
25              
26             die "$directory does not contain ServiceInformation.xml"
27             unless -f file($directory, "ServiceInformation.xml");
28              
29             my $self = {};
30             bless $self, $class;
31             $self->directory($directory);
32             return $self;
33             }
34              
35             sub _find_files {
36             my ($self, $id, $type) = @_;
37             my @files =
38             File::Find::Rule->file->name("*${id}_${type}.xml")->in($self->directory);
39             return sort @files;
40             }
41              
42             sub _programs {
43             my ($self, $id) = @_;
44             my @programs = $self->_program_information($id);
45             my @events = $self->_program_location($id);
46              
47             my %programs;
48             $programs{ $_->id } = $_ foreach @programs;
49              
50             my %events;
51             foreach my $event (@events) {
52             $event->program($programs{ $event->crid });
53             push @{ $events{ $event->crid } }, $event;
54             }
55              
56             foreach my $program (@programs) {
57             $program->events_ref($events{ $program->id });
58             }
59              
60             return \@programs, \@events;
61             }
62              
63             sub _program_information {
64             my ($self, $id) = @_;
65             my @programs;
66             foreach my $file ($self->_find_files($id, "pi")) {
67             push @programs, $self->_program_information_single($id, $file);
68             }
69             return @programs;
70             }
71              
72             my %flags = (
73             'AD' => 'is_audio_described',
74             'S' => 'is_subtitled',
75             'SL' => 'is_deaf_signed',
76             );
77              
78             sub _program_information_single {
79             my ($self, $id, $filename) = @_;
80             my $xpc = $self->_parse_file($filename);
81             my @programs;
82             foreach my $node ($xpc->findnodes("//tva:ProgramInformation")) {
83             my $program = TV::Anytime::Program->new;
84             $program->id($node->getAttribute('programId'));
85             $program->title($xpc->findvalue(".//tva:Title", $node));
86             $program->synopsis($xpc->findvalue(".//tva:Synopsis[attribute::length='short']", $node));
87             $program->synopsis_long($xpc->findvalue(".//tva:Synopsis[attribute::length='long']", $node));
88            
89             # clean up synopsis
90             foreach my $s (qw(synopsis synopsis_long)) {
91             my $synopsis = $program->$s;
92             $synopsis =~s /^(CBeebies:?|CBBC|\[Ages? \d+-\d+\])\.? //;
93             # fix title when title is Julian Fellowes Investigates...
94             # and synopsis is ...a Most Mysterious Murder. The Case of etc.
95             if ($synopsis =~ s/^\.\.\. ?//) {
96             my $title = $program->title;
97             $title =~ s/\.\.\.//;
98             $synopsis =~ s/^(.+?)\. //;
99             if ($1) {
100             $title .= ' ' . $1;
101             $title =~ s/ {2,}/ /;
102             $program->title($title);
103             }
104            
105             }
106             $program->$s($synopsis);
107             }
108            
109             # extract audio described / subtitled / deaf_signed from synopsis
110             foreach my $s (qw(synopsis synopsis_long)) {
111             my $synopsis = $program->$s;
112             next unless $synopsis =~ s/\[([A-Z,]+)\]//;
113             my $flags = $1;
114             foreach my $flag (split ",", $flags) {
115             my $method = $flags{$flag} || next; # bad data
116             $program->$method(1);
117             }
118             $program->$s($synopsis);
119             }
120            
121             $program->caption_language(
122             $xpc->findvalue(".//tva:CaptionLanguage", $node));
123             $program->audio_channels($xpc->findvalue(".//tva:NumOfChannels", $node));
124             $program->aspect_ratio($xpc->findvalue(".//tva:AspectRatio", $node));
125              
126             my @member_of;
127             foreach my $subnode ($self->_xpc($node)->findnodes(".//tva:MemberOf")) {
128             push @member_of, $subnode->getAttribute('crid');
129             }
130             $program->member_of(\@member_of);
131              
132             my @genres;
133             foreach my $subnode ($self->_xpc($node)->findnodes(".//tva:Genre")) {
134             my $href = $subnode->getAttribute('href');
135             $href =~ s/^urn:tva:metadata:cs:(.+?):.+$/$1/;
136             push @genres,
137             TV::Anytime::Genre->new(
138             {
139             name => $href,
140             value => $self->_xpc($subnode)->findvalue("./tva:Name"),
141             }
142             );
143             }
144             $program->genres_ref(\@genres);
145              
146             push @programs, $program;
147             }
148             return @programs;
149             }
150              
151             sub _program_location {
152             my ($self, $id) = @_;
153              
154             my @events;
155             foreach my $file ($self->_find_files($id, "pl")) {
156             push @events, $self->_program_location_single($id, $file);
157             }
158             return @events;
159             }
160              
161             sub _program_location_single {
162             my ($self, $id, $filename) = @_;
163             my $xpc = $self->_parse_file($filename);
164             my @events;
165             foreach my $node ($xpc->findnodes("//tva:ScheduleEvent")) {
166             my $nodexpc = $self->_xpc($node);
167             my $event = TV::Anytime::Event->new;
168             $event->crid($nodexpc->findnodes("./tva:Program", $node)->get_node(0)
169             ->getAttribute('crid'));
170             $event->start(
171             $self->_parse_date($nodexpc->findvalue('./tva:PublishedStartTime')));
172             my $duration =
173             $self->_parse_duration($nodexpc->findvalue('./tva:PublishedDuration'));
174             $event->stop($event->start + $duration);
175              
176             # warn $event->crid . ": " . $event->start->datetime . " -> " . $event->stop->datetime . "\n" if $event->start->datetime =~ /2005-08-.?.?T07:00/;
177             # eq 'crid://bbc.co.uk/277092412'
178             #or $event->crid eq 'crid://bbc.co.uk/277092882';
179             push @events, $event;
180             }
181             return @events;
182             }
183              
184             sub groups {
185             my $self = shift;
186             my @services;
187             my $xpc = $self->_parse_file("groups_cr.xml");
188             my ($members, $parents);
189             foreach my $node ($xpc->findnodes("//cr:Result")) {
190             my $id = $node->getAttribute("CRID");
191             my @members =
192             map { $_->textContent } $self->_xpc($node)->findnodes(".//cr:Crid");
193             $members->{$id} = \@members;
194             push @{ $parents->{$_} }, $id foreach @members;
195             }
196             $xpc = $self->_parse_file("groups_gr.xml");
197             my @groups;
198             foreach my $node ($xpc->findnodes("//tva:GroupInformation")) {
199             my $id = $node->getAttribute("groupId");
200             my $members = $members->{$id};
201             next unless $members;
202             push @groups,
203             TV::Anytime::Group->new(
204             {
205             id => $id,
206             type => $self->_xpc($node)->findnodes("./tva:GroupType")->[0]
207             ->getAttribute("value"),
208             title => $self->_xpc($node)->findvalue(".//tva:Title"),
209             members_ref => $members,
210             parents_ref => $parents->{$id},
211             }
212             );
213             }
214             return @groups;
215             }
216              
217             sub services {
218             my $self = shift;
219             my @services;
220             my $xpc = $self->_parse_file("ServiceInformation.xml");
221             foreach my $node ($xpc->findnodes("//tva:ServiceInformation")) {
222              
223             my @genres;
224             foreach my $subnode ($self->_xpc($node)->findnodes("./tva:ServiceGenre")) {
225             my $href = $subnode->getAttribute('href');
226             $href =~ s/^urn:tva:metadata:cs:(.+?):.+$/$1/;
227             push @genres,
228             TV::Anytime::Genre->new(
229             {
230             name => $href,
231             value => $self->_xpc($subnode)->findvalue("./tva:Name"),
232             }
233             );
234             }
235             push @services,
236             TV::Anytime::Service->new(
237             {
238             anytime => $self,
239             id => $node->getAttribute('serviceId'),
240             name => $xpc->findvalue("./tva:Name", $node),
241             owner => $xpc->findvalue("./tva:Owner", $node),
242             logo => $xpc->findvalue("./tva:Logo", $node),
243             genres_ref => \@genres,
244             }
245             );
246             }
247             return @services;
248             }
249              
250             sub services_television {
251             my $self = shift;
252             return grep { $_->is_television } $self->services;
253             }
254              
255             sub services_radio {
256             my $self = shift;
257             return grep { $_->is_radio } $self->services;
258             }
259              
260             sub _parse_file {
261             my ($self, $filename) = @_;
262             my $directory = $self->directory;
263             my $path = $filename;
264             $path = dir($self->directory, $filename) unless $filename =~ /$directory/;
265             my $parser = XML::LibXML->new;
266             my $doc = $parser->parse_file($path);
267             return $self->_xpc($doc);
268             }
269              
270             sub _xpc {
271             my ($self, $node) = @_;
272             my $xpc = XML::LibXML::XPathContext->new($node);
273             $xpc->registerNs('tva', 'urn:tva:metadata:2002');
274             $xpc->registerNs('rss', 'http://purl.org/rss/1.0/');
275             $xpc->registerNs('cr',
276             'http://www.tv-anytime.org/2002/02/ContentReferencing');
277             return $xpc;
278             }
279              
280             sub _parse_date {
281             my ($self, $string) = @_;
282             my $dt = DateTime::Format::ISO8601->parse_datetime($string);
283             return $dt;
284             }
285              
286             sub _parse_duration {
287             my ($self, $string) = @_;
288             my $d = DateTime::Format::Duration->new(pattern => 'PT%HH%MM%SS',);
289             return $d->parse_duration($string);
290             }
291              
292             1;
293              
294             __END__