Repository: streamlink/streamlink

Issue:
plugins.twitch: is_bad_bot=true in headless mode (forced integrity-token acquirement)
### Checklist

- [X] This is a bug report and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

in the latest version client integrity token fetching is disabled, this is a specific version.

### Description

i have a specific version of streamlink that tries to fetch integrity tokens regardless. same exact commands used to work, now suddenly i am getting bad integrity tokens. 

i am guessing they might have changed something in their protocol so that the current solution (https://github.com/streamlink/streamlink/issues/5380) might not work when enabled.

### Debug log

```text
streamlink --loglevel=debug https://www.twitch.tv/thebausffs best    
[cli][debug] OS:         macOS 12.4
[cli][debug] Python:     3.10.5
[cli][debug] OpenSSL:    OpenSSL 1.1.1n  15 Mar 2022
[cli][debug] Streamlink: 0.0.0+unknown
[cli][debug] Dependencies:
[cli][debug]  certifi: 2022.9.14
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.2
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.17
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.28.1
[cli][debug]  trio: 0.22.0
[cli][debug]  trio-websocket: 0.10.2
[cli][debug]  typing-extensions: 4.3.0
[cli][debug]  urllib3: 1.26.12
[cli][debug]  websocket-client: 1.4.1
[cli][debug] Arguments:
[cli][debug]  url=https://www.twitch.tv/thebausffs
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin twitch for URL https://www.twitch.tv/thebausffs
[plugins.twitch][debug] Getting live HLS streams for thebausffs
[plugins.twitch][info] Acquiring new client-integrity token...
[webbrowser.webbrowser][info] Launching web browser: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[webbrowser.webbrowser][debug] Waiting for web browser process to terminate
[plugins.twitch][debug] Client-Integrity token: {"client_id":"<redacted>","client_ip":"<redacted>","device_id":"<redacted>","exp":"2023-10-10T00:56:42Z","iat":"2023-10-09T08:56:42Z","is_bad_bot":"true","iss":"Twitch Client Integrity","nbf":"2023-10-09T08:56:42Z","user_id":""}
[plugins.twitch][info] Is bad bot? True
[plugins.twitch][warning] No client-integrity token acquired
[plugins.twitch][debug] {'adblock': False, 'geoblock_reason': '', 'hide_ads': False, 'server_ads': True, 'show_ads': True}
[utils.l10n][debug] Language code: en_US
error: No playable streams found on this URL: https://www.twitch.tv/thebausffs
```



Discussion:
Kasada has seemingly started detecting Chrome's new headless mode, it was previously undetected. Visiting Twitch directly in headless mode also results in a bad bot token, so it doesn't have anything to do with streamlink's implementation.
Acquiring a token still works with `--webbrowser-headless false`.
alright, did not know i had headless true by default. yeah only headless seems to be detected 👍 following worked:
```
streamlink --loglevel=debug --webbrowser-headless false https://www.twitch.tv/thebausffs best
```
We could change the default value to `False`, but I'm not entirely sure if that's a good idea.

I had a generic `webbrowser://URL` plugin in the works after implementing the webbrowser API in 6.0.0, but that still hasn't been finished, and TBH, I totally forgot about it. As you know, the Twitch plugin attempts to open streams without an integrity-token first. If we'd set the default value of `--webbrowser-headless` to `False`, then this would mean that for regular webbrowser API usages where the website doesn't detect headless mode, this would be an inferior experience unless the user sets the option to `True` manually. On the other hand, the Twitch plugin is (so far) the only reason why the webbrowser API was added, and if Twitch detects headless mode, having the default value set to `True` is useless, because the user needs to change config option to actually make it work.

So the question is, does setting the default value to `False` make sense for the sake of compatibility, or would it be bad because of an inferior user experience (in other future plugin implementations)?

----

> [cli][debug] Streamlink: 0.0.0+unknown

This version string does only get shown if you messed up your Streamlink install. You either need to clone the git repo and fetch at least one tag while having Streamlink installed in editable mode, or you need to install from the `sdist` tarball, as the correct version is built in there, same as the `bdist` wheels. Don't use the tarball created by GitHub from the tag's snapshot, as it's lacking the necessary info.

And in order to force the integrity-token, you don't need a "custom version of Streamlink" at all. All you need is a simple plugin override while [sideloading](https://streamlink.github.io/cli/plugin-sideloading.html) it.

```diff
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index e74e901a..d9efb307 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -787,7 +787,7 @@ class Twitch(Plugin):
         response, *data = self.api.access_token(is_live, channel_or_vod)
 
         # try again with a client-integrity token if the API response was erroneous
-        if response != "token":
+        if True or response != "token":
             client_integrity = self._client_integrity_token(channel_or_vod) if is_live else None
             response, *data = self.api.access_token(is_live, channel_or_vod, client_integrity)
```

Resolving patch:
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -565,7 +565,12 @@ async def acquire_client_integrity_token(client: CDPClient):
                     return await client_session.evaluate(js_get_integrity_token, timeout=eval_timeout)
 
         try:
-            client_integrity: Optional[str] = CDPClient.launch(session, acquire_client_integrity_token)
+            client_integrity: Optional[str] = CDPClient.launch(
+                session,
+                acquire_client_integrity_token,
+                # headless mode gets detected by Twitch, so we have to disable it regardless the user config
+                headless=False,
+            )
         except WebbrowserError as err:
             log.error(f"{type(err).__name__}: {err}")
             return None


Test patch: