mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-21 15:22:28 +02:00
255 lines
9.0 KiB
Python
255 lines
9.0 KiB
Python
# streaming_providers/providers/eon/constants.py
|
|
"""
|
|
Eon constants -- the SINGLE SOURCE OF TRUTH for every URL, header value,
|
|
identifier and default of this provider.
|
|
|
|
Identity rule: ONE id, used everywhere
|
|
directory name == AVAILABLE_PROVIDERS key == provider_name
|
|
== PROVIDER_NAME == credentials / session / enable-setting key
|
|
Never rename it after release.
|
|
|
|
Eon is a white-label platform (united.cloud): the same API serves several
|
|
operator brands. The brand selects the CDN/API host at runtime (see
|
|
bootstrap.EonEndpoints); the platform ("web" / "androidtv") selects the
|
|
client identity (OAuth client, device bodies, URL payload layout, UA).
|
|
|
|
Never log anything defined in PLATFORMS (client secrets) or any stream key.
|
|
"""
|
|
|
|
import base64
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Optional
|
|
|
|
from ...base.errors import ConfigurationError
|
|
|
|
|
|
class EonDefaults:
|
|
PROVIDER_NAME = "eon"
|
|
PROVIDER_LABEL = "EON"
|
|
PROVIDER_LOGO = "" # TODO: no logo URL known yet
|
|
|
|
# One entry pins the registry's construction country (UPPERCASE).
|
|
# Brands are not 1:1 with countries; the default brand is Vivacom (BG).
|
|
SUPPORTED_COUNTRIES = ("BG",)
|
|
|
|
DEFAULT_BRAND = "vivacom"
|
|
DEFAULT_PLATFORM = "web"
|
|
DEFAULT_LANGUAGE = "bg"
|
|
|
|
# --- Broker (brand / CDN discovery, generic client token) ------------
|
|
BROKER_URL = "https://broker.global.united.cloud/"
|
|
GLOBAL_HOST = "global.united.cloud/"
|
|
PATH_BRANDS = "v2/brands"
|
|
PATH_CDNINFO = "v1/cdninfo"
|
|
PATH_OAUTH_TOKEN = "oauth/token"
|
|
|
|
# --- API (relative to the resolved api base) -------------------------
|
|
PATH_OTP = "v1/otp"
|
|
PATH_DEVICES_V1 = "v1/devices"
|
|
PATH_DEVICES_V2 = "v2/devices"
|
|
PATH_SERVICE_PROVIDER = "v1/sp"
|
|
PATH_PROFILES = "v1/rndprofiles"
|
|
PATH_SERVERS = "v1/servers"
|
|
PATH_TIME = "v1/time"
|
|
PATH_CHANNELS = "v4/channels"
|
|
PATH_EPG = "v2/events/epg"
|
|
|
|
CHANNEL_TYPE_TV = "TV"
|
|
CHANNEL_TYPE_RADIO = "RADIO"
|
|
CHANNEL_SORT = "POSITION"
|
|
|
|
# --- Stream URL ------------------------------------------------------
|
|
PLAYER = "m3u8"
|
|
CONN_TYPE_BROWSER = "BROWSER"
|
|
CONN_TYPE_ETHERNET = "ETHERNET"
|
|
SIG_LIVE = "live"
|
|
SIG_CUTV = "cutv"
|
|
SIG_RADIO = "radio"
|
|
# The CDN rejects an encrypted URL whose ctime is older than ~20 s.
|
|
URL_CACHE_TTL = 5.0
|
|
# Position (1-based) in /v1/servers; the addon always took the 2nd
|
|
# entry (3rd on Android TV). Positional on purpose -- see README of the
|
|
# port; log when the list is shorter.
|
|
SERVER_INDEX_WEB = 2
|
|
SERVER_INDEX_ANDROIDTV = 3
|
|
# video-bitrate floor sent as minvbr
|
|
MIN_VIDEO_BITRATE = 100
|
|
|
|
# --- Catalog cache ---------------------------------------------------
|
|
CATALOG_TTL_SECONDS = 3600
|
|
|
|
TIMEOUT = 30
|
|
|
|
# --- EPG -------------------------------------------------------------
|
|
# Rolling 7-day archive (older dates answer HTTP 400 invalid_input).
|
|
# The future horizon is NOT verified against a real response.
|
|
EPG_PAST_DAYS = 7
|
|
EPG_FUTURE_DAYS = 7
|
|
EPG_ICON_SIZE = "STB_XL"
|
|
|
|
# --- Catchup ---------------------------------------------------------
|
|
# Provider-wide maximum (the per-channel value is cutvDelay of v4/channels;
|
|
# 604800000 ms = 168 h in every sample seen).
|
|
CATCHUP_WINDOW_HOURS = 168
|
|
|
|
|
|
# brand key (config) -> CDN identifier as the broker knows it
|
|
BRAND_IDENTIFIERS: Dict[str, str] = {
|
|
"sbb": "sbb-qa",
|
|
"telemach": "telemach",
|
|
"telemach-hr": "telemach-hr",
|
|
"telemach-si": "telemach-si",
|
|
"vivacom": "vivacom",
|
|
"nova": "nova",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EonPlatform:
|
|
key: str
|
|
api_prefix: str
|
|
api_selector: str
|
|
device_type: str
|
|
device_name: str
|
|
device_model: str
|
|
device_platform: str
|
|
client_sw_version: str
|
|
client_sw_build: str
|
|
system_sw: str
|
|
system_version: str
|
|
user_agent: str
|
|
client_id: str
|
|
client_secret: str
|
|
conn_type: str
|
|
server_index: int
|
|
|
|
@property
|
|
def is_android_tv(self) -> bool:
|
|
return self.key == "androidtv"
|
|
|
|
|
|
PLATFORMS: Dict[str, EonPlatform] = {
|
|
"web": EonPlatform(
|
|
key="web",
|
|
api_prefix="web",
|
|
api_selector="be",
|
|
device_type="web_linux_chrome",
|
|
device_name="",
|
|
device_model="Chrome 150",
|
|
device_platform="web",
|
|
client_sw_version="",
|
|
client_sw_build="",
|
|
system_sw="Linux",
|
|
system_version="x86_64",
|
|
user_agent=(
|
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
"(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
|
|
),
|
|
client_id="b8d9ade4-1093-46a7-a4f7-0e47be463c10",
|
|
client_secret="1w4dmww87x1e9l89essqvc81pidrqsa0li1rva23",
|
|
conn_type=EonDefaults.CONN_TYPE_BROWSER,
|
|
server_index=EonDefaults.SERVER_INDEX_WEB,
|
|
),
|
|
"androidtv": EonPlatform(
|
|
key="androidtv",
|
|
api_prefix="android-tv",
|
|
api_selector="af31",
|
|
device_type="Android 11",
|
|
device_name="Android TV 30",
|
|
device_model="SHIELD Android TV",
|
|
device_platform="android_tv",
|
|
client_sw_version="8.1.3",
|
|
client_sw_build="8.1.35906",
|
|
system_sw="Android",
|
|
system_version="11",
|
|
user_agent=(
|
|
"Mozilla/5.0 (Linux; Android 11; SHIELD Android TV "
|
|
"Build/RQ1A.210105.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
"Version/4.0 Chrome/150.0.0.0 Mobile Safari/537.36; "
|
|
"XDKAndroidWebView/3.0.1/XDKWebView NVIDIA NVIDIA/mdarcy/"
|
|
"mdarcy:11/RQ1A.210105.003/7825230_3167.5736:user/release-keys "
|
|
"NVIDIA AndroidTV 1.00A_ATV SHIELD Android TV Android/11 "
|
|
"ExoPlayer ((1.00A_ATV::1.14.1::androidtv::)"
|
|
),
|
|
client_id="5a3e24b8-70cd-4958-b716-af9ce053e594",
|
|
client_secret="aazy6orsi9elhhs17e47lfb4palgszw6igf4y26z",
|
|
conn_type=EonDefaults.CONN_TYPE_ETHERNET,
|
|
server_index=EonDefaults.SERVER_INDEX_ANDROIDTV,
|
|
),
|
|
}
|
|
|
|
|
|
class EonConfig:
|
|
"""
|
|
Per-instance configuration.
|
|
|
|
ONE instance is created by the provider and SHARED with auth, api,
|
|
bootstrap and managers. Never rebuild a second config from a subset.
|
|
"""
|
|
|
|
def __init__(self, config_dict: Optional[dict] = None):
|
|
c = config_dict or {}
|
|
|
|
self.brand = str(c.get("brand", EonDefaults.DEFAULT_BRAND)).lower()
|
|
if self.brand not in BRAND_IDENTIFIERS:
|
|
raise ConfigurationError(
|
|
f"{EonDefaults.PROVIDER_LABEL}: unknown brand {self.brand!r} "
|
|
f"(known: {', '.join(sorted(BRAND_IDENTIFIERS))})"
|
|
)
|
|
self.brand_identifier = BRAND_IDENTIFIERS[self.brand]
|
|
|
|
platform_key = str(c.get("platform", EonDefaults.DEFAULT_PLATFORM)).lower()
|
|
if platform_key not in PLATFORMS:
|
|
raise ConfigurationError(
|
|
f"{EonDefaults.PROVIDER_LABEL}: unknown platform {platform_key!r} "
|
|
f"(known: {', '.join(sorted(PLATFORMS))})"
|
|
)
|
|
self.platform = PLATFORMS[platform_key]
|
|
|
|
self.user_agent = c.get("user_agent") or self.platform.user_agent
|
|
self.timeout = c.get("timeout", EonDefaults.TIMEOUT)
|
|
self.language = c.get("language", EonDefaults.DEFAULT_LANGUAGE)
|
|
self.hide_unsubscribed = bool(c.get("hide_unsubscribed", False))
|
|
self.include_radio = bool(c.get("include_radio", True))
|
|
self.short_names = bool(c.get("short_names", False))
|
|
self.channel_start_number = int(c.get("channel_start_number", 1))
|
|
# `adaptive=true` in the URL payload. The addon sent it whenever
|
|
# inputstream.adaptive plays (live AND replay); ffmpegdirect replay
|
|
# rejects it. The backend feeds inputstream.adaptive.
|
|
self.adaptive_hint = bool(c.get("adaptive_hint", True))
|
|
self.server_index = int(c.get("server_index", self.platform.server_index))
|
|
|
|
# ----- Header builders ---------------------------------------------
|
|
def basic_auth_value(self) -> str:
|
|
raw = f"{self.platform.client_id}:{self.platform.client_secret}"
|
|
return "Basic " + base64.b64encode(raw.encode("ascii")).decode("ascii")
|
|
|
|
def basic_headers(self) -> dict:
|
|
"""Client-credentials headers (broker calls, token endpoints)."""
|
|
return {
|
|
"User-Agent": self.user_agent,
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
"Authorization": self.basic_auth_value(),
|
|
}
|
|
|
|
def bearer_headers(self, access_token: str) -> dict:
|
|
"""Headers for the provider's JSON API (NOT for the CDN)."""
|
|
return {
|
|
"User-Agent": self.user_agent,
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
"Authorization": f"bearer {access_token}",
|
|
}
|
|
|
|
def epg_headers(self) -> dict:
|
|
"""Extra header the EPG endpoint wants (epoch times)."""
|
|
return {"x-ucp-time-format": "timestamp"}
|
|
|
|
def stream_headers(self) -> dict:
|
|
"""
|
|
Headers for MPD / segment fetches by the player (CDN). The CDN blocks
|
|
requests without a recognised browser / smart-TV UA; it must NOT get
|
|
API headers (tokens).
|
|
"""
|
|
return {"User-Agent": self.user_agent} |