diff options
Diffstat (limited to 'lib/WWW/FairViewer/InitialData.pm')
-rw-r--r-- | lib/WWW/FairViewer/InitialData.pm | 1050 |
1 files changed, 1050 insertions, 0 deletions
diff --git a/lib/WWW/FairViewer/InitialData.pm b/lib/WWW/FairViewer/InitialData.pm new file mode 100644 index 0000000..50ea500 --- /dev/null +++ b/lib/WWW/FairViewer/InitialData.pm @@ -0,0 +1,1050 @@ +package WWW::FairViewer::InitialData; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::FairViewer::InitialData - Extract initial data. + +=head1 SYNOPSIS + + use WWW::FairViewer; + my $obj = WWW::FairViewer->new(%opts); + + my $results = $obj->yt_search(q => $keywords); + my $playlists = $obj->yt_channel_playlists($channel_ID); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _time_to_seconds { + my ($time) = @_; + + my ($hours, $minutes, $seconds) = (0, 0, 0); + + if ($time =~ /(\d+):(\d+):(\d+)/) { + ($hours, $minutes, $seconds) = ($1, $2, $3); + } + elsif ($time =~ /(\d+):(\d+)/) { + ($minutes, $seconds) = ($1, $2); + } + elsif ($time =~ /(\d+)/) { + $seconds = $1; + } + + $hours * 3600 + $minutes * 60 + $seconds; +} + +sub _human_number_to_int { + my ($text) = @_; + + # 7.6K -> 7600; 7.6M -> 7600000 + if ($text =~ /([\d,.]+)\s*([KMB])/i) { + + my $v = $1; + my $u = $2; + my $m = ($u eq 'K' ? 1e3 : ($u eq 'M' ? 1e6 : ($u eq 'B' ? 1e9 : 1))); + + $v =~ tr/,/./; + + return int($v * $m); + } + + if ($text =~ /([\d,.]+)/) { + my $v = $1; + $v =~ tr/,.//d; + return int($v); + } + + return 0; +} + +sub _thumbnail_quality { + my ($width) = @_; + + $width // return 'medium'; + + if ($width == 1280) { + return "maxres"; + } + + if ($width == 640) { + return "sddefault"; + } + + if ($width == 480) { + return 'high'; + } + + if ($width == 320) { + return 'medium'; + } + + if ($width == 120) { + return 'default'; + } + + if ($width <= 120) { + return 'small'; + } + + if ($width <= 176) { + return 'medium'; + } + + if ($width <= 480) { + return 'high'; + } + + if ($width <= 640) { + return 'sddefault'; + } + + if ($width <= 1280) { + return "maxres"; + } + + return 'medium'; +} + +sub _fix_url_protocol { + my ($url) = @_; + + $url // return undef; + + if ($url =~ m{^https://}) { # ok + return $url; + } + if ($url =~ s{^.*?//}{}) { + return "https://" . $url; + } + if ($url =~ /^\w+\./) { + return "https://" . $url; + } + + return $url; +} + +sub _unscramble { + my ($str) = @_; + + my $i = my $l = length($str); + + $str =~ s/(.)(.{$i})/$2$1/sg while (--$i > 0); + $str =~ s/(.)(.{$i})/$2$1/sg while (++$i < $l); + + return $str; +} + +sub _extract_youtube_mix { + my ($self, $data) = @_; + + my $info = eval { $data->{callToAction}{watchCardHeroVideoRenderer} } || return; + my $header = eval { $data->{header}{watchCardRichHeaderRenderer} }; + + my %mix; + + $mix{type} = 'playlist'; + + $mix{title} = + eval { $header->{title}{runs}[0]{text} } + // eval { $info->{accessibility}{accessibilityData}{label} } + // eval { $info->{callToActionButton}{callToActionButtonRenderer}{label}{runs}[0]{text} } // 'Youtube Mix'; + + $mix{playlistId} = eval { $info->{navigationEndpoint}{watchEndpoint}{playlistId} } || return; + + $mix{playlistThumbnail} = eval { _fix_url_protocol($header->{avatar}{thumbnails}[0]{url}) } + // eval { _fix_url_protocol($info->{heroImage}{collageHeroImageRenderer}{leftThumbnail}{thumbnails}[0]{url}) }; + + $mix{description} = _extract_description({title => $info}); + + $mix{author} = eval { $header->{title}{runs}[0]{text} } // "YouTube"; + $mix{authorId} = eval { $header->{titleNavigationEndpoint}{browseEndpoint}{browseId} } // "youtube"; + + return \%mix; +} + +sub _extract_author_name { + my ($info) = @_; + eval { $info->{longBylineText}{runs}[0]{text} } // eval { $info->{shortBylineText}{runs}[0]{text} }; +} + +sub _extract_video_id { + my ($info) = @_; + eval { $info->{videoId} } || eval { $info->{navigationEndpoint}{watchEndpoint}{videoId} } || undef; +} + +sub _extract_length_seconds { + my ($info) = @_; + eval { $info->{lengthSeconds} } + || _time_to_seconds(eval { $info->{thumbnailOverlays}[0]{thumbnailOverlayTimeStatusRenderer}{text}{runs}[0]{text} } // 0) + || _time_to_seconds(eval { $info->{lengthText}{runs}[0]{text} // 0 }); +} + +sub _extract_published_text { + my ($info) = @_; + + my $text = eval { $info->{publishedTimeText}{runs}[0]{text} } || return undef; + + if ($text =~ /(\d+)\s+(\w+)/) { + return "$1 $2 ago"; + } + + if ($text =~ /(\d+)\s*(\w+)/) { + return "$1 $2 ago"; + } + + return $text; +} + +sub _extract_channel_id { + my ($info) = @_; + eval { $info->{channelId} } + // eval { $info->{shortBylineText}{runs}[0]{navigationEndpoint}{browseEndpoint}{browseId} } + // eval { $info->{navigationEndpoint}{browseEndpoint}{browseId} }; +} + +sub _extract_view_count_text { + my ($info) = @_; + eval { $info->{shortViewCountText}{runs}[0]{text} }; +} + +sub _extract_thumbnails { + my ($info) = @_; + eval { + [ + map { + my %thumb = %$_; + $thumb{quality} = _thumbnail_quality($thumb{width}); + $thumb{url} = _fix_url_protocol($thumb{url}); + \%thumb; + } @{$info->{thumbnail}{thumbnails}} + ] + }; +} + +sub _extract_playlist_thumbnail { + my ($info) = @_; + eval { + _fix_url_protocol( + ( + grep { _thumbnail_quality($_->{width}) =~ /medium|high/ } + @{$info->{thumbnailRenderer}{playlistVideoThumbnailRenderer}{thumbnail}{thumbnails}} + )[0]{url} // $info->{thumbnailRenderer}{playlistVideoThumbnailRenderer}{thumbnail}{thumbnails}[0]{url} + ); + } // eval { + _fix_url_protocol((grep { _thumbnail_quality($_->{width}) =~ /medium|high/ } @{$info->{thumbnail}{thumbnails}})[0]{url} + // $info->{thumbnail}{thumbnails}[0]{url}); + }; +} + +sub _extract_title { + my ($info) = @_; + eval { $info->{title}{runs}[0]{text} } // eval { $info->{title}{accessibility}{accessibilityData}{label} }; +} + +sub _extract_description { + my ($info) = @_; + + # FIXME: this is not the video description + eval { $info->{title}{accessibility}{accessibilityData}{label} }; +} + +sub _extract_view_count { + my ($info) = @_; + _human_number_to_int(eval { $info->{viewCountText}{runs}[0]{text} } || 0); +} + +sub _extract_video_count { + my ($info) = @_; + _human_number_to_int( eval { $info->{videoCountShortText}{runs}[0]{text} } + || eval { $info->{videoCountText}{runs}[0]{text} } + || 0); +} + +sub _extract_subscriber_count { + my ($info) = @_; + _human_number_to_int(eval { $info->{subscriberCountText}{runs}[0]{text} } || 0); +} + +sub _extract_playlist_id { + my ($info) = @_; + eval { $info->{playlistId} }; +} + +sub _extract_itemSection_entry { + my ($self, $data, %args) = @_; + + ref($data) eq 'HASH' or return; + + # Album + if ($args{type} eq 'all' and exists $data->{horizontalCardListRenderer}) { # TODO + return; + } + + # Video + if (exists($data->{compactVideoRenderer}) or exists($data->{playlistVideoRenderer})) { + + my %video; + my $info = $data->{compactVideoRenderer} // $data->{playlistVideoRenderer}; + + $video{type} = 'video'; + + # Deleted video + if (defined(eval { $info->{isPlayable} }) and not $info->{isPlayable}) { + return; + } + + $video{videoId} = _extract_video_id($info) // return; + $video{title} = _extract_title($info) // return; + $video{lengthSeconds} = _extract_length_seconds($info) || 0; + $video{liveNow} = ($video{lengthSeconds} == 0); + $video{author} = _extract_author_name($info); + $video{authorId} = _extract_channel_id($info); + $video{publishedText} = _extract_published_text($info); + $video{viewCountText} = _extract_view_count_text($info); + $video{videoThumbnails} = _extract_thumbnails($info); + $video{description} = _extract_description($info); + $video{viewCount} = _extract_view_count($info); + + # Filter out private/deleted videos from playlists + if (exists($data->{playlistVideoRenderer})) { + $video{author} // return; + $video{authorId} // return; + } + + return \%video; + } + + # Playlist + if ($args{type} ne 'video' and exists $data->{compactPlaylistRenderer}) { + + my %playlist; + my $info = $data->{compactPlaylistRenderer}; + + $playlist{type} = 'playlist'; + + $playlist{title} = _extract_title($info) // return; + $playlist{playlistId} = _extract_playlist_id($info) // return; + $playlist{author} = _extract_author_name($info); + $playlist{authorId} = _extract_channel_id($info); + $playlist{videoCount} = _extract_video_count($info); + $playlist{playlistThumbnail} = _extract_playlist_thumbnail($info); + $playlist{description} = _extract_description($info); + + return \%playlist; + } + + # Channel + if ($args{type} ne 'video' and exists $data->{compactChannelRenderer}) { + + my %channel; + my $info = $data->{compactChannelRenderer}; + + $channel{type} = 'channel'; + + $channel{author} = _extract_title($info) // return; + $channel{authorId} = _extract_channel_id($info) // return; + $channel{subCount} = _extract_subscriber_count($info); + $channel{videoCount} = _extract_video_count($info); + $channel{authorThumbnails} = _extract_thumbnails($info); + $channel{description} = _extract_description($info); + + return \%channel; + } + + return; +} + +sub _parse_itemSection { + my ($self, $entry, %args) = @_; + + eval { ref($entry->{contents}) eq 'ARRAY' } || return; + + my @results; + + foreach my $entry (@{$entry->{contents}}) { + + my $item = $self->_extract_itemSection_entry($entry, %args); + + if (defined($item) and ref($item) eq 'HASH') { + push @results, $item; + } + } + + if (exists($entry->{continuations}) and ref($entry->{continuations}) eq 'ARRAY') { + + my $token = eval { $entry->{continuations}[0]{nextContinuationData}{continuation} }; + + if (defined($token)) { + push @results, + scalar { + type => 'nextpage', + token => "ytplaylist:$args{type}:$token", + }; + } + } + + return @results; +} + +sub _parse_itemSection_nextpage { + my ($self, $entry, %args) = @_; + + eval { ref($entry->{contents}) eq 'ARRAY' } || return; + + foreach my $entry (@{$entry->{contents}}) { + + # Continuation page + if (exists $entry->{continuationItemRenderer}) { + + my $info = $entry->{continuationItemRenderer}; + my $token = eval { $info->{continuationEndpoint}{continuationCommand}{token} }; + + if (defined($token)) { + return + scalar { + type => 'nextpage', + token => "ytbrowse:$args{type}:$token", + }; + } + } + } + + return; +} + +sub _extract_sectionList_results { + my ($self, $data, %args) = @_; + + eval { ref($data->{contents}) eq 'ARRAY' } or return; + + my @results; + + foreach my $entry (@{$data->{contents}}) { + + # Playlists + if (eval { ref($entry->{shelfRenderer}{content}{verticalListRenderer}{items}) eq 'ARRAY' }) { + my $res = {contents => $entry->{shelfRenderer}{content}{verticalListRenderer}{items}}; + push @results, $self->_parse_itemSection($res, %args); + push @results, $self->_parse_itemSection_nextpage($res, %args); + next; + } + + # Playlist videos + if (eval { ref($entry->{itemSectionRenderer}{contents}[0]{playlistVideoListRenderer}{contents}) eq 'ARRAY' }) { + my $res = $entry->{itemSectionRenderer}{contents}[0]{playlistVideoListRenderer}; + push @results, $self->_parse_itemSection($res, %args); + push @results, $self->_parse_itemSection_nextpage($res, %args); + next; + } + + # YouTube Mix + if ($args{type} eq 'all' and exists $entry->{universalWatchCardRenderer}) { + + my $mix = $self->_extract_youtube_mix($entry->{universalWatchCardRenderer}); + + if (defined($mix)) { + push(@results, $mix); + } + } + + # Video results + if (exists $entry->{itemSectionRenderer}) { + my $res = $entry->{itemSectionRenderer}; + push @results, $self->_parse_itemSection($res, %args); + push @results, $self->_parse_itemSection_nextpage($res, %args); + } + + # Continuation page + if (exists $entry->{continuationItemRenderer}) { + + my $info = $entry->{continuationItemRenderer}; + my $token = eval { $info->{continuationEndpoint}{continuationCommand}{token} }; + + if (defined($token)) { + push @results, + scalar { + type => 'nextpage', + token => "ytsearch:$args{type}:$token", + }; + } + } + } + + if (@results and exists $data->{continuations}) { + push @results, $self->_parse_itemSection($data, %args); + } + + return @results; +} + +sub _extract_channel_header { + my ($self, $data, %args) = @_; + eval { $data->{header}{c4TabbedHeaderRenderer} } // eval { $data->{metadata}{channelMetadataRenderer} }; +} + +sub _add_author_to_results { + my ($self, $data, $results, %args) = @_; + + my $header = $self->_extract_channel_header($data, %args); + + my $channel_id = eval { $header->{channelId} } // eval { $header->{externalId} }; + my $channel_name = eval { $header->{title} }; + + foreach my $result (@$results) { + if (ref($result) eq 'HASH') { + $result->{author} = $channel_name if defined($channel_name); + $result->{authorId} = $channel_id if defined($channel_id); + } + } + + return 1; +} + +sub _find_sectionList { + my ($self, $data) = @_; + + eval { + ( + grep { + eval { exists($_->{tabRenderer}{content}{sectionListRenderer}{contents}) } + } @{$data->{contents}{singleColumnBrowseResultsRenderer}{tabs}} + )[0]{tabRenderer}{content}{sectionListRenderer}; + } // undef; +} + +sub _extract_channel_uploads { + my ($self, $data, %args) = @_; + + my @results = $self->_extract_sectionList_results($self->_find_sectionList($data), %args); + $self->_add_author_to_results($data, \@results, %args); + return @results; +} + +sub _extract_channel_playlists { + my ($self, $data, %args) = @_; + + my @results = $self->_extract_sectionList_results($self->_find_sectionList($data), %args); + $self->_add_author_to_results($data, \@results, %args); + return @results; +} + +sub _extract_playlist_videos { + my ($self, $data, %args) = @_; + + my @results = $self->_extract_sectionList_results($self->_find_sectionList($data), %args); + $self->_add_author_to_results($data, \@results, %args); + return @results; +} + +sub _get_initial_data { + my ($self, $url) = @_; + + $self->get_prefer_invidious() and return; + + my $content = $self->lwp_get($url) // return; + + if ($content =~ m{var\s+ytInitialData\s*=\s*'(.*?)'}is) { + my $json = $1; + + $json =~ s{\\x([[:xdigit:]]{2})}{chr(hex($1))}ge; + $json =~ s{\\u([[:xdigit:]]{4})}{chr(hex($1))}ge; + $json =~ s{\\(["&])}{$1}g; + + my $hash = $self->parse_utf8_json_string($json); + return $hash; + } + + if ($content =~ m{<div id="initial-data"><!--(.*?)--></div>}is) { + my $json = $1; + my $hash = $self->parse_utf8_json_string($json); + return $hash; + } + + return; +} + +sub _channel_data { + my ($self, $channel, %args) = @_; + + state $yv_utils = WWW::FairViewer::Utils->new(); + + my $url = $self->get_m_youtube_url; + + if ($yv_utils->is_channelID($channel)) { + $url .= "/channel/$channel/$args{type}"; + } + else { + $url .= "/c/$channel/$args{type}"; + } + + my %params = (hl => "en"); + + if (defined(my $sort = $args{sort_by})) { + if ($sort eq 'popular') { + $params{sort} = 'p'; + } + elsif ($sort eq 'old') { + $params{sort} = 'da'; + } + } + + if (exists($args{params}) and ref($args{params}) eq 'HASH') { + %params = (%params, %{$args{params}}); + } + + $url = $self->_append_url_args($url, %params); + my $result = $self->_get_initial_data($url); + + # When /c/ failed, try /user/ + if ((!defined($result) or !scalar(keys %$result)) and $url =~ s{/c/}{/user/}) { + $result = $self->_get_initial_data($url); + } + + ($url, $result); +} + +sub _prepare_results_for_return { + my ($self, $results, %args) = @_; + + (defined($results) and ref($results) eq 'ARRAY') || return; + + my @results = @$results; + + @results || return; + + if (@results and $results[-1]{type} eq 'nextpage') { + + my $nextpage = pop(@results); + + if (defined($nextpage->{token}) and @results) { + + if ($self->get_debug) { + say STDERR ":: Returning results with a continuation page token..."; + } + + return { + url => $args{url}, + results => { + entries => \@results, + continuation => $nextpage->{token}, + }, + }; + } + } + + my $url = $args{url}; + + if ($url =~ m{^https://m\.youtube\.com}) { + $url = undef; + } + + return { + url => $url, + results => \@results, + }; +} + +=head2 yt_search(q => $keyword, %args) + +Search for videos given a keyword string (uri-escaped). + +=cut + +sub yt_search { + my ($self, %args) = @_; + + my $url = $self->get_m_youtube_url . "/results?search_query=$args{q}"; + + my @sp; + my %params = (hl => 'en',); + + $args{type} //= 'video'; + + if ($args{type} eq 'video') { + + if (defined(my $duration = $self->get_videoDuration)) { + if ($duration eq 'long') { + push @sp, 'EgQQARgC'; + } + elsif ($duration eq 'short') { + push @sp, 'EgQQARgB'; + } + } + + if (defined(my $date = $self->get_date)) { + if ($date eq 'hour') { + push @sp, 'EgQIARAB'; + } + elsif ($date eq 'today') { + push @sp, "EgQIAhAB"; + } + elsif ($date eq 'week') { + push @sp, "EgQIAxAB"; + } + elsif ($date eq 'month') { + push @sp, "EgQIBBAB"; + } + elsif ($date eq 'year') { + push @sp, "EgQIBRAB"; + } + } + + if (defined(my $order = $self->get_order)) { + if ($order eq 'upload_date') { + push @sp, "CAISAhAB"; + } + elsif ($order eq 'view_count') { + push @sp, "CAMSAhAB"; + } + elsif ($order eq 'rating') { + push @sp, "CAESAhAB"; + } + } + + if (defined(my $license = $self->get_videoLicense)) { + if ($license eq 'creative_commons') { + push @sp, "EgIwAQ%253D%253D"; + } + } + + if (defined(my $vd = $self->get_videoDefinition)) { + if ($vd eq 'high') { + push @sp, "EgIgAQ%253D%253D"; + } + } + + if (defined(my $vc = $self->get_videoCaption)) { + if ($vc eq 'true' or $vc eq '1') { + push @sp, "EgIoAQ%253D%253D"; + } + } + + if (defined(my $vd = $self->get_videoDimension)) { + if ($vd eq '3d') { + push @sp, "EgI4AQ%253D%253D"; + } + } + } + + if ($args{type} eq 'video') { + push @sp, "EgIQAQ%253D%253D"; + } + elsif ($args{type} eq 'playlist') { + push @sp, "EgIQAw%253D%253D"; + } + elsif ($args{type} eq 'channel') { + push @sp, "EgIQAg%253D%253D"; + } + elsif ($args{type} eq 'movie') { # TODO: implement support for movies + push @sp, "EgIQBA%253D%253D"; + } + + $params{sp} = join('+', @sp); + $url = $self->_append_url_args($url, %params); + + my $hash = $self->_get_initial_data($url) // return; + my @results = $self->_extract_sectionList_results(eval { $hash->{contents}{sectionListRenderer} } // undef, %args); + + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head2 yt_channel_search($channel, q => $keyword, %args) + +Search for videos given a keyword string (uri-escaped) from a given channel ID or username. + +=cut + +sub yt_channel_search { + my ($self, $channel, %args) = @_; + my ($url, $hash) = $self->_channel_data($channel, %args, type => 'search', params => {query => $args{q}}); + + $hash // return; + + my @results = $self->_extract_sectionList_results($self->_find_sectionList($hash), %args, type => 'video'); + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head2 yt_channel_uploads($channel, %args) + +Latest uploads for a given channel ID or username. + +=cut + +sub yt_channel_uploads { + my ($self, $channel, %args) = @_; + my ($url, $hash) = $self->_channel_data($channel, %args, type => 'videos'); + + $hash // return; + + my @results = $self->_extract_channel_uploads($hash, %args, type => 'video'); + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head2 yt_channel_info($channel, %args) + +Channel info (such as title) for a given channel ID or username. + +=cut + +sub yt_channel_info { + my ($self, $channel, %args) = @_; + my ($url, $hash) = $self->_channel_data($channel, %args, type => ''); + return $hash; +} + +=head2 yt_channel_title($channel, %args) + +Exact the channel title (as a string) for a given channel ID or username. + +=cut + +sub yt_channel_title { + my ($self, $channel, %args) = @_; + my ($url, $hash) = $self->_channel_data($channel, %args, type => ''); + $hash // return; + my $header = $self->_extract_channel_header($hash, %args) // return; + my $title = eval { $header->{title} }; + return $title; +} + +=head2 yt_channel_id($username, %args) + +Exact the channel ID (as a string) for a given channel username. + +=cut + +sub yt_channel_id { + my ($self, $username, %args) = @_; + my ($url, $hash) = $self->_channel_data($username, %args, type => ''); + $hash // return; + my $header = $self->_extract_channel_header($hash, %args) // return; + my $id = eval { $header->{channelId} } // eval { $header->{externalId} }; + return $id; +} + +=head2 yt_channel_playlists($channel, %args) + +Playlists for a given channel ID or username. + +=cut + +sub yt_channel_playlists { + my ($self, $channel, %args) = @_; + my ($url, $hash) = $self->_channel_data($channel, %args, type => 'playlists'); + + $hash // return; + + my @results = $self->_extract_channel_playlists($hash, %args, type => 'playlist'); + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head2 yt_playlist_videos($playlist_id, %args) + +Videos from a given playlist ID. + +=cut + +sub yt_playlist_videos { + my ($self, $playlist_id, %args) = @_; + + my $url = $self->_append_url_args($self->get_m_youtube_url . "/playlist", list => $playlist_id, hl => "en"); + my $hash = $self->_get_initial_data($url) // return; + + my @results = $self->_extract_sectionList_results($self->_find_sectionList($hash), %args, type => 'video'); + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head2 yt_playlist_next_page($url, $token, %args) + +Load more items from a playlist, given a continuation token. + +=cut + +sub yt_playlist_next_page { + my ($self, $url, $token, %args) = @_; + + my $request_url = $self->_append_url_args($url, ctoken => $token); + my $hash = $self->_get_initial_data($request_url) // return; + + my @results = $self->_parse_itemSection( + eval { $hash->{continuationContents}{playlistVideoListContinuation} } + // eval { $hash->{continuationContents}{itemSectionContinuation} }, + %args + ); + + if (!@results) { + @results = + $self->_extract_sectionList_results(eval { $hash->{continuationContents}{sectionListContinuation} } // undef, %args); + } + + $self->_add_author_to_results($hash, \@results, %args); + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +sub yt_browse_next_page { + my ($self, $url, $token, %args) = @_; + + my %request = ( + context => { + client => { + browserName => "Firefox", + browserVersion => "83.0", + clientFormFactor => "LARGE_FORM_FACTOR", + clientName => "MWEB", + clientVersion => "2.20210308.03.00", + deviceMake => "Generic", + deviceModel => "Android 11.0", + hl => "en", + mainAppWebInfo => { + graftUrl => $url, + }, + originalUrl => $url, + osName => "Android", + osVersion => "11", + platform => "TABLET", + playerType => "UNIPLAYER", + screenDensityFloat => 1, + screenHeightPoints => 500, + screenPixelDensity => 1, + screenWidthPoints => 1800, + timeZone => "UTC", + userAgent => "Mozilla/5.0 (Android 11; Tablet; rv:83.0) Gecko/83.0 Firefox/83.0,gzip(gfe)", + userInterfaceTheme => "USER_INTERFACE_THEME_LIGHT", + utcOffsetMinutes => 0, + }, + request => { + consistencyTokenJars => [], + internalExperimentFlags => [], + }, + user => {}, + }, + continuation => $token, + ); + + my $content = $self->post_as_json( + $self->get_m_youtube_url . '/youtubei/v1/browse?key=' . _unscramble('1HUCiSlOalFEcYQSS8_9q1LW4y8JAwI2zT_qA_G'), + \%request) // return; + + my $hash = $self->parse_json_string($content); + + my $res = + eval { $hash->{continuationContents}{playlistVideoListContinuation} } + // eval { $hash->{continuationContents}{itemSectionContinuation} } + // eval { {contents => $hash->{onResponseReceivedActions}[0]{appendContinuationItemsAction}{continuationItems}} } + // undef; + + my @results = $self->_parse_itemSection($res, %args); + + if (@results) { + push @results, $self->_parse_itemSection_nextpage($res, %args); + } + + if (!@results) { + @results = + $self->_extract_sectionList_results(eval { $hash->{continuationContents}{sectionListContinuation} } // undef, %args); + } + + $self->_add_author_to_results($hash, \@results, %args); + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head2 yt_search_next_page($url, $token, %args) + +Load more search results, given a continuation token. + +=cut + +sub yt_search_next_page { + my ($self, $url, $token, %args) = @_; + + my %request = ( + "context" => { + "client" => { + "browserName" => "Firefox", + "browserVersion" => "83.0", + "clientFormFactor" => "LARGE_FORM_FACTOR", + "clientName" => "MWEB", + "clientVersion" => "2.20201030.01.00", + "deviceMake" => "generic", + "deviceModel" => "android 11.0", + "gl" => "US", + "hl" => "en", + "mainAppWebInfo" => { + "graftUrl" => "https://m.youtube.com/results?search_query=youtube" + }, + "osName" => "Android", + "osVersion" => "11", + "platform" => "TABLET", + "playerType" => "UNIPLAYER", + "screenDensityFloat" => 1, + "screenHeightPoints" => 420, + "screenPixelDensity" => 1, + "screenWidthPoints" => 1442, + "userAgent" => "Mozilla/5.0 (Android 11; Tablet; rv:83.0) Gecko/83.0 Firefox/83.0,gzip(gfe)", + "userInterfaceTheme" => "USER_INTERFACE_THEME_LIGHT", + "utcOffsetMinutes" => 0, + }, + "request" => { + "consistencyTokenJars" => [], + "internalExperimentFlags" => [], + }, + "user" => {} + }, + "continuation" => $token, + ); + + my $content = $self->post_as_json( + $self->get_m_youtube_url + . _unscramble('o/ebseky?u1ri//hvcuyta=e') + . _unscramble('1HUCiSlOalFEcYQSS8_9q1LW4y8JAwI2zT_qA_G'), + \%request + ) // return; + + my $hash = $self->parse_json_string($content); + + my @results = $self->_extract_sectionList_results( + scalar { + contents => eval { + $hash->{onResponseReceivedCommands}[0]{appendContinuationItemsAction}{continuationItems}; + } // undef + }, + %args + ); + + $self->_prepare_results_for_return(\@results, %args, url => $url); +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + +Jesus, C<< <echo aGVja3llbEBoeXBlcmJvbGEuaW5mbw== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::FairViewer::InitialData + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 Trizen. + +Copyright 2020 Jesus E. + +This program is free software; you can redistribute it and/or modify it +under the terms of either: the GNU General Public License as published +by the Free Software Foundation; or the Artistic License. + +See L<http://dev.perl.org/licenses/> for more information. + +=cut + +1; # End of WWW::FairViewer::InitialData |