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 |
824
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
the title of the video |
826
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
=item C |
828
|
|
|
|
|
|
|
|
829
|
|
|
|
|
|
|
the YouTube user owning the video |
830
|
|
|
|
|
|
|
|
831
|
|
|
|
|
|
|
=item C |
832
|
|
|
|
|
|
|
|
833
|
|
|
|
|
|
|
the video identifier |
834
|
|
|
|
|
|
|
|
835
|
|
|
|
|
|
|
=item C |
836
|
|
|
|
|
|
|
|
837
|
|
|
|
|
|
|
the URL of the video associated to the default format (see C |
838
|
|
|
|
|
|
|
above). |
839
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
=item C |
841
|
|
|
|
|
|
|
|
842
|
|
|
|
|
|
|
an hash reference containing details about all available formats. |
843
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
=back |
845
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
The C has one key/value pair for each available format, |
847
|
|
|
|
|
|
|
where the key is the format identifier (can be used as C parameter |
848
|
|
|
|
|
|
|
for L, for example) and the value is a hash reference with |
849
|
|
|
|
|
|
|
the following data: |
850
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
=over |
852
|
|
|
|
|
|
|
|
853
|
|
|
|
|
|
|
=item C |
854
|
|
|
|
|
|
|
|
855
|
|
|
|
|
|
|
the format specifier, that can be passed to L |
856
|
|
|
|
|
|
|
|
857
|
|
|
|
|
|
|
=item C |
858
|
|
|
|
|
|
|
|
859
|
|
|
|
|
|
|
the resolution as IxI |
860
|
|
|
|
|
|
|
|
861
|
|
|
|
|
|
|
=item C |
862
|
|
|
|
|
|
|
|
863
|
|
|
|
|
|
|
the suffix, providing a hint about the video format (e.g. webm, flv, ...) |
864
|
|
|
|
|
|
|
|
865
|
|
|
|
|
|
|
=item C |
866
|
|
|
|
|
|
|
|
867
|
|
|
|
|
|
|
the URL where the video can be found |
868
|
|
|
|
|
|
|
|
869
|
|
|
|
|
|
|
=back |
870
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
=item B |
872
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
$self->ua->agent(); |
874
|
|
|
|
|
|
|
$self->ua($LWP_LIKE_OBJECT); |
875
|
|
|
|
|
|
|
|
876
|
|
|
|
|
|
|
Sets and gets LWP::UserAgent object. |
877
|
|
|
|
|
|
|
|
878
|
|
|
|
|
|
|
=item B |
879
|
|
|
|
|
|
|
|
880
|
|
|
|
|
|
|
Parses given URL and returns video ID. |
881
|
|
|
|
|
|
|
|
882
|
|
|
|
|
|
|
=item B |
883
|
|
|
|
|
|
|
|
884
|
|
|
|
|
|
|
Parses given URL and returns playlist ID. |
885
|
|
|
|
|
|
|
|
886
|
|
|
|
|
|
|
=item B |
887
|
|
|
|
|
|
|
|
888
|
|
|
|
|
|
|
Parses given URL and returns YouTube username. |
889
|
|
|
|
|
|
|
|
890
|
|
|
|
|
|
|
=item B |
891
|
|
|
|
|
|
|
|
892
|
|
|
|
|
|
|
=item B |
893
|
|
|
|
|
|
|
|
894
|
|
|
|
|
|
|
=item B |
895
|
|
|
|
|
|
|
|
896
|
|
|
|
|
|
|
=item B |
897
|
|
|
|
|
|
|
|
898
|
|
|
|
|
|
|
=item B |
899
|
|
|
|
|
|
|
|
900
|
|
|
|
|
|
|
=item B |
901
|
|
|
|
|
|
|
|
902
|
|
|
|
|
|
|
=item B |
903
|
|
|
|
|
|
|
|
904
|
|
|
|
|
|
|
=item B |
905
|
|
|
|
|
|
|
|
906
|
|
|
|
|
|
|
Fetches a playlist and returns a ref to an array of hashes, where each hash |
907
|
|
|
|
|
|
|
represents a video from the playlist with youtube id, runtime in seconds |
908
|
|
|
|
|
|
|
and title. On playlists with many videos, the method iteratively downloads |
909
|
|
|
|
|
|
|
pages until the playlist is complete. |
910
|
|
|
|
|
|
|
|
911
|
|
|
|
|
|
|
Optionally accepts a second argument, a hashref of options. Currently, you |
912
|
|
|
|
|
|
|
can pass a "limit" value to stop downloading of subsequent pages on larger |
913
|
|
|
|
|
|
|
playlists after x-amount of fetches (a limit of fetches, not playlist items). |
914
|
|
|
|
|
|
|
For example, pass 1 to only download the first page of videos from a playlist |
915
|
|
|
|
|
|
|
in order to "skim" the "tip" of new videos in a playlist. YouTube currently |
916
|
|
|
|
|
|
|
returns 100 videos at max per page. |
917
|
|
|
|
|
|
|
|
918
|
|
|
|
|
|
|
This method is used by the I script. |
919
|
|
|
|
|
|
|
|
920
|
|
|
|
|
|
|
=back |
921
|
|
|
|
|
|
|
|
922
|
|
|
|
|
|
|
=head1 CONTRIBUTORS |
923
|
|
|
|
|
|
|
|
924
|
|
|
|
|
|
|
yusukebe |
925
|
|
|
|
|
|
|
|
926
|
|
|
|
|
|
|
=head1 BUG REPORTING |
927
|
|
|
|
|
|
|
|
928
|
|
|
|
|
|
|
Please use github issues: L<< https://github.com/xaicron/p5-www-youtube-download/issues >>. |
929
|
|
|
|
|
|
|
|
930
|
|
|
|
|
|
|
=head1 SEE ALSO |
931
|
|
|
|
|
|
|
|
932
|
|
|
|
|
|
|
L and L. |
933
|
|
|
|
|
|
|
L |
934
|
|
|
|
|
|
|
L |
935
|
|
|
|
|
|
|
|
936
|
|
|
|
|
|
|
=head1 AUTHOR |
937
|
|
|
|
|
|
|
|
938
|
|
|
|
|
|
|
xaicron |
939
|
|
|
|
|
|
|
|
940
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
941
|
|
|
|
|
|
|
|
942
|
|
|
|
|
|
|
This software is copyright (c) 2013 by Yuji Shimada. |
943
|
|
|
|
|
|
|
|
944
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
945
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
946
|
|
|
|
|
|
|
|
947
|
|
|
|
|
|
|
=cut |
948
|
|
|
|
|
|
|
|
949
|
|
|
|
|
|
|
__END__ |