mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-20 08:02:28 +02:00
Add movetv
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user