331 lines
9.8 KiB
Perl
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;
|