Add movetv

This commit is contained in:
Nirvana
2026-03-27 08:50:11 +01:00
parent 592e28f3a0
commit 7b9fbd2f65
3 changed files with 140 additions and 4 deletions
@@ -264,9 +264,89 @@ class MoveTVAuthenticator(BaseAuthenticator):
return False
# ------------------------------------------------------------------
# Override authenticate() to bypass expiry when session data exists
# Token refresh via /api/v2/token/status
# ------------------------------------------------------------------
def refresh_token(self, token: "MoveTVAuthToken") -> Optional["MoveTVAuthToken"]:
"""
Exchange the current refresh_token for a fresh auth_token + refresh_token
using the ``/api/v2/token/status`` endpoint.
The API expects the *old* refresh_token in the ``x-refresh-token`` header
and ``customerProfileId`` / ``appVersion`` in the JSON body. On success
it returns new ``auth_token`` and ``refresh_token`` values together with
updated server URLs.
Returns a new :class:`MoveTVAuthToken` (already persisted) on success,
or ``None`` if the refresh fails (caller should fall back to full login).
"""
if not token.refresh_token:
logger.debug("move.tv: refresh_token called but no refresh_token stored, skipping")
return None
headers = {
**MoveTVConfig.get_base_headers(),
"x-refresh-token": token.refresh_token,
}
payload = {
"customerProfileId": token.customer_profile_id,
"appVersion": MoveTVConfig.APP_VERSION,
}
logger.debug(
f"move.tv: Refreshing token for customer_profile_id={token.customer_profile_id}"
)
try:
response = self.http_manager.post(
MoveTVConfig.token_status_url(),
json=payload,
headers=headers,
operation="token_refresh",
)
response.raise_for_status()
data = response.json()
except Exception as exc:
logger.debug(f"move.tv: Token refresh request failed: {exc}")
return None
if not data.get("success") or data.get("message") != "Token active.":
logger.debug(f"move.tv: Token refresh rejected by server: {data}")
return None
# Merge refreshed fields into a new token, preserving any fields not
# returned by the status endpoint (e.g. credential_type).
existing = token.to_dict()
merged = {
**existing,
# The status response uses "auth_token"; map to both keys.
"access_token": data.get("auth_token", existing.get("access_token")),
"auth_token": data.get("auth_token", existing.get("auth_token")),
"refresh_token": data.get("refresh_token", existing.get("refresh_token")),
# "t" is the new expires_in value from the status response.
"expires_in": data.get("t", existing.get("expires_in")),
"dedicated_server": data.get("dedicated_server", existing.get("dedicated_server")),
"customer_id": int(data.get("customer_id") or existing.get("customer_id", 0)),
# IMPORTANT: The Move.tv API response field "device_id" is a
# server-assigned device identifier — it is NOT the same as the
# base-class device_id, which is our persistent local UID slot
# (analogous to how the API calls the auth token "auth_token" but
# the base class calls it "access_token"). We must preserve the
# existing uid/device_id so the base class keeps its value intact.
}
new_token = self._create_token_from_response(merged)
self._current_token = new_token
self._save_session()
logger.info(
f"move.tv: Token refreshed successfully — "
f"customer_id={new_token.customer_id}, uid={new_token.uid}"
)
return new_token
def authenticate(self, force_refresh: bool = False) -> MoveTVAuthToken:
"""
Move.tv sessions do not carry an explicit server-side expiry we can
@@ -293,7 +373,12 @@ class MoveTVAuthenticator(BaseAuthenticator):
if self.validate_token(token):
return token
logger.info("move.tv: Stored token failed validation, performing full login")
logger.info("move.tv: Stored token failed validation, attempting token refresh")
refreshed = self.refresh_token(token)
if refreshed:
return refreshed
logger.info("move.tv: Token refresh failed, performing full login")
return self._full_login()
def _full_login(self) -> MoveTVAuthToken:
@@ -22,6 +22,7 @@ class MoveTVConfig:
# -------------------------------------------------------------------------
PATH_LOGIN: str = "/api/v2/login"
PATH_VALIDATE: str = "/api/v2/token/validate"
PATH_TOKEN_STATUS: str = "/api/v2/token/status"
PATH_LIVE_CHANNELS: str = "/api/v2/content/live/all"
PATH_LIVE_SOURCE: str = "/api/v2/content/live/source/get"
@@ -100,6 +101,10 @@ class MoveTVConfig:
def validate_url(cls) -> str:
return f"{cls.API_BASE_URL}{cls.PATH_VALIDATE}"
@classmethod
def token_status_url(cls) -> str:
return f"{cls.API_BASE_URL}{cls.PATH_TOKEN_STATUS}"
@classmethod
def channels_url(cls) -> str:
return f"{cls.API_BASE_URL}{cls.PATH_LIVE_CHANNELS}"
@@ -1,12 +1,12 @@
# streaming_providers/providers/movetv/provider.py
import requests
from typing import ClassVar, Dict, List, Optional, Any
from typing import ClassVar, Dict, List, Optional, Any, cast
from ...base.models import DRMConfig, StreamingChannel
from ...base.models.proxy_models import ProxyConfig
from ...base.provider import StreamingProvider
from ...base.utils.logger import logger
from .auth import MoveTVAuthenticator
from .auth import MoveTVAuthenticator, MoveTVAuthToken
from .constants import MoveTVConfig
@@ -171,6 +171,28 @@ class MoveTVProvider(StreamingProvider):
logger.info(f"move.tv: Loaded {len(channels)} subscribed channels")
return channels
except requests.HTTPError as exc:
if exc.response is not None and exc.response.status_code == 401:
logger.info("move.tv: 401 on channel fetch — attempting token refresh …")
try:
token = cast(MoveTVAuthToken, self.authenticator._current_token)
if token and self.authenticator.refresh_token(token):
channels = self._fetch_channels()
self.channels = channels # type: ignore[assignment]
return channels
except Exception:
pass
logger.info("move.tv: Refresh failed, retrying channel fetch after full login …")
else:
logger.error(f"move.tv: HTTP error fetching channels: {exc}")
try:
self.authenticator.authenticate(force_refresh=True)
channels = self._fetch_channels()
self.channels = channels # type: ignore[assignment]
return channels
except Exception as retry_exc:
logger.error(f"move.tv: Channel fetch retry failed: {retry_exc}")
return []
except requests.RequestException as exc:
logger.error(f"move.tv: HTTP error fetching channels: {exc}")
try:
@@ -414,6 +436,30 @@ class MoveTVProvider(StreamingProvider):
json=payload,
headers=headers,
)
# On 401, attempt a token refresh and retry once before giving up.
if response.status_code == 401:
logger.info(f"move.tv: 401 on manifest fetch for liveId={live_id} — attempting token refresh …")
token = cast(MoveTVAuthToken, self.authenticator._current_token)
refreshed = self.authenticator.refresh_token(token) if token else None
if not refreshed:
logger.info("move.tv: Refresh failed, falling back to full login for manifest fetch")
self.authenticator.authenticate(force_refresh=True)
# Rebuild session and headers with the new token.
session = self.authenticator.get_session_info()
if not session:
logger.error("move.tv: Unable to obtain session after token refresh for manifest fetch")
return None
payload["customerId"] = session["customer_id"]
payload["customerProfileId"] = session["customer_profile_id"]
headers = MoveTVConfig.get_api_headers(auth_token=session["auth_token"])
response = self.http_manager.post(
MoveTVConfig.live_source_url(),
operation="manifest",
json=payload,
headers=headers,
)
response.raise_for_status()
data = response.json()