mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-04 06:52:34 +02:00
breaking change: consolidate routes with parameters
This commit is contained in:
+16
-2
@@ -253,7 +253,19 @@ def setup_m3u_routes(app, manager, service):
|
||||
)
|
||||
epg_id = service.get_epg_id(channel_id)
|
||||
epg_id_attr = f' tvg-epgid="{epg_id}"' if epg_id else ""
|
||||
stream_path = "stream/proxied/index.mpd" if proxied else "stream/index.mpd"
|
||||
# /stream/proxied/ no longer exists as a separate route —
|
||||
# folded into client_drm on the single /stream/index.mpd
|
||||
# endpoint. client_drm=false for proxied (matches the
|
||||
# static KODIPROP line below, server decrypts);
|
||||
# client_drm=true otherwise (matches the dynamic
|
||||
# per-channel DRM lookup below, client decrypts) — it
|
||||
# now defaults to false, so this must be explicit or the
|
||||
# non-proxied branch's entries would mismatch their own
|
||||
# KODIPROP directives.
|
||||
stream_path = (
|
||||
"stream/index.mpd?client_drm=false" if proxied
|
||||
else "stream/index.mpd?client_drm=true"
|
||||
)
|
||||
stream_url = (
|
||||
f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/{stream_path}"
|
||||
)
|
||||
@@ -264,7 +276,9 @@ def setup_m3u_routes(app, manager, service):
|
||||
)
|
||||
|
||||
if proxied:
|
||||
m3u_content += "#KODIPROP:inputstream=inputstream.adaptive\n"
|
||||
# No KODIPROP line — client_drm=false, client doesn't
|
||||
# use inputstream.adaptive when the server decrypts.
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
drm_configs = manager.get_channel_drm_configs(provider_name, channel_id)
|
||||
|
||||
+66
-80
@@ -17,15 +17,18 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
CONTENT_TYPE_CHANNEL = helpers["CONTENT_TYPE_CHANNEL"]
|
||||
_build_drm_header = helpers["_build_drm_header"]
|
||||
_build_stream_headers = helpers["_build_stream_headers"]
|
||||
_resolve_stream = helpers["_resolve_stream"]
|
||||
_resolve_decrypted_stream = helpers["_resolve_decrypted_stream"]
|
||||
_resolve_stream_unified = helpers["_resolve_stream_unified"]
|
||||
_get_drm_configs = helpers["_get_drm_configs"]
|
||||
|
||||
def _handle_channel_stream(provider, channel_id, *, drm_variant="auto", no_proxy=False):
|
||||
"""Shared implementation for /stream/index.mpd, /stream/sw-drm/index.mpd,
|
||||
and /stream/noproxy/index.mpd. All three variants get the same catchup
|
||||
window validation and int parsing — no_proxy only changes whether
|
||||
_resolve_stream is told to force a redirect past the media proxy."""
|
||||
def _handle_channel_stream(provider, channel_id):
|
||||
"""Single implementation backing /stream/index.mpd — the only channel
|
||||
stream route. Replaces the former /stream/sw-drm/, /stream/noproxy/,
|
||||
/stream/proxied/, and /stream/proxied/ffmpeg/ routes, each of which
|
||||
used to hardcode one fixed combination of drm_variant/receiver_side/
|
||||
no_proxy/highest_quality_only. Those axes are independent, so folding
|
||||
them into query params makes every combination reachable — including
|
||||
ones no route could express before (e.g. client-side decrypt +
|
||||
highest_quality_only)."""
|
||||
try:
|
||||
start_time = request.query.get("start_time")
|
||||
end_time = request.query.get("end_time")
|
||||
@@ -33,16 +36,25 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
country = request.query.get("country")
|
||||
is_catchup = bool(start_time and end_time)
|
||||
|
||||
drm_variant = request.query.get("drm_variant", "auto")
|
||||
no_proxy = request.query.get("no_proxy", "false").lower() == "true"
|
||||
# client_drm is the public query-param name (self-explanatory in a
|
||||
# URL); receiver_side is what _resolve_stream_unified calls the
|
||||
# same axis internally — translated here at the route boundary.
|
||||
client_drm = request.query.get("client_drm", "false").lower() == "true"
|
||||
highest_quality_only = request.query.get("highest_quality_only", "false").lower() == "true"
|
||||
|
||||
logger.debug(
|
||||
f"_handle_channel_stream: provider={provider} channel={channel_id} "
|
||||
f"start_time={start_time!r} end_time={end_time!r} "
|
||||
f"epg_id={epg_id!r} country={country!r} is_catchup={is_catchup} "
|
||||
f"drm_variant={drm_variant}"
|
||||
f"drm_variant={drm_variant} client_drm={client_drm} "
|
||||
f"no_proxy={no_proxy} highest_quality_only={highest_quality_only}"
|
||||
)
|
||||
|
||||
# Always defined so the _resolve_stream call below is unconditionally safe,
|
||||
# even though the ternary guards already prevent None from being passed when
|
||||
# is_catchup is False.
|
||||
# Always defined so the _resolve_stream_unified call below is
|
||||
# unconditionally safe, even though the ternary guards already
|
||||
# prevent None from being passed when is_catchup is False.
|
||||
start_time_int: int | None = None
|
||||
end_time_int: int | None = None
|
||||
|
||||
@@ -58,13 +70,10 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
response.status = 400
|
||||
return {"error": "Invalid start_time or end_time format"}
|
||||
|
||||
# Window validation (catchup_hours lookup, age check) now lives
|
||||
# inside _resolve_stream_unified via _validate_catchup_window,
|
||||
# shared across every mode (auto/noproxy/decrypt) instead of
|
||||
# being duplicated here and in the decrypted-stream path
|
||||
# separately. No channel lookup needed here anymore.
|
||||
# Window validation (catchup_hours lookup, age check) lives
|
||||
# inside _resolve_stream_unified via _validate_catchup_window.
|
||||
|
||||
return _resolve_stream(
|
||||
return _resolve_stream_unified(
|
||||
CONTENT_TYPE_CHANNEL, provider, channel_id,
|
||||
country=country,
|
||||
is_catchup=is_catchup,
|
||||
@@ -72,19 +81,19 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
end_time=end_time_int if is_catchup else None,
|
||||
epg_id=epg_id if is_catchup else None,
|
||||
drm_variant=drm_variant,
|
||||
receiver_side=client_drm,
|
||||
no_proxy=no_proxy,
|
||||
highest_quality_only=highest_quality_only,
|
||||
)
|
||||
|
||||
except HTTPResponse:
|
||||
raise
|
||||
except ValueError as e:
|
||||
label = "no-proxy " if no_proxy else ("sw-drm " if drm_variant == "software" else "")
|
||||
logger.error(f"{label}stream error for channel {provider}/{channel_id}: {e}")
|
||||
logger.error(f"stream error for channel {provider}/{channel_id}: {e}")
|
||||
response.status = 404
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
label = "no-proxy " if no_proxy else ("sw-drm " if drm_variant == "software" else "")
|
||||
logger.error(f"{label}stream error for channel {provider}/{channel_id}: {e}")
|
||||
logger.error(f"stream error for channel {provider}/{channel_id}: {e}")
|
||||
response.status = 500
|
||||
return {"error": f"Internal server error: {str(e)}"}
|
||||
|
||||
@@ -95,16 +104,15 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
@app.route("/api/providers/<provider>/channels/<channel_id>/manifest")
|
||||
def get_channel_manifest(provider, channel_id):
|
||||
"""
|
||||
Returns JSON with a manifest_url pointing to the stream endpoint.
|
||||
Attaches x-kodi-drm-configs header.
|
||||
Returns JSON with a manifest_url pointing to the single stream
|
||||
endpoint. Attaches x-kodi-drm-configs header.
|
||||
|
||||
Response includes both stream_url (auto DRM) and sw_drm_stream_url
|
||||
(software / ClearKey DRM) so callers can pick the appropriate variant
|
||||
without a second round-trip.
|
||||
|
||||
Also includes catchup_stream_url_template — a URL with {start_time} and
|
||||
{end_time} placeholders (Unix timestamps) that callers can expand for
|
||||
DVR/catchup playback, avoiding the need to construct the URL manually.
|
||||
As of the query-param consolidation, there's one stream_url rather
|
||||
than a separate URL per DRM/proxy/quality combination — the caller
|
||||
appends whatever combination of client_drm/drm_variant/no_proxy/
|
||||
highest_quality_only query params it needs (see get_channel_stream's
|
||||
docstring for the full list). catchup_stream_url_template still
|
||||
carries {start_time}/{end_time} placeholders for DVR/catchup playback.
|
||||
"""
|
||||
try:
|
||||
country = request.query.get("country")
|
||||
@@ -114,14 +122,6 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
f"{base_url}/api/providers/{provider}/channels/{channel_id}"
|
||||
f"/stream/index.mpd{qs}"
|
||||
)
|
||||
sw_drm_stream_url = (
|
||||
f"{base_url}/api/providers/{provider}/channels/{channel_id}"
|
||||
f"/stream/sw-drm/index.mpd{qs}"
|
||||
)
|
||||
noproxy_stream_url = (
|
||||
f"{base_url}/api/providers/{provider}/channels/{channel_id}"
|
||||
f"/stream/noproxy/index.mpd{qs}"
|
||||
)
|
||||
# Catchup template — callers substitute {start_time}/{end_time} with
|
||||
# Unix timestamps. Matches the query params consumed by _handle_channel_stream.
|
||||
catchup_qs_sep = "&" if qs else "?"
|
||||
@@ -138,8 +138,6 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
"provider": provider,
|
||||
"channel_id": channel_id,
|
||||
"manifest_url": stream_url,
|
||||
"sw_drm_manifest_url": sw_drm_stream_url,
|
||||
"noproxy_manifest_url": noproxy_stream_url,
|
||||
"catchup_stream_url_template": catchup_stream_url_template,
|
||||
}
|
||||
|
||||
@@ -154,48 +152,36 @@ def setup_channel_routes(app, manager, service, helpers):
|
||||
|
||||
@app.route("/api/providers/<provider>/channels/<channel_id>/stream/index.mpd")
|
||||
def get_channel_stream(provider, channel_id):
|
||||
"""Returns HTTP 302 redirect to the actual manifest, or a rewritten
|
||||
manifest body when media proxy is active. Supports live and catchup."""
|
||||
"""
|
||||
Single stream endpoint for all channel playback — live and catchup.
|
||||
Replaces the former /stream/sw-drm/, /stream/noproxy/, /stream/proxied/,
|
||||
and /stream/proxied/ffmpeg/ routes, which are removed. Every
|
||||
combination those routes used to hardcode (and some that were
|
||||
previously unreachable through any route) is now expressed via
|
||||
independent, freely-combinable query params:
|
||||
|
||||
client_drm=true|false client decrypts ClearKey itself
|
||||
(receiver-side rewrite) if true;
|
||||
server decrypts to plaintext
|
||||
segments if false. Default false.
|
||||
drm_variant=auto|software which upstream DRM/quality variant
|
||||
to request (e.g. Widevine L1 vs
|
||||
L3). Default auto.
|
||||
no_proxy=true|false force redirect/fetch even if the
|
||||
content would normally be proxied.
|
||||
Default false.
|
||||
highest_quality_only=true|false collapse to a single
|
||||
highest-quality representation
|
||||
(e.g. for ffmpeg piping). Default
|
||||
false.
|
||||
start_time / end_time / epg_id / country - catchup + context, as
|
||||
before.
|
||||
|
||||
Returns an HTTP 302 redirect to the upstream manifest, or a rewritten
|
||||
manifest body when the media proxy is involved.
|
||||
"""
|
||||
return _handle_channel_stream(provider, channel_id)
|
||||
|
||||
@app.route("/api/providers/<provider>/channels/<channel_id>/stream/sw-drm/index.mpd")
|
||||
def get_channel_stream_sw_drm(provider, channel_id):
|
||||
"""Software-DRM variant. Identical transport to the standard
|
||||
endpoint; passes drm_variant='software' through to _resolve_stream."""
|
||||
return _handle_channel_stream(provider, channel_id, drm_variant="software")
|
||||
|
||||
@app.route("/api/providers/<provider>/channels/<channel_id>/stream/noproxy/index.mpd")
|
||||
def get_channel_stream_noproxy(provider, channel_id):
|
||||
"""
|
||||
Returns HTTP 302 redirect to the actual manifest, forcing a bypass of any
|
||||
configured media proxy even if the provider would normally be proxied.
|
||||
Supports live and catchup — reuses _handle_channel_stream so it gets the
|
||||
same catchup window validation, timestamp parsing, and error handling as
|
||||
the standard and sw-drm routes.
|
||||
|
||||
Useful when:
|
||||
- Media proxy is misbehaving
|
||||
- You want to test upstream performance directly
|
||||
- Proxy is not needed for a specific provider
|
||||
"""
|
||||
return _handle_channel_stream(provider, channel_id, no_proxy=True)
|
||||
|
||||
@app.route("/api/providers/<provider>/channels/<channel_id>/stream/proxied/index.mpd")
|
||||
def get_channel_stream_decrypted(provider, channel_id):
|
||||
"""Proxied stream — all quality representations."""
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_CHANNEL, provider, channel_id, highest_quality_only=False
|
||||
)
|
||||
|
||||
@app.route(
|
||||
"/api/providers/<provider>/channels/<channel_id>/stream/proxied/ffmpeg/index.mpd"
|
||||
)
|
||||
def get_channel_stream_decrypted_ffmpeg(provider, channel_id):
|
||||
"""Proxied stream — highest quality only, optimised for ffmpeg."""
|
||||
return _resolve_decrypted_stream(
|
||||
CONTENT_TYPE_CHANNEL, provider, channel_id, highest_quality_only=True
|
||||
)
|
||||
|
||||
@app.route("/api/providers/<provider>/channels/<channel_id>/drm")
|
||||
def get_channel_drm(provider, channel_id):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user