Initial commit

This commit is contained in:
mschuepbach
2024-03-24 18:17:49 +01:00
commit fe4a7dc49e
16 changed files with 1784 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
package AuthType;
use constant {
UNAUTHORIZED => 1,
BROWSER => 2,
OAUTH_DEFAULT => 3, # client auth via OAuth token refreshing
OAUTH_CUSTOM_CLIENT =>
4, # YTM instance is using a non-default OAuth client (id & secret)
OAUTH_CUSTOM_FULL =>
5, # allows fully formed OAuth headers to ignore browser auth refresh flow
};
sub oauth_types {
return ( OAUTH_DEFAULT, OAUTH_CUSTOM_CLIENT, OAUTH_CUSTOM_FULL );
}
1;

57
YTMusicAPI/Constants.pm Normal file
View File

@@ -0,0 +1,57 @@
package YTMusicAPI::Constants;
use strict;
use warnings;
use Exporter 'import';
use constant YTM_DOMAIN => 'https://music.youtube.com';
use constant YTM_BASE_API => YTM_DOMAIN . '/youtubei/v1/';
use constant YTM_PARAMS => '?alt=json';
use constant YTM_PARAMS_KEY => '&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30';
use constant USER_AGENT =>
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0';
our $SUPPORTED_LANGUAGES = [
'ar', 'de', 'en', 'es', 'fr', 'hi', 'it', 'ja',
'ko', 'nl', 'pt', 'ru', 'tr', 'ur', 'zh_CN', 'zh_TW'
];
our $SUPPORTED_LOCATIONS = [
'AE', 'AR', 'AT', 'AU', 'AZ', 'BA', 'BD', 'BE', 'BG', 'BH', 'BO', 'BR',
'BY', 'CA', 'CH', 'CL', 'CO', 'CR', 'CY', 'CZ', 'DE', 'DK', 'DO', 'DZ',
'EC', 'EE', 'EG', 'ES', 'FI', 'FR', 'GB', 'GE', 'GH', 'GR', 'GT', 'HK',
'HN', 'HR', 'HU', 'ID', 'IE', 'IL', 'IN', 'IQ', 'IS', 'IT', 'JM', 'JO',
'JP', 'KE', 'KH', 'KR', 'KW', 'KZ', 'LA', 'LB', 'LI', 'LK', 'LT', 'LU',
'LV', 'LY', 'MA', 'ME', 'MK', 'MT', 'MX', 'MY', 'NG', 'NI', 'NL', 'NO',
'NP', 'NZ', 'OM', 'PA', 'PE', 'PG', 'PH', 'PK', 'PL', 'PR', 'PT', 'PY',
'QA', 'RO', 'RS', 'RU', 'SA', 'SE', 'SG', 'SI', 'SK', 'SN', 'SV', 'TH',
'TN', 'TR', 'TW', 'TZ', 'UA', 'UG', 'US', 'UY', 'VE', 'VN', 'YE', 'ZA',
'ZW'
];
use constant OAUTH_CLIENT_ID =>
'861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com';
use constant OAUTH_CLIENT_SECRET => 'SboVhoG9s0rNafixCSGGKXAT';
use constant OAUTH_SCOPE => 'https://www.googleapis.com/auth/youtube';
use constant OAUTH_CODE_URL => 'https://www.youtube.com/o/oauth2/device/code';
use constant OAUTH_TOKEN_URL => 'https://oauth2.googleapis.com/token';
use constant OAUTH_USER_AGENT => USER_AGENT . ' Cobalt/Version';
our @EXPORT = qw(
YTM_DOMAIN
YTM_BASE_API
YTM_PARAMS
YTM_PARAMS_KEY
USER_AGENT
$SUPPORTED_LANGUAGES
$SUPPORTED_LOCATIONS
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET
OAUTH_SCOPE
OAUTH_CODE_URL
OAUTH_TOKEN_URL
OAUTH_USER_AGENT
);
1;

128
YTMusicAPI/Helpers.pm Normal file
View File

@@ -0,0 +1,128 @@
package YTMusicAPI::Helpers;
use strict;
use warnings;
use Exporter 'import';
use POSIX qw(strftime locale_h);
use LWP::UserAgent;
use JSON;
use HTTP::Cookies;
use Digest::SHA qw(sha1_hex);
use Time::HiRes qw(time);
use Unicode::Normalize;
use locale;
use YTMusicAPI::Constants qw(YTM_DOMAIN USER_AGENT);
sub initialize_headers {
return {
'user-agent' => USER_AGENT,
'accept' => '*/*',
'accept-encoding' => 'gzip, deflate',
'content-type' => 'application/json',
'content-encoding' => 'gzip',
'origin' => YTM_DOMAIN,
};
}
sub initialize_context {
return {
'context' => {
'client' => {
'clientName' => "WEB_REMIX",
'clientVersion' => "1."
. strftime( "%Y%m%d", gmtime )
. ".01.00",
},
'user' => {},
}
};
}
sub get_visitor_id {
my ($content) = @_;
my @matches = $content =~ /ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;/g;
my $visitor_id = "";
if (@matches) {
my $ytcfg = decode_json( $matches[0] );
$visitor_id = $ytcfg->{"VISITOR_DATA"} // "";
}
return $visitor_id;
}
sub sapisid_from_cookie {
my ($raw_cookie) = @_;
my $cookie_jar = HTTP::Cookies->new();
$raw_cookie =~ s/\"//g; # Replace double quotes
$cookie_jar->parse_headers( { 'set-cookie' => $raw_cookie } );
my $sapisid = $cookie_jar->get_cookie("__Secure-3PAPISID");
return $sapisid;
}
sub get_authorization {
my ($auth) = @_;
my $unix_timestamp = int(time);
my $digest = sha1_hex( $unix_timestamp . " " . $auth );
return "SAPISIDHASH " . $unix_timestamp . "_" . $digest;
}
sub to_int {
my ($string) = @_;
$string = NFKD($string);
my $number_string = $string =~ s/\D//gr;
setlocale( LC_ALL, "en_US.UTF-8" );
my $int_value;
eval { $int_value = atoi($number_string); };
if ($@) {
$number_string =~ s/,//g;
$int_value = int($number_string);
}
return $int_value;
}
sub sum_total_duration {
my ($item) = @_;
return 0 unless exists $item->{tracks};
my $total_duration =
sum( map { exists $_->{duration_seconds} ? $_->{duration_seconds} : 0 }
@{ $item->{tracks} } );
return $total_duration;
}
sub array_index {
my ( $list, $value ) = @_;
my $index = 0;
my $found_index = -1;
foreach my $type (@$list) {
if ( $type eq $value ) {
$found_index = $index;
last;
}
$index++;
}
return $found_index;
}
our @EXPORT = qw(
initialize_headers
initialize_context
get_visitor_id
sapisid_from_cookie
get_authorization
to_int
sum_total_duration
array_index
);
1;

View File

@@ -0,0 +1,177 @@
package YTMusicAPI::Mixins::SearchMixin;
use strict;
use warnings;
use Moose::Role;
use Data::Dumper;
use YTMusicAPI::Helpers;
use YTMusicAPI::Navigation;
use YTMusicAPI::Parsers::Search;
sub search {
my ( $self, $query, $filter, $scope, $limit, $ignore_spelling ) = @_;
$filter //= undef;
$scope //= undef;
$limit //= 20;
$ignore_spelling //= 0;
my $body = { 'query' => $query };
my $endpoint = "search";
my $search_results = [];
my $filters = [
"albums", "artists",
"playlists", "community_playlists",
"featured_playlists", "songs",
"videos", "profiles",
"podcasts", "episodes",
];
if ( $filter and !grep { $_ eq $filter } @$filters ) {
die "Invalid filter provided. "
. "Please use one of the following filters or leave out the parameter: "
. join( ", ", @$filters );
}
my $scopes = [ "library", "uploads" ];
if ( $scope and !grep { $_ eq $scope } @$scopes ) {
die "Invalid scope provided. "
. "Please use one of the following scopes or leave out the parameter: "
. join( ", ", @$scopes );
}
if ( $scope and $scope eq @$scopes[1] and $filter ) {
die "No filter can be set when searching uploads. "
. "Please unset the filter parameter when scope is set to uploads.";
}
if ( $scope
and $scope eq @$scopes[0]
and grep { $_ eq $filter } @$filters[ 3 .. 4 ] )
{
die "$filter cannot be set when searching library. "
. "Please use one of the following filters or leave out the parameter: "
. join( ", ", ( @$filters[ 0 .. 2 ], @$filters[ 5 .. $#$filters ] ) );
}
my $params = get_search_params( $filter, $scope, $ignore_spelling );
if ($params) {
$body->{"params"} = $params;
}
my $response = $self->_send_request( $endpoint, $body );
if ( !exists $response->{"contents"} ) {
return $search_results;
}
my $results;
if ( exists $response->{"contents"}{"tabbedSearchResultsRenderer"} ) {
my $tab_index =
!( $scope or $filter ) ? 0 : array_index( $scopes, $scope ) + 1;
$results =
$response->{"contents"}{"tabbedSearchResultsRenderer"}{"tabs"}
[$tab_index]{"tabRenderer"}{"content"};
}
else {
$results = $response->{"contents"};
}
$results = nav( $results, $SECTION_LIST );
if ( scalar @{$results} == 1
and exists $results->[0]->{"itemSectionRenderer"} )
{
return $search_results;
}
if ( $filter and index( $filter, "playlists" ) != -1 ) {
$filter = "playlists";
}
elsif ( $scope and $scope == $scopes->[1] ) {
$filter = $scopes->[1];
}
foreach my $res ( @{$results} ) {
my $category = undef;
my $type = undef;
if ( exists $res->{"musicCardShelfRenderer"} ) {
my $top_result = parse_top_result( $res->{"musicCardShelfRenderer"},
$self->{parser}->get_search_result_types() );
push( @$search_results, $top_result );
if ( $results =
nav( $res, [ "musicCardShelfRenderer", "contents" ], 1 ) )
{
# category "more from youtube" is missing sometimes
if ( exists $results->[0]->{"messageRenderer"} ) {
$category = nav( shift(@$results),
[ "messageRenderer", @$TEXT_RUN_TEXT ] );
}
}
else {
next;
}
}
elsif ( exists $res->{"musicShelfRenderer"} ) {
$results = $res->{"musicShelfRenderer"}{"contents"};
my $type_filter = $filter;
$category = nav( $res, [ @$MUSIC_SHELF, @$TITLE_TEXT ], 1 );
if ( !$type_filter and $scope and $scope eq $scopes->[0] ) {
$type_filter = $category;
}
$type = substr( $type_filter, 0, -1 ) if $type_filter;
$type = lc($type) if $type;
}
else {
next;
}
my $search_result_types = $self->{parser}->get_search_result_types();
push(
@$search_results,
parse_search_results(
$results, $search_result_types, $type, $category
)
);
if ($filter) { # if filter is set, there are continuations
my $request_func = sub {
my ($additionalParams) = @_;
return $self->_send_request( $endpoint, $body,
$additionalParams );
};
my $parse_func = sub {
my ($contents) = @_;
return parse_search_results( $contents, $search_result_types,
$type, $category );
};
push(
@$search_results,
get_continuations(
$res->{"musicShelfRenderer"},
"musicShelfContinuation",
$limit - scalar(@$search_results),
$request_func,
$parse_func
)
);
}
}
return $search_results;
}
sub get_search_suggestions {
my ( $self, $query, $detailed_runs ) = @_;
$detailed_runs //= 0;
my %body = ( "input" => $query );
my $endpoint = "music/get_search_suggestions";
my $response = $self->_send_request( $endpoint, %body );
my $search_suggestions =
parse_search_suggestions( $response, $detailed_runs );
return $search_suggestions;
}
1;

253
YTMusicAPI/Navigation.pm Normal file
View File

@@ -0,0 +1,253 @@
package YTMusicAPI::Navigation;
use strict;
use warnings;
use Exporter 'import';
our $CONTENT = [ "contents", 0 ];
our $RUN_TEXT = [ "runs", 0, "text" ];
our $TAB_CONTENT = [ "tabs", 0, "tabRenderer", "content" ];
our $TAB_1_CONTENT = [ "tabs", 1, "tabRenderer", "content" ];
our $TWO_COLUMN_RENDERER = [ "contents", "twoColumnBrowseResultsRenderer" ];
our $SINGLE_COLUMN = [ "contents", "singleColumnBrowseResultsRenderer" ];
our $SINGLE_COLUMN_TAB = [ @$SINGLE_COLUMN, @$TAB_CONTENT ];
our $SECTION = ["sectionListRenderer"];
our $SECTION_LIST = [ @$SECTION, "contents" ];
our $SECTION_LIST_ITEM = [ @$SECTION, @$CONTENT ];
our $RESPONSIVE_HEADER = ["musicResponsiveHeaderRenderer"];
our $ITEM_SECTION = [ "itemSectionRenderer", @$CONTENT ];
our $MUSIC_SHELF = ["musicShelfRenderer"];
our $GRID = ["gridRenderer"];
our $GRID_ITEMS = [ @$GRID, "items" ];
our $MENU = [ "menu", "menuRenderer" ];
our $MENU_ITEMS = [ @$MENU, "items" ];
our $MENU_LIKE_STATUS =
[ @$MENU, "topLevelButtons", 0, "likeButtonRenderer", "likeStatus" ];
our $MENU_SERVICE = [ "menuServiceItemRenderer", "serviceEndpoint" ];
our $TOGGLE_MENU = "toggleMenuServiceItemRenderer";
our $OVERLAY_RENDERER =
[ "musicItemThumbnailOverlayRenderer", "content", "musicPlayButtonRenderer" ];
our $PLAY_BUTTON = [ "overlay", @$OVERLAY_RENDERER ];
our $NAVIGATION_BROWSE = [ "navigationEndpoint", "browseEndpoint" ];
our $NAVIGATION_BROWSE_ID = [ @$NAVIGATION_BROWSE, "browseId" ];
our $PAGE_TYPE = [
"browseEndpointContextSupportedConfigs",
"browseEndpointContextMusicConfig",
"pageType"
];
our $WATCH_VIDEO_ID = [ "watchEndpoint", "videoId" ];
our $NAVIGATION_VIDEO_ID = [ "navigationEndpoint", @$WATCH_VIDEO_ID ];
our $QUEUE_VIDEO_ID = [ "queueAddEndpoint", "queueTarget", "videoId" ];
our $NAVIGATION_PLAYLIST_ID =
[ "navigationEndpoint", "watchEndpoint", "playlistId" ];
our $WATCH_PID = [ "watchPlaylistEndpoint", "playlistId" ];
our $NAVIGATION_WATCH_PLAYLIST_ID = [ "navigationEndpoint", @$WATCH_PID ];
our $NAVIGATION_VIDEO_TYPE = [
"watchEndpoint", "watchEndpointMusicSupportedConfigs",
"watchEndpointMusicConfig", "musicVideoType",
];
our $ICON_TYPE = [ "icon", "iconType" ];
our $TOGGLED_BUTTON = [ "toggleButtonRenderer", "isToggled" ];
our $TITLE = [ "title", "runs", 0 ];
our $TITLE_TEXT = [ "title", @$RUN_TEXT ];
our $TEXT_RUNS = [ "text", "runs" ];
our $TEXT_RUN = [ @$TEXT_RUNS, 0 ];
our $TEXT_RUN_TEXT = [ @$TEXT_RUN, "text" ];
our $SUBTITLE = [ "subtitle", @$RUN_TEXT ];
our $SUBTITLE_RUNS = [ "subtitle", "runs" ];
our $SUBTITLE_RUN = [ @$SUBTITLE_RUNS, 0 ];
our $SUBTITLE2 = [ @$SUBTITLE_RUNS, 2, "text" ];
our $SUBTITLE3 = [ @$SUBTITLE_RUNS, 4, "text" ];
our $THUMBNAIL = [ "thumbnail", "thumbnails" ];
our $THUMBNAILS = [ "thumbnail", "musicThumbnailRenderer", @$THUMBNAIL ];
our $THUMBNAIL_RENDERER =
[ "thumbnailRenderer", "musicThumbnailRenderer", @$THUMBNAIL ];
our $THUMBNAIL_OVERLAY = [
"thumbnailOverlay", @$OVERLAY_RENDERER,
"playNavigationEndpoint", @$WATCH_PID
];
our $THUMBNAIL_CROPPED =
[ "thumbnail", "croppedSquareThumbnailRenderer", @$THUMBNAIL ];
our $FEEDBACK_TOKEN = [ "feedbackEndpoint", "feedbackToken" ];
our $BADGE_PATH = [
0, "musicInlineBadgeRenderer",
"accessibilityData", "accessibilityData",
"label"
];
our $BADGE_LABEL = [ "badges", @$BADGE_PATH ];
our $SUBTITLE_BADGE_LABEL = [ "subtitleBadges", @$BADGE_PATH ];
our $CATEGORY_TITLE =
[ "musicNavigationButtonRenderer", "buttonText", @$RUN_TEXT ];
our $CATEGORY_PARAMS = [
"musicNavigationButtonRenderer", "clickCommand",
"browseEndpoint", "params"
];
our $MMRIR = "musicMultiRowListItemRenderer";
our $MRLIR = "musicResponsiveListItemRenderer";
our $MTRIR = "musicTwoRowItemRenderer";
our $MNIR = "menuNavigationItemRenderer";
our $TASTE_PROFILE_ITEMS = [ "contents", "tastebuilderRenderer", "contents" ];
our $TASTE_PROFILE_ARTIST = [ "title", "runs" ];
our $SECTION_LIST_CONTINUATION =
[ "continuationContents", "sectionListContinuation" ];
our $MENU_PLAYLIST_ID =
[ @$MENU_ITEMS, 0, $MNIR, @$NAVIGATION_WATCH_PLAYLIST_ID ];
our $MULTI_SELECT = ["musicMultiSelectMenuItemRenderer"];
our $HEADER_DETAIL = [ "header", "musicDetailHeaderRenderer" ];
our $HEADER_SIDE = [ "header", "musicSideAlignedItemRenderer" ];
our $HEADER_MUSIC_VISUAL = [ "header", "musicVisualHeaderRenderer" ];
our $DESCRIPTION_SHELF = ["musicDescriptionShelfRenderer"];
our $DESCRIPTION = [ "description", @$RUN_TEXT ];
our $CAROUSEL = ["musicCarouselShelfRenderer"];
our $IMMERSIVE_CAROUSEL = ["musicImmersiveCarouselShelfRenderer"];
our $CAROUSEL_CONTENTS = [ @$CAROUSEL, "contents" ];
our $CAROUSEL_TITLE =
[ "header", "musicCarouselShelfBasicHeaderRenderer", @$TITLE ];
our $CARD_SHELF_TITLE =
[ "header", "musicCardShelfHeaderBasicRenderer", @$TITLE_TEXT ];
our $FRAMEWORK_MUTATIONS =
[ "frameworkUpdates", "entityBatchUpdate", "mutations" ];
sub nav {
my ( $root, $items, $none_if_absent ) = @_;
$none_if_absent //= 0;
if (!ref($items)) {
$root = $root->{$items};
return $root;
}
foreach my $k (@$items) {
if ( ref($root) eq 'HASH' && exists $root->{$k} ) {
$root = $root->{$k};
}
elsif ( ref($root) eq 'ARRAY' && $k =~ /^\d+$/ && $k < @$root ) {
$root = $root->[$k];
}
else {
if ($none_if_absent) {
return undef;
}
else {
die "Unable to find '$k' using path @$items on $root";
}
}
}
return $root;
}
sub find_object_by_key {
my ( $object_list, $key, $nested, $is_key ) = @_;
foreach my $item (@$object_list) {
if ($nested) {
$item = $item->{$nested};
}
if ( exists $item->{$key} ) {
return $is_key ? $item->{$key} : $item;
}
}
return undef;
}
sub find_objects_by_key {
my ( $object_list, $key, $nested ) = @_;
my @objects;
foreach my $item (@$object_list) {
if ($nested) {
$item = $item->{$nested};
}
if ( exists $item->{$key} ) {
push @objects, $item;
}
}
return \@objects;
}
our @EXPORT = qw(
$CONTENT
$RUN_TEXT
$TAB_CONTENT
$TAB_1_CONTENT
$TWO_COLUMN_RENDERER
$SINGLE_COLUMN
$SINGLE_COLUMN_TAB
$SECTION
$SECTION_LIST
$SECTION_LIST_ITEM
$RESPONSIVE_HEADER
$ITEM_SECTION
$MUSIC_SHELF
$GRID
$GRID_ITEMS
$MENU
$MENU_ITEMS
$MENU_LIKE_STATUS
$MENU_SERVICE
$TOGGLE_MENU
$OVERLAY_RENDERER
$PLAY_BUTTON
$NAVIGATION_BROWSE
$NAVIGATION_BROWSE_ID
$PAGE_TYPE
$WATCH_VIDEO_ID
$NAVIGATION_VIDEO_ID
$QUEUE_VIDEO_ID
$NAVIGATION_PLAYLIST_ID
$WATCH_PID
$NAVIGATION_WATCH_PLAYLIST_ID
$NAVIGATION_VIDEO_TYPE
$ICON_TYPE
$TOGGLED_BUTTON
$TITLE
$TITLE_TEXT
$TEXT_RUNS
$TEXT_RUN
$TEXT_RUN_TEXT
$SUBTITLE
$SUBTITLE_RUNS
$SUBTITLE_RUN
$SUBTITLE2
$SUBTITLE3
$THUMBNAIL
$THUMBNAILS
$THUMBNAIL_RENDERER
$THUMBNAIL_OVERLAY
$THUMBNAIL_CROPPED
$FEEDBACK_TOKEN
$BADGE_PATH
$BADGE_LABEL
$SUBTITLE_BADGE_LABEL
$CATEGORY_TITLE
$CATEGORY_PARAMS
$MMRIR
$MRLIR
$MTRIR
$MNIR
$TASTE_PROFILE_ITEMS
$TASTE_PROFILE_ARTIST
$SECTION_LIST_CONTINUATION
$MENU_PLAYLIST_ID
$MULTI_SELECT
$HEADER_DETAIL
$HEADER_SIDE
$HEADER_MUSIC_VISUAL
$DESCRIPTION_SHELF
$DESCRIPTION
$CAROUSEL
$IMMERSIVE_CAROUSEL
$CAROUSEL_CONTENTS
$CAROUSEL_TITLE
$CARD_SHELF_TITLE
$FRAMEWORK_MUTATIONS
nav
find_object_by_key
find_objects_by_key
);
1;

View File

@@ -0,0 +1,56 @@
package YTMusicAPI::Parsers::Albums;
use strict;
use warnings;
use YTMusicAPI::Navigation;
sub parse_album_header {
my ($response) = @_;
my $header = nav( $response, $HEADER_DETAIL );
my $album = {
title => nav( $header, $TITLE_TEXT ),
type => nav( $header, $SUBTITLE ),
thumbnails => nav( $header, $THUMBNAIL_CROPPED ),
isExplicit => defined( nav( $header, $SUBTITLE_BADGE_LABEL, 1 ) )
? 1
: 0,
};
if ( exists $header->{description} ) {
$album->{description} = $header->{description}->{runs}->[0]->{text};
}
my $album_info = parse_song_runs(
$header->{subtitle}->{runs}->[ 2 .. $#{ $header->{subtitle}->{runs} } ]
);
@$album{ keys %$album_info } = values %$album_info;
if ( scalar @{ $header->{secondSubtitle}->{runs} } > 1 ) {
$album->{trackCount} =
to_int( $header->{secondSubtitle}->{runs}->[0]->{text} );
$album->{duration} = $header->{secondSubtitle}->{runs}->[2]->{text};
}
else {
$album->{duration} = $header->{secondSubtitle}->{runs}->[0]->{text};
}
my $menu = nav( $header, $MENU );
my $toplevel = $menu->{topLevelButtons};
$album->{audioPlaylistId} =
nav( $toplevel, [ 0, 'buttonRenderer', $NAVIGATION_WATCH_PLAYLIST_ID ],
1 );
unless ( $album->{audioPlaylistId} ) {
$album->{audioPlaylistId} =
nav( $toplevel, [ 0, 'buttonRenderer', $NAVIGATION_PLAYLIST_ID ], 1 );
}
my $service =
nav( $toplevel, [ 1, 'buttonRenderer', 'defaultServiceEndpoint' ], 1 );
if ($service) {
$album->{likeStatus} = parse_like_status($service);
}
return $album;
}
1;

View File

@@ -0,0 +1,78 @@
package YTMusicAPI::Parsers::Parser;
use strict;
use warnings;
use YTMusicAPI::Navigation;
sub new {
my ( $class, $language ) = @_;
my $self = { lang => $language };
bless $self, $class;
return $self;
}
sub _ {
my ($input) = @_;
return $input;
}
sub get_search_result_types {
my ($self) = @_;
return [
_("artist"), _("playlist"), _("song"), _("video"),
_("station"), _("profile"), _("podcast"), _("episode"),
];
}
sub parse_channel_contents {
my ( $self, $results ) = @_;
my @categories = (
[ "albums", _("albums"), \&parse_album, $MTRIR ],
[ "singles", _("singles"), \&parse_single, $MTRIR ],
[ "shows", _("shows"), \&parse_album, $MTRIR ],
[ "videos", _("videos"), \&parse_video, $MTRIR ],
[ "playlists", _("playlists"), \&parse_playlist, $MTRIR ],
[ "related", _("related"), \&parse_related_artist, $MTRIR ],
[ "episodes", _("episodes"), \&parse_episode, $MMRIR ],
[ "podcasts", _("podcasts"), \&parse_podcast, $MTRIR ],
);
my %artist = {};
foreach my $category_tuple (@categories) {
my ( $category, $category_local, $category_parser, $category_key ) =
@$category_tuple;
my @data = map { $_->{"musicCarouselShelfRenderer"} }
grep {
exists $_->{"musicCarouselShelfRenderer"}
&& nav( $_, $CAROUSEL . $CAROUSEL_TITLE )->{"text"} =~
/^$category_local$/i
} $results;
if ( scalar @data > 0 ) {
$artist{$category} = { "browseId" => undef, "results" => [] };
if (
exists nav( $data[0], $CAROUSEL_TITLE )->{"navigationEndpoint"}
)
{
$artist{$category}->{"browseId"} =
nav( $data[0], $CAROUSEL_TITLE . $NAVIGATION_BROWSE_ID );
$artist{$category}->{"params"} = nav( $data[0],
$CAROUSEL_TITLE . $NAVIGATION_BROWSE . ["params"], 1 );
}
$artist{$category}->{"results"} =
parse_content_list( $data[0]->{"contents"},
$category_parser, "key" => $category_key );
}
}
return %artist;
}
1;

View File

@@ -0,0 +1,397 @@
package YTMusicAPI::Parsers::Search;
use strict;
use warnings;
use Exporter 'import';
use YTMusicAPI::Parsers::Utils;
use YTMusicAPI::Parsers::Songs;
use YTMusicAPI::Navigation;
sub get_search_result_type {
my ( $result_type_local, $result_types_local ) = @_;
if ( !$result_type_local ) {
return undef;
}
my @result_types = (
"artist", "playlist", "song", "video",
"station", "profile", "podcast", "episode"
);
$result_type_local = lc $result_type_local;
my $result_type;
my $index = 0;
my $found_index = -1;
# Search for index
foreach my $type (@$result_types_local) {
if ( $type eq $result_type_local ) {
$found_index = $index;
last;
}
$index++;
}
if ( $found_index == -1 ) {
$result_type = "album";
}
else {
$result_type = $result_types[$found_index];
}
return $result_type;
}
sub parse_top_result {
my ( $data, $search_result_types ) = @_;
my $result_type =
get_search_result_type( nav( $data, $SUBTITLE ), $search_result_types );
my $search_result = {
"category" => nav( $data, $CARD_SHELF_TITLE ),
"resultType" => $result_type
};
if ( $result_type eq "artist" ) {
my $subscribers = nav( $data, $SUBTITLE2, 1 );
if ($subscribers) {
$search_result->{"subscribers"} = ( split " ", $subscribers )[0];
}
my $artist_info = parse_song_runs( nav( $data, [ "title", "runs" ] ) );
@$search_result{ keys %$artist_info } = values %$artist_info;
}
if ( $result_type eq "song" || $result_type eq "video" ) {
my $on_tap = $data->{"onTap"};
if ($on_tap) {
$search_result->{"videoId"} = nav( $on_tap, $WATCH_VIDEO_ID );
$search_result->{"videoType"} =
nav( $on_tap, $NAVIGATION_VIDEO_TYPE );
}
}
if ( grep { $_ eq $result_type } [ "song", "video", "album" ] ) {
$search_result->{"videoId"} =
nav( $data, [ "onTap", $WATCH_VIDEO_ID ], 1 );
$search_result->{"videoType"} =
nav( $data, [ "onTap", $NAVIGATION_VIDEO_TYPE ], 1 );
$search_result->{"title"} = nav( $data, $TITLE_TEXT );
my $runs = nav( $data, [ "subtitle", "runs" ] );
my $song_info = parse_song_runs($runs);
@$search_result{ keys %$song_info } = values %$song_info;
}
if ( $result_type eq "album" ) {
$search_result->{"browseId"} =
nav( $data, $TITLE . $NAVIGATION_BROWSE_ID, 1 );
}
if ( $result_type eq "playlist" ) {
$search_result->{"playlistId"} = nav( $data, $MENU_PLAYLIST_ID );
$search_result->{"title"} = nav( $data, $TITLE_TEXT );
my @x = nav( $data, [ "subtitle", "runs" ], 0 );
$search_result->{"author"} = parse_song_artists_runs( $x[ 2 .. $#x ] );
}
$search_result->{"thumbnails"} = nav( $data, $THUMBNAILS, 1 );
return $search_result;
}
sub parse_search_result {
my ( $data, $search_result_types, $result_type, $category ) = @_;
my $default_offset = ( !$result_type || $result_type eq "album" ) ? 2 : 0;
my $search_result = { "category" => $category };
my $video_type = nav( $data,
[ $PLAY_BUTTON, "playNavigationEndpoint", $NAVIGATION_VIDEO_TYPE ], 1 );
if ( !$result_type && $video_type ) {
$result_type = $video_type eq "MUSIC_VIDEO_TYPE_ATV" ? "song" : "video";
}
unless ($result_type) {
$result_type = get_search_result_type( get_item_text( $data, 1 ),
$search_result_types );
}
$search_result->{"resultType"} = $result_type;
if ( $result_type ne "artist" ) {
$search_result->{"title"} = get_item_text( $data, 0 );
}
if ( $result_type eq "artist" ) {
$search_result->{"artist"} = get_item_text( $data, 0 );
parse_menu_playlists( $data, $search_result );
}
elsif ( $result_type eq "album" ) {
$search_result->{"type"} = get_item_text( $data, 1 );
}
elsif ( $result_type eq "playlist" ) {
my $flex_item = get_flex_column_item( $data, 1 )->{"text"}->{"runs"};
my $has_author = scalar(@$flex_item) == $default_offset + 3;
$search_result->{"itemCount"} = (
split(
" ",
get_item_text( $data, 1, $default_offset + $has_author * 2 )
)
)[0];
$search_result->{"author"} =
$has_author ? get_item_text( $data, 1, $default_offset ) : undef;
}
elsif ( $result_type eq "station" ) {
$search_result->{"videoId"} = nav( $data, $NAVIGATION_VIDEO_ID );
$search_result->{"playlistId"} = nav( $data, $NAVIGATION_PLAYLIST_ID );
}
elsif ( $result_type eq "profile" ) {
$search_result->{"name"} = get_item_text( $data, 1, 2, 1 );
}
elsif ( $result_type eq "song" ) {
$search_result->{"album"} = undef;
if ( exists $data->{"menu"} ) {
my $toggle_menu =
find_object_by_key( nav( $data, $MENU_ITEMS ), $TOGGLE_MENU );
if ($toggle_menu) {
$search_result->{"inLibrary"} =
parse_song_library_status($toggle_menu);
$search_result->{"feedbackTokens"} =
parse_song_menu_tokens($toggle_menu);
}
}
}
elsif ( $result_type eq "upload" ) {
my $browse_id = nav( $data, $NAVIGATION_BROWSE_ID, 1 );
if ( !$browse_id ) { # song result
my @flex_items = map {
nav( get_flex_column_item( $data, $_ ), [ "text", "runs" ], 1 )
} ( 0 .. 1 );
if ( $flex_items[0] ) {
$search_result->{"videoId"} =
nav( $flex_items[0]->[0], $NAVIGATION_VIDEO_ID, 1 );
$search_result->{"playlistId"} =
nav( $flex_items[0]->[0], $NAVIGATION_PLAYLIST_ID, 1 );
}
if ( $flex_items[1] ) {
my $song_runs = parse_song_runs( $flex_items[1] );
@$search_result{ keys %$song_runs } = values %$song_runs;
}
$search_result->{"resultType"} = "song";
}
else {
# artist or album result
$search_result->{"browseId"} = $browse_id;
if ( $search_result->{"browseId"} =~ /artist/ ) {
$search_result->{"resultType"} = "artist";
}
else {
my $flex_item2 = get_flex_column_item( $data, 1 );
my $i = 0;
my @runs = map { $_->{"text"} }
grep { $i++ % 2 == 0 } @{ $flex_item2->{"text"}->{"runs"} };
if ( scalar(@runs) > 1 ) {
$search_result->{"artist"} = $runs[1];
}
if ( scalar(@runs) > 2 ) { # date may be missing
$search_result->{"releaseDate"} = $runs[2];
}
$search_result->{"resultType"} = "album";
}
}
}
if ( $result_type =~ /song|video|episode/ ) {
$search_result->{"videoId"} = nav(
$data,
[
$PLAY_BUTTON, "playNavigationEndpoint",
"watchEndpoint", "videoId"
],
1
);
$search_result->{"videoType"} = $video_type;
}
if ( grep { $_ eq $result_type } [ "song", "video", "album" ] ) {
$search_result->{"duration"} = undef;
$search_result->{"year"} = undef;
my $flex_item = get_flex_column_item( $data, 1 );
my $runs = $flex_item->{"text"}->{"runs"};
my $song_info = parse_song_runs($runs);
@$search_result{ keys %$song_info } = values %$song_info;
}
if ( grep { $_ eq $result_type }
[ "artist", "album", "playlist", "profile", "podcast" ] )
{
$search_result->{"browseId"} = nav( $data, $NAVIGATION_BROWSE_ID, 1 );
}
if ( grep { $_ eq $result_type } [ "song", "album" ] ) {
$search_result->{"isExplicit"} =
defined( nav( $data, $BADGE_LABEL, 1 ) ) ? 1 : undef;
}
if ( $result_type eq "episode" ) {
my $flex_item = get_flex_column_item( $data, 1 );
my $has_date = scalar( @{ nav( $flex_item, $TEXT_RUNS ) } ) > 1 ? 1 : 0;
$search_result->{"live"} =
nav( $data, [ "badges", 0, "liveBadgeRenderer" ], 1 ) ? 1 : undef;
if ($has_date) {
$search_result->{"date"} = nav( $flex_item, $TEXT_RUN_TEXT );
}
$search_result->{"podcast"} =
parse_id_name( nav( $flex_item, [ "text", "runs", $has_date * 2 ] ) );
}
$search_result->{"thumbnails"} = nav( $data, $THUMBNAILS, 1 );
return $search_result;
}
sub parse_search_results {
my ( $results, $search_result_types, $result_type, $category ) = @_;
my @parsed_results;
foreach my $result (@$results) {
push @parsed_results,
parse_search_result( $result->{$MRLIR}, $search_result_types,
$result_type, $category );
}
return \@parsed_results;
}
sub get_search_params {
my ( $filter, $scope, $ignore_spelling ) = @_;
my $filtered_param1 = "EgWKAQ";
my $params;
my $param1;
my $param2;
my $param3;
return $params unless defined $filter || defined $scope || $ignore_spelling;
if ( $scope and $scope eq "uploads" ) {
$params = "agIYAw%3D%3D";
}
if ( $scope and $scope eq "library" ) {
if ($filter) {
$param1 = $filtered_param1;
$param2 = _get_param2($filter);
$param3 = "AWoKEAUQCRADEAoYBA%3D%3D";
}
else {
$params = "agIYBA%3D%3D";
}
}
if ( !defined $scope && $filter ) {
if ( $filter eq "playlists" ) {
$params = "Eg-KAQwIABAAGAAgACgB";
if ( !$ignore_spelling ) {
$params .= "MABqChAEEAMQCRAFEAo%3D";
}
else {
$params .= "MABCAggBagoQBBADEAkQBRAK";
}
}
elsif ( $filter =~ /playlists/ ) {
$param1 = "EgeKAQQoA";
$param2 = $filter eq "featured_playlists" ? "Dg" : "EA";
if ( !$ignore_spelling ) {
$param3 = "BagwQDhAKEAMQBBAJEAU%3D";
}
else {
$param3 = "BQgIIAWoMEA4QChADEAQQCRAF";
}
}
else {
$param1 = $filtered_param1;
$param2 = _get_param2($filter);
$param3 =
$ignore_spelling
? "AUICCAFqDBAOEAoQAxAEEAkQBQ%3D%3D"
: "AWoMEA4QChADEAQQCRAF";
}
}
if ( !defined $scope && !defined $filter && $ignore_spelling ) {
$params = "EhGKAQ4IARABGAEgASgAOAFAAUICCAE%3D";
}
return $params if defined $params;
return $param1 . $param2 . $param3
if defined $param1 && defined $param2 && defined $param3;
}
sub _get_param2 {
my ($filter) = @_;
my $filter_params = {
"songs" => "II",
"videos" => "IQ",
"albums" => "IY",
"artists" => "Ig",
"playlists" => "Io",
"profiles" => "JY",
"podcasts" => "JQ",
"episodes" => "JI",
};
return $filter_params->{$filter};
}
sub parse_search_suggestions {
my ( $results, $detailed_runs ) = @_;
return []
unless $results->{"contents"}[0]{"searchSuggestionsSectionRenderer"}
{"contents"};
my $raw_suggestions =
$results->{"contents"}[0]{"searchSuggestionsSectionRenderer"}{"contents"};
my @suggestions = ();
foreach my $raw_suggestion (@$raw_suggestions) {
my ( $suggestion_content, $from_history );
if ( exists $raw_suggestion->{"historySuggestionRenderer"} ) {
$suggestion_content =
$raw_suggestion->{"historySuggestionRenderer"};
$from_history = 1;
}
else {
$suggestion_content = $raw_suggestion->{"searchSuggestionRenderer"};
$from_history = 0;
}
my $text = $suggestion_content->{"navigationEndpoint"}{"searchEndpoint"}
{"query"};
my $runs = $suggestion_content->{"suggestion"}{"runs"};
if ($detailed_runs) {
push @suggestions,
{
"text" => $text,
"runs" => $runs,
"fromHistory" => $from_history
};
}
else {
push @suggestions, $text;
}
}
return \@suggestions;
}
our @EXPORT = qw(
get_search_result_type
parse_top_result
parse_search_result
parse_search_results
get_search_params
parse_search_suggestions
);
1;

155
YTMusicAPI/Parsers/Songs.pm Normal file
View File

@@ -0,0 +1,155 @@
package YTMusicAPI::Parsers::Songs;
use strict;
use warnings;
use Exporter 'import';
use Regexp::Common qw(number);
use YTMusicAPI::Parsers::Utils;
use YTMusicAPI::Navigation;
sub parse_song_artists {
my ( $data, $index ) = @_;
my $flex_item = get_flex_column_item( $data, $index );
if ( !$flex_item ) {
return undef;
}
else {
my $runs = $flex_item->{"text"}{"runs"};
return parse_song_artists_runs($runs);
}
}
sub parse_song_artists_runs {
my ($runs) = @_;
my @artists = ();
for my $j ( 0 .. int( @$runs / 2 ) ) {
push @artists,
{
"name" => $runs->[ $j * 2 ]{"text"},
"id" => nav( $runs->[ $j * 2 ], $NAVIGATION_BROWSE_ID, 1 )
};
}
return \@artists;
}
sub parse_song_runs {
my ($runs) = @_;
my %parsed = ( "artists" => [] );
my $i = 0;
for my $run (@$runs) {
if ( $i % 2 ) { # uneven items are always separators
$i++;
next;
}
my $text = $run->{"text"};
if ( exists $run->{"navigationEndpoint"} ) { # artist or album
my $item = {
"name" => $text,
"id" => nav( $run, $NAVIGATION_BROWSE_ID, 1 )
};
if (
$item->{"id"}
&& ( $item->{"id"} =~ /^MPRE/
|| $item->{"id"} =~ /release_detail/ )
)
{ # album
$parsed{"album"} = $item;
}
else { # artist
push @{ $parsed{"artists"} }, $item;
}
}
else {
# note: YT uses non-breaking space \xa0 to separate number and magnitude
if ( $text =~ /^\d([^ ])* [^ ]*$/ && $i > 0 ) {
( $parsed{"views"} ) = split( / /, $text, 2 );
}
elsif ( $text =~ /^(\d+:)*\d+:\d+$/ ) {
$parsed{"duration"} = $text;
$parsed{"duration_seconds"} = parse_duration($text);
}
elsif ( $text =~ /^\d{4}$/ ) {
$parsed{"year"} = $text;
}
else { # artist without id
push @{ $parsed{"artists"} },
{ "name" => $text, "id" => undef };
}
}
$i++;
}
return \%parsed;
}
sub parse_song_album {
my ( $data, $index ) = @_;
my $flex_item = get_flex_column_item( $data, $index );
my $browse_id = nav( $flex_item, $TEXT_RUN + $NAVIGATION_BROWSE_ID, 1 );
return !$flex_item
? undef
: { "name" => get_item_text( $data, $index ), "id" => $browse_id };
}
sub parse_song_library_status {
my ($item) = @_;
my $library_status =
nav( $item, [ $TOGGLE_MENU, "defaultIcon", "iconType" ], 1 );
return $library_status eq "LIBRARY_SAVED";
}
sub parse_song_menu_tokens {
my ($item) = @_;
my $toggle_menu = $item->{$TOGGLE_MENU};
my $library_add_token =
nav( $toggle_menu, [ "defaultServiceEndpoint", $FEEDBACK_TOKEN ], 1 );
my $library_remove_token =
nav( $toggle_menu, [ "toggledServiceEndpoint", $FEEDBACK_TOKEN ], 1 );
my $in_library = parse_song_library_status($item);
if ($in_library) {
my $tmp = $library_remove_token;
$library_add_token = $library_remove_token;
$library_remove_token = $tmp;
}
return { "add" => $library_add_token, "remove" => $library_remove_token };
}
sub parse_like_status {
my ($service) = @_;
my @status = ( "LIKE", "INDIFFERENT" );
my $status_index = 0;
for my $i ( 0 .. $#status ) {
if ( $status[$i] eq $service->{"likeEndpoint"}{"status"} ) {
$status_index = $i;
last;
}
}
return $status[ $status_index - 1 ];
}
our @EXPORT = qw(
parse_song_artists
parse_song_artists_runs
parse_song_runs
parse_song_album
parse_song_library_status
parse_song_menu_tokens
parse_like_status
);
1;

140
YTMusicAPI/Parsers/Utils.pm Normal file
View File

@@ -0,0 +1,140 @@
package YTMusicAPI::Parsers::Utils;
use strict;
use warnings;
use Exporter 'import';
use YTMusicAPI::Navigation;
sub parse_menu_playlists {
my ( $data, $result ) = @_;
my $watch_menu = find_objects_by_key( nav( $data, \@$MENU_ITEMS ), $MNIR );
foreach my $item ( map { $_->{$MNIR} } @$watch_menu ) {
my $icon = nav( $item, \@$ICON_TYPE, 1 );
my $watch_key;
if ( $icon eq "MUSIC_SHUFFLE" ) {
$watch_key = "shuffleId";
}
elsif ( $icon eq "MIX" ) {
$watch_key = "radioId";
}
else {
next;
}
my $watch_id =
nav( $item,
[ "navigationEndpoint", "watchPlaylistEndpoint", "playlistId" ],
1 );
unless ($watch_id) {
$watch_id = nav( $item,
[ "navigationEndpoint", "watchEndpoint", "playlistId" ], 1 );
}
if ($watch_id) {
$result->{$watch_key} = $watch_id;
}
}
}
sub get_item_text {
my ( $item, $index, $run_index, $none_if_absent ) = @_;
$run_index //= 0;
$none_if_absent //= 0;
my $column = get_flex_column_item( $item, $index );
return unless $column;
if ( $none_if_absent
&& scalar( @{ $column->{text}->{runs} } ) < $run_index + 1 )
{
return;
}
return $column->{text}->{runs}->[$run_index]->{text};
}
sub get_flex_column_item {
my ( $item, $index ) = @_;
if ( scalar( @{ $item->{'flexColumns'} } ) <= $index
|| !
exists $item->{'flexColumns'}->[$index]
->{'musicResponsiveListItemFlexColumnRenderer'}->{'text'}
|| !
exists $item->{'flexColumns'}->[$index]
->{'musicResponsiveListItemFlexColumnRenderer'}->{'text'}->{'runs'} )
{
return undef;
}
return $item->{'flexColumns'}->[$index]
->{'musicResponsiveListItemFlexColumnRenderer'};
}
sub get_fixed_column_item {
my ( $item, $index ) = @_;
if ( !exists $item->{'fixedColumns'}->[$index]
->{'musicResponsiveListItemFixedColumnRenderer'}->{'text'}
|| !
exists $item->{'fixedColumns'}->[$index]
->{'musicResponsiveListItemFixedColumnRenderer'}->{'text'}->{'runs'} )
{
return undef;
}
return $item->{'fixedColumns'}->[$index]
->{'musicResponsiveListItemFixedColumnRenderer'};
}
sub get_dot_separator_index {
my ($runs) = @_;
my $index = 0;
foreach my $run (@$runs) {
last if ( $run->{"text"} eq " • " );
$index++;
}
return $index == scalar @$runs ? $index : $index;
}
sub parse_duration {
my ($duration) = @_;
return $duration unless defined $duration;
my @mapped_increments = ( [ 1, 60, 3600 ], reverse split /:/, $duration );
my $seconds = 0;
while ( my ( $multiplier, $time ) = splice( @mapped_increments, 0, 2 ) ) {
$seconds += $multiplier * $time;
}
return $seconds;
}
sub parse_id_name {
my ($sub_run) = @_;
return {
"id" => nav(
$sub_run, [ "navigationEndpoint", "browseEndpoint", "browseId" ], 1
),
"name" => nav( $sub_run, ["text"], 1 ),
};
}
our @EXPORT = qw(
parse_menu_playlists
get_item_text
get_flex_column_item
get_fixed_column_item
get_dot_separator_index
parse_duration
parse_id_name
);
1;

174
YTMusicAPI/YTMusic.pm Normal file
View File

@@ -0,0 +1,174 @@
package YTMusicAPI::YTMusic;
use strict;
use warnings;
use Moose;
with 'YTMusicAPI::Mixins::SearchMixin';
use JSON;
use LWP::UserAgent;
use HTTP::Cookies;
use URI::Escape;
use Encode qw(encode_utf8);
use Data::Dumper;
use YTMusicAPI::Constants qw(
$SUPPORTED_LANGUAGES
$SUPPORTED_LOCATIONS
USER_AGENT
YTM_BASE_API
YTM_DOMAIN
YTM_PARAMS
YTM_PARAMS_KEY
);
use YTMusicAPI::Helpers;
use YTMusicAPI::Parsers::Parser;
use YTMusicAPI::Auth::AuthTypes;
sub new {
my ( $class, $args ) = @_;
my $self = {
auth => $args->{auth} // undef,
user => $args->{user} // undef,
requests_session => $args->{requests_session} // 1,
proxies => $args->{proxies} // undef,
language => $args->{language} // 'en',
location => $args->{location} // '',
oauth_credentials => $args->{oauth_credentials} // undef,
_base_headers => undef,
_headers => undef,
_input_dict => undef,
_token => undef,
_session => undef,
_proxies => undef,
};
bless $self, $class;
$self->{auth_type} = AuthType::UNAUTHORIZED;
if ( UNIVERSAL::isa( $self->{requests_session}, "LWP::UserAgent" ) ) {
$self->{_session} = $self->{requests_session};
}
elsif ( $self->{requests_session} ) {
$self->{_session} = LWP::UserAgent->new;
}
$self->{cookies} = "SOCS=CAI";
$self->{context} = initialize_context();
$self->{context}{"context"}{"client"}{"hl"} = "en";
$self->{parser} = YTMusicAPI::Parsers::Parser->new();
if ( $self->{user} ) {
$self->{context}{'context'}{'user'}{'onBehalfOfUser'} = $self->{user};
}
$self->{params} = YTM_PARAMS;
if ( $self->{auth_type} == AuthType::BROWSER ) {
$self->{params} .= YTM_PARAMS_KEY;
}
return $self;
}
sub base_headers {
my ($self) = @_;
if ( !$self->{_base_headers} ) {
if ( $self->{auth_type} == AuthType::BROWSER
or $self->{auth_type} == AuthType::OAUTH_CUSTOM_FULL )
{
$self->{_base_headers} = $self->{_input_dict};
}
else {
$self->{_base_headers} = {
"user-agent" => USER_AGENT,
"accept" => "*/*",
"accept-encoding" => "gzip, deflate",
"content-type" => "application/json",
"content-encoding" => "gzip",
"origin" => YTM_DOMAIN,
};
}
}
return $self->{_base_headers};
}
sub headers {
my ($self) = @_;
if ( !$self->{_headers} ) {
$self->{_headers} = $self->base_headers();
}
if ( $self->{auth_type} == AuthType::BROWSER ) {
$self->{_headers}{'authorization'} =
get_authorization( $self->{sapisid} . ' ' . $self->{origin} );
}
elsif ( $self->{auth_type} != AuthType::OAUTH_CUSTOM_FULL ) {
# $self->{_headers}{'authorization'} = $self->{_token}->as_auth();
$self->{_headers}{'X-Goog-Request-Time'} = '' . time();
}
return $self->{_headers};
}
sub _send_request {
my ( $self, $url, $body, $additionalParams ) = @_;
$additionalParams //= "";
@$body{ keys %{ $self->{context} } } = values %{ $self->{context} };
if ( $self->{_headers} and !exists $self->{_headers}{'X-Goog-Visitor-Id'} )
{
my $visitor_id = get_visitor_id( $self->{_send_get_request} );
$self->{_headers}{'X-Goog-Visitor-Id'} = %$visitor_id;
}
$self->{_headers}{'Cookie'} = $self->{cookies};
my $request = HTTP::Request->new(
POST => YTM_BASE_API . $url . $self->{params} . $additionalParams );
my $headers = $self->headers();
foreach my $header_name ( keys %$headers ) {
$request->header( $header_name => $headers->{$header_name} );
}
$request->content( encode_json($body) );
my $response = $self->{_session}->request($request);
if ( !$response->is_success ) {
my $message = "Server returned HTTP " . $response->code . ".\n";
my $error = $response->message;
die $message . $error;
}
return decode_json( $response->decoded_content );
}
sub _send_get_request {
my ( $self, $url, $params ) = @_;
my $response =
$self->{_session}
->get( $url, $self->{_headers} ? $self->headers() : $self->base_headers(),
);
return $response;
}
sub _check_auth {
my ($self) = @_;
unless ( $self->{auth} ) {
die "Please provide authentication before using this function";
}
}
1;