# 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