Initial commit
This commit is contained in:
56
YTMusicAPI/Parsers/Albums.pm
Normal file
56
YTMusicAPI/Parsers/Albums.pm
Normal file
@@ -0,0 +1,56 @@
|
||||
package YTMusicAPI::Parsers::Albums;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use YTMusicAPI::Navigation;
|
||||
|
||||
sub parse_album_header {
|
||||
my ($response) = @_;
|
||||
my $header = nav( $response, $HEADER_DETAIL );
|
||||
my $album = {
|
||||
title => nav( $header, $TITLE_TEXT ),
|
||||
type => nav( $header, $SUBTITLE ),
|
||||
thumbnails => nav( $header, $THUMBNAIL_CROPPED ),
|
||||
isExplicit => defined( nav( $header, $SUBTITLE_BADGE_LABEL, 1 ) )
|
||||
? 1
|
||||
: 0,
|
||||
};
|
||||
|
||||
if ( exists $header->{description} ) {
|
||||
$album->{description} = $header->{description}->{runs}->[0]->{text};
|
||||
}
|
||||
|
||||
my $album_info = parse_song_runs(
|
||||
$header->{subtitle}->{runs}->[ 2 .. $#{ $header->{subtitle}->{runs} } ]
|
||||
);
|
||||
@$album{ keys %$album_info } = values %$album_info;
|
||||
|
||||
if ( scalar @{ $header->{secondSubtitle}->{runs} } > 1 ) {
|
||||
$album->{trackCount} =
|
||||
to_int( $header->{secondSubtitle}->{runs}->[0]->{text} );
|
||||
$album->{duration} = $header->{secondSubtitle}->{runs}->[2]->{text};
|
||||
}
|
||||
else {
|
||||
$album->{duration} = $header->{secondSubtitle}->{runs}->[0]->{text};
|
||||
}
|
||||
|
||||
my $menu = nav( $header, $MENU );
|
||||
my $toplevel = $menu->{topLevelButtons};
|
||||
$album->{audioPlaylistId} =
|
||||
nav( $toplevel, [ 0, 'buttonRenderer', $NAVIGATION_WATCH_PLAYLIST_ID ],
|
||||
1 );
|
||||
unless ( $album->{audioPlaylistId} ) {
|
||||
$album->{audioPlaylistId} =
|
||||
nav( $toplevel, [ 0, 'buttonRenderer', $NAVIGATION_PLAYLIST_ID ], 1 );
|
||||
}
|
||||
my $service =
|
||||
nav( $toplevel, [ 1, 'buttonRenderer', 'defaultServiceEndpoint' ], 1 );
|
||||
if ($service) {
|
||||
$album->{likeStatus} = parse_like_status($service);
|
||||
}
|
||||
|
||||
return $album;
|
||||
}
|
||||
|
||||
1;
|
||||
78
YTMusicAPI/Parsers/Parser.pm
Normal file
78
YTMusicAPI/Parsers/Parser.pm
Normal file
@@ -0,0 +1,78 @@
|
||||
package YTMusicAPI::Parsers::Parser;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use YTMusicAPI::Navigation;
|
||||
|
||||
sub new {
|
||||
my ( $class, $language ) = @_;
|
||||
|
||||
my $self = { lang => $language };
|
||||
|
||||
bless $self, $class;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub _ {
|
||||
my ($input) = @_;
|
||||
return $input;
|
||||
}
|
||||
|
||||
sub get_search_result_types {
|
||||
my ($self) = @_;
|
||||
|
||||
return [
|
||||
_("artist"), _("playlist"), _("song"), _("video"),
|
||||
_("station"), _("profile"), _("podcast"), _("episode"),
|
||||
];
|
||||
}
|
||||
|
||||
sub parse_channel_contents {
|
||||
my ( $self, $results ) = @_;
|
||||
|
||||
my @categories = (
|
||||
[ "albums", _("albums"), \&parse_album, $MTRIR ],
|
||||
[ "singles", _("singles"), \&parse_single, $MTRIR ],
|
||||
[ "shows", _("shows"), \&parse_album, $MTRIR ],
|
||||
[ "videos", _("videos"), \&parse_video, $MTRIR ],
|
||||
[ "playlists", _("playlists"), \&parse_playlist, $MTRIR ],
|
||||
[ "related", _("related"), \&parse_related_artist, $MTRIR ],
|
||||
[ "episodes", _("episodes"), \&parse_episode, $MMRIR ],
|
||||
[ "podcasts", _("podcasts"), \&parse_podcast, $MTRIR ],
|
||||
);
|
||||
|
||||
my %artist = {};
|
||||
foreach my $category_tuple (@categories) {
|
||||
my ( $category, $category_local, $category_parser, $category_key ) =
|
||||
@$category_tuple;
|
||||
my @data = map { $_->{"musicCarouselShelfRenderer"} }
|
||||
grep {
|
||||
exists $_->{"musicCarouselShelfRenderer"}
|
||||
&& nav( $_, $CAROUSEL . $CAROUSEL_TITLE )->{"text"} =~
|
||||
/^$category_local$/i
|
||||
} $results;
|
||||
|
||||
if ( scalar @data > 0 ) {
|
||||
$artist{$category} = { "browseId" => undef, "results" => [] };
|
||||
|
||||
if (
|
||||
exists nav( $data[0], $CAROUSEL_TITLE )->{"navigationEndpoint"}
|
||||
)
|
||||
{
|
||||
$artist{$category}->{"browseId"} =
|
||||
nav( $data[0], $CAROUSEL_TITLE . $NAVIGATION_BROWSE_ID );
|
||||
$artist{$category}->{"params"} = nav( $data[0],
|
||||
$CAROUSEL_TITLE . $NAVIGATION_BROWSE . ["params"], 1 );
|
||||
}
|
||||
$artist{$category}->{"results"} =
|
||||
parse_content_list( $data[0]->{"contents"},
|
||||
$category_parser, "key" => $category_key );
|
||||
}
|
||||
}
|
||||
|
||||
return %artist;
|
||||
}
|
||||
|
||||
1;
|
||||
397
YTMusicAPI/Parsers/Search.pm
Normal file
397
YTMusicAPI/Parsers/Search.pm
Normal file
@@ -0,0 +1,397 @@
|
||||
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;
|
||||
155
YTMusicAPI/Parsers/Songs.pm
Normal file
155
YTMusicAPI/Parsers/Songs.pm
Normal file
@@ -0,0 +1,155 @@
|
||||
package YTMusicAPI::Parsers::Songs;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Exporter 'import';
|
||||
use Regexp::Common qw(number);
|
||||
|
||||
use YTMusicAPI::Parsers::Utils;
|
||||
use YTMusicAPI::Navigation;
|
||||
|
||||
sub parse_song_artists {
|
||||
my ( $data, $index ) = @_;
|
||||
my $flex_item = get_flex_column_item( $data, $index );
|
||||
|
||||
if ( !$flex_item ) {
|
||||
return undef;
|
||||
}
|
||||
else {
|
||||
my $runs = $flex_item->{"text"}{"runs"};
|
||||
return parse_song_artists_runs($runs);
|
||||
}
|
||||
}
|
||||
|
||||
sub parse_song_artists_runs {
|
||||
my ($runs) = @_;
|
||||
my @artists = ();
|
||||
|
||||
for my $j ( 0 .. int( @$runs / 2 ) ) {
|
||||
push @artists,
|
||||
{
|
||||
"name" => $runs->[ $j * 2 ]{"text"},
|
||||
"id" => nav( $runs->[ $j * 2 ], $NAVIGATION_BROWSE_ID, 1 )
|
||||
};
|
||||
}
|
||||
return \@artists;
|
||||
}
|
||||
|
||||
sub parse_song_runs {
|
||||
my ($runs) = @_;
|
||||
my %parsed = ( "artists" => [] );
|
||||
my $i = 0;
|
||||
|
||||
for my $run (@$runs) {
|
||||
if ( $i % 2 ) { # uneven items are always separators
|
||||
$i++;
|
||||
next;
|
||||
}
|
||||
|
||||
my $text = $run->{"text"};
|
||||
|
||||
if ( exists $run->{"navigationEndpoint"} ) { # artist or album
|
||||
my $item = {
|
||||
"name" => $text,
|
||||
"id" => nav( $run, $NAVIGATION_BROWSE_ID, 1 )
|
||||
};
|
||||
|
||||
if (
|
||||
$item->{"id"}
|
||||
&& ( $item->{"id"} =~ /^MPRE/
|
||||
|| $item->{"id"} =~ /release_detail/ )
|
||||
)
|
||||
{ # album
|
||||
$parsed{"album"} = $item;
|
||||
}
|
||||
else { # artist
|
||||
push @{ $parsed{"artists"} }, $item;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
# note: YT uses non-breaking space \xa0 to separate number and magnitude
|
||||
if ( $text =~ /^\d([^ ])* [^ ]*$/ && $i > 0 ) {
|
||||
( $parsed{"views"} ) = split( / /, $text, 2 );
|
||||
}
|
||||
elsif ( $text =~ /^(\d+:)*\d+:\d+$/ ) {
|
||||
$parsed{"duration"} = $text;
|
||||
$parsed{"duration_seconds"} = parse_duration($text);
|
||||
}
|
||||
elsif ( $text =~ /^\d{4}$/ ) {
|
||||
$parsed{"year"} = $text;
|
||||
}
|
||||
else { # artist without id
|
||||
push @{ $parsed{"artists"} },
|
||||
{ "name" => $text, "id" => undef };
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
return \%parsed;
|
||||
}
|
||||
|
||||
sub parse_song_album {
|
||||
my ( $data, $index ) = @_;
|
||||
my $flex_item = get_flex_column_item( $data, $index );
|
||||
my $browse_id = nav( $flex_item, $TEXT_RUN + $NAVIGATION_BROWSE_ID, 1 );
|
||||
return !$flex_item
|
||||
? undef
|
||||
: { "name" => get_item_text( $data, $index ), "id" => $browse_id };
|
||||
}
|
||||
|
||||
sub parse_song_library_status {
|
||||
my ($item) = @_;
|
||||
my $library_status =
|
||||
nav( $item, [ $TOGGLE_MENU, "defaultIcon", "iconType" ], 1 );
|
||||
return $library_status eq "LIBRARY_SAVED";
|
||||
}
|
||||
|
||||
sub parse_song_menu_tokens {
|
||||
my ($item) = @_;
|
||||
my $toggle_menu = $item->{$TOGGLE_MENU};
|
||||
|
||||
my $library_add_token =
|
||||
nav( $toggle_menu, [ "defaultServiceEndpoint", $FEEDBACK_TOKEN ], 1 );
|
||||
my $library_remove_token =
|
||||
nav( $toggle_menu, [ "toggledServiceEndpoint", $FEEDBACK_TOKEN ], 1 );
|
||||
|
||||
my $in_library = parse_song_library_status($item);
|
||||
if ($in_library) {
|
||||
my $tmp = $library_remove_token;
|
||||
$library_add_token = $library_remove_token;
|
||||
$library_remove_token = $tmp;
|
||||
}
|
||||
|
||||
return { "add" => $library_add_token, "remove" => $library_remove_token };
|
||||
}
|
||||
|
||||
sub parse_like_status {
|
||||
my ($service) = @_;
|
||||
my @status = ( "LIKE", "INDIFFERENT" );
|
||||
|
||||
my $status_index = 0;
|
||||
for my $i ( 0 .. $#status ) {
|
||||
if ( $status[$i] eq $service->{"likeEndpoint"}{"status"} ) {
|
||||
$status_index = $i;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
return $status[ $status_index - 1 ];
|
||||
}
|
||||
|
||||
our @EXPORT = qw(
|
||||
parse_song_artists
|
||||
parse_song_artists_runs
|
||||
parse_song_runs
|
||||
parse_song_album
|
||||
parse_song_library_status
|
||||
parse_song_menu_tokens
|
||||
parse_like_status
|
||||
);
|
||||
|
||||
1;
|
||||
140
YTMusicAPI/Parsers/Utils.pm
Normal file
140
YTMusicAPI/Parsers/Utils.pm
Normal file
@@ -0,0 +1,140 @@
|
||||
package YTMusicAPI::Parsers::Utils;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Exporter 'import';
|
||||
|
||||
use YTMusicAPI::Navigation;
|
||||
|
||||
sub parse_menu_playlists {
|
||||
my ( $data, $result ) = @_;
|
||||
my $watch_menu = find_objects_by_key( nav( $data, \@$MENU_ITEMS ), $MNIR );
|
||||
|
||||
foreach my $item ( map { $_->{$MNIR} } @$watch_menu ) {
|
||||
my $icon = nav( $item, \@$ICON_TYPE, 1 );
|
||||
my $watch_key;
|
||||
|
||||
if ( $icon eq "MUSIC_SHUFFLE" ) {
|
||||
$watch_key = "shuffleId";
|
||||
}
|
||||
elsif ( $icon eq "MIX" ) {
|
||||
$watch_key = "radioId";
|
||||
}
|
||||
else {
|
||||
next;
|
||||
}
|
||||
|
||||
my $watch_id =
|
||||
nav( $item,
|
||||
[ "navigationEndpoint", "watchPlaylistEndpoint", "playlistId" ],
|
||||
1 );
|
||||
unless ($watch_id) {
|
||||
$watch_id = nav( $item,
|
||||
[ "navigationEndpoint", "watchEndpoint", "playlistId" ], 1 );
|
||||
}
|
||||
|
||||
if ($watch_id) {
|
||||
$result->{$watch_key} = $watch_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub get_item_text {
|
||||
my ( $item, $index, $run_index, $none_if_absent ) = @_;
|
||||
$run_index //= 0;
|
||||
$none_if_absent //= 0;
|
||||
|
||||
my $column = get_flex_column_item( $item, $index );
|
||||
return unless $column;
|
||||
|
||||
if ( $none_if_absent
|
||||
&& scalar( @{ $column->{text}->{runs} } ) < $run_index + 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
return $column->{text}->{runs}->[$run_index]->{text};
|
||||
}
|
||||
|
||||
sub get_flex_column_item {
|
||||
my ( $item, $index ) = @_;
|
||||
|
||||
if ( scalar( @{ $item->{'flexColumns'} } ) <= $index
|
||||
|| !
|
||||
exists $item->{'flexColumns'}->[$index]
|
||||
->{'musicResponsiveListItemFlexColumnRenderer'}->{'text'}
|
||||
|| !
|
||||
exists $item->{'flexColumns'}->[$index]
|
||||
->{'musicResponsiveListItemFlexColumnRenderer'}->{'text'}->{'runs'} )
|
||||
{
|
||||
return undef;
|
||||
}
|
||||
|
||||
return $item->{'flexColumns'}->[$index]
|
||||
->{'musicResponsiveListItemFlexColumnRenderer'};
|
||||
}
|
||||
|
||||
sub get_fixed_column_item {
|
||||
my ( $item, $index ) = @_;
|
||||
|
||||
if ( !exists $item->{'fixedColumns'}->[$index]
|
||||
->{'musicResponsiveListItemFixedColumnRenderer'}->{'text'}
|
||||
|| !
|
||||
exists $item->{'fixedColumns'}->[$index]
|
||||
->{'musicResponsiveListItemFixedColumnRenderer'}->{'text'}->{'runs'} )
|
||||
{
|
||||
return undef;
|
||||
}
|
||||
|
||||
return $item->{'fixedColumns'}->[$index]
|
||||
->{'musicResponsiveListItemFixedColumnRenderer'};
|
||||
}
|
||||
|
||||
sub get_dot_separator_index {
|
||||
my ($runs) = @_;
|
||||
my $index = 0;
|
||||
|
||||
foreach my $run (@$runs) {
|
||||
last if ( $run->{"text"} eq " • " );
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $index == scalar @$runs ? $index : $index;
|
||||
}
|
||||
|
||||
sub parse_duration {
|
||||
my ($duration) = @_;
|
||||
return $duration unless defined $duration;
|
||||
|
||||
my @mapped_increments = ( [ 1, 60, 3600 ], reverse split /:/, $duration );
|
||||
my $seconds = 0;
|
||||
|
||||
while ( my ( $multiplier, $time ) = splice( @mapped_increments, 0, 2 ) ) {
|
||||
$seconds += $multiplier * $time;
|
||||
}
|
||||
|
||||
return $seconds;
|
||||
}
|
||||
|
||||
sub parse_id_name {
|
||||
my ($sub_run) = @_;
|
||||
return {
|
||||
"id" => nav(
|
||||
$sub_run, [ "navigationEndpoint", "browseEndpoint", "browseId" ], 1
|
||||
),
|
||||
"name" => nav( $sub_run, ["text"], 1 ),
|
||||
};
|
||||
}
|
||||
|
||||
our @EXPORT = qw(
|
||||
parse_menu_playlists
|
||||
get_item_text
|
||||
get_flex_column_item
|
||||
get_fixed_column_item
|
||||
get_dot_separator_index
|
||||
parse_duration
|
||||
parse_id_name
|
||||
);
|
||||
|
||||
1;
|
||||
Reference in New Issue
Block a user