244 lines
6.8 KiB
Perl
244 lines
6.8 KiB
Perl
package YTMusicAPI::YTMusic;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Moose;
|
|
|
|
with
|
|
'YTMusicAPI::Mixins::BrowsingMixin',
|
|
'YTMusicAPI::Mixins::SearchMixin',
|
|
'YTMusicAPI::Mixins::PlaylistMixin';
|
|
|
|
use JSON;
|
|
use LWP::UserAgent;
|
|
use HTTP::Cookies;
|
|
use URI::Escape;
|
|
use Encode qw(encode_utf8);
|
|
use Path::Tiny qw(path);
|
|
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;
|
|
use YTMusicAPI::Auth::OAuth::OAuthCredentials;
|
|
use YTMusicAPI::Auth::OAuth::OAuthToken;
|
|
use YTMusicAPI::Auth::OAuth::RefreshingToken;
|
|
|
|
sub new {
|
|
my ( $class, $auth, $user, $requests_session, $proxies, $language,
|
|
$location, $oauth_credentials )
|
|
= @_;
|
|
|
|
$requests_session //= 1;
|
|
$language //= 'en';
|
|
$location //= '';
|
|
|
|
my $self = {};
|
|
bless $self, $class;
|
|
|
|
$self->{_base_headers} =
|
|
undef; #: for authless initializing requests during OAuth flow
|
|
$self->{_headers} = undef; #: cache formed headers including auth
|
|
|
|
$self->{auth} = $auth; #: raw auth
|
|
$self->{_input_dict} = undef; #: parsed auth arg value in dictionary format
|
|
|
|
$self->{auth_type} = AuthType::UNAUTHORIZED;
|
|
|
|
$self->{_token} = undef; #: OAuth credential handler
|
|
$self->{oauth_credentials} = undef; #: Client used for OAuth refreshing
|
|
|
|
$self->{_session} = undef; #: request session for connection pooling
|
|
$self->{proxies} = $proxies; #: params for session modification
|
|
|
|
if ( UNIVERSAL::isa( $requests_session, "LWP::UserAgent" ) ) {
|
|
$self->{_session} = $requests_session;
|
|
}
|
|
elsif ($requests_session) { # Build a new session.
|
|
$self->{_session} = LWP::UserAgent->new;
|
|
}
|
|
|
|
# see google cookie docs: https://policies.google.com/technologies/cookies
|
|
# value from https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L502
|
|
$self->{cookies} = "SOCS=CAI";
|
|
|
|
if ( $self->{auth} ) {
|
|
$self->{oauth_credentials} =
|
|
$oauth_credentials
|
|
? $oauth_credentials
|
|
: YTMusicAPI::Auth::OAuth::OAuthCredentials->new();
|
|
my $auth_filepath = undef;
|
|
if ( !ref $self->{auth} ) {
|
|
my $auth_str = $self->{auth};
|
|
my $input_json;
|
|
if ( path($auth_str)->is_file() ) {
|
|
open( my $fh, '<', $auth_str );
|
|
my $json_file = do { local $/; <$fh> };
|
|
close($fh);
|
|
$auth_filepath = $auth_str;
|
|
$input_json = decode_json($json_file);
|
|
}
|
|
else {
|
|
$input_json = decode_json($auth_str);
|
|
}
|
|
$self->{_input_dict} = $input_json;
|
|
}
|
|
else {
|
|
$self->{_input_dict} = $self->{auth};
|
|
}
|
|
|
|
if (
|
|
YTMusicAPI::Auth::OAuth::OAuthToken::is_oauth(
|
|
$self->{_input_dict}
|
|
)
|
|
)
|
|
{
|
|
$self->{_token} = YTMusicAPI::Auth::OAuth::RefreshingToken->new(
|
|
$self->{oauth_credentials},
|
|
$auth_filepath, $self->{_input_dict} );
|
|
$self->{auth_type} =
|
|
$oauth_credentials
|
|
? AuthType::OAUTH_CUSTOM_CLIENT
|
|
: AuthType::OAUTH_DEFAULT;
|
|
}
|
|
}
|
|
|
|
# prepare context
|
|
$self->{context} = initialize_context();
|
|
|
|
$self->{context}{"context"}{"client"}{"hl"} = "en";
|
|
$self->{language} = $language;
|
|
|
|
$self->{parser} = YTMusicAPI::Parsers::Parser->new();
|
|
|
|
if ($user) {
|
|
$self->{context}{'context'}{'user'}{'onBehalfOfUser'} = $user;
|
|
}
|
|
|
|
my $auth_headers = $self->{_input_dict}->{"authorization"};
|
|
if ($auth_headers) {
|
|
|
|
}
|
|
|
|
$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} };
|
|
my $headers = $self->headers();
|
|
|
|
if ( $self->{_headers} and !exists $self->{_headers}{'X-Goog-Visitor-Id'} )
|
|
{
|
|
my $visitor_id = get_visitor_id( $self->_send_get_request(YTM_DOMAIN) );
|
|
$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 );
|
|
|
|
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 ) = @_;
|
|
$params //= "";
|
|
|
|
my $request = HTTP::Request->new( GET => $url . $params );
|
|
my $headers = $self->{_headers} ? $self->headers() : $self->base_headers();
|
|
$self->{_headers}{'Cookie'} = $self->{cookies};
|
|
|
|
foreach my $header_name ( keys %$headers ) {
|
|
$request->header( $header_name => $headers->{$header_name} );
|
|
}
|
|
|
|
my $response = $self->{_session}->request($request);
|
|
|
|
return $response;
|
|
}
|
|
|
|
sub _check_auth {
|
|
my ($self) = @_;
|
|
unless ( $self->{auth} ) {
|
|
die "Please provide authentication before using this function";
|
|
}
|
|
}
|
|
|
|
1;
|