mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Add VOD failover logic for M3U relations
Implemented VOD failover logic to iterate over all active M3U relations, selecting the first account with spare capacity instead of relying on a single highest-priority relation. This change enhances reliability by allowing for provider failover in case of account saturation.
This commit is contained in:
parent
136fb81d41
commit
502ca5bfc8
1 changed files with 87 additions and 25 deletions
|
|
@ -226,6 +226,42 @@ def _get_content_and_relation(content_type, content_id, preferred_m3u_account_id
|
|||
logger.error(f"Error getting content object: {e}")
|
||||
return None, None
|
||||
|
||||
def _get_all_relations_ordered(content_obj, content_type, preferred_relation=None):
|
||||
"""Return ALL active M3U relations for the content, ordered by account
|
||||
priority, with the already-selected preferred_relation first.
|
||||
|
||||
This enables provider failover: stream_vod/head_vod iterate over these
|
||||
relations and use the first whose account has spare capacity, instead of
|
||||
giving up after a single saturated account (the upstream behaviour).
|
||||
"""
|
||||
try:
|
||||
if content_type == 'series':
|
||||
# series resolves to its first episode's relations
|
||||
episode = content_obj.episodes.first() if hasattr(content_obj, 'episodes') else None
|
||||
rel_owner = episode if episode is not None else content_obj
|
||||
else:
|
||||
rel_owner = content_obj
|
||||
|
||||
if not hasattr(rel_owner, 'm3u_relations'):
|
||||
return [preferred_relation] if preferred_relation else []
|
||||
|
||||
relations = list(
|
||||
rel_owner.m3u_relations
|
||||
.filter(m3u_account__is_active=True)
|
||||
.select_related('m3u_account')
|
||||
.order_by('-m3u_account__priority', 'id')
|
||||
)
|
||||
|
||||
if preferred_relation is not None:
|
||||
# Put the preferred relation first, drop duplicates
|
||||
relations = [preferred_relation] + [
|
||||
r for r in relations if r.id != preferred_relation.id
|
||||
]
|
||||
return relations
|
||||
except Exception as e:
|
||||
logger.error(f"[VOD-FAILOVER] Error collecting relations: {e}")
|
||||
return [preferred_relation] if preferred_relation else []
|
||||
|
||||
def _get_stream_url_from_relation(relation):
|
||||
"""Get stream URL from the M3U relation"""
|
||||
try:
|
||||
|
|
@ -567,25 +603,41 @@ def stream_vod(request, content_type, content_id, session_id=None, profile_id=No
|
|||
|
||||
logger.info(f"[VOD-CONTENT] Found content: {getattr(content_obj, 'name', 'Unknown')}")
|
||||
|
||||
# Get M3U account from relation
|
||||
m3u_account = relation.m3u_account
|
||||
logger.info(f"[VOD-ACCOUNT] Using M3U account: {m3u_account.name}")
|
||||
# --- VOD FAILOVER PATCH ---
|
||||
# Upstream uses only the single highest-priority relation and returns
|
||||
# 503 if that account is saturated. Instead, iterate over ALL relations
|
||||
# (preferred first, then by priority) and use the first account that
|
||||
# has spare capacity.
|
||||
candidate_relations = _get_all_relations_ordered(content_obj, content_type, relation)
|
||||
|
||||
# Get stream URL from relation
|
||||
stream_url = _get_stream_url_from_relation(relation)
|
||||
logger.info(f"[VOD-CONTENT] Content URL: {stream_url or 'No URL found'}")
|
||||
m3u_account = None
|
||||
stream_url = None
|
||||
profile_result = None
|
||||
for cand in candidate_relations:
|
||||
cand_account = cand.m3u_account
|
||||
cand_url = _get_stream_url_from_relation(cand)
|
||||
if not cand_url:
|
||||
logger.warning(f"[VOD-FAILOVER] No URL for relation on account {cand_account.name}, skipping")
|
||||
continue
|
||||
cand_profile = _get_m3u_profile(cand_account, profile_id, session_id)
|
||||
if cand_profile and cand_profile[0]:
|
||||
relation = cand
|
||||
m3u_account = cand_account
|
||||
stream_url = cand_url
|
||||
profile_result = cand_profile
|
||||
logger.info(f"[VOD-FAILOVER] Selected account {cand_account.name} (priority {cand_account.priority})")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"[VOD-FAILOVER] Account {cand_account.name} at capacity, trying next provider")
|
||||
|
||||
if not stream_url:
|
||||
logger.error(f"[VOD-ERROR] No stream URL available for {content_type} {content_id}")
|
||||
return HttpResponse("No stream URL available", status=503)
|
||||
|
||||
# Get M3U profile (returns profile and current connection count)
|
||||
profile_result = _get_m3u_profile(m3u_account, profile_id, session_id)
|
||||
|
||||
if not profile_result or not profile_result[0]:
|
||||
logger.error(f"[VOD-ERROR] No suitable M3U profile found for {content_type} {content_id}")
|
||||
if not m3u_account or not stream_url or not profile_result or not profile_result[0]:
|
||||
logger.error(f"[VOD-ERROR] No available provider with capacity for {content_type} {content_id}")
|
||||
return HttpResponse("No available stream", status=503)
|
||||
|
||||
logger.info(f"[VOD-ACCOUNT] Using M3U account: {m3u_account.name}")
|
||||
logger.info(f"[VOD-CONTENT] Content URL: {stream_url or 'No URL found'}")
|
||||
# --- END VOD FAILOVER PATCH ---
|
||||
|
||||
m3u_profile, current_connections = profile_result
|
||||
logger.info(f"[VOD-PROFILE] Using M3U profile: {m3u_profile.id} (max_streams: {m3u_profile.max_streams}, current: {current_connections})")
|
||||
|
||||
|
|
@ -686,20 +738,30 @@ def head_vod(request, content_type, content_id, session_id=None, profile_id=None
|
|||
logger.error(f"[VOD-HEAD] Content or relation not found: {content_type} {content_id}")
|
||||
return HttpResponse("Content not found", status=404)
|
||||
|
||||
# Get M3U account and stream URL
|
||||
m3u_account = relation.m3u_account
|
||||
stream_url = _get_stream_url_from_relation(relation)
|
||||
if not stream_url:
|
||||
logger.error(f"[VOD-HEAD] No stream URL available for {content_type} {content_id}")
|
||||
return HttpResponse("No stream URL available", status=503)
|
||||
# --- VOD FAILOVER PATCH (HEAD) ---
|
||||
candidate_relations = _get_all_relations_ordered(content_obj, content_type, relation)
|
||||
m3u_account = None
|
||||
stream_url = None
|
||||
profile_result = None
|
||||
for cand in candidate_relations:
|
||||
cand_account = cand.m3u_account
|
||||
cand_url = _get_stream_url_from_relation(cand)
|
||||
if not cand_url:
|
||||
continue
|
||||
cand_profile = _get_m3u_profile(cand_account, profile_id, session_id)
|
||||
if cand_profile and cand_profile[0]:
|
||||
relation = cand
|
||||
m3u_account = cand_account
|
||||
stream_url = cand_url
|
||||
profile_result = cand_profile
|
||||
break
|
||||
|
||||
# Get M3U profile (returns profile and current connection count)
|
||||
profile_result = _get_m3u_profile(m3u_account, profile_id, session_id)
|
||||
if not profile_result or not profile_result[0]:
|
||||
logger.error(f"[VOD-HEAD] No M3U profile found or all profiles at capacity")
|
||||
if not m3u_account or not stream_url or not profile_result or not profile_result[0]:
|
||||
logger.error(f"[VOD-HEAD] No available provider with capacity for {content_type} {content_id}")
|
||||
return HttpResponse("No available stream", status=503)
|
||||
|
||||
m3u_profile, current_connections = profile_result
|
||||
# --- END VOD FAILOVER PATCH (HEAD) ---
|
||||
|
||||
# Transform URL if needed
|
||||
final_stream_url = _transform_url(stream_url, m3u_profile)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue