File Coverage

blib/lib/XML/FeedPP/MediaRSS.pm
Criterion Covered Total %
statement 140 163 85.8
branch 40 56 71.4
condition 11 19 57.8
subroutine 11 12 91.6
pod 2 2 100.0
total 204 252 80.9


line stmt bran cond sub pod time code
1             package XML::FeedPP::MediaRSS;
2             BEGIN {
3 1     1   86276 $XML::FeedPP::MediaRSS::VERSION = '0.02';
4             }
5              
6 1     1   12 use strict;
  1         2  
  1         37  
7 1     1   44 use warnings;
  1         3  
  1         2637  
8              
9             # ABSTRACT: MediaRSS support for XML::FeedPP
10              
11              
12             sub new {
13 5     5 1 174720 my ($class, $feed) = @_;
14 5         37 bless { feed => $feed }, $class;
15             }
16              
17             sub _find_optional {
18 0     0   0 my ($key, $item, @parents) = @_;
19 0         0 $key = "media:$key";
20 0         0 unshift(@parents, $item);
21 0         0 for my $hash (@parents) {
22 0         0 my $val = $hash->{key};
23 0 0       0 return $val if $val;
24             }
25 0         0 return undef;
26             }
27              
28             sub _force_array {
29 64     64   104 my ($hash, $key) = @_;
30 64   100     222 my $raw = $hash->{$key} || return [];
31 59 100       572 return (ref $raw eq 'ARRAY' ? $raw : [ $raw ]);
32             }
33              
34             sub _force_hash {
35 49     49   206 my $value = shift;
36 49 100       164 return $value if ref $value eq 'HASH';
37 18         69 return { '#text' => $value };
38             }
39              
40             sub _process_group {
41 6     6   14 my ($self, $item, $group) = @_;
42 6         11 my $contents = _force_array($group, 'media:content');
43 11         19 map {
44 6         24 my $c = $_;
45              
46             # all found entries, from least-specific to most-specific
47             my $find = sub {
48 264     264   455 my $key = "media:$_[0]";
49 264         271 my @found;
50 264         430 for my $place ($self->{channel}, $item, $group, $c) {
51 1056 100       4608 if (exists $place->{$key}) {
52 49         90 my @these =
53 43         69 map { _force_hash($_) }
54 43         199 @{ _force_array($place, $key) };
55              
56 43         150 push @found, @these;
57             }
58             }
59 264 100       3177 return wantarray ? @found : $found[-1];
60 11         62 };
61              
62 11         25 my %hash;
63 11         50 my @atts = grep { /^-/ } keys %$c;
  65         579  
64 11         41 @hash{map { s/^-//; $_ } @atts} = @{$c}{@atts};
  49         128  
  49         140  
  11         65  
65              
66 11 100       301 if ($hash{isDefault}) {
67 1         33 $hash{isDefault} = $hash{isDefault} eq 'true';
68             }
69              
70              
71 11 50       26 if (my $adult = $find->('adult')) {
72 0         0 $hash{adult} = $adult->{'#text'} eq 'true';
73             }
74              
75             my $make_hash = sub {
76 9     9   16 my ($el, $required, $optional) = @_;
77 9         8 my %h;
78 9         12 @h{@$required} = @{$el}{map { "-$_" } @$required};
  9         32  
  7         15  
79 9         53 for my $k (@$optional) {
80 19         67 my $v = $el->{"-$k"};
81 19 100       152 $h{$k} = $v if $v;
82             }
83 9         27 return \%h;
84 11         63 };
85              
86 11         22 for my $rating ($find->('rating')) {
87 10   50     48 my $schema = $rating->{'-schema'} || 'simple';
88 10         41 $hash{rating}{$schema} = $rating->{'#text'};
89             }
90              
91 11         35 for my $title ($find->('title')) {
92 0   0     0 my $type = $title->{'-type'} || 'plain';
93 0         0 $hash{title}{$type} = $title->{'#text'};
94             }
95              
96 11         18 my %keyword_set;
97 11         22 for my $keywords ($find->('keywords')) {
98 0         0 my @words = split /,\s*/, $keywords->{'#text'};
99 0         0 @keyword_set{@words} = ();
100             }
101              
102 11         23 my @keywords = keys %keyword_set;
103              
104 11 50       25 $hash{keywords} = \@keywords if @keywords;
105              
106 11         84 for my $thumb ($find->('thumbnail')) {
107 0         0 push @{$hash{thumbnails}}, $make_hash->(
  0         0  
108             $thumb, ['url'], [qw(width height time)]
109             );
110             }
111              
112              
113 11         26 for my $category ($find->('category')) {
114 9   100     57 my $scheme = $category->{'-scheme'} || 'none';
115 9         44 $hash{category}{$scheme} = $category->{'#text'};
116             }
117              
118 11 100       32 if (my $checksum = $find->('hash')) {
119 1   50     6 $hash{hash} = {
120             checksum => $checksum->{'#text'},
121             algorithm => $checksum->{'-algo'} || 'md5',
122             };
123             }
124              
125 11 100       36 if (my $player = $find->('player')) {
126 1         5 $hash{player} = $make_hash->(
127             $player, ['url'], [qw(width height)]
128             );
129             }
130              
131 11         19 CREDIT: for my $credit ($find->('credit')) {
132 15   50     54 my $scheme = $credit->{'-scheme'} || 'urn:ebu';
133 15         173 my $role = $credit->{'-role'};
134 15         126 my $entity = $credit->{'#text'};
135 15   100     159 my $list = $hash{credit}{$scheme}{$role} ||= [];
136 15         29 for my $e (@$list) {
137 5 50       21 next CREDIT if $entity eq $e;
138             }
139 15         41 push(@$list, $entity);
140             }
141              
142 11 50       27 if (my $copyright = $find->('copyright')) {
143 0         0 $hash{copyright}{text} = $copyright->{'#text'};
144 0         0 my $url = $copyright->{'-url'};
145 0 0       0 $hash{copyright}{url} = $url if $url;
146             }
147              
148 11         31 for my $text ($find->('text')) {
149 1         4 my $t = $hash{text} = $make_hash->(
150             $text, [], [qw(lang start end type)]
151             );
152 1   50     5 $t->{type} ||= 'plain';
153 1         4 $t->{text} = $text->{'#text'};
154             }
155              
156              
157 11 100       30 if (my $restriction = $find->('restriction')) {
158 1         5 my %r = (allow => $restriction->{'-relationship'} eq 'allow');
159 1         11 my @list;
160 1 50       4 if (my $unparsed = $restriction->{'#text'}) {
161 0         0 @list = split /\s+/, $unparsed;
162             }
163              
164 1 50 33     16 if (grep { $_ eq 'all' } @list) {
  0 50       0  
  0         0  
165 0         0 $r{list} = 'all';
166             }
167             elsif (@list < 1 || grep { $_ eq 'none' } @list) {
168 1         4 $r{list} = 'none';
169             }
170             else {
171 0         0 $r{list} = \@list;
172             }
173 1         4 $r{type} = $restriction->{'-type'};
174 1         11 $hash{restriction} = \%r;
175             }
176              
177              
178 11 100       21 if (my $community = $find->('community')) {
179 1         2 my %c;
180 1 50       5 if (my $starRating = $community->{'media:starRating'}) {
181 1         13 $c{starRating} = $make_hash->(
182             $starRating, [], [qw(average min max count)]
183             );
184             }
185 1 50       6 if (my $stats = $community->{'media:statistics'}) {
186 1         11 $c{statistics} = $make_hash->(
187             $stats, [], [qw(views favorites)]
188             );
189             }
190 1 50       6 if (my $tags = $community->{'media:tags'}) {
191 1         17 for (split /,\s*/, $tags) {
192 2         5 s/^\s+//;
193 2         5 s/\s+$//;
194 2         7 s/:\s+/:/g;
195 2         7 my ($key, $val) = split /:/;
196 2         8 $c{tags}{$key} = $val;
197             }
198             }
199 1         4 $hash{community} = \%c;
200             }
201              
202             my $simple_list = sub {
203 33     33   50 my ($singular, $plural) = @_;
204 33         50 for my $thing ($find->($plural)) {
205 3         8 my $things = _force_array($thing, "media:$singular");
206 3         5 push @{$hash{$plural}}, @$things;
  3         16  
207             }
208 11         61 };
209              
210 11         29 $simple_list->(qw(comment comments));
211              
212              
213 11         24 for my $embed ($find->('embed')) {
214 1         2 for my $param (@{ _force_array($embed, 'media:param') }) {
  1         3  
215 5         61 $hash{embed}{$param->{'-name'}} = $param->{'#text'};
216             }
217             }
218              
219 11         1639 $simple_list->(qw(response responses));
220              
221 11         22 $simple_list->(qw(backLink backLinks));
222              
223              
224 11 100       25 if (my $status = $find->('status')) {
225 1         4 $hash{status} = $make_hash->($status, ['state'], ['reason']);
226             }
227              
228              
229 11         21 for my $price ($find->('price')) {
230 1         5 my $p = $make_hash->($price, [], [qw(price info currency type)]);
231 1 50       6 push (@{ $hash{price} }, $p) if keys %$p;
  1         4  
232             }
233              
234 11 100       45 if (my $license = $find->('license')) {
235 1         4 my $l = $hash{license} = $make_hash->(
236             $license, [], [qw(type href)]
237             );
238 1         4 $l->{name} = $license->{'#text'};
239             }
240              
241              
242 11         27 for my $st ($find->('subTitle')) {
243 1         5 my $s = $make_hash->($st, [qw(lang href type)], []);
244 1         3 my $l = delete $s->{lang};
245 1         5 $hash{subTitle}{$l} = $s;
246             }
247              
248 11 100       36 if (my $peerLink = $find->('peerLink')) {
249 1         4 $hash{peerLink} = $make_hash->($peerLink, [qw(type href)], []);
250             }
251              
252 11 50       21 if (my $r = $find->('rights')) {
253 0         0 $hash{rights} = $r->{'-status'};
254             }
255              
256 11 100       23 if (my $sl = $find->('scenes')) {
257 1         3 for my $scene (@{ _force_array($sl, 'media:scene') }) {
  1         7  
258 1         3 push @{$hash{scenes}}, {
  1         5  
259             title => $scene->{sceneTitle},
260             description => $scene->{sceneDescription},
261             start_time => $scene->{sceneStartTime},
262             end_time => $scene->{sceneEndTime},
263             };
264             }
265             }
266              
267 11         396 bless \%hash, 'XML::FeedPP::MediaRSS::Content';
268             } @$contents;
269             }
270              
271              
272             sub for_item {
273 5     5 1 336 my ($self, $item) = @_;
274 5         18 my $contents = _force_array($item, 'media:content');
275 5         21 my $groups = _force_array($item, 'media:group');
276              
277             return (
278 5         53 (map { $self->_process_group($item, $_) } @$groups),
  1         5  
279             $self->_process_group($item, { 'media:content' => $contents }),
280             );
281             }
282              
283             1;
284              
285              
286              
287             =pod
288              
289             =head1 NAME
290              
291             XML::FeedPP::MediaRSS - MediaRSS support for XML::FeedPP
292              
293             =head1 VERSION
294              
295             version 0.02
296              
297             =head1 SYNOPSIS
298              
299             use XML::FeedPP;
300              
301             my $feed = XML::FeedPP->new('http://a.media.rss/source');
302             my $media = XML::FeedPP::MediaRSS->new($feed);
303             for my $i ( $feed->get_item ) {
304             for my $content ( $media->for_item($i) ) {
305             die "18 or over" if $content->{adult};
306             }
307             }
308              
309             =head1 DESCRIPTION
310              
311             XML::FeedPP does not support Yahoo's MediaRSS extension, and it shouldn't.
312             It's only supported in some formats, and XML::FeedPP is a
313             lowest-common-denominator kind of module. That said, sometimes you need to
314             consume feeds with MediaRSS in them.
315              
316             =head1 METHODS
317              
318             =head2 new ( feed )
319              
320             You have to pass in an L object. C isn't a
321             subclass of L - it has one, and inspects its dirty innards (which
322             is somewhat safe since they're produced by L) to find media
323             content.
324              
325             =head2 for_item ( item )
326              
327             Pass in a feed item (the things returned by C<< $feed->get_item >>) and get
328             back a list of L objects.
329              
330             =head1 KEYS
331              
332             =head2 adult
333              
334             1 or ''
335              
336             =head2 rating
337              
338             A hash of all the ratings found, schema => rating.
339              
340             =head2 title
341              
342             A hash of all titles found, type => value.
343              
344             =head2 keywords
345              
346             An arrayref of all the keywords found. The comma-delimiting is undone and
347             duplicates are removed.
348              
349             =head2 thumbnails
350              
351             All thumnails found, from most specific (deepest) to least specific. This
352             means that if the channel has a thumbnail and the item has a thumbnail, you'll
353             get the item first, then the channel. If there are multiple thumbnails at the
354             same level, you'll get them in document order. Time coding is not considered.
355             They look like this:
356              
357             { url => '...', width => 400, height => 300, time => 'timecode' }
358              
359             =head2 category
360              
361             Hash of scheme => plain contents of tag
362              
363             =head2 hash
364              
365             Deepest only.
366              
367             {
368             algorithm => 'md5',
369             checksum => 'dfdec888b72151965a34b4b59031290a',
370             }
371              
372             =head2 player
373              
374             Deepest only.
375              
376             {
377             url => '...',
378             height => 300,
379             width => 400
380             }
381              
382             =head2 credit
383              
384             Hash of scheme to role-hash, like this:
385              
386             {
387             'urn:ebu' => {
388             actor => [
389             'Julia Roberts',
390             'Tom Hanks',
391             ],
392             director => [
393             'Stevan Spielberg',
394             ]
395             }
396             }
397              
398             =head2 copyright
399              
400             Deepest only.
401              
402             { url => '...', text => '2005 Foobar Media' }
403              
404             =head2 text
405              
406             A list of text objects in document order, like this:
407              
408             [
409             {
410             type => 'plain',
411             lang => 'en',
412             start => 'timecode',
413             end => 'timecode',
414             text => 'The actual value',
415             },
416             ]
417              
418             =head2 restriction
419              
420             {
421             allow => (1|0),
422             type => (country|uri|sharing)
423             list => [ ... ] | 'all' | 'none'
424             }
425              
426             If allow is false, that means deny.
427              
428             =head2 community
429              
430             Deepest only.
431              
432             {
433             starRating => {
434             average => 3.5,
435             count => 20,
436             min => 1,
437             max => 10,
438             },
439             statistics => {
440             views => 5,
441             favorites => 5,
442             },
443             tags => {
444             news => 5,
445             abc => 3,
446             reuters => undef,
447             },
448             }
449              
450             =head2 comments
451              
452             Simple list of strings.
453              
454             =head2 embed
455              
456             Hash of key-value pairs. Deepest only.
457              
458             =head2 responses
459              
460             Simple list of strings
461              
462             =head2 backlinks
463              
464             Simple list of strings
465              
466             =head2 status
467              
468             Deepest only.
469              
470             { state => 'status', reason => 'reason' }
471              
472             =head2 price
473              
474             List of pricing structures, which are hashes with the keys C
475             (optional), C (optional), C (optional), and C (optional).
476             If none of these is present for a given price tag, we're going to pretend it
477             doesn't exist.
478              
479             =head2 license
480              
481             Hash of type, href, and name. Deepest only.
482              
483             =head2 subTitle
484              
485             Only one per language as per the spec.
486              
487             {
488             'en_us' => {
489             href => 'http://www.example.org/subtitle.smil',
490             type => 'application/smil',
491             }
492             }
493              
494             =head2 peerLink
495              
496             Deepest only, hash of type and href.
497              
498             =head2 location
499              
500             B, mostly cause I don't need it and I don't feel like reading
501             the geoRSS spec right now. Patches welcome!
502              
503             =head2 rights
504              
505             value of the status attribute for the deepest rights element.
506              
507             =head2 scenes
508              
509             Deepest only, list of hashes with keys title, description, start_time, and
510             end_time.
511              
512             =head1 ALPHA
513              
514             This software hasn't yet been tested beyond the examples provided in the mRSS
515             spec. Failing tests (even better, with patches that fix the failures) are
516             very welcome! Fork and send a pull request on L.
517              
518             =head1 XML::FeedPP::MediaRSS::Content
519              
520             These are blessed hashes, but you're allowed to look inside them. In fact,
521             you're really supposed to. It's okay, don't be nervous.
522              
523             The mapping from the MediaRSS spec (L) to
524             this hash is really straightforward. See the L section for more
525             detail. The shallowness-rules talked about in the spec are applied, e.g.
526             specifiers at higher levels are applied to lower level objects unless they
527             have a more specific rule.
528              
529             =head1 LIMITATIONS
530              
531             =head2 Groups
532              
533             You don't have to (get to?) deal with media groups. All the content for an
534             item gets flattened into one list. Future versions of this module may add
535             support for media groups under a different method name (C) if
536             anyone ever sends me a patch or I can ever find an actual use for it.
537              
538             =head2 Order
539              
540             The MediaRSS spec says some things about order being dependent on document
541             order. We go by the order we get things from L's hashes, which
542             will only be the same as document order if you C<< use_ixhash => 1 >> in the
543             feed. And even then, content in media:groups will come before content outside
544             them.
545              
546             =head2 Read-Write
547              
548             This module only supports reading MediaRSS information from a feed, not adding
549             it. I might add this someday, but of course patches are welcome in the
550             meantime.
551              
552             =head1 GITHUB
553              
554             This project is hosted on github at
555             L.
556              
557             =head1 AUTHOR
558              
559             Paul Driver
560              
561             =head1 COPYRIGHT AND LICENSE
562              
563             This software is copyright (c) 2011 by Paul Driver .
564              
565             This is free software; you can redistribute it and/or modify it under
566             the same terms as the Perl 5 programming language system itself.
567              
568             =cut
569              
570              
571             __END__