Skip to content

Twitch music provider#3492

Draft
Drizzt321 wants to merge 57 commits intomusic-assistant:devfrom
Drizzt321:twitch-music-provider
Draft

Twitch music provider#3492
Drizzt321 wants to merge 57 commits intomusic-assistant:devfrom
Drizzt321:twitch-music-provider

Conversation

@Drizzt321
Copy link
Copy Markdown

@Drizzt321 Drizzt321 commented Mar 27, 2026

Created a Twitch Music Provider, to listen to audio_only streams. Primarily for DJ streams, but could probably be used for any stream.

I'm not a Python developer, and I heavily used Claude Code, so while I did look at things, I'm more a Java developer, rather than Python.

Website docs update: music-assistant/music-assistant.io#561

Drizzt321 and others added 30 commits March 22, 2026 15:34
Provider scaffold with manifest, get_stream_details(), get_audio_stream()
with reconnection loop and quality selection. 29 tests covering provider
lifecycle, stream details, audio streaming, reconnection, and error cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Config entries for client credentials, streamlink token, ad handling,
auto-raid toggle. OAuth code flow via AuthenticationHelper, token
refresh with 401 auto-retry, best-effort revocation. 22 new auth tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browse with Live/Following folders, library radios (live-only),
channel search. Twitch API client with pagination, batching (100/req),
5-min live status cache. 25 new tests with JSON fixtures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Monkey-patches Streamlink's TwitchHLSStreamWriter for ad segments.
Silence mode: replaces ad bytes with generated silence.ts, scales to
segment duration, drains ad data without buffering. Passthrough mode:
logs and passes ad audio through. Module-level ad_break_active flag.
14 new tests with mock Streamlink classes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EventSub client: WebSocket lifecycle with reconnect/backoff, session
management, channel.raid subscription/unsubscription, revocation
handling. Raid state machine: queue event handling, stale raid
filtering, auto-raid toggle, timer-based cleanup on unload.
32 new tests (21 eventsub + 11 raid).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Step 3 TODO comments with streamlink token code path test.
All checks clean: ruff, mypy, codespell, formatting. 123 total tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use fixed https://music-assistant.io/callback as redirect_uri with
local callback URL passed via state parameter, matching the Spotify
provider pattern. Twitch requires exact redirect URI matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the required callback URL (https://music-assistant.io/callback)
in the provider config dialog so users know what to register in their
Twitch application, matching the Spotify provider pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explain what the token is, why it helps (Turbo/subscriber ad reduction),
and how to obtain it (browser cookie or Twitch CLI).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Batch-fetch user profiles via /helix/users alongside followed channels
and live streams (cached together). Use profile_image_url as thumbnail
fallback when no stream preview is available (offline channels).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update streamdetails.stream_title dynamically during ad breaks.
Fix type: ignore comments for mypy now that streamlink is installed
and types are resolved (override, attr-defined, no-untyped-call).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import ad_handling module instead of ad_break_active value to get
a live reference to the flag. Importing the bool captured a snapshot;
reading the module attribute gets the current value each iteration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes test gaps from code review:
- test_provider: ad_handling/auto_raid config, subscribe verification
- test_streaming: streamlink token behavioral tests, reconnect delay
- test_auth: token exchange failures, revoke no-op, cache invalidation
- test_browse: thumbnails, viewer counts, sort, error handling, empty states
- test_twitch_api: page/batch aggregation, empty user lookup
- test_ad_handling: patch targets existence
- test_eventsub: backoff, reconnect, resubscription, revocation logging
- test_raid: channel switching, rapid raids, auto-raid toggle, cleanup
Also adds token_exchange.json and token_refresh.json fixtures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P0-1: Implement _on_queue_updated — parse queue events, dispatch to
      _handle_queue_playing/_handle_queue_stopped based on state/URI
P0-2: Persist refreshed tokens via _update_config_value (encrypted)
P0-3: Validate authentication in setup() — raise LoginFailed if no token
P0-4: Fix _auto_raid boolean bug (was always True due to `or True`)
P0-5: Replace bare Exception with MA-specific exceptions in _api_get
P0-6: Add "stage": "beta" to manifest.json

Also: remove hasattr guard, add EventType filter to subscribe, deduplicate
timer cancellation, clear cache in unload, log revoke errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1-1: Implement get_radio() for single-channel lookup (queue history,
      deep links, bookmarks)
P1-2: Propagate LoginFailed from handle_async_init instead of swallowing
P2-7: Move patch_ad_handling to handle_async_init (once, not per-resolve)
P2-8: Remove per-resolve ad_handling import from _resolve_streams
P2-9: Use asyncio.gather for concurrent streams + profiles fetch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create session once on first resolve, reuse for reconnects. Avoids
repeated plugin loading and session setup overhead per resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New tests:
- test_invalid_streamlink_token_stream_still_plays (streaming)
- test_user_profiles_batches_over_100 (twitch_api)
- test_ad_segment_logged with caplog (ad_handling)
- test_refresh_no_refresh_token_raises (auth)

Fixed:
- test_browse_api_error: asserts ProviderUnavailableError specifically
- Moved load_fixture() to conftest.py, removed 3 duplicates

164 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split _handle_queue_stopped into pause/idle/stop paths:
- Pause: unsubscribe EventSub, keep WebSocket warm
- Idle: 15s grace period → unsubscribe → 5min idle → disconnect WS
- Stop (non-Twitch): immediate cleanup
- Resume: cancel timers, re-subscribe

6 new timer tests (was blocked by missing implementation). 170 total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests the reconnect mechanism via state verification: backoff doubles
on disconnect, resets on welcome, _stopped flag prevents reconnect.
171 tests, all passing. All spec tests now implemented.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_auth_callback_exchanges_code_for_tokens: happy-path OAuth flow
- test_post_includes_auth_headers: EventSub POST has auth headers
- Fix test_authenticated_label docstring to match implementation

173 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MockResponse: raise ValueError on .json() for non-2xx without explicit
  json_data; add headers param; clear exhaustion error on sequential mocks
- Add make_queue_event() and config_side_effect() conftest helpers
- Add 6 _on_queue_updated event bus dispatch tests (test_raid.py)
- Add 2 handle_async_init lifecycle tests (test_provider.py)
- Rewrite test_streamlink_exception_handled to exercise real except block
- Remove duplicate test_nonexistent_channel_returns_empty
- Strengthen test_subscribe_skips_if_welcome_already_subscribed with
  race condition simulation and meaningful assertions
- Migrate auth tests to use token_exchange.json/token_refresh.json fixtures

180 tests passing, ruff clean, mypy clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Twitch's HLS output is 48000 Hz AAC. The silence clip was generated
at 44100 Hz, causing FFmpeg to reject the data when silence bytes
were the first input (pre-roll ad at stream start). Regenerated at
48000 Hz to match Twitch's actual output format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logs at key decision points: _on_queue_updated dispatch, _handle_queue_playing
(auto_raid/auth checks, EventSub creation, subscription), pause/idle/stop
handlers, raid events, and provider init (auto_raid, ad_handling, user ID).

Needed to diagnose auto-raid not triggering during integration testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…kipping

Streamlink's TwitchHLSStreamWriter.should_filter_segment() returns
segment.ad, which causes the base HLS writer to skip ad segments
entirely before write() is ever called. Our silence-injecting writer
overrode write() but ad segments never reached it.

Fix: override should_filter_segment() to return False, so ad segments
reach write() where they're replaced with silence data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The base HLSStreamWriter pauses the reader when filtering segments
and resumes it after writing data. Our silence-injecting writer
bypasses the base write() for ad segments, writing silence directly
to the buffer — but never called reader.resume(), leaving fd.read()
blocked until a non-ad segment arrived via the base class path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix FakeTwitchHLSStreamWriter to match real base class: should_filter_segment
  returns segment.ad (was always False), reader has is_paused/resume mocks
- test_silence_writer_does_not_filter_ad_segments: verifies override returns False
- test_silence_writer_resumes_paused_reader: verifies resume() called when paused
- test_silence_writer_skips_resume_when_not_paused: verifies no spurious resume

These tests would have caught both silence injection bugs found during
integration testing (should_filter_segment blocking write(), and reader
staying paused after silence injection).

183 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies silence clip is 48000 Hz AAC stereo MPEG-TS, matching Twitch's
HLS output format. A sample rate mismatch (previously 44100 Hz) caused
FFmpeg to reject silence data during pre-roll ads.

ffprobe-based test skips if ffprobe unavailable (CI). Pure Python test
validates MPEG-TS sync byte as a fallback.

185 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

When playing from MA's Library (Radio section), the QueueItem URI is
library://radio/N instead of twitch://radio/channel_login. The raid
state machine checked uri.startswith("twitch://") which failed for
library URIs, causing raid tracking to never activate.

Fix: add _extract_twitch_login() that checks both URI schemes — direct
twitch:// URIs extract the login from the path, library:// URIs look
up the media_item's provider_mappings for a Twitch provider domain.

Also adds test for library URI dispatch path and updates conftest
make_queue_event to support provider_domain/provider_item_id params.

186 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ported copy

Streamlink's plugin system loads plugins into a fresh module namespace on
each streams() call, creating a different TwitchHLSStreamReader class object
than the one obtained via `from streamlink.plugins.twitch import ...`. The
monkey-patch was applied to the imported class but Streamlink used the
plugin-loaded class, so silence injection never activated.

Fix: apply the patch after streams() resolves, targeting the reader class
from the actual stream object (`type(stream).__reader__`). patch_ad_handling
now accepts an optional reader_cls parameter.

Also removes the one-time patch from handle_async_init() since it patched
the wrong class object — the per-resolution patch is now the only path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drizzt321 and others added 5 commits April 3, 2026 16:55
Create a new Streamlink session as a local in get_audio_stream rather
than caching one on self. Streamlink sessions are not thread-safe
(shared HTTP state, options, plugin instance vars), so concurrent
get_audio_stream calls from multiple queues would race on a shared
session. Each stream now gets its own session, reused across
reconnections within that stream. Session options (queue deadline,
OAuth token) applied once at creation. Ad-handling monkey-patch
applied once after first resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The file-wide # mypy: disable-error-code="unreachable" suppressed
false positives in _connect_loop where mypy can't reason about
_stopped being set externally or finally blocks with mixed
return/fall-through paths. Narrowed to inline ignores on the specific
lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stream_title is a property backed by stream_metadata, so clearing
metadata also clears the title. Added comment to make this non-obvious
relationship explicit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reference python/mypy#12784 (async for + except CancelledError) and
python/mypy#13104 (finally with mixed return/fall-through) to explain
why the ignore comments are needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Drizzt321
Copy link
Copy Markdown
Author

Sorry for the delay, it's been a busy week. Should have your concerns handled now. Hopefully doesn't raise new ones :)

@Drizzt321 Drizzt321 requested a review from MarvinSchenkel April 4, 2026 04:48
@MarvinSchenkel MarvinSchenkel marked this pull request as ready for review April 7, 2026 12:58
Copilot AI review requested due to automatic review settings April 7, 2026 12:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Twitch Audio music provider to Music Assistant, enabling playback of Twitch channel audio-only streams (via Streamlink) with optional raid-following via Twitch EventSub, plus a comprehensive provider-specific test suite.

Changes:

  • Introduces music_assistant.providers.twitch provider implementation (OAuth, browse/search, radio streaming, caching, ad passthrough, raid-following).
  • Adds an EventSubClient WebSocket implementation for channel.raid subscriptions and switching active queues on raids.
  • Adds extensive pytest coverage (API client behavior, streaming/reconnects, recommendations, browsing/search, OAuth/refresh/revoke, EventSub, raid-following) and fixtures; adds Streamlink dependency.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
music_assistant/providers/twitch/__init__.py Main Twitch provider: OAuth config flow, Twitch API helpers, browse/search/radios, Streamlink streaming with reconnects and ad-state tracking, raid-following logic.
music_assistant/providers/twitch/eventsub.py EventSub WebSocket client for raid subscriptions + reconnect/backoff management.
music_assistant/providers/twitch/ad_handling.py Streamlink Twitch plugin monkey-patch to pass through ad segments while tracking ad-break state.
music_assistant/providers/twitch/manifest.json Provider manifest declaring domain, beta stage, and Streamlink requirement.
requirements_all.txt Adds streamlink>=8.0,<9 to the full requirements set.
tests/providers/twitch/__init__.py Test package marker for Twitch provider tests.
tests/providers/twitch/conftest.py Shared fixtures/mocks for Twitch provider tests (mock responses, mock MA/core objects, fixture loader).
tests/providers/twitch/test_provider.py Provider lifecycle/config entry tests (setup/init/unload/features).
tests/providers/twitch/test_auth.py OAuth action handling, token exchange, refresh, and revoke behavior tests.
tests/providers/twitch/test_twitch_api.py Tests for request headers, pagination, batching, caching, and error handling in API calls.
tests/providers/twitch/test_browse.py Browse structure + library radios + search behavior tests.
tests/providers/twitch/test_recommendations.py Recommendations folder creation and contents tests.
tests/providers/twitch/test_streaming.py StreamDetails + Streamlink streaming generator behavior, reconnect logic, and token header tests.
tests/providers/twitch/test_ad_handling.py Validates Streamlink monkey-patching behavior using injected fake Streamlink modules.
tests/providers/twitch/test_eventsub.py EventSub client message handling, subscribe/unsubscribe, reconnect/backoff behavior tests.
tests/providers/twitch/test_raid.py Raid-following behavior tests, including grace-period handling and multi-queue switching.
tests/providers/twitch/fixtures/followed_channels.json Twitch API fixture: followed channels pagination.
tests/providers/twitch/fixtures/live_streams.json Twitch API fixture: live streams response.
tests/providers/twitch/fixtures/search_results.json Twitch API fixture: search results response.
tests/providers/twitch/fixtures/user_lookup.json Twitch API fixture: /helix/users response.
tests/providers/twitch/fixtures/token_exchange.json Twitch OAuth token exchange fixture.
tests/providers/twitch/fixtures/token_refresh.json Twitch OAuth refresh fixture.
tests/providers/twitch/fixtures/eventsub_welcome.json EventSub fixture: welcome message.
tests/providers/twitch/fixtures/eventsub_reconnect.json EventSub fixture: reconnect message.
tests/providers/twitch/fixtures/eventsub_revocation.json EventSub fixture: revocation message.
tests/providers/twitch/fixtures/eventsub_raid.json EventSub fixture: raid notification message.

Comment thread music_assistant/providers/twitch/__init__.py Outdated
Comment thread music_assistant/providers/twitch/__init__.py
Comment thread music_assistant/providers/twitch/__init__.py Outdated
Comment thread tests/providers/twitch/test_streaming.py Outdated
Comment thread tests/providers/twitch/test_raid.py
Comment thread tests/providers/twitch/test_eventsub.py Outdated
Comment thread tests/providers/twitch/test_raid.py
Drizzt321 and others added 7 commits April 7, 2026 18:58
Hardcoded twitch:// URI would resolve to the wrong provider
instance when multiple Twitch instances are configured. Uses
self.instance_id:// to match the pattern used elsewhere in the
provider and across the MA codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously 401/403 fell through to generic MusicAssistantError,
preventing the UI from prompting re-authentication. Now surfaces
LoginFailed so MA can show the re-auth flow when credentials are
invalid or revoked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Raise LoginFailed early when session_id is missing from auth
values, preventing empty-string routing collisions in
AuthenticationHelper. Follows the Deezer provider pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous test used inspect.getsource() to check for
asyncio.to_thread usage, which breaks on refactors that don't
change behavior. Now patches asyncio.to_thread with a tracking
wrapper and asserts the blocking Streamlink callables
(create_session, open, read, close) are dispatched through it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests waiting for fire-and-forget asyncio.create_task() used
asyncio.sleep(0.05) which is timing-dependent and flaky under
CI load. Positive assertions now use asyncio.Event signaled by
the mocked coroutine with wait_for timeout. Negative assertion
uses sleep(0) for a single event loop yield.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `not values` check was after two `values.get()` calls, making
it dead code — if values were None, line 101 would already crash
with AttributeError. Moves the guard to the function entry point
and simplifies the session_id check to just a key membership test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Drizzt321
Copy link
Copy Markdown
Author

Updated as per Copilot reivew.


return [] # pragma: no cover

async def get_library_radios(self) -> AsyncGenerator[Radio, None]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The current approach with both browse() and get_library_radios() implemented is not correct. We recently standardised the radio provider pattern — if a provider declares LIBRARY_RADIOS and implements get_library_radios(), the base class handles browse automatically by calling the get_library_*() methods. You should not also be implementing browse() or declaring BROWSE in SUPPORTED_FEATURES.
The correct pattern is:

get_library_radios() yields all followed channels (all of them) and MA's library sync takes care of adding and removing from the library automatically
recommendations() is the right place to surface currently live channels since it is surfaced to the user on demand

Users therefore have all of their library from the provider in the MA library and use the search to find more.

Please remove browse() and remove BROWSE from SUPPORTED_FEATURES.
If you have a specific use case that genuinely requires a dedicated browse implementation that cannot be served by this pattern, please raise it and we can discuss, but our strong preference is to maintain the standardised approach across all providers."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To be clear. You are filtering out non-live channels but this method is only called on each full sync of the library at the specified interval, so that could be hours old. A channel that was live when the sync ran will appear in the library, and one that wasn't live will be missing — even though the user follows both. The live status enrichment (viewer count in the name, live thumbnail) will also be stale by the time the user sees it, so it has no value here. The library should be a stable list of all followed channels with basic profile info and nothing more.

msg = f"Twitch API: server error ({status})"
raise ProviderUnavailableError(msg)
msg = f"Twitch API error ({status})"
raise MusicAssistantError(msg)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The other thing here is that for unhandled status codes it raises MusicAssistantError directly — this is the base exception and should not be used directly. ProviderUnavailableError or SetupFailedError would be appropriate for an unexpected HTTP status I would think.

data={"client_id": client_id, "token": access_token},
):
pass
except Exception:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There are nine of these. You need to be catching the specific errors you are looking for to ensure they are handled correctly.

self.logger.debug("Stream started for %s (active: %d)", channel_login, prev_count + 1)

if prev_count == 0:
asyncio.create_task(self._subscribe_raids_for_channel(channel_login))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When you call asyncio.create_task() without storing the result, any exceptions raised by that task are silently lost with no way to know something went wrong. Look at how how other providers do this — please apply the same pattern to the other create_task() calls in eventsub.py


def __init__(
self,
http_session: Any,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Typing this as Any is defeating mypy type checking. I think this should be aiohttp.ClientSession and below ws should be aiohttp.ClientWebSocketResponse

if self._stopped:
# mypy false positive (python/mypy#12784): async for + except
# CancelledError makes mypy think the loop body is unreachable
break # type: ignore[unreachable]
Copy link
Copy Markdown
Contributor

@OzGav OzGav Apr 8, 2026

Choose a reason for hiding this comment

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

You need to fix these rather than ignore them. If you fix the types I mentioned before then somthing like this should work (so look at all the ignores again you will be able to fix them)

try:
    self._ws = await self._http_session.ws_connect(url)
except aiohttp.ClientError:
    logger.debug("EventSub: WebSocket connection failed", exc_info=True)
    continue

async for msg in self._ws:
    if self._stopped:
        break
    ...


async def test_cache_cleared_on_logout(provider: TwitchProvider) -> None:
"""Logout invalidates the cache."""
provider._cached_channels = [{"broadcaster_id": "123"}]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How come you aren't using MA's cache system?

@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented Apr 8, 2026

General comment on the docstrings. They should provide clarity to the caller, not explain implementation. Several docstrings describe internal mechanics, e.g.: _create_streamlink_session: "Blocking — call via to_thread" is an implementation note, not caller-facing
The format needs to be Sphinx-style. e.g.

def my_function(param1: str, param2: int, param3: bool = False) -> str:
    """
    Brief one-line description of the function.

    :param param1: Description of what param1 is used for.
    :param param2: Description of what param2 is used for.
    :param param3: Description of what param3 is used for.
    """

@MarvinSchenkel
Copy link
Copy Markdown
Contributor

So I just did some digging in the TOS and I am afraid we cannot merge this PR because of it :(. Specifically:

  • D1 - Embeddable Experiences: You must adopt and integrate our embeddable player for all Twitch video content. You may not use any other player or system to display Twitch video content without Twitch’s prior written permission.
  • X1.I: You will not access undocumented Program Materials or otherwise attempt to derive or use the underlying source code of undocumented Program Materials without Twitch’s prior written permission. You may only access data from Program Materials consistent with the terms of this Agreement and, unless you have Twitch’s prior written permission, will only access Program Materials documented on the Twitch. StreamLink seems to use undocumented GraphQL API's

What is your interpretation of this?

@MarvinSchenkel MarvinSchenkel marked this pull request as draft April 28, 2026 10:05
@Drizzt321
Copy link
Copy Markdown
Author

Sorry for how delayed getting back to you I have been, work has slammed me. I should be able to pick this back up in a few weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants