diff options
author | trizen <trizen@protonmail.com> | 2020-02-15 00:05:24 +0200 |
---|---|---|
committer | trizen <trizen@protonmail.com> | 2020-02-15 00:05:24 +0200 |
commit | 8e4ba906343718a4a1c2fbd939a9dbe0a75287f8 (patch) | |
tree | 340f4a502ddefbdfc0a5eec997751b5e19206aa8 /lib/WWW/StrawViewer.pm | |
parent | 3e92a9d96d2ff6d2718a5a1e0d69dac766f6a141 (diff) | |
download | fair-viewer-8e4ba906343718a4a1c2fbd939a9dbe0a75287f8.tar.lz fair-viewer-8e4ba906343718a4a1c2fbd939a9dbe0a75287f8.tar.xz fair-viewer-8e4ba906343718a4a1c2fbd939a9dbe0a75287f8.zip |
Import files.
Diffstat (limited to 'lib/WWW/StrawViewer.pm')
-rw-r--r-- | lib/WWW/StrawViewer.pm | 1045 |
1 files changed, 1045 insertions, 0 deletions
diff --git a/lib/WWW/StrawViewer.pm b/lib/WWW/StrawViewer.pm new file mode 100644 index 0000000..ecd31c9 --- /dev/null +++ b/lib/WWW/StrawViewer.pm @@ -0,0 +1,1045 @@ +package WWW::StrawViewer; + +use utf8; +use 5.016; +use warnings; + +use parent qw( + WWW::StrawViewer::Search + WWW::StrawViewer::Videos + WWW::StrawViewer::Channels + WWW::StrawViewer::Playlists + WWW::StrawViewer::ParseJSON + WWW::StrawViewer::Activities + WWW::StrawViewer::Subscriptions + WWW::StrawViewer::PlaylistItems + WWW::StrawViewer::CommentThreads + WWW::StrawViewer::Authentication + WWW::StrawViewer::VideoCategories + ); + +=head1 NAME + +WWW::StrawViewer - A very easy interface to YouTube. + +=cut + +our $VERSION = '3.7.4'; + +=head1 SYNOPSIS + + use WWW::StrawViewer; + + my $yv_obj = WWW::StrawViewer->new(); + ... + +=head1 SUBROUTINES/METHODS + +=cut + +my %valid_options = ( + + # Main options + v => {valid => q[], default => 3}, + page => {valid => [qr/^(?!0+\z)\d+\z/], default => 1}, + http_proxy => {valid => [qr{.}], default => undef}, + hl => {valid => [qr/^\w+(?:[\-_]\w+)?\z/], default => undef}, + maxResults => {valid => [1 .. 50], default => 10}, + topicId => {valid => [qr/^./], default => undef}, + order => {valid => [qw(relevance date rating viewCount title videoCount)], default => undef}, + publishedAfter => {valid => [qr/^\d+/], default => undef}, + publishedBefore => {valid => [qr/^\d+/], default => undef}, + channelId => {valid => [qr/^[-\w]{2,}\z/], default => undef}, + channelType => {valid => [qw(any show)], default => undef}, + + # Video only options + videoCaption => {valid => [qw(any closedCaption none)], default => undef}, + videoDefinition => {valid => [qw(any high standard)], default => undef}, + videoCategoryId => {valid => [qr/^\d+\z/], default => undef}, + videoDimension => {valid => [qw(any 2d 3d)], default => undef}, + videoDuration => {valid => [qw(any short medium long)], default => undef}, + videoEmbeddable => {valid => [qw(any true)], default => undef}, + videoLicense => {valid => [qw(any creativeCommon youtube)], default => undef}, + videoSyndicated => {valid => [qw(any true)], default => undef}, + eventType => {valid => [qw(completed live upcoming)], default => undef}, + chart => {valid => [qw(mostPopular)], default => 'mostPopular'}, + + regionCode => {valid => [qr/^[A-Z]{2}\z/i], default => undef}, + relevanceLanguage => {valid => [qr/^[a-z](?:\-\w+)?\z/i], default => undef}, + safeSearch => {valid => [qw(none moderate strict)], default => undef}, + videoType => {valid => [qw(any episode movie)], default => undef}, + + comments_order => {valid => [qw(time relevance)], default => 'time'}, + subscriptions_order => {valid => [qw(alphabetical relevance unread)], default => undef}, + + # Misc + debug => {valid => [0 .. 3], default => 0}, + lwp_timeout => {valid => [qr/^\d+\z/], default => 1}, + config_dir => {valid => [qr/^./], default => q{.}}, + cache_dir => {valid => [qr/^./], default => q{.}}, + + # Booleans + lwp_env_proxy => {valid => [1, 0], default => 1}, + escape_utf8 => {valid => [1, 0], default => 0}, + prefer_mp4 => {valid => [1, 0], default => 0}, + prefer_av1 => {valid => [1, 0], default => 0}, + + use_invidious_api => {valid => [1, 0], default => 0}, + + # API/OAuth + key => {valid => [qr/^.{15}/], default => undef}, + client_id => {valid => [qr/^.{15}/], default => undef}, + client_secret => {valid => [qr/^.{15}/], default => undef}, + redirect_uri => {valid => [qr/^.{15}/], default => undef}, + access_token => {valid => [qr/^.{15}/], default => undef}, + refresh_token => {valid => [qr/^.{15}/], default => undef}, + + authentication_file => {valid => [qr/^./], default => undef}, + + # No input value allowed + feeds_url => {valid => q[], default => 'https://invidio.us/api/v1/'}, + video_info_url => {valid => q[], default => 'https://www.youtube.com/get_video_info'}, + oauth_url => {valid => q[], default => 'https://accounts.google.com/o/oauth2/'}, + video_info_args => {valid => q[], default => '?video_id=%s&el=detailpage&ps=default&eurl=&gl=US&hl=en'}, + www_content_type => {valid => q[], default => 'application/x-www-form-urlencoded'}, + + # LWP user agent + lwp_agent => {valid => [qr/^.{5}/], default => 'Mozilla/5.0 (X11; U; Linux i686; gzip; en-US) Chrome/10.0.648.45'}, +); + +sub _our_smartmatch { + my ($value, $arg) = @_; + + $value // return 0; + + if (ref($arg) eq '') { + return ($value eq $arg); + } + + if (ref($arg) eq ref(qr//)) { + return scalar($value =~ $arg); + } + + if (ref($arg) eq 'ARRAY') { + foreach my $item (@$arg) { + return 1 if __SUB__->($value, $item); + } + } + + return 0; +} + +{ + no strict 'refs'; + + foreach my $key (keys %valid_options) { + + if (ref $valid_options{$key}{valid} eq 'ARRAY') { + + # Create the 'set_*' subroutines + *{__PACKAGE__ . '::set_' . $key} = sub { + my ($self, $value) = @_; + $self->{$key} = + _our_smartmatch($value, $valid_options{$key}{valid}) + ? $value + : $valid_options{$key}{default}; + }; + } + + # Create the 'get_*' subroutines + *{__PACKAGE__ . '::get_' . $key} = sub { + my ($self) = @_; + + if (not exists $self->{$key}) { + return ($self->{$key} = $valid_options{$key}{default}); + } + + $self->{$key}; + }; + } +} + +=head2 new(%opts) + +Returns a blessed object. + +=cut + +sub new { + my ($class, %opts) = @_; + + my $self = bless {}, $class; + + foreach my $key (keys %valid_options) { + if (exists $opts{$key}) { + my $method = "set_$key"; + $self->$method(delete $opts{$key}); + } + } + + foreach my $invalid_key (keys %opts) { + warn "Invalid key: '${invalid_key}'"; + } + + return $self; +} + +sub page_token { + my ($self) = @_; + + my $page = $self->get_page; + + # Don't generate the token for the first page + return undef if $page == 1; + + my $index = $page * $self->get_maxResults() - $self->get_maxResults(); + my $k = int($index / 128) - 1; + $index -= 128 * $k; + + my @f = (8, $index); + if ($k > 0 or $index > 127) { + push @f, $k + 1; + } + + require MIME::Base64; + MIME::Base64::encode_base64(pack('C*', @f, 16, 0)) =~ tr/=\n//dr; +} + +=head2 escape_string($string) + +Escapes a string with URI::Escape and returns it. + +=cut + +sub escape_string { + my ($self, $string) = @_; + + require URI::Escape; + + $self->get_escape_utf8 + ? URI::Escape::uri_escape_utf8($string) + : URI::Escape::uri_escape($string); +} + +=head2 set_lwp_useragent() + +Initializes the LWP::UserAgent module and returns it. + +=cut + +sub set_lwp_useragent { + my ($self) = @_; + + my $lwp = ( + eval { require LWP::UserAgent::Cached; 'LWP::UserAgent::Cached' } + // do { require LWP::UserAgent; 'LWP::UserAgent' } + ); + + $self->{lwp} = $lwp->new( + + timeout => $self->get_lwp_timeout, + show_progress => $self->get_debug, + agent => $self->get_lwp_agent, + + ssl_opts => {verify_hostname => 1, SSL_version => 'TLSv1_2'}, + + $lwp eq 'LWP::UserAgent::Cached' + ? ( + cache_dir => $self->get_cache_dir, + nocache_if => sub { + my ($response) = @_; + my $code = $response->code; + + $code >= 500 # do not cache any bad response + or $code == 401 # don't cache an unauthorized response + or $response->request->method ne 'GET' # cache only GET requests + + # don't cache if "cache-control" specifies "max-age=0" or "no-store" + or (($response->header('cache-control') // '') =~ /\b(?:max-age=0|no-store)\b/) + + # don't cache video or audio files + or (($response->header('content-type') // '') =~ /\b(?:video|audio)\b/); + }, + + recache_if => sub { + my ($response, $path) = @_; + not($response->is_fresh) # recache if the response expired + or ($response->code == 404 && -M $path > 1); # recache any 404 response older than 1 day + } + ) + : (), + + env_proxy => (defined($self->get_http_proxy) ? 0 : $self->get_lwp_env_proxy), + ); + + require LWP::ConnCache; + state $cache = LWP::ConnCache->new; + $cache->total_capacity(undef); # no limit + + state $accepted_encodings = do { + require HTTP::Message; + HTTP::Message::decodable(); + }; + + my $agent = $self->{lwp}; + $agent->ssl_opts(Timeout => 30); + $agent->default_header('Accept-Encoding' => $accepted_encodings); + $agent->conn_cache($cache); + $agent->proxy(['http', 'https'], $self->get_http_proxy) if defined($self->get_http_proxy); + + push @{$self->{lwp}->requests_redirectable}, 'POST'; + return $self->{lwp}; +} + +=head2 prepare_access_token() + +Returns a string. used as header, with the access token. + +=cut + +sub prepare_access_token { + my ($self) = @_; + + if (defined(my $auth = $self->get_access_token)) { + return "Bearer $auth"; + } + + return; +} + +sub _auth_lwp_header { + my ($self) = @_; + + my %lwp_header; + if (defined $self->get_access_token) { + $lwp_header{'Authorization'} = $self->prepare_access_token; + } + + return %lwp_header; +} + +sub _warn_reponse_error { + my ($resp, $url) = @_; + warn sprintf("[%s] Error occurred on URL: %s\n", $resp->status_line, $url =~ s/([&?])key=(.*?)&/${1}key=[...]&/r); +} + +=head2 lwp_get($url, %opt) + +Get and return the content for $url. + +Where %opt can be: + + simple => [bool] + +When the value of B<simple> is set to a true value, the +authentication header will not be set in the HTTP request. + +=cut + +sub lwp_get { + my ($self, $url, %opt) = @_; + + $url // return; + $self->{lwp} // $self->set_lwp_useragent(); + + my %lwp_header = ($opt{simple} ? () : $self->_auth_lwp_header); + my $response = $self->{lwp}->get($url, %lwp_header); + + if ($response->is_success) { + return $response->decoded_content; + } + + if ($response->status_line() =~ /^401 / and defined($self->get_refresh_token)) { + if (defined(my $refresh_token = $self->oauth_refresh_token())) { + if (defined $refresh_token->{access_token}) { + + $self->set_access_token($refresh_token->{access_token}); + + # Don't be tempted to use recursion here, because bad things will happen! + $response = $self->{lwp}->get($url, $self->_auth_lwp_header); + + if ($response->is_success) { + $self->save_authentication_tokens(); + return $response->decoded_content; + } + elsif ($response->status_line() =~ /^401 /) { + $self->set_refresh_token(); # refresh token was invalid + $self->set_access_token(); # access token is also broken + warn "[!] Can't refresh the access token! Logging out...\n"; + } + } + else { + warn "[!] Can't get the access_token! Logging out...\n"; + $self->set_refresh_token(); + $self->set_access_token(); + } + } + else { + warn "[!] Invalid refresh_token! Logging out...\n"; + $self->set_refresh_token(); + $self->set_access_token(); + } + } + + $opt{depth} ||= 0; + + # Try again on 500+ HTTP errors + if ( $opt{depth} < 3 + and $response->code() >= 500 + and $response->status_line() =~ /(?:Temporary|Server) Error|Timeout|Service Unavailable/i) { + return $self->lwp_get($url, %opt, depth => $opt{depth} + 1); + } + + _warn_reponse_error($response, $url); + return; +} + +=head2 lwp_post($url, [@args]) + +Post and return the content for $url. + +=cut + +sub lwp_post { + my ($self, $url, @args) = @_; + + $self->{lwp} // $self->set_lwp_useragent(); + + my $response = $self->{lwp}->post($url, @args); + + if ($response->is_success) { + return $response->decoded_content; + } + else { + _warn_reponse_error($response, $url); + } + + return; +} + +=head2 lwp_mirror($url, $output_file) + +Downloads the $url into $output_file. Returns true on success. + +=cut + +sub lwp_mirror { + my ($self, $url, $output_file) = @_; + $self->{lwp} // $self->set_lwp_useragent(); + $self->{lwp}->mirror($url, $output_file); +} + +sub _get_results { + my ($self, $url, %opt) = @_; + + return + scalar { + url => $url, + results => $self->parse_json_string($self->lwp_get($url, %opt)), + }; +} + +=head2 list_to_url_arguments(\%options) + +Returns a valid string of arguments, with defined values. + +=cut + +sub list_to_url_arguments { + my ($self, %args) = @_; + join(q{&}, map { "$_=$args{$_}" } grep { defined $args{$_} } sort keys %args); +} + +sub _append_url_args { + my ($self, $url, %args) = @_; + %args + ? ($url . ($url =~ /\?/ ? '&' : '?') . $self->list_to_url_arguments(%args)) + : $url; +} + +sub _simple_feeds_url { + my ($self, $suburl, %args) = @_; + $self->get_feeds_url() . $suburl . '?' . $self->list_to_url_arguments(key => $self->get_key, %args); +} + +=head2 default_arguments(%args) + +Merge the default arguments with %args and concatenate them together. + +=cut + +sub default_arguments { + my ($self, %args) = @_; + + my %defaults = ( + #key => $self->get_key, + #part => 'snippet', + #prettyPrint => 'false', + #maxResults => $self->get_maxResults, + #regionCode => $self->get_regionCode, + %args, + ); + + $self->list_to_url_arguments(%defaults); +} + +sub _make_feed_url { + my ($self, $path, %args) = @_; + $self->get_feeds_url() . $path . '?' . $self->default_arguments(%args); +} + +sub _extract_from_invidious { + my ($self, $videoID) = @_; + + my $url = sprintf("https://invidio.us/api/v1/videos/%s?fields=formatStreams,adaptiveFormats", $videoID); + + my $tries = 3; + my $resp = $self->{lwp}->get($url); + + while (not $resp->is_success() and $resp->status_line() =~ /read timeout/i and --$tries >= 0) { + $resp = $self->{lwp}->get($url); + } + + $resp->is_success() || return; + + my $json = $resp->decoded_content() // return; + my $ref = $self->parse_json_string($json) // return; + + my @formats; + + # The entries are already in the format that we want. + if (exists($ref->{adaptiveFormats}) and ref($ref->{adaptiveFormats}) eq 'ARRAY') { + push @formats, @{$ref->{adaptiveFormats}}; + } + + if (exists($ref->{formatStreams}) and ref($ref->{formatStreams}) eq 'ARRAY') { + push @formats, @{$ref->{formatStreams}}; + } + + return @formats; +} + +sub _extract_from_ytdl { + my ($self, $videoID) = @_; + + ((state $x = system('youtube-dl', '--version')) == 0) || return; + + my $json = $self->proxy_stdout('youtube-dl', '--all-formats', '--dump-single-json', + quotemeta("https://www.youtube.com/watch?v=" . $videoID)); + + my $ref = $self->parse_json_string($json); + + my @formats; + if (ref($ref) eq 'HASH' and exists($ref->{formats}) and ref($ref->{formats}) eq 'ARRAY') { + foreach my $format (@{$ref->{formats}}) { + if (exists($format->{format_id}) and exists($format->{url})) { + + my $entry = { + itag => $format->{format_id}, + url => $format->{url}, + type => ((($format->{format} // '') =~ /audio only/i) ? 'audio/' : 'video/') . $format->{ext}, + }; + + push @formats, $entry; + } + } + } + + return @formats; +} + +sub _fallback_extract_urls { + my ($self, $videoID) = @_; + + my @formats; + + if ($self->get_use_invidious_api) { # use the API of invidio.us + + if ($self->get_debug) { + say STDERR ":: Using invidio.us to extract the streaming URLs..."; + } + + push @formats, $self->_extract_from_invidious($videoID); + + if ($self->get_debug) { + say STDERR ":: Found ", scalar(@formats), " streaming URLs."; + } + + @formats && return @formats; + } + + if ($self->get_debug) { + say STDERR ":: Using youtube-dl to extract the streaming URLs..."; + } + + push @formats, $self->_extract_from_ytdl($videoID); + + if ($self->get_debug) { + my $count = scalar(@formats); + say STDERR ":: Found $count streaming URLs..."; + } + + return @formats; +} + +=head2 parse_query_string($string, multi => [0,1]) + +Parse a query string and return a data structure back. + +When the B<multi> option is set to a true value, the function will store multiple values for a given key. + +Returns back a list of key-value pairs. + +=cut + +sub parse_query_string { + my ($self, $str, %opt) = @_; + + if (not defined($str)) { + return; + } + + require URI::Escape; + + my @pairs; + foreach my $statement (split(/,/, $str)) { + foreach my $pair (split(/&/, $statement)) { + push @pairs, $pair; + } + } + + my %result; + + foreach my $pair (@pairs) { + my ($key, $value) = split(/=/, $pair, 2); + + if (not defined($value) or $value eq '') { + next; + } + + $value = URI::Escape::uri_unescape($value =~ tr/+/ /r); + + if ($opt{multi}) { + push @{$result{$key}}, $value; + } + else { + $result{$key} = $value; + } + } + + return %result; +} + +sub _group_keys_with_values { + my ($self, %data) = @_; + + my @hashes; + + foreach my $key (keys %data) { + foreach my $i (0 .. $#{$data{$key}}) { + $hashes[$i]{$key} = $data{$key}[$i]; + } + } + + return @hashes; +} + +sub _old_extract_streaming_urls { + my ($self, $info, $videoID) = @_; + + if ($self->get_debug) { + say STDERR ":: Using `url_encoded_fmt_stream_map` to extract the streaming URLs..."; + } + + my %stream_map = $self->parse_query_string($info->{url_encoded_fmt_stream_map}, multi => 1); + my %adaptive_fmts = $self->parse_query_string($info->{adaptive_fmts}, multi => 1); + + if ($self->get_debug >= 2) { + require Data::Dump; + Data::Dump::pp(\%stream_map); + Data::Dump::pp(\%adaptive_fmts); + } + + my @results; + + push @results, $self->_group_keys_with_values(%stream_map); + push @results, $self->_group_keys_with_values(%adaptive_fmts); + + foreach my $video (@results) { + if (exists $video->{s}) { # has an encrypted signature :( + + if ($self->get_debug) { + say STDERR ":: Detected an encrypted signature..."; + } + + my @formats = $self->_fallback_extract_urls($videoID); + + foreach my $format (@formats) { + foreach my $ref (@results) { + if (defined($ref->{itag}) and ($ref->{itag} eq $format->{itag})) { + $ref->{url} = $format->{url}; + last; + } + } + } + + last; + } + } + + if ($info->{livestream} or $info->{live_playback}) { + + if ($self->get_debug) { + say STDERR ":: Live stream detected..."; + } + + if (my @formats = $self->_fallback_extract_urls($videoID)) { + @results = @formats; + } + elsif (exists $info->{hlsvp}) { + push @results, + { + itag => 38, + type => 'video/ts', + url => $info->{hlsvp}, + }; + } + } + + if ($self->get_debug) { + my $count = scalar(@results); + say STDERR ":: Found $count streaming URLs..."; + } + + return @results; +} + +sub _extract_streaming_urls { + my ($self, $info, $videoID) = @_; + + if (exists $info->{url_encoded_fmt_stream_map}) { + return $self->_old_extract_streaming_urls($info, $videoID); + } + + if ($self->get_debug) { + say STDERR ":: Using `player_response` to extract the streaming URLs..."; + } + + my $json = $self->parse_json_string($info->{player_response} // return); + + if ($self->get_debug >= 2) { + require Data::Dump; + Data::Dump::pp($json); + } + + ref($json) eq 'HASH' or return; + + my @results; + if (exists $json->{streamingData}) { + + my $streamingData = $json->{streamingData}; + + if (exists $streamingData->{adaptiveFormats}) { + push @results, @{$streamingData->{adaptiveFormats}}; + } + + if (exists $streamingData->{formats}) { + push @results, @{$streamingData->{formats}}; + } + } + + foreach my $item (@results) { + + if (exists $item->{cipher} and not exists $item->{url}) { + + my %data = $self->parse_query_string($item->{cipher}); + + $item->{url} = $data{url} if defined($data{url}); + + if (defined($data{s})) { # unclear how this can be decrypted... + require URI::Escape; + my $sig = $data{s}; + $sig = URI::Escape::uri_escape($sig); + $item->{url} .= "&sig=$sig"; + } + } + + if (exists $item->{mimeType}) { + $item->{type} = $item->{mimeType}; + } + } + + # Cipher streaming URLs are currently unsupported, so let's filter them out. + @results = grep { not exists $_->{cipher} } @results; + + # Keep only streams with contentLength > 0. + @results = grep { exists($_->{contentLength}) and $_->{contentLength} > 0 } @results; + + # Detect livestream + if (!@results and exists($json->{streamingData}) and exists($json->{streamingData}{hlsManifestUrl})) { + + if ($self->get_debug) { + say STDERR ":: Live stream detected..."; + } + + @results = $self->_fallback_extract_urls($videoID); + + if (!@results) { + push @results, + { + itag => 38, + type => "video/ts", + url => $json->{streamingData}{hlsManifestUrl}, + }; + } + } + + if ($self->get_debug) { + my $count = scalar(@results); + say STDERR ":: Found $count streaming URLs..."; + } + + return @results; +} + +sub _get_video_info { + my ($self, $videoID) = @_; + + my $url = $self->get_video_info_url() . sprintf($self->get_video_info_args(), $videoID); + my $content = $self->lwp_get($url, simple => 1) // return; + my %info = $self->parse_query_string($content); + + return %info; +} + +=head2 get_streaming_urls($videoID) + +Returns a list of streaming URLs for a videoID. +({itag=>..., url=>...}, {itag=>..., url=>....}, ...) + +=cut + +sub get_streaming_urls { + my ($self, $videoID) = @_; + + my %info = $self->_get_video_info($videoID); + my @streaming_urls = $self->_extract_streaming_urls(\%info, $videoID); + + my @caption_urls; + if (exists $info{player_response}) { + + require URI::Escape; + my $captions_json = URI::Escape::uri_unescape($info{player_response}); + my $caption_data = $self->parse_json_string($captions_json); + + if (eval { ref($caption_data->{captions}{playerCaptionsTracklistRenderer}{captionTracks}) eq 'ARRAY' }) { + push @caption_urls, @{$caption_data->{captions}{playerCaptionsTracklistRenderer}{captionTracks}}; + } + } + + # Try again with youtube-dl + if (!@streaming_urls or $info{status} =~ /fail|error/i) { + @streaming_urls = $self->_fallback_extract_urls($videoID); + } + + if ($self->get_prefer_mp4 or $self->get_prefer_av1) { + + my @video_urls; + my @audio_urls; + + require WWW::StrawViewer::Itags; + + my %audio_itags; + @audio_itags{@{WWW::StrawViewer::Itags->get_itags->{audio}}} = (); + + foreach my $url (@streaming_urls) { + + if (exists($audio_itags{$url->{itag}})) { + push @audio_urls, $url; + next; + } + + if ($url->{type} =~ /\bvideo\b/i) { + if ($self->get_prefer_mp4 and $url->{type} =~ /\bmp4\b/i) { + push @video_urls, $url; + } + elsif ($self->get_prefer_av1 and $url->{type} =~ /\bav[0-9]+\b/i) { + push @video_urls, $url; + } + } + else { + push @audio_urls, $url; + } + } + + if (@video_urls) { + @streaming_urls = (@video_urls, @audio_urls); + } + } + + # Filter out streams with `clen = 0`. + @streaming_urls = grep { defined($_->{clen}) ? ($_->{clen} > 0) : 1 } @streaming_urls; + + # Return the YouTube URL when there are no streaming URLs + if (!@streaming_urls) { + push @streaming_urls, + { + itag => 38, + type => "video/mp4", + url => "https://www.youtube.com/watch?v=$videoID", + }; + } + + if ($self->get_debug >= 2) { + require Data::Dump; + Data::Dump::pp(\%info) if ($self->get_debug >= 3); + Data::Dump::pp(\@streaming_urls); + Data::Dump::pp(\@caption_urls); + } + + return (\@streaming_urls, \@caption_urls, \%info); +} + +sub _request { + my ($self, $req) = @_; + + $self->{lwp} // $self->set_lwp_useragent(); + + my $res = $self->{lwp}->request($req); + + if ($res->is_success) { + return $res->decoded_content; + } + else { + warn 'Request error: ' . $res->status_line(); + } + + return; +} + +sub _prepare_request { + my ($self, $req, $length) = @_; + + $req->header('Content-Length' => $length) if ($length); + + if (defined $self->get_access_token) { + $req->header('Authorization' => $self->prepare_access_token); + } + + return 1; +} + +sub _save { + my ($self, $method, $uri, $content) = @_; + + require HTTP::Request; + my $req = HTTP::Request->new($method => $uri); + $req->content_type('application/json; charset=UTF-8'); + $self->_prepare_request($req, length($content)); + $req->content($content); + + $self->_request($req); +} + +sub post_as_json { + my ($self, $url, $ref) = @_; + my $json_str = $self->make_json_string($ref); + $self->_save('POST', $url, $json_str); +} + +# SUBROUTINE FACTORY +{ + no strict 'refs'; + + # Create {next,previous}_page subroutines + foreach my $name ('next_page', 'previous_page') { + *{__PACKAGE__ . '::' . $name} = sub { + my ($self, $url, $token) = @_; + + my $pt_url = ( + $url =~ s/[?&]pageToken=\K[^&]+/$token/ + ? $url + : $self->_append_url_args($url, pageToken => $token) + ); + + my $res = $self->_get_results($pt_url); + $res->{url} = $pt_url; + return $res; + }; + } + + # Create proxy_{exec,system} subroutines + foreach my $name ('exec', 'system', 'stdout') { + *{__PACKAGE__ . '::proxy_' . $name} = sub { + my ($self, @args) = @_; + + $self->{lwp} // $self->set_lwp_useragent(); + + local $ENV{http_proxy} = $self->{lwp}->proxy('http'); + local $ENV{https_proxy} = $self->{lwp}->proxy('https'); + + local $ENV{HTTP_PROXY} = $self->{lwp}->proxy('http'); + local $ENV{HTTPS_PROXY} = $self->{lwp}->proxy('https'); + + $name eq 'exec' ? exec(@args) + : $name eq 'system' ? system(@args) + : $name eq 'stdout' ? qx(@args) + : (); + }; + } +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + +=head1 SEE ALSO + +https://developers.google.com/youtube/v3/docs/ + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012-2015 Trizen. + +This program is free software; you can redistribute it and/or modify it +under the terms of the the Artistic License (2.0). You may obtain a +copy of the full license at: + +L<http://www.perlfoundation.org/artistic_license_2_0> + +Any use, modification, and distribution of the Standard or Modified +Versions is governed by this Artistic License. By using, modifying or +distributing the Package, you accept this license. Do not use, modify, +or distribute the Package, if you do not accept this license. + +If your Modified Version has been derived from a Modified Version made +by someone other than you, you are nevertheless required to ensure that +your Modified Version complies with the requirements of this license. + +This license does not grant you the right to use any trademark, service +mark, tradename, or logo of the Copyright Holder. + +This license includes the non-exclusive, worldwide, free-of-charge +patent license to make, have made, use, offer to sell, sell, import and +otherwise transfer the Package with respect to any patent claims +licensable by the Copyright Holder that are necessarily infringed by the +Package. If you institute patent litigation (including a cross-claim or +counterclaim) against any party alleging that the Package constitutes +direct or contributory patent infringement, then this Artistic License +to you shall terminate on the date that such litigation is filed. + +Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER +AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. +THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY +YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR +CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR +CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +=cut + +1; # End of WWW::StrawViewer + +__END__ |