Final route consolidation

This commit is contained in:
Nirvana
2026-08-29 19:17:27 +02:00
parent 7988e4f1aa
commit 948cb07b7f
2 changed files with 56 additions and 150 deletions
+15 -96
View File
@@ -19,12 +19,11 @@ helpers:
Returns the fetched DRMConfig list (or [] on failure) so callers that
also need the configs don't have to fetch them a second time.
_resolve_stream(content_type, provider, content_id, ...)
_resolve_stream_unified(content_type, provider, content_id, ...)
The single place that understands how to turn (type, provider, id) into a
manifest response — redirect, proxied rewrite, or decrypted rewrite.
_resolve_decrypted_stream(content_type, provider, content_id, ...)
Returns a manifest with server-side decryption (ClearKey keys injected).
receiver_side selects who decrypts (client vs. server); every content
type's route handler calls this directly with an explicit value.
"""
import base64
@@ -33,7 +32,7 @@ import re
import time
from urllib.parse import urljoin
from bottle import HTTPResponse, redirect, request, response
from bottle import redirect, request, response
from streaming_providers.base.utils import logger
from streaming_providers.base.utils.manifest_utils import ManifestUtils
@@ -252,8 +251,8 @@ def make_helpers(manager, service):
Returns:
The list of DRMConfig objects that were fetched (or [] on failure).
Callers that also need the raw configs (e.g. _resolve_stream) can
reuse this instead of fetching them again.
Callers that also need the raw configs (e.g. _resolve_stream_unified)
can reuse this instead of fetching them again.
"""
try:
if is_catchup and content_type == CONTENT_TYPE_CHANNEL:
@@ -418,10 +417,11 @@ def make_helpers(manager, service):
doesn't support catchup or the window has been exceeded; None if valid.
Extracted from channels.py's _handle_channel_stream so
_resolve_decrypted_stream can apply the same check — previously it
skipped this validation entirely, letting catchup requests outside the
provider's DVR window through to the decrypted-stream path when the
standard path would reject them with a 400.
_resolve_stream_unified can apply the same check for every
receiver_side value — previously the decrypted-stream path skipped
this validation entirely, letting catchup requests outside the
provider's DVR window through when the standard path would reject
them with a 400.
"""
channels = manager.get_channels(provider_name=provider, fetch_manifests=False)
channel_obj = next((c for c in channels if c.channel_id == content_id), None)
@@ -534,8 +534,10 @@ def make_helpers(manager, service):
"""
Single resolver for every stream endpoint — replaces the former
_resolve_stream / _resolve_decrypted_stream split (and the later
mode="auto"/"playable" split). Both wrapper names still exist below
for existing call sites.
mode="auto"/"playable" split before that). Every content type's
route handler (channels.py, events.py, vod.py, recordings.py) calls
this directly with an explicit receiver_side; the two wrapper
functions this replaced have been removed.
Routing is entirely data-driven: whenever content has ClearKey DRM and
a media proxy is configured, the proxy is used to inject correct
@@ -857,87 +859,6 @@ def make_helpers(manager, service):
)
}
def _resolve_stream(
content_type: str,
provider: str,
content_id: str,
country=None,
is_catchup: bool = False,
start_time: int = None,
end_time: int = None,
epg_id: str = None,
drm_variant: str = "auto",
no_proxy: bool = False,
):
"""
Deprecated: thin wrapper around _resolve_stream_unified, kept so
existing call sites (channels.py, events.py, vod.py, recordings.py)
don't need to change. New code should call _resolve_stream_unified
directly with an explicit receiver_side.
"""
return _resolve_stream_unified(
content_type, provider, content_id,
country=country,
is_catchup=is_catchup,
start_time=start_time,
end_time=end_time,
epg_id=epg_id,
drm_variant=drm_variant,
receiver_side=True,
no_proxy=no_proxy,
)
def _resolve_decrypted_stream(
content_type: str,
provider: str,
content_id: str,
highest_quality_only: bool = False,
):
"""
Deprecated: thin wrapper around _resolve_stream_unified, kept so
existing call sites (channels.py's /stream/proxied/ routes) don't need
to change. Reads start_time/end_time/epg_id/country from the request
query string itself, matching the original function's contract — those
routes never passed catchup args explicitly. New code should call
_resolve_stream_unified directly with receiver_side=False.
"""
try:
country = request.query.get("country")
start_time = request.query.get("start_time")
end_time = request.query.get("end_time")
epg_id = request.query.get("epg_id")
is_catchup = bool(start_time and end_time and content_type == CONTENT_TYPE_CHANNEL)
if is_catchup:
try:
start_time = int(start_time)
end_time = int(end_time)
except (ValueError, TypeError):
response.status = 400
return {"error": "Invalid start_time or end_time format"}
return _resolve_stream_unified(
content_type, provider, content_id,
country=country,
is_catchup=is_catchup,
start_time=start_time,
end_time=end_time,
epg_id=epg_id,
receiver_side=False,
highest_quality_only=highest_quality_only,
)
except HTTPResponse:
raise
except ValueError as e:
logger.error(f"API Error in decrypted {content_type} stream: {e}")
response.status = 404
return {"error": str(e)}
except Exception as e:
logger.error(f"API Error in decrypted {content_type} stream: {e}")
response.status = 500
return {"error": f"Internal server error: {str(e)}"}
# Return all helpers as a dict for submodules to use
return {
"CONTENT_TYPE_CHANNEL": CONTENT_TYPE_CHANNEL,
@@ -951,8 +872,6 @@ def make_helpers(manager, service):
"_stream_needs_headers": _stream_needs_headers,
"_inject_base_url": _inject_base_url,
"_validate_catchup_window": _validate_catchup_window,
"_resolve_stream": _resolve_stream,
"_resolve_decrypted_stream": _resolve_decrypted_stream,
"_resolve_stream_unified": _resolve_stream_unified,
"_serialize_drm_configs": _serialize_drm_configs,
}
+41 -54
View File
@@ -836,6 +836,42 @@ class UltimateService:
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/{stream_path}"
return header + f"{stream_url}\n"
def _iter_m3u_provider_channels(self, providers_to_process):
"""
Yield (provider_name, provider_label, channels) for each provider in
providers_to_process — sorted channels, with provider_label resolved
(falling back to provider_name on lookup failure).
Extracted from the four _generate_m3u_* generators, which duplicated
this exact prefix. Structurally identical to what each of them did
inline: channels-fetch and provider_label lookup share one
try/except Exception (log-and-skip-this-provider), with the label
lookup nested inside on its own narrower
(AttributeError, KeyError, ValueError) fallback — same nesting as
before, just here once instead of four times. Each caller still
wraps its own per-channel body in its own try/except Exception,
log-and-continue-to-next-provider, exactly as it did before this
was split out — this generator doesn't change where or how
failures are caught, only removes the duplicated lookup code ahead
of that point.
"""
for provider_name in providers_to_process:
try:
channels = self._sort_channels(
self.manager.get_channels(provider_name=provider_name, fetch_manifests=False)
)
try:
provider_instance = self.manager.get_provider(provider_name)
provider_label = provider_instance.provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
except Exception as provider_err:
logger.warning(
f"Failed to process provider '{provider_name}': {str(provider_err)}"
)
continue
yield provider_name, provider_label, channels
def _generate_m3u_proxied_fast(self, providers=None):
"""
Fast generation of decrypted M3U content for specified providers.
@@ -873,20 +909,8 @@ class UltimateService:
channels_included = 0
for provider_name in providers_to_process:
for provider_name, provider_label, channels in self._iter_m3u_provider_channels(providers_to_process):
try:
# Get channels for this provider
channels = self._sort_channels(
self.manager.get_channels(provider_name=provider_name, fetch_manifests=False)
)
# Get provider label
try:
provider_instance = self.manager.get_provider(provider_name)
provider_label = provider_instance.provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
# Process each channel - no DRM checks (all channels are
# already routed through the media proxy at playback time)
for channel in channels:
@@ -963,20 +987,8 @@ class UltimateService:
channels_included = 0
for provider_name in providers_to_process:
for provider_name, provider_label, channels in self._iter_m3u_provider_channels(providers_to_process):
try:
# Get channels for this provider
channels = self._sort_channels(
self.manager.get_channels(provider_name=provider_name, fetch_manifests=False)
)
# Get provider label
try:
provider_instance = self.manager.get_provider(provider_name)
provider_label = provider_instance.provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
# Process each channel - no DRM checks
for channel in channels:
channel_id = channel.channel_id
@@ -1075,20 +1087,8 @@ class UltimateService:
channels_included = 0
channels_skipped = 0
for provider_name in providers_to_process:
for provider_name, provider_label, channels in self._iter_m3u_provider_channels(providers_to_process):
try:
# Get channels for this provider
channels = self._sort_channels(
self.manager.get_channels(provider_name=provider_name, fetch_manifests=False)
)
# Get provider instance for label
try:
provider_instance = self.manager.get_provider(provider_name)
provider_label = provider_instance.provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
# Process each channel
for channel in channels:
channel_id = channel.channel_id
@@ -1228,21 +1228,8 @@ class UltimateService:
# every entry here would silently mismatch its own KODIPROP directives.
stream_path = "stream/index.mpd?client_drm=true&no_proxy=true" if no_proxy else "stream/index.mpd?client_drm=true"
for provider_name in providers_to_process:
for provider_name, provider_label, channels in self._iter_m3u_provider_channels(providers_to_process):
try:
# Get channels for this provider
channels = self._sort_channels(
self.manager.get_channels(provider_name=provider_name, fetch_manifests=False)
)
# Resolve provider_label once per provider instead of once per
# channel (previously done inside _generate_m3u_channel_entry
# on every call — same result, redundant lookups).
try:
provider_label = self.manager.get_provider(provider_name).provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
# Add each channel to M3U
for channel in channels:
m3u_content += self._generate_m3u_entry(
@@ -1253,7 +1240,7 @@ class UltimateService:
except Exception as provider_err:
logger.warning(
f"Failed to get channels for provider '{provider_name}': {str(provider_err)}"
f"Failed to process provider '{provider_name}': {str(provider_err)}"
)
continue