Add OAuth
This commit is contained in:
@@ -6,16 +6,27 @@ use warnings;
|
||||
use Data::Dumper;
|
||||
|
||||
use YTMusicAPI::YTMusic;
|
||||
use YTMusicAPI::Auth::OAuth::OAuthCredentials;
|
||||
|
||||
my $yt = YTMusicAPI::YTMusic->new();
|
||||
# my $c = YTMusicAPI::Auth::OAuth::OAuthCredentials->new();
|
||||
|
||||
# my $code = $c->get_code();
|
||||
|
||||
# print Dumper($code);
|
||||
|
||||
my $yt = YTMusicAPI::YTMusic->new('Tests/oauth.json');
|
||||
|
||||
# my $search_results = $yt->search('Oasis Wonderwall');
|
||||
my $search_results = $yt->get_liked_songs(2);
|
||||
|
||||
# print Dumper($search_results);
|
||||
|
||||
foreach my $track ( @{ $search_results->{"tracks"} } ) {
|
||||
my $liked_songs = $yt->get_liked_songs(2);
|
||||
|
||||
foreach my $track ( @{ $liked_songs->{"tracks"} } ) {
|
||||
print $track->{"title"} . "\n";
|
||||
}
|
||||
|
||||
# my $song = $yt->get_song("CF1rt_9pdgU");
|
||||
# print Dumper($song);
|
||||
|
||||
1;
|
||||
|
||||
109
YTMusicAPI/Auth/OAuth/OAuthCredentials.pm
Normal file
109
YTMusicAPI/Auth/OAuth/OAuthCredentials.pm
Normal file
@@ -0,0 +1,109 @@
|
||||
package YTMusicAPI::Auth::OAuth::OAuthCredentials;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use JSON;
|
||||
use LWP::UserAgent;
|
||||
use Data::Dumper;
|
||||
|
||||
use YTMusicAPI::Constants qw(
|
||||
OAUTH_CLIENT_ID
|
||||
OAUTH_CLIENT_SECRET
|
||||
OAUTH_CODE_URL
|
||||
OAUTH_SCOPE
|
||||
OAUTH_TOKEN_URL
|
||||
OAUTH_USER_AGENT
|
||||
);
|
||||
|
||||
sub new {
|
||||
my ( $class, $args ) = @_;
|
||||
|
||||
unless ( defined $args->{client_id} == defined $args->{client_secret} ) {
|
||||
die "OAuthCredential init failure. "
|
||||
. "Provide both client_id and client_secret or neither.";
|
||||
}
|
||||
|
||||
my $self = {};
|
||||
bless $self, $class;
|
||||
|
||||
$self->{client_id} =
|
||||
$args->{client_id} ? $args->{client_id} : OAUTH_CLIENT_ID;
|
||||
$self->{client_secret} =
|
||||
$args->{client_secret} ? $args->{client_secret} : OAUTH_CLIENT_SECRET;
|
||||
|
||||
$self->{_session} =
|
||||
$args->{session} ? $args->{session} : LWP::UserAgent->new;
|
||||
|
||||
if ( $args->{proxies} ) {
|
||||
@$self->{_session}{ keys %{ $args->{proxies} } } =
|
||||
values %{ $args->{proxies} };
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub get_code {
|
||||
my ($self) = @_;
|
||||
my $code_response =
|
||||
$self->_send_request( OAUTH_CODE_URL, { "scope" => OAUTH_SCOPE } );
|
||||
return decode_json( $code_response->decoded_content );
|
||||
}
|
||||
|
||||
sub _send_request {
|
||||
my ( $self, $url, $data ) = @_;
|
||||
|
||||
$data->{"client_id"} = $self->{client_id};
|
||||
my $request = HTTP::Request->new( POST => $url );
|
||||
$request->header( "User-Agent" => OAUTH_USER_AGENT );
|
||||
$request->content( encode_json($data) );
|
||||
my $response = $self->{_session}->request($request);
|
||||
|
||||
if ( $response->code == 401 ) {
|
||||
my $data = $response->decode_json;
|
||||
my $issue = $data->{"error"};
|
||||
if ( $issue eq "unauthorized_client" ) {
|
||||
die "Token refresh error. Most likely client/token mismatch.";
|
||||
}
|
||||
elsif ( $issue eq "invalid_client" ) {
|
||||
die "OAuth client failure. Most likely client_id and client_secret "
|
||||
. "mismatch or YouTubeData API is not enabled.";
|
||||
}
|
||||
else {
|
||||
die "OAuth request error. status_code: "
|
||||
. $response->code
|
||||
. ", url: $url, content: "
|
||||
. Dumper($data);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
sub token_from_code {
|
||||
my ( $self, $device_code ) = @_;
|
||||
my $response = $self->_send_request(
|
||||
OAUTH_TOKEN_URL,
|
||||
{
|
||||
"client_secret" => $self->{client_secret},
|
||||
"grant_type" => "http://oauth.net/grant_type/device/1.0",
|
||||
"code" => $device_code,
|
||||
}
|
||||
);
|
||||
return decode_json( $response->decoded_content );
|
||||
}
|
||||
|
||||
sub refresh_token {
|
||||
my ( $self, $refresh_token ) = @_;
|
||||
my $response = $self->_send_request(
|
||||
OAUTH_TOKEN_URL,
|
||||
{
|
||||
"client_secret" => $self->{client_secret},
|
||||
"grant_type" => "refresh_token",
|
||||
"refresh_token" => $refresh_token,
|
||||
}
|
||||
);
|
||||
return decode_json( $response->decoded_content );
|
||||
}
|
||||
|
||||
1;
|
||||
51
YTMusicAPI/Auth/OAuth/OAuthToken.pm
Normal file
51
YTMusicAPI/Auth/OAuth/OAuthToken.pm
Normal file
@@ -0,0 +1,51 @@
|
||||
package YTMusicAPI::Auth::OAuth::OAuthToken;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use JSON qw(decode_json);
|
||||
use Path::Tiny qw(path);
|
||||
|
||||
use parent 'YTMusicAPI::Auth::OAuth::Token';
|
||||
|
||||
sub new {
|
||||
my ( $class, $args ) = @_;
|
||||
my $self = $class->SUPER::new($args);
|
||||
bless $self, $class;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub is_oauth {
|
||||
my ($headers) = @_;
|
||||
my @required_keys = YTMusicAPI::Auth::OAuth::Token::members();
|
||||
|
||||
foreach my $key (@required_keys) {
|
||||
return 0 unless exists $headers->{$key};
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub update {
|
||||
my ( $self, $fresh_access ) = @_;
|
||||
$self->{access_token} = $fresh_access->{access_token};
|
||||
$self->{expires_at} = int( time() ) + $fresh_access->{expires_in};
|
||||
}
|
||||
|
||||
sub is_expiring {
|
||||
my ($self) = @_;
|
||||
return $self->{expires_at} - int( time() ) < 60 ? 1 : 0;
|
||||
}
|
||||
|
||||
sub from_json {
|
||||
my ( $class, $file_path ) = @_;
|
||||
my $file_pack;
|
||||
|
||||
if ( path($file_path)->is_file ) {
|
||||
$file_pack = decode_json( path($file_path)->slurp_utf8 );
|
||||
}
|
||||
|
||||
return $class->new(%$file_pack);
|
||||
}
|
||||
|
||||
1;
|
||||
80
YTMusicAPI/Auth/OAuth/RefreshingToken.pm
Normal file
80
YTMusicAPI/Auth/OAuth/RefreshingToken.pm
Normal file
@@ -0,0 +1,80 @@
|
||||
package YTMusicAPI::Auth::OAuth::RefreshingToken;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use parent 'YTMusicAPI::Auth::OAuth::OAuthToken';
|
||||
use JSON qw(encode_json);
|
||||
use Path::Tiny qw(path);
|
||||
|
||||
sub new {
|
||||
my ( $class, $credentials, $local_cache, $args ) = @_;
|
||||
my $self = $class->SUPER::new($args);
|
||||
$self->{credentials} = $credentials;
|
||||
$self->{_local_cache} = $local_cache;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub get_attribute {
|
||||
my ( $self, $item ) = @_;
|
||||
if ( $item eq "access_token" && $self->is_expiring ) {
|
||||
my $fresh =
|
||||
$self->{credentials}->refresh_token( $self->{refresh_token} );
|
||||
$self->update($fresh);
|
||||
$self->store_token();
|
||||
}
|
||||
return $self->{$item};
|
||||
}
|
||||
|
||||
sub local_cache {
|
||||
my $self = shift;
|
||||
if (@_) {
|
||||
$self->{_local_cache} = shift;
|
||||
$self->store_token();
|
||||
}
|
||||
return $self->{_local_cache};
|
||||
}
|
||||
|
||||
sub prompt_for_token {
|
||||
my ( $class, $credentials, $open_browser, $to_file ) = @_;
|
||||
$open_browser //= 0;
|
||||
|
||||
my $code = $credentials->get_code();
|
||||
my $url = "$code->{verification_url}?user_code=$code->{user_code}";
|
||||
if ($open_browser) {
|
||||
if ( $^O eq 'MSWin32' ) {
|
||||
system("start $url");
|
||||
}
|
||||
elsif ( $^O eq 'darwin' ) {
|
||||
system("open $url");
|
||||
}
|
||||
elsif ( $^O eq 'linux' ) {
|
||||
system("xdg-open $url");
|
||||
}
|
||||
else {
|
||||
warn "Unable to open url. Unsupported OS";
|
||||
}
|
||||
}
|
||||
print
|
||||
"Go to $url, finish the login flow and press Enter when done, Ctrl-C to abort\n";
|
||||
<STDIN>;
|
||||
my $raw_token = $credentials->token_from_code( $code->{device_code} );
|
||||
|
||||
my $ref_token = $class->new( %$raw_token, credentials => $credentials );
|
||||
$ref_token->update( $ref_token->as_dict() );
|
||||
if ($to_file) {
|
||||
$ref_token->local_cache($to_file);
|
||||
}
|
||||
return $ref_token;
|
||||
}
|
||||
|
||||
sub store_token {
|
||||
my ( $self, $path ) = @_;
|
||||
$path //= $self->local_cache();
|
||||
|
||||
if ($path) {
|
||||
path($path)->spew_utf8( encode_json( $self->as_dict() ) );
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
61
YTMusicAPI/Auth/OAuth/Token.pm
Normal file
61
YTMusicAPI/Auth/OAuth/Token.pm
Normal file
@@ -0,0 +1,61 @@
|
||||
package YTMusicAPI::Auth::OAuth::Token;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use JSON;
|
||||
|
||||
sub new {
|
||||
my ( $class, $args ) = @_;
|
||||
my $self = {
|
||||
scope => $args->{scope} // "https://www.googleapis.com/auth/youtube",
|
||||
token_type => $args->{token_type} // "Bearer",
|
||||
access_token => $args->{access_token} // undef,
|
||||
refresh_token => $args->{refresh_token} // undef,
|
||||
expires_at => $args->{expires_at} // 0,
|
||||
expires_in => $args->{expires_in} // 0,
|
||||
};
|
||||
bless $self, $class;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub members {
|
||||
return qw(
|
||||
scope
|
||||
token_type
|
||||
access_token
|
||||
refresh_token
|
||||
expires_at
|
||||
expires_in
|
||||
);
|
||||
}
|
||||
|
||||
sub repr {
|
||||
my ($self) = @_;
|
||||
return ref($self) . ": " . $self->as_json();
|
||||
}
|
||||
|
||||
sub as_dict {
|
||||
my ($self) = @_;
|
||||
my %copy;
|
||||
@copy{ $self->members() } = @{$self}{ members() };
|
||||
return \%copy;
|
||||
}
|
||||
|
||||
sub as_json {
|
||||
my ($self) = @_;
|
||||
my $dict = $self->as_dict();
|
||||
return encode_json($dict);
|
||||
}
|
||||
|
||||
sub as_auth {
|
||||
my ($self) = @_;
|
||||
return $self->{token_type} . " " . $self->{access_token};
|
||||
}
|
||||
|
||||
sub is_expiring {
|
||||
my ($self) = @_;
|
||||
return $self->{expires_in} < 60 ? 1 : 0;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -15,6 +15,7 @@ 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(
|
||||
@@ -30,45 +31,104 @@ use YTMusicAPI::Constants qw(
|
||||
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, $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,
|
||||
};
|
||||
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;
|
||||
|
||||
if ( UNIVERSAL::isa( $self->{requests_session}, "LWP::UserAgent" ) ) {
|
||||
$self->{_session} = $self->{requests_session};
|
||||
$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 ( $self->{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 ( $self->{user} ) {
|
||||
$self->{context}{'context'}{'user'}{'onBehalfOfUser'} = $self->{user};
|
||||
if ($user) {
|
||||
$self->{context}{'context'}{'user'}{'onBehalfOfUser'} = $user;
|
||||
}
|
||||
|
||||
my $auth_headers = $self->{_input_dict}->{"authorization"};
|
||||
if ($auth_headers) {
|
||||
|
||||
}
|
||||
|
||||
$self->{params} = YTM_PARAMS;
|
||||
@@ -115,8 +175,7 @@ sub headers {
|
||||
get_authorization( $self->{sapisid} . ' ' . $self->{origin} );
|
||||
}
|
||||
elsif ( $self->{auth_type} != AuthType::OAUTH_CUSTOM_FULL ) {
|
||||
|
||||
# $self->{_headers}{'authorization'} = $self->{_token}->as_auth();
|
||||
$self->{_headers}{'authorization'} = $self->{_token}->as_auth();
|
||||
$self->{_headers}{'X-Goog-Request-Time'} = '' . time();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user