diff --git a/CHANGELOG.md b/CHANGELOG.md index 5248ed3a..251ee799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `DraggableRow` (stream rows inside the expanded channel) is now wrapped in `React.memo` with a comparator on `row.original` identity and `index`, so rows whose stream data and position haven't changed are skipped entirely during re-renders caused by a sibling row moving. - Removed dead code in the `name` column cell: `playlists[stream.m3u_account]?.name` was indexing an array by an integer ID, which always returns `undefined`, the value was immediately overridden by `m3uAccountsMap`. The dead access and its fallback variable are gone; `m3uAccountsMap` is now the sole lookup. - Removed spurious `state: { data }` from the `useReactTable` call. `data` is a root-level TanStack Table option, not a controlled-state entry; passing it in `state` was a no-op but misleading. + - Adding streams to a channel (per-row "Add to Channel" button and bulk "Add selected to Channel") now completes in a single PATCH request instead of three. Previously each operation called `API.updateChannel` (which internally triggered `requeryStreams`), then `requeryChannels()`. Both paths now use a dedicated `API.addStreamsToChannel()` method that issues only the PATCH, merges the existing channel streams with the newly added stream objects locally, and updates the store in-place, no `requeryStreams` or `requeryChannels` round-trips. + - Removed an unnecessary `requeryStreams()` call from `API.createChannelFromStream()`. Stream data does not change when a channel is created from it; the caller already calls `requeryChannels()` to display the new channel. - Eliminated repeated DB queries in the `ts_proxy` hot path. `StreamManager`, `StreamGenerator`, and `ProxyServer` were each calling `Channel.objects.get(uuid=...)` on every retry, reconnect, failover, and buffering event solely to retrieve `channel.name` for log events. `StreamManager` and `StreamGenerator` now fetch the channel name once at construction via a lightweight `values_list` query and store it as `self.channel_name`. `ProxyServer` caches the name in a `_channel_names` dict keyed by channel ID at channel-start time and pops it at channel-stop time. (Fixes #1138) ### Changed diff --git a/frontend/src/api.js b/frontend/src/api.js index aa15864b..761cb259 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -638,7 +638,7 @@ export default class API { } /** - * Lightweight stream reorder: PATCHes only the stream order for a channel + * PATCHes only the stream order for a channel * without triggering requeryStreams or requeryChannels. The caller is * responsible for optimistic UI updates. */ @@ -666,6 +666,48 @@ export default class API { } } + /** + * PATCHes the channel with the + * combined stream list and updates the channelsTable store in-place + * using the stream objects the caller already has. Skips requeryStreams + * (stream data doesn't change) and requeryChannels (we build the + * result locally). + * + * @param {number} channelId + * @param {Array} existingStreams - current channel.streams (full objects) + * @param {Array} newStreams - stream objects to append + */ + static async addStreamsToChannel(channelId, existingStreams, newStreams) { + try { + const existing = existingStreams || []; + // Deduplicate by ID, preserving order (existing first, new appended) + const seen = new Set(existing.map((s) => s.id)); + const merged = [...existing]; + for (const s of newStreams) { + if (!seen.has(s.id)) { + seen.add(s.id); + merged.push(s); + } + } + + await request(`${host}/api/channels/channels/${channelId}/`, { + method: 'PATCH', + body: { id: channelId, streams: merged.map((s) => s.id) }, + }); + + // Update the channelsTable store in-place with the merged streams + const store = useChannelsTableStore.getState(); + const channel = store.channels.find((c) => c.id === channelId); + if (channel) { + store.updateChannel({ ...channel, streams: merged }); + } + } catch (e) { + errorNotification('Failed to add streams to channel', e); + // On failure, requery to restore correct state + await API.requeryChannels(); + } + } + static async updateChannels(ids, values) { const body = []; for (const id of ids) { @@ -898,7 +940,6 @@ export default class API { useChannelsStore.getState().addChannel(response); } - await API.requeryStreams(); return response; } catch (e) { errorNotification('Failed to create channel', e); diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index a164b885..0197631b 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -89,15 +89,9 @@ const StreamRowActions = ({ ); const addStreamToChannel = async () => { - await API.updateChannel({ - id: targetChannelId, - streams: [ - ...new Set( - channelSelectionStreams.map((s) => s.id).concat([row.original.id]) - ), - ], - }); - await API.requeryChannels(); + await API.addStreamsToChannel(targetChannelId, channelSelectionStreams, [ + row.original, + ]); }; const onEdit = useCallback(() => { @@ -1040,15 +1034,14 @@ const StreamsTable = ({ onReady }) => { }; const addStreamsToChannel = async () => { - await API.updateChannel({ - id: targetChannelId, - streams: [ - ...new Set( - channelSelectionStreams.map((s) => s.id).concat(selectedStreamIds) - ), - ], - }); - await API.requeryChannels(); + // Look up full stream objects from the current page data + const selectedIdSet = new Set(selectedStreamIds); + const newStreams = data.filter((s) => selectedIdSet.has(s.id)); + await API.addStreamsToChannel( + targetChannelId, + channelSelectionStreams, + newStreams + ); }; const onRowSelectionChange = (updatedIds) => {