Skip to content

Fix: VOD provider data handling and episode deduplication (#556, #569)#4

Open
cmc0619 wants to merge 6 commits into
mainfrom
claude/vod-provider-fixes-clean-01XhVh1SQBWk6RBt5HXp612c
Open

Fix: VOD provider data handling and episode deduplication (#556, #569)#4
cmc0619 wants to merge 6 commits into
mainfrom
claude/vod-provider-fixes-clean-01XhVh1SQBWk6RBt5HXp612c

Conversation

@cmc0619

@cmc0619 cmc0619 commented Dec 7, 2025

Copy link
Copy Markdown
Owner

Summary

Comprehensive fixes for VOD provider data handling issues, resolving duplicate key constraint violations, NULL value crashes, and malformed data imports.

Issues Resolved

Major Changes

1. Episode Deduplication

  • One Episode object with multiple M3UEpisodeRelation objects for duplicate streams
  • Batch-level tracking prevents duplicate key violations
  • Proper PK resolution after bulk_create with ignore_conflicts

2. NULL/Empty Name Handling

  • Changed from .get('name', 'Unknown') to .get('name') or 'MovieNameNull'
  • Handles both missing keys AND falsy values
  • Applied to Movie, Series, Episode names

3. Malformed Episode Data

  • Handles providers returning episodes as list instead of dict
  • Extracts season numbers from individual episode data
  • Fixed 337 series that had 0 episodes

4. Bulk Update Fixes

  • Maps both relations_to_create AND relations_to_update to saved DB objects
  • Prevents "bulk_update() prohibited" errors

5. VOD Proxy Improvements

  • Distinguishes provider failures from systemic errors in failover
  • Centralized Redis client usage (Docker compatibility)
  • Proper Http404 propagation

Files Modified

  • apps/vod/tasks.py - Core deduplication, NULL handling, safe_int
  • apps/output/views.py - Episode lookup via M3UEpisodeRelation
  • apps/proxy/vod_proxy/views.py - Exception handling, Redis client
  • apps/proxy/vod_proxy/connection_manager.py - Redis client
  • Documentation: FIXES_DETAILED.md, EPISODE_DEDUP_FIX.md, PR_DESCRIPTION.md

Testing

Verify episode deduplication:

SELECT episode_id, COUNT(*) 
FROM vod_m3uepisoderelation 
GROUP BY episode_id 
HAVING COUNT(*) > 1;

Check NULL handling:

SELECT name FROM vod_movie WHERE name LIKE '%NameNull';

Backward Compatibility

✅ No database migrations required
✅ No API changes
✅ No configuration changes
✅ Works with existing data


Clean atomic commits - easy to cherry-pick individual fixes

Summary by CodeRabbit

  • New Features

    • Added automatic failover mechanism for streaming—attempts multiple stream sources if the primary source fails.
    • Introduced stream comparison tool to identify duplicate vs. different quality stream variants.
  • Bug Fixes

    • Fixed duplicate episode creation and bulk update errors.
    • Improved handling of malformed episode data and missing provider names.
    • Enhanced episode lookup to use highest-priority stream account.
  • Improvements

    • Better logging for batch processing progress and stream candidate selection.
    • Reduced memory usage through improved cleanup routines.

✏️ Tip: You can customize this high-level summary in your review settings.

…r#556, Dispatcharr#569)

Fixes multiple critical issues with VOD provider data import and playback:

Issue Dispatcharr#556 - Duplicate key constraint violations during VOD refresh:
- Episodes from providers with multiple quality streams (same episode, different stream_ids)
  now create ONE Episode object with multiple M3UEpisodeRelation objects
- Added batch_episodes dict to deduplicate episodes within batch by (series_id, season, episode)
- Fixed PK resolution after bulk_create with ignore_conflicts=True by re-fetching all episodes
- Properly map both relations_to_create AND relations_to_update to resolved Episode PKs

Issue Dispatcharr#569 - MultipleObjectsReturned errors during episode playback:
- Updated XC series stream lookup (apps/output/views.py) to resolve episodes via M3UEpisodeRelation
- Filters by stream_id and orders by account priority, supporting deduplicated episode model

NULL/empty name handling:
- Changed from .get('name', 'Unknown') to .get('name') or 'MovieNameNull'
- Prevents NOT NULL violations and distinguishes between missing vs explicitly empty names
- Applied to Movie, Series, and Episode name fields

Malformed episode data handling:
- Added support for providers returning episodes as list instead of dict-by-season
- Falls back to extracting season numbers from individual episode data
- Logs warning when malformed structure detected

Safe integer conversions:
- Added safe_int() helper to handle None, empty strings, and non-numeric values
- Applied to season_number and episode_number parsing to prevent ValueError crashes
- Logs warnings for malformed data while continuing processing

Bulk update fixes:
- Extended Movie/Series/Episode relations_to_update mapping to reference saved DB objects
- Prevents "bulk_update() prohibited due to unsaved related object" errors
- Applies ID mapping before both bulk_create and bulk_update operations

Scoped deletion:
- Changed refresh_series_episodes to delete M3UEpisodeRelation instead of Episode objects
- Supports cross-account episode sharing while handling provider deletions

Technical Details:
- apps/vod/tasks.py: batch_process_episodes, process_movie_batch, process_series_batch
- apps/output/views.py: xc_series_stream lookup via M3UEpisodeRelation

Impact:
- Fixes 337 series that previously had 0 episodes due to malformed provider data
- Prevents database crashes during VOD refresh with multiple providers
- Enables proper playback of episodes from multiple provider sources
Exception Handling Improvements:
- Distinguish provider/network failures from systemic errors in stream failover logic
- Provider failures (RequestException, Timeout, ConnectionError, HTTPError) trigger automatic
  failover to next stream candidate with warning log
- Systemic errors (Redis down, DB errors, code bugs) fail immediately with full traceback
  to prevent wasting resources trying all streams when infrastructure is down
- Applied to both GET and HEAD methods in VOD stream failover loops

Redis Client Centralization:
- Replace hardcoded redis.StrictRedis(host='localhost', port=6379) with RedisClient.get_client()
- Ensures compatibility with containerized deployments (redis:6379 vs localhost:6379)
- Uses centralized configuration from environment variables with retry logic and health checks
- Applied to views.py HEAD method (content length storage) and connection_manager.py (retrieval)

Technical Details:
- apps/proxy/vod_proxy/views.py: Exception handling in lines 233-245 (GET), 428-439 (HEAD)
- apps/proxy/vod_proxy/views.py: RedisClient usage in line 393 (HEAD content length storage)
- apps/proxy/vod_proxy/connection_manager.py: RedisClient usage in line 99 (content length retrieval)

Impact:
- Prevents full stream iteration when infrastructure issues exist (faster failure, clearer errors)
- Enables deployment in Docker/Kubernetes environments with non-localhost Redis
- Provides better error visibility for debugging with targeted logging
Provides detailed documentation of all VOD provider fixes including:
- Problem descriptions with actual error messages
- Root cause analysis for each issue
- Step-by-step failure scenarios
- Complete fix explanations with code examples
- Testing procedures
- Impact analysis

File: FIXES_DETAILED.md (573 lines)

Note: This documentation file can be excluded from merge if only source code changes are desired.
Focused documentation on the episode deduplication solution:
- Explains the "One Episode, Many Relations" model
- Details the deduplication algorithm
- Provides testing procedures and example queries

File: EPISODE_DEDUP_FIX.md

Note: This documentation file can be excluded from merge if only source code changes are desired.
Summary of all fixes with examples and testing steps.
Useful as a pull request description or release notes.

File: PR_DESCRIPTION.md

Note: This documentation file can be excluded from merge if only source code changes are desired.
Prevents swallowing Django's Http404 exceptions which lose error details.
Http404 now propagates properly while other exceptions are logged and return None.

Addresses code review feedback about proper exception handling in helper methods.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Dec 7, 2025

Copy link
Copy Markdown

Walkthrough

Comprehensive refactor of the VOD task workflow introducing episode deduplication via batch tracking, improved handling for malformed/NULL provider data, bulk operation fixes with proper ID mapping, memory cleanup routines, and updated streaming logic with ranked account/stream selection and failover support across multiple proxy and output components.

Changes

Cohort / File(s) Summary
Episode Deduplication & Data Handling
apps/vod/tasks.py
Introduces batch-level deduplication with episodes_to_update_set and batch_episodes to prevent duplicate episode creation/updates. Adds malformed episode data handling (dict vs. list formats), NULL/empty name fallbacks (MovieNameNull, SeriesNameNull, EpisodeNameNull), bulk update fixes with proper ID mapping for newly created objects, memory cleanup post-batch, logo processing, and comprehensive data validation utilities.
Stream Selection & Ranking
apps/proxy/vod_proxy/views.py
Refactors GET/HEAD flows to fetch content objects and compute ranked M3U relations based on preferred stream, preferred account, account priority, and ID. Implements multi-candidate failover iteration with per-candidate URL retrieval, profile selection, and streaming attempts. Adds helper methods _get_content_object() and _get_ranked_relations() with enhanced logging at each selection step.
Episode Relation Lookup
apps/output/views.py
Updates xc_series_stream to filter episode relations by stream_id (external provider ID) instead of episode_id, prioritizing active accounts and selecting highest-priority account via ordering.
Redis Client Management
apps/proxy/vod_proxy/connection_manager.py
Replaces direct Redis import with RedisClient.get_client() retrieval in _establish_connection, with guard to raise if Redis is unavailable.
Documentation & Specifications
EPISODE_DEDUP_FIX.md, FIXES_DETAILED.md, PR_DESCRIPTION.md
Detailed documentation of episode deduplication mechanism, malformed data handling, NULL name strategies, bulk operation fixes, memory cleanup, stream comparison tool refactor, testing procedures, and backward compatibility notes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant VODProxy as VOD Proxy (GET)
    participant ContentLookup as Content Lookup
    participant RelationRanking as Relation Ranking
    participant M3UAccount as M3U Account Selection
    participant StreamURL as Stream URL Builder
    participant Provider as Provider Stream
    participant Redis

    Client->>VODProxy: GET /stream/{content_type}/{content_id}
    VODProxy->>ContentLookup: _get_content_object(content_type, content_id)
    ContentLookup-->>VODProxy: content_obj
    
    VODProxy->>RelationRanking: _get_ranked_relations(content_obj)
    RelationRanking-->>VODProxy: [relation₁, relation₂, relation₃...]
    
    loop For each ranked relation (failover)
        VODProxy->>M3UAccount: Select account from relation
        M3UAccount-->>VODProxy: m3u_account
        
        VODProxy->>StreamURL: _get_stream_url_from_relation(relation)
        StreamURL->>M3UAccount: Get M3U profile
        M3UAccount-->>StreamURL: profile
        StreamURL->>StreamURL: _transform_url(raw_url, profile)
        StreamURL-->>VODProxy: transformed_url
        
        VODProxy->>Provider: Attempt stream (HEAD/GET)
        alt Stream Available
            Provider-->>VODProxy: 200/206 + headers
            VODProxy->>Redis: Cache content_length
            Redis-->>VODProxy: OK
            VODProxy-->>Client: Stream response
        else Provider/Network Error
            Provider-->>VODProxy: Error
            Note over VODProxy: Continue to next candidate
        else Systemic Error
            VODProxy->>VODProxy: Abort loop
        end
    end
    
    alt All candidates failed
        VODProxy-->>Client: 503 Service Unavailable
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • apps/vod/tasks.py: Dense logic with multiple interacting deduplication, data normalization, bulk operation, and memory management patterns requiring careful verification of edge cases (malformed data, ID collision resolution, relation remapping post-bulk-create).
  • apps/proxy/vod_proxy/views.py: Significant control-flow restructuring introducing multi-candidate ranking and failover iteration across GET/HEAD paths; verify correct ordering logic, error handling, and candidate exhaustion scenarios.
  • Episode relation lookup change in apps/output/views.py: Verify stream_id-based filtering logic and account-priority ordering produce expected highest-priority account selection.
  • Redis client acquisition change in connection_manager.py: Ensure guard condition properly raises when Redis unavailable and does not mask configuration errors.

Possibly related PRs

Poem

🐰 Batches sorted, episodes deduped clean,
Malformed data now handled (gracefully seen),
Ranked streams failover with purpose and care,
One Episode per series, relations we share!
Memory springs free, the Redis gleams bright—
VOD workflows now robust, day and night! 🌟

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing VOD provider data handling and introducing episode deduplication, with specific issue references.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/vod-provider-fixes-clean-01XhVh1SQBWk6RBt5HXp612c

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (15)
apps/proxy/vod_proxy/connection_manager.py (1)

99-101: Use explicit branch instead of raising generic Exception for missing Redis

The RedisClient.get_client() call and fallback behavior are fine, but using raise Exception("Redis not available") just to hit the except and fall back to response headers is a bit noisy.

You could simplify control flow and keep logs cleaner by branching directly:

-                try:
-                    r = RedisClient.get_client()
-                    if not r:
-                        raise Exception("Redis not available")
+                try:
+                    r = RedisClient.get_client()
+                    if not r:
+                        logger.warning(f"[{self.session_id}] Redis not available, falling back to response content-length")
+                        self.content_length = response.headers.get('content-length')
+                        logger.info(f"[{self.session_id}] *** USING RESPONSE CONTENT LENGTH: {self.content_length} ***")
+                        # Skip Redis lookup entirely
+                        raise StopIteration  # or `return` from the try-block context

(or equivalent early‑return pattern without introducing a new exception type).

Not urgent, but it would make intent clearer and avoid using a bare Exception for normal control flow.

PR_DESCRIPTION.md (1)

1-152: Add explicit languages to all fenced code blocks for markdownlint compliance

The documentation is clear and detailed. markdownlint is still flagging a few fenced code blocks without a language (MD040). For consistency with the blocks already tagged as python/bash, consider adding a language to the remaining fences (even text is fine) so editors and renderers can apply proper highlighting and linters go quiet.

FIXES_DETAILED.md (1)

27-531: Tag remaining fenced code blocks with a language

This deep-dive doc is very helpful. markdownlint is still complaining about a few raw fenced blocks (error snippets, log excerpts, sample output) without a language spec (MD040). To keep CI/lint noise down and improve readability in editors, it’s worth tagging those as something like:

```text
<error/output snippet>

while keeping `python`/`bash` for actual code.

</blockquote></details>
<details>
<summary>apps/vod/tasks.py (4)</summary><blockquote>

`658-661`: **Unnecessary code path for `relations_to_update`.**

The `relations_to_update` list contains existing relations from `existing_relations` dict (line 584), which are already fetched from the database with valid PKs. The `created_movies` dict only contains movies that were just created via `bulk_create`. Existing relations already reference existing movies (not newly created ones), so this loop will never find a match.

This code is harmless but misleading. Consider adding a clarifying comment or removing if truly unreachable.

---

`995-998`: **Same observation as movies - relations_to_update referencing newly created series is unlikely.**

Same logic applies here: `relations_to_update` contains existing relations that already reference existing series objects. However, this is harmless defensive code.

---

`1536-1538`: **Memory cleanup is good practice but `del` may not be necessary.**

The explicit `clear()` calls and `del` statements help with garbage collection in low-memory environments. However, since these are local variables, they would be garbage collected when the function returns anyway.

The `del` statement on line 1538 will raise `NameError` if the earlier code paths set `valid_relations` instead of these original variable names (since they're reassigned/filtered).


This cleanup is defensive and acceptable for memory-constrained environments. Consider wrapping in try-except if you want to be extra safe:

```python
# Explicit cleanup to help garbage collection in low-memory environments
try:
    batch_episodes.clear()
    episodes_to_update_set.clear()
except:
    pass

1253-1291: Robust handling of malformed provider data formats.

Good defensive code handling both dict (normal) and list (malformed) episode data formats. The safe_int helper and warning logs are helpful for debugging provider issues.

One minor observation: if episodes_data is a list and episodes don't have season_number or season fields, they'll all default to season 0. Multiple episodes without season information but sharing the same episode number will be deduplicated into a single episode (see batch_episodes tracking at lines 1324-1326). This is intentionally logged ("Reusing episode from batch"), but worth monitoring if providers commonly omit season data, as stream relations from distinct episodes could incorrectly link to the same episode object.

EPISODE_DEDUP_FIX.md (1)

45-78: Line number references will become stale.

The documentation references specific line numbers (e.g., "Lines 1198-1204", "Lines 1235-1299") which will drift as the code evolves. Consider using function names or code patterns instead of line numbers for more maintainable documentation.

Replace line number references with function/section descriptions:

-**1. Added Batch Tracking** (Lines 1198-1204)
+**1. Added Batch Tracking** (in `batch_process_episodes()`)
apps/proxy/vod_proxy/views.py (7)

202-202: Unused variable current_connections.

As flagged by static analysis, current_connections from the profile result tuple is never used.

-                    m3u_profile, current_connections = profile_result
+                    m3u_profile, _ = profile_result

339-339: Same unused variable in HEAD handler.

-                    m3u_profile, current_connections = profile_result
+                    m3u_profile, _ = profile_result

376-378: Replace bare except with specific exception handling.

As flagged by static analysis, bare except: catches all exceptions including KeyboardInterrupt and SystemExit.

                         try:
                             total_size = content_range.split('/')[-1]
-                        except:
+                        except (IndexError, ValueError):
                             total_size = response.headers.get('Content-Length', '0')

381-381: Unnecessary f-string prefix.

The log message has no placeholders.

-                            logger.warning(f"[VOD-HEAD] No Content-Range header in 206 response")
+                            logger.warning("[VOD-HEAD] No Content-Range header in 206 response")

392-401: Good Redis integration for session state, but consider error handling granularity.

The Redis client retrieval and error handling is appropriate. However, raising a generic Exception("Redis not available") could be improved.

Consider a more specific exception or graceful degradation:

                     try:
                         from core.utils import RedisClient
                         r = RedisClient.get_client()
                         if not r:
-                            raise Exception("Redis not available")
+                            logger.warning("[VOD-HEAD] Redis not available, skipping content length caching")
+                        else:
+                            content_length_key = f"vod_content_length:{session_id}"
+                            r.set(content_length_key, total_size, ex=1800)
+                            logger.info(f"[VOD-HEAD] Stored total content length {total_size} for session {session_id}")
-                        content_length_key = f"vod_content_length:{session_id}"
-                        r.set(content_length_key, total_size, ex=1800)
-                        logger.info(f"[VOD-HEAD] Stored total content length {total_size} for session {session_id}")
                     except Exception as e:
                         logger.error(f"[VOD-HEAD] Failed to store content length in Redis: {e}")

451-468: Http404 propagation is correct but could use logging.exception.

The method correctly re-raises Http404 for Django's error handling. However, the generic Exception catch should use logging.exception for better stack traces.

         except Http404:
             raise  # Let 404s propagate to Django's error handling
         except Exception as e:
-            logger.error(f"Error getting content object: {e}")
+            logger.exception(f"Error getting content object: {e}")
             return None

517-518: Use logging.exception for better error context.

As flagged by static analysis:

         except Exception as e:
-            logger.error(f"Error getting ranked relations: {e}")
+            logger.exception(f"Error getting ranked relations: {e}")
             return []
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b19324 and 5474356.

📒 Files selected for processing (7)
  • EPISODE_DEDUP_FIX.md (1 hunks)
  • FIXES_DETAILED.md (1 hunks)
  • PR_DESCRIPTION.md (1 hunks)
  • apps/output/views.py (1 hunks)
  • apps/proxy/vod_proxy/connection_manager.py (1 hunks)
  • apps/proxy/vod_proxy/views.py (2 hunks)
  • apps/vod/tasks.py (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/vod/tasks.py (1)
apps/vod/models.py (2)
  • M3UEpisodeRelation (257-295)
  • Episode (153-187)
apps/proxy/vod_proxy/connection_manager.py (1)
core/utils.py (2)
  • RedisClient (44-194)
  • get_client (49-138)
apps/output/views.py (1)
apps/vod/models.py (1)
  • M3UEpisodeRelation (257-295)
apps/proxy/vod_proxy/views.py (3)
apps/proxy/vod_proxy/multi_worker_connection_manager.py (4)
  • MultiWorkerVODConnectionManager (622-1370)
  • get_instance (628-632)
  • stream_content_with_session (699-1007)
  • infer_content_type_from_url (27-77)
apps/proxy/vod_proxy/connection_manager.py (2)
  • get_instance (285-289)
  • stream_content_with_session (960-1245)
core/utils.py (2)
  • RedisClient (44-194)
  • get_client (49-138)
🪛 markdownlint-cli2 (0.18.1)
PR_DESCRIPTION.md

8-8: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


14-14: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


83-83: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


94-94: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

FIXES_DETAILED.md

27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


179-179: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


268-268: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


274-274: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


486-486: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.7)
apps/proxy/vod_proxy/connection_manager.py

101-101: Abstract raise to an inner function

(TRY301)


101-101: Create your own exception

(TRY002)


101-101: Avoid specifying long messages outside the exception class

(TRY003)

apps/proxy/vod_proxy/views.py

163-163: Abstract raise to an inner function

(TRY301)


163-163: Avoid specifying long messages outside the exception class

(TRY003)


202-202: Unpacked variable current_connections is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


231-231: Consider moving this statement to an else block

(TRY300)


339-339: Unpacked variable current_connections is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


377-377: Do not use bare except

(E722)


381-381: f-string without any placeholders

Remove extraneous f prefix

(F541)


396-396: Abstract raise to an inner function

(TRY301)


396-396: Create your own exception

(TRY002)


396-396: Avoid specifying long messages outside the exception class

(TRY003)


400-400: Do not catch blind exception: Exception

(BLE001)


401-401: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


428-428: Consider moving this statement to an else block

(TRY300)


449-449: Use explicit conversion flag

Replace with conversion flag

(RUF010)


466-466: Do not catch blind exception: Exception

(BLE001)


467-467: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


515-515: Consider moving this statement to an else block

(TRY300)


517-517: Do not catch blind exception: Exception

(BLE001)


518-518: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (13)
apps/output/views.py (1)

2889-2896: Episode lookup by stream_id and priority looks correct

Using M3UEpisodeRelation.objects.filter(stream_id=stream_id, m3u_account__is_active=True).order_by('-m3u_account__priority', 'id').first() cleanly aligns the XC episode stream lookup with the new “one Episode, many relations” model and multi‑account priority selection. The 404 fallback is also appropriate.

I’d just suggest verifying end‑to‑end that the incoming XC stream_id really is the provider stream_id (not the internal episode or relation PK) across all call sites, since this is a behavior change at the API edge.

apps/vod/tasks.py (8)

377-378: LGTM! Good defensive handling for NULL/empty names.

Using movie_data.get('name') or 'MovieNameNull' correctly handles both None and empty string '' cases, preventing database crashes from NULL name constraint violations.


695-696: Consistent NULL/empty name handling for Series.

Good - same defensive pattern applied for series names.


1219-1225: Correctly scoped deletion to M3UEpisodeRelation only.

The comment and implementation correctly preserve Episode objects for cross-account deduplication while removing only the relation entries for the current account. This is essential for the deduplication strategy.


1320-1326: Core deduplication tracking structures properly initialized.

The episodes_to_update_set and batch_episodes dict are the key data structures enabling the deduplication fix. They're correctly initialized within the function scope (not as module-level globals, despite the AI summary mention), which is appropriate for batch-level tracking.


1331-1335: Good NULL handling for episode names and safe episode number conversion.

The pattern episode_data.get('title') or 'EpisodeNameNull' and safe_int() usage are consistent with the movie/series handling.


1359-1423: Episode deduplication logic is correct and well-structured.

The batch-first lookup pattern (batch_episodesexisting_episodes → create new) properly ensures:

  1. Only one Episode object per (series, season, episode) tuple
  2. Updates are queued only once via episodes_to_update_set
  3. All subsequent stream entries reuse the same Episode object

The id(episode) usage for tracking is clever - it distinguishes between different in-memory Episode instances even if they represent the same database record.


1508-1516: Good filtering of relations with unresolved episodes.

The validation r.episode and r.episode.pk before bulk_create prevents integrity errors from relations pointing to unsaved episodes. The warning log helps with debugging.


1456-1499: No issue found with PK resolution after bulk_create.

The code correctly uses relation.episode.series_id to access the integer foreign key ID. Django's ForeignKey field always stores the ID value separately from the related object instance, regardless of whether the Episode is saved. When an Episode is instantiated with series=series_obj (line 59 of context), Django immediately stores the integer ID in memory via the _id accessor. Therefore, relation.episode.series_id will correctly return the integer ID, not the Series object.

EPISODE_DEDUP_FIX.md (1)

1-263: Comprehensive and well-structured documentation.

The documentation clearly explains the problem, solution, verification steps, and backward compatibility. The SQL verification queries and expected log output are particularly helpful for debugging.

apps/proxy/vod_proxy/views.py (3)

159-172: Good refactoring to content-first approach with ranked relations.

The separation of content fetching (_get_content_object) from relation ranking (_get_ranked_relations) improves code organization and enables the multi-candidate failover pattern.


181-249: Well-implemented failover loop with proper error categorization.

The distinction between provider/network errors (continue to next candidate) and systemic errors (fail immediately) is correct. This prevents wasting time on retries when the issue isn't provider-specific.


470-519: Well-designed ranking algorithm with clear priority order.

The ranking logic (preferred stream → preferred account → priority → id) is correct and well-documented. The early return for empty relations is efficient.

One observation: the query remaining_relations.exclude(id=specific.id) creates new querysets each time. For small result sets this is fine, but for large relation counts, consider collecting IDs to exclude in a single query.

Comment thread apps/vod/tasks.py
Comment on lines +1524 to +1525
# Calculate actual relations created (valid_relations if it was set, otherwise relations_to_create)
relations_created_count = len(valid_relations) if 'valid_relations' in locals() else len(relations_to_create)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential NameError if valid_relations not defined.

The condition 'valid_relations' in locals() is a code smell and can fail in edge cases. If episodes_to_create is empty (falsy), the if episodes_to_create: block (line 1459) is skipped, and valid_relations is never assigned.

Consider initializing valid_relations before the conditional block:

+    valid_relations = relations_to_create  # Default to all relations
+
     # Create new episodes with ignore_conflicts to handle race conditions
     # where another account might create the same episode simultaneously
     if episodes_to_create:
         # ... existing code ...
         
         # Filter out any relations that still have unsaved episodes
         valid_relations = [r for r in relations_to_create if r.episode and r.episode.pk]
🤖 Prompt for AI Agents
In apps/vod/tasks.py around lines 1524-1525, the code checks for
'valid_relations' in locals() which can raise a NameError when the earlier block
that assigns valid_relations is skipped; initialize valid_relations as an empty
list (or the appropriate empty collection) before the conditional that populates
it so the later len(...) call can always safely use valid_relations, and then
replace the conditional locals() check with a direct use of valid_relations
(i.e., relations_created_count = len(valid_relations) if valid_relations else
len(relations_to_create) or simply len(valid_relations) depending on intended
semantics).

@coderabbitai

coderabbitai Bot commented Dec 9, 2025

Copy link
Copy Markdown

Warning

Docstrings generation is disabled for your repository or organization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Series VOD Internal Error -- more than one M3UEpisodeRelation [Bug]: VOD Series Episodes Disappear after sync

2 participants