Skip to content

Commit

Permalink
refactor authtypes
Browse files Browse the repository at this point in the history
  • Loading branch information
sigma67 committed Dec 31, 2023
1 parent 3abbbf8 commit c848e50
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 36 deletions.
9 changes: 5 additions & 4 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from requests import Response

from ytmusicapi.auth.types import AuthType
from ytmusicapi.setup import main, setup # noqa: E402
from ytmusicapi.ytmusic import YTMusic, OAuthCredentials # noqa: E402
from ytmusicapi.constants import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET
Expand Down Expand Up @@ -82,7 +83,7 @@ def test_setup_oauth(self, session_mock, json_mock):
# OAUTH
###############
# 000 so test is run first and fresh token is available to others
def test_000_oauth_tokens(self):
def test_oauth_tokens(self):
# ensure instance initialized token
self.assertIsNotNone(self.yt_oauth._token)

Expand Down Expand Up @@ -115,14 +116,14 @@ def test_000_oauth_tokens(self):
# ensure token is updating local file
self.assertNotEqual(first_json, second_json)

def test_alt_oauth(self):
def test_oauth_custom_client(self):
# ensure client works/ignores alt if browser credentials passed as auth
self.assertFalse(self.yt_alt_oauth.is_alt_oauth)
self.assertNotEqual(self.yt_alt_oauth.auth_type, AuthType.OAUTH_CUSTOM_CLIENT)
with open(oauth_filepath, 'r') as f:
token_dict = json.load(f)
# oauth token dict entry and alt
self.yt_alt_oauth = YTMusic(token_dict, oauth_credentials=alt_oauth_creds)
self.assertTrue(self.yt_alt_oauth.is_alt_oauth)
self.assertEqual(self.yt_alt_oauth.auth_type, AuthType.OAUTH_CUSTOM_CLIENT)

###############
# BROWSING
Expand Down
12 changes: 0 additions & 12 deletions ytmusicapi/auth/headers.py

This file was deleted.

25 changes: 25 additions & 0 deletions ytmusicapi/auth/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""enum representing types of authentication supported by this library"""

from enum import Enum, auto
from typing import List


class AuthType(int, Enum):
"""enum representing types of authentication supported by this library"""

UNAUTHORIZED = auto()

BROWSER = auto()

#: client auth via OAuth token refreshing
OAUTH_DEFAULT = auto()

#: YTM instance is using a non-default OAuth client (id & secret)
OAUTH_CUSTOM_CLIENT = auto()

#: allows fully formed OAuth headers to ignore browser auth refresh flow
OAUTH_CUSTOM_FULL = auto()

@classmethod
def oauth_types(cls) -> List["AuthType"]:
return [cls.OAUTH_DEFAULT, cls.OAUTH_CUSTOM_CLIENT, cls.OAUTH_CUSTOM_FULL]
3 changes: 2 additions & 1 deletion ytmusicapi/mixins/uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ytmusicapi.parsers.library import parse_library_albums, parse_library_artists, get_library_contents
from ytmusicapi.parsers.albums import parse_album_header
from ytmusicapi.parsers.uploads import parse_uploaded_items
from ..auth.types import AuthType


class UploadsMixin:
Expand Down Expand Up @@ -194,7 +195,7 @@ def upload_song(self, filepath: str) -> Union[str, requests.Response]:
:return: Status String or full response
"""
self._check_auth()
if not self.is_browser_auth:
if not self.auth_type == AuthType.BROWSER:
raise Exception("Please provide authentication before using this function")
if not os.path.isfile(filepath):
raise Exception("The provided file does not exist.")
Expand Down
37 changes: 18 additions & 19 deletions ytmusicapi/ytmusic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
from ytmusicapi.mixins.playlists import PlaylistsMixin
from ytmusicapi.mixins.uploads import UploadsMixin

from .auth.headers import load_headers_file
from .auth.oauth import OAuthCredentials, RefreshingToken, OAuthToken
from .auth.oauth.base import Token
from .auth.types import AuthType


class YTMusic(BrowsingMixin, SearchMixin, WatchMixin, ExploreMixin, LibraryMixin, PlaylistsMixin,
Expand Down Expand Up @@ -82,11 +82,7 @@ def __init__(self,
self.auth = auth #: raw auth
self._input_dict = {} #: parsed auth arg value in dictionary format

# (?) may be better implemented as an auth_type attribute with a literal/enum value (?)
self.is_alt_oauth = False #: YTM instance is using a non-default OAuth client (id & secret)
self.is_oauth_auth = False #: client auth via OAuth token refreshing
self.is_browser_auth = False #: authorization via extracted browser headers, enables uploading capabilities
self.is_custom_oauth = False #: allows fully formed OAuth headers to ignore browser auth refresh flow
self.auth_type: AuthType = AuthType.UNAUTHORIZED

self._token: Token #: OAuth credential handler
self.oauth_credentials: OAuthCredentials #: Client used for OAuth refreshing
Expand All @@ -103,25 +99,28 @@ def __init__(self,
else: # Use the Requests API module as a "session".
self._session = requests.api

self.oauth_credentials = oauth_credentials if oauth_credentials is not None else OAuthCredentials()

# 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 is not None:
self.oauth_credentials = oauth_credentials if oauth_credentials is not None else OAuthCredentials()
auth_filepath = None
if isinstance(self.auth, str):
input_json = load_headers_file(self.auth)
if os.path.isfile(auth):
with open(auth) as json_file:
auth_filepath = auth
input_json = json.load(json_file)
else:
input_json = json.loads(auth)
self._input_dict = CaseInsensitiveDict(input_json)

else:
self._input_dict = self.auth

if OAuthToken.is_oauth(self._input_dict):
base_token = OAuthToken(**self._input_dict)
self._token = RefreshingToken(base_token, self.oauth_credentials,
self._input_dict.get('filepath'))
self.is_oauth_auth = True
self.is_alt_oauth = oauth_credentials is not None
self._token = RefreshingToken(base_token, self.oauth_credentials, auth_filepath)
self.auth_type = AuthType.OAUTH_CUSTOM_CLIENT if oauth_credentials else AuthType.OAUTH_DEFAULT

# prepare context
self.context = initialize_context()
Expand Down Expand Up @@ -152,13 +151,13 @@ def __init__(self,
auth_headers = self._input_dict.get("authorization")
if auth_headers:
if "SAPISIDHASH" in auth_headers:
self.is_browser_auth = True
self.auth_type = AuthType.BROWSER
elif auth_headers.startswith('Bearer'):
self.is_custom_oauth = True
self.auth_type = AuthType.OAUTH_CUSTOM_FULL

# sapsid, origin, and params all set once during init
self.params = YTM_PARAMS
if self.is_browser_auth:
if self.auth_type == AuthType.BROWSER:
self.params += YTM_PARAMS_KEY
try:
cookie = self.base_headers.get('cookie')
Expand All @@ -170,7 +169,7 @@ def __init__(self,
@property
def base_headers(self):
if not self._base_headers:
if self.is_browser_auth or self.is_custom_oauth:
if self.auth_type == AuthType.BROWSER or self.auth_type == AuthType.OAUTH_CUSTOM_FULL:
self._base_headers = self._input_dict
else:
self._base_headers = {
Expand All @@ -191,10 +190,10 @@ def headers(self):
self._headers = self.base_headers

# keys updated each use, custom oauth implementations left untouched
if self.is_browser_auth:
if self.auth_type == AuthType.BROWSER:
self._headers["authorization"] = get_authorization(self.sapisid + ' ' + self.origin)

elif self.is_oauth_auth:
elif self.auth_type in AuthType.oauth_types():
self._headers['authorization'] = self._token.as_auth()
self._headers['X-Goog-Request-Time'] = str(int(time.time()))

Expand Down

0 comments on commit c848e50

Please sign in to comment.