Files
script.service.ultimate/lib/streaming_providers/providers/magentaeu/vod_errors.py
T

185 lines
7.1 KiB
Python

# streaming_providers/providers/magentaeu/vod_errors.py
# -*- coding: utf-8 -*-
"""
Typed exceptions for the MagentaEU VOD path.
Rebased on the shared hierarchy in base.errors. The class names are
unchanged so existing callers (except VodError: ...) keep working; the
only change is what these classes inherit from.
Rationale: the bifrost API returns 4xx for several distinct conditions --
expired token, geo-block, entitlement denial, content removed -- and they
need to be handled differently. `raise_for_status()` alone collapses them
into one opaque HTTPError. VODOperations and the Kodi player layer need
to distinguish "refresh and retry" from "tell the user they can't watch
this here" from "this title is gone" from "this is actually catch-up,
not VOD -- hand it to the channel/catchup pathway instead".
Hierarchy (now rebased on base.errors):
VodError (base, catch-all -- subclass of ProviderError)
├── VodAuthError (401 -- token expired/invalid)
├── VodGeoBlockError (403 -- not available in your region)
├── VodEntitlementError (403 -- not in your subscription)
│ └── VodAccountVodDisabledError (account-level VOD gate is off)
├── VodNotFoundError (404 -- content removed)
├── VodBadRequestError (400 -- routing signal)
├── VodRateLimitError (429)
├── VodServerError (5xx -- retryable)
├── VodCatchupRequiredError (item has no watch/trailer action but
│ DOES have schedules/catchup_schedules
│ -- it's a linear-catchup item, not a
│ playable VOD asset)
└── VodNotImplementedError (feature captured-but-not-yet-mapped)
Callers can now catch either the local name (VodAuthError) or the shared
base (AuthError) depending on their intent.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from ...base.errors import (
AuthError,
BadRequestError,
CatchupRequiredError,
EntitlementError,
GeoBlockError,
NotFoundError,
NotImplementedYetError,
ProviderError,
RateLimitError,
ServerError,
)
class VodError(ProviderError):
"""
Base class for all MagentaEU VOD errors.
Kept as a distinct name so existing `except VodError:` sites continue
to work; it is now a subclass of the shared ProviderError, so callers
that prefer the shared name (`except ProviderError:`) also catch it.
"""
class VodAuthError(AuthError, VodError):
"""
Raised on 401 or on a locally-detected expired Bff_token.
`MagentaEUVodManager._request()` catches this internally, refreshes
the token once via a forced re-authentication, and retries the
request exactly once. If the retry also fails, VodAuthError
propagates to the caller -- at that point re-auth is not going to
fix it (credentials are likely actually invalid).
"""
class VodGeoBlockError(GeoBlockError, VodError):
"""Content is geo-blocked for the current network egress."""
class VodEntitlementError(EntitlementError, VodError):
"""
The subscriber is authenticated but not entitled to this content.
Raised by the actions endpoint when `actions.watch` is empty and
`svod_subscription_message` is present, AND the item is not a
catchup-eligible linear item (see VodCatchupRequiredError -- that
takes priority when both watch/trailer are empty).
"""
class VodAccountVodDisabledError(VodEntitlementError):
"""
Account-level VOD gate is off.
This is distinct from a per-title entitlement error: the account's
`managed_settings["TVSOA-setting-VodEnabled"]` is "false", which
means *no* VOD can be played, regardless of package. The captured
HR test account ships in this state (`vod_enabled: false` at
/user/account and echoed in every X-Account-Details header).
Callers should surface this differently than "you need to add the
HBO package".
"""
class VodNotFoundError(NotFoundError, VodError):
"""Content has been removed or never existed."""
class VodBadRequestError(BadRequestError, VodError):
"""
400 -- malformed request for the endpoint called.
Confirmed in production (not just capture): passing a component id
to `/home/page/{id}` returns 400, not 404. `get_category_children()`'s
page-then-component dispatch guess relies on this to know when its
first guess was wrong, since content_id alone doesn't reveal
whether it names a page or a component.
"""
class VodRateLimitError(RateLimitError, VodError):
"""429 -- caller should back off."""
class VodServerError(ServerError, VodError):
"""5xx -- retryable."""
class VodCatchupRequiredError(CatchupRequiredError, VodError):
"""
This "VOD" list entry is actually a catch-up item from a linear
channel, not a TVOD/SVOD asset.
Evidence from capture: some rail/search items marked as VOD series
(e.g. "Nakon poplave, serija") return `actions.watch: []` and
`actions.trailer: []` -- same shape as a genuine entitlement denial
-- but ALSO carry a non-empty `actions.schedules` / a `station_id`
/`channel_number`, and the episode list includes populated
`catchup_schedules[]` entries with their own `pid` and
`catchup_start_utc`/`catchup_end_utc` window. Treating this as a
plain VodEntitlementError is wrong: the content IS watchable, just
via the existing live-channel catchup path
(`provider.get_catchup_manifest()`), not via
`MagentaEUVodManager.get_manifest()`.
This exception carries the raw schedule/catchup data so a caller
that wants to bridge into the catchup flow can do so without a
second round-trip. Bridging itself (mapping station_id +
catchup_start_utc/catchup_end_utc into a get_catchup_manifest()
call) is NOT implemented here -- it needs the channel/EPG manager,
which this module deliberately has no dependency on. Treat this as
a routing signal, not a playback failure.
"""
def __init__(
self,
message: str,
*,
station_id: Optional[str] = None,
schedules: Optional[List[Dict[str, Any]]] = None,
catchup_schedules: Optional[List[Dict[str, Any]]] = None,
status: Optional[int] = None,
url: Optional[str] = None,
) -> None:
super().__init__(message, status=status, url=url)
self.station_id = station_id
self.schedules = schedules or []
self.catchup_schedules = catchup_schedules or []
class VodNotImplementedError(NotImplementedYetError, VodError):
"""
Raised by methods whose endpoint shape has not been captured yet.
Current cases:
* pricing resolution (needs a non-empty rent[]/purchase[] sample)
* recommendations rails (needs the recommendations endpoint)
* related-content parsing beyond the raw pass-through in
get_related_content() (field shapes only partially captured)
These raise rather than returning empty so callers can't silently
ship with a bug they don't know about.
"""