mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-25 10:32:24 +02:00
345 lines
11 KiB
Python
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 |