Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication refactor #488

Merged
merged 23 commits into from
Dec 31, 2023
Merged

Authentication refactor #488

merged 23 commits into from
Dec 31, 2023

Conversation

jcbirdwell
Copy link
Contributor

@jcbirdwell jcbirdwell commented Dec 17, 2023

This is the implementation of #487. It also fixes some issues I ran across while writing the tests for my proposed feature that were preventing a full pass. I can open issues for each or all together if needed, just let me know. Otherwise, they were:

  1. A test was throwing a depreciation warning stemming from a regex search in get_album_browse_id, I changed it to use a str.find slicer, which fixed the warning and made it a touch faster.
  2. It looks like the songs section of charts is no longer provided by YouTube (could be regional), which was producing an indexing error in get_charts when authenticated. I changed it to check that the results array had enough members before it inserted the songs key. This prevents the error without depreciating the feature should the section be added back on googles end.
  3. While prepping a YouTube music account library for the tests I couldn't find the method for saving albums, so I wrote one, only to find afterwards it was written in as a footnote of the rate_playlists method. I went ahead and committed it as an alias/convenience method for other idiots like myself that couldn't find the other. Ignore it if you want.
  4. I may have just been cluelessly butchering my test.py and test.cfg trying to get unitests up and running, but there were a few variables that seemed to be library or region dependent, which I externalized to the cfg so as to be more clearly customizable. If that was a bad call and the purpose of the tests went over my head, let me know so I can redo and rerun with the other variables.

Copy link
Owner

@sigma67 sigma67 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jcbirdwell , thanks so much for contributing this back. Glad to accept this contribution with a few changes.

Please also add documentation in the Setup section (new chapter in Oauth section). How could one obtain such custom credentials?

Requesting a review from @MarvinSchenkel who contributed #396

I will wait for these changes before running the tests myself, I assume you have done so on your machine

ytmusicapi/mixins/browsing.py Outdated Show resolved Hide resolved
has_genres = country == 'US'
has_trending = country != 'ZZ'
# songs section appears to no longer exist, extra length check avoids
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the previous check was too simple since you'd actually need to check for a premium subscriber instead of a just a logged-in user. Should probably add a separate account to the tests that is authenticated but not subscribed

So I think we can just remove the bool(self.auth) part altogether since it does not actually make a difference

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was indeed performing all tests using non-premium accounts.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can remove bool(self.auth) and ?

ytmusicapi/mixins/library.py Outdated Show resolved Hide resolved
ytmusicapi/ytmusic.py Outdated Show resolved Hide resolved
ytmusicapi/ytmusic.py Outdated Show resolved Hide resolved
ytmusicapi/auth/headers.py Outdated Show resolved Hide resolved
ytmusicapi/ytmusic.py Outdated Show resolved Hide resolved
@heisen273
Copy link

Tested this PR and I think all requests through mixin's are broken._.

at first, __init__() goes OK and i can obtain refresh token with my custom client id & secret pair:

Screenshot 2023-12-17 at 23 39 12

But then, when i do any request(.get_me(), .get_playlist(), etc) - it always returns 401:

Screenshot 2023-12-17 at 23 39 31

I think you have to update _send_request() with logics that handles if self.alt_oauth: ...

ytmusicapi/auth/oauth.py Outdated Show resolved Hide resolved
@jcbirdwell
Copy link
Contributor Author

jcbirdwell commented Dec 19, 2023

I ended up going with a mix of my original issue solution alternative 2 and the dataclass implementation mentioned previously. While I was adding a credential dataclass I was reminded of why I was originally trying to pass the YTMusicOAuth instance from YTMusic, it was to prevent recreating the OAuth class on every single request. I believe the original intent was to ensure that the authorization field of the header was always up-to-date, this version accomplishes that but instead of rebuilding all the headers each time it just does authorization/ones that update.

It also catches #492 in the crossfire and I believe the issue @heisen273 was having with mixins, but I couldn't replicate it so I'm unsure. It's a decent chunk of changes, but should act as a drop-in replacement.

I added custom oauth handling and believe it maintains compatibility with #396, but I don't think there is a test for it.

I haven't written any of the docs or additional feature tests yet as I wanted to get feedback before tackling em, but I'm getting a full pass for existing tests. In light of that, I'd convert the pull request to a draft, but I'm not positive what that does and don't want to mess anything up.

Copy link
Owner

@sigma67 sigma67 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this second changeset was needed based on what you wanted to achieve. But I like the improvements in general, so let's try to make it work.

I have a very hard time verifying that everything is still working, considering that 18 tests are breaking for me locally even after updating the test.cfg (those changes are quite welcome, thanks).

I suspect most failures have the same origin, some sort of (breaking?) change to the local oauth.json file that I have on my machine. I have attached my test log, maybe you can point out what's wrong.
tests.log

I will also state here that this MR should be free of any breaking changes to the public facing API.

Don't worry about draft mode, it has no effect in this repo (since we're currently not running tests on PRs due to somewhat sensitive nature of the credentials)

ytmusicapi/setup.py Outdated Show resolved Hide resolved
Copy link
Owner

@sigma67 sigma67 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message has changed slightly, but I'm still getting this:

======================================================================
ERROR: test_search_library (test.TestYTMusic.test_search_library)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "\ytmusicapi\tests\test.py", line 164, in test_search_library
    results = self.yt_oauth.search(config['queries']['library_any'], scope="library")
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\mixins\search.py", line 165, in search
    response = self._send_request(endpoint, body)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\ytmusic.py", line 200, in _send_request
    if 'X-Goog-Visitor-Id' not in self.headers:
                                  ^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\ytmusic.py", line 191, in headers
    self._headers['authorization'] = self._token.as_auth()
                                     ^^^^^^^^^^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\auth\oauth.py", line 85, in as_auth
    return f'{self.token_type} {self.access_token}'
                                ^^^^^^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\auth\oauth.py", line 178, in access_token
    self.token = self.credentials.refresh_token(self.token.refresh_token)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\auth\oauth.py", line 141, in refresh_token
    return OAuthToken.from_response(response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ytmusicapi\ytmusicapi\auth\oauth.py", line 30, in from_response
    return cls(**data)
           ^^^^^^^^^^^
TypeError: OAuthToken.__init__() missing 1 required positional argument: 'refresh_token'

ytmusicapi/auth/oauth.py Outdated Show resolved Hide resolved
ytmusicapi/auth/oauth.py Outdated Show resolved Hide resolved
Copy link
Owner

@sigma67 sigma67 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for all the comments, I'd consider this to be the final review hopefully.

Tests are passing for me.

This one's come a long way and I think it's in a great state now.

Really need to add a proper linter like ruff and some sort of basic no-oauth PR pipeline...

tests/test.py Outdated Show resolved Hide resolved
ytmusicapi/ytmusic.py Outdated Show resolved Hide resolved
ytmusicapi/auth/oauth/credentials.py Show resolved Hide resolved
ytmusicapi/auth/oauth/refreshing.py Show resolved Hide resolved
has_genres = country == 'US'
has_trending = country != 'ZZ'
# songs section appears to no longer exist, extra length check avoids
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can remove bool(self.auth) and ?

ytmusicapi/auth/oauth/models.py Outdated Show resolved Hide resolved
ytmusicapi/auth/oauth/refreshing.py Outdated Show resolved Hide resolved
ytmusicapi/auth/oauth/base.py Outdated Show resolved Hide resolved

@property
def token_type(self):
return self.token.token_type
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l.51 through l. 73 can be removed as they are not used anywhere, unless I'm missing something

Perhaps a leftover from the refactor

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has not been resolved

@@ -16,7 +17,7 @@
from ytmusicapi.mixins.library import LibraryMixin
from ytmusicapi.mixins.playlists import PlaylistsMixin
from ytmusicapi.mixins.uploads import UploadsMixin
from ytmusicapi.auth.oauth import YTMusicOAuth, is_oauth
from ytmusicapi.auth.oauth import OAuthCredentials, is_oauth, RefreshingToken, OAuthToken


class YTMusic(BrowsingMixin, SearchMixin, WatchMixin, ExploreMixin, LibraryMixin, PlaylistsMixin,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this line, but please add/update documentation in docs/source/setup

@jcbirdwell
Copy link
Contributor Author

Sorry for all the comments, I'd consider this to be the final review hopefully.

I really appreciate all the continued feedback on this PR and I'm satisfied with the direction we indeed up taking as a result. I'll be busy for the next few days, but it shouldn't take long to wrap up the final stuff when I can get back on it.

Copy link
Owner

@sigma67 sigma67 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for these changes again. It would be great if you could turn all the comments into proper docstrings (most of the comments I added are just todo items regarding this):

https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoproperty

See the class example here for properly documenting instance variables.

You can use either style - just add a colon to the current comments or place them in the line above with #: (the latter is my preferred style)

Other than that there is only one finding left regarding a try-catch you added

tests/test.py Show resolved Hide resolved
ytmusicapi/ytmusic.py Outdated Show resolved Hide resolved
ytmusicapi/ytmusic.py Outdated Show resolved Hide resolved
ytmusicapi/auth/oauth/refreshing.py Outdated Show resolved Hide resolved
ytmusicapi/auth/oauth/models.py Show resolved Hide resolved
ytmusicapi/auth/oauth/credentials.py Show resolved Hide resolved
@sigma67 sigma67 changed the title Alt auth Authentication refactor Dec 31, 2023
@sigma67 sigma67 merged commit 48e95ad into sigma67:master Dec 31, 2023
1 check failed
@sigma67
Copy link
Owner

sigma67 commented Dec 31, 2023

Hi @jcbirdwell , I fixed the remaining issues & merged. Feel free to add the documentation in a separate PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants