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;