mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-21 23:32:48 +02:00
Make magenta2 platform configurable
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# streaming_providers/base/settings/provider_settings_store.py
|
||||
"""
|
||||
Small per-provider, per-country settings store (provider_settings.json).
|
||||
|
||||
Holds user *choices* (e.g. which client a provider emulates). Deliberately
|
||||
separate from session.json (volatile state: tokens, device ids — safe to
|
||||
wipe) and from credentials (secrets).
|
||||
|
||||
Layout mirrors session.json: {provider: {country: {key: value}}}
|
||||
"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..utils.logger import logger
|
||||
from ..utils.vfs import VFS
|
||||
|
||||
|
||||
class ProviderSettingsStore:
|
||||
FILE = "provider_settings.json"
|
||||
|
||||
def __init__(self, config_dir: Optional[str] = None):
|
||||
self.vfs = VFS()
|
||||
if config_dir:
|
||||
self.vfs._base_path = config_dir
|
||||
self.vfs.mkdirs("")
|
||||
|
||||
def get(self, provider: str, country: str, key: str, default: Any = None) -> Any:
|
||||
data = self.vfs.read_json(self.FILE) or {}
|
||||
provider_data = data.get(provider)
|
||||
country_data = provider_data.get(country) if isinstance(provider_data, dict) else None
|
||||
if not isinstance(country_data, dict):
|
||||
return default
|
||||
return country_data.get(key, default)
|
||||
|
||||
def set(self, provider: str, country: str, key: str, value: Any) -> bool:
|
||||
data = self.vfs.read_json(self.FILE) or {}
|
||||
provider_data = data.get(provider)
|
||||
if not isinstance(provider_data, dict):
|
||||
provider_data = data[provider] = {}
|
||||
country_data = provider_data.get(country)
|
||||
if not isinstance(country_data, dict):
|
||||
country_data = provider_data[country] = {}
|
||||
country_data[key] = value
|
||||
ok = self.vfs.write_json(self.FILE, data)
|
||||
if not ok:
|
||||
logger.error(f"Failed to write {self.FILE}")
|
||||
return ok
|
||||
|
||||
def clear(self, provider: str, country: str, key: str) -> bool:
|
||||
data = self.vfs.read_json(self.FILE) or {}
|
||||
country_data = (data.get(provider) or {}).get(country)
|
||||
if isinstance(country_data, dict) and key in country_data:
|
||||
del country_data[key]
|
||||
return self.vfs.write_json(self.FILE, data)
|
||||
return True
|
||||
@@ -1276,7 +1276,8 @@ class Magenta2Authenticator(BaseAuthenticator):
|
||||
|
||||
Saves:
|
||||
- tvhubs scope: access_token only
|
||||
- Provider level: refresh_token and device_id only
|
||||
- Provider level: refresh_token and device_id, merged into the
|
||||
existing session (other keys are preserved)
|
||||
"""
|
||||
# Save ONLY the access token data under 'tvhubs' scope
|
||||
scoped_token_data = {
|
||||
@@ -1291,20 +1292,24 @@ class Magenta2Authenticator(BaseAuthenticator):
|
||||
self.provider_name, "tvhubs", scoped_token_data, self.country
|
||||
)
|
||||
|
||||
# Save provider session data without access_token and without persona fields
|
||||
# Only keep refresh_token and device_id
|
||||
provider_session_data = {
|
||||
"refresh_token": token.refresh_token,
|
||||
"device_id": self._device_id,
|
||||
}
|
||||
# Merge refresh_token and device_id into the existing provider session.
|
||||
# save_session() REPLACES the whole [provider][country] blob, so it must
|
||||
# be handed the loaded session — a fresh dict here would erase the
|
||||
# 'tvhubs' scope saved just above, plus serial_number, client_platform
|
||||
# and every other scope.
|
||||
session_manager = self.settings_manager.session_manager
|
||||
provider_session_data = (
|
||||
session_manager.load_session(self.provider_name, self.country) or {}
|
||||
)
|
||||
provider_session_data["refresh_token"] = token.refresh_token
|
||||
provider_session_data["device_id"] = self._device_id
|
||||
|
||||
# Save provider session data
|
||||
self.settings_manager.session_manager.save_session(
|
||||
session_manager.save_session(
|
||||
self.provider_name, provider_session_data, self.country
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"✓ Access token saved under 'tvhubs' scope, only refresh_token and device_id saved at provider level"
|
||||
"✓ Access token saved under 'tvhubs' scope, refresh_token and device_id merged at provider level"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -15,6 +15,7 @@ that delegates to the domain managers:
|
||||
Magenta2EpgManager – EPG grid + programme-details (ThePlatform API)
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
import uuid
|
||||
@@ -28,6 +29,7 @@ from ...base.models.auth import AuthState
|
||||
from ...base.models.epg_models import EPGEntry, EPGProgramDetails
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.network import HTTPManagerFactory, ProxyConfigManager
|
||||
from ...base.settings.provider_settings_store import ProviderSettingsStore
|
||||
from ...base.provider import StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
from .recordings_manager import RecordingsManager
|
||||
@@ -112,7 +114,7 @@ class Magenta2Provider(StreamingProvider):
|
||||
def __init__(
|
||||
self,
|
||||
country: str = DEFAULT_COUNTRY,
|
||||
platform: str = DEFAULT_PLATFORM,
|
||||
platform: Optional[str] = None,
|
||||
config_dir: Optional[str] = None,
|
||||
proxy_config: Optional[ProxyConfig] = None,
|
||||
proxy_url: Optional[str] = None,
|
||||
@@ -126,11 +128,13 @@ class Magenta2Provider(StreamingProvider):
|
||||
f"Unsupported country: {country}. Must be one of: {SUPPORTED_COUNTRIES}"
|
||||
)
|
||||
|
||||
# Platform resolution: explicit arg > MAGENTA2_PLATFORM env var >
|
||||
# persisted setting (provider_settings.json) > DEFAULT_PLATFORM.
|
||||
# ProviderRegistry only passes `country`, so in production this is
|
||||
# what actually selects the emulated client. An unknown value raises
|
||||
# (no silent fallback) so a typo can't quietly change client identity.
|
||||
platform = self._resolve_platform(platform, config_dir, country)
|
||||
self.platform = platform
|
||||
if platform not in MAGENTA2_PLATFORMS:
|
||||
raise ValueError(
|
||||
f"Unknown platform '{platform}'. Supported: {list(MAGENTA2_PLATFORMS.keys())}"
|
||||
)
|
||||
self.platform_config = MAGENTA2_PLATFORMS[platform]
|
||||
self.user_agent_plain = render_user_agent(platform, subscriber_suffix=False)
|
||||
self.user_agent_subscriber = render_user_agent(platform, subscriber_suffix=True)
|
||||
@@ -154,6 +158,11 @@ class Magenta2Provider(StreamingProvider):
|
||||
# the same VFS paths (one device_id / serial_number per installation).
|
||||
self._session_manager: SessionManager = SessionManager(config_dir)
|
||||
|
||||
# Tokens, device_id and serial_number all belong to ONE emulated client
|
||||
# identity. If the configured platform differs from the one the stored
|
||||
# session was created for, reset the session BEFORE device_id is read.
|
||||
self._bind_session_to_platform(platform)
|
||||
|
||||
# device_id: read from the SAME persisted source TokenFlowManager
|
||||
# already uses (SessionManager.get_device_id), which itself
|
||||
# generates-and-persists on first call. Do NOT generate a second,
|
||||
@@ -373,6 +382,82 @@ class Magenta2Provider(StreamingProvider):
|
||||
return f"<redacted len={len(value)} fp={cls._token_fingerprint(value)}>"
|
||||
return value
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Platform selection #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
PLATFORM_ENV_VAR: ClassVar[str] = "MAGENTA2_PLATFORM"
|
||||
PLATFORM_SETTING_KEY: ClassVar[str] = "platform"
|
||||
|
||||
@staticmethod
|
||||
def _validate_platform(platform: str) -> str:
|
||||
if platform not in MAGENTA2_PLATFORMS:
|
||||
raise ValueError(
|
||||
f"Unknown platform '{platform}'. Supported: {list(MAGENTA2_PLATFORMS.keys())}"
|
||||
)
|
||||
return platform
|
||||
|
||||
@classmethod
|
||||
def _resolve_platform(
|
||||
cls, explicit: Optional[str], config_dir: Optional[str], country: str
|
||||
) -> str:
|
||||
"""explicit arg > env var > persisted setting > DEFAULT_PLATFORM."""
|
||||
candidate = explicit or os.environ.get(cls.PLATFORM_ENV_VAR)
|
||||
if not candidate:
|
||||
try:
|
||||
candidate = ProviderSettingsStore(config_dir).get(
|
||||
"magenta2", country, cls.PLATFORM_SETTING_KEY
|
||||
)
|
||||
except Exception as e:
|
||||
# Unreadable settings file must not take the provider down;
|
||||
# an invalid *value* still raises below.
|
||||
logger.warning(f"Could not read Magenta2 platform setting: {e}")
|
||||
return cls._validate_platform(candidate or DEFAULT_PLATFORM)
|
||||
|
||||
@classmethod
|
||||
def set_platform_setting(
|
||||
cls, platform: str, country: str = DEFAULT_COUNTRY, config_dir: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Persist the platform choice. Takes effect on the next instantiation,
|
||||
e.g. registry.reinitialize_provider("magenta2"). Ignored while the
|
||||
MAGENTA2_PLATFORM env var is set (it takes precedence).
|
||||
"""
|
||||
cls._validate_platform(platform)
|
||||
return ProviderSettingsStore(config_dir).set(
|
||||
"magenta2", country, cls.PLATFORM_SETTING_KEY, platform
|
||||
)
|
||||
|
||||
def _bind_session_to_platform(self, platform: str) -> None:
|
||||
"""
|
||||
Stamp the session with the platform it was created for and reset it
|
||||
when the platform changes.
|
||||
|
||||
Sessions that predate this marker were necessarily created as
|
||||
DEFAULT_PLATFORM (the registry never passed another one), so a
|
||||
marker-less, non-empty session is treated as DEFAULT_PLATFORM rather
|
||||
than wiped on first start after upgrade.
|
||||
|
||||
Known cost: the abandoned device_id may still count against
|
||||
Telekom's device limit, so avoid flipping platforms repeatedly.
|
||||
"""
|
||||
session = self._session_manager.load_session(self.provider_name, self.country) or {}
|
||||
stored = session.get("client_platform")
|
||||
if stored is None and session:
|
||||
stored = DEFAULT_PLATFORM
|
||||
|
||||
if stored and stored != platform:
|
||||
logger.warning(
|
||||
f"Magenta2 platform changed ({stored} -> {platform}): "
|
||||
f"resetting session (tokens, device_id, serial_number)"
|
||||
)
|
||||
self._session_manager.clear_session(self.provider_name, self.country)
|
||||
session = {}
|
||||
|
||||
if session.get("client_platform") != platform:
|
||||
session["client_platform"] = platform
|
||||
self._session_manager.save_session(self.provider_name, session, self.country)
|
||||
|
||||
def _get_or_create_serial_number(self) -> str:
|
||||
"""
|
||||
Return a stable serial number for this installation, persisted
|
||||
|
||||
Reference in New Issue
Block a user