File Coverage

blib/lib/WWW/YouTube/Download.pm
Criterion Covered Total %
statement 111 343 32.3
branch 29 182 15.9
condition 1 148 0.6
subroutine 23 45 51.1
pod 9 9 100.0
total 173 727 23.8


line stmt bran cond sub pod time code
1             package WWW::YouTube::Download;
2              
3 7     7   427897 use strict;
  7         67  
  7         190  
4 7     7   34 use warnings;
  7         10  
  7         166  
5 7     7   166 use 5.008001;
  7         22  
6              
7             our $VERSION = '0.65';
8              
9 7     7   43 use Carp qw(croak);
  7         10  
  7         306  
10 7     7   3434 use URI ();
  7         43162  
  7         162  
11 7     7   4236 use LWP::UserAgent;
  7         243199  
  7         290  
12 7     7   3540 use JSON::MaybeXS 'JSON';
  7         35995  
  7         406  
13 7     7   3121 use HTML::Entities qw/decode_entities/;
  7         35177  
  7         490  
14 7     7   52 use HTTP::Request;
  7         14  
  7         184  
15 7     7   2788 use MIME::Type;
  7         7864  
  7         285  
16              
17             $Carp::Internal{ (__PACKAGE__) }++;
18              
19 7     7   46 use constant DEFAULT_FMT => 18;
  7         16  
  7         1032  
20              
21             my $base_url = 'https://www.youtube.com/watch?v=';
22              
23             sub new {
24 32     32 1 19378 my $class = shift;
25 32         61 my %args = @_;
26             $args{ua} = LWP::UserAgent->new(
27             agent => __PACKAGE__.'/'.$VERSION,
28             parse_head => 0,
29 32 50       186 ) unless exists $args{ua};
30 32         5441 bless \%args, $class;
31             }
32              
33             for my $name (qw[video_id video_url title user fmt fmt_list suffix]) {
34             ## no critic (TestingAndDebugging::ProhibitNoStrict)
35 7     7   43 no strict 'refs';
  7         13  
  7         290  
36             *{"get_$name"} = sub {
37 7     7   33 use strict 'refs';
  7         12  
  7         37084  
38 0     0   0 my ($self, $video_id) = @_;
39 0 0       0 croak "Usage: $self->get_$name(\$video_id|\$watch_url)" unless $video_id;
40 0         0 my $data = $self->prepare_download($video_id);
41 0         0 return $data->{$name};
42             };
43             }
44              
45             sub playback_url {
46 0     0 1 0 my ($self, $video_id, $args) = @_;
47 0 0       0 croak "Usage: $self->playback_url('[video_id|video_url]')" unless $video_id;
48 0   0     0 $args ||= {};
49              
50 0         0 my $data = $self->prepare_download($video_id);
51 0   0     0 my $fmt = $args->{fmt} || $data->{fmt} || DEFAULT_FMT;
52 0   0     0 my $video_url = $data->{video_url_map}{$fmt}{url} || croak "this video does not offer format (fmt) $fmt";
53              
54 0         0 return $video_url;
55             }
56              
57             sub download {
58 0     0 1 0 my ($self, $video_id, $args) = @_;
59 0 0       0 croak "Usage: $self->download('[video_id|video_url]')" unless $video_id;
60 0   0     0 $args ||= {};
61              
62 0         0 my $data = $self->prepare_download($video_id);
63              
64 0   0     0 my $fmt = $args->{fmt} || $data->{fmt} || DEFAULT_FMT;
65              
66 0   0     0 my $video_url = $data->{video_url_map}{$fmt}{url} || croak "this video has not supported fmt: $fmt";
67 0   0     0 $args->{filename} ||= $args->{file_name};
68             my $filename = $self->_format_filename($args->{filename}, {
69             video_id => $data->{video_id},
70             title => $data->{title},
71             user => $data->{user},
72             fmt => $fmt,
73             suffix => $data->{video_url_map}{$fmt}{suffix} || _suffix($fmt),
74 0   0     0 resolution => $data->{video_url_map}{$fmt}{resolution} || '0x0',
      0        
75             });
76              
77             $args->{cb} = $self->_default_cb({
78             filename => $filename,
79             verbose => $args->{verbose},
80             progress => $args->{progress},
81             overwrite => defined $args->{overwrite} ? $args->{overwrite} : 1,
82 0 0       0 }) unless ref $args->{cb} eq 'CODE';
    0          
83              
84 0         0 my $res = $self->ua->get($video_url, ':content_cb' => $args->{cb});
85 0 0       0 croak "!! $video_id download failed: ", $res->status_line if $res->is_error;
86             }
87              
88             sub _format_filename {
89 0     0   0 my ($self, $filename, $data) = @_;
90 0 0       0 return "$data->{video_id}.$data->{suffix}" unless defined $filename;
91 0 0       0 $filename =~ s#{([^}]+)}#$data->{$1} || "{$1}"#eg;
  0         0  
92 0         0 return $filename;
93             }
94              
95             sub _is_supported_fmt {
96 0     0   0 my ($self, $video_id, $fmt) = @_;
97 0         0 my $data = $self->prepare_download($video_id);
98 0 0       0 defined($data->{video_url_map}{$fmt}{url}) ? 1 : 0;
99             }
100              
101             sub _progress {
102 0     0   0 my ($self, $args, $total) = @_;
103              
104 0 0       0 if (not defined $args->{_progress}) {
105 0 0       0 eval "require Term::ProgressBar" or return; ## no critic
106 0         0 $args->{_progress} = Term::ProgressBar->new( { count => $total, ETA => 'linear', remove => 0, fh => \*STDOUT } );
107 0 0       0 $args->{_progress}->minor( $total > 50_000_000 ? 1 : 0 );
108 0         0 $args->{_progress}->max_update_rate(1);
109              
110 0         0 $args->{_progress}->message("Total $total");
111             }
112              
113 0         0 return $args->{_progress};
114             }
115              
116             sub _default_cb {
117 0     0   0 my ($self, $args) = @_;
118 0         0 my ($file, $verbose, $overwrite, $progress) = @$args{qw/filename verbose overwrite progress/};
119              
120 0 0 0     0 croak "file exists! $file" if -f $file and !$overwrite;
121 0 0       0 open my $wfh, '>', $file or croak $file, " $!";
122 0         0 binmode $wfh;
123              
124 0 0       0 print "Downloading `$file`\n" if $verbose;
125             return sub {
126 0     0   0 my ($chunk, $res, $proto) = @_;
127 0         0 print $wfh $chunk; # write file
128              
129 0 0 0     0 if ($verbose || $self->{verbose}) {
130 0         0 my $size = tell $wfh;
131 0         0 my $total = $res->header('Content-Length');
132              
133 0 0       0 if ($progress) {
134 0 0       0 if (my $p = $self->_progress($args, $total)){
135 0         0 $p->update($size);
136 0         0 return;
137             }
138             else{
139 0         0 print "(You need Term::ProgressBar module to show progress bar with -P switch)\n";
140 0         0 $progress = 0;
141             }
142             }
143              
144 0         0 printf "%d/%d (%.2f%%)\r", $size, $total, $size / $total * 100;
145 0 0       0 print "\n" if $total == $size;
146             }
147 0         0 };
148             }
149              
150             sub prepare_download {
151 1     1 1 7 my ($self, $video_id) = @_;
152 1 50       4 croak "Usage: $self->prepare_download('[video_id|watch_url]')" unless $video_id;
153 1         4 $video_id = $self->video_id($video_id);
154              
155 1 50       5 return $self->{cache}{$video_id} if ref $self->{cache}{$video_id} eq 'HASH';
156              
157 1         3 my ($title, $user, $video_url_map);
158 1         3 my $content = $self->_get_content($video_id);
159 1 50       203 if ($self->_is_new($content)) {
160 1         5 my $args = $self->_get_args($content);
161 1         3 my $player_resp = JSON()->new->decode($args->{player_response});
162 1         524 $video_url_map = $self->_decode_player_response($player_resp);
163 1         11 $title = decode_entities $player_resp->{videoDetails}{title};
164 1         94 $user = decode_entities $player_resp->{videoDetails}{author};
165             } else {
166 0         0 $title = $self->_fetch_title($content);
167 0         0 $user = $self->_fetch_user($content);
168 0         0 $video_url_map = $self->_fetch_video_url_map($content);
169             }
170              
171 1         4 my $fmt_list = [];
172             my $sorted = [
173             map {
174 2         5 push @$fmt_list, $_->[0]->{fmt};
175 2         4 $_->[0]
176             } sort {
177 1         4 $b->[1] <=> $a->[1]
178             } map {
179 1         5 my $resolution = $_->{resolution};
  2         4  
180 2         12 $resolution =~ s/(\d+)x(\d+)/$1 * $2/e;
  2         8  
181 2         10 [ $_, $resolution ]
182             } values %$video_url_map,
183             ];
184              
185 1         3 my $hq_data = $sorted->[0];
186              
187             return $self->{cache}{$video_id} = {
188             video_id => $video_id,
189             video_url => $hq_data->{url},
190             title => $title,
191             user => $user,
192             video_url_map => $video_url_map,
193             fmt => $hq_data->{fmt},
194             fmt_list => $fmt_list,
195             suffix => $hq_data->{suffix},
196             resolution => $hq_data->{resolution},
197 1         25 };
198             }
199              
200             sub _mime_to_suffix {
201 2     2   12 (my $mime = shift) =~ s{^([^;]+);.*}{$1};
202 2         14 my $stype = MIME::Type->new(type => $mime)->subType;
203 2 0       131 return $stype eq 'webm' ? 'webm'
    50          
    50          
204             : $stype eq 'mp4' ? 'mp4'
205             : $stype eq '3gp' ? '3gp'
206             : 'flv'
207             ;
208             }
209              
210             sub _decode_player_response {
211 1     1   4 my ($self, $player_data) = @_;
212             # need to decode $player_data->{streamingData}{formats};
213 1         2 my $fmt_map = { map { $_->{mimeType} => join 'x', $_->{width}, $_->{height} } @{$player_data->{streamingData}{formats}} };
  2         13  
  1         3  
214 1         2 my $fmt_url_map = { map { $_->{mimeType} => $_->{url} } @{$player_data->{streamingData}{formats}} };
  2         5  
  1         3  
215             # more formats in $player_data->{streamingData}{adaptiveFormats};
216             return +{
217             map {
218 2         15 $_->{fmt} => $_,
219             } map +{
220             fmt => $_,
221             resolution => $fmt_map->{$_},
222 1         7 url => $fmt_url_map->{$_},
223             suffix => _mime_to_suffix($_),
224             }, keys %$fmt_map
225             };
226             }
227              
228             sub _fetch_title {
229 0     0   0 my ($self, $content) = @_;
230              
231 0 0       0 my ($title) = $content =~ // or return;
232 0         0 return decode_entities($title);
233             }
234              
235             sub _fetch_user {
236 1     1   10 my ($self, $content) = @_;
237              
238 1 50       8 if( $content =~ /     0          
239 1         9 return decode_entities($1);
240             }elsif( $content =~ /","author":"([^"]+)","/ ){
241 0         0 return decode_entities($1);
242             }else{
243 0         0 return;
244             }
245             }
246              
247             sub _fetch_video_url_map {
248 0     0   0 my ($self, $content) = @_;
249              
250 0         0 my $args = $self->_get_args($content);
251 0 0 0     0 unless ($args->{fmt_list} and $args->{url_encoded_fmt_stream_map}) {
252 0         0 croak 'failed to find video urls';
253             }
254              
255 0         0 my $fmt_map = _parse_fmt_map($args->{fmt_list});
256 0         0 my $fmt_url_map = _parse_stream_map($args->{url_encoded_fmt_stream_map});
257              
258             my $video_url_map = +{
259             map {
260 0         0 $_->{fmt} => $_,
261             } map +{
262             fmt => $_,
263             resolution => $fmt_map->{$_},
264 0         0 url => $fmt_url_map->{$_},
265             suffix => _suffix($_),
266             }, keys %$fmt_map
267             };
268              
269 0         0 return $video_url_map;
270             }
271              
272             sub _get_content {
273 0     0   0 my ($self, $video_id) = @_;
274              
275 0         0 my $url = "$base_url$video_id";
276              
277 0         0 my $req = HTTP::Request->new;
278 0         0 $req->method('GET');
279 0         0 $req->uri($url);
280 0         0 $req->header('Accept-Language' => 'en-US');
281              
282 0         0 my $res = $self->ua->request($req);
283 0 0       0 croak "GET $url failed. status: ", $res->status_line if $res->is_error;
284              
285 0         0 return $res->content;
286             }
287              
288             sub _get_args {
289 2     2   5 my ($self, $content) = @_;
290              
291 2         3 my $data;
292 2         206 for my $line (split "\n", $content) {
293 2 50       7 next unless $line;
294 2 50       1337 if ($line =~ /the uploader has not made this video available in your country/i) {
    50          
295 0         0 croak 'Video not available in your country';
296             }
297             elsif ($line =~ /^.+ytplayer\.config\s*=\s*(\{.*})/) {
298 2         11 ($data, undef) = JSON->new->utf8(1)->decode_prefix($1);
299 2         727 last;
300             }
301             }
302              
303 2 50       10 croak 'failed to extract JSON data' unless $data->{args};
304              
305 2         7 return $data->{args};
306             }
307              
308             sub _is_new {
309 1     1   3 my ($self, $content) = @_;
310 1         3 my $args = $self->_get_args($content);
311 1 50 33     27 return 1 unless ($args->{fmt_list} and $args->{url_encoded_fmt_stream_map});
312 0         0 return 0;
313             }
314              
315             sub _parse_fmt_map {
316 0     0   0 my $param = shift;
317 0         0 my $fmt_map = {};
318 0         0 for my $stuff (split ',', $param) {
319 0         0 my ($fmt, $resolution) = split '/', $stuff;
320 0         0 $fmt_map->{$fmt} = $resolution;
321             }
322              
323 0         0 return $fmt_map;
324             }
325              
326             sub _sigdecode {
327 0     0   0 my @s = @_;
328              
329             # based on youtube_dl/extractor/youtube.py from yt-dl.org
330 0 0       0 if (@s == 93) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
331 0         0 return (reverse(@s[30..86]), $s[88], reverse(@s[6..28]));
332             } elsif (@s == 92) {
333 0         0 return ($s[25], @s[3..24], $s[0], @s[26..41], $s[79], @s[43..78], $s[91], @s[80..82]);
334             } elsif (@s == 91) {
335 0         0 return (reverse(@s[28..84]), $s[86], reverse(@s[6..26]));
336             } elsif (@s == 90) {
337 0         0 return ($s[25], @s[3..24], $s[2], @s[26..39], $s[77], @s[41..76], $s[89], @s[78..80]);
338             } elsif (@s == 89) {
339 0         0 return (reverse(@s[79..84]), $s[87], reverse(@s[61..77]), $s[0], reverse(@s[4..59]));
340             } elsif (@s == 88) {
341 0         0 return (@s[7..27], $s[87], @s[29..44], $s[55], @s[46..54], $s[2], @s[56..86], $s[28]);
342             } elsif (@s == 87) {
343 0         0 return (@s[6..26], $s[4], @s[28..38], $s[27], @s[40..58], $s[2], @s[60..86]);
344             } elsif (@s == 86) {
345 0         0 return (@s[4..30], $s[3], @s[32..84]);
346             } elsif (@s == 85) {
347 0         0 return (@s[3..10], $s[0], @s[12..54], $s[84], @s[56..83]);
348             } elsif (@s == 84) {
349 0         0 return (reverse(@s[71..78]), $s[14], reverse(@s[38..69]), $s[70], reverse(@s[15..36]), $s[80], reverse(@s[0..13]));
350             } elsif (@s == 83) {
351 0         0 return (reverse(@s[64..80]), $s[0], reverse(@s[1..62]), $s[63]);
352             } elsif (@s == 82) {
353 0         0 return (reverse(@s[38..80]), $s[7], reverse(@s[8..36]), $s[0], reverse(@s[1..6]), $s[37]);
354             } elsif (@s == 81) {
355 0         0 return ($s[56], reverse(@s[57..79]), $s[41], reverse(@s[42..55]), $s[80], reverse(@s[35..40]), $s[0], reverse(@s[30..33]), $s[34], reverse(@s[10..28]), $s[29], reverse(@s[1..8]), $s[9]);
356             } elsif (@s == 80) {
357 0         0 return (@s[1..18], $s[0], @s[20..67], $s[19], @s[69..79]);
358             } elsif (@s == 79) {
359 0         0 return ($s[54], reverse(@s[55..77]), $s[39], reverse(@s[40..53]), $s[78], reverse(@s[35..38]), $s[0], reverse(@s[30..33]), $s[34], reverse(@s[10..28]), $s[29], reverse(@s[1..8]), $s[9]);
360             }
361              
362 0         0 return (); # fail
363             }
364              
365             sub _getsig {
366 0     0   0 my $sig = shift;
367 0 0       0 croak 'Unable to find signature' unless $sig;
368 0         0 my @sig = _sigdecode(split(//, $sig));
369 0 0       0 croak "Unable to decode signature $sig of length " . length($sig) unless @sig;
370 0         0 return join('', @sig);
371             }
372              
373             sub _parse_stream_map {
374 0     0   0 my $param = shift;
375 0         0 my $fmt_url_map = {};
376 0         0 for my $stuff (split ',', $param) {
377 0         0 my $uri = URI->new;
378 0         0 $uri->query($stuff);
379 0         0 my $query = +{ $uri->query_form };
380 0   0     0 my $sig = $query->{sig} || ($query->{s} ? _getsig($query->{s}) : undef);
381 0         0 my $url = $query->{url};
382 0 0       0 $fmt_url_map->{$query->{itag}} = $url . ($sig ? '&signature='.$sig : '');
383             }
384              
385 0         0 return $fmt_url_map;
386             }
387              
388             sub ua {
389 0     0 1 0 my ($self, $ua) = @_;
390 0 0       0 return $self->{ua} unless $ua;
391 0 0       0 croak "Usage: $self->ua(\$LWP_LIKE_OBJECT)" unless eval { $ua->isa('LWP::UserAgent') };
  0         0  
392 0         0 $self->{ua} = $ua;
393             }
394              
395             sub _suffix {
396 0     0   0 my $fmt = shift;
397 0 0       0 return $fmt =~ /43|44|45|46/ ? 'webm'
    0          
    0          
398             : $fmt =~ /18|22|37|38/ ? 'mp4'
399             : $fmt =~ /13|17/ ? '3gp'
400             : 'flv'
401             ;
402             }
403              
404             sub video_id {
405 17     17 1 33 my ($self, $stuff) = @_;
406 17 50       36 return unless $stuff;
407 17 100       140 if ($stuff =~ m{/.*?[?&;!](?:v|video_id)=([^&#?=/;]+)}) {
    100          
    100          
    100          
408 8         38 return $1;
409             }
410             elsif ($stuff =~ m{/(?:e|v|embed)/([^&#?=/;]+)}) {
411 4         17 return $1;
412             }
413             elsif ($stuff =~ m{#p/(?:u|search)/\d+/([^&?/]+)}) {
414 1         6 return $1;
415             }
416             elsif ($stuff =~ m{youtu.be/([^&#?=/;]+)}) {
417 1         5 return $1;
418             }
419             else {
420 3         11 return $stuff;
421             }
422             }
423              
424             sub playlist_id {
425 10     10 1 22 my ($self, $stuff) = @_;
426 10 50       19 return unless $stuff;
427 10 100       60 if ($stuff =~ m{/.*?[?&;!]list=([^&#?=/;]+)}) {
    100          
428 4         18 return $1;
429             }
430             elsif ($stuff =~ m{^\s*([FP]L[\w\-]+)\s*$}) {
431 3         13 return $1;
432             }
433 3         8 return $stuff;
434             }
435              
436             sub user_id {
437 4     4 1 9 my ($self, $stuff) = @_;
438 4 50       9 return unless $stuff;
439 4 100       20 if ($stuff =~ m{/user/([^&#?=/;]+)}) {
440 3         13 return $1;
441             }
442 1         3 return $stuff;
443             }
444              
445             sub playlist {
446 0     0 1   my ( $self, $id, $args ) = @_;
447              
448 0           my $fetchCnt = 1;
449 0           my $html = $self->_fetch_playlist($id);
450              
451 0           my ( $videos, $nextUrl ) = $self->_find_playlist_videos($html);
452              
453 0 0         return $videos unless $nextUrl;
454 0 0 0       return $videos if $args && $args->{limit} && $fetchCnt >= $args->{limit};
      0        
455              
456 0           while ($nextUrl) {
457 0           $fetchCnt++;
458 0           my $json = $self->_fetch_playlist_json($nextUrl);
459              
460 0           ( my $moreVideos, $nextUrl )
461             = $self->_find_playlist_json_videos($json);
462              
463 0           push( @$videos, @$moreVideos );
464              
465 0 0         last unless $nextUrl;
466 0 0 0       last if $args && $args->{limit} && $fetchCnt >= $args->{limit};
      0        
467             }
468              
469 0           return $videos;
470             }
471              
472             sub _fetch_playlist {
473 0     0     my $self = shift;
474 0           my $id = shift;
475 0           my $url = 'https://www.youtube.com/playlist?list=' . $id;
476              
477 0 0         print 'Fetching playlist page ' . $url . ' ... ' if $self->{verbose};
478              
479 0           my $res = $self->ua->get($url);
480              
481 0 0         croak( 'Error: fetch_playlist failed for url:'
482             . $url
483             . ' status:'
484             . $res->status_line )
485             unless $res->is_success;
486              
487 0 0         print "ok \n" if $self->{verbose};
488              
489 0           return $res->decoded_content;
490             }
491              
492             sub _find_playlist_videos {
493 0     0     my $self = shift;
494 0           my $html = shift;
495              
496             ## find JSON blob
497 0           my $line;
498 0           for ( split( "\n", $html ) ) {
499 0 0         if ( $_ =~ /window\["ytInitialData"\]/ ) {
500 0           $line = $_;
501 0           last;
502             }
503             }
504              
505 0 0         die "Error: could not find YT JSON data!" unless $line;
506              
507 0           my $start = index( $line, '{' );
508 0           my $end = rindex( $line, '}' );
509 0           my $json = substr( $line, $start, $end - $start + 1 );
510              
511 0           my $ref = JSON()->new->utf8(0)->decode($json);
512              
513             ## drill into YT's data structure
514             die "Error: YT JSON data not structed as expected!"
515             unless $ref->{contents}
516             && $ref->{contents}->{twoColumnBrowseResultsRenderer}
517             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}
518             && ref( $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs} ) eq 'ARRAY'
519             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]
520             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}
521             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}
522             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}
523             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}
524             && ref(
525             $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]
526             ->{tabRenderer}->{content}->{sectionListRenderer}->{contents} )
527             eq 'ARRAY'
528             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]
529             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}
530             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}->{contents}
531             && ref(
532             $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}->{contents} ) eq 'ARRAY'
533             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}->{contents}->[0]
534             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}->{contents}->[0]->{playlistVideoListRenderer}
535             && $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}->{contents}->[0]->{playlistVideoListRenderer}->{contents}
536             && ref(
537 0 0 0       $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]->{itemSectionRenderer}->{contents}->[0]->{playlistVideoListRenderer}->{contents} ) eq 'ARRAY';
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
538              
539             my $playlistVideoListRenderer
540             = $ref->{contents}->{twoColumnBrowseResultsRenderer}->{tabs}->[0]
541             ->{tabRenderer}->{content}->{sectionListRenderer}->{contents}->[0]
542 0           ->{itemSectionRenderer}->{contents}->[0]->{playlistVideoListRenderer};
543              
544             ## weed out videos
545 0           my @videos;
546 0           my $cnt = 0;
547 0           for my $renderer ( @{ $playlistVideoListRenderer->{contents} } ) {
  0            
548 0           $cnt++;
549 0           my $video = $renderer->{playlistVideoRenderer};
550 0 0         my $titleText = @{$video->{title}->{runs}} ? $video->{title}->{runs}->[0]->{text} : $video->{title}->{simpleText};
  0            
551              
552             push(
553             @videos,
554             {
555             id => $video->{videoId},
556             runtime => $video->{lengthSeconds},
557 0   0       title => ( $titleText || 'undef' )
558             , # encoded in Perl internal
559             }
560             );
561              
562 0 0         if ( $self->{verbose} ) {
563 0           print " $cnt id:" . $video->{videoId};
564 0   0       print " runtime:" . ( $video->{lengthSeconds} || 'undef' );
565 0   0       print " title:"
566             . Encode::encode_utf8( $titleText
567             || 'undef' );
568 0           print "\n";
569             }
570             }
571              
572             ## next page
573 0           my $nextUrl;
574 0 0 0       if ( $playlistVideoListRenderer->{continuations}
      0        
      0        
575             && ref( $playlistVideoListRenderer->{continuations} ) eq 'ARRAY'
576             && $playlistVideoListRenderer->{continuations}->[0]
577             && $playlistVideoListRenderer->{continuations}->[0]->{nextContinuationData} ) {
578             my $next = $playlistVideoListRenderer->{continuations}->[0]
579 0           ->{nextContinuationData};
580 0           my $continuation = $next->{continuation};
581 0           my $ctoken = $continuation;
582 0           my $itct = $next->{clickTrackingParams};
583              
584 0 0         if ( $self->{verbose} ) {
585 0           print " next page continuation data:\n cont:"
586             . $continuation . "\n";
587 0           print " itct:" . $itct . "\n";
588 0           print "\n";
589             }
590              
591             $nextUrl
592 0           = 'https://www.youtube.com/browse_ajax?ctoken='
593             . $ctoken
594             . '&continuation='
595             . $continuation
596             . '&itct='
597             . $itct;
598             }
599              
600 0           return ( \@videos, $nextUrl );
601             }
602              
603             sub _fetch_playlist_json {
604 0     0     my $self = shift;
605 0           my $url = shift;
606              
607 0 0         print 'Fetching playlist next page '. $url .' ... ' if $self->{verbose};
608              
609 0           my $res = $self->ua->get($url,
610             # thanks youtube-dl for the hint towards these headers
611             'x-youtube-client-name' => '1',
612             'x-youtube-client-version' => '2.20200825.04.00', # version '1.20200609.04.02' will return a HTML blob in content_html, this version returns only JSON
613             );
614              
615 0 0         croak('Error: fetch_playlist_json failed for url:'. $url .' status:'. $res->status_line) unless $res->is_success;
616              
617 0 0         print "ok \n" if $self->{verbose};
618              
619 0           return $res->decoded_content;
620             }
621              
622             sub _find_playlist_json_videos {
623 0     0     my $self = shift;
624 0           my $json = shift;
625              
626 0           my $ref = JSON()->new->utf8(1)->decode($json);
627              
628             ## drill into YT's data structure
629             die "Error: YT JSON data not structed as expected!"
630             unless ref($ref) eq 'ARRAY'
631             && $ref->[1]
632             && $ref->[1]->{response}
633             && $ref->[1]->{response}->{continuationContents}
634             && $ref->[1]->{response}->{continuationContents}->{playlistVideoListContinuation}
635             && $ref->[1]->{response}->{continuationContents}->{playlistVideoListContinuation}->{contents}
636 0 0 0       && ref( $ref->[1]->{response}->{continuationContents}->{playlistVideoListContinuation}->{contents} ) eq 'ARRAY';
      0        
      0        
      0        
      0        
      0        
637              
638             my $playlistVideoListContinuation
639             = $ref->[1]->{response}->{continuationContents}
640 0           ->{playlistVideoListContinuation};
641              
642             ## weed out videos
643 0           my @videos;
644 0           my $cnt = 0;
645 0           for my $renderer ( @{ $playlistVideoListContinuation->{contents} } ) {
  0            
646 0           $cnt++;
647 0           my $video = $renderer->{playlistVideoRenderer};
648 0 0         my $titleText = @{$video->{title}->{runs}} ? $video->{title}->{runs}->[0]->{text} : $video->{title}->{simpleText};
  0            
649              
650             push(
651             @videos,
652             {
653             id => $video->{videoId},
654             runtime => $video->{lengthSeconds},
655 0   0       title => ( $titleText || 'undef' )
656             , # encoded in Perl internal
657             }
658             );
659              
660 0 0         if ( $self->{verbose} ) {
661 0           print " $cnt id:" . $video->{videoId};
662 0   0       print " runtime:" . ( $video->{lengthSeconds} || 'undef' );
663 0   0       print " title:"
664             . Encode::encode_utf8( $titleText
665             || 'undef' );
666 0           print "\n";
667             }
668             }
669              
670             ## next page
671 0           my $nextUrl;
672 0 0 0       if ( $playlistVideoListContinuation->{continuations}
      0        
      0        
673             && ref( $playlistVideoListContinuation->{continuations} ) eq 'ARRAY'
674             && $playlistVideoListContinuation->{continuations}->[0]
675             && $playlistVideoListContinuation->{continuations}->[0]->{nextContinuationData} ) {
676 0           my $next = $playlistVideoListContinuation->{continuations}->[0]->{nextContinuationData};
677 0           my $continuation = $next->{continuation};
678 0           my $ctoken = $continuation;
679 0           my $itct = $next->{clickTrackingParams};
680              
681 0 0         if ( $self->{verbose} ) {
682 0           print " next page continuation data:\n cont:"
683             . $continuation . "\n";
684 0           print " itct:" . $itct . "\n";
685 0           print "\n";
686             }
687              
688             $nextUrl
689 0           = 'https://www.youtube.com/browse_ajax?ctoken='
690             . $ctoken
691             . '&continuation='
692             . $continuation
693             . '&itct='
694             . $itct;
695             }
696              
697 0           return ( \@videos, $nextUrl );
698             }
699              
700             1;
701              
702             =pod
703              
704             =encoding UTF-8
705              
706             =head1 NAME
707              
708             WWW::YouTube::Download - WWW::YouTube::Download - Very simple YouTube video download interface
709              
710             =head1 VERSION
711              
712             version 0.65
713              
714             =head1 SYNOPSIS
715              
716             use WWW::YouTube::Download;
717              
718             my $client = WWW::YouTube::Download->new;
719             $client->download($video_id);
720              
721             my $video_url = $client->get_video_url($video_id);
722             my $title = $client->get_title($video_id); # maybe encoded utf8 string.
723             my $fmt = $client->get_fmt($video_id); # maybe highest quality.
724             my $suffix = $client->get_suffix($video_id); # maybe highest quality file suffix
725              
726             =head1 DESCRIPTION
727              
728             WWW::YouTube::Download is a library to download videos from YouTube. It relies entirely on
729             scraping a video's webpage and does not use YT's /get_video_info URL space.
730              
731             =head1 METHODS
732              
733             =over
734              
735             =item B
736              
737             $client = WWW::YouTube::Download->new;
738              
739             Creates a WWW::YouTube::Download instance.
740              
741             =item B
742              
743             $client->download($video_id);
744             $client->download($video_id, {
745             fmt => 37,
746             filename => 'sample.mp4', # save file name
747             });
748             $client->download($video_id, {
749             filename => '{title}.{suffix}', # maybe `video_title.mp4`
750             });
751             $client->download($video_id, {
752             cb => \&callback,
753             });
754              
755             Download the video file.
756             The first parameter is passed to YouTube video url.
757              
758             Allowed arguments:
759              
760             =over
761              
762             =item C
763              
764             Set a callback subroutine, SEE L ':content_cb'
765             for details.
766              
767             =item C
768              
769             Set the filename, possibly using placeholders to be filled with
770             information gathered about the video.
771              
772             C<< filename >> supported format placeholders:
773              
774             {video_id}
775             {title}
776             {user}
777             {fmt}
778             {suffix}
779             {resolution}
780              
781             Output filename is set to C<{video_id}.{suffix}> by default.
782              
783             =item C
784              
785             B<< DEPRECATED >> alternative for C.
786              
787             =item C
788              
789             set the format to download. Defaults to the best video quality
790             (inferred by the available resolutions).
791              
792             =back
793              
794             =item B
795              
796             $client->playback_url($video_id);
797             $client->playback_url($video_id, { fmt => 37 });
798              
799             Return playback URL of the video. This is direct link to the movie file.
800             Function supports only "fmt" option.
801              
802             =item B
803              
804             Gather data about the video. A hash reference is returned, with the following
805             keys:
806              
807             =over
808              
809             =item C
810              
811             the default, suggested format. It is inferred by selecting the
812             alternative with the highest resolution.
813              
814             =item C
815              
816             the list of available formats, as an array reference.
817              
818             =item C
819              
820             the filename extension associated to the default format (see C
821             above).
822              
823             =item C </td> </tr> <tr> <td class="h" > <a name="824">824</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="825">825</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the title of the video </td> </tr> <tr> <td class="h" > <a name="826">826</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="827">827</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<user> </td> </tr> <tr> <td class="h" > <a name="828">828</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="829">829</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the YouTube user owning the video </td> </tr> <tr> <td class="h" > <a name="830">830</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="831">831</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<video_id> </td> </tr> <tr> <td class="h" > <a name="832">832</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="833">833</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the video identifier </td> </tr> <tr> <td class="h" > <a name="834">834</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="835">835</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<video_url> </td> </tr> <tr> <td class="h" > <a name="836">836</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="837">837</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the URL of the video associated to the default format (see C<fmt> </td> </tr> <tr> <td class="h" > <a name="838">838</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> above). </td> </tr> <tr> <td class="h" > <a name="839">839</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="840">840</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<video_url_map> </td> </tr> <tr> <td class="h" > <a name="841">841</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="842">842</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> an hash reference containing details about all available formats. </td> </tr> <tr> <td class="h" > <a name="843">843</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="844">844</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="845">845</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="846">846</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> The C<video_url_map> has one key/value pair for each available format, </td> </tr> <tr> <td class="h" > <a name="847">847</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> where the key is the format identifier (can be used as C<fmt> parameter </td> </tr> <tr> <td class="h" > <a name="848">848</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> for L</download>, for example) and the value is a hash reference with </td> </tr> <tr> <td class="h" > <a name="849">849</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the following data: </td> </tr> <tr> <td class="h" > <a name="850">850</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="851">851</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =over </td> </tr> <tr> <td class="h" > <a name="852">852</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="853">853</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<fmt> </td> </tr> <tr> <td class="h" > <a name="854">854</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="855">855</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the format specifier, that can be passed to L</download> </td> </tr> <tr> <td class="h" > <a name="856">856</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="857">857</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<resolution> </td> </tr> <tr> <td class="h" > <a name="858">858</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="859">859</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the resolution as I<width>xI<height> </td> </tr> <tr> <td class="h" > <a name="860">860</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="861">861</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<suffix> </td> </tr> <tr> <td class="h" > <a name="862">862</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="863">863</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the suffix, providing a hint about the video format (e.g. webm, flv, ...) </td> </tr> <tr> <td class="h" > <a name="864">864</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="865">865</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item C<url> </td> </tr> <tr> <td class="h" > <a name="866">866</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="867">867</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the URL where the video can be found </td> </tr> <tr> <td class="h" > <a name="868">868</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="869">869</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="870">870</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="871">871</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<ua([$ua])> </td> </tr> <tr> <td class="h" > <a name="872">872</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="873">873</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $self->ua->agent(); </td> </tr> <tr> <td class="h" > <a name="874">874</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> $self->ua($LWP_LIKE_OBJECT); </td> </tr> <tr> <td class="h" > <a name="875">875</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="876">876</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Sets and gets LWP::UserAgent object. </td> </tr> <tr> <td class="h" > <a name="877">877</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="878">878</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<video_id($url)> </td> </tr> <tr> <td class="h" > <a name="879">879</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="880">880</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Parses given URL and returns video ID. </td> </tr> <tr> <td class="h" > <a name="881">881</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="882">882</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<playlist_id($url)> </td> </tr> <tr> <td class="h" > <a name="883">883</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="884">884</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Parses given URL and returns playlist ID. </td> </tr> <tr> <td class="h" > <a name="885">885</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="886">886</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<user_id($url)> </td> </tr> <tr> <td class="h" > <a name="887">887</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="888">888</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Parses given URL and returns YouTube username. </td> </tr> <tr> <td class="h" > <a name="889">889</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="890">890</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_video_id($video_id)> </td> </tr> <tr> <td class="h" > <a name="891">891</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="892">892</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_video_url($video_id)> </td> </tr> <tr> <td class="h" > <a name="893">893</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="894">894</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_title($video_id)> </td> </tr> <tr> <td class="h" > <a name="895">895</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="896">896</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_user($video_id)> </td> </tr> <tr> <td class="h" > <a name="897">897</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="898">898</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_fmt($video_id)> </td> </tr> <tr> <td class="h" > <a name="899">899</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="900">900</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_fmt_list($video_id)> </td> </tr> <tr> <td class="h" > <a name="901">901</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="902">902</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<get_suffix($video_id)> </td> </tr> <tr> <td class="h" > <a name="903">903</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="904">904</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =item B<playlist($id,$ref)> </td> </tr> <tr> <td class="h" > <a name="905">905</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="906">906</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Fetches a playlist and returns a ref to an array of hashes, where each hash </td> </tr> <tr> <td class="h" > <a name="907">907</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> represents a video from the playlist with youtube id, runtime in seconds </td> </tr> <tr> <td class="h" > <a name="908">908</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> and title. On playlists with many videos, the method iteratively downloads </td> </tr> <tr> <td class="h" > <a name="909">909</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> pages until the playlist is complete. </td> </tr> <tr> <td class="h" > <a name="910">910</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="911">911</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Optionally accepts a second argument, a hashref of options. Currently, you </td> </tr> <tr> <td class="h" > <a name="912">912</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> can pass a "limit" value to stop downloading of subsequent pages on larger </td> </tr> <tr> <td class="h" > <a name="913">913</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> playlists after x-amount of fetches (a limit of fetches, not playlist items). </td> </tr> <tr> <td class="h" > <a name="914">914</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> For example, pass 1 to only download the first page of videos from a playlist </td> </tr> <tr> <td class="h" > <a name="915">915</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> in order to "skim" the "tip" of new videos in a playlist. YouTube currently </td> </tr> <tr> <td class="h" > <a name="916">916</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> returns 100 videos at max per page. </td> </tr> <tr> <td class="h" > <a name="917">917</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="918">918</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> This method is used by the I<youtube-playlists.pl> script. </td> </tr> <tr> <td class="h" > <a name="919">919</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="920">920</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =back </td> </tr> <tr> <td class="h" > <a name="921">921</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="922">922</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 CONTRIBUTORS </td> </tr> <tr> <td class="h" > <a name="923">923</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="924">924</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> yusukebe </td> </tr> <tr> <td class="h" > <a name="925">925</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="926">926</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 BUG REPORTING </td> </tr> <tr> <td class="h" > <a name="927">927</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="928">928</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> Please use github issues: L<< https://github.com/xaicron/p5-www-youtube-download/issues >>. </td> </tr> <tr> <td class="h" > <a name="929">929</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="930">930</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 SEE ALSO </td> </tr> <tr> <td class="h" > <a name="931">931</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="932">932</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<WWW::YouTube::Info> and L<WWW::YouTube::Info::Simple>. </td> </tr> <tr> <td class="h" > <a name="933">933</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<WWW::NicoVideo::Download> </td> </tr> <tr> <td class="h" > <a name="934">934</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> L<http://rg3.github.io/youtube-dl/> </td> </tr> <tr> <td class="h" > <a name="935">935</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="936">936</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 AUTHOR </td> </tr> <tr> <td class="h" > <a name="937">937</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="938">938</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> xaicron <xaicron {@} cpan.org> </td> </tr> <tr> <td class="h" > <a name="939">939</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="940">940</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =head1 COPYRIGHT AND LICENSE </td> </tr> <tr> <td class="h" > <a name="941">941</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="942">942</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> This software is copyright (c) 2013 by Yuji Shimada. </td> </tr> <tr> <td class="h" > <a name="943">943</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="944">944</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> This is free software; you can redistribute it and/or modify it under </td> </tr> <tr> <td class="h" > <a name="945">945</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> the same terms as the Perl 5 programming language system itself. </td> </tr> <tr> <td class="h" > <a name="946">946</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="947">947</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> =cut </td> </tr> <tr> <td class="h" > <a name="948">948</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s">   </td> </tr> <tr> <td class="h" > <a name="949">949</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> __END__ </td> </tr> </table> </body> </html>