package YTMusicAPI::Mixins::PlaylistMixin; use strict; use warnings; use Moose::Role; use YTMusicAPI::Helpers; use YTMusicAPI::Navigation; use YTMusicAPI::Parsers::Playlists; use YTMusicAPI::Continuations; sub get_playlist { my ( $self, $playlistId, $limit, $related, $suggestions_limit ) = @_; $limit //= 100; $related //= 0; $suggestions_limit //= 0; my $browseId = $playlistId =~ /^VL/ ? $playlistId : "VL" . $playlistId; my $body = { "browseId" => $browseId }; my $endpoint = "browse"; my $response = $self->_send_request( $endpoint, $body ); my $results = nav( $response, [ @$SINGLE_COLUMN_TAB, @$SECTION_LIST_ITEM, "musicPlaylistShelfRenderer" ] ); my %playlist = ( "id" => $results->{"playlistId"} ); %playlist = ( %playlist, %{ parse_playlist_header($response) } ); $playlist{"trackCount"} = defined $playlist{"trackCount"} ? $playlist{"trackCount"} : scalar @{ $results->{"contents"} }; my $request_func = sub { my $additionalParams = shift; return $self->_send_request( $endpoint, $body, $additionalParams ); }; # suggestions and related are missing e.g. on liked songs my $section_list = nav( $response, [ @$SINGLE_COLUMN_TAB, "sectionListRenderer" ] ); $playlist{"related"} = []; my $parse_func; my $continuation; if ( exists $section_list->{"continuations"} ) { my $additionalParams = get_continuation_params($section_list); my $own_playlist = exists $response->{"header"} ->{"musicEditablePlaylistDetailHeaderRenderer"}; if ( $own_playlist && ( $suggestions_limit > 0 || $related ) ) { $parse_func = sub { my $results = shift; return parse_playlist_items($results); }; my $suggested = $request_func->($additionalParams); $continuation = nav( $suggested, $SECTION_LIST_CONTINUATION ); $additionalParams = get_continuation_params($continuation); my $suggestions_shelf = nav( $continuation, [ $CONTENT, $MUSIC_SHELF ] ); $playlist{"suggestions"} = get_continuation_contents( $suggestions_shelf, $parse_func ); $playlist{"suggestions"} = [ @{ $playlist{"suggestions"} }, @{ get_continuations( $suggestions_shelf, "musicShelfContinuation", $suggestions_limit - scalar @{ $playlist{"suggestions"} }, $request_func, $parse_func, 1 ) } ]; } if ($related) { $response = $request_func->($additionalParams); $continuation = nav( $response, $SECTION_LIST_CONTINUATION, 1 ); if ($continuation) { $parse_func = sub { my $results = shift; return parse_content_list( $results, \&parse_playlist ); }; $playlist{"related"} = get_continuation_contents( nav( $continuation, [ $CONTENT, $CAROUSEL ] ), $parse_func ); } } } $playlist{"tracks"} = []; if ( exists $results->{"contents"} ) { $playlist{"tracks"} = parse_playlist_items( $results->{"contents"} ); $parse_func = sub { my $contents = shift; return parse_playlist_items($contents); }; if ( exists $results->{"continuations"} ) { $playlist{"tracks"} = [ @{ $playlist{"tracks"} }, @{ get_continuations( $results, "musicPlaylistShelfContinuation", $limit, $request_func, $parse_func ) } ]; } } $playlist{"duration_seconds"} = sum_total_duration( \%playlist ); return \%playlist; } sub get_liked_songs { my ( $self, $limit ) = @_; $limit //= 100; return $self->get_playlist( "LM", $limit ); } sub get_saved_episodes { my ( $self, $limit ) = @_; $limit //= 100; return $self->get_playlist( "SE", $limit ); } sub create_playlist { my ( $self, $title, $description, $privacy_status, $video_ids, $source_playlist ) = @_; $privacy_status //= 'PRIVATE'; $self->_check_auth(); my $body = { "title" => $title, "description" => html_to_txt($description) , # YT does not allow HTML tags "privacyStatus" => $privacy_status, }; $body->{"videoIds"} = $video_ids if defined $video_ids; $body->{"sourcePlaylistId"} = $source_playlist if defined $source_playlist; my $endpoint = "playlist/create"; my $response = $self->_send_request( $endpoint, $body ); return exists $response->{"playlistId"} ? $response->{"playlistId"} : $response; } sub edit_playlist { my ( $self, $playlistId, $title, $description, $privacyStatus, $moveItem, $addPlaylistId, $addToTop ) = @_; $self->_check_auth(); my $body = { "playlistId" => validate_playlist_id($playlistId) }; my @actions = (); push( @actions, { "action" => "ACTION_SET_PLAYLIST_NAME", "playlistName" => $title } ) if defined $title; push( @actions, { "action" => "ACTION_SET_PLAYLIST_DESCRIPTION", "playlistDescription" => $description } ) if defined $description; push( @actions, { "action" => "ACTION_SET_PLAYLIST_PRIVACY", "playlistPrivacy" => $privacyStatus } ) if defined $privacyStatus; if ( defined $moveItem ) { push( @actions, { "action" => "ACTION_MOVE_VIDEO_BEFORE", "setVideoId" => $moveItem->[0], "movedSetVideoIdSuccessor" => $moveItem->[1], } ); } push( @actions, { "action" => "ACTION_ADD_PLAYLIST", "addedFullListId" => $addPlaylistId } ) if defined $addPlaylistId; if ( defined $addToTop ) { my $addToTopValue = $addToTop ? "true" : "false"; push( @actions, { "action" => "ACTION_SET_ADD_TO_TOP", "addToTop" => $addToTopValue } ); } $body->{"actions"} = \@actions; my $endpoint = "browse/edit_playlist"; my $response = $self->_send_request( $endpoint, $body ); return exists $response->{"status"} ? $response->{"status"} : $response; } sub delete_playlist { my ( $self, $playlistId ) = @_; $self->_check_auth(); my $body = { "playlistId" => validate_playlist_id($playlistId) }; my $endpoint = "playlist/delete"; my $response = $self->_send_request( $endpoint, $body ); return exists $response->{"status"} ? $response->{"status"} : $response; } sub add_playlist_items { my ( $self, $playlistId, $videoIds, $source_playlist, $duplicates ) = @_; $duplicates //= 0; $self->_check_auth(); my $body = { "playlistId" => validate_playlist_id($playlistId), "actions" => [] }; if ( !$videoIds && !$source_playlist ) { die "You must provide either videoIds or " . "a source_playlist to add to the playlist"; } if ($videoIds) { foreach my $videoId (@$videoIds) { my $action = { "action" => "ACTION_ADD_VIDEO", "addedVideoId" => $videoId }; $action->{"dedupeOption"} = "DEDUPE_OPTION_SKIP" if $duplicates; push( @{ $body->{"actions"} }, $action ); } } if ($source_playlist) { push( @{ $body->{"actions"} }, { "action" => "ACTION_ADD_PLAYLIST", "addedFullListId" => $source_playlist } ); # add an empty ACTION_ADD_VIDEO because otherwise # YTM doesn't return the dict that maps videoIds to their new setVideoIds if ( !$videoIds ) { push( @{ $body->{"actions"} }, { "action" => "ACTION_ADD_VIDEO", "addedVideoId" => undef } ); } } my $endpoint = "browse/edit_playlist"; my $response = $self->_send_request( $endpoint, $body ); if ( exists $response->{"status"} && $response->{"status"} =~ /SUCCEEDED/ ) { my @result_dict = map { $_->get("playlistEditVideoAddedResultData") } @{ $response->{"playlistEditResults"} // [] }; return { "status" => $response->{"status"}, "playlistEditResults" => \@result_dict }; } else { return $response; } } sub remove_playlist_items { my ( $self, $playlistId, $videos ) = @_; $self->_check_auth(); @$videos = grep { exists $_->{"videoId"} && exists $_->{"setVideoId"} } @$videos; die "Cannot remove songs, because setVideoId is missing. " . "Do you own this playlist?" if scalar(@$videos) == 0; my $body = { "playlistId" => validate_playlist_id($playlistId), "actions" => [] }; foreach my $video (@$videos) { push( @{ $body->{"actions"} }, { "setVideoId" => $video->{"setVideoId"}, "removedVideoId" => $video->{"videoId"}, "action" => "ACTION_REMOVE_VIDEO", } ); } my $endpoint = "browse/edit_playlist"; my $response = $self->_send_request( $endpoint, $body ); return exists $response->{"status"} ? $response->{"status"} : $response; } 1;