Add discovery

This commit is contained in:
Nirvana
2026-03-14 14:07:26 +01:00
parent 46f3867218
commit 6a3853f7bd
3 changed files with 111 additions and 35 deletions
@@ -297,4 +297,15 @@ VOD_PREFIX_MOVIE_SH = "GN_SH"
# Name of the personal-bar tile that leads to the VOD StructuredGrid.
# Looked up by exact title match in the primary.tiles list returned by homeUrl.
VOD_STREAMING_TILE_TITLE = "Streaming"
# Ordered list of personal-bar tile titles that lead to the VOD StructuredGrid.
# Tried in order — first match wins. Different user subscriptions/locales
# may show different tile names (e.g. "MagentaTV+" instead of "Streaming").
VOD_STREAMING_TILE_TITLES = [
"Streaming", # Default / standard subscription
"MagentaTV+", # Alternative name observed in some accounts
"Heimkino", # Fallback seen in basic-tier bars
]
# Keep the singular alias for backwards compatibility with any external code
# that still imports VOD_STREAMING_TILE_TITLE directly.
VOD_STREAMING_TILE_TITLE = VOD_STREAMING_TILE_TITLES[0]
@@ -75,9 +75,13 @@ class Magenta2Provider(StreamingProvider):
)
self.terminal_type = self.platform_config["terminal_type"]
# Generate session ID and device ID
# Generate session ID, device ID, and serial number.
# serial_number is stable for the lifetime of this provider instance —
# it mimics the hardware serial that a real device would send and must
# be consistent across all requests in the same session.
self.session_id = self._generate_uuid()
self.device_id = self._generate_device_id()
self.serial_number = self._generate_uuid()
# Setup proxy configuration
self.proxy_config = (
@@ -277,6 +281,7 @@ class Magenta2Provider(StreamingProvider):
bootstrap=self.endpoint_manager.config.bootstrap,
provider_config=self.endpoint_manager.config,
session_id=self.session_id,
serial_number=self.serial_number,
auth_headers_callback=self._vod_auth_headers,
)
logger.info("✓ VodManager initialized with discovered config")
@@ -1116,37 +1121,81 @@ class Magenta2Provider(StreamingProvider):
def _vod_auth_headers(self) -> Dict[str, str]:
"""
Build authentication headers required by authenticated VOD endpoints:
vodproductinformation (wcps.t-online.de/uspip/...)
VodPlayer (wcps.t-online.de/vops/...)
Build authentication headers required by authenticated VOD endpoints
(vodproductinformation, VodPlayer, PersonalBar).
Required headers (observed from browser traffic):
Authorization: Bearer <persona_jwt>
x-mpx-authorization: Basic <persona_token> (account_uri:jwt b64)
x-dt-session-id: <session_id>
x-dt-call-id: <fresh uuid per request>
origin / referer: magenta.tv
Headers are platform-dependent:
ftv-web
-------
Authorization: Bearer <persona_jwt>
x-mpx-authorization: Basic <persona_token>
x-dt-session-id: <session_id>
x-dt-call-id: <fresh uuid per request>
origin: https://www.magenta.tv
referer: https://www.magenta.tv/
user-agent: <web UA>
x-permissionflagpersonalizeduireco: false
ftv-android / ftv-androidtv (and variants)
-------------------------------------------
Authorization: Bearer <persona_jwt>
x-mpx-authorization: Basic <persona_token>
x-stbserialnumber: <stable serial UUID>
dt-session-id: <session_id> (no x- prefix)
dt-call-id: <fresh uuid per request> (no x- prefix)
user-agent: <android UA>
accept-encoding: gzip
Note: _get_with_serial() in VodManager may further override dt-call-id
and x-stbserialnumber per-request; the values set here act as a
sensible default for plain _get() / _get_auth() calls.
"""
persona_token = self._ensure_authenticated()
# The persona_token is already Base64(account_uri:jwt) — used as-is
# for x-mpx-authorization. For Authorization we need just the raw JWT.
# persona_token is Base64(account_uri:jwt) — used as-is for
# x-mpx-authorization. Strip to just the raw JWT for Authorization.
persona_jwt = self._extract_persona_jwt_from_token(persona_token)
headers = {
"x-mpx-authorization": f"Basic {persona_token}",
"x-dt-session-id": self.session_id,
"x-dt-call-id": self._generate_call_id(),
"origin": "https://www.magenta.tv",
"referer": "https://www.magenta.tv/",
"User-Agent": self.platform_config["user_agent"],
"Accept": "application/json",
"x-permissionflagpersonalizeduireco": "false",
}
if persona_jwt:
headers["Authorization"] = f"Bearer {persona_jwt}"
auth_value = (
f"Bearer {persona_jwt}" if persona_jwt else f"Basic {persona_token}"
)
# Determine which header flavour to use based on the client_model
# resolved at init time (ftv-web vs everything else).
client_model: str = (
self.provider_config.bootstrap.client_model
if self.provider_config and self.provider_config.bootstrap
else f"ftv-{self.platform}"
)
is_web = client_model == "ftv-web"
if is_web:
headers: Dict[str, str] = {
"Authorization": auth_value,
"x-mpx-authorization": f"Basic {persona_token}",
"x-dt-session-id": self.session_id,
"x-dt-call-id": self._generate_call_id(),
"origin": "https://www.magenta.tv",
"referer": "https://www.magenta.tv/",
"user-agent": self.platform_config["user_agent"],
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "de-DE,de;q=0.9",
"x-permissionflagpersonalizeduireco": "false",
}
else:
# Fallback: some endpoints accept Basic auth too
headers["Authorization"] = f"Basic {persona_token}"
# Android TV / Android Mobile / ATV-Launcher / iOS all use the
# non-prefixed dt-* header names and expose the serial number.
headers = {
"Authorization": auth_value,
"x-mpx-authorization": f"Basic {persona_token}",
"x-stbserialnumber": self.serial_number,
"dt-session-id": self.session_id,
"dt-call-id": self._generate_call_id(),
"user-agent": self.platform_config["user_agent"],
"accept-encoding": "gzip",
}
return headers
def get_vod_category(self, category_path, **kwargs):
@@ -57,7 +57,7 @@ from .constants import (
VOD_PREFIX_EPISODE,
VOD_PREFIX_SEASON,
VOD_PREFIX_SERIES,
VOD_STREAMING_TILE_TITLE,
VOD_STREAMING_TILE_TITLES,
)
class VodManager:
@@ -94,6 +94,7 @@ class VodManager:
bootstrap=None,
provider_config=None,
session_id: Optional[str] = None,
serial_number: Optional[str] = None,
preferred_quality: str = "HD",
auth_headers_callback=None,
):
@@ -101,6 +102,12 @@ class VodManager:
self._provider = provider_name
self._provider_config = provider_config
self._session_id: str = session_id or ""
# Stable serial number for the lifetime of this manager instance.
# Passed in from the provider so it stays consistent across all
# requests (auth headers callback, _get_with_serial calls, etc.).
# Falls back to a fresh UUID only when not supplied (e.g. unit tests).
import uuid as _uuid_mod
self._serial_number: str = serial_number or str(_uuid_mod.uuid4())
# Normalise to uppercase; fall back to "HD" for unknown values.
_q = (preferred_quality or "HD").upper()
self._preferred_quality: str = _q if _q in self._QUALITY_FALLBACK else "HD"
@@ -356,9 +363,10 @@ class VodManager:
dt_call_id_1 = str(_uuid.uuid4())
cid = f"{dt_session_id}::{dt_call_id_1}"
# Random serial number UUID — the real device sends its hardware serial,
# but any stable UUID is accepted.
serial_number = str(_uuid.uuid4())
# Use the stable serial number that was set at construction time.
# Generating a new UUID here on every call would cause the server to
# treat each request as a different device, breaking session correlation.
serial_number = self._serial_number
# Params mirror the real Android TV DocumentGroupRedirect request exactly.
# Note: $subscriberType and $reloadAfterChange are NOT sent by real devices;
@@ -457,15 +465,23 @@ class VodManager:
f"{self._provider}: Personal bar tiles: "
f"{[t.get('title') for t in tiles]}"
)
for tile in tiles:
if tile.get("title") == VOD_STREAMING_TILE_TITLE:
# Build a lookup so we can find the first matching tile by title
# regardless of which subscription/locale variant the user has.
tile_by_title = {t.get("title"): t for t in tiles}
for candidate in VOD_STREAMING_TILE_TITLES:
tile = tile_by_title.get(candidate)
if tile:
href = tile.get("onFocus", {}).get("screen", {}).get("href")
if href:
logger.debug(f"{self._provider}: Found Streaming grid URL: {href}")
logger.debug(
f"{self._provider}: Found VOD grid tile '{candidate}': {href}"
)
return href
logger.warning(
f"{self._provider}: '{VOD_STREAMING_TILE_TITLE}' tile not found in personal bar"
f"{self._provider}: No VOD tile found in personal bar "
f"(tried: {VOD_STREAMING_TILE_TITLES}); "
f"available: {list(tile_by_title.keys())}"
)
return None