line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package WWW::YouTube::Download; |
2
|
|
|
|
|
|
|
|
3
|
7
|
|
|
7
|
|
488157
|
use strict; |
|
7
|
|
|
|
|
79
|
|
|
7
|
|
|
|
|
216
|
|
4
|
7
|
|
|
7
|
|
39
|
use warnings; |
|
7
|
|
|
|
|
14
|
|
|
7
|
|
|
|
|
178
|
|
5
|
7
|
|
|
7
|
|
153
|
use 5.008001; |
|
7
|
|
|
|
|
24
|
|
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
our $VERSION = '0.63'; |
8
|
|
|
|
|
|
|
|
9
|
7
|
|
|
7
|
|
51
|
use Carp qw(croak); |
|
7
|
|
|
|
|
14
|
|
|
7
|
|
|
|
|
356
|
|
10
|
7
|
|
|
7
|
|
4086
|
use URI (); |
|
7
|
|
|
|
|
49160
|
|
|
7
|
|
|
|
|
174
|
|
11
|
7
|
|
|
7
|
|
4850
|
use LWP::UserAgent; |
|
7
|
|
|
|
|
278858
|
|
|
7
|
|
|
|
|
291
|
|
12
|
7
|
|
|
7
|
|
3582
|
use JSON::MaybeXS 'JSON'; |
|
7
|
|
|
|
|
41523
|
|
|
7
|
|
|
|
|
494
|
|
13
|
7
|
|
|
7
|
|
3551
|
use HTML::Entities qw/decode_entities/; |
|
7
|
|
|
|
|
40981
|
|
|
7
|
|
|
|
|
491
|
|
14
|
7
|
|
|
7
|
|
53
|
use HTTP::Request; |
|
7
|
|
|
|
|
14
|
|
|
7
|
|
|
|
|
215
|
|
15
|
7
|
|
|
7
|
|
3170
|
use MIME::Type; |
|
7
|
|
|
|
|
8712
|
|
|
7
|
|
|
|
|
313
|
|
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
$Carp::Internal{ (__PACKAGE__) }++; |
18
|
|
|
|
|
|
|
|
19
|
7
|
|
|
7
|
|
51
|
use constant DEFAULT_FMT => 18; |
|
7
|
|
|
|
|
17
|
|
|
7
|
|
|
|
|
1073
|
|
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
my $base_url = 'http://www.youtube.com/watch?v='; |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
sub new { |
24
|
32
|
|
|
32
|
1
|
16097
|
my $class = shift; |
25
|
32
|
|
|
|
|
73
|
my %args = @_; |
26
|
|
|
|
|
|
|
$args{ua} = LWP::UserAgent->new( |
27
|
|
|
|
|
|
|
agent => __PACKAGE__.'/'.$VERSION, |
28
|
|
|
|
|
|
|
parse_head => 0, |
29
|
32
|
50
|
|
|
|
212
|
) unless exists $args{ua}; |
30
|
32
|
|
|
|
|
6242
|
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
|
|
52
|
no strict 'refs'; |
|
7
|
|
|
|
|
17
|
|
|
7
|
|
|
|
|
318
|
|
36
|
|
|
|
|
|
|
*{"get_$name"} = sub { |
37
|
7
|
|
|
7
|
|
41
|
use strict 'refs'; |
|
7
|
|
|
|
|
15
|
|
|
7
|
|
|
|
|
30854
|
|
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
|
9
|
my ($self, $video_id) = @_; |
152
|
1
|
50
|
|
|
|
5
|
croak "Usage: $self->prepare_download('[video_id|watch_url]')" unless $video_id; |
153
|
1
|
|
|
|
|
3
|
$video_id = $self->video_id($video_id); |
154
|
|
|
|
|
|
|
|
155
|
1
|
50
|
|
|
|
7
|
return $self->{cache}{$video_id} if ref $self->{cache}{$video_id} eq 'HASH'; |
156
|
|
|
|
|
|
|
|
157
|
1
|
|
|
|
|
4
|
my ($title, $user, $video_url_map); |
158
|
1
|
|
|
|
|
3
|
my $content = $self->_get_content($video_id); |
159
|
1
|
50
|
|
|
|
250
|
if ($self->_is_new($content)) { |
160
|
1
|
|
|
|
|
5
|
my $args = $self->_get_args($content); |
161
|
1
|
|
|
|
|
4
|
my $player_resp = JSON()->new->decode($args->{player_response}); |
162
|
1
|
|
|
|
|
607
|
$video_url_map = $self->_decode_player_response($player_resp); |
163
|
1
|
|
|
|
|
12
|
$title = decode_entities $player_resp->{videoDetails}{title}; |
164
|
1
|
|
|
|
|
113
|
$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
|
|
|
|
|
5
|
my $fmt_list = []; |
172
|
|
|
|
|
|
|
my $sorted = [ |
173
|
|
|
|
|
|
|
map { |
174
|
2
|
|
|
|
|
6
|
push @$fmt_list, $_->[0]->{fmt}; |
175
|
2
|
|
|
|
|
4
|
$_->[0] |
176
|
|
|
|
|
|
|
} sort { |
177
|
1
|
|
|
|
|
6
|
$b->[1] <=> $a->[1] |
178
|
|
|
|
|
|
|
} map { |
179
|
1
|
|
|
|
|
5
|
my $resolution = $_->{resolution}; |
|
2
|
|
|
|
|
5
|
|
180
|
2
|
|
|
|
|
14
|
$resolution =~ s/(\d+)x(\d+)/$1 * $2/e; |
|
2
|
|
|
|
|
10
|
|
181
|
2
|
|
|
|
|
9
|
[ $_, $resolution ] |
182
|
|
|
|
|
|
|
} values %$video_url_map, |
183
|
|
|
|
|
|
|
]; |
184
|
|
|
|
|
|
|
|
185
|
1
|
|
|
|
|
4
|
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
|
|
|
|
|
29
|
}; |
198
|
|
|
|
|
|
|
} |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
sub _mime_to_suffix { |
201
|
2
|
|
|
2
|
|
15
|
(my $mime = shift) =~ s{^([^;]+);.*}{$1}; |
202
|
2
|
|
|
|
|
16
|
my $stype = MIME::Type->new(type => $mime)->subType; |
203
|
2
|
0
|
|
|
|
147
|
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
|
|
5
|
my ($self, $player_data) = @_; |
212
|
|
|
|
|
|
|
# need to decode $player_data->{streamingData}{formats}; |
213
|
1
|
|
|
|
|
3
|
my $fmt_map = { map { $_->{mimeType} => join 'x', $_->{width}, $_->{height} } @{$player_data->{streamingData}{formats}} }; |
|
2
|
|
|
|
|
12
|
|
|
1
|
|
|
|
|
5
|
|
214
|
1
|
|
|
|
|
2
|
my $fmt_url_map = { map { $_->{mimeType} => $_->{url} } @{$player_data->{streamingData}{formats}} }; |
|
2
|
|
|
|
|
7
|
|
|
1
|
|
|
|
|
11
|
|
215
|
|
|
|
|
|
|
# more formats in $player_data->{streamingData}{adaptiveFormats}; |
216
|
|
|
|
|
|
|
return +{ |
217
|
|
|
|
|
|
|
map { |
218
|
2
|
|
|
|
|
8
|
$_->{fmt} => $_, |
219
|
|
|
|
|
|
|
} map +{ |
220
|
|
|
|
|
|
|
fmt => $_, |
221
|
|
|
|
|
|
|
resolution => $fmt_map->{$_}, |
222
|
1
|
|
|
|
|
8
|
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
|
|
12
|
my ($self, $content) = @_; |
237
|
|
|
|
|
|
|
|
238
|
1
|
50
|
|
|
|
11
|
if( $content =~ /
|
|
0
|
|
|
|
|
|
239
|
1
|
|
|
|
|
11
|
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
|
|
6
|
my ($self, $content) = @_; |
290
|
|
|
|
|
|
|
|
291
|
2
|
|
|
|
|
2
|
my $data; |
292
|
2
|
|
|
|
|
246
|
for my $line (split "\n", $content) { |
293
|
2
|
50
|
|
|
|
8
|
next unless $line; |
294
|
2
|
50
|
|
|
|
1613
|
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
|
|
|
|
|
14
|
($data, undef) = JSON->new->utf8(1)->decode_prefix($1); |
299
|
2
|
|
|
|
|
872
|
last; |
300
|
|
|
|
|
|
|
} |
301
|
|
|
|
|
|
|
} |
302
|
|
|
|
|
|
|
|
303
|
2
|
50
|
|
|
|
12
|
croak 'failed to extract JSON data' unless $data->{args}; |
304
|
|
|
|
|
|
|
|
305
|
2
|
|
|
|
|
9
|
return $data->{args}; |
306
|
|
|
|
|
|
|
} |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
sub _is_new { |
309
|
1
|
|
|
1
|
|
5
|
my ($self, $content) = @_; |
310
|
1
|
|
|
|
|
4
|
my $args = $self->_get_args($content); |
311
|
1
|
50
|
33
|
|
|
31
|
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
|
40
|
my ($self, $stuff) = @_; |
406
|
17
|
50
|
|
|
|
44
|
return unless $stuff; |
407
|
17
|
100
|
|
|
|
175
|
if ($stuff =~ m{/.*?[?&;!](?:v|video_id)=([^?=/;]+)}) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
408
|
8
|
|
|
|
|
40
|
return $1; |
409
|
|
|
|
|
|
|
} |
410
|
|
|
|
|
|
|
elsif ($stuff =~ m{/(?:e|v|embed)/([^?=/;]+)}) { |
411
|
4
|
|
|
|
|
20
|
return $1; |
412
|
|
|
|
|
|
|
} |
413
|
|
|
|
|
|
|
elsif ($stuff =~ m{#p/(?:u|search)/\d+/([^&?/]+)}) { |
414
|
1
|
|
|
|
|
5
|
return $1; |
415
|
|
|
|
|
|
|
} |
416
|
|
|
|
|
|
|
elsif ($stuff =~ m{youtu.be/([^?=/;]+)}) { |
417
|
1
|
|
|
|
|
6
|
return $1; |
418
|
|
|
|
|
|
|
} |
419
|
|
|
|
|
|
|
else { |
420
|
3
|
|
|
|
|
11
|
return $stuff; |
421
|
|
|
|
|
|
|
} |
422
|
|
|
|
|
|
|
} |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
sub playlist_id { |
425
|
10
|
|
|
10
|
1
|
24
|
my ($self, $stuff) = @_; |
426
|
10
|
50
|
|
|
|
25
|
return unless $stuff; |
427
|
10
|
100
|
|
|
|
73
|
if ($stuff =~ m{/.*?[?&;!]list=([^?=/;]+)}) { |
|
|
100
|
|
|
|
|
|
428
|
4
|
|
|
|
|
23
|
return $1; |
429
|
|
|
|
|
|
|
} |
430
|
|
|
|
|
|
|
elsif ($stuff =~ m{^\s*([FP]L[\w\-]+)\s*$}) { |
431
|
3
|
|
|
|
|
14
|
return $1; |
432
|
|
|
|
|
|
|
} |
433
|
3
|
|
|
|
|
10
|
return $stuff; |
434
|
|
|
|
|
|
|
} |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
sub user_id { |
437
|
4
|
|
|
4
|
1
|
10
|
my ($self, $stuff) = @_; |
438
|
4
|
50
|
|
|
|
11
|
return unless $stuff; |
439
|
4
|
100
|
|
|
|
23
|
if ($stuff =~ m{/user/([^?=/;]+)}) { |
440
|
3
|
|
|
|
|
17
|
return $1; |
441
|
|
|
|
|
|
|
} |
442
|
1
|
|
|
|
|
6
|
return $stuff; |
443
|
|
|
|
|
|
|
} |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
1; |
446
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
=pod |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
=encoding UTF-8 |
450
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
=head1 NAME |
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
WWW::YouTube::Download - WWW::YouTube::Download - Very simple YouTube video download interface |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
=head1 VERSION |
456
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
version 0.63 |
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
=head1 SYNOPSIS |
460
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
use WWW::YouTube::Download; |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
my $client = WWW::YouTube::Download->new; |
464
|
|
|
|
|
|
|
$client->download($video_id); |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
my $video_url = $client->get_video_url($video_id); |
467
|
|
|
|
|
|
|
my $title = $client->get_title($video_id); # maybe encoded utf8 string. |
468
|
|
|
|
|
|
|
my $fmt = $client->get_fmt($video_id); # maybe highest quality. |
469
|
|
|
|
|
|
|
my $suffix = $client->get_suffix($video_id); # maybe highest quality file suffix |
470
|
|
|
|
|
|
|
|
471
|
|
|
|
|
|
|
=head1 DESCRIPTION |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
WWW::YouTube::Download is a library to download videos from YouTube. It relies entirely on |
474
|
|
|
|
|
|
|
scraping a video's webpage and does not use YT's /get_video_info URL space. |
475
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
=head1 METHODS |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
=over |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
=item B |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
$client = WWW::YouTube::Download->new; |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
Creates a WWW::YouTube::Download instance. |
485
|
|
|
|
|
|
|
|
486
|
|
|
|
|
|
|
=item B |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
$client->download($video_id); |
489
|
|
|
|
|
|
|
$client->download($video_id, { |
490
|
|
|
|
|
|
|
fmt => 37, |
491
|
|
|
|
|
|
|
filename => 'sample.mp4', # save file name |
492
|
|
|
|
|
|
|
}); |
493
|
|
|
|
|
|
|
$client->download($video_id, { |
494
|
|
|
|
|
|
|
filename => '{title}.{suffix}', # maybe `video_title.mp4` |
495
|
|
|
|
|
|
|
}); |
496
|
|
|
|
|
|
|
$client->download($video_id, { |
497
|
|
|
|
|
|
|
cb => \&callback, |
498
|
|
|
|
|
|
|
}); |
499
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
Download the video file. |
501
|
|
|
|
|
|
|
The first parameter is passed to YouTube video url. |
502
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
Allowed arguments: |
504
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
=over |
506
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
=item C |
508
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
Set a callback subroutine, SEE L ':content_cb' |
510
|
|
|
|
|
|
|
for details. |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
=item C |
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
Set the filename, possibly using placeholders to be filled with |
515
|
|
|
|
|
|
|
information gathered about the video. |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
C<< filename >> supported format placeholders: |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
{video_id} |
520
|
|
|
|
|
|
|
{title} |
521
|
|
|
|
|
|
|
{user} |
522
|
|
|
|
|
|
|
{fmt} |
523
|
|
|
|
|
|
|
{suffix} |
524
|
|
|
|
|
|
|
{resolution} |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
Output filename is set to C<{video_id}.{suffix}> by default. |
527
|
|
|
|
|
|
|
|
528
|
|
|
|
|
|
|
=item C |
529
|
|
|
|
|
|
|
|
530
|
|
|
|
|
|
|
B<< DEPRECATED >> alternative for C. |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
=item C |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
set the format to download. Defaults to the best video quality |
535
|
|
|
|
|
|
|
(inferred by the available resolutions). |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
=back |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
=item B |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
$client->playback_url($video_id); |
542
|
|
|
|
|
|
|
$client->playback_url($video_id, { fmt => 37 }); |
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
Return playback URL of the video. This is direct link to the movie file. |
545
|
|
|
|
|
|
|
Function supports only "fmt" option. |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=item B |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
Gather data about the video. A hash reference is returned, with the following |
550
|
|
|
|
|
|
|
keys: |
551
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
=over |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
=item C |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
the default, suggested format. It is inferred by selecting the |
557
|
|
|
|
|
|
|
alternative with the highest resolution. |
558
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
=item C |
560
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
the list of available formats, as an array reference. |
562
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
=item C |
564
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
the filename extension associated to the default format (see C |
566
|
|
|
|
|
|
|
above). |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
=item C |
569
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
the title of the video |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
=item C |
573
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
the YouTube user owning the video |
575
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
=item C |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
the video identifier |
579
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
=item C |
581
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
the URL of the video associated to the default format (see C |
583
|
|
|
|
|
|
|
above). |
584
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
=item C |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
an hash reference containing details about all available formats. |
588
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
=back |
590
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
The C has one key/value pair for each available format, |
592
|
|
|
|
|
|
|
where the key is the format identifier (can be used as C parameter |
593
|
|
|
|
|
|
|
for L, for example) and the value is a hash reference with |
594
|
|
|
|
|
|
|
the following data: |
595
|
|
|
|
|
|
|
|
596
|
|
|
|
|
|
|
=over |
597
|
|
|
|
|
|
|
|
598
|
|
|
|
|
|
|
=item C |
599
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
the format specifier, that can be passed to L |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
=item C |
603
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
the resolution as IxI |
605
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
=item C |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
the suffix, providing a hint about the video format (e.g. webm, flv, ...) |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
=item C |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
the URL where the video can be found |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
=back |
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
=item B |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
$self->ua->agent(); |
619
|
|
|
|
|
|
|
$self->ua($LWP_LIKE_OBJECT); |
620
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
Sets and gets LWP::UserAgent object. |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
=item B |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
Parses given URL and returns video ID. |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
=item B |
628
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
Parses given URL and returns playlist ID. |
630
|
|
|
|
|
|
|
|
631
|
|
|
|
|
|
|
=item B |
632
|
|
|
|
|
|
|
|
633
|
|
|
|
|
|
|
Parses given URL and returns YouTube username. |
634
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
=item B |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
=item B |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=item B |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
=item B |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
=item B |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
=item B |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
=item B |
648
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
=back |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
=head1 CONTRIBUTORS |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
yusukebe |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=head1 BUG REPORTING |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
Please use github issues: L<< https://github.com/xaicron/p5-www-youtube-download/issues >>. |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
=head1 SEE ALSO |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
L and L. |
662
|
|
|
|
|
|
|
L |
663
|
|
|
|
|
|
|
L |
664
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
=head1 AUTHOR |
666
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
xaicron |
668
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
670
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
This software is copyright (c) 2013 by Yuji Shimada. |
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
674
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
675
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
=cut |
677
|
|
|
|
|
|
|
|
678
|
|
|
|
|
|
|
__END__ |