Issue: When `process_movie_batch` / `process_series_batch`
(apps/vod/tasks.py) creates duplicate vod_movie / vod_episode records
during a refresh, existing M3U*Relation rows get repointed to the new
records. The old UUIDs that external players (Emby / Jellyfin /
ChannelsDVR) cached in `.strm` URLs are left orphaned, and the proxy
404s on every subsequent request — even though the same request
already carries a stable `stream_id` that uniquely identifies a live
relation.
This patch makes `_get_content_and_relation` use a soft UUID lookup
and, if it misses, fall back to resolving content via
`M3UMovieRelation.stream_id` / `M3UEpisodeRelation.stream_id`. The
fallback respects the strictest match first: when
`preferred_m3u_account_id` is also present, that account is queried
first; otherwise the highest-priority active relation matching the
stream_id wins (same ordering the existing relation-selection logic
uses).
This is read-side graceful degradation, complementary to the
write-side root fix proposed in closed#973 (re-use the relation's
existing movie/series during refresh rather than rematching by TMDB
ID). The two PRs together would fully resolve#961: #973 prevents new
orphaning, this PR makes any URL that ships a stream_id survive past
orphaning. URLs without a stream_id (e.g. XC-compat
`/movie/<user>/<pass>/<id>.mkv`) are NOT covered by this patch — they
need the root fix.
Series-UUID lookups (`/proxy/vod/series/<UUID>`) are left UUID-only by
design — players cache episode and movie URLs, not series URLs. Same
pattern can be added as a follow-up if needed.
Tests: 7 new mock-based regression tests covering both branches,
verified against `:dev`. UUID-first happy path is unchanged
(M3U*Relation table never queried when the UUID resolves).
Related: #961 (open), closed#973 (root fix abandoned for inactivity).
- Fix socket leak in local IP detection using try/finally; replace 8.8.8.8
with RFC 5737 test address 203.0.113.1
- Validate ipify response with ipaddress.ip_address() before using in URL
- Push ip_lookup_complete WebSocket event when background lookup finishes,
eliminating frontend polling entirely
- Show Skeleton placeholder in sidebar while IP lookup is pending
- Replace hover-to-reveal blur with click-to-toggle for mobile support
- Fix copy button propagating click to blur toggle via stopPropagation
- Add missing city field to null-env fallback in settings store
- Add setEnvironmentFields() store action for WebSocket-driven updates
- Remove unused imports from api_views.py
- Replace per-entry ProgramData DB query with single batch fetch
- Pass pre-loaded EPGData objects to find_current_program_for_tvg_id
to avoid redundant select_related re-fetch
- Delete Redis programme index before XMLTV download so stale byte
offsets are never used during the refresh window
When selecting an EPG channel in the channel form, if that EPG entry's
programs had never been parsed, the preview showed "No current program".
This was because parse_programs_for_tvg_id requires a linked channel.
- Add force parameter to parse_programs_for_tvg_id to bypass channel check
- Auto-trigger parsing from current-programs API when no programs exist
- Check Redis task lock to prevent duplicate parse tasks from concurrent requests
- Frontend retries up to 3 times with 10s delay to pick up parsed results
- Skip parsing for dummy EPG sources
- Extract ProgramPreview component from duplicated code in Channel.jsx
and StreamConnectionCard.jsx (~80+ lines each)
- Fix watch('epg_data_id') called directly in useEffect dependency array,
which could cause infinite re-renders
- Add request cancellation for rapid EPG selection changes in channel form
- Remove unused imports (Radio, Progress, ChevronDown, ChevronRight)
- Extended CurrentProgramsAPIView to accept epg_data_ids parameter
- Added getCurrentProgramForEpg() API method in frontend
- Added UI in Channel form showing current program with tooltip
- Shows loading state, no program state, and program details
- Includes expandable description, progress bar with elapsed/remaining time
- Matches stats page 'Now Playing' feature behavior