File Coverage

blib/lib/WWW/TV/Episode.pm
Criterion Covered Total %
statement 49 157 31.2
branch 19 64 29.6
condition 5 15 33.3
subroutine 12 36 33.3
pod 19 19 100.0
total 104 291 35.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             WWW::TV::Episode - Parse TV.com for TV Episode information.
4              
5             =head1 SYNOPSIS
6              
7             use WWW::TV::Episode qw();
8             my $episode = WWW::TV::Series->new(id => '475567');
9              
10             # with optional paramers
11              
12             print $episode->summary;
13              
14             =head1 DESCRIPTION
15              
16             The L module parses TV.com episode information using
17             L. Unfortunately I can't see a way to search for an episode
18             by name, so I haven't implemented it. It is probably possible to do so if you
19             populate a series object and grep $series->episodes for the episode name you
20             are searching for.
21              
22             =head1 METHODS
23              
24             =cut
25              
26             package WWW::TV::Episode;
27 1     1   876 use strict;
  1         3  
  1         43  
28 1     1   5 use warnings;
  1         2  
  1         70  
29              
30             our $VERSION = '0.14';
31              
32 1     1   17 use Carp qw(croak);
  1         2  
  1         52  
33 1     1   4 use LWP::UserAgent qw();
  1         2  
  1         2355  
34              
35             =head2 new
36              
37             The new() method is the constructor. It takes the id of the show
38             assuming you have previously looked that up.
39              
40             # default usage
41             my $episode = WWW::TV::Episode->new(id => 924072);
42              
43             # change user-agent from the default of "libwww-perl/#.##"
44             my $episode = WWW::TV::Episode->new(id => 924072, agent => 'WWW::TV');
45              
46             It also (optionally) takes the name of the episode. This is not used
47             in any way to search for the episode, but is used as initial data
48             population for that field so that the html isn't parsed if you only
49             want an object with the name. This is used by the L
50             object to populate a big array of episodes that have names without
51             needing to fetch any pages.
52              
53             # pre-populate episode name
54             my $episode = WWW::TV::Episode->new(id => 924072, name => 'Run!');
55              
56             =cut
57              
58             sub new {
59 2 50   2 1 1370 my $class = ref $_[0] ? ref(shift) : shift;
60              
61 2         3 my %data;
62              
63 2 50       11 if (@_ == 1) {
    50          
64 0         0 $data{id} = shift;
65             }
66             elsif (scalar(@_) % 2 == 0) {
67 2         7 %data = @_;
68             }
69              
70 2 50       6 croak 'No id given to constructor' unless exists $data{id};
71 2 50 33     23 croak "Invalid id: $data{id}" unless ($data{id} =~ /^\d+$/ && $data{id});
72              
73 2 100       26 return bless {
74             id => $data{id},
75             name => $data{name},
76             _agent => $data{agent},
77             _site => $data{site},
78             filled => {
79             id => 1,
80             $data{name}
81             ? (name => 1)
82             : (),
83             },
84             }, $class;
85             }
86              
87             =head2 id
88              
89             The ID of this episode, according to TV.com
90              
91             =cut
92              
93             sub id {
94 3     3 1 59 my $self = shift;
95              
96 3         414 return $self->{id};
97             }
98              
99             =head2 name
100              
101             Returns a string containing the name of the episode.
102              
103             =cut
104              
105             sub name {
106 1     1 1 2 my $self = shift;
107              
108 1 50       5 unless (exists $self->{filled}->{name}) {
109 0         0 $self->{filled}->{name} = 1;
110 0         0 ($self->{name}) = $self->_html =~ m{
111             (.*)\n
112             \s*
113             }x;
114             }
115              
116 1         5 return $self->{name};
117             }
118              
119             =head2 summary
120              
121             Returns a string containing basic information about this series.
122              
123             =cut
124              
125             sub summary {
126 1     1 1 2 my $self = shift;
127              
128 1 50       10 unless (exists $self->{filled}->{summary}) {
129 1         3 $self->{filled}->{summary} = 1;
130 1         4 ($self->{summary}) = $self->_html =~ m{
131             (.*?)

132             }smx;
133 0         0 $self->{summary} =~ s/
/\n/g;
134 0         0 $self->{summary} =~ s/.*?<\/a>//g;
135 0         0 $self->{summary} =~ s/^\s*//;
136 0         0 $self->{summary} =~ s/\s*$//;
137             }
138              
139 0         0 return $self->{summary};
140             }
141              
142             =head2 season_number
143              
144             Returns the season number that this episode appeared in.
145              
146             =cut
147              
148             sub season_number {
149 0     0 1 0 my $self = shift;
150              
151 0 0       0 unless (exists $self->{filled}->{season_number}) {
152 0         0 $self->_fill_vitals;
153             }
154              
155 0         0 return $self->{season_number};
156             }
157              
158             =head2 episode_number
159              
160             Returns the overall number of this episode. Note, this is not
161             necessarily the production order of the episodes, but is the order
162             in which they aired.
163              
164             =cut
165              
166             sub episode_number {
167 0     0 1 0 my $self = shift;
168              
169 0 0       0 unless (exists $self->{filled}->{episode_number}) {
170 0         0 $self->_fill_vitals;
171             }
172              
173 0         0 return $self->{episode_number};
174             }
175              
176             =head2 format_details ($format_str)
177              
178             Returns episode details using a special format string, similar to printf:
179             %I - series ID
180             %N - series name
181             %s - season number
182             %S - season number (0-padded to two digits, if required)
183             %i - episode ID
184             %e - episode number
185             %E - episode number (0-padded to two digits, if required)
186             %n - episode name
187             %d - date episode first aired
188              
189             The default format is:
190             %N.s%Se%E - %n (eg: "Heroes.s1e02 - Don't Look Back")
191              
192             =cut
193              
194             sub format_details {
195 0     0 1 0 my $self = shift;
196              
197 0   0     0 my $format_str = shift || '%N.s%Se%E - %n';
198              
199             # format subs .. expecting $_[0] is $self
200             my %formats = (
201 0     0   0 'I' => sub { $_[0]->series_id },
202 0     0   0 'N' => sub { $_[0]->series->name },
203 0     0   0 's' => sub { $_[0]->season_number },
204 0     0   0 'S' => sub { sprintf('%02d', $_[0]->season_number) },
205 0     0   0 'i' => sub { $_[0]->id },
206 0     0   0 'e' => sub { $_[0]->episode_number },
207 0     0   0 'E' => sub { sprintf('%02d', $_[0]->episode_number) },
208 0     0   0 'n' => sub { $_[0]->name },
209 0     0   0 'd' => sub { $_[0]->first_aired },
210 0         0 );
211              
212             # substitution
213 0         0 $format_str =~
214             s/
215             # look for single character format specifier
216             %([a-zA-Z])
217             /
218             # use format sub if found, otherwise leave as-is
219 0 0       0 $formats{$1} ? $formats{$1}->($self) : "\%$1"
220              
221             /sgex;
222              
223 0         0 return $format_str;
224             }
225              
226             =head2 first_aired
227              
228             Returns a string of the date this episode first aired in ISO 8601 (yyyy-mm-dd) format.
229              
230             =cut
231              
232             sub first_aired {
233 0     0 1 0 my $self = shift;
234              
235 0 0       0 unless (exists $self->{filled}->{first_aired}) {
236 0         0 $self->_fill_vitals;
237             }
238              
239 0         0 return $self->{first_aired};
240             }
241              
242             =head2 stars
243              
244             Returns a list of the stars that appeared in this episode.
245              
246             # in scalar context, returns a comma-delimited string
247             my $stars = $episode->stars;
248              
249             # in array context, returns an array
250             my @stars = $episode->stars;
251              
252             =cut
253              
254             sub stars {
255 0     0 1 0 my $self = shift;
256              
257 0 0       0 unless (exists $self->{filled}->{stars}) {
258 0         0 my ($stars) = $self->_html =~ m{
259             \s*
260            
Stars?:
\s*
261             (
.*?
)\s*
262            
263             }x;
264              
265 0         0 $self->{stars} = $self->_parse_people($stars);
266 0         0 $self->{filled}->{stars} = 1;
267             }
268              
269 0         0 return $self->{stars};
270             }
271              
272             =head2 guest_stars
273              
274             Returns a list of the guest stars that appeared in this episode.
275              
276             # in scalar context, returns a comma-delimited string
277             my $guest_stars = $episode->guest_stars;
278              
279             # in array context, returns an array
280             my @guest_stars = $episode->guest_stars;
281              
282             =cut
283              
284             sub guest_stars {
285 0     0 1 0 my $self = shift;
286              
287 0 0       0 unless (exists $self->{filled}->{guest_stars}) {
288 0         0 my ($stars) = $self->_html =~ m{
289             \s*
290            
Guest\sStars?:
\s*
291             (
.*?
)\s*
292            
293             }x;
294              
295 0         0 $self->{guest_stars} = $self->_parse_people($stars);
296 0         0 $self->{filled}->{guest_stars} = 1;
297             }
298              
299 0         0 return $self->{guest_stars};
300             }
301              
302             =head2 recurring_roles
303              
304             Returns a list of the people who have recurring roles
305             that appeared in this episode
306              
307             # in scalar context, returns a comma-delimited string
308             my $recurring_roless = $episode->recurring_roless;
309              
310             # in array context, returns an array
311             my @recurring_roless = $episode->recurring_roless;
312              
313             =cut
314              
315             sub recurring_roles {
316 0     0 1 0 my $self = shift;
317              
318 0 0       0 unless (exists $self->{filled}->{recurring_roles}) {
319 0         0 my ($stars) = $self->_html =~ m{
320             \s*
321            
Recurring\sRoles?:
\s*
322             (
.*?
)\s*
323            
324             }x;
325              
326 0         0 $self->{recurring_roles} = $self->_parse_people($stars);
327 0         0 $self->{filled}->{recurring_roles} = 1;
328             }
329              
330 0         0 return $self->{recurring_roles};
331             }
332              
333             sub _parse_people {
334 0     0   0 my $self = shift;
335 0 0       0 my $stars = shift or return;
336              
337 0         0 my @stars;
338 0         0 for my $star (split /<\/dd>/, $stars) {
339 0 0       0 next unless $star =~ m{(.*?)};
340 0         0 push @stars, $1;
341             }
342              
343 0         0 return join(', ', @stars);
344             }
345              
346             =head2 writers
347              
348             Returns a list of the people that wrote this episode.
349              
350             # in scalar context, returns a comma-delimited string
351             my $writers = $episode->writers;
352              
353             # in array context, returns an array
354             my @writers = $episode->writers;
355              
356             =cut
357              
358             sub writers {
359 0     0 1 0 my $self = shift;
360              
361 0 0       0 unless (exists $self->{filled}->{writers}) {
362 0         0 my ($writers) = $self->_html =~ m{
363             \s*
364            
Writers?:
\s*
365             (
.*?
)\s*
366            
367             }x;
368              
369 0         0 $self->{writers} = $self->_parse_people($writers);
370 0         0 $self->{filled}->{writers} = 1;
371             }
372              
373 0         0 return $self->{writers};
374             }
375              
376             =head2 directors
377              
378             Returns a list of the people that directed this episode.
379              
380             # in scalar context, returns a comma-delimited string
381             my $directors = $episode->directors;
382              
383             # in array context, returns an array
384             my @directors = $episode->directors;
385              
386             =cut
387              
388             sub directors {
389 0     0 1 0 my $self = shift;
390              
391 0 0       0 unless (exists $self->{filled}->{directors}) {
392 0         0 my ($directors) = $self->_html =~ m{
393             \s*
394            
Directors?:
\s*
395             (
.*?
)\s*
396            
397             }x;
398              
399 0         0 $self->{directors} = $self->_parse_people($directors);
400 0         0 $self->{filled}->{directors} = 1;
401             }
402              
403 0         0 return $self->{directors};
404             }
405              
406             =head2 agent ($value)
407              
408             Returns the current user agent setting, and sets to $value if provided.
409              
410             =cut
411              
412             sub agent {
413 6     6 1 488 my $self = shift; # may be called as $self or $class
414 6         8 my $value = shift;
415              
416 6 50       15 if (ref $self) {
417 6 100       14 if (defined $value) {
418 2         4 $self->{_agent} = $value;
419             }
420 6   66     33 return ($self->{_agent} || LWP::UserAgent::_agent);
421             } else {
422 0   0     0 return ($value || LWP::UserAgent::_agent);
423             }
424             }
425              
426             =head2 site ($value)
427              
428             Returns the current mirror site setting, and sets to $value if provided.
429              
430             Default site is "www"; other options include: us, uk, au
431              
432             =cut
433              
434             sub site {
435 8     8 1 14 my $self = shift; # may be called as $self or $class
436 8         11 my $value = shift;
437              
438 8 50       22 if (ref $self) {
439 8 100       19 if (defined $value) {
440 3 100       22 if ($value =~ /^(au|uk|us|www|)$/i) {
441 2         6 $self->{_site} = $value;
442             } else {
443 1         113 warn "Ignoring unknown site value: [$value]\n";
444             }
445             }
446 8   100     55 return ($self->{_site} || 'www');
447             } else {
448 0   0     0 return ($value || 'www');
449             }
450             }
451              
452             =head2 url
453              
454             Returns the url that was used to create this object.
455              
456             =cut
457              
458             sub url {
459 1     1 1 2 my $self = shift;
460              
461 1         5 return sprintf('http://%s.tv.com/episode/%d/summary.html', $self->site, $self->id);
462             }
463              
464             =head2 season
465              
466             Returns an array of other episodes for the same season of this series.
467              
468             =cut
469              
470             sub season {
471 0     0 1 0 my $self = shift;
472 0         0 my @episodes = $self->series->episodes( season => $self->season_number );
473 0 0       0 return wantarray ? @episodes : \@episodes;
474             }
475              
476             =head2 series_id
477              
478             Returns the series ID for this episode.
479              
480             =cut
481              
482             sub series_id {
483 0     0 1 0 my $self = shift;
484              
485 0 0       0 unless (exists $self->{filled}->{series_id}) {
486 0         0 my ($id) = $self->_html =~ m{};
487 0         0 $self->{series_id} = $id;
488 0         0 $self->{filled}->{series_id} = 1;
489             }
490              
491 0         0 return $self->{series_id};
492             }
493              
494             =head2 series
495              
496             Returns an L object which is the complete series
497             that this episode is a part of.
498              
499             =cut
500              
501             sub series {
502 0     0 1 0 my $self = shift;
503              
504 0 0       0 unless (exists $self->{filled}->{series}) {
505 0 0       0 if ($self->series_id) {
506 0         0 require WWW::TV::Series;
507 0         0 $self->{series} = WWW::TV::Series->new(id => $self->series_id);
508 0         0 $self->{filled}->{series} = 1;
509             } else {
510 0         0 croak "Can't find series_id for this episode";
511             }
512             }
513              
514 0         0 return $self->{series};
515             }
516              
517             sub _fill_vitals {
518 0     0   0 my $self = shift;
519              
520 0         0 ($self->{season_number}, $self->{episode_number}, $self->{first_aired})
521             = $self->_html
522             =~ m{
523            
524            
  • .*?
  • 525            
  • Season:\s*(.*?)\s*
  • 526            
  • Episode:\s*(.*?)\s*
  • 527             (?:
  • First\sAired:\s*(?:\w*?\s*)?(\d+/\d+/\d+|n/a)\s*
  • )?
    528             (?:
  • Prod\sCode:\s*.*\s*
  • )?
    529            
    530             }sx;
    531              
    532 0         0 $self->{filled}->{$_} = 1 for qw(episode_number season_number first_aired);
    533              
    534 0         0 return $self->_parse_first_aired;
    535             }
    536              
    537             sub _parse_first_aired {
    538 0     0   0 my $self = shift;
    539              
    540 0 0       0 if (not defined $self->{first_aired}) {
    541 0         0 $self->{first_aired} = 'n/a';
    542             }
    543              
    544 0 0       0 return if $self->{first_aired} eq 'n/a';
    545              
    546 0         0 my ($month, $day, $year) = split('/', $self->{first_aired});
    547 0         0 $self->{first_aired} = sprintf('%04d-%02d-%02d', $year, $month, $day);
    548              
    549 0         0 return 1;
    550             }
    551              
    552             sub _html {
    553 1     1   3 my $self = shift;
    554              
    555 1 50       5 unless ($self->{filled}->{html}) {
    556 1         4 my $ua = LWP::UserAgent->new( agent => $self->agent );
    557 1         4777 my $rc = $ua->get($self->url);
    558              
    559 1 50       1520175 croak sprintf('Unable to fetch page for series %s', $self->id)
    560             unless $rc->is_success;
    561 0           $self->{html} =
    562             join(
    563             "\n",
    564 0           map { s/^\s*//; s/\s*$//; $_ }
      0            
      0            
    565             split /\n/, $rc->content
    566             );
    567 0           $self->{filled}->{html} = 1;
    568             }
    569              
    570 0           return $self->{html};
    571             }
    572              
    573             1;
    574              
    575             __END__