From 6fed444e439e9d98cee68ffe20aa50e7d6192eff Mon Sep 17 00:00:00 2001 From: None <190783071+CodeBormen@users.noreply.github.com> Date: Fri, 1 May 2026 13:45:46 -0500 Subject: [PATCH] feat: Auto-sync overhaul (FR #1196) Per-field channel overrides for auto-synced channels, hide-from-output flag, range-bounded auto-numbering with re-pack, multi-stream channel safety, multi-provider shared-range merging, and an across-the-board move from per-row sync writes to bulk operations. Migrations: apps/channels: 0036_channeloverride_and_user_hidden, 0037_backfill_auto_created_by_null, 0038_channelgroupm3uaccount_auto_sync_channel_end, 0039_channel_channel_number_nullable apps/m3u: 0020_m3uaccount_auto_cleanup_unused_channels See CHANGELOG.md for the full commit log --- CHANGELOG.md | 18 + apps/channels/api_views.py | 625 +++- apps/channels/compact_numbering.py | 363 +++ apps/channels/managers.py | 55 + .../0037_channeloverride_and_user_hidden.py | 40 + .../0038_backfill_auto_created_by_null.py | 71 + ...elgroupm3uaccount_auto_sync_channel_end.py | 18 + .../0040_channel_channel_number_nullable.py | 64 + apps/channels/models.py | 149 +- apps/channels/serializers.py | 400 ++- apps/channels/signals.py | 133 +- apps/channels/tests/test_channel_api.py | 132 +- apps/channels/utils.py | 12 + apps/hdhr/api_views.py | 32 +- apps/hdhr/views.py | 30 +- apps/m3u/api_views.py | 224 ++ ...m3uaccount_auto_cleanup_unused_channels.py | 18 + apps/m3u/models.py | 9 + apps/m3u/serializers.py | 15 + apps/m3u/tasks.py | 1124 +++++--- apps/m3u/tests/test_account_destroy.py | 143 + apps/m3u/tests/test_sync_compound.py | 240 ++ apps/m3u/tests/test_sync_correctness.py | 1973 +++++++++++++ apps/output/views.py | 343 ++- frontend/src/api.js | 92 +- .../src/components/M3URefreshNotification.jsx | 156 +- .../__tests__/M3URefreshNotification.test.jsx | 137 + frontend/src/components/forms/Channel.jsx | 279 +- .../src/components/forms/ChannelBatch.jsx | 113 +- .../components/forms/GroupConfigureModal.jsx | 38 + .../src/components/forms/LiveGroupFilter.jsx | 2524 ++++++++++------- frontend/src/components/forms/Logo.jsx | 4 + frontend/src/components/forms/M3U.jsx | 40 +- .../src/components/forms/M3UGroupFilter.jsx | 19 +- .../forms/__tests__/Channel.test.jsx | 135 +- .../forms/__tests__/ChannelBatch.test.jsx | 121 +- .../src/components/tables/ChannelsTable.jsx | 84 +- .../ChannelsTable/ChannelTableHeader.jsx | 56 +- .../tables/ChannelsTable/EditableCell.jsx | 109 +- .../__tests__/EditableCell.test.jsx | 82 + frontend/src/components/tables/M3UsTable.jsx | 75 +- frontend/src/utils/forms/ChannelBatchUtils.js | 50 + frontend/src/utils/forms/ChannelUtils.js | 215 +- frontend/src/utils/forms/GroupSyncUtils.js | 70 + ...nnelBatchUtils.bulkOverrideRouting.test.js | 105 + .../forms/__tests__/ChannelUtils.test.js | 340 +++ .../forms/__tests__/GroupSyncUtils.test.js | 178 ++ 47 files changed, 9509 insertions(+), 1744 deletions(-) create mode 100644 apps/channels/compact_numbering.py create mode 100644 apps/channels/managers.py create mode 100644 apps/channels/migrations/0037_channeloverride_and_user_hidden.py create mode 100644 apps/channels/migrations/0038_backfill_auto_created_by_null.py create mode 100644 apps/channels/migrations/0039_channelgroupm3uaccount_auto_sync_channel_end.py create mode 100644 apps/channels/migrations/0040_channel_channel_number_nullable.py create mode 100644 apps/m3u/migrations/0020_m3uaccount_auto_cleanup_unused_channels.py create mode 100644 apps/m3u/tests/test_account_destroy.py create mode 100644 apps/m3u/tests/test_sync_compound.py create mode 100644 apps/m3u/tests/test_sync_correctness.py create mode 100644 frontend/src/components/forms/GroupConfigureModal.jsx create mode 100644 frontend/src/components/tables/ChannelsTable/__tests__/EditableCell.test.jsx create mode 100644 frontend/src/utils/forms/GroupSyncUtils.js create mode 100644 frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js create mode 100644 frontend/src/utils/forms/__tests__/GroupSyncUtils.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c94de6f..aa89dc43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Plugin discovery re-running on every connect event**. The `PluginManager` cache hit check used `if self._registry and ...`, which always evaluated false when zero plugins were installed because an empty dict is falsy. Every Connect event (`recording_start`, `client_connect`, `channel_start`, etc.) was therefore triggering a full filesystem walk of `/data/plugins` and emitting `Discovering plugins (no DB sync) in /data/plugins` / `Discovered 0 plugin(s)` log lines. Tracked separately via a new `_discovery_completed` flag so the cache short-circuits subsequent calls regardless of registry contents, dropping discovery to once per worker process lifetime. - **Empty show/season folders left behind after deleting a recording**. Deleting a recording removes the MKV (or HLS working directory if still in progress) but previously left the parent show / season directories on disk even when they no longer contained any other recordings. After file cleanup, `RecordingViewSet.destroy` now walks up from the deleted file's parent directory and removes any now-empty directories, stopping at the `/data/recordings` library root so the root itself is never touched. - **Premature DVR concat on graceful Celery worker shutdown**. A `worker_shutting_down` signal handler in `apps/channels/tasks.py` now sets a module-level `_DVR_SHUTTING_DOWN` flag. The `run_recording` loop checks this flag after FFmpeg exits and, if the recording's `end_time` has not yet passed, skips concatenation and persists `status="interrupted"` with `interrupted_reason="server_shutdown"`. The HLS working directory is preserved across the restart so the recovery path on the next worker startup can resume segment numbering from where it left off rather than truncating the show. +- **Bulk channel edit silently failed on API rejection.** The bulk-edit submit handler in `ChannelBatch.jsx` only wrote `console.error` when the PATCH was rejected; the user saw the spinner stop but received no notification, leading them to assume the save succeeded. The catch block now surfaces a red toast with the server-provided detail (or a generic fallback), and the form stays open with the in-progress selection intact so the user can correct and retry. - **Channel start/stop notifications missing name**: the "channel started" toast never showed up and the "channel stopped" toast showed the raw UUID instead of the channel name. `channel_name` is now written into the Redis metadata hash at init time and included in every stats payload pushed over WebSocket; the frontend notification functions read `ch.channel_name` from the stats payload directly, falling back to the store lookup and then a formatted placeholder. Both notifications now display the correct channel name. - **Channel form reset on group creation**: creating a new channel group from within the channel create/edit form no longer wipes all filled-in form fields. The newly created group is also automatically selected in the channel group field after it is saved. (Fixes #545) - **EPG channel name truncation**: EPG sources that include long `` values (e.g. event-based channels with descriptions appended to the name) would crash the channel-parse task with a `value too long for type character varying(255)` PostgreSQL error and silently discard the entire batch. The `EPGData.name`, `Stream.name`, and `Channel.name` fields have been widened to 512 characters, and names exceeding this limit are now truncated with a warning log rather than aborting the import. (Fixes #1134) @@ -26,6 +27,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Auto-sync overhaul (FR #1196)**: comprehensive rebuild of the M3U auto-channel-sync flow. The work introduces a per-field override system, hide-from-output flag, range-bounded auto-numbering with a re-pack helper, multi-stream channel safety, multi-provider shared-range merging, and an across-the-board move from per-row writes to bulk operations. Migrations included: `apps/channels/0037_channeloverride_and_user_hidden`, `0038_backfill_auto_created_by_null`, `0039_channelgroupm3uaccount_auto_sync_channel_end`, `0040_channel_channel_number_nullable`, and `apps/m3u/0020_m3uaccount_auto_cleanup_unused_channels`. + - **Per-field channel overrides for auto-synced channels.** A new `ChannelOverride` table (one-to-one with `Channel`) holds user-specified values for `name`, `channel_number`, `channel_group`, `logo`, `tvg_id`, `tvc_guide_stationid`, `epg_data`, and `stream_profile`. Sync writes only to `Channel.*` columns; the override row takes precedence at read time via a new `with_effective_values()` queryset helper that coalesces both sources at the SQL layer. Every output surface that consumes channel data, HDHR (`/hdhr/lineup.json` and per-profile variants, `/hdhr/discover.json`), M3U (`/output/m3u`, per-profile variants), EPG (`/output/epg`, per-profile variants), XC API (`get_live_streams`, `xmltv.php`), the channels list and detail endpoints, and the TV Guide summary endpoint (`GET /api/channels/channels/summary/`), reads through this helper so overrides surface consistently. The channel edit form shows "Provider: " subtext beneath each overridable field, and the channels table flags rows with active overrides via a Pencil icon and tooltip listing the overridden field names. + - **Per-field reset-to-provider icon.** FK pickers (channel group, logo, EPG, stream profile) display a small reset icon when the field has an active override on an auto-synced channel. Clicking it sets the form value back to the provider value; the override for that field is cleared on save while other overrides remain intact. + - **Hide-from-output flag (`user_hidden`).** A user-toggleable boolean that excludes a channel from HDHR, M3U, EPG, and XC output without deleting it. Hidden channels are preserved across auto-sync refreshes and excluded from auto-cleanup. The channels table shows an EyeOff icon on hidden rows and accepts a "Hidden" filter in the visibility selector. + - **Auto-sync channel ranges per group.** `ChannelGroupM3UAccount` accepts `auto_sync_channel_start` and `auto_sync_channel_end`. Channels created by auto-sync are assigned numbers within the configured range; streams that do not fit are surfaced in the failure detail modal with a typed "range exhausted" reason. + - **Compact numbering with re-pack helper.** When `compact_numbering=True` on a group's account relation, sync packs visible auto-sync channels sequentially into the configured range. A "Re-pack now" button on the group settings runs the pack manually. Hidden channels do not occupy slots; un-hiding shifts visible numbers down to fill the gap. Override-pinned channel numbers are respected as fixed anchors. + - **Multi-stream channel safety in sync.** A user-attached second stream on an auto-created channel no longer causes the channel to be deleted when one of those streams disappears from the provider. The deletion path filters out channels where at least one current stream remains alive, and per-stream iteration mutates the same `Channel` instance so `bulk_update` writes the merged final state. + - **Auto-cleanup unused channels toggle on M3U accounts.** A new boolean controls whether sync deletes auto-created channels that no longer match any stream. Off by default; users with manual curation workflows can opt in. + - **Failure modal grouped by reason.** The auto-sync completion notification's failure detail modal groups entries by typed reason (range exhausted, parse error, FK integrity, etc.) with collapsible sections. The in-memory cap was raised from 50 to 1000 so realistic multi-provider failure sets are not truncated. + - **Multi-provider shared-range merging.** Two M3U accounts can target the same group with overlapping channel-number ranges. Sync seeds `used_numbers` globally so the second provider's channels pick up where the first left off without colliding. A warning surfaces in the group-settings UI when ranges overlap; the merge is intentional, the warning is informational. + - **"Has Overrides" filter on the channels table.** A new toggle in the table header shows only channels that currently have an active override row. + - **Selection summary in the bulk edit modal.** The bulk edit form shows `Selection: X auto-synced, Y manual` at the top so users can see how a single set of changes will route between override rows and direct writes. + - **Custom `Channel.objects` manager.** A thin manager wraps the existing `with_effective_values()` helper so new code can call `Channel.objects.with_effective_values(...)` directly. The module-level helper remains the canonical implementation; existing call sites are unchanged. - **In-progress recording playback from the DVR page**. The Watch button on a recording card is now enabled while the recording is still in progress, and the in-app floating player can play the live HLS playlist with full timeshift / scrub-back to the start of the recording. - The frontend now bundles `hls.js` and routes any `.m3u8` URL through it (Chrome, Edge, Firefox, and other Chromium-based browsers). Native HLS via `