Files
script.service.ultimate/lib/streaming_providers/base/models/streaming_channel.py
T
2025-12-03 11:48:36 +01:00

345 lines
11 KiB
Python

# streaming_providers/base/models.py
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Optional, List
import logging
from .drm_models 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
audio_only: bool = False
radio_frequency: Optional[str] = None # e.g., "FM 98.5"
radio_station_id: Optional[str] = None
bitrate: Optional[str] = None # e.g., "128kbps"
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-set audio_only if is_radio is True
if self.is_radio:
self.audio_only = True
# Auto-update content_type for radio
if self.is_radio and self.content_type == 'LIVE':
self.content_type = 'RADIO'
# Ensure quality is appropriate for audio content
if self.audio_only and not self.quality:
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"
)
# Validate radio fields
if self.is_radio and not self.audio_only:
logger.info(
f"Channel {self.name} ({self.channel_id}): "
f"is_radio=True but audio_only=False - auto-setting audio_only=True"
)
self.audio_only = True
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,
'AudioOnly': self.audio_only,
'RadioFrequency': self.radio_frequency,
'RadioStationId': self.radio_station_id,
'Bitrate': self.bitrate
}
# 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,
audio_only=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 or self.audio_only
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
self.audio_only = 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