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;