Files
script.service.ultimate/lib/streaming_providers/base/models/content.py
T

242 lines
7.6 KiB
Python
Raw Normal View History

2026-02-27 12:14:43 +01:00
from dataclasses import dataclass
2026-08-12 09:45:25 +02:00
from decimal import Decimal
from typing import Dict, List, Optional, Set
2026-02-27 12:14:43 +01:00
from .drm import DRMConfig
2026-08-12 09:45:25 +02:00
from .pricing import AccessType, PricePoint, Pricing
2026-02-27 12:14:43 +01:00
class StreamingMode:
"""Enum for streaming modes"""
LIVE = "live"
VOD = "vod"
class ContentType:
"""Enum for content types"""
LIVE = "LIVE"
VOD = "VOD"
SERIES = "SERIES"
MOVIE = "MOVIE"
RADIO = "RADIO"
@dataclass
class Content:
"""
Base dataclass for all provider content (channels, events, etc.)
Contains all fields shared between content types.
"""
# Core identification
name: str
content_id: str
provider: str
# Visual
logo_url: Optional[str] = None
# Streaming configuration
mode: str = "live"
session_manifest: bool = False
manifest: Optional[str] = None
manifest_script: Optional[str] = None
# DRM/CDM settings
cdm_type: Optional[str] = None
use_cdm: bool = True
cdm: Optional[str] = None
cdm_mode: str = "external"
drm_config: Optional[DRMConfig] = None
# Video settings
video: str = "best"
on_demand: bool = True
speed_up: bool = True
# Metadata
quality: Optional[str] = None
content_type: str = "LIVE"
description: Optional[str] = None
genre: Optional[str] = None
language: str = "de"
country: str = "DE"
# Streaming URLs
license_url: Optional[str] = None
certificate_url: Optional[str] = None
streaming_format: Optional[str] = None
2026-08-12 09:45:25 +02:00
# Pricing (None = UNKNOWN, NOT FREE)
pricing: Optional[Pricing] = None
def __post_init__(self):
"""Validate pricing and mode consistency."""
if not self.pricing:
return
# Define mode mappings
live_types: Set[AccessType] = {
AccessType.PPV_LIVE,
AccessType.PPV_REPLAY,
AccessType.SVOD_PPV,
AccessType.AVOD
}
vod_types: Set[AccessType] = {
AccessType.TVOD_RENTAL,
AccessType.TVOD_PURCHASE
}
# Check mode consistency
if self.pricing.access_type in live_types and self.mode != StreamingMode.LIVE:
raise ValueError(
f"Content '{self.name}' has {self.pricing.access_type.value} pricing "
f"but mode is '{self.mode}' (expected '{StreamingMode.LIVE}')"
)
elif self.pricing.access_type in vod_types and self.mode != StreamingMode.VOD:
raise ValueError(
f"Content '{self.name}' has {self.pricing.access_type.value} pricing "
f"but mode is '{self.mode}' (expected '{StreamingMode.VOD}')"
)
# --- Pricing Properties ---
@property
def is_free(self) -> Optional[bool]:
"""Returns None if pricing is unknown."""
if not self.pricing:
return None
return self.pricing.is_free_at_point_of_use
@property
def requires_subscription(self) -> Optional[bool]:
if not self.pricing:
return None
return self.pricing.requires_subscription
@property
def requires_payment(self) -> Optional[bool]:
if not self.pricing:
return None
return self.pricing.requires_transactional_payment
@property
def has_pricing(self) -> bool:
"""Check if pricing is set."""
return self.pricing is not None
# --- Pricing Helpers ---
def set_free(self) -> None:
"""Mark content as free."""
self.pricing = Pricing(access_type=AccessType.FREE)
def set_subscription(self, tiers: Optional[List[str]] = None, bouquets: Optional[List[str]] = None) -> None:
"""Mark content as subscription-only."""
self.pricing = Pricing(
access_type=AccessType.SVOD,
required_tiers=tiers or [],
required_bouquets=bouquets or []
)
def set_ppv(self, amount: Decimal, currency: str = "EUR",
access_type: AccessType = AccessType.PPV_LIVE, **kwargs) -> None:
"""Mark content as PPV (live or replay)."""
if access_type not in (AccessType.PPV_LIVE, AccessType.PPV_REPLAY):
raise ValueError("access_type must be PPV_LIVE or PPV_REPLAY")
self.pricing = Pricing(
access_type=access_type,
price_points=[PricePoint(amount=amount, currency=currency, **kwargs)]
)
def set_rental(self, amount: Decimal, currency: str = "EUR",
rental_duration_hours: int = 48, **kwargs) -> None:
"""Mark content as TVOD rental."""
self.pricing = Pricing(
access_type=AccessType.TVOD_RENTAL,
rental_duration_hours=rental_duration_hours,
price_points=[PricePoint(amount=amount, currency=currency, **kwargs)]
)
def set_purchase(self, amount: Decimal, currency: str = "EUR", **kwargs) -> None:
"""Mark content as EST purchase (buy-to-own)."""
self.pricing = Pricing(
access_type=AccessType.TVOD_PURCHASE,
price_points=[PricePoint(amount=amount, currency=currency, **kwargs)]
)
# --- Manifest & DRM Methods ---
2026-02-27 12:14:43 +01:00
def set_static_manifest(self, manifest_url: str) -> None:
"""Set a static manifest URL."""
self.manifest = manifest_url
self.session_manifest = False
self.manifest_script = None
def set_dynamic_manifest(self, manifest_script_params: str) -> None:
"""Set dynamic manifest parameters fetched at request time."""
self.manifest = None
self.session_manifest = True
self.manifest_script = manifest_script_params
def get_streaming_urls(self) -> List[str]:
"""Return all relevant URLs."""
urls = []
if self.manifest:
urls.append(self.manifest)
if self.license_url:
urls.append(self.license_url)
if self.certificate_url:
urls.append(self.certificate_url)
return urls
def requires_drm(self) -> bool:
return bool(self.drm_config) or bool(self.license_url)
@property
def dynamic_manifest(self) -> bool:
return self.session_manifest
@dynamic_manifest.setter
def dynamic_manifest(self, value: bool):
self.session_manifest = value
@property
def requires_session_manifest(self) -> bool:
return self.session_manifest
2026-08-12 09:45:25 +02:00
# --- Serialization ---
2026-02-27 12:14:43 +01:00
def to_dict(self) -> Dict:
result = {
"Name": self.name,
"Id": self.content_id,
"Provider": self.provider,
"LogoUrl": self.logo_url,
"Quality": self.quality,
"Mode": self.mode,
"SessionManifest": self.session_manifest,
"Manifest": self.manifest,
"ManifestScript": self.manifest_script,
"CdmType": self.cdm_type,
"UseCdm": self.use_cdm,
"Cdm": self.cdm,
"CdmMode": self.cdm_mode,
"Video": self.video,
"OnDemand": self.on_demand,
"SpeedUp": self.speed_up,
"ContentType": self.content_type,
"Country": self.country,
"Language": self.language,
"StreamingFormat": self.streaming_format,
2026-08-08 19:40:19 +02:00
"LicenseUrl": self.license_url,
"CertificateUrl": self.certificate_url,
2026-08-12 09:45:25 +02:00
# Pricing derived fields
"IsFree": self.is_free,
"RequiresSubscription": self.requires_subscription,
"RequiresPayment": self.requires_payment,
"HasPricing": self.has_pricing,
2026-02-27 12:14:43 +01:00
}
if self.drm_config:
result["DrmConfig"] = self.drm_config.to_dict()
2026-08-12 09:45:25 +02:00
if self.pricing:
result["Pricing"] = self.pricing.to_dict()
2026-02-27 12:14:43 +01:00
return result