Files
script.service.ultimate/lib/streaming_providers/base/models/streaming_channel.py
T
2026-02-13 16:56:51 +01:00

334 lines
10 KiB
Python

# streaming_providers/base/models.py
import logging
from dataclasses import dataclass
from enum import Enum
from typing import Dict, List, Optional
from .drm import DRMConfig
# Setup logging for validation warnings
logger = logging.getLogger(__name__)
class StreamingMode(Enum):
"""Enum for streaming modes"""
LIVE = "live"
VOD = "vod"
class ContentType(Enum):
"""Enum for content types"""
LIVE = "LIVE"
VOD = "VOD"
SERIES = "SERIES"
MOVIE = "MOVIE"
RADIO = "RADIO"
class Quality(Enum):
"""Enum for stream quality"""
SD = "SD"
HD = "HD"
UHD = "UHD"
FOUR_K = "4K"
AUDIO = "AUDIO"
@dataclass
class StreamingChannel:
"""
Universal channel representation for all providers
Backward Compatibility Note:
- All existing fields remain unchanged
- New fields have default values
- All existing methods remain functional
"""
# Core identification
name: str
channel_id: str
provider: str # 'joyn', 'zdf', 'ard', etc.
# Visual elements
logo_url: Optional[str] = None
# Channel metadata
channel_number: Optional[int] = None # Display channel number (e.g., 1, 3, 99)
quality: Optional[str] = None # 'SD', 'HD', 'UHD', '4K'
# Streaming configuration
mode: str = "live" # "live" or "vod" - kept as string for compatibility
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 Configuration
drm_config: Optional[DRMConfig] = None
# Video settings
video: str = "best"
on_demand: bool = True
speed_up: bool = True
# Additional metadata
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
# NEW: Radio support (backward compatible - defaults to False)
is_radio: bool = False
def __post_init__(self):
"""
Post-initialization processing for backward compatibility
and automatic field synchronization
"""
# Run validation checks (warnings only for backward compatibility)
self._validate_fields()
# Auto-update content_type for radio
if self.is_radio and self.content_type == "LIVE":
self.content_type = "RADIO"
self.quality = "AUDIO"
def _validate_fields(self):
"""
Internal validation method that logs warnings instead of raising errors
for backward compatibility
"""
# Validate content_type consistency
if self.mode == "vod" and self.content_type == "LIVE":
logger.warning(
f"Channel {self.name} ({self.channel_id}): "
f"VOD mode with LIVE content_type - consider changing to VOD"
)
# Validate manifest consistency
if self.session_manifest and self.manifest:
logger.warning(
f"Channel {self.name} ({self.channel_id}): "
f"session_manifest=True but manifest URL is set - manifest will be ignored"
)
def to_dict(self) -> Dict:
"""Convert to dictionary format - backward compatible with new fields added"""
result = {
"Name": self.name,
"Id": self.channel_id,
"Provider": self.provider,
"LogoUrl": self.logo_url,
"ChannelNumber": self.channel_number,
"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,
# NEW: Radio fields (backward compatible addition)
"IsRadio": self.is_radio,
}
# Add DRM config if present
if self.drm_config:
result["DrmConfig"] = self.drm_config.to_dict()
return result
def set_static_manifest(self, manifest_url: str) -> None:
"""
Set a static manifest URL (scenario 1: provider gives manifest directly)
"""
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 (scenario 3: manifest needs to be fetched at request time)
Args:
manifest_script_params: Parameters needed to fetch manifest (e.g., channel_id, api_endpoint)
"""
self.manifest = None
self.session_manifest = True
self.manifest_script = manifest_script_params
# NEW: Backward compatible enhancements
@classmethod
def create_live_channel(
cls, name: str, channel_id: str, provider: str, **kwargs
) -> "StreamingChannel":
"""
Factory method for live channels with proper defaults
Backward compatible: Existing code can continue using direct instantiation
"""
return cls(
name=name,
channel_id=channel_id,
provider=provider,
mode="live",
content_type="LIVE",
**kwargs,
)
@classmethod
def create_vod_channel(
cls, name: str, content_id: str, provider: str, **kwargs
) -> "StreamingChannel":
"""
Factory method for VOD content
Backward compatible: Existing code can continue using direct instantiation
"""
return cls(
name=name,
channel_id=content_id,
provider=provider,
mode="vod",
content_type="VOD",
**kwargs,
)
@classmethod
def create_radio_channel(
cls, name: str, channel_id: str, provider: str, **kwargs
) -> "StreamingChannel":
"""
Factory method for radio channels
Backward compatible: Existing code can continue using direct instantiation
"""
return cls(
name=name,
channel_id=channel_id,
provider=provider,
is_radio=True,
content_type="RADIO",
quality="AUDIO",
**kwargs,
)
def get_streaming_urls(self) -> List[str]:
"""
Extract all relevant URLs for logging/validation
New method - doesn't affect backward compatibility
"""
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:
"""
Check if this channel needs DRM handling
New method - doesn't affect backward compatibility
"""
return bool(self.drm_config) or bool(self.license_url)
def is_audio_content(self) -> bool:
"""
Check if this is audio-only content (radio or audio track)
New method - doesn't affect backward compatibility
"""
return self.is_radio
def detect_and_set_radio(self) -> None:
"""
Auto-detect if this is likely a radio channel
New method - doesn't affect backward compatibility
"""
if self.is_radio: # Already set
return
radio_indicators = [
self.name.lower().startswith("radio"),
"radio" in self.name.lower(),
self.quality in ["audio", "aac", "mp3", "AUDIO"],
self.description and "radio" in self.description.lower(),
self.genre and "radio" in self.genre.lower(),
]
if any(radio_indicators):
self.is_radio = True
# Update quality if not set
if not self.quality or self.quality.upper() not in [
q.value for q in Quality
]:
self.quality = "AUDIO"
# Update content_type if it's still LIVE
if self.content_type == "LIVE":
self.content_type = "RADIO"
# Compatibility properties (optional - for clearer naming)
@property
def dynamic_manifest(self) -> bool:
"""Alias for session_manifest with clearer name"""
return self.session_manifest
@dynamic_manifest.setter
def dynamic_manifest(self, value: bool):
"""Setter for dynamic_manifest alias"""
self.session_manifest = value
@property
def requires_session_manifest(self) -> bool:
"""Alternative property name for clarity"""
return self.session_manifest
def validate(self) -> List[str]:
"""
Run comprehensive validation and return list of warnings/issues
New method - doesn't affect backward compatibility
"""
warnings = []
# Check for missing required fields for streaming
if not self.manifest and not self.manifest_script:
warnings.append("No manifest URL or manifest script provided")
# Check DRM configuration
if self.license_url and not self.drm_config:
warnings.append("License URL provided but no DRM configuration")
# Check content type consistency
if self.mode == "vod" and self.content_type == "LIVE":
warnings.append("VOD mode should not have LIVE content_type")
# Check radio consistency
if self.is_radio and self.quality not in ["AUDIO", "audio", None]:
warnings.append(f"Radio channel has video quality setting: {self.quality}")
return warnings