File Coverage

blib/lib/StreamFinder/Apple.pm
Criterion Covered Total %
statement 18 265 6.7
branch 0 170 0.0
condition 0 39 0.0
subroutine 6 17 35.2
pod 11 11 100.0
total 35 502 6.9


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             StreamFinder::Apple - Fetch actual raw streamable URLs from Apple
4             podcasts on podcasts.apple.com
5              
6             =head1 AUTHOR
7              
8             This module is Copyright (C) 2017-2019 by
9              
10             Jim Turner, C<< >>
11            
12             Email: turnerjw784@yahoo.com
13              
14             All rights reserved.
15              
16             You may distribute this module under the terms of either the GNU General
17             Public License or the Artistic License, as specified in the Perl README
18             file.
19              
20             =head1 SYNOPSIS
21              
22             use strict;
23              
24             use StreamFinder::Apple;
25              
26             my $podcast = new StreamFinder::Apple();
27              
28             die "Invalid URL or no streams found!\n" unless ($podcast);
29              
30             my $firstStream = $podcast->get();
31              
32             print "First Stream URL=$firstStream\n";
33              
34             my $url = $podcast->getURL();
35              
36             print "Stream URL=$url\n";
37              
38             my $podcastTitle = $podcast->getTitle();
39            
40             print "Title=$podcastTitle\n";
41            
42             my $podcastDescription = $podcast->getTitle('desc');
43            
44             print "Description=$podcastDescription\n";
45            
46             my $podcastID = $podcast->getID();
47              
48             print "PODCAST ID=$podcastID\n";
49            
50             my $artist = $podcast->{'artist'};
51              
52             print "Artist=$artist\n" if ($artist);
53            
54             my $genre = $podcast->{'genre'};
55              
56             print "Genre=$genre\n" if ($genre);
57            
58             my $icon_url = $podcast->getIconURL();
59              
60             if ($icon_url) { #SAVE THE ICON TO A TEMP. FILE:
61              
62             my ($image_ext, $icon_image) = $podcast->getIconData();
63              
64             if ($icon_image && open IMGOUT, ">/tmp/${podcastID}.$image_ext") {
65              
66             binmode IMGOUT;
67              
68             print IMGOUT $icon_image;
69              
70             close IMGOUT;
71              
72             }
73              
74             }
75              
76             my $stream_count = $podcast->count();
77              
78             print "--Stream count=$stream_count=\n";
79              
80             my @streams = $podcast->get();
81              
82             foreach my $s (@streams) {
83              
84             print "------ stream URL=$s=\n";
85              
86             }
87              
88             =head1 DESCRIPTION
89              
90             StreamFinder::Apple accepts a valid podcast or episode URL on
91             podcasts.apple.com, or an album or song (free sample clips only) from
92             music.apple.com, and returns the actual stream URL(s), title, and cover
93             art icon for that podcast. The purpose is that one needs one of these URLs
94             in order to have the option to stream the station in one's own choice of
95             media player software rather than using their web browser and accepting any /
96             all flash, ads, javascript, cookies, trackers, web-bugs, and other crapware
97             that can come with that method of playing. The author uses his own custom
98             all-purpose media player called "fauxdacious" (his custom hacked version of
99             the open-source "audacious" audio player. "fauxdacious" incorporates this
100             module to decode and play podcasts.apple.com streams.
101              
102             NOTE: The URL must be either a podcast site, format:
103             https://podcasts.apple.com/I/podcast/idB
104             (returns stream(s) for all "episodes" for that site, OR a specific podcast /
105             "episode" page site, format:
106             https://podcasts.apple.com/I/podcast/idB?i=B
107             (returns a single stream for that specific podcast). Music samples also
108             seem to work using the format (with or without the ?i=B part):
109             https://music.apple.com/I/album/I/idB?i=B
110              
111             =head1 SUBROUTINES/METHODS
112              
113             =over 4
114              
115             =item B(I [, "debug" [ => 0|1|2 ]])
116              
117             Accepts a podcasts.apple.com ID or URL or music.apple.com URL and creates and
118             returns a new podcast object, or I if the URL is not a valid podcast,
119             album, etc. or no streams are found. The URL can be the full URL,
120             ie. https://podcasts.apple.com/podcast/idI,
121             https://podcasts.apple.com/podcast/idB?i=B, or just
122             I, or I/I. NOTE: If the ID is an album
123             or song clip, then the full URL must be given, ie. http://music.apple.com/...
124              
125             =item $podcast->B()
126              
127             Returns an array of strings representing all stream urls found.
128              
129             =item $podcast->B([I])
130              
131             Similar to B() except it only returns a single stream representing
132             the first valid stream found.
133              
134             Current options are: I<"random"> and I<"noplaylists">. By default, the
135             first ("best"?) stream is returned. If I<"random"> is specified, then
136             a random one is selected from the list of streams found.
137             If I<"noplaylists"> is specified, and the stream to be returned is a
138             "playlist" (.pls or .m3u? extension), it is first fetched and the first entry
139             in the playlist is returned. This is needed by Fauxdacious Mediaplayer.
140              
141             =item $podcast->B()
142              
143             Returns the number of streams found for the podcast / episode / album / song.
144             Episodes and songs usually return 1, whereas podcasts and albums usually 1
145             for each episode / sample song clip in the podcast / album respectively.
146              
147             =item $podcast->B()
148              
149             Returns the station's Apple ID (numeric). For podcasts and albums, this
150             is a single numeric value. For episodes and songs, it's two numbers
151             separated by a slash ("/").
152              
153             =item $podcast->B(['desc'])
154              
155             Returns the podcast's, album's, episode's or song clip's title,
156             or (long description).
157              
158             =item $podcast->B()
159              
160             Returns the url for the podcast's / album's "cover art" icon image,
161             if any.
162              
163             =item $podcast->B()
164              
165             Returns a two-element array consisting of the extension (ie. "png",
166             "gif", "jpeg", etc.) and the actual icon image (binary data), if any.
167              
168             =item $podcast->B()
169              
170             Returns the url for the podcast's / album's "cover art" banner image,
171             which for Apple is always the icon image, as Apple does not support
172             a separate banner image at this time.
173              
174             =item $podcast->B()
175              
176             Returns a two-element array consisting of the extension (ie. "png",
177             "gif", "jpeg", etc.) and the actual station's banner image (binary data).
178              
179             =item $podcast->B()
180              
181             Returns the station's type ("Apple").
182              
183             =back
184              
185             =head1 CONFIGURATION FILES
186              
187             =over 4
188              
189             =item ~/.config/StreamFinder/Apple/config
190              
191             Optional text file for specifying various configuration options
192             for a specific site (submodule). Each option is specified on a
193             separate line in the format below:
194              
195             'option' => 'value' [,]
196              
197             and the options are loaded into a hash used only by the specific
198             (submodule) specified. Valid options include
199             I<-debug> => [0|1|2], and most of the L options.
200             Blank lines and lines starting with a "#" sign are ignored.
201              
202             Options specified here override any specified in I<~/.config/StreamFinder/config>.
203              
204             =item ~/.config/StreamFinder/config
205              
206             Optional text file for specifying various configuration options.
207             Each option is specified on a separate line in the format below:
208              
209             'option' => 'value' [,]
210              
211             and the options are loaded into a hash used by all sites
212             (submodules) that support them. Valid options include
213             I<-debug> => [0|1|2], and most of the L options.
214              
215             =back
216              
217             NOTE: Options specified in the options parameter list will override
218             those corresponding options specified in these files.
219              
220             =head1 KEYWORDS
221              
222             apple podcasts
223              
224             =head1 DEPENDENCIES
225              
226             L, L, L
227              
228             =head1 RECCOMENDS
229              
230             wget
231              
232             =head1 BUGS
233              
234             Please report any bugs or feature requests to C, or through
235             the web interface at L. I will be notified, and then you'll
236             automatically be notified of progress on your bug as I make changes.
237              
238             =head1 SUPPORT
239              
240             You can find documentation for this module with the perldoc command.
241              
242             perldoc StreamFinder::Apple
243              
244             You can also look for information at:
245              
246             =over 4
247              
248             =item * RT: CPAN's request tracker (report bugs here)
249              
250             L
251              
252             =item * AnnoCPAN: Annotated CPAN documentation
253              
254             L
255              
256             =item * CPAN Ratings
257              
258             L
259              
260             =item * Search CPAN
261              
262             L
263              
264             =back
265              
266             =head1 LICENSE AND COPYRIGHT
267              
268             Copyright 2017-2019 Jim Turner.
269              
270             This program is free software; you can redistribute it and/or modify it
271             under the terms of the the Artistic License (2.0). You may obtain a
272             copy of the full license at:
273              
274             L
275              
276             Any use, modification, and distribution of the Standard or Modified
277             Versions is governed by this Artistic License. By using, modifying or
278             distributing the Package, you accept this license. Do not use, modify,
279             or distribute the Package, if you do not accept this license.
280              
281             If your Modified Version has been derived from a Modified Version made
282             by someone other than you, you are nevertheless required to ensure that
283             your Modified Version complies with the requirements of this license.
284              
285             This license does not grant you the right to use any trademark, service
286             mark, tradename, or logo of the Copyright Holder.
287              
288             This license includes the non-exclusive, worldwide, free-of-charge
289             patent license to make, have made, use, offer to sell, sell, import and
290             otherwise transfer the Package with respect to any patent claims
291             licensable by the Copyright Holder that are necessarily infringed by the
292             Package. If you institute patent litigation (including a cross-claim or
293             counterclaim) against any party alleging that the Package constitutes
294             direct or contributory patent infringement, then this Artistic License
295             to you shall terminate on the date that such litigation is filed.
296              
297             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
298             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
299             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
300             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
301             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
302             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
303             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
304             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
305              
306             =cut
307              
308             package StreamFinder::Apple;
309              
310 1     1   6 use strict;
  1         1  
  1         24  
311 1     1   4 use warnings;
  1         2  
  1         19  
312 1     1   418 use URI::Escape;
  1         1201  
  1         57  
313 1     1   441 use HTML::Entities ();
  1         4980  
  1         23  
314 1     1   740 use LWP::UserAgent ();
  1         41396  
  1         40  
315 1     1   7 use vars qw(@ISA @EXPORT);
  1         2  
  1         3595  
316              
317             my $DEBUG = 0;
318             my %uops = ();
319             my @userAgentOps = ();
320              
321             require Exporter;
322              
323             @ISA = qw(Exporter);
324             @EXPORT = qw(get getURL getType getID getTitle getIconURL getIconData getImageURL getImageData);
325              
326             sub new
327             {
328 0     0 1   my $class = shift;
329 0           my $url = shift;
330              
331 0           my $self = {};
332 0 0         return undef unless ($url);
333              
334 0           foreach my $p ("$ENV{HOME}/.config/StreamFinder/config", "$ENV{HOME}/.config/StreamFinder/Apple/config") {
335 0 0         if (open IN, $p) {
336 0           my ($atr, $val);
337 0           while () {
338 0           chomp;
339 0 0         next if (/^\s*\#/o);
340 0           ($atr, $val) = split(/\s*\=\>\s*/o, $_, 2);
341 0           eval "\$uops{$atr} = $val";
342             }
343 0           close IN;
344             }
345             }
346 0           foreach my $i (qw(agent from conn_cache default_headers local_address ssl_opts max_size
347             max_redirect parse_head protocols_allowed protocols_forbidden requests_redirectable
348             proxy no_proxy)) {
349 0 0         push @userAgentOps, $i, $uops{$i} if (defined $uops{$i});
350             }
351             push (@userAgentOps, 'agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0')
352 0 0         unless (defined $uops{'agent'});
353 0 0         $uops{'timeout'} = 10 unless (defined $uops{'timeout'});
354 0 0         $DEBUG = $uops{'debug'} if (defined $uops{'debug'});
355              
356 0           while (@_) {
357 0 0         if ($_[0] =~ /^\-?debug$/o) {
358 0           shift;
359 0 0 0       $DEBUG = (defined($_[0]) && $_[0] =~/^[0-9]$/) ? shift : 1;
360             }
361             }
362              
363 0           $self->{'id'} = '';
364 0           (my $url2fetch = $url);
365 0 0         if ($url2fetch =~ m#^https?\:\/\/(?:podcasts|music)\.apple\.#) {
    0          
366             #EXAMPLE1:my $url = 'https://podcasts.apple.com/us/podcast/wnbc-sec-shorts-josh-snead/id1440412195?i=1000448441439';
367             #EXAMPLE2:my $url = 'https://podcasts.apple.com/us/podcast/good-bull-hunting-for-texas-a-m-fans/id1440412195';
368             #EXAMPLE3:my $url = 'https://music.apple.com/us/album/big-legged-woman/723550112';
369 0 0         $self->{'id'} = ($url =~ m#\/(?:id)?(\d+)(?:\?i\=(\d+))?\/?#) ? $1 : '';
370 0 0         $self->{'id'} .= '/'. $2 if (defined $2);
371             } elsif ($url2fetch !~ m#^https?\:\/\/#) {
372 0           my ($id, $podcastid) = split(m#\/#, $url2fetch);
373 0           $self->{'id'} = $id;
374 0           $url2fetch = 'https://podcasts.apple.com/podcast/id' . $id;
375 0 0         $url2fetch .= '?i=' . $podcastid if ($podcastid);
376             }
377              
378 0 0         print STDERR "--URL=$url2fetch= ID=".$self->{'id'}."=\n" if ($DEBUG);
379 0 0         return undef unless ($self->{'id'});
380              
381 0           my $html = '';
382 0 0         print STDERR "-0(Apple): ID=".$self->{'id'}."= AGENT=".join('|',@userAgentOps)."=\n" if ($DEBUG);
383 0           my $ua = LWP::UserAgent->new(@userAgentOps);
384 0           $ua->timeout($uops{'timeout'});
385 0           $ua->cookie_jar({});
386 0           $ua->env_proxy;
387 0 0         print STDERR "i:FETCHING URL ($url2fetch)...\n" if ($DEBUG);
388 0           my $response = $ua->get($url2fetch);
389 0 0         if ($response->is_success) {
390 0           $html = $response->decoded_content;
391             } else {
392 0 0         print STDERR $response->status_line if ($DEBUG);
393 0           my $no_wget = system('wget','-V');
394 0 0         unless ($no_wget) {
395 0 0         print STDERR "\n..trying wget...\n" if ($DEBUG);
396 0           $html = `wget -t 2 -T 20 -O- -o /dev/null \"$url2fetch\" 2>/dev/null `;
397             }
398             }
399              
400 0 0         print STDERR "-1: html=$html=\n" if ($DEBUG > 1);
401 0 0         return undef unless ($html);
402              
403 0           $self->{'title'} = '';
404 0           $self->{'artist'} = '';
405 0           $self->{'created'} = '';
406 0           $self->{'year'} = '';
407 0           $self->{'streams'} = [];
408 0           $self->{'cnt'} = 0;
409 0           $self->{'Url'} = '';
410 0           my ($pre, $post) = split(/\"included\"\:/, $html, 2);
411 0           $html = '';
412 0 0 0       return undef unless ($pre && $post);
413 0 0         $self->{'iconurl'} = ($pre =~ m#\
414 0           $self->{'imageurl'} = $self->{'iconurl'};
415 0 0         if ($pre =~ m#\#s) {
416 0           my $span = $1;
417 0 0         $self->{'artist'} = $1 if ($span =~ m#\"\>\s*([^\<]+)\<\/#s);
418 0           $self->{'artist'} =~ s/\s+$//;
419             }
420 0 0         if ($pre =~ m#\"assetUrl\"\:\"([^\"]+)\"#s) { #INVIDUAL EPISODE:
421 0 0         print STDERR "---EPISODE---\n" if ($DEBUG);
422 0           $self->{'streams'}->[0] = $1;
423 0           my $rest = $2;
424 0 0         $self->{'title'} = $1 if ($pre =~ m#\"mediaKind\"\:\"[^\"]*\"\,\"name\"\:\"([^\"]+)\"#s);
425 0 0         $self->{'description'} = $1 if ($pre =~ m#\"description\"\:\{\"standard\"\:\"([^\"]+)\"#s);
426 0 0 0       $self->{'description'} ||= $1 if ($pre =~ m#\"short\"\:\"([^\"]+)\"#s);
427 0 0         $self->{'created'} = $1 if ($pre =~ m#\"datePublished\"\:\"([^\"]+)#s);
428             } else { #PAGE (multiple episodes):
429 0 0         print STDERR "---PAGE (multiple episodes)---\n" if ($DEBUG);
430 0 0         if ($pre =~ m#type\=\".*?json\"\>([^\<]+)#) {
431 0           my $json = $1;
432 0 0         $self->{'title'} = $1 if ($json =~ m#\"name\"\:\"([^\"]+)\"#s);
433 0 0         $self->{'description'} = $1 if ($json =~ m#\"description\"\:\"([^\"]+)\"#s);
434 0 0         $self->{'created'} = $1 if ($json =~ m#\"datePublished\"\:\"([^\"]+)\"#s);
435             }
436 0           while ($post =~ s#\"assetUrl\"\:\"([^\"]+)\"##s) {
437 0           push @{$self->{'streams'}}, $1;
  0            
438             }
439 0 0         if ($url2fetch =~ m#^https?\:\/\/music#) {
440 0 0         if ($self->{'id'} =~ m#(\d+)\/(\d+)$#) {
441 0           my ($albumID, $episodeID) = ($1, $2);
442 0 0 0       if (!defined($self->{'album'}) && $pre =~ m#\/${albumID}\?i\=${episodeID}\"\,\"name\"\:\"([^\"]+)\"#s) {
443 0           $self->{'album'} = $self->{'title'};
444 0           $self->{'title'} = $1;
445 0           $self->{'album'} = HTML::Entities::decode_entities($self->{'album'});
446 0           $self->{'album'} = uri_unescape($self->{'album'});
447 0           $self->{'album'} =~ s/(?:\%|\\?u?00)([0-9A-Fa-f]{2})/chr(hex($1))/egs;
  0            
448             }
449 0           while ($post =~ s#\{\"duration\"\:\d+\,\"url\"\:\"([^\"]+)\".*?salableAdamId\=${episodeID}\D##s) { #WILL HANDLE iTUNES MUSICK *SAMPLES* FROM (https://music.apple.com/us/album/*):
450 0           push @{$self->{'streams'}}, $1;
  0            
451             }
452             } else {
453 0           while ($post =~ s#\{\"duration\"\:\d+\,\"url\"\:\"([^\"]+)\"##s) { #WILL HANDLE iTUNES MUSICK *SAMPLES* FROM (https://music.apple.com/us/album/*):
454 0           push @{$self->{'streams'}}, $1;
  0            
455             }
456             }
457 0           $self->{'title'} .= ' (-SAMPLE-)';
458             }
459             }
460 0 0         $self->{'year'} = $1 if ($self->{'created'} =~ /(\d\d\d\d)/);
461 0 0         if ($pre =~ m#\(.*)?\<\/ul\>#s) {
462 0           my $prodlistitemdata = $1;
463 0 0         $self->{'genre'} = $1 if ($prodlistitemdata =~ s#\"\>\s*([^\<]+)\<\/##s);
464 0           $self->{'genre'} =~ s/\s+$//;
465 0           $self->{'genre'} = HTML::Entities::decode_entities($self->{'genre'});
466 0           $self->{'genre'} = uri_unescape($self->{'genre'});
467 0           $self->{'genre'} =~ s/(?:\%|\\?u?00)([0-9A-Fa-f]{2})/chr(hex($1))/egs;
  0            
468 0 0         $self->{'year'} = $1 if ($prodlistitemdata =~ m#\>([\d]+)\D*\<\/time\>#s);
469             }
470 0           $self->{'cnt'} = scalar @{$self->{'streams'}};
  0            
471 0 0         $self->{'Url'} = ($self->{'cnt'} > 0) ? $self->{'streams'}->[0] : '';
472 0           $self->{'total'} = $self->{'cnt'};
473 0           $self->{'imageurl'} = $self->{'iconurl'};
474 0 0         if ($self->{'description'} =~ /\w/) {
475 0           $self->{'description'} =~ s/\s+$//;
476             } else {
477 0           $self->{'description'} = $self->{'title'};
478             }
479 0           foreach my $i (qw(title artist description)) {
480 0           $self->{$i} = HTML::Entities::decode_entities($self->{$i});
481 0           $self->{$i} = uri_unescape($self->{$i});
482 0           $self->{$i} =~ s/(?:\%|\\?u?00)([0-9A-Fa-f]{2})/chr(hex($1))/egs;
  0            
483             }
484 0 0         print STDERR "-SUCCESS: 1st stream=".${$self->{'streams'}}[0]."=\n" if ($DEBUG);
  0            
485             #print STDERR "\n***** --ID=".$self->{'id'}."=\n--CNT=".$self->{'cnt'}."=\n--TITLE=".$self->{'title'}."=\n--ARTIST=".$self->{'artist'}."=\n--GENRE=".$self->{'genre'}."=\n--ALBUM=".$self->{'album'}."=\n--YEAR=".$self->{'year'}."=\n--ICON=".$self->{'iconurl'}."=\n--1ST=".$self->{'Url'}."=\n*****\n" if ($DEBUG);
486              
487 0           bless $self, $class; #BLESS IT!
488              
489 0           return $self;
490             }
491              
492             sub get
493             {
494 0     0 1   my $self = shift;
495              
496 0 0         return wantarray ? @{$self->{'streams'}} : ${$self->{'streams'}}[0];
  0            
  0            
497             }
498              
499             sub getURL #LIKE GET, BUT ONLY RANDOMLY SELECT ONE TO RETURN:
500             {
501 0     0 1   my $self = shift;
502 0 0         my $arglist = (defined $_[0]) ? join('|',@_) : '';
503 0 0         my $idx = ($arglist =~ /\b\-?random\b/) ? int rand scalar @{$self->{'streams'}} : 0;
  0            
504 0 0 0       if ($arglist =~ /\b\-?noplaylists\b/ && ${$self->{'streams'}}[$idx] =~ /\.(pls|m3u8?)$/i) {
  0            
505 0           my $plType = $1;
506 0           my $firstStream = ${$self->{'streams'}}[$idx];
  0            
507 0 0         print STDERR "-getURL($idx): NOPLAYLISTS and (".${$self->{'streams'}}[$idx].")\n" if ($DEBUG);
  0            
508 0           my $ua = LWP::UserAgent->new(@userAgentOps);
509 0 0 0       if ($firstStream =~ /\breciva\b/ && defined $self->{'_reciva_ssl_opts'}) {
510 0           foreach my $i (keys %{$self->{'_reciva_ssl_opts'}}) {
  0            
511 0           $ua->ssl_opts($i, $self->{'_default_ssl_opts'}->{$i});
512 0 0         print STDERR "--SSL OPTS SET2 ($i) BACK TO (".$self->{'_default_ssl_opts'}->{$i}.")!\n" if ($DEBUG);
513             }
514             }
515 0           $ua->timeout($uops{'timeout'});
516 0           $ua->cookie_jar({});
517 0           $ua->env_proxy;
518 0           my $html = '';
519 0           my $response = $ua->get($firstStream);
520 0 0         if ($response->is_success) {
521 0           $html = $response->decoded_content;
522             } else {
523 0 0         print STDERR $response->status_line if ($DEBUG);
524 0           my $no_wget = system('wget','-V');
525 0 0         unless ($no_wget) {
526 0 0         print STDERR "\n..trying wget...\n" if ($DEBUG);
527 0           $html = `wget -t 2 -T 20 -O- -o /dev/null \"$firstStream\" 2>/dev/null `;
528             }
529             }
530 0           my @lines = split(/\r?\n/, $html);
531 0           $firstStream = '';
532 0 0         if ($plType =~ /pls/) { #PLS:
533 0           my $firstTitle = '';
534 0           foreach my $line (@lines) {
535 0 0         if ($line =~ m#^\s*File\d+\=(.+)$#) {
    0          
536 0   0       $firstStream ||= $1;
537             } elsif ($line =~ m#^\s*Title\d+\=(.+)$#) {
538 0   0       $firstTitle ||= $1;
539             }
540             }
541 0   0       $self->{'title'} ||= $firstTitle;
542 0 0         print STDERR "-getURL(PLS): first=$firstStream= title=$firstTitle=\n" if ($DEBUG);
543             } else { #m3u8:
544 0           (my $urlpath = ${$self->{'streams'}}[$idx]) =~ s#[^\/]+$##;
  0            
545 0           foreach my $line (@lines) {
546 0 0         if ($line =~ m#^\s*([^\#].+)$#) {
547 0           my $urlpart = $1;
548 0           $urlpart =~ s#^\s+##;
549 0           $urlpart =~ s#^\/##;
550 0 0         $firstStream = ($urlpart =~ m#https?\:#) ? $urlpart : ($urlpath . '/' . $urlpart);
551 0           last;
552             }
553             }
554 0 0         print STDERR "-getURL(m3u?): first=$firstStream=\n" if ($DEBUG);
555             }
556 0   0       return $firstStream || ${$self->{'streams'}}[$idx];
557             }
558 0           return ${$self->{'streams'}}[$idx];
  0            
559             }
560              
561             sub count
562             {
563 0     0 1   my $self = shift;
564 0           return $self->{'total'}; #TOTAL NUMBER OF PLAYABLE STREAM URLS FOUND.
565             }
566              
567             sub getType
568             {
569 0     0 1   my $self = shift;
570 0           return 'Apple'; #STATION TYPE (FOR PARENT StreamFinder MODULE).
571             }
572              
573             sub getID
574             {
575 0     0 1   my $self = shift;
576 0           return $self->{'id'}; #STATION'S APPLE-ID.
577             }
578              
579             sub getTitle
580             {
581 0     0 1   my $self = shift;
582 0 0 0       return $self->{'description'} if (defined($_[0]) && $_[0] =~ /^\-?(?:long|desc)/i);
583 0           return $self->{'title'}; #STATION'S TITLE(DESCRIPTION), IF ANY.
584             }
585              
586             sub getIconURL
587             {
588 0     0 1   my $self = shift;
589 0           return $self->{'iconurl'}; #URL TO THE STATION'S THUMBNAIL ICON, IF ANY.
590             }
591              
592             sub getIconData
593             {
594 0     0 1   my $self = shift;
595 0 0         return () unless ($self->{'iconurl'});
596 0           my $ua = LWP::UserAgent->new(@userAgentOps);
597 0           $ua->timeout($uops{'timeout'});
598 0           $ua->cookie_jar({});
599 0           $ua->env_proxy;
600 0 0 0       if ($self->{'iconurl'} =~ /\breciva\b/ && defined $self->{'_reciva_ssl_opts'}) {
601 0           foreach my $i (keys %{$self->{'_reciva_ssl_opts'}}) {
  0            
602 0           $ua->ssl_opts($i, $self->{'_default_ssl_opts'}->{$i});
603 0 0         print STDERR "--SSL OPTS SET3 ($i) BACK TO (".$self->{'_default_ssl_opts'}->{$i}.")!\n" if ($DEBUG);
604             }
605             }
606 0           my $art_image = '';
607 0           my $response = $ua->get($self->{'iconurl'});
608 0 0         if ($response->is_success) {
609 0           $art_image = $response->decoded_content;
610             } else {
611 0 0         print STDERR $response->status_line if ($DEBUG);
612 0           my $no_wget = system('wget','-V');
613 0 0         unless ($no_wget) {
614 0 0         print STDERR "\n..trying wget...\n" if ($DEBUG);
615 0           my $iconUrl = $self->{'iconurl'};
616 0           $art_image = `wget -t 2 -T 20 -O- -o /dev/null \"$iconUrl\" 2>/dev/null `;
617             }
618             }
619 0 0         return () unless ($art_image);
620 0           (my $image_ext = $self->{'iconurl'}) =~ s/^.+\.//;
621 0           $image_ext =~ s/[^A-Za-z].*$//;
622 0           return ($image_ext, $art_image);
623             }
624              
625             sub getImageURL
626             {
627 0     0 1   my $self = shift;
628 0           return $self->{'imageurl'}; #URL TO THE STATION'S BANNER IMAGE, IF ANY.
629             }
630              
631             sub getImageData
632             {
633 0     0 1   my $self = shift;
634 0 0         return () unless ($self->{'imageurl'});
635 0           my $ua = LWP::UserAgent->new(@userAgentOps);
636 0           $ua->timeout($uops{'timeout'});
637 0           $ua->cookie_jar({});
638 0           $ua->env_proxy;
639 0 0 0       if ($self->{'imageurl'} =~ /\breciva\b/ && defined $self->{'_reciva_ssl_opts'}) {
640 0           foreach my $i (keys %{$self->{'_reciva_ssl_opts'}}) {
  0            
641 0           $ua->ssl_opts($i, $self->{'_default_ssl_opts'}->{$i});
642 0 0         print STDERR "--SSL OPTS SET4 ($i) BACK TO (".$self->{'_default_ssl_opts'}->{$i}.")!\n" if ($DEBUG);
643             }
644             }
645 0           my $art_image = '';
646 0           my $response = $ua->get($self->{'imageurl'});
647 0 0         if ($response->is_success) {
648 0           $art_image = $response->decoded_content;
649             } else {
650 0 0         print STDERR $response->status_line if ($DEBUG);
651 0           my $no_wget = system('wget','-V');
652 0 0         unless ($no_wget) {
653 0 0         print STDERR "\n..trying wget...\n" if ($DEBUG);
654 0           my $iconUrl = $self->{'iconurl'};
655 0           $art_image = `wget -t 2 -T 20 -O- -o /dev/null \"$iconUrl\" 2>/dev/null `;
656             }
657             }
658 0 0         return () unless ($art_image);
659 0           my $image_ext = $self->{'imageurl'};
660 0 0         $image_ext = ($self->{'imageurl'} =~ /\.(\w+)$/) ? $1 : 'png';
661 0           $image_ext =~ s/[^A-Za-z].*$//;
662 0           return ($image_ext, $art_image);
663             }
664              
665             1