Add discovery

This commit is contained in:
Nirvana
2026-03-04 12:10:23 +01:00
parent 61ab2e292b
commit 5c28fdbb04
2 changed files with 138 additions and 80 deletions
+119 -9
View File
@@ -35,7 +35,7 @@ from streaming_providers.base.utils import logger
# ---------------------------------------------------------------------------
CONTENT_TYPE_CHANNEL = "channel"
CONTENT_TYPE_EVENT = "event"
# CONTENT_TYPE_VOD = "vod" # uncomment when VOD ops are implemented
CONTENT_TYPE_VOD = "vod"
def setup_stream_routes(app, manager, service):
@@ -59,10 +59,10 @@ def setup_stream_routes(app, manager, service):
return manager.get_event_drm_configs(
provider_name=provider, event_id=content_id, **kwargs
)
# elif content_type == CONTENT_TYPE_VOD:
# return manager.get_vod_drm_configs(
# provider_name=provider, vod_id=content_id, **kwargs
# )
elif content_type == CONTENT_TYPE_VOD:
return manager.get_vod_drm_configs(
provider_name=provider, vod_id=content_id, **kwargs
)
else:
raise ValueError(f"Unknown content_type '{content_type}'")
@@ -79,10 +79,10 @@ def setup_stream_routes(app, manager, service):
return manager.get_event_manifest(
provider_name=provider, event_id=content_id, **kwargs
)
# elif content_type == CONTENT_TYPE_VOD:
# return manager.get_vod_manifest(
# provider_name=provider, vod_id=content_id, **kwargs
# )
elif content_type == CONTENT_TYPE_VOD:
return manager.get_vod_manifest(
provider_name=provider, vod_id=content_id, **kwargs
)
else:
raise ValueError(f"Unknown content_type '{content_type}'")
@@ -551,6 +551,116 @@ def setup_stream_routes(app, manager, service):
response.status = 500
return {"error": f"Internal server error: {str(e)}"}
# =========================================================================
# VOD ROUTES — identical transport pattern to event routes
# /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/<provider>/vod/<vod_id>/manifest")
def get_vod_stream_manifest(provider, vod_id):
"""
Returns JSON with a manifest_url pointing to the VOD 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}/vod/{vod_id}/stream/index.mpd"
)
if country:
stream_url += f"?country={country}"
_build_drm_header(CONTENT_TYPE_VOD, provider, vod_id, country=country)
return {
"provider": provider,
"vod_id": vod_id,
"manifest_url": stream_url,
}
except ValueError as e:
logger.error(f"manifest endpoint error for VOD {provider}/{vod_id}: {e}")
response.status = 404
return {"error": str(e)}
except Exception as e:
logger.error(f"manifest endpoint error for VOD {provider}/{vod_id}: {e}")
response.status = 500
return {"error": f"Internal server error: {str(e)}"}
@app.route("/api/providers/<provider>/vod/<vod_id>/stream/index.mpd")
def get_vod_stream(provider, vod_id):
"""
Returns HTTP 302 redirect to the VOD manifest, or a rewritten
manifest body when media proxy is active.
"""
try:
country = request.query.get("country")
return _resolve_stream(
CONTENT_TYPE_VOD, provider, vod_id, country=country
)
except HTTPResponse:
raise
except ValueError as e:
logger.error(f"stream error for VOD {provider}/{vod_id}: {e}")
response.status = 404
return {"error": str(e)}
except Exception as e:
logger.error(f"stream error for VOD {provider}/{vod_id}: {e}")
response.status = 500
return {"error": f"Internal server error: {str(e)}"}
@app.route(
"/api/providers/<provider>/vod/<vod_id>/stream/decrypted/index.mpd"
)
def get_vod_stream_decrypted(provider, vod_id):
"""Decrypted VOD stream — all quality representations."""
return _resolve_decrypted_stream(
CONTENT_TYPE_VOD, provider, vod_id, highest_quality_only=False
)
@app.route(
"/api/providers/<provider>/vod/<vod_id>/stream/decrypted/ffmpeg/index.mpd"
)
def get_vod_stream_decrypted_ffmpeg(provider, vod_id):
"""Decrypted VOD stream — highest quality only, optimised for ffmpeg."""
return _resolve_decrypted_stream(
CONTENT_TYPE_VOD, provider, vod_id, highest_quality_only=True
)
@app.route("/api/providers/<provider>/vod/<vod_id>/drm")
def get_vod_drm(provider, vod_id):
"""Return DRM configs for a specific VOD item."""
try:
country = request.query.get("country")
drm_configs = _get_drm_configs(
CONTENT_TYPE_VOD, provider, vod_id, country=country
)
merged = {}
for config in drm_configs:
merged.update(
config.to_dict() if hasattr(config, "to_dict") else config
)
return {
"provider": provider,
"vod_id": vod_id,
"content_type": CONTENT_TYPE_VOD,
"drm_configs": merged,
}
except ValueError as e:
logger.error(f"DRM endpoint error for VOD {provider}/{vod_id}: {e}")
response.status = 404
return {"error": str(e)}
except Exception as e:
logger.error(f"DRM endpoint error for VOD {provider}/{vod_id}: {e}")
response.status = 500
return {"error": f"Internal server error: {str(e)}"}
# =========================================================================
# EPG ROUTES (unchanged from original)
# =========================================================================
+19 -71
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# routes/vod.py
"""
VOD (Video on Demand) route handlers.
VOD (Video on Demand) browse route handlers.
Browse endpoints
----------------
@@ -9,21 +9,26 @@ GET /api/providers/<provider>/vod
Returns the root-level VOD entries (categories and/or items).
GET /api/providers/<provider>/vod/<path:path>
Navigates the VOD tree by URL-safe name slugs.
e.g. /api/providers/myprovider/vod/sports/golf/pga/tournament_x
Navigates the VOD tree by URL-safe path segments.
e.g. /api/providers/discovery_de/vod/sports/nordic-combined
Stream/manifest/DRM endpoints
------------------------------
GET /api/providers/<provider>/vod/<path:path>/manifest
Returns the manifest URL for the VOD item whose id is the last
segment of <path>.
e.g. /api/providers/discovery_de/vod/sports/nordic-combined/6bbea4ab-.../manifest
Response:
{
"provider": "discovery_de",
"path": "sports/nordic-combined",
"entries": [
{"type": "vod_category", "id": "...", "name": "...", "slug": "..."},
{"type": "vod", "id": "...", "name": "...", "slug": "..."}
],
"count": 12
}
GET /api/providers/<provider>/vod/<path:path>/drm
Returns DRM configs for the VOD item whose id is the last segment of <path>.
Route registration order matters: the explicit /manifest and /drm routes are
registered BEFORE the generic <path:path> route so Bottle matches them first.
Stream / manifest / DRM endpoints for VodItems are in streams.py:
GET /api/providers/<provider>/vod/<vod_id>/manifest
GET /api/providers/<provider>/vod/<vod_id>/stream/index.mpd
GET /api/providers/<provider>/vod/<vod_id>/drm
These are registered in streams.py (same pattern as channels and events)
and must be set up BEFORE setup_vod_routes() so Bottle matches them first.
"""
from bottle import response
@@ -35,10 +40,6 @@ def setup_vod_routes(app, manager):
def _serialize(entries) -> list:
return [e.to_dict() for e in entries]
# ------------------------------------------------------------------
# Root
# ------------------------------------------------------------------
@app.route("/api/providers/<provider>/vod", method="GET")
def get_vod_root(provider):
try:
@@ -54,59 +55,6 @@ def setup_vod_routes(app, manager):
response.status = 200
return {"provider": provider, "path": "", "entries": serialized, "count": len(serialized)}
# ------------------------------------------------------------------
# Manifest & DRM — must be registered BEFORE the generic path route
# so Bottle matches /manifest and /drm suffixes here first.
# <path:path> captures everything before the suffix, e.g.
# "sports/nordic-combined/6bbea4ab-735c-469b-89d2-2a38c195ce07"
# The last segment of that path is the vod_id (edit_id / content_id).
# ------------------------------------------------------------------
@app.route("/api/providers/<provider>/vod/<path:path>/manifest", method="GET")
def get_vod_item_manifest(provider, path):
segments = [s for s in path.split("/") if s]
if not segments:
response.status = 400
return {"error": "Missing VOD item id", "provider": provider}
vod_id = segments[-1]
try:
manifest_url = manager.get_vod_manifest(provider_name=provider, vod_id=vod_id)
except ValueError as e:
response.status = 404
return {"error": "Not found", "message": str(e), "provider": provider, "vod_id": vod_id}
except Exception as e:
logger.error(f"Failed to get VOD manifest: {e}")
response.status = 500
return {"error": "Failed to get manifest", "message": str(e), "provider": provider, "vod_id": vod_id}
if not manifest_url:
response.status = 404
return {"error": "No manifest available", "provider": provider, "vod_id": vod_id}
response.status = 200
return {"provider": provider, "vod_id": vod_id, "manifest_url": manifest_url}
@app.route("/api/providers/<provider>/vod/<path:path>/drm", method="GET")
def get_vod_item_drm(provider, path):
segments = [s for s in path.split("/") if s]
if not segments:
response.status = 400
return {"error": "Missing VOD item id", "provider": provider}
vod_id = segments[-1]
try:
drm_configs = manager.get_vod_drm_configs(provider_name=provider, vod_id=vod_id)
except ValueError as e:
response.status = 404
return {"error": "Not found", "message": str(e), "provider": provider, "vod_id": vod_id}
except Exception as e:
logger.error(f"Failed to get VOD DRM configs: {e}")
response.status = 500
return {"error": "Failed to get DRM configs", "message": str(e), "provider": provider, "vod_id": vod_id}
response.status = 200
return {"provider": provider, "vod_id": vod_id, "drm": [d.to_dict() for d in (drm_configs or [])]}
# ------------------------------------------------------------------
# Generic browse — registered last so /manifest and /drm win above
# ------------------------------------------------------------------
@app.route("/api/providers/<provider>/vod/<path:path>", method="GET")
def get_vod_path(provider, path):
slug_segments = [s for s in path.split("/") if s]