236 lines
7.1 KiB
Perl
236 lines
7.1 KiB
Perl
package YTMusicAPI::Parsers::Playlists;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Exporter 'import';
|
|
|
|
use YTMusicAPI::Parsers::Utils;
|
|
use YTMusicAPI::Parsers::Songs;
|
|
use YTMusicAPI::Navigation;
|
|
|
|
sub parse_playlist_header {
|
|
my ($response) = @_;
|
|
|
|
my $playlist = {};
|
|
my $header;
|
|
my $own_playlist =
|
|
exists $response->{"header"}{"musicEditablePlaylistDetailHeaderRenderer"};
|
|
|
|
if ( !$own_playlist ) {
|
|
$header = $response->{"header"}{"musicDetailHeaderRenderer"};
|
|
$playlist->{"privacy"} = "PUBLIC";
|
|
}
|
|
else {
|
|
$header =
|
|
$response->{"header"}{"musicEditablePlaylistDetailHeaderRenderer"};
|
|
$playlist->{"privacy"} =
|
|
$header->{"editHeader"}{"musicPlaylistEditHeaderRenderer"}{"privacy"};
|
|
$header = $header->{"header"}{"musicDetailHeaderRenderer"};
|
|
}
|
|
|
|
$playlist->{"owned"} = $own_playlist;
|
|
$playlist->{"title"} = nav( $header, $TITLE_TEXT );
|
|
$playlist->{"thumbnails"} = nav( $header, $THUMBNAIL_CROPPED );
|
|
$playlist->{"description"} = nav( $header, $DESCRIPTION, 1 );
|
|
my $run_count = scalar( @{ nav( $header, $SUBTITLE_RUNS ) } );
|
|
|
|
if ( $run_count > 1 ) {
|
|
$playlist->{"author"} = {
|
|
"name" => nav( $header, $SUBTITLE2 ),
|
|
"id" =>
|
|
nav( $header, [ $SUBTITLE_RUNS, 2, $NAVIGATION_BROWSE_ID ], 1 ),
|
|
};
|
|
if ( $run_count == 5 ) {
|
|
$playlist->{"year"} = nav( $header, $SUBTITLE3 );
|
|
}
|
|
|
|
}
|
|
|
|
$playlist->{"views"} = undef;
|
|
$playlist->{"duration"} = undef;
|
|
$playlist->{"trackCount"} = undef;
|
|
|
|
if ( exists $header->{"secondSubtitle"}{"runs"} ) {
|
|
my $second_subtitle_runs = $header->{"secondSubtitle"}{"runs"};
|
|
my $has_views = ( $#$second_subtitle_runs > 3 ) * 2;
|
|
$playlist->{"views"} =
|
|
!$has_views ? undef : scalar $second_subtitle_runs->[0]->{"text"};
|
|
my $has_duration = ( $#$second_subtitle_runs > 1 ) * 2;
|
|
$playlist->{"duration"} = (
|
|
!$has_duration
|
|
? undef
|
|
: $second_subtitle_runs->[ $has_views + $has_duration ]->{"text"}
|
|
);
|
|
my @song_count =
|
|
split( " ", $second_subtitle_runs->[ $has_views + 0 ]->{"text"} );
|
|
@song_count = $#song_count > 1 ? ( scalar $song_count[0] ) : 0;
|
|
$playlist->{"trackCount"} = @song_count;
|
|
}
|
|
|
|
return $playlist;
|
|
}
|
|
|
|
sub parse_playlist_items {
|
|
my ( $results, $menu_entries, $is_album ) = @_;
|
|
$menu_entries //= [];
|
|
$is_album //= 0;
|
|
|
|
my $songs = [];
|
|
foreach my $result (@$results) {
|
|
next unless exists $result->{$MRLIR};
|
|
my $data = $result->{$MRLIR};
|
|
my $song = parse_playlist_item( $data, $menu_entries, $is_album );
|
|
push @$songs, $song if $song;
|
|
}
|
|
|
|
return $songs;
|
|
}
|
|
|
|
sub parse_playlist_item {
|
|
my ( $data, $menu_entries, $is_album ) = @_;
|
|
$menu_entries //= [];
|
|
$is_album //= 0;
|
|
|
|
my ( $videoId, $setVideoId ) = ( undef, undef );
|
|
my ( $like, $feedback_tokens, $library_status ) = ( undef, undef, undef );
|
|
|
|
# if the item has a menu, find its setVideoId
|
|
if ( exists $data->{"menu"} ) {
|
|
foreach my $item ( @{ nav( $data, $MENU_ITEMS ) } ) {
|
|
if ( exists $item->{"menuServiceItemRenderer"} ) {
|
|
my $menu_service = nav( $item, $MENU_SERVICE );
|
|
if ( exists $menu_service->{"playlistEditEndpoint"} ) {
|
|
$setVideoId = nav(
|
|
$menu_service,
|
|
[ "playlistEditEndpoint", "actions", 0, "setVideoId" ],
|
|
1
|
|
);
|
|
$videoId = nav(
|
|
$menu_service,
|
|
[
|
|
"playlistEditEndpoint", "actions",
|
|
0, "removedVideoId"
|
|
],
|
|
1
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( exists $item->{$TOGGLE_MENU} ) {
|
|
$feedback_tokens = parse_song_menu_tokens($item);
|
|
$library_status = parse_song_library_status($item);
|
|
}
|
|
}
|
|
}
|
|
|
|
# if item is not playable, the videoId was retrieved above
|
|
if ( defined nav( $data, $PLAY_BUTTON, 1 ) ) {
|
|
if ( exists nav( $data, $PLAY_BUTTON )->{"playNavigationEndpoint"} ) {
|
|
$videoId = nav( $data, $PLAY_BUTTON )->{"playNavigationEndpoint"}
|
|
->{"watchEndpoint"}->{"videoId"};
|
|
|
|
if ( exists $data->{"menu"} ) {
|
|
$like = nav( $data, $MENU_LIKE_STATUS, 1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
my $title = get_item_text( $data, 0 );
|
|
return undef if $title eq "Song deleted";
|
|
|
|
my $flex_column_count = scalar @{ $data->{"flexColumns"} };
|
|
|
|
my $artists = parse_song_artists( $data, 1 );
|
|
|
|
my $album =
|
|
!$is_album ? parse_song_album( $data, $flex_column_count - 1 ) : undef;
|
|
|
|
my $views =
|
|
$flex_column_count == 4 || $is_album ? get_item_text( $data, 2 ) : undef;
|
|
|
|
my $duration;
|
|
if ( exists $data->{"fixedColumns"} ) {
|
|
if (
|
|
exists get_fixed_column_item( $data, 0 )->{"text"}->{"simpleText"} )
|
|
{
|
|
$duration =
|
|
get_fixed_column_item( $data, 0 )->{"text"}->{"simpleText"};
|
|
}
|
|
else {
|
|
$duration = get_fixed_column_item( $data, 0 )->{"text"}->{"runs"}[0]
|
|
->{"text"};
|
|
}
|
|
}
|
|
|
|
my $thumbnails = nav( $data, $THUMBNAILS, 1 );
|
|
|
|
my $isAvailable =
|
|
exists $data->{"musicItemRendererDisplayPolicy"}
|
|
? $data->{"musicItemRendererDisplayPolicy"} ne
|
|
"MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT"
|
|
: 1;
|
|
|
|
my $isExplicit = defined nav( $data, $BADGE_LABEL, 1 );
|
|
|
|
my $videoType = nav(
|
|
$data,
|
|
[
|
|
$MENU_ITEMS, 0, $MNIR, "navigationEndpoint",
|
|
@$NAVIGATION_VIDEO_TYPE
|
|
],
|
|
1,
|
|
);
|
|
|
|
my $song = {
|
|
"videoId" => $videoId,
|
|
"title" => $title,
|
|
"artists" => $artists,
|
|
"album" => $album,
|
|
"likeStatus" => $like,
|
|
"inLibrary" => $library_status,
|
|
"thumbnails" => $thumbnails,
|
|
"isAvailable" => $isAvailable,
|
|
"isExplicit" => $isExplicit,
|
|
"videoType" => $videoType,
|
|
"views" => $views,
|
|
};
|
|
|
|
if ($is_album) {
|
|
$song->{"trackNumber"} =
|
|
$isAvailable
|
|
? int( nav( $data, [ "index", "runs", 0, "text" ] ) )
|
|
: undef;
|
|
}
|
|
|
|
if ( defined $duration ) {
|
|
$song->{"duration"} = $duration;
|
|
$song->{"duration_seconds"} = parse_duration($duration);
|
|
}
|
|
$song->{"setVideoId"} = $setVideoId if defined $setVideoId;
|
|
$song->{"feedbackTokens"} = $feedback_tokens if defined $feedback_tokens;
|
|
|
|
if ($menu_entries) {
|
|
foreach my $menu_entry (@$menu_entries) {
|
|
$song->{ $menu_entry->[-1] } =
|
|
nav( $data, [ @{$MENU_ITEMS}, @$menu_entry ] );
|
|
}
|
|
}
|
|
|
|
return $song;
|
|
}
|
|
|
|
sub validate_playlist_id {
|
|
my ($playlistId) = @_;
|
|
return $playlistId =~ /^VL/ ? substr( $playlistId, 2 ) : $playlistId;
|
|
}
|
|
|
|
our @EXPORT = qw(
|
|
parse_playlist_header
|
|
parse_playlist_items
|
|
parse_playlist_item
|
|
validate_playlist_id
|
|
);
|
|
|
|
1;
|