File Coverage

blib/lib/IHeartRadio/Streams.pm
Criterion Covered Total %
statement 12 124 9.6
branch 0 62 0.0
condition 0 8 0.0
subroutine 4 15 26.6
pod 9 11 81.8
total 25 220 11.3


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             IHeartRadio::Streams - Fetch actual raw streamable URLs from radio-station websites on IHeartRadio.com
4              
5             =head1 AUTHOR
6              
7             This module is Copyright (C) 2017 by
8              
9             Jim Turner, C<< >>
10            
11             Email: turnerjw784@yahoo.com
12              
13             All rights reserved.
14              
15             You may distribute this module under the terms of either the GNU General
16             Public License or the Artistic License, as specified in the Perl README
17             file.
18              
19             =head1 SYNOPSIS
20              
21             use strict;
22              
23             use IHeartRadio::Streams;
24              
25             my $station = new IHeartRadio::Streams();
26              
27             die "Invalid URL or no streams found!\n" unless ($station);
28              
29             my @streams = $station->get();
30              
31             my $first = $station->get();
32              
33             my $best = $station->getBest();
34              
35             print "Best stream URL=".$best->{'Url'}."\n";
36              
37             my $besturl = $station->getBest('Url');
38              
39             my $stationTitle = $station->getStationTitle();
40            
41             print "Best stream URL=$besturl, Title=$stationTitle\n";
42              
43             my @allfields = $station->validFields();
44              
45             for (my $i=0; $i<$station->count(); $i++) {
46              
47             foreach my $field (@allfields) {
48              
49             print "--$field: ".$streams[$i]->{$field}."\n";
50              
51             }
52              
53             }
54            
55             =head1 DESCRIPTION
56              
57             IHeartRadio::Streams accepts a valid radio station URL on http://iheart.com and
58             returns the urls and other information properties for the actual stream URLs
59             available for that station. The purpose is that one needs one of these URLs
60             in order to have the option to stream the station in one's own choice of
61             audio player software rather than using their web browser and accepting any /
62             all flash, ads, javascript, cookies, trackers, web-bugs, and other crapware
63             that can come with that method of playing. The author uses his own custom
64             all-purpose audio player called "fauxdacious" (his custom hacked version of
65             the open-source "audacious" media player. "fauxdacious" incorporates this
66             module to decode and play iheart.com streams.
67              
68             One or more streams can be returned for each station. The available
69             properties for each stream returned are normally: Bandwidth,
70             HasPlaylist (1|0), MediaType (ie. MP3, AAC, etc.), Reliability (1-100),
71             StreamId (numeric), Type (ie. Live) and Url.
72              
73             =head1 EXAMPLES
74              
75             #!/usr/bin/perl
76              
77             use strict;
78              
79             use IHeartRadio::Streams;
80              
81             my $wbap = new IHeartRadio::Streams('https://www.iheart.com/live/news-talk-820-5350/',
82             'secure_shoutcast', 'secure', 'any', '!rtmp');
83              
84             die "Invalid URL or no streams found!\n" unless ($wbap);
85              
86             my $streamurl = $wbap->getStream(); #OR: $wbap->get('stream');
87              
88             my $stationname = $wbap->getStationTitle(); #OR: $wbap->get('title');
89              
90             my $stationid = $wbap->getStationID(); #OR: $wbap->get('id');
91              
92             my $stationcallletters = $wbap->getFccID(); #OR: $wbap->get('fccid');
93              
94             my @allfields = $wbap->validFields();
95              
96             my $stationdata = $wbap->get();
97            
98             print "--$stationid ($stationcallletters): name=$stationname stream=$streamurl\n==========\n";
99              
100             foreach my $field (@allfields) {
101              
102             print "--$field: ".$stationdata->{$field}."\n";
103              
104             }
105              
106             print "=========\n";
107              
108             my @streamlist = $wbap->get('streams');
109              
110             foreach my $stream (@streamlist) {
111              
112             print "--stream=$stream\n";
113              
114             }
115              
116             This would print:
117              
118             --5350 (WBAP-AM): name=Dallas' WBAP - News Talk 820 stream=https://17263.live.streamtheworld.com:443/WBAPAMAAC_SC
119              
120             ==========
121              
122             --cnt: 6
123              
124             --fccid: WBAP-AM
125              
126             --iconurl: https://iscale.iheart.com/catalog/live/5350?ops=fit(100%2C100)
127              
128             --id: 5350
129              
130             --imageurl: https://iscale.iheart.com/catalog/live/5350
131              
132             --plsid: WBAPAMAAC
133              
134             --streams: ARRAY(0x80c466a8)
135              
136             --streamtype: secure_pls_stream
137              
138             --streamurl: https://playerservices.streamtheworld.com/pls/WBAPAMAAC.pls
139              
140             --title: Dallas' WBAP - News Talk 820
141              
142             =========
143              
144             --stream=https://17523.live.streamtheworld.com:443/WBAPAMAAC_SC
145              
146             --stream=https://16143.live.streamtheworld.com:443/WBAPAMAAC_SC
147              
148             --stream=https://18653.live.streamtheworld.com:443/WBAPAMAAC_SC
149              
150             --stream=https://17483.live.streamtheworld.com:443/WBAPAMAAC_SC
151              
152             --stream=https://17263.live.streamtheworld.com:443/WBAPAMAAC_SC
153              
154             --stream=https://19293.live.streamtheworld.com:443/WBAPAMAAC_SC
155              
156             =head1 SUBROUTINES/METHODS
157              
158             =over 4
159              
160             =item B(I [, I... ] )
161              
162             Accepts an iheart.com URL and creates and returns a new station object, or
163             I if the URL is not a valid iHeartRadio station or no streams are found.
164              
165             The optional I can be one of: any, secure, secure_pls, pls,
166             secure_hls, hls, secure_shortcast, shortcast, secure_rtmp, rtmp, etc. More
167             than one value can be specified to control order of search. A I
168             can be preceeded by an exclamantion point ("!") to reject that type of stream.
169              
170             For example, the list: 'secure_shoutcast', 'secure', 'any', '!rtmp'
171             would try to find a "secure_shoutcast" (https) shortcast stream, if none found,
172             would then look for any secure (https) stream, failing that, would look for
173             any valid stream (http or https). All the while skipping any that are "rtmp"
174             streams. Searching stops when a stream matching the criteria is found.
175              
176             =item $station->B(I<[property]>)
177              
178             Returns the value for the property or a hash reference to all the properties.
179             If the property is 'stream' then either a randomly-selected stream from the
180             list of valid streams is returned or an array of all the valid streams
181             (depending on context). If the property is 'streams' then either an array
182             or an array reference to all the valid streams (depending on context).
183              
184             =item $station->B([ I<[stream-index]> ])
185              
186             Similar to B('stream') except it returns a single stream from the list
187             corresponding to the numeric I<[stream-index]> position in the list. If
188             negative, then indexed from the last position in the list. If out of
189             bounds, then either the first element (if negative) or last (if positive).
190              
191             If I<[stream-index]> is not specified, a random index value is used and
192             thus a random (but valid) stream url is returned.
193              
194             =item $station->B()
195              
196             Returns the number of streams found for the station.
197              
198             =item $station->B()
199              
200             Returns an array containing all the valid property names found. This
201             list is normally: (B, B, B, B, B,
202             B, B, and B). These can be used in the I functions and
203             as the keys in the hash references returned to fetch the corresponding
204             property values.
205              
206             =item $station->B()
207              
208             Returns the station's IHeartRadio ID, for eample, the station:
209             'https://www.iheart.com/live/news-talk-820-5350/' would return "5350".
210              
211             =item $station->B()
212              
213             Returns the station's title (description). for eample, the station:
214             'https://www.iheart.com/live/news-talk-820-5350/' would return:
215             "Dallas' WBAP - News Talk 820".
216              
217             =item $station->B()
218              
219             Returns the url for the station's "cover art" icon image.
220              
221             =item $station->B()
222              
223             Returns the url for the station's IHeartRadio site's banner image.
224              
225             =back
226              
227             =head1 KEYWORDS
228              
229             iheartradio iheart
230              
231             =head1 DEPENDENCIES
232              
233             LWP::Simple
234              
235             =head1 BUGS
236              
237             Please report any bugs or feature requests to C, or through
238             the web interface at L. I will be notified, and then you'll
239             automatically be notified of progress on your bug as I make changes.
240              
241             =head1 SUPPORT
242              
243             You can find documentation for this module with the perldoc command.
244              
245             perldoc IHeartRadio::Streams
246              
247             You can also look for information at:
248              
249             =over 4
250              
251             =item * RT: CPAN's request tracker (report bugs here)
252              
253             L
254              
255             =item * AnnoCPAN: Annotated CPAN documentation
256              
257             L
258              
259             =item * CPAN Ratings
260              
261             L
262              
263             =item * Search CPAN
264              
265             L
266              
267             =back
268              
269             =head1 ACKNOWLEDGEMENTS
270              
271             The idea for this module came from a Python script that does this same task named
272             "getstream", but I wanted a Perl module that could be called from within another
273             program! I do not know the author of getstream.py.
274              
275             =head1 LICENSE AND COPYRIGHT
276              
277             Copyright 2017 Jim Turner.
278              
279             This program is free software; you can redistribute it and/or modify it
280             under the terms of the the Artistic License (2.0). You may obtain a
281             copy of the full license at:
282              
283             L
284              
285             Any use, modification, and distribution of the Standard or Modified
286             Versions is governed by this Artistic License. By using, modifying or
287             distributing the Package, you accept this license. Do not use, modify,
288             or distribute the Package, if you do not accept this license.
289              
290             If your Modified Version has been derived from a Modified Version made
291             by someone other than you, you are nevertheless required to ensure that
292             your Modified Version complies with the requirements of this license.
293              
294             This license does not grant you the right to use any trademark, service
295             mark, tradename, or logo of the Copyright Holder.
296              
297             This license includes the non-exclusive, worldwide, free-of-charge
298             patent license to make, have made, use, offer to sell, sell, import and
299             otherwise transfer the Package with respect to any patent claims
300             licensable by the Copyright Holder that are necessarily infringed by the
301             Package. If you institute patent litigation (including a cross-claim or
302             counterclaim) against any party alleging that the Package constitutes
303             direct or contributory patent infringement, then this Artistic License
304             to you shall terminate on the date that such litigation is filed.
305              
306             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
307             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
308             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
309             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
310             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
311             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
312             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
313             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
314              
315             =cut
316              
317             package IHeartRadio::Streams;
318              
319 1     1   13708 use strict;
  1         2  
  1         25  
320 1     1   3 use warnings;
  1         1  
  1         23  
321             #use Carp qw(croak);
322 1     1   401 use LWP::Simple qw();
  1         52103  
  1         29  
323 1     1   6 use vars qw(@ISA @EXPORT $VERSION);
  1         1  
  1         1172  
324              
325             our $VERSION = '1.00';
326              
327             require Exporter;
328              
329             @ISA = qw(Exporter);
330             @EXPORT = qw(get getBest count validFields);
331             $Carp::Internal{ (__PACKAGE__) }++;
332              
333             sub new
334             {
335 0     0 1   my $class = shift;
336 0           my $url = shift;
337 0           my (@okStreams, @skipStreams);
338 0           while (@_) {
339 0 0         if ($_[0] =~ /^\!/o) {
340 0           (my $i = shift) =~ s/\!//o;
341 0           push @skipStreams, $i;
342             } else {
343 0           push @okStreams, shift;
344             }
345             }
346 0 0         @okStreams = ('any') unless (defined $okStreams[0]); # one of: {secure_pls | pls | stw}
347              
348 0           my $self = {};
349              
350 0 0         return undef unless ($url);
351              
352 0           my $html = '';
353 0           my $wait = 1;
354 0           for (my $i=0; $i<=2; $i++) { #WE TRY THIS FETCH 3 TIMES SINCE FOR SOME REASON, DOESN'T ALWAYS RETURN RESULTS 1ST TIME?!:
355 0           $html = LWP::Simple::get($url);
356 0 0         last if ($html);
357 0           sleep $wait;
358 0           ++$wait;
359             }
360 0 0         return undef unless ($html); #STEP 1 FAILED, INVALID STATION URL, PUNT!
361              
362 0           my $html2 = '';
363 0 0         my $streamhtml0 = ($html =~ /\"streams\"\s*\:\s*\{([^\}]+)\}/) ? $1 : '';
364 0           my $streamhtml;
365 0 0         return undef unless ($streamhtml0);
366              
367 0           my $streampattern;
368              
369             # OUTTERMOST LOOP TO TRY EACH STREAM-TYPE: (CONSTRAINED IF USER SPECIFIES STREAMTYPES AS EXTRA ARGS TO new())
370 0           foreach my $streamtype (@okStreams) {
371 0           $streamhtml = $streamhtml0;
372 0           $streampattern = $streamtype;
373 0 0         if ($streamtype eq 'secure') {
    0          
374 0           $streampattern = '\"secure_\w+';
375             } elsif ($streamtype eq 'any') {
376 0           $streampattern = '\"\w+';
377             } else {
378 0           $streampattern = '\"' . $streamtype;
379             }
380 0           $self->{'cnt'} = 0;
381 0 0         $self->{'id'} = ($html =~ m#\"id\"\s*\:\s*([^\,\s]+)#) ? $1 : '';
382 0 0 0       $self->{'id'} = $1 if (!$self->{'id'} && ($url =~ m#\/([^\/]+)\/?$#));
383 0 0         $self->{'fccid'} = ($html =~ m#\"callLetters\"\s*\:\s*\"([^\"]+)\"#i) ? $1 : '';
384 0 0         $self->{'title'} = ($html =~ m#\"description\"\s*\:\s*\"([^\"]+)\"#) ? $1 : $url;
385 0           $self->{'title'} =~ s#http[s]?\:\/\/www\.iheart\.com\/live\/##;
386 0 0         $self->{'imageurl'} = ($html =~ m#\"image_src\"\s+href=\"([^\"]+)\"#) ? $1 : '';
387 0           $self->{'iconurl'} = $self->{'imageurl'} . '?ops=fit(100%2C100)';
388             # INNER LOOP: MATCH STREAM URLS BY TYPE PATTEREN REGEX UNTIL WE FIND ONE THAT'S ACCEPTABLE (NOT EXCLUDED TYPE):
389 0           INNER: while ($streamhtml =~ s#(${streampattern}_stream)\"\s*\:\s*\"([^\"]+)\"##)
390             {
391 0           $self->{'streamtype'} = substr($1, 1);
392 0           $self->{'streamurl'} = $2;
393 0           foreach my $xp (@skipStreams) {
394 0 0         next INNER if ($self->{'streamtype'} =~ /$xp/); #REJECTED STREAM-TYPE.
395             }
396              
397             # WE NOW HAVE A STREAM THAT MATCHES OUR CONSTRAINTS:
398 0 0         $self->{'cnt'} = 1 if ($self->{'streamurl'});
399              
400             # IF IT'S A ".pls" (PLAYLIST) STREAM, WE NEED TO FETCH THE LIST OF ACTUAL STREAMS:
401             # streamurl WILL STILL CONTAIN THE PLAYLIST STREAM ITSELF!
402 0 0 0       if ($self->{'cnt'} && $self->{'streamtype'} =~ /pls/) {
403 0           my @streams;
404 0 0         $self->{'plsid'} = $1 if ($self->{'streamurl'} =~ m#\/([^\/]+)\.pls$#i);
405 0           for (my $i=0; $i<=2; $i++) { #WE TRY THIS FETCH 3 TIMES SINCE FOR SOME REASON, DOESN'T ALWAYS RETURN RESULTS 1ST TIME?!:
406 0           $html2 = LWP::Simple::get($self->{'streamurl'});
407 0 0         last if ($html);
408 0           sleep $wait;
409 0           ++$wait;
410             }
411 0           $self->{'cnt'} = 0;
412 0           while ($html2 =~ s#File\d+\=(\S+)##) {
413 0           push @streams, $1;
414 0           ++$self->{'cnt'};
415             }
416 0           $self->{'streams'} = \@streams; #WE'LL HAVE A LIST OF 'EM TO RANDOMLY CHOOSE ONE FROM:
417             }
418             else #NON-pls STREAM, WE'LL HAVE A LIST CONTAINING A SINGLE STREAM:
419             {
420 0           $self->{'streams'} = [$self->{'streamurl'}];
421             }
422 0 0         return undef unless ($self->{'cnt'}); #STEP 2 FAILED - NO PLAYABLE STREAMS FOUND, PUNT!
423              
424             #SAVE WHAT PROPERTY NAMES WE HAVE (FOR $station->validFields()):
425            
426 0           @{$self->{fields}} = ();
  0            
427 0           foreach my $field (sort keys %{$self}) {
  0            
428 0 0         next if ($field =~ /fields/o);
429 0           push @{$self->{fields}}, $field;
  0            
430             }
431              
432 0           bless $self, $class; #BLESS IT!
433              
434 0           return $self;
435             }
436             }
437 0           return undef;
438             }
439              
440             sub get
441             {
442 0     0 1   my $self = shift;
443 0   0       my $field = shift || 0;
444              
445 0           my @streams = ();
446 0           my $subcnt;
447 0 0         if ($field) { #USER SUPPLIED A PROPERTY NAME, FETCH ONLY THAT PROPERTY, (ie. "Url"):
448 0 0         if ($field eq 'stream') {
    0          
449 0 0         return wantarray ? @{$self->{'streams'}} : ${$self->{'streams'}}[int rand scalar @{$self->{'streams'}}];
  0            
  0            
  0            
450             } elsif ($field eq 'streams') {
451 0 0         return wantarray ? @{$self->{$field}} : $self->{$field};
  0            
452             } else {
453 0 0         return wantarray ? ($self->{$field}) : $self->{$field};
454             }
455             } else { #NO PROPERTY NAME, RETURN A HASH-REF TO ALL THE PROPERTIES:
456 0           return $self;
457             }
458             }
459              
460             sub count
461             {
462 0     0 1   my $self = shift;
463 0           return $self->{'cnt'}; #TOTAL NUMBER OF PLAYABLE STREAM URLS FOUND.
464             }
465              
466             sub validFields
467             {
468 0     0 1   my $self = shift;
469 0           return @{$self->{'fields'}}; #LIST OF ALL VALID PROPERTY NAME FIELDS.
  0            
470             }
471              
472             sub getStationID
473             {
474 0     0 1   my $self = shift;
475 0           return $self->{'id'}; #URL TO THE STATION'S THUMBNAIL ICON, IF ANY.
476             }
477              
478             sub getPlsID
479             {
480 0     0 0   my $self = shift;
481 0           return $self->{'plsid'}; #URL TO THE STATION'S THUMBNAIL ICON, IF ANY.
482             }
483              
484             sub getFccID
485             {
486 0     0 0   my $self = shift;
487 0           return $self->{'fccid'}; #URL TO THE STATION'S THUMBNAIL ICON, IF ANY.
488             }
489              
490             sub getStationTitle
491             {
492 0     0 1   my $self = shift;
493 0           return $self->{'title'}; #URL TO THE STATION'S TITLE(DESCRIPTION), IF ANY.
494             }
495              
496             sub getStream
497             {
498 0     0 1   my $self = $_[0];
499 0 0         my $streamNumber = defined($_[1]) ? $_[1] : int rand scalar @{$self->{'streams'}};
  0            
500 0 0         $streamNumber = $#{$self->{'streams'}} if ($streamNumber > $#{$self->{'streams'}});
  0            
  0            
501 0 0         $streamNumber = scalar(@{$self->{'streams'}}) + $streamNumber if ($streamNumber < 0);
  0            
502 0 0         $streamNumber = 0 if ($streamNumber < 0);
503 0           return ${$self->{'streams'}}[$streamNumber]; #URL TO RANDOM PLAYABLE STREAM.
  0            
504             }
505              
506             sub getIconURL
507             {
508 0     0 1   my $self = shift;
509 0           return $self->{'iconurl'}; #URL TO THE STATION'S THUMBNAIL ICON, IF ANY.
510             }
511              
512             sub getImageURL
513             {
514 0     0 1   my $self = shift;
515 0           return $self->{'imageurl'}; #URL TO THE STATION'S BANNER IMAGE, IF ANY.
516             }
517              
518             1