Skip to content

fix browser setup for Chrome (#857)#865

Open
apastel wants to merge 5 commits intosigma67:mainfrom
apastel:fix-chrome-auth-857
Open

fix browser setup for Chrome (#857)#865
apastel wants to merge 5 commits intosigma67:mainfrom
apastel:fix-chrome-auth-857

Conversation

@apastel
Copy link
Copy Markdown
Contributor

@apastel apastel commented Jan 31, 2026

Fixes #857

Supports all three of the following formats for pasting headers:

  1. Firefox
  2. Chrome (new format without colons)
  3. Chrome (old format with colons on header keys)

Had to add a corner case to handled the odd Decoded: header in Chrome, which looks like the following:

Decoded:
message ClientVariations {
  // Active Google-visible variation IDs on this client. These are reported for analysis, but do not directly affect any server-side behavior.
  repeated int32 variation_id = [3300102, 3300132, 3313321, 3323055, 3330197, 3362821, 3393046, 3393674, 3395745, 3396053, 3396249, 3396352, 3396365, 3396446, 3396569, 3396819, 3396954, 3397042, 3397046];
  // Active Google-visible variation IDs on this client that trigger server-side behavior. These are reported for analysis *and* directly affect server-side behavior.
  repeated int32 trigger_variation_id = [3392236, 3396349];
}

It's the only one that still has a colon on the header key so it had to be handled differently. The end result in the parsed user_headers is the same as it was before, where the property ends up being Decoded: "}"

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 31, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
122 1 121 3
View the top 1 failed test(s) by shortest run time
tests.mixins.test_playlists.TestPlaylists::test_get_playlist_unavailable
Stack Traces | 1.22s run time
root = {'microformat': {'microformatDataRenderer': {'noindex': True}}, 'responseContext': {'serviceTrackingParams': [{'params...ient.name', 'value': 'WEB_REMIX'}], 'service': 'ECATCHER'}]}, 'trackingParams': 'CAAQhGciEwi65YCZ6LaSAxVH2OcDHYYVMKQ='}
items = ['contents', 'twoColumnBrowseResultsRenderer', 'secondaryContents', 'sectionListRenderer']
none_if_absent = False

    def nav(root: JsonDict | None, items: list[Any], none_if_absent: bool = False) -> Any | None:
        """Access a nested object in root by item sequence."""
        if root is None:
            return None
        try:
            for k in items:
>               root = root[k]
                       ^^^^^^^
E               KeyError: 'contents'

ytmusicapi/navigation.py:122: KeyError

During handling of the above exception, another exception occurred:

self = <tests.mixins.test_playlists.TestPlaylists object at 0x7fc1c8dfb6c0>
yt = <ytmusicapi.ytmusic.YTMusic object at 0x7fc1c7936c90>

    def test_get_playlist_unavailable(self, yt):
        playlist_id = "OLAK5uy_mnDXXKVcY1Z_HKY00a_ZBrnE679EzsM50"
>       playlist = yt.get_playlist(playlist_id)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/mixins/test_playlists.py:102: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
ytmusicapi/mixins/playlists.py:142: in get_playlist
    return parse_audio_playlist(response, limit, request_func_continuations)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ytmusicapi/parsers/playlists.py:109: in parse_audio_playlist
    section_list = nav(response, [*TWO_COLUMN_RENDERER, "secondaryContents", *SECTION])
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

root = {'microformat': {'microformatDataRenderer': {'noindex': True}}, 'responseContext': {'serviceTrackingParams': [{'params...ient.name', 'value': 'WEB_REMIX'}], 'service': 'ECATCHER'}]}, 'trackingParams': 'CAAQhGciEwi65YCZ6LaSAxVH2OcDHYYVMKQ='}
items = ['contents', 'twoColumnBrowseResultsRenderer', 'secondaryContents', 'sectionListRenderer']
none_if_absent = False

    def nav(root: JsonDict | None, items: list[Any], none_if_absent: bool = False) -> Any | None:
        """Access a nested object in root by item sequence."""
        if root is None:
            return None
        try:
            for k in items:
                root = root[k]
        except (KeyError, IndexError) as e:
            if none_if_absent:
                return None
>           raise type(e)(f"Unable to find '{k}' using path {items!r} on {root!r}, exception: {e}")
E           KeyError: "Unable to find 'contents' using path ['contents', 'twoColumnBrowseResultsRenderer', 'secondaryContents', 'sectionListRenderer'] on {'responseContext': {'serviceTrackingParams': [{'service': 'GFEEDBACK', 'params': [{'key': 'has_unlimited_entitlement', 'value': 'False'}, {'key': 'browse_id', 'value': 'VLOLAK5uy_mnDXXKVcY1Z_HKY00a_ZBrnE679EzsM50'}, {'key': 'browse_id_prefix', 'value': ''}, {'key': 'logged_in', 'value': '0'}]}, {'service': 'CSI', 'params': [{'key': 'c', 'value': 'WEB_REMIX'}, {'key': 'cver', 'value': '1.20260131.01.00'}, {'key': 'yt_li', 'value': '0'}, {'key': 'GetBrowsePlaylistDetailPage_rid', 'value': '0xc6a4680993d5e0b4'}]}, {'service': 'ECATCHER', 'params': [{'key': 'client.version', 'value': '1.20000101'}, {'key': 'client.name', 'value': 'WEB_REMIX'}]}]}, 'trackingParams': 'CAAQhGciEwi65YCZ6LaSAxVH2OcDHYYVMKQ=', 'microformat': {'microformatDataRenderer': {'noindex': True}}}, exception: 'contents'"

ytmusicapi/navigation.py:126: KeyError
View the full list of 1 ❄️ flaky test(s)
tests.mixins.test_library.TestLibrary::test_edit_song_library_status

Flake rate in main: 29.17% (Passed 17 times, Failed 7 times)

Stack Traces | 2.62s run time
self = <tests.mixins.test_library.TestLibrary object at 0x7f16489ec460>
yt_brand = <ytmusicapi.ytmusic.YTMusic object at 0x7f1648b32f90>
sample_album = 'MPREb_4pL8gzRtw1p'

    def test_edit_song_library_status(self, yt_brand, sample_album):
        album = yt_brand.get_album(sample_album)
        response = yt_brand.edit_song_library_status(album["tracks"][0]["feedbackTokens"]["add"])
        album = yt_brand.get_album(sample_album)
        assert album["tracks"][0]["inLibrary"]
        assert response["feedbackResponses"][0]["isProcessed"]
        response = yt_brand.edit_song_library_status(album["tracks"][0]["feedbackTokens"]["remove"])
        album = yt_brand.get_album(sample_album)
>       assert not album["tracks"][0]["inLibrary"]
E       assert not True

tests/mixins/test_library.py:133: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@apastel apastel force-pushed the fix-chrome-auth-857 branch from d944d8e to 149b4dd Compare January 31, 2026 22:19
@apastel apastel closed this Jan 31, 2026
@apastel apastel reopened this Jan 31, 2026
@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Feb 14, 2026

Anyone had time to test this out yet? Just checking.

@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Feb 22, 2026

Please let me know if there's anything faulty or missing from this PR and I will update. Thanks.

@sigma67 sigma67 self-requested a review February 23, 2026 13:38
@sigma67
Copy link
Copy Markdown
Owner

sigma67 commented Feb 23, 2026

Hi, thanks for the contribution! Could you add a test that asserts that it's working as expected?

@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Feb 24, 2026

Hi, thanks for the contribution! Could you add a test that asserts that it's working as expected?

Test Steps

  1. Paste raw headers from Chrome into headers_raw in test.cfg (overflowing lines must be indented)
  2. Run pdm run pytest -k test_setup_browser
  3. Observe test passes (on main, this fails)
  4. Re-run steps 1-3 with raw headers from Firefox to ensure there are no regressions.

@sigma67
Copy link
Copy Markdown
Owner

sigma67 commented Feb 24, 2026

I'm looking for an automated test (pytest), thanks

So basically add headers in the new/supported formats as a static file, while redacting confidential information

No manual steps should be needed

@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Feb 27, 2026

I'm looking for an automated test (pytest), thanks

So basically add headers in the new/supported formats as a static file, while redacting confidential information

No manual steps should be needed

Done.
Let me know if I should delete the existing test_setup_browser along with the headers_raw config item in test.example.cfg since this new test covers everything that existing test did without the need for manual pasting.

@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Mar 14, 2026

Any feedback on my pytest? Thanks.

@sigma67
Copy link
Copy Markdown
Owner

sigma67 commented Mar 15, 2026

Sorry I seem to have missed that. Thanks for adding the tests.

At first glance it seems that a lot of tests are failing. Will have a look at the code later today.

@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Mar 15, 2026

At first glance it seems that a lot of tests are failing. Will have a look at the code later today.

Ok, I better have another look at that too. I mistakenly thought it was the code coverage check failing to generate a report for some external reason, but upon closer look I see tests erroring out:

TypeError: RefreshingToken.__init__() got an unexpected keyword argument 'accept'
ytmusicapi.exceptions.YTMusicUserError: oauth JSON provided via auth argument, but oauth_credentials not provided.Please provide oauth_credentials as specified in the OAuth setup documentation.

@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Mar 16, 2026

Fixed issue with failing tests. It was because my new test was creating a new browser.json with [REDACTED] values and this was messing up all the subsequent tests.

Now, there are two unrelated tests failing, both referenced in this other developer's PR from a couple days ago: #884 (comment)

@apastel apastel closed this Apr 15, 2026
@apastel apastel reopened this Apr 15, 2026
@apastel
Copy link
Copy Markdown
Contributor Author

apastel commented Apr 15, 2026

@sigma67 is the pipeline in a weird state currently with regard to secrets? I saw your comment on another PR and wondering if that's why the sudden errors with config[auth]: #895 (comment)

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.

"Authentication Failed" when pasting request headers from Chrome

2 participants