Files
2024-03-24 18:17:49 +01:00

398 lines
12 KiB
Perl

package YTMusicAPI::Parsers::Search;
use strict;
use warnings;
use Exporter 'import';
use YTMusicAPI::Parsers::Utils;
use YTMusicAPI::Parsers::Songs;
use YTMusicAPI::Navigation;
sub get_search_result_type {
my ( $result_type_local, $result_types_local ) = @_;
if ( !$result_type_local ) {
return undef;
}
my @result_types = (
"artist", "playlist", "song", "video",
"station", "profile", "podcast", "episode"
);
$result_type_local = lc $result_type_local;
my $result_type;
my $index = 0;
my $found_index = -1;
# Search for index
foreach my $type (@$result_types_local) {
if ( $type eq $result_type_local ) {
$found_index = $index;
last;
}
$index++;
}
if ( $found_index == -1 ) {
$result_type = "album";
}
else {
$result_type = $result_types[$found_index];
}
return $result_type;
}
sub parse_top_result {
my ( $data, $search_result_types ) = @_;
my $result_type =
get_search_result_type( nav( $data, $SUBTITLE ), $search_result_types );
my $search_result = {
"category" => nav( $data, $CARD_SHELF_TITLE ),
"resultType" => $result_type
};
if ( $result_type eq "artist" ) {
my $subscribers = nav( $data, $SUBTITLE2, 1 );
if ($subscribers) {
$search_result->{"subscribers"} = ( split " ", $subscribers )[0];
}
my $artist_info = parse_song_runs( nav( $data, [ "title", "runs" ] ) );
@$search_result{ keys %$artist_info } = values %$artist_info;
}
if ( $result_type eq "song" || $result_type eq "video" ) {
my $on_tap = $data->{"onTap"};
if ($on_tap) {
$search_result->{"videoId"} = nav( $on_tap, $WATCH_VIDEO_ID );
$search_result->{"videoType"} =
nav( $on_tap, $NAVIGATION_VIDEO_TYPE );
}
}
if ( grep { $_ eq $result_type } [ "song", "video", "album" ] ) {
$search_result->{"videoId"} =
nav( $data, [ "onTap", $WATCH_VIDEO_ID ], 1 );
$search_result->{"videoType"} =
nav( $data, [ "onTap", $NAVIGATION_VIDEO_TYPE ], 1 );
$search_result->{"title"} = nav( $data, $TITLE_TEXT );
my $runs = nav( $data, [ "subtitle", "runs" ] );
my $song_info = parse_song_runs($runs);
@$search_result{ keys %$song_info } = values %$song_info;
}
if ( $result_type eq "album" ) {
$search_result->{"browseId"} =
nav( $data, $TITLE . $NAVIGATION_BROWSE_ID, 1 );
}
if ( $result_type eq "playlist" ) {
$search_result->{"playlistId"} = nav( $data, $MENU_PLAYLIST_ID );
$search_result->{"title"} = nav( $data, $TITLE_TEXT );
my @x = nav( $data, [ "subtitle", "runs" ], 0 );
$search_result->{"author"} = parse_song_artists_runs( $x[ 2 .. $#x ] );
}
$search_result->{"thumbnails"} = nav( $data, $THUMBNAILS, 1 );
return $search_result;
}
sub parse_search_result {
my ( $data, $search_result_types, $result_type, $category ) = @_;
my $default_offset = ( !$result_type || $result_type eq "album" ) ? 2 : 0;
my $search_result = { "category" => $category };
my $video_type = nav( $data,
[ $PLAY_BUTTON, "playNavigationEndpoint", $NAVIGATION_VIDEO_TYPE ], 1 );
if ( !$result_type && $video_type ) {
$result_type = $video_type eq "MUSIC_VIDEO_TYPE_ATV" ? "song" : "video";
}
unless ($result_type) {
$result_type = get_search_result_type( get_item_text( $data, 1 ),
$search_result_types );
}
$search_result->{"resultType"} = $result_type;
if ( $result_type ne "artist" ) {
$search_result->{"title"} = get_item_text( $data, 0 );
}
if ( $result_type eq "artist" ) {
$search_result->{"artist"} = get_item_text( $data, 0 );
parse_menu_playlists( $data, $search_result );
}
elsif ( $result_type eq "album" ) {
$search_result->{"type"} = get_item_text( $data, 1 );
}
elsif ( $result_type eq "playlist" ) {
my $flex_item = get_flex_column_item( $data, 1 )->{"text"}->{"runs"};
my $has_author = scalar(@$flex_item) == $default_offset + 3;
$search_result->{"itemCount"} = (
split(
" ",
get_item_text( $data, 1, $default_offset + $has_author * 2 )
)
)[0];
$search_result->{"author"} =
$has_author ? get_item_text( $data, 1, $default_offset ) : undef;
}
elsif ( $result_type eq "station" ) {
$search_result->{"videoId"} = nav( $data, $NAVIGATION_VIDEO_ID );
$search_result->{"playlistId"} = nav( $data, $NAVIGATION_PLAYLIST_ID );
}
elsif ( $result_type eq "profile" ) {
$search_result->{"name"} = get_item_text( $data, 1, 2, 1 );
}
elsif ( $result_type eq "song" ) {
$search_result->{"album"} = undef;
if ( exists $data->{"menu"} ) {
my $toggle_menu =
find_object_by_key( nav( $data, $MENU_ITEMS ), $TOGGLE_MENU );
if ($toggle_menu) {
$search_result->{"inLibrary"} =
parse_song_library_status($toggle_menu);
$search_result->{"feedbackTokens"} =
parse_song_menu_tokens($toggle_menu);
}
}
}
elsif ( $result_type eq "upload" ) {
my $browse_id = nav( $data, $NAVIGATION_BROWSE_ID, 1 );
if ( !$browse_id ) { # song result
my @flex_items = map {
nav( get_flex_column_item( $data, $_ ), [ "text", "runs" ], 1 )
} ( 0 .. 1 );
if ( $flex_items[0] ) {
$search_result->{"videoId"} =
nav( $flex_items[0]->[0], $NAVIGATION_VIDEO_ID, 1 );
$search_result->{"playlistId"} =
nav( $flex_items[0]->[0], $NAVIGATION_PLAYLIST_ID, 1 );
}
if ( $flex_items[1] ) {
my $song_runs = parse_song_runs( $flex_items[1] );
@$search_result{ keys %$song_runs } = values %$song_runs;
}
$search_result->{"resultType"} = "song";
}
else {
# artist or album result
$search_result->{"browseId"} = $browse_id;
if ( $search_result->{"browseId"} =~ /artist/ ) {
$search_result->{"resultType"} = "artist";
}
else {
my $flex_item2 = get_flex_column_item( $data, 1 );
my $i = 0;
my @runs = map { $_->{"text"} }
grep { $i++ % 2 == 0 } @{ $flex_item2->{"text"}->{"runs"} };
if ( scalar(@runs) > 1 ) {
$search_result->{"artist"} = $runs[1];
}
if ( scalar(@runs) > 2 ) { # date may be missing
$search_result->{"releaseDate"} = $runs[2];
}
$search_result->{"resultType"} = "album";
}
}
}
if ( $result_type =~ /song|video|episode/ ) {
$search_result->{"videoId"} = nav(
$data,
[
$PLAY_BUTTON, "playNavigationEndpoint",
"watchEndpoint", "videoId"
],
1
);
$search_result->{"videoType"} = $video_type;
}
if ( grep { $_ eq $result_type } [ "song", "video", "album" ] ) {
$search_result->{"duration"} = undef;
$search_result->{"year"} = undef;
my $flex_item = get_flex_column_item( $data, 1 );
my $runs = $flex_item->{"text"}->{"runs"};
my $song_info = parse_song_runs($runs);
@$search_result{ keys %$song_info } = values %$song_info;
}
if ( grep { $_ eq $result_type }
[ "artist", "album", "playlist", "profile", "podcast" ] )
{
$search_result->{"browseId"} = nav( $data, $NAVIGATION_BROWSE_ID, 1 );
}
if ( grep { $_ eq $result_type } [ "song", "album" ] ) {
$search_result->{"isExplicit"} =
defined( nav( $data, $BADGE_LABEL, 1 ) ) ? 1 : undef;
}
if ( $result_type eq "episode" ) {
my $flex_item = get_flex_column_item( $data, 1 );
my $has_date = scalar( @{ nav( $flex_item, $TEXT_RUNS ) } ) > 1 ? 1 : 0;
$search_result->{"live"} =
nav( $data, [ "badges", 0, "liveBadgeRenderer" ], 1 ) ? 1 : undef;
if ($has_date) {
$search_result->{"date"} = nav( $flex_item, $TEXT_RUN_TEXT );
}
$search_result->{"podcast"} =
parse_id_name( nav( $flex_item, [ "text", "runs", $has_date * 2 ] ) );
}
$search_result->{"thumbnails"} = nav( $data, $THUMBNAILS, 1 );
return $search_result;
}
sub parse_search_results {
my ( $results, $search_result_types, $result_type, $category ) = @_;
my @parsed_results;
foreach my $result (@$results) {
push @parsed_results,
parse_search_result( $result->{$MRLIR}, $search_result_types,
$result_type, $category );
}
return \@parsed_results;
}
sub get_search_params {
my ( $filter, $scope, $ignore_spelling ) = @_;
my $filtered_param1 = "EgWKAQ";
my $params;
my $param1;
my $param2;
my $param3;
return $params unless defined $filter || defined $scope || $ignore_spelling;
if ( $scope and $scope eq "uploads" ) {
$params = "agIYAw%3D%3D";
}
if ( $scope and $scope eq "library" ) {
if ($filter) {
$param1 = $filtered_param1;
$param2 = _get_param2($filter);
$param3 = "AWoKEAUQCRADEAoYBA%3D%3D";
}
else {
$params = "agIYBA%3D%3D";
}
}
if ( !defined $scope && $filter ) {
if ( $filter eq "playlists" ) {
$params = "Eg-KAQwIABAAGAAgACgB";
if ( !$ignore_spelling ) {
$params .= "MABqChAEEAMQCRAFEAo%3D";
}
else {
$params .= "MABCAggBagoQBBADEAkQBRAK";
}
}
elsif ( $filter =~ /playlists/ ) {
$param1 = "EgeKAQQoA";
$param2 = $filter eq "featured_playlists" ? "Dg" : "EA";
if ( !$ignore_spelling ) {
$param3 = "BagwQDhAKEAMQBBAJEAU%3D";
}
else {
$param3 = "BQgIIAWoMEA4QChADEAQQCRAF";
}
}
else {
$param1 = $filtered_param1;
$param2 = _get_param2($filter);
$param3 =
$ignore_spelling
? "AUICCAFqDBAOEAoQAxAEEAkQBQ%3D%3D"
: "AWoMEA4QChADEAQQCRAF";
}
}
if ( !defined $scope && !defined $filter && $ignore_spelling ) {
$params = "EhGKAQ4IARABGAEgASgAOAFAAUICCAE%3D";
}
return $params if defined $params;
return $param1 . $param2 . $param3
if defined $param1 && defined $param2 && defined $param3;
}
sub _get_param2 {
my ($filter) = @_;
my $filter_params = {
"songs" => "II",
"videos" => "IQ",
"albums" => "IY",
"artists" => "Ig",
"playlists" => "Io",
"profiles" => "JY",
"podcasts" => "JQ",
"episodes" => "JI",
};
return $filter_params->{$filter};
}
sub parse_search_suggestions {
my ( $results, $detailed_runs ) = @_;
return []
unless $results->{"contents"}[0]{"searchSuggestionsSectionRenderer"}
{"contents"};
my $raw_suggestions =
$results->{"contents"}[0]{"searchSuggestionsSectionRenderer"}{"contents"};
my @suggestions = ();
foreach my $raw_suggestion (@$raw_suggestions) {
my ( $suggestion_content, $from_history );
if ( exists $raw_suggestion->{"historySuggestionRenderer"} ) {
$suggestion_content =
$raw_suggestion->{"historySuggestionRenderer"};
$from_history = 1;
}
else {
$suggestion_content = $raw_suggestion->{"searchSuggestionRenderer"};
$from_history = 0;
}
my $text = $suggestion_content->{"navigationEndpoint"}{"searchEndpoint"}
{"query"};
my $runs = $suggestion_content->{"suggestion"}{"runs"};
if ($detailed_runs) {
push @suggestions,
{
"text" => $text,
"runs" => $runs,
"fromHistory" => $from_history
};
}
else {
push @suggestions, $text;
}
}
return \@suggestions;
}
our @EXPORT = qw(
get_search_result_type
parse_top_result
parse_search_result
parse_search_results
get_search_params
parse_search_suggestions
);
1;