Add native IPTV Live TV support#16
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a complete Live TV IPTV flow: source parsing, secure fetching, storage and caching, guide rendering, live playback gating, and home/routing integration. ChangesLive TV feature
Sequence Diagram(s)sequenceDiagram
participant User
participant SearchBar as SearchBar.svelte
participant LiveTV as LiveTV.svelte
participant Refresh as refreshIptvSource
participant Cache as cache.ts
participant Player as Player.svelte
User->>SearchBar: click Live TV
SearchBar->>LiveTV: navigate("live")
User->>LiveTV: refresh source
LiveTV->>Refresh: refreshIptvSource(source)
Refresh-->>LiveTV: refresh result
LiveTV->>Cache: persistIptvRefreshResult(source, result)
User->>LiveTV: play channel
LiveTV->>Player: navigate("player", liveMode=true)
Player->>Player: gate live playback logic
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (2)
packages/app/src/lib/iptv/xmltv.test.ts (1)
15-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a malformed programme tolerance test.
Please add a case where one
<programme>has an invalidstart/stoptimestamp and verify valid programmes still parse, to lock in partial-recovery behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/lib/iptv/xmltv.test.ts` around lines 15 - 73, Add a new test case within the describe("parseXmltv") block that validates partial recovery behavior when encountering malformed data. Create a test XML string containing both a programme with invalid start/stop timestamps and a valid programme, then verify that parseXmltv still successfully parses the valid programme while gracefully handling the malformed one. This ensures the parser can recover from partial data corruption and continues processing valid entries.packages/app/src/lib/iptv/m3u.test.ts (1)
44-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a regression test for group ID collision cases.
Please add a case with two distinct
group-titlevalues that normalize to the same slug and assert unique group ids in output.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/lib/iptv/m3u.test.ts` around lines 44 - 71, Add a new regression test case after the existing test functions in the test suite to verify that group ID collisions are handled correctly. Create a test that uses a playlist with two distinct group-title values that normalize to the same slug (for example, "News Group" and "News-Group" or "tech news" and "tech-news"), then call parseM3U and assert that the result contains groups with unique group IDs even though the normalized slugs would collide. This ensures the parseM3U function properly handles slug collision scenarios by generating distinct identifiers for groups with different original titles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/desktop/electron/services/mainIpc.cjs`:
- Around line 91-121: The DNS validation in assertSafeIptvFetchUrl validates
resolved addresses at one moment, but getSafeIptvFetchResponse later calls fetch
which performs a separate DNS resolution on the hostname, creating a window for
DNS rebinding attacks. Modify assertSafeIptvFetchUrl to return the validated
resolved IP addresses instead of just throwing errors, then update
getSafeIptvFetchResponse to use the returned resolved IP address directly in the
fetch call instead of passing the original hostname URL to fetch, ensuring the
same validated address is actually used for the network connection.
- Around line 275-313: The IPTV fetch response streams are not being properly
canceled when errors are thrown, causing potential socket and memory leaks. You
need to cancel the response body when throwing an error on the non-OK response
check (when response.ok is false), and cancel the reader stream when throwing on
the size overflow condition (when totalBytes exceeds maxBytes in the while
loop). Additionally, ensure the arrayBuffer size overflow path (when
arrayBuffer.byteLength exceeds maxBytes) also properly cleans up any resources
before throwing. Use the appropriate cancellation/abort methods for the response
and reader objects before each throw statement to ensure streams are properly
cleaned up on all error paths.
In `@packages/app/src/lib/iptv/cache.test.ts`:
- Line 100: The assertion on line 100 stringifies the MemoryStorage instance
itself, but this doesn't reveal the actual cached data because the private Map
containing the values is not included in the JSON serialization. Instead of
checking if the stringified storage does not contain "programmesByChannel",
access the actual stored cache values from the MemoryStorage instance and verify
that "programmesByChannel" is not present in the real stored data. Check the
MemoryStorage implementation to determine the appropriate method or property to
retrieve the actual cached values.
In `@packages/app/src/lib/iptv/cache.ts`:
- Around line 140-145: The cache sanitization validation in the conditional
statement (where sanitizeChannel, sanitizeGroup, and sanitizeStats are called)
only checks for null values but does not verify sourceId consistency between the
parent object and individual channels/groups. Add additional checks to the
existing if condition to ensure that every channel object has a sourceId
matching the parent value.sourceId and every group object has a sourceId
matching the parent value.sourceId, returning null if any mismatch is found.
This validation should be added to the same condition alongside the existing
null checks.
In `@packages/app/src/lib/iptv/fetch.ts`:
- Around line 52-55: The current implementation in the fetch function calls
response.text() which buffers the entire response payload into memory before
checking against maxBytes, defeating the size limit's purpose for responses
without a content-length header. Replace the response.text() buffering approach
with streaming via response.body?.getReader() to incrementally read the response
in chunks, accumulating the byte length as you read. Check if the accumulated
byte length exceeds maxBytes during the streaming process and throw an error
immediately without buffering the remaining data. Only decode the accumulated
buffer to text after confirming the total size is within the maxBytes limit.
In `@packages/app/src/lib/iptv/guideGrid.ts`:
- Around line 32-52: The resolveGuideChannelId function accepts tvgId when
either guide.channels or guide.programmesByChannel contains that key (via the
hasGuideChannelId check), but the getNowNext function in xmltv.ts resolves tvgId
using guide.channels only. This inconsistency causes channels to be found in the
grid but fail to resolve during playback. Create a shared channel resolver
function that both resolveGuideChannelId and getNowNext can use, ensuring they
follow the same resolution logic. The shared resolver should define whether
tvgId lookups check both guide.channels and guide.programmesByChannel, or only
guide.channels, consistently across both call sites.
- Around line 91-102: In the code block where startMs and stopMs are calculated
from programme.start and programme.stop, add an early validation check to detect
malformed programme intervals where stopMs is less than or equal to startMs, and
return null if this condition is found. This check must occur before the
viewport clipping and percentage calculations (roundPercent calls for
leftPercent and widthPercent) to prevent invalid programme tiles from being
created due to zero or negative widthPercent values.
In `@packages/app/src/lib/iptv/m3u.ts`:
- Around line 105-109: The group ID generation in the map function uses only the
slugified name via slugForId(name), which causes different original names that
normalize to the same slug (like "Café" and "Cafe") to produce duplicate IDs.
Include the original group name parameter in the ID construction alongside the
slug to ensure uniqueness, so that even if names normalize identically, the
original names keep the IDs distinct and prevent unintended group merging
downstream.
In `@packages/app/src/lib/iptv/store.ts`:
- Around line 58-67: The persistSources function's storage.setItem() call can
throw exceptions (QuotaExceededError, SecurityError) but lacks error handling,
and setSources calls persistSources before updating in-memory state, causing
complete operation failure if storage is unavailable. Wrap the storage.setItem()
call inside persistSources with a try-catch block to gracefully handle storage
errors, then reorder setSources to call iptvSources.set(sources) before
persistSources(sources) so the in-memory state is always updated even when
storage persistence fails.
In `@packages/app/src/lib/iptv/xmltv.ts`:
- Around line 154-171: The parseXmltvTime function calls on lines 164-165 can
throw exceptions that abort the entire guide parse when encountering a malformed
programme entry. Wrap both parseXmltvTime calls (for start and stop times) in a
try-catch block within the while loop that processes programmes, and use
continue to skip to the next programme if parsing fails, ensuring that one
invalid entry does not invalidate the entire guide.
In `@packages/app/src/pages/live/LiveTV.svelte`:
- Around line 363-369: The formatLoadedAt function does not guard against
invalid date strings, which can cause the Intl.DateTimeFormat().format() call to
throw an error and crash the page render. Add validation in the formatLoadedAt
function to check if the date string is valid before formatting it, returning a
fallback value for invalid dates. Additionally, add date validation for the
loadedAt field in packages/app/src/lib/iptv/cache.ts to ensure the cache
sanitization only accepts valid date strings and rejects or sanitizes
malformed/legacy localStorage entries.
In `@packages/app/src/pages/player/Player.svelte`:
- Around line 1276-1278: The reactive condition in the watch party modal close
logic on line 1276 is missing a check for local mode. Add `$localMode` to the OR
condition alongside `liveMode`, `embedSrc`, and the cloud sync check so that the
watch party modal is properly closed when local mode is active, preventing it
from unexpectedly reopening when local mode is disabled.
In `@scripts/smoke-iptv-dispatcharr.mjs`:
- Around line 46-47: After calling parseM3U(m3u.text, "smoke"), add a validation
check to verify that the parsed result contains channels. If the parseM3U
function returns zero channels (or an empty result), the smoke test should fail
by throwing an error or exiting with a non-zero code. This ensures the smoke
test properly validates the contract expected by the app code in refresh.ts and
doesn't succeed with an invalid empty playlist.
- Around line 15-24: The fetchText function lacks a timeout mechanism for
network requests, which can cause the smoke test to hang indefinitely on stalled
endpoints. Add an AbortController with a bounded timeout to the fetch call in
fetchText. Create an AbortController instance, set up a timeout using setTimeout
that calls abort() after a reasonable duration (e.g., 30 seconds), pass the
abort signal to the fetch options, and handle the AbortError in the catch block
to provide a clear timeout error message. Make sure to clear the timeout after
the response completes to prevent memory leaks.
---
Nitpick comments:
In `@packages/app/src/lib/iptv/m3u.test.ts`:
- Around line 44-71: Add a new regression test case after the existing test
functions in the test suite to verify that group ID collisions are handled
correctly. Create a test that uses a playlist with two distinct group-title
values that normalize to the same slug (for example, "News Group" and
"News-Group" or "tech news" and "tech-news"), then call parseM3U and assert that
the result contains groups with unique group IDs even though the normalized
slugs would collide. This ensures the parseM3U function properly handles slug
collision scenarios by generating distinct identifiers for groups with different
original titles.
In `@packages/app/src/lib/iptv/xmltv.test.ts`:
- Around line 15-73: Add a new test case within the describe("parseXmltv") block
that validates partial recovery behavior when encountering malformed data.
Create a test XML string containing both a programme with invalid start/stop
timestamps and a valid programme, then verify that parseXmltv still successfully
parses the valid programme while gracefully handling the malformed one. This
ensures the parser can recover from partial data corruption and continues
processing valid entries.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6bbc4d26-8937-40b7-b6eb-dcc914eef927
📒 Files selected for processing (27)
apps/desktop/electron/preload.cjsapps/desktop/electron/services/mainIpc.cjspackages/app/src/App.sveltepackages/app/src/components/home/SearchBar.sveltepackages/app/src/lib/iptv/cache.test.tspackages/app/src/lib/iptv/cache.tspackages/app/src/lib/iptv/fetch.tspackages/app/src/lib/iptv/guideGrid.test.tspackages/app/src/lib/iptv/guideGrid.tspackages/app/src/lib/iptv/m3u.test.tspackages/app/src/lib/iptv/m3u.tspackages/app/src/lib/iptv/refresh.test.tspackages/app/src/lib/iptv/refresh.tspackages/app/src/lib/iptv/store.test.tspackages/app/src/lib/iptv/store.tspackages/app/src/lib/iptv/types.tspackages/app/src/lib/iptv/utils.tspackages/app/src/lib/iptv/xmltv.test.tspackages/app/src/lib/iptv/xmltv.tspackages/app/src/lib/stores/router.tspackages/app/src/pages/live/LiveTV.sveltepackages/app/src/pages/player/Player.sveltepackages/app/src/pages/player/components/PlayerLoadingScreen.sveltepackages/app/src/pages/player/playerAnalytics.tspackages/app/src/pages/player/types.tspackages/app/tsconfig.jsonscripts/smoke-iptv-dispatcharr.mjs
|
Hi @cat5edopeHA, thank you for your contribution! Took a quick look so far, a few things stand out: The TV icon in the nav should be opt-in via Settings, not on by default, it's a niche feature compared to everything else up there. The Live TV page needs to match the shell/chrome of the rest of the app: see Home, Library, and Meta as reference. Innovative layout is fine as long as you're reusing the existing design tokens and patterns (modals, posters, player) instead of introducing new ones (unless absolutely necessary), avoid overly excessive AI patterns such as sidebars, cards, missing hierarchy... No judgment if this was built with AI tools, it just means it needs a human pass on the UI specifically to actually match Raffi instead of defaulting to generic patterns. I haven't reviewed the code yet as I want to get the user-facing parts straightened out first before going deeper into the backend, since changes there might shift things around anyway. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/app/src/lib/iptv/cache.ts (1)
265-273: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftAvoid persisting Xtream credentials inside cached channel URLs.
Line 269 stores
result.channelsverbatim. Xtream stream URLs can include plaintext credentials, which then get written tolocalStorageeven though cache metadata is fingerprinted. Please sanitize/redact Xtream channel URLs before persistence (with safe reconstruction on read), or disable Xtream channel caching until that path is safe.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/lib/iptv/cache.ts` around lines 265 - 273, The StoredIptvRefreshResult object stores result.channels directly without sanitizing Xtream stream URLs that may contain plaintext credentials, creating a security risk when persisted to localStorage. Before storing channels in the cached result object, implement URL sanitization logic to redact or remove credentials from Xtream URLs, ensuring credentials are not written to the cache. Add corresponding deserialization logic during cache reads to reconstruct the safe URLs, or alternatively disable channel caching for Xtream sources entirely if sanitization cannot be reliably implemented.
🧹 Nitpick comments (1)
packages/app/src/pages/live/LiveTV.svelte (1)
760-771: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winSource manager modal: Escape won’t close it for keyboard users and focus isn’t trapped.
The
onkeydownEscape handler is bound to the backdropdiv(Lines 766-768). On open, focus stays on the trigger button outside this subtree, so the handler never receives the keydown and Escape does nothing until the backdrop itself is focused. Focus is also not moved into the dialog and not trapped. The visibleXbutton (Lines 787-793) is still tab-reachable, so this is a degradation rather than a hard block.Suggested fix: window-level Escape + focus the dialog on open
+<svelte:window + onkeydown={(event) => { + if (showSourceManager && event.key === "Escape") showSourceManager = false; + }} +/> + {`#if` showSourceManager} <div class="fixed inset-0 z-[220] flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm" onclick={(event) => { if (event.currentTarget === event.target) showSourceManager = false; }} - onkeydown={(event) => { - if (event.key === "Escape") showSourceManager = false; - }} role="button" tabindex="0" > <section class="max-h-[92vh] w-full max-w-[1040px] overflow-y-auto rounded-[28px] border border-white/10 bg-[`#2b2b2b`]/95 p-5 shadow-[0_32px_100px_rgba(0,0,0,0.48)] backdrop-blur-xl" role="dialog" aria-modal="true" tabindex="-1" + use:focusOnMount >(
focusOnMount= a tiny action that callsnode.focus(); pair with a focus trap for full keyboard support.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/live/LiveTV.svelte` around lines 760 - 771, The Escape key handler attached to the backdrop div element (in the onkeydown binding) won't work because focus remains on the trigger button outside the modal, so the div never receives keydown events. Move the Escape key handling to a window-level listener instead, and add focus management to automatically focus the modal dialog when showSourceManager becomes true. You can use an action like focusOnMount that calls node.focus() when the modal element mounts, ensuring keyboard navigation begins inside the dialog rather than on the external trigger button.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/src/pages/live/LiveTV.svelte`:
- Around line 656-756: The channel guide currently renders all entries in the
guideRows array as DOM nodes, which causes performance issues with large
playlists. Every 60 seconds when guideNow updates, it triggers a full
recomputation of guideViewport and rebuilds the entire guideRows array, forcing
a complete re-render of all channel rows. Implement virtualization/windowing for
the channel rail (the section iterating over guideRows with the channel logos)
and the timeline rows section (the section iterating over guideRows with the
programmes) to only render the visible channels within the viewport.
Alternatively, implement pagination by capping the number of visibleChannels
rendered in guideRows rather than rendering all channels, so the re-render cost
is bounded by the viewport size instead of total channel count.
---
Outside diff comments:
In `@packages/app/src/lib/iptv/cache.ts`:
- Around line 265-273: The StoredIptvRefreshResult object stores result.channels
directly without sanitizing Xtream stream URLs that may contain plaintext
credentials, creating a security risk when persisted to localStorage. Before
storing channels in the cached result object, implement URL sanitization logic
to redact or remove credentials from Xtream URLs, ensuring credentials are not
written to the cache. Add corresponding deserialization logic during cache reads
to reconstruct the safe URLs, or alternatively disable channel caching for
Xtream sources entirely if sanitization cannot be reliably implemented.
---
Nitpick comments:
In `@packages/app/src/pages/live/LiveTV.svelte`:
- Around line 760-771: The Escape key handler attached to the backdrop div
element (in the onkeydown binding) won't work because focus remains on the
trigger button outside the modal, so the div never receives keydown events. Move
the Escape key handling to a window-level listener instead, and add focus
management to automatically focus the modal dialog when showSourceManager
becomes true. You can use an action like focusOnMount that calls node.focus()
when the modal element mounts, ensuring keyboard navigation begins inside the
dialog rather than on the external trigger button.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9b79eb1b-b546-4539-aab6-256c0cb72f9a
📒 Files selected for processing (24)
apps/desktop/electron/services/mainIpc.cjspackages/app/src/components/home/SearchBar.sveltepackages/app/src/components/home/modals/settings/PreferencesSection.sveltepackages/app/src/lib/home/searchBarSettings.test.tspackages/app/src/lib/home/searchBarSettings.tspackages/app/src/lib/iptv/cache.test.tspackages/app/src/lib/iptv/cache.tspackages/app/src/lib/iptv/fetch.test.tspackages/app/src/lib/iptv/fetch.tspackages/app/src/lib/iptv/guideGrid.test.tspackages/app/src/lib/iptv/guideGrid.tspackages/app/src/lib/iptv/m3u.test.tspackages/app/src/lib/iptv/m3u.tspackages/app/src/lib/iptv/refresh.test.tspackages/app/src/lib/iptv/refresh.tspackages/app/src/lib/iptv/store.test.tspackages/app/src/lib/iptv/store.tspackages/app/src/lib/iptv/types.tspackages/app/src/lib/iptv/xmltv.test.tspackages/app/src/lib/iptv/xmltv.tspackages/app/src/lib/iptv/xtream.tspackages/app/src/pages/live/LiveTV.sveltepackages/app/src/pages/player/Player.sveltescripts/smoke-iptv-dispatcharr.mjs
✅ Files skipped from review due to trivial changes (1)
- packages/app/src/lib/iptv/xtream.ts
🚧 Files skipped from review as they are similar to previous changes (10)
- packages/app/src/lib/iptv/xmltv.test.ts
- packages/app/src/lib/iptv/m3u.test.ts
- packages/app/src/lib/iptv/refresh.ts
- apps/desktop/electron/services/mainIpc.cjs
- packages/app/src/lib/iptv/m3u.ts
- scripts/smoke-iptv-dispatcharr.mjs
- packages/app/src/lib/iptv/fetch.ts
- packages/app/src/lib/iptv/store.ts
- packages/app/src/lib/iptv/xmltv.ts
- packages/app/src/pages/player/Player.svelte
|
Hi, reviewed the new changes, we're still not quite there. A few specific asks:
If you think, of course, that it would be better to have the search or selectors displayed in another way I'm open to that too, but you must use at least the left side (raffi icon) and right side (navigation buttons, though addons etc could be hidden on the TV page, in other words the navigation can be dynamic as long as it looks like it's part of the same identity) On the code side: keep IPTV-specific logic out of general helper files, it'll hurt maintainability long-term. Also try to keep files under ~500-600 lines, split them into components/helpers the way other page folders already do, you can use those as reference for the pattern. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/app/src/pages/live/LiveTV.svelte (1)
152-157: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReset the channel query when hydrating a different source.
hydrateStoredSource()clears the group filter but keepssearchQuery. If I switch to an unrefreshed source, the old query stays applied while the input is disabled, and the newly refreshed source can immediately render the"no-results"state for the stale filter.Also applies to: 274-280
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/live/LiveTV.svelte` around lines 152 - 157, `hydrateStoredSource()` in LiveTV.svelte resets the selected group but leaves the existing channel search filter in place, which can carry a stale query into a newly hydrated source. Update the source hydration flow so that when `hydrateStoredSource()` switches to a different source, it also clears `searchQuery` alongside `selectedGroup`, keeping the query state in sync with the disabled input and avoiding an immediate no-results view. Make the same reset in the other source-hydration path referenced by the review so both code paths behave consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/src/components/home/SearchBar.svelte`:
- Around line 763-766: The Play button in SearchBar.svelte has the wrong
accessible name and is currently announced as “addons” even though it triggers
openPlayModal. Update the button’s aria-label (or equivalent accessible text
source) in the relevant button markup so screen readers announce the correct
Play action, using the button definition tied to openPlayModal.
In `@packages/app/src/pages/live/components/LiveGuide.svelte`:
- Around line 83-91: The channel logo error handling in LiveGuide.svelte hides
the broken <img> but never shows the Tv fallback, leaving empty tiles. Update
the row.channel.logo rendering branch so the image failure path in the LiveGuide
component switches to the existing fallback icon/UI instead of only setting
display:none on the image, preserving a visible tile when the logo URL is
invalid.
---
Outside diff comments:
In `@packages/app/src/pages/live/LiveTV.svelte`:
- Around line 152-157: `hydrateStoredSource()` in LiveTV.svelte resets the
selected group but leaves the existing channel search filter in place, which can
carry a stale query into a newly hydrated source. Update the source hydration
flow so that when `hydrateStoredSource()` switches to a different source, it
also clears `searchQuery` alongside `selectedGroup`, keeping the query state in
sync with the disabled input and avoiding an immediate no-results view. Make the
same reset in the other source-hydration path referenced by the review so both
code paths behave consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ba3a3a9c-2454-4dd2-8ed6-afa5dcf7a8d0
📒 Files selected for processing (8)
packages/app/src/components/home/SearchBar.sveltepackages/app/src/pages/live/LiveTV.sveltepackages/app/src/pages/live/components/LiveEmptyState.sveltepackages/app/src/pages/live/components/LiveGroupFilter.sveltepackages/app/src/pages/live/components/LiveGuide.sveltepackages/app/src/pages/live/components/LiveSourceManager.sveltepackages/app/src/pages/live/components/LiveSourceSelector.sveltepackages/app/src/pages/live/liveHelpers.ts
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/src/lib/iptv/cache.ts`:
- Around line 297-303: The `serializeGuide` function builds
`programmesByChannel` from a plain `{}`, which allows feed-controlled
`channelId` values like `__proto__` or `constructor` to hit special object slots
instead of normal keys. Change the `programmesByChannel` accumulator to use a
null-prototype object so arbitrary channel IDs are stored safely as data. Keep
the rest of the serialization logic unchanged, and make sure the
`StoredXmltvGuide` shape still receives the same entries from
`guide.programmesByChannel`.
In `@packages/app/src/pages/live/liveHelpers.ts`:
- Around line 26-30: The Live TV selection state is missing per-source
last-stream persistence, so the restored selection cannot bring back the
previously active channel. Update LiveTvSelection in liveHelpers.ts to include a
per-source last-channel field alongside sourceId, groupsBySourceId, and
favoritesBySourceId, then make sure the selection hydration/persistence path
reads and writes that field together with the existing state. Use the
LiveTvSelection type and the related load/save logic that consumes it to keep
the selected group and stream restored between visits.
- Around line 214-225: The channel filtering logic in getVisibleChannels should
treat any persisted group value that is not recognized as ALL_GROUPS, because
restored selectedGroup values can become stale when providers rename or remove
groups. Update the selection/group normalization path before or within
getVisibleChannels so unknown non-favorites groups fall back to ALL_GROUPS
instead of comparing against channel.group and filtering everything out. Use the
existing group checks in getVisibleChannels and the selectedGroup restoration
flow to ensure only valid group names are preserved.
In `@packages/app/src/pages/player/videoSession.ts`:
- Around line 559-560: The Hls error handler in videoSession should not log the
raw `data` object because it may expose sensitive IPTV request URLs and
credentials. Update the `hls.on(Hls.Events.ERROR, ...)` handler to log only
sanitized, non-sensitive fields from the hls.js error payload, and avoid
printing request context, full URLs, or nested objects that may contain secrets.
- Around line 570-575: The native HLS fallback branch in videoSession should not
return null because cleanupSession() needs a handle to clear startTimeout and
remove the native media listeners if the player unmounts early. Update the logic
around the videoElem.canPlayType("application/vnd.apple.mpegurl") path to return
a cleanup function or session handle that unregisters handleNativeReady from
loadedmetadata/canplay and clears any timeout, so stale player error state
cannot fire after navigation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bcb08de0-b65c-44a5-9c6d-8663e80b5158
📒 Files selected for processing (13)
packages/app/src/components/home/SearchBar.sveltepackages/app/src/lib/iptv/cache.test.tspackages/app/src/lib/iptv/cache.tspackages/app/src/pages/live/LiveTV.sveltepackages/app/src/pages/live/components/LiveChannelLogo.sveltepackages/app/src/pages/live/components/LiveGroupFilter.sveltepackages/app/src/pages/live/components/LiveGuide.sveltepackages/app/src/pages/live/components/LiveSourceManager.sveltepackages/app/src/pages/live/liveHelpers.test.tspackages/app/src/pages/live/liveHelpers.tspackages/app/src/pages/player/Player.sveltepackages/app/src/pages/player/playerSessionLoader.tspackages/app/src/pages/player/videoSession.ts
✅ Files skipped from review due to trivial changes (1)
- packages/app/src/pages/live/components/LiveChannelLogo.svelte
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/app/src/pages/live/components/LiveSourceManager.svelte
- packages/app/src/pages/live/LiveTV.svelte
- packages/app/src/components/home/SearchBar.svelte
|
Hello again:
|
|
Can you give me the free playlist you have that is failing? I can’t reproduce the failure |
Sure, https://github.com/free-tv/iptv (the url is https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8) Obviously it's likely horrible in terms of quality but VLC does handle it (most of it) so I think we should be able to too. |
|
Hi, I just wanted to check in on this PR, if you need any help please say so I can help implement it! |
|
I’m still working on it, didn’t want to keep spamming updates |
|
I dug into the Free-TV playlist playback failures a bit more, especially the random favorites that appear to fail before the player can do anything useful. This looks like an upstream playlist/content issue rather than something this PR should try to special-case in Raffi. A lot of entries in
VLC is not a perfect comparison here because it has a much broader media stack and can sometimes resolve or tolerate things Raffi intentionally does not handle yet: webpage-like entries, redirects, odd TLS/server behavior, broken MIME types, and other provider quirks. Raffi's Live TV path is currently expecting a direct playable stream URL/manifest, not a general-purpose web video extractor. |
|
Alright that's okay then you can ignore it, you should still address the remaining items:
|
|
CodeRabbit review-body items are handled in current head
Validation run on the pushed head: |
|
This is looking much better overall! A couple things I’d still change and check before merging:
Also, I don’t think the Live TV page should visually select the last played channel as soon as you open it. That feels a bit odd because the user hasn’t actually clicked/started that channel in the current visit yet. Remembering the last channel is useful, but maybe only use it for resume/open behavior if we add that later, not as the active selected state on page load. One question too: Xtream is exposed as a first-class source type, but the cache layer intentionally does not persist Xtream refresh results. Is that intentional? If Xtream is meant to ship in this PR, I’d like to make sure we’re okay with those sources not restoring refreshed channel/guide state between visits the way M3U sources do. After these are resolved, I think we can merge! |






Summary
Adds native IPTV / M3U Live TV support with XMLTV guide data and a TV-guide-style EPG grid.
Highlights
Verification
bun test packages/app/src/lib/iptv/guideGrid.test.ts packages/app/src/lib/iptv/*.test.tsbun run check:appbun run check:desktopbun run build:desktopSummary by CodeRabbit