mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-07 00:12:29 +02:00
add raw route
This commit is contained in:
@@ -27,6 +27,7 @@ FALLBACK_WARN_INTERVAL_SECONDS = 60 * 60
|
||||
# visible to clients and when debugging (the URL is the same either way).
|
||||
M3U_MODE_PLAIN = "plain"
|
||||
M3U_MODE_CLIENTDRM_FALLBACK = "clientdrm-fallback"
|
||||
M3U_MODE_NOPROXY_RAW = "noproxy-raw"
|
||||
|
||||
|
||||
def setup_m3u_routes(app, manager, service):
|
||||
@@ -357,6 +358,35 @@ def setup_m3u_routes(app, manager, service):
|
||||
log_ctx=f"/api/providers/{provider}/m3u/noproxy/generate",
|
||||
)
|
||||
|
||||
# ── No-proxy / raw-URL playlists (deliberately UNCACHED) ──────────────
|
||||
# clientdrm M3U with the raw upstream manifest URL written directly into
|
||||
# each entry — no /stream/index.mpd redirect hop. Strict: channels that
|
||||
# need headers or manifest context are skipped, not downgraded (counts
|
||||
# are exposed via X-M3U-Channels-Included / -Skipped). Uncached, unlike
|
||||
# /api/m3u/noproxy: raw URLs may carry short-lived tokens. Hence no
|
||||
# /generate variants either — there is nothing to regenerate.
|
||||
# Unlike /api/m3u/noproxy, entries carry no catchup attributes: the raw
|
||||
# upstream live URL can't take the start/end times the redirect route
|
||||
# translates.
|
||||
|
||||
@app.route("/api/m3u/noproxy/raw")
|
||||
def get_m3u_all_noproxy_raw():
|
||||
"""Generates clientdrm M3U with raw upstream manifest URLs. No caching."""
|
||||
response.headers["X-M3U-Mode"] = M3U_MODE_NOPROXY_RAW
|
||||
return _handle_m3u_route(
|
||||
lambda: service.generate_m3u_noproxy_raw_all(),
|
||||
log_ctx="/api/m3u/noproxy/raw",
|
||||
)
|
||||
|
||||
@app.route("/api/providers/<provider>/m3u/noproxy/raw")
|
||||
def get_m3u_provider_noproxy_raw(provider):
|
||||
"""Generates clientdrm M3U with raw upstream manifest URLs for a provider. No caching."""
|
||||
response.headers["X-M3U-Mode"] = M3U_MODE_NOPROXY_RAW
|
||||
return _handle_m3u_route(
|
||||
lambda: service.generate_m3u_noproxy_raw_provider(provider),
|
||||
log_ctx=f"/api/providers/{provider}/m3u/noproxy/raw",
|
||||
)
|
||||
|
||||
# ── ffmpeg-piped playlist (deliberately UNCACHED) ─────────────────────
|
||||
# Intentionally left as-is (not part of the plain/clientdrm split).
|
||||
|
||||
|
||||
+189
@@ -1292,6 +1292,166 @@ class UltimateService:
|
||||
|
||||
return m3u_content
|
||||
|
||||
@staticmethod
|
||||
def _provider_needs_headers(provider_instance, channel_id: str) -> bool:
|
||||
"""
|
||||
True if playing this channel requires manifest or segment headers
|
||||
(auth tokens etc.) — the same two provider calls
|
||||
fetch_manifest_for_rewriter makes at playback time. A raw upstream
|
||||
URL in an M3U carries no headers, so such channels would 403.
|
||||
|
||||
Matches fetch_manifest_for_rewriter exactly: no catchup kwargs (only
|
||||
live is emitted) and no country (not known at playlist time). Do not
|
||||
add either here without also changing what the raw path emits.
|
||||
|
||||
Deliberately lets exceptions propagate: "could not determine" must
|
||||
not be read as "no headers needed". Callers skip the channel.
|
||||
"""
|
||||
return bool(
|
||||
provider_instance.get_manifest_headers(channel_id)
|
||||
or provider_instance.get_segment_headers(channel_id)
|
||||
)
|
||||
|
||||
def _generate_m3u_noproxy_raw_content(self, providers=None):
|
||||
"""
|
||||
Like _generate_m3u_content(no_proxy=True), but resolves each channel's
|
||||
upstream manifest URL at generation time and writes it straight into
|
||||
the playlist, removing the /stream/index.mpd redirect hop.
|
||||
|
||||
STRICT: a channel that cannot safely be served by a bare upstream URL
|
||||
is SKIPPED, never downgraded to the redirect URL. Skipped when:
|
||||
- the provider has requires_manifest_context (manifest not usable
|
||||
standalone), or
|
||||
- manifest/segment headers are required (the M3U carries no auth), or
|
||||
that could not be determined, or
|
||||
- the manifest URL could not be resolved / is empty.
|
||||
|
||||
Catchup attributes are intentionally NOT emitted: catchup_type
|
||||
"append" would append ?start_time=...&end_time=... to the raw
|
||||
upstream live URL, which the upstream does not understand (the
|
||||
redirect route is what translates it).
|
||||
|
||||
Deliberately UNCACHED — raw URLs can carry short-lived tokens.
|
||||
|
||||
Returns:
|
||||
(m3u_content, channels_included, channels_skipped)
|
||||
"""
|
||||
m3u_content = "#EXTM3U\n"
|
||||
|
||||
if providers is None:
|
||||
providers_to_process = self.manager.list_providers()
|
||||
else:
|
||||
providers_to_process = (
|
||||
[providers] if isinstance(providers, str) else providers
|
||||
)
|
||||
|
||||
total_included = 0
|
||||
total_skipped = 0
|
||||
|
||||
for provider_name, provider_label, channels in self._iter_m3u_provider_channels(providers_to_process):
|
||||
included = 0
|
||||
provider_skip_reason = None
|
||||
try:
|
||||
provider_instance = self.manager.get_provider(provider_name)
|
||||
if provider_instance is None:
|
||||
provider_skip_reason = "provider resolved to None"
|
||||
elif getattr(provider_instance, "requires_manifest_context", False):
|
||||
provider_skip_reason = "provider requires manifest context"
|
||||
|
||||
if provider_skip_reason is None:
|
||||
for channel in channels:
|
||||
channel_id = channel.channel_id
|
||||
|
||||
# Cheap check first, expensive manifest resolution last.
|
||||
try:
|
||||
if self._provider_needs_headers(provider_instance, channel_id):
|
||||
logger.debug(
|
||||
f"noproxy/raw: skipping {provider_name}/{channel_id} — headers required"
|
||||
)
|
||||
continue
|
||||
except Exception as hdr_err:
|
||||
logger.warning(
|
||||
f"noproxy/raw: header check failed for "
|
||||
f"{provider_name}/{channel_id}: {hdr_err} — skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
manifest_url = self.manager.get_channel_manifest(
|
||||
provider_name=provider_name, channel_id=channel_id
|
||||
)
|
||||
except Exception as url_err:
|
||||
logger.warning(
|
||||
f"noproxy/raw: could not resolve manifest URL for "
|
||||
f"{provider_name}/{channel_id}: {url_err} — skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
if not manifest_url:
|
||||
logger.warning(
|
||||
f"noproxy/raw: empty manifest URL for "
|
||||
f"{provider_name}/{channel_id} — skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
# DRM lookup done here, not via drm_directives=None:
|
||||
# _build_m3u_entry_header swallows lookup errors and
|
||||
# would emit a keyless (undecryptable) entry. Strict
|
||||
# like the checks above — same as
|
||||
# _generate_m3u_proxied_filtered_content, which also
|
||||
# skips a channel whose DRM lookup raises.
|
||||
try:
|
||||
drm_configs = self.manager.get_channel_drm_configs(
|
||||
provider_name=provider_name, channel_id=channel_id
|
||||
)
|
||||
except Exception as drm_err:
|
||||
logger.warning(
|
||||
f"noproxy/raw: DRM lookup failed for "
|
||||
f"{provider_name}/{channel_id}: {drm_err} — skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
m3u_content += self._build_m3u_entry_header(
|
||||
provider_name, channel,
|
||||
provider_label=provider_label,
|
||||
drm_directives=(
|
||||
self._generate_drm_directives(drm_configs)
|
||||
if drm_configs else ""
|
||||
),
|
||||
include_catchup=False,
|
||||
)
|
||||
m3u_content += f"{manifest_url}\n"
|
||||
included += 1
|
||||
|
||||
except Exception as provider_err:
|
||||
logger.warning(
|
||||
f"noproxy/raw: failed to process provider '{provider_name}': {provider_err}"
|
||||
)
|
||||
|
||||
skipped = len(channels) - included
|
||||
total_included += included
|
||||
total_skipped += skipped
|
||||
|
||||
if provider_skip_reason:
|
||||
logger.warning(
|
||||
f"noproxy/raw: provider '{provider_name}' skipped entirely "
|
||||
f"({provider_skip_reason}) — {skipped} channels omitted"
|
||||
)
|
||||
elif included == 0 and skipped > 0:
|
||||
logger.warning(
|
||||
f"noproxy/raw: provider '{provider_name}' contributed 0 of "
|
||||
f"{skipped} channels (all need headers or failed to resolve)"
|
||||
)
|
||||
elif skipped:
|
||||
logger.info(
|
||||
f"noproxy/raw: provider '{provider_name}': {included} included, {skipped} skipped"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"noproxy/raw M3U: included {total_included} channels, skipped {total_skipped}"
|
||||
)
|
||||
return m3u_content, total_included, total_skipped
|
||||
|
||||
def _generate_m3u_proxied_filtered_all(self, save_to_cache: bool = False) -> str:
|
||||
"""Internal method to generate filtered decrypted M3U for all providers."""
|
||||
logger.info("Generating filtered decrypted M3U playlist for all providers")
|
||||
@@ -1557,6 +1717,35 @@ class UltimateService:
|
||||
|
||||
return m3u_content
|
||||
|
||||
def _serve_m3u_noproxy_raw(self, providers, filename: str) -> str:
|
||||
"""Shared body of the noproxy/raw public wrappers: generate, set headers."""
|
||||
m3u_content, included, skipped = self._generate_m3u_noproxy_raw_content(providers=providers)
|
||||
|
||||
response.content_type = "audio/x-mpegurl; charset=utf-8"
|
||||
response.headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
# Raw upstream URLs may embed short-lived tokens: keep clients and
|
||||
# intermediaries from caching the playlist either.
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
# Skips are otherwise invisible to the client (a shorter playlist).
|
||||
response.headers["X-M3U-Channels-Included"] = str(included)
|
||||
response.headers["X-M3U-Channels-Skipped"] = str(skipped)
|
||||
|
||||
return m3u_content
|
||||
|
||||
def generate_m3u_noproxy_raw_all(self) -> str:
|
||||
"""
|
||||
Public wrapper: clientdrm M3U with raw upstream manifest URLs (no
|
||||
redirect hop) for all providers. Backs "/api/m3u/noproxy/raw".
|
||||
Deliberately UNCACHED — see _generate_m3u_noproxy_raw_content.
|
||||
"""
|
||||
logger.info("Generating noproxy/raw M3U playlist for all providers")
|
||||
return self._serve_m3u_noproxy_raw(None, "playlist_noproxy_raw.m3u8")
|
||||
|
||||
def generate_m3u_noproxy_raw_provider(self, provider: str) -> str:
|
||||
"""Public wrapper: noproxy/raw M3U for a single provider. Uncached."""
|
||||
logger.info(f"Generating noproxy/raw M3U playlist for provider '{provider}'")
|
||||
return self._serve_m3u_noproxy_raw(provider, f"{provider}_playlist_noproxy_raw.m3u8")
|
||||
|
||||
def generate_m3u_proxied_ffmpeg_fast(self, providers=None) -> str:
|
||||
"""Public wrapper for fast ffmpeg-decrypted M3U generation."""
|
||||
return self._generate_m3u_proxied_ffmpeg_fast(providers)
|
||||
|
||||
Reference in New Issue
Block a user