Files
ytmusicapi-perl/YTMusicAPI/Mixins/PlaylistMixin.pm
2024-03-24 23:34:26 +01:00

331 lines
9.8 KiB
Perl

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;