From 189a40e86691dea0429cc93506f999ac77a742c5 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Fri, 20 Mar 2026 14:35:38 +0100 Subject: [PATCH] Prepare recordings --- routes/streams.py | 131 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/routes/streams.py b/routes/streams.py index 8cb15ba..1299915 100644 --- a/routes/streams.py +++ b/routes/streams.py @@ -36,6 +36,7 @@ from streaming_providers.base.utils import logger CONTENT_TYPE_CHANNEL = "channel" CONTENT_TYPE_EVENT = "event" CONTENT_TYPE_VOD = "vod" +CONTENT_TYPE_RECORDING = "recording" def setup_stream_routes(app, manager, service): @@ -63,6 +64,10 @@ def setup_stream_routes(app, manager, service): return manager.get_vod_drm_configs( provider_name=provider, vod_id=content_id, **kwargs ) + elif content_type == CONTENT_TYPE_RECORDING: + return manager.get_recording_drm_configs( + provider_name=provider, recording_id=content_id, **kwargs + ) else: raise ValueError(f"Unknown content_type '{content_type}'") @@ -83,6 +88,10 @@ def setup_stream_routes(app, manager, service): return manager.get_vod_manifest( provider_name=provider, vod_id=content_id, **kwargs ) + elif content_type == CONTENT_TYPE_RECORDING: + return manager.get_recording_manifest( + provider_name=provider, recording_id=content_id, **kwargs + ) else: raise ValueError(f"Unknown content_type '{content_type}'") @@ -695,6 +704,128 @@ def setup_stream_routes(app, manager, service): response.status = 500 return {"error": f"Internal server error: {str(e)}"} + # ========================================================================= + # RECORDING ROUTES — identical transport pattern to VOD routes + # + # Recordings are always on-demand (pre-captured), so: + # - No catchup path (unlike channels) + # - No ffmpeg variant (not a live/adaptive stream that needs quality pinning) + # - recording_id is a flat identifier, no path hierarchy needed + # + # /manifest → returns local stream URL + attaches DRM header + # /stream → proxy-rewrites or redirects to upstream manifest + # /drm → returns raw DRM configs + # ========================================================================= + + @app.route("/api/providers//recordings//manifest") + def get_recording_manifest(provider, recording_id): + """ + Returns JSON with a manifest_url pointing to the recording stream endpoint. + Attaches x-kodi-drm-configs header. + """ + try: + country = request.query.get("country") + base_url = f"{request.urlparts.scheme}://{request.urlparts.netloc}" + stream_url = ( + f"{base_url}/api/providers/{provider}" + f"/recordings/{recording_id}/stream/index.mpd" + ) + if country: + stream_url += f"?country={country}" + + _build_drm_header( + CONTENT_TYPE_RECORDING, provider, recording_id, country=country + ) + + return { + "provider": provider, + "recording_id": recording_id, + "manifest_url": stream_url, + } + + except ValueError as e: + logger.error( + f"manifest endpoint error for recording {provider}/{recording_id}: {e}" + ) + response.status = 404 + return {"error": str(e)} + except Exception as e: + logger.error( + f"manifest endpoint error for recording {provider}/{recording_id}: {e}" + ) + response.status = 500 + return {"error": f"Internal server error: {str(e)}"} + + @app.route("/api/providers//recordings//stream/index.mpd") + def get_recording_stream(provider, recording_id): + """ + Returns HTTP 302 redirect to the recording manifest, or a rewritten + manifest body when media proxy is active. + """ + try: + country = request.query.get("country") + return _resolve_stream( + CONTENT_TYPE_RECORDING, provider, recording_id, country=country + ) + except HTTPResponse: + raise + except ValueError as e: + logger.error( + f"stream error for recording {provider}/{recording_id}: {e}" + ) + response.status = 404 + return {"error": str(e)} + except Exception as e: + logger.error( + f"stream error for recording {provider}/{recording_id}: {e}" + ) + response.status = 500 + return {"error": f"Internal server error: {str(e)}"} + + @app.route( + "/api/providers//recordings//stream/decrypted/index.mpd" + ) + def get_recording_stream_decrypted(provider, recording_id): + """Decrypted recording stream — all quality representations.""" + return _resolve_decrypted_stream( + CONTENT_TYPE_RECORDING, provider, recording_id, highest_quality_only=False + ) + + @app.route("/api/providers//recordings//drm") + def get_recording_drm(provider, recording_id): + """Return DRM configs for a specific recording.""" + try: + country = request.query.get("country") + drm_configs = _get_drm_configs( + CONTENT_TYPE_RECORDING, provider, recording_id, country=country + ) + + merged = {} + for config in drm_configs: + merged.update( + config.to_dict() if hasattr(config, "to_dict") else config + ) + + return { + "provider": provider, + "recording_id": recording_id, + "content_type": CONTENT_TYPE_RECORDING, + "drm_configs": merged, + } + + except ValueError as e: + logger.error( + f"DRM endpoint error for recording {provider}/{recording_id}: {e}" + ) + response.status = 404 + return {"error": str(e)} + except Exception as e: + logger.error( + f"DRM endpoint error for recording {provider}/{recording_id}: {e}" + ) + response.status = 500 + return {"error": f"Internal server error: {str(e)}"} + # ========================================================================= # EPG ROUTES (unchanged from original) # =========================================================================