You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #200. Fully shippable on its own (load + scan Dispatcharr source streams, read-only).
New src-tauri/src/engine/dispatcharr.rs (register in engine/mod.rs)
DispatcharrClient — normalize base URL like normalize_xtream_server (playlist.rs:775); reqwest client with the same knobs as the playlist downloader. ensure_token(): API-key header if set, else JWT login → cache access token (expiry now+25 min) → refresh on expiry → full re-login on 401; request() retries once on 401.
fetch_all<T>() handling both bare arrays and DRF {count, next, results} pagination (untagged enum), rewriting next host onto the base URL (reverse-proxy gotcha), hard page ceiling (500). Fetchers: channels, per-channel streams, groups, m3u accounts (accounts tolerate failure).
build_m3u(...): one #EXTINF per stream; name = channel name, or "{name} [{order+1}/{count}] {account-or-stream-name}" for multi-stream channels; attrs tvg-id, tvg-chno, tvg-logo ({base}/api/channels/logos/{id}/cache/), group-title, plus the five x-dispatcharr-* attrs (escape quotes). URL line = provider stream.url.
dispatcharr_ids_from_extinf(&str) -> Option<DispatcharrStreamRef> built on parse_extinf_attributes.
Session registry: register_session(identity, Arc<DispatcharrClient>) / get_session(identity) (same pattern as SERVER_LOCATION_CACHE).
DispatcharrOpenRequest { server, username, password, api_key } (next to XtreamOpenRequest).
open_playlist_dispatcharr + _inner mirroring open_playlist_xtream_inner: validate (api_key OR user+pass) → source key dispatcharr:{host}|{user} (mirror build_xtream_source_key) → emit playlist://load-progress stages → fetch channels/groups/accounts via tokio::join!, per-channel streams with buffer_unordered(8) → build_m3u → write_bytes_to_cache → parse_playlist_with_progress → set preview.file_name = "{host} (Dispatcharr)" and preview.source_identity → register_session.
Saved/recent/wiring
SavedPlaylistSource::Dispatcharr { server, username?, password?, api_key? } variant + SavedPlaylistKind arm (models/saved_playlist.rs); arms in commands/saved.rs (default_display_name, sanitize_saved_playlist_entry, open_saved_playlist with identity override saved:{id}); RecentPlaylistKind::Dispatcharr in commands/recent.rs (secrets only if "remember" checked); register the command in lib.rs invoke_handler.
Frontend
src/lib/types.ts + src/lib/tauri.ts: request type + openPlaylistDispatcharr wrapper.
src/components/OpenSourceDialog.tsx: new "Dispatcharr" tab (server URL, API-key vs username/password toggle, remember-credentials); autofill server from pasted /proxy/ts/stream/ or /output/m3u URLs (mirror parseXtreamM3ULink).
src/App.tsx: openPlaylistDispatcharrValue callback modeled on openPlaylistXtreamValue.
SavedPlaylistEditorDialog.tsx + src/lib/savedPlaylists.ts: dispatcharr form branch.
Tests
engine/dispatcharr.rs unit tests using the spawn_http_server pattern from checker.rs, extended to capture method+body: JWT login/refresh/401-retry, API-key header, pagination (paginated + bare array + host rewrite).
Pure tests: build_m3u → parser::parse_m3u → dispatcharr_ids_from_extinf round-trip (the load-bearing invariant); attr escaping.
SavedPlaylistSource::Dispatcharr serde round-trip; legacy JSON still parses.
Part of #200. Fully shippable on its own (load + scan Dispatcharr source streams, read-only).
New
src-tauri/src/engine/dispatcharr.rs(register inengine/mod.rs)DispatcharrClient— normalize base URL likenormalize_xtream_server(playlist.rs:775); reqwest client with the same knobs as the playlist downloader.ensure_token(): API-key header if set, else JWT login → cache access token (expiry now+25 min) → refresh on expiry → full re-login on 401;request()retries once on 401.DispatcharrChannel { id, uuid, channel_number, name, channel_group, tvg_id, logo, streams },DispatcharrStream { id, name, url, m3u_account, is_custom, tvg_id, is_stale, stream_stats, ... },DispatcharrGroup.fetch_all<T>()handling both bare arrays and DRF{count, next, results}pagination (untagged enum), rewritingnexthost onto the base URL (reverse-proxy gotcha), hard page ceiling (500). Fetchers: channels, per-channel streams, groups, m3u accounts (accounts tolerate failure).build_m3u(...): one#EXTINFper stream; name = channel name, or"{name} [{order+1}/{count}] {account-or-stream-name}"for multi-stream channels; attrstvg-id,tvg-chno,tvg-logo({base}/api/channels/logos/{id}/cache/),group-title, plus the fivex-dispatcharr-*attrs (escape quotes). URL line = providerstream.url.dispatcharr_ids_from_extinf(&str) -> Option<DispatcharrStreamRef>built onparse_extinf_attributes.register_session(identity, Arc<DispatcharrClient>)/get_session(identity)(same pattern asSERVER_LOCATION_CACHE).patch_stream_stats,set_channel_streams,bulk_delete_streams.Modified
src-tauri/src/commands/playlist.rsDispatcharrOpenRequest { server, username, password, api_key }(next toXtreamOpenRequest).open_playlist_dispatcharr+_innermirroringopen_playlist_xtream_inner: validate (api_key OR user+pass) → source keydispatcharr:{host}|{user}(mirrorbuild_xtream_source_key) → emitplaylist://load-progressstages → fetch channels/groups/accounts viatokio::join!, per-channel streams withbuffer_unordered(8)→build_m3u→write_bytes_to_cache→parse_playlist_with_progress→ setpreview.file_name = "{host} (Dispatcharr)"andpreview.source_identity→register_session.Saved/recent/wiring
SavedPlaylistSource::Dispatcharr { server, username?, password?, api_key? }variant +SavedPlaylistKindarm (models/saved_playlist.rs); arms incommands/saved.rs(default_display_name,sanitize_saved_playlist_entry,open_saved_playlistwith identity overridesaved:{id});RecentPlaylistKind::Dispatcharrincommands/recent.rs(secrets only if "remember" checked); register the command inlib.rsinvoke_handler.Frontend
src/lib/types.ts+src/lib/tauri.ts: request type +openPlaylistDispatcharrwrapper.src/components/OpenSourceDialog.tsx: new "Dispatcharr" tab (server URL, API-key vs username/password toggle, remember-credentials); autofill server from pasted/proxy/ts/stream/or/output/m3uURLs (mirrorparseXtreamM3ULink).src/App.tsx:openPlaylistDispatcharrValuecallback modeled onopenPlaylistXtreamValue.SavedPlaylistEditorDialog.tsx+src/lib/savedPlaylists.ts: dispatcharr form branch.Tests
engine/dispatcharr.rsunit tests using thespawn_http_serverpattern from checker.rs, extended to capture method+body: JWT login/refresh/401-retry, API-key header, pagination (paginated + bare array + host rewrite).build_m3u→parser::parse_m3u→dispatcharr_ids_from_extinfround-trip (the load-bearing invariant); attr escaping.SavedPlaylistSource::Dispatcharrserde round-trip; legacy JSON still parses.