mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-23 09:32:22 +02:00
update models
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .content import Content, Quality
|
||||
from .content import Content
|
||||
from .quality import Quality
|
||||
from ..utils.logger import logger
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# streaming_providers/base/models/content.py
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
from decimal import Decimal
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from .drm import DRMConfig
|
||||
|
||||
from .pricing import AccessType, PricePoint, Pricing
|
||||
|
||||
class StreamingMode:
|
||||
"""Enum for streaming modes"""
|
||||
@@ -19,16 +19,6 @@ class ContentType:
|
||||
MOVIE = "MOVIE"
|
||||
RADIO = "RADIO"
|
||||
|
||||
|
||||
class Quality:
|
||||
"""Enum for stream quality"""
|
||||
SD = "SD"
|
||||
HD = "HD"
|
||||
UHD = "UHD"
|
||||
FOUR_K = "4K"
|
||||
AUDIO = "AUDIO"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Content:
|
||||
"""
|
||||
@@ -75,6 +65,106 @@ class Content:
|
||||
certificate_url: Optional[str] = None
|
||||
streaming_format: Optional[str] = None
|
||||
|
||||
# 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 ---
|
||||
|
||||
def set_static_manifest(self, manifest_url: str) -> None:
|
||||
"""Set a static manifest URL."""
|
||||
self.manifest = manifest_url
|
||||
@@ -113,6 +203,8 @@ class Content:
|
||||
def requires_session_manifest(self) -> bool:
|
||||
return self.session_manifest
|
||||
|
||||
# --- Serialization ---
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
result = {
|
||||
"Name": self.name,
|
||||
@@ -137,7 +229,14 @@ class Content:
|
||||
"StreamingFormat": self.streaming_format,
|
||||
"LicenseUrl": self.license_url,
|
||||
"CertificateUrl": self.certificate_url,
|
||||
# Pricing derived fields
|
||||
"IsFree": self.is_free,
|
||||
"RequiresSubscription": self.requires_subscription,
|
||||
"RequiresPayment": self.requires_payment,
|
||||
"HasPricing": self.has_pricing,
|
||||
}
|
||||
if self.drm_config:
|
||||
result["DrmConfig"] = self.drm_config.to_dict()
|
||||
if self.pricing:
|
||||
result["Pricing"] = self.pricing.to_dict()
|
||||
return result
|
||||
@@ -0,0 +1,159 @@
|
||||
# streaming_providers/base/models/pricing.py
|
||||
from decimal import Decimal
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Dict
|
||||
from .quality import Quality
|
||||
|
||||
|
||||
class AccessType(Enum):
|
||||
"""How content can be accessed monetarily."""
|
||||
FREE = "free" # No payment, no ads
|
||||
AVOD = "avod" # Ad-supported free
|
||||
SVOD = "svod" # Subscription included
|
||||
TVOD_RENTAL = "tvod_rental" # Time-limited rental
|
||||
TVOD_PURCHASE = "tvod_purchase" # Buy-to-own (EST)
|
||||
PPV_LIVE = "ppv_live" # Pay-per-view live event
|
||||
PPV_REPLAY = "ppv_replay" # Pay-per-view replay window
|
||||
SVOD_PPV = "svod_ppv" # Subscription + extra PPV surcharge
|
||||
|
||||
|
||||
@dataclass
|
||||
class PricePoint:
|
||||
"""A specific price for a region/quality/time period."""
|
||||
amount: Decimal
|
||||
currency: str # ISO 4217
|
||||
sku: Optional[str] = None # Billing system ID
|
||||
quality: Optional[Quality] = None # "SD", "HD", "4K"
|
||||
region: Optional[str] = None # "DE", "AT", "CH", etc.
|
||||
valid_from: Optional[datetime] = None
|
||||
valid_until: Optional[datetime] = None
|
||||
tax_inclusive: bool = True # EU requirement
|
||||
|
||||
def is_active(self, at: Optional[datetime] = None) -> bool:
|
||||
"""Check if this price point is currently valid."""
|
||||
now = at or datetime.now()
|
||||
if self.valid_from and now < self.valid_from:
|
||||
return False
|
||||
if self.valid_until and now > self.valid_until:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pricing:
|
||||
"""
|
||||
Monetization model for content.
|
||||
|
||||
None/unknown pricing should never be treated as free - this is a revenue-leak
|
||||
risk. Always handle unknown explicitly in entitlement checks.
|
||||
"""
|
||||
|
||||
# Access model
|
||||
access_type: AccessType # No default - must be explicit
|
||||
|
||||
# Price points (empty for FREE/AVOD/SVOD)
|
||||
price_points: List[PricePoint] = field(default_factory=list)
|
||||
|
||||
# Subscription gates (ANY grants access)
|
||||
required_tiers: List[str] = field(default_factory=list) # "premium", "basic"
|
||||
required_bouquets: List[str] = field(default_factory=list) # "sports", "movies"
|
||||
|
||||
# Time windows (hours)
|
||||
rental_duration_hours: Optional[int] = None # TVOD_RENTAL
|
||||
catchup_duration_hours: Optional[int] = None # Linear catch-up feature
|
||||
replay_window_hours: Optional[int] = None # PPV event replay
|
||||
preview_minutes: Optional[int] = None # Free preview
|
||||
|
||||
# SVOD_PPV specific
|
||||
ppv_is_surcharge: bool = True # True = extra on top of subscription
|
||||
|
||||
# Metadata
|
||||
description: Optional[str] = None # UI display text
|
||||
tax_class: Optional[str] = None # VAT rate group
|
||||
|
||||
# --- Derived predicates ---
|
||||
|
||||
@property
|
||||
def is_free_at_point_of_use(self) -> bool:
|
||||
"""User pays nothing at consumption time."""
|
||||
return self.access_type in (AccessType.FREE, AccessType.AVOD)
|
||||
|
||||
@property
|
||||
def requires_subscription(self) -> bool:
|
||||
"""User must have an active subscription."""
|
||||
return self.access_type in (AccessType.SVOD, AccessType.SVOD_PPV)
|
||||
|
||||
@property
|
||||
def requires_transactional_payment(self) -> bool:
|
||||
"""User must make a one-time payment."""
|
||||
return self.access_type in (
|
||||
AccessType.TVOD_RENTAL,
|
||||
AccessType.TVOD_PURCHASE,
|
||||
AccessType.PPV_LIVE,
|
||||
AccessType.PPV_REPLAY,
|
||||
AccessType.SVOD_PPV,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_ad_supported(self) -> bool:
|
||||
"""Content includes ads (FAST/AVOD)."""
|
||||
return self.access_type == AccessType.AVOD
|
||||
|
||||
@property
|
||||
def has_time_limit(self) -> bool:
|
||||
"""Access expires after a fixed time."""
|
||||
return (
|
||||
self.access_type == AccessType.TVOD_RENTAL
|
||||
or self.replay_window_hours is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def primary_price(self) -> Optional[PricePoint]:
|
||||
"""Get the first active price point (simplified access)."""
|
||||
active = [p for p in self.price_points if p.is_active()]
|
||||
return active[0] if active else None
|
||||
|
||||
def get_price_for_region(self, region: str, quality: Optional[str] = None) -> Optional[PricePoint]:
|
||||
"""Get best matching price point for region/quality."""
|
||||
matches = [
|
||||
p for p in self.price_points
|
||||
if p.is_active() and p.region == region
|
||||
]
|
||||
if quality:
|
||||
matches = [p for p in matches if p.quality == quality]
|
||||
return matches[0] if matches else None
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return {
|
||||
"access_type": self.access_type.value,
|
||||
"price_points": [
|
||||
{
|
||||
"amount": str(p.amount), # Decimal → str for JSON
|
||||
"currency": p.currency,
|
||||
"sku": p.sku,
|
||||
"quality_label": p.quality,
|
||||
"region": p.region,
|
||||
"valid_from": p.valid_from.isoformat() if p.valid_from else None,
|
||||
"valid_until": p.valid_until.isoformat() if p.valid_until else None,
|
||||
"tax_inclusive": p.tax_inclusive,
|
||||
}
|
||||
for p in self.price_points
|
||||
],
|
||||
"required_tiers": self.required_tiers,
|
||||
"required_bouquets": self.required_bouquets,
|
||||
"rental_duration_hours": self.rental_duration_hours,
|
||||
"catchup_duration_hours": self.catchup_duration_hours,
|
||||
"replay_window_hours": self.replay_window_hours,
|
||||
"preview_minutes": self.preview_minutes,
|
||||
"ppv_is_surcharge": self.ppv_is_surcharge,
|
||||
"description": self.description,
|
||||
"tax_class": self.tax_class,
|
||||
# Derived
|
||||
"is_free_at_point_of_use": self.is_free_at_point_of_use,
|
||||
"requires_subscription": self.requires_subscription,
|
||||
"requires_transactional_payment": self.requires_transactional_payment,
|
||||
"is_ad_supported": self.is_ad_supported,
|
||||
"has_time_limit": self.has_time_limit,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# streaming_providers/base/models/quality.py
|
||||
from enum import Enum
|
||||
|
||||
class Quality(str, Enum):
|
||||
"""Stream quality levels."""
|
||||
SD = "SD"
|
||||
HD = "HD"
|
||||
UHD = "UHD"
|
||||
FOUR_K = "4K"
|
||||
AUDIO = "AUDIO"
|
||||
|
||||
class StreamingFormat(str, Enum):
|
||||
"""Streaming protocol formats."""
|
||||
HLS = "hls"
|
||||
DASH = "dash"
|
||||
MSS = "mss"
|
||||
@@ -1,37 +1,31 @@
|
||||
"""
|
||||
Subscription models for provider packages and user entitlements.
|
||||
"""
|
||||
|
||||
# streaming_providers/base/models/subscription.py
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Set
|
||||
from typing import Dict, List, Optional, Set, Any # <-- Added Any
|
||||
|
||||
from .pricing import Pricing
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubscriptionPackage:
|
||||
"""Represents a subscription package with provider-specific naming"""
|
||||
"""Represents a subscription package (a purchasable bouquet/tier)."""
|
||||
|
||||
package_id: str
|
||||
"""Internal ID (e.g., 'sports_package_2024', 'joyn_plus')"""
|
||||
|
||||
name: str
|
||||
"""Display name (e.g., 'Sports Package', 'Sky Sport', 'Joyn Plus')"""
|
||||
|
||||
tier: Optional[str] = None
|
||||
bouquet: Optional[str] = None
|
||||
pricing: Optional[Pricing] = None
|
||||
|
||||
description: Optional[str] = None
|
||||
"""Optional description of the package"""
|
||||
|
||||
price_info: Optional[str] = None
|
||||
"""Optional price information (e.g., '€9.99/month', 'included')"""
|
||||
|
||||
channel_ids: List[str] = field(default_factory=list)
|
||||
"""List of channel IDs included in this package"""
|
||||
|
||||
metadata: Dict[str, any] = field(default_factory=dict)
|
||||
"""Provider-specific metadata (e.g., {'sky_id': 'SPORT1', 'category': 'sports'})"""
|
||||
# Fix is here:
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
"""Provider-specific metadata"""
|
||||
|
||||
@property
|
||||
def channel_count(self) -> int:
|
||||
"""Number of channels in this package"""
|
||||
return len(self.channel_ids)
|
||||
|
||||
|
||||
@@ -40,72 +34,73 @@ class UserSubscription:
|
||||
"""User's subscription status for a provider"""
|
||||
|
||||
provider: str
|
||||
"""Provider name (e.g., 'joyn', 'magenta')"""
|
||||
|
||||
country: str
|
||||
"""Country code (e.g., 'DE', 'AT')"""
|
||||
|
||||
active: bool = False
|
||||
"""Whether the subscription is currently active"""
|
||||
"""Whether the account subscription is currently active"""
|
||||
|
||||
packages: List[SubscriptionPackage] = field(default_factory=list)
|
||||
"""All subscription packages the user has access to"""
|
||||
|
||||
accessible_channel_ids: Set[str] = field(default_factory=set)
|
||||
"""Set of all channel IDs the user can access (derived from packages)"""
|
||||
"""All subscription packages the user has purchased"""
|
||||
|
||||
valid_from: Optional[datetime] = None
|
||||
"""When the subscription becomes/starts valid"""
|
||||
|
||||
valid_until: Optional[datetime] = None
|
||||
"""When the subscription expires"""
|
||||
|
||||
status_message: Optional[str] = None
|
||||
"""Human-readable status message (e.g., 'Active until 2024-12-31')"""
|
||||
|
||||
billing_status: Optional[str] = None
|
||||
"""Billing status (e.g., 'paid', 'trial', 'expired', 'cancelled')"""
|
||||
|
||||
def __post_init__(self):
|
||||
"""Populate derived fields after initialization"""
|
||||
self._update_derived_fields()
|
||||
|
||||
def _update_derived_fields(self):
|
||||
"""Update derived fields like accessible_channel_ids"""
|
||||
self.accessible_channel_ids.clear()
|
||||
"""Pre-calculate sets for fast entitlement checks"""
|
||||
self.accessible_channel_ids: Set[str] = set()
|
||||
self.active_tiers: Set[str] = set()
|
||||
self.active_bouquets: Set[str] = set()
|
||||
|
||||
if not self.active or not self._is_currently_valid():
|
||||
return
|
||||
|
||||
for package in self.packages:
|
||||
self.accessible_channel_ids.update(package.channel_ids)
|
||||
if package.tier:
|
||||
self.active_tiers.add(package.tier)
|
||||
if package.bouquet:
|
||||
self.active_bouquets.add(package.bouquet)
|
||||
|
||||
@property
|
||||
def has_packages(self) -> bool:
|
||||
"""Check if user has any packages"""
|
||||
return len(self.packages) > 0
|
||||
def _is_currently_valid(self) -> bool:
|
||||
"""Check if subscription is within valid date range"""
|
||||
now = datetime.now()
|
||||
if self.valid_from and now < self.valid_from:
|
||||
return False
|
||||
if self.valid_until and now > self.valid_until:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def package_count(self) -> int:
|
||||
"""Number of packages"""
|
||||
return len(self.packages)
|
||||
# --- Entitlement Checks ---
|
||||
|
||||
@property
|
||||
def accessible_channel_count(self) -> int:
|
||||
"""Number of accessible channels"""
|
||||
return len(self.accessible_channel_ids)
|
||||
def has_tier(self, tier: str) -> bool:
|
||||
"""Check if user has a specific tier"""
|
||||
return self.active and tier in self.active_tiers
|
||||
|
||||
@property
|
||||
def package_names(self) -> List[str]:
|
||||
"""List of package names for display"""
|
||||
return [pkg.name for pkg in self.packages]
|
||||
def has_bouquet(self, bouquet: str) -> bool:
|
||||
"""Check if user has a specific bouquet"""
|
||||
return self.active and bouquet in self.active_bouquets
|
||||
|
||||
def can_access_channel(self, channel_id: str) -> bool:
|
||||
"""Check if user can access a specific channel"""
|
||||
return self.active and channel_id in self.accessible_channel_ids
|
||||
|
||||
# --- Serialization ---
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to dictionary for JSON serialization"""
|
||||
return {
|
||||
"provider": self.provider,
|
||||
"country": self.country,
|
||||
"active": self.active,
|
||||
"has_packages": self.has_packages,
|
||||
"package_count": self.package_count,
|
||||
"package_names": self.package_names,
|
||||
"accessible_channel_count": self.accessible_channel_count,
|
||||
"has_packages": len(self.packages) > 0,
|
||||
"package_count": len(self.packages),
|
||||
"accessible_channel_count": len(self.accessible_channel_ids),
|
||||
"active_tiers": list(self.active_tiers),
|
||||
"active_bouquets": list(self.active_bouquets),
|
||||
"valid_from": self.valid_from.isoformat() if self.valid_from else None,
|
||||
"valid_until": self.valid_until.isoformat() if self.valid_until else None,
|
||||
"status_message": self.status_message,
|
||||
@@ -114,20 +109,11 @@ class UserSubscription:
|
||||
{
|
||||
"package_id": pkg.package_id,
|
||||
"name": pkg.name,
|
||||
"description": pkg.description,
|
||||
"price_info": pkg.price_info,
|
||||
"tier": pkg.tier,
|
||||
"bouquet": pkg.bouquet,
|
||||
"pricing": pkg.pricing.to_dict() if pkg.pricing else None,
|
||||
"channel_count": pkg.channel_count,
|
||||
"metadata": pkg.metadata,
|
||||
}
|
||||
for pkg in self.packages
|
||||
],
|
||||
}
|
||||
|
||||
def add_package(self, package: SubscriptionPackage):
|
||||
"""Add a package and update derived fields"""
|
||||
self.packages.append(package)
|
||||
self._update_derived_fields()
|
||||
|
||||
def can_access_channel(self, channel_id: str) -> bool:
|
||||
"""Check if user can access a specific channel"""
|
||||
return channel_id in self.accessible_channel_ids
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import unicodedata
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .content import Content
|
||||
from .content import Content, StreamingMode, ContentType
|
||||
from ..utils.logger import logger
|
||||
|
||||
|
||||
@@ -129,13 +129,10 @@ class VodCategory:
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and clean up required fields."""
|
||||
# Defensive: ensure name is a string
|
||||
if self.name is None:
|
||||
logger.warning(f"VodCategory created with None name for content_id {self.content_id}")
|
||||
# Defensive: ensure name is a valid string
|
||||
if not isinstance(self.name, str) or not self.name.strip():
|
||||
logger.warning(f"VodCategory created with invalid name for content_id {self.content_id}")
|
||||
self.name = f"Unbenannt_{self.content_id}"
|
||||
elif not isinstance(self.name, str):
|
||||
logger.warning(f"VodCategory created with non-string name '{self.name}' for content_id {self.content_id}")
|
||||
self.name = str(self.name)
|
||||
|
||||
# Ensure content_id is valid
|
||||
if not self.content_id:
|
||||
@@ -226,26 +223,26 @@ class VodItem(Content):
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and clean up required fields."""
|
||||
# Defensive: ensure name is a string
|
||||
if self.name is None:
|
||||
logger.warning(f"VodItem created with None name for content_id {self.content_id}")
|
||||
# Defensive: ensure name is a valid string
|
||||
if not isinstance(self.name, str) or not self.name.strip():
|
||||
logger.warning(f"VodItem created with invalid name for content_id {self.content_id}")
|
||||
self.name = f"Unbenanntes Video_{self.content_id}"
|
||||
elif not isinstance(self.name, str):
|
||||
logger.warning(f"VodItem created with non-string name '{self.name}' for content_id {self.content_id}")
|
||||
self.name = str(self.name)
|
||||
|
||||
# Ensure mode and content_type are set correctly for on-demand content
|
||||
if self.mode == "live":
|
||||
self.mode = "vod"
|
||||
if self.content_type == "LIVE":
|
||||
self.content_type = "VOD"
|
||||
if self.mode == StreamingMode.LIVE:
|
||||
self.mode = StreamingMode.VOD
|
||||
if self.content_type == ContentType.LIVE:
|
||||
self.content_type = ContentType.VOD
|
||||
|
||||
# Defensive: handle None for season/episode numbers
|
||||
# Defensive: handle negative season/episode numbers
|
||||
if self.season_number is not None and self.season_number < 0:
|
||||
self.season_number = None
|
||||
if self.episode_number is not None and self.episode_number < 0:
|
||||
self.episode_number = None
|
||||
|
||||
# 🔥 CRITICAL: Call parent __post_init__ to validate Pricing vs Mode!
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
if not self._slug:
|
||||
@@ -312,32 +309,32 @@ class VodItem(Content):
|
||||
# Factory methods
|
||||
@classmethod
|
||||
def create_movie(
|
||||
cls, name: str, content_id: str, provider: str, **kwargs
|
||||
cls, name: Optional[str], content_id: str, provider: str, **kwargs
|
||||
) -> "VodItem":
|
||||
# Defensive: ensure name is string
|
||||
if name is None:
|
||||
if not isinstance(name, str) or not name:
|
||||
name = f"Film_{content_id}"
|
||||
return cls(
|
||||
name=name,
|
||||
content_id=content_id,
|
||||
provider=provider,
|
||||
mode="vod",
|
||||
content_type="MOVIE",
|
||||
mode=StreamingMode.VOD,
|
||||
content_type=ContentType.MOVIE,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_episode(
|
||||
cls,
|
||||
name: str,
|
||||
name: Optional[str],
|
||||
content_id: str,
|
||||
provider: str,
|
||||
season_number: int,
|
||||
episode_number: int,
|
||||
season_number: Optional[int],
|
||||
episode_number: Optional[int],
|
||||
**kwargs,
|
||||
) -> "VodItem":
|
||||
# Defensive: ensure name is string
|
||||
if name is None:
|
||||
if not isinstance(name, str) or not name:
|
||||
name = f"Episode_{content_id}"
|
||||
# Defensive: handle negative season/episode numbers
|
||||
if season_number is not None and season_number < 0:
|
||||
@@ -349,8 +346,8 @@ class VodItem(Content):
|
||||
name=name,
|
||||
content_id=content_id,
|
||||
provider=provider,
|
||||
mode="vod",
|
||||
content_type="SERIES",
|
||||
mode=StreamingMode.VOD,
|
||||
content_type=ContentType.SERIES,
|
||||
season_number=season_number,
|
||||
episode_number=episode_number,
|
||||
**kwargs,
|
||||
|
||||
Reference in New Issue
Block a user