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;