mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-25 02:22:44 +02:00
Add discovery
This commit is contained in:
@@ -28,14 +28,14 @@ class ChannelOperations:
|
||||
channels = provider.get_channels(**kwargs)
|
||||
logger.info(f"Retrieved {len(channels)} channels from '{provider_name}'")
|
||||
|
||||
if fetch_manifests and not provider.uses_dynamic_manifests:
|
||||
enriched = []
|
||||
for channel in channels:
|
||||
enriched_channel = provider.enrich_channel_data(channel, **kwargs)
|
||||
if enriched_channel:
|
||||
enriched.append(enriched_channel)
|
||||
logger.info(f"Enriched {len(enriched)}/{len(channels)} channels")
|
||||
return enriched
|
||||
# if fetch_manifests and not provider.uses_dynamic_manifests:
|
||||
# enriched = []
|
||||
# for channel in channels:
|
||||
# enriched_channel = provider.enrich_channel_data(channel, **kwargs)
|
||||
# if enriched_channel:
|
||||
# enriched.append(enriched_channel)
|
||||
# logger.info(f"Enriched {len(enriched)}/{len(channels)} channels")
|
||||
# return enriched
|
||||
|
||||
return channels
|
||||
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
# streaming_providers/base/models/__init__.py
|
||||
from .channel import Channel, StreamingChannel
|
||||
from .content import Content, ContentType, Quality, StreamingMode
|
||||
from .drm import DRMConfig, DRMSystem, LicenseConfig, LicenseUnwrapperParams
|
||||
from .streaming_channel import StreamingChannel
|
||||
from .event import Event, EventStatus
|
||||
from .subscription import SubscriptionPackage, UserSubscription
|
||||
|
||||
__all__ = [
|
||||
# Content hierarchy
|
||||
"Content",
|
||||
"Channel",
|
||||
"Event",
|
||||
# Backward compatibility
|
||||
"StreamingChannel",
|
||||
# Enums
|
||||
"StreamingMode",
|
||||
"ContentType",
|
||||
"Quality",
|
||||
"EventStatus",
|
||||
# DRM
|
||||
"DRMConfig",
|
||||
"LicenseConfig",
|
||||
"LicenseUnwrapperParams",
|
||||
"DRMSystem",
|
||||
# Subscription
|
||||
"SubscriptionPackage",
|
||||
"UserSubscription",
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,126 @@
|
||||
# streaming_providers/base/models/channel.py
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .content import Content, Quality
|
||||
from ..utils.logger import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class Channel(Content):
|
||||
"""
|
||||
Represents a continuously available live channel or VOD stream.
|
||||
"""
|
||||
|
||||
channel_number: Optional[int] = None
|
||||
is_radio: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
self._validate_fields()
|
||||
if self.is_radio and self.content_type == "LIVE":
|
||||
self.content_type = "RADIO"
|
||||
self.quality = "AUDIO"
|
||||
|
||||
def _validate_fields(self):
|
||||
if self.mode == "vod" and self.content_type == "LIVE":
|
||||
logger.warning(
|
||||
f"Channel {self.name} ({self.content_id}): "
|
||||
f"VOD mode with LIVE content_type - consider changing to VOD"
|
||||
)
|
||||
if self.session_manifest and self.manifest:
|
||||
logger.warning(
|
||||
f"Channel {self.name} ({self.content_id}): "
|
||||
f"session_manifest=True but manifest URL is set - manifest will be ignored"
|
||||
)
|
||||
|
||||
# Backward compatibility alias
|
||||
@property
|
||||
def channel_id(self) -> str:
|
||||
return self.content_id
|
||||
|
||||
@channel_id.setter
|
||||
def channel_id(self, value: str):
|
||||
self.content_id = value
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
result = super().to_dict()
|
||||
result["ChannelNumber"] = self.channel_number
|
||||
result["IsRadio"] = self.is_radio
|
||||
return result
|
||||
|
||||
def is_audio_content(self) -> bool:
|
||||
return self.is_radio
|
||||
|
||||
def detect_and_set_radio(self) -> None:
|
||||
if self.is_radio:
|
||||
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
|
||||
if not self.quality or self.quality.upper() not in [q for q in vars(Quality).values() if isinstance(q, str)]:
|
||||
self.quality = "AUDIO"
|
||||
if self.content_type == "LIVE":
|
||||
self.content_type = "RADIO"
|
||||
|
||||
def validate(self) -> List[str]:
|
||||
warnings = []
|
||||
if not self.manifest and not self.manifest_script:
|
||||
warnings.append("No manifest URL or manifest script provided")
|
||||
if self.license_url and not self.drm_config:
|
||||
warnings.append("License URL provided but no DRM configuration")
|
||||
if self.mode == "vod" and self.content_type == "LIVE":
|
||||
warnings.append("VOD mode should not have LIVE content_type")
|
||||
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
|
||||
|
||||
# Factory methods
|
||||
@classmethod
|
||||
def create_live_channel(
|
||||
cls, name: str, channel_id: str, provider: str, **kwargs
|
||||
) -> "Channel":
|
||||
return cls(
|
||||
name=name,
|
||||
content_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
|
||||
) -> "Channel":
|
||||
return cls(
|
||||
name=name,
|
||||
content_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
|
||||
) -> "Channel":
|
||||
return cls(
|
||||
name=name,
|
||||
content_id=channel_id,
|
||||
provider=provider,
|
||||
is_radio=True,
|
||||
content_type="RADIO",
|
||||
quality="AUDIO",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Backward compatibility alias — all existing code using StreamingChannel continues to work
|
||||
StreamingChannel = Channel
|
||||
@@ -0,0 +1,141 @@
|
||||
# streaming_providers/base/models/content.py
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .drm import DRMConfig
|
||||
|
||||
|
||||
class StreamingMode:
|
||||
"""Enum for streaming modes"""
|
||||
LIVE = "live"
|
||||
VOD = "vod"
|
||||
|
||||
|
||||
class ContentType:
|
||||
"""Enum for content types"""
|
||||
LIVE = "LIVE"
|
||||
VOD = "VOD"
|
||||
SERIES = "SERIES"
|
||||
MOVIE = "MOVIE"
|
||||
RADIO = "RADIO"
|
||||
|
||||
|
||||
class Quality:
|
||||
"""Enum for stream quality"""
|
||||
SD = "SD"
|
||||
HD = "HD"
|
||||
UHD = "UHD"
|
||||
FOUR_K = "4K"
|
||||
AUDIO = "AUDIO"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Content:
|
||||
"""
|
||||
Base dataclass for all provider content (channels, events, etc.)
|
||||
Contains all fields shared between content types.
|
||||
"""
|
||||
|
||||
# Core identification
|
||||
name: str
|
||||
content_id: str
|
||||
provider: str
|
||||
|
||||
# Visual
|
||||
logo_url: Optional[str] = None
|
||||
|
||||
# Streaming configuration
|
||||
mode: str = "live"
|
||||
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_config: Optional[DRMConfig] = None
|
||||
|
||||
# Video settings
|
||||
video: str = "best"
|
||||
on_demand: bool = True
|
||||
speed_up: bool = True
|
||||
|
||||
# Metadata
|
||||
quality: Optional[str] = None
|
||||
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
|
||||
|
||||
def set_static_manifest(self, manifest_url: str) -> None:
|
||||
"""Set a static manifest URL."""
|
||||
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 fetched at request time."""
|
||||
self.manifest = None
|
||||
self.session_manifest = True
|
||||
self.manifest_script = manifest_script_params
|
||||
|
||||
def get_streaming_urls(self) -> List[str]:
|
||||
"""Return all relevant URLs."""
|
||||
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:
|
||||
return bool(self.drm_config) or bool(self.license_url)
|
||||
|
||||
@property
|
||||
def dynamic_manifest(self) -> bool:
|
||||
return self.session_manifest
|
||||
|
||||
@dynamic_manifest.setter
|
||||
def dynamic_manifest(self, value: bool):
|
||||
self.session_manifest = value
|
||||
|
||||
@property
|
||||
def requires_session_manifest(self) -> bool:
|
||||
return self.session_manifest
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
result = {
|
||||
"Name": self.name,
|
||||
"Id": self.content_id,
|
||||
"Provider": self.provider,
|
||||
"LogoUrl": self.logo_url,
|
||||
"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,
|
||||
}
|
||||
if self.drm_config:
|
||||
result["DrmConfig"] = self.drm_config.to_dict()
|
||||
return result
|
||||
@@ -0,0 +1,109 @@
|
||||
# streaming_providers/base/models/event.py
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .content import Content
|
||||
|
||||
|
||||
class EventStatus(Enum):
|
||||
SCHEDULED = "SCHEDULED"
|
||||
LIVE = "LIVE"
|
||||
ENDED = "ENDED"
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event(Content):
|
||||
"""
|
||||
Represents a one-time live or scheduled event (concert, sports match, etc.)
|
||||
"""
|
||||
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
status: EventStatus = EventStatus.SCHEDULED
|
||||
|
||||
def __post_init__(self):
|
||||
# Auto-derive status from times if left at default
|
||||
if self.status == EventStatus.SCHEDULED and self.start_time and self.end_time:
|
||||
now = datetime.now()
|
||||
if self.start_time <= now <= self.end_time:
|
||||
self.status = EventStatus.LIVE
|
||||
elif now > self.end_time:
|
||||
self.status = EventStatus.ENDED
|
||||
|
||||
# Semantic alias
|
||||
@property
|
||||
def event_id(self) -> str:
|
||||
return self.content_id
|
||||
|
||||
@event_id.setter
|
||||
def event_id(self, value: str):
|
||||
self.content_id = value
|
||||
|
||||
@property
|
||||
def is_live(self) -> bool:
|
||||
return self.status == EventStatus.LIVE
|
||||
|
||||
@property
|
||||
def is_upcoming(self) -> bool:
|
||||
return self.status == EventStatus.SCHEDULED
|
||||
|
||||
@property
|
||||
def duration_minutes(self) -> Optional[int]:
|
||||
if self.start_time and self.end_time:
|
||||
return int((self.end_time - self.start_time).total_seconds() / 60)
|
||||
return None
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
result = super().to_dict()
|
||||
result.update({
|
||||
"StartTime": self.start_time.isoformat() if self.start_time else None,
|
||||
"EndTime": self.end_time.isoformat() if self.end_time else None,
|
||||
"Status": self.status.value,
|
||||
"DurationMinutes": self.duration_minutes,
|
||||
})
|
||||
return result
|
||||
|
||||
def validate(self) -> List[str]:
|
||||
warnings = []
|
||||
if not self.manifest and not self.manifest_script:
|
||||
warnings.append("No manifest URL or manifest script provided")
|
||||
if self.license_url and not self.drm_config:
|
||||
warnings.append("License URL provided but no DRM configuration")
|
||||
if self.start_time and self.end_time and self.start_time >= self.end_time:
|
||||
warnings.append("start_time must be before end_time")
|
||||
return warnings
|
||||
|
||||
# Factory methods
|
||||
@classmethod
|
||||
def create_live_event(
|
||||
cls, name: str, event_id: str, provider: str, **kwargs
|
||||
) -> "Event":
|
||||
return cls(
|
||||
name=name,
|
||||
content_id=event_id,
|
||||
provider=provider,
|
||||
status=EventStatus.LIVE,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_scheduled_event(
|
||||
cls,
|
||||
name: str,
|
||||
event_id: str,
|
||||
provider: str,
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
**kwargs,
|
||||
) -> "Event":
|
||||
return cls(
|
||||
name=name,
|
||||
content_id=event_id,
|
||||
provider=provider,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -1,333 +0,0 @@
|
||||
# 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
|
||||
@@ -15,9 +15,8 @@ from enum import Enum
|
||||
from typing import Any, Callable, ClassVar, Dict, List, Optional
|
||||
|
||||
from ..providers.auth import AuthContext, AuthStatus
|
||||
from .models.drm import DRMConfig
|
||||
from .models.proxy_models import ProxyConfig
|
||||
from .models.streaming_channel import StreamingChannel
|
||||
from .models import DRMConfig, Event, StreamingChannel
|
||||
from .models.subscription import SubscriptionPackage, UserSubscription
|
||||
from .network import HTTPManager, HTTPManagerFactory
|
||||
from .utils.logger import logger
|
||||
@@ -212,6 +211,27 @@ class StreamingProvider(ABC):
|
||||
"""Fetch channels from the provider"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
"""
|
||||
Fetch one-time events (concerts, sports matches, etc.) from the provider.
|
||||
|
||||
Args:
|
||||
start_time: Optional lower bound — only return events ending after this time.
|
||||
end_time: Optional upper bound — only return events starting before this time.
|
||||
If neither is provided, the provider returns all known events
|
||||
(both upcoming and currently live).
|
||||
|
||||
Returns:
|
||||
List of Event objects, or empty list if provider has no events.
|
||||
"""
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def get_drm(self, channel_id: str, **kwargs) -> List[DRMConfig]:
|
||||
"""Get all DRM configurations for a channel by ID"""
|
||||
@@ -252,11 +272,11 @@ class StreamingProvider(ABC):
|
||||
"""Get complete EPG data for this provider in XMLTV format"""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def enrich_channel_data(
|
||||
self, channel: StreamingChannel, **kwargs
|
||||
) -> Optional[StreamingChannel]:
|
||||
"""Enrich channel with additional data including manifest URL"""
|
||||
"""Optional: Enrich channel with additional data including manifest URL.
|
||||
Override in subclasses that need pre-fetching of manifests/DRM before playback."""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
@@ -1210,4 +1230,4 @@ class StreamingProvider(ABC):
|
||||
Returns:
|
||||
Dictionary with provider-specific information
|
||||
"""
|
||||
return {}
|
||||
return {}
|
||||
@@ -7,13 +7,12 @@ Provides access to BH Telecom's live TV streaming service
|
||||
with support for channel discovery and manifest URLs.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.models import DRMConfig, StreamingChannel, Event
|
||||
from ...base.provider import AuthType, StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
from .constants import (
|
||||
@@ -213,6 +212,14 @@ class BHTelecomProvider(StreamingProvider):
|
||||
return self._channels_cache
|
||||
raise
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def _parse_channel(self, entry: Dict, base_url: str) -> StreamingChannel:
|
||||
channel_id = entry.get("ch")
|
||||
channel_name = entry.get("title")
|
||||
@@ -227,7 +234,7 @@ class BHTelecomProvider(StreamingProvider):
|
||||
|
||||
return StreamingChannel(
|
||||
name=channel_name,
|
||||
channel_id=channel_id,
|
||||
content_id=channel_id,
|
||||
provider=self.provider_name,
|
||||
logo_url=logo_url,
|
||||
manifest=manifest_url,
|
||||
|
||||
@@ -286,7 +286,7 @@ class DiscoveryChannel:
|
||||
|
||||
channel = StreamingChannel(
|
||||
name=self.name,
|
||||
channel_id=self.channel_id,
|
||||
content_id=self.channel_id,
|
||||
provider=provider_name,
|
||||
logo_url=self.logo_url,
|
||||
mode=self.mode,
|
||||
|
||||
@@ -13,9 +13,8 @@ import base64
|
||||
from datetime import datetime
|
||||
from typing import ClassVar, Dict, List, Optional, Any
|
||||
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig,StreamingChannel, Event
|
||||
from ...base.provider import AuthType, StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
|
||||
@@ -368,6 +367,14 @@ class DiscoveryProvider(StreamingProvider):
|
||||
logger.error(f"Error fetching channels: {e}")
|
||||
return []
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def _extract_distribution_channels(
|
||||
self, data: Dict
|
||||
) -> List[DiscoveryChannel]:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# lib/streaming_providers/providers/hrti/provider.py
|
||||
import json
|
||||
import datetime
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, LicenseUnwrapperParams
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, LicenseUnwrapperParams, StreamingChannel, Event
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.provider import AuthType, StreamingProvider
|
||||
from ...base.utils import logger
|
||||
from .auth import HRTiAuthenticator
|
||||
@@ -187,6 +187,14 @@ class HRTiProvider(StreamingProvider):
|
||||
logger.error(f"Error parsing HRTi channels: {e}")
|
||||
return []
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def _parse_channel_data(self, channel_data: Dict) -> Optional[StreamingChannel]:
|
||||
"""
|
||||
Parse HRTi channel data to StreamingChannel
|
||||
@@ -206,7 +214,7 @@ class HRTiProvider(StreamingProvider):
|
||||
# Create channel object
|
||||
channel = StreamingChannel(
|
||||
name=name,
|
||||
channel_id=channel_id,
|
||||
content_id=channel_id,
|
||||
provider=self.provider_name,
|
||||
logo_url=icon_url,
|
||||
mode="live",
|
||||
|
||||
@@ -153,7 +153,7 @@ class JoynChannel:
|
||||
"""
|
||||
return StreamingChannel(
|
||||
name=self.name,
|
||||
channel_id=self.channel_id,
|
||||
content_id=self.channel_id,
|
||||
provider=provider_name,
|
||||
logo_url=self.logo_url,
|
||||
mode=self.mode,
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import urllib.parse
|
||||
from base64 import b64decode
|
||||
from datetime import datetime, timedelta
|
||||
from json import dumps
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.provider import AuthType, StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
from .auth import JoynAuthenticator
|
||||
@@ -270,6 +270,14 @@ class JoynProvider(StreamingProvider):
|
||||
except Exception as e:
|
||||
raise Exception(f"Error fetching channels from GraphQL: {e}")
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def _process_graphql_response(self, response_data: Dict) -> List[StreamingChannel]:
|
||||
"""Process GraphQL response and convert to StreamingChannel objects"""
|
||||
if "data" not in response_data or "liveStreams" not in response_data["data"]:
|
||||
|
||||
@@ -13,11 +13,11 @@ Environment Variable:
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import datetime
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
from ...base.models.drm import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.provider import StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
from ...base.utils.vfs import get_vfs
|
||||
@@ -1172,6 +1172,14 @@ class M3UProvider(StreamingProvider):
|
||||
return self._channels_cache
|
||||
raise
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def populate_streaming_data(
|
||||
self,
|
||||
channels: List[StreamingChannel],
|
||||
|
||||
@@ -160,7 +160,7 @@ class Magenta2Channel:
|
||||
"""
|
||||
return StreamingChannel(
|
||||
name=self.name,
|
||||
channel_id=self.channel_id,
|
||||
content_id=self.channel_id,
|
||||
provider=provider_name,
|
||||
logo_url=self.logo_url,
|
||||
mode=self.mode,
|
||||
|
||||
@@ -4,14 +4,14 @@ import base64
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import datetime
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Tuple
|
||||
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event
|
||||
from ...base.models.auth import AuthState
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.network import HTTPManagerFactory, ProxyConfigManager
|
||||
from ...base.provider import StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
@@ -799,6 +799,14 @@ class Magenta2Provider(StreamingProvider):
|
||||
except Exception as e:
|
||||
raise Exception(f"Error fetching channels from Magenta2 API: {e}")
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def _get_channels_from_mpx_feeds(self) -> List[StreamingChannel]:
|
||||
"""Get channels from MPX feeds discovered in manifest"""
|
||||
try:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# streaming_providers/providers/magentaeu/provider.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
import datetime
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
from ...base.auth import UserPasswordCredentials
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.network import ProxyConfigManager
|
||||
from ...base.provider import StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
@@ -237,7 +237,7 @@ class MagentaEUProvider(StreamingProvider):
|
||||
# Create streaming channel
|
||||
streaming_channel = StreamingChannel(
|
||||
name=title,
|
||||
channel_id=station_id or pid or title,
|
||||
content_id=station_id or pid or title,
|
||||
provider=self.provider_name,
|
||||
logo_url=logo,
|
||||
mode="live",
|
||||
@@ -265,6 +265,14 @@ class MagentaEUProvider(StreamingProvider):
|
||||
|
||||
return channels
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def enrich_channel_data(
|
||||
self, channel: StreamingChannel, **kwargs
|
||||
) -> Optional[StreamingChannel]:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# lib/streaming_providers/providers/rtlplus/provider.py
|
||||
import json
|
||||
import datetime
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.provider import StreamingProvider
|
||||
from ...base.utils import logger
|
||||
from .auth import RTLPlusAuthenticator
|
||||
@@ -158,6 +158,14 @@ class RTLPlusProvider(StreamingProvider):
|
||||
logger.error(f"Error parsing RTL+ channels: {e}")
|
||||
return []
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def _parse_station_to_channel(self, station: Dict) -> Optional[StreamingChannel]:
|
||||
"""
|
||||
Parse a station object from RTL+ API to StreamingChannel
|
||||
@@ -186,7 +194,7 @@ class RTLPlusProvider(StreamingProvider):
|
||||
# Create channel object
|
||||
channel = StreamingChannel(
|
||||
name=name,
|
||||
channel_id=channel_id,
|
||||
content_id=channel_id,
|
||||
provider=self.provider_name,
|
||||
logo_url=logo_url,
|
||||
mode="live",
|
||||
|
||||
@@ -16,11 +16,11 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
from typing import ClassVar, Dict, List, Optional, Any
|
||||
|
||||
from ...base.models.drm import DRMConfig, DRMSystem, LicenseConfig
|
||||
from ...base.models import DRMConfig, DRMSystem, LicenseConfig, StreamingChannel, Event
|
||||
from ...base.models.proxy_models import ProxyConfig
|
||||
from ...base.models.streaming_channel import StreamingChannel
|
||||
from ...base.provider import StreamingProvider
|
||||
from ...base.utils.logger import logger
|
||||
from ...base.utils.vfs import get_vfs
|
||||
@@ -726,6 +726,14 @@ class ScriptsProvider(StreamingProvider):
|
||||
|
||||
return channels
|
||||
|
||||
def get_events(
|
||||
self,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
**kwargs,
|
||||
) -> List[Event]:
|
||||
return []
|
||||
|
||||
def get_manifest(
|
||||
self,
|
||||
channel_id: str,
|
||||
|
||||
Reference in New Issue
Block a user