diff --git a/lib/streaming_providers/base/channel_operations.py b/lib/streaming_providers/base/channel_operations.py index 95df6ec..4e42028 100644 --- a/lib/streaming_providers/base/channel_operations.py +++ b/lib/streaming_providers/base/channel_operations.py @@ -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 diff --git a/lib/streaming_providers/base/models/__init__.py b/lib/streaming_providers/base/models/__init__.py index 8ac36cd..e90c277 100644 --- a/lib/streaming_providers/base/models/__init__.py +++ b/lib/streaming_providers/base/models/__init__.py @@ -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", -] +] \ No newline at end of file diff --git a/lib/streaming_providers/base/models/channel.py b/lib/streaming_providers/base/models/channel.py new file mode 100644 index 0000000..3f4f8f1 --- /dev/null +++ b/lib/streaming_providers/base/models/channel.py @@ -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 \ No newline at end of file diff --git a/lib/streaming_providers/base/models/content.py b/lib/streaming_providers/base/models/content.py new file mode 100644 index 0000000..0057fed --- /dev/null +++ b/lib/streaming_providers/base/models/content.py @@ -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 \ No newline at end of file diff --git a/lib/streaming_providers/base/models/event.py b/lib/streaming_providers/base/models/event.py new file mode 100644 index 0000000..f9bc207 --- /dev/null +++ b/lib/streaming_providers/base/models/event.py @@ -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, + ) \ No newline at end of file diff --git a/lib/streaming_providers/base/models/streaming_channel.py b/lib/streaming_providers/base/models/streaming_channel.py deleted file mode 100644 index ca54cf2..0000000 --- a/lib/streaming_providers/base/models/streaming_channel.py +++ /dev/null @@ -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 diff --git a/lib/streaming_providers/base/provider.py b/lib/streaming_providers/base/provider.py index a3dacb1..5f43289 100644 --- a/lib/streaming_providers/base/provider.py +++ b/lib/streaming_providers/base/provider.py @@ -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 {} \ No newline at end of file diff --git a/lib/streaming_providers/providers/bhtelecom/provider.py b/lib/streaming_providers/providers/bhtelecom/provider.py index 207188b..9d47301 100644 --- a/lib/streaming_providers/providers/bhtelecom/provider.py +++ b/lib/streaming_providers/providers/bhtelecom/provider.py @@ -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, diff --git a/lib/streaming_providers/providers/discovery/models.py b/lib/streaming_providers/providers/discovery/models.py index b9e4259..784afdd 100644 --- a/lib/streaming_providers/providers/discovery/models.py +++ b/lib/streaming_providers/providers/discovery/models.py @@ -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, diff --git a/lib/streaming_providers/providers/discovery/provider.py b/lib/streaming_providers/providers/discovery/provider.py index c629023..138e3cf 100644 --- a/lib/streaming_providers/providers/discovery/provider.py +++ b/lib/streaming_providers/providers/discovery/provider.py @@ -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]: diff --git a/lib/streaming_providers/providers/hrti/provider.py b/lib/streaming_providers/providers/hrti/provider.py index 0d502d9..df15eae 100644 --- a/lib/streaming_providers/providers/hrti/provider.py +++ b/lib/streaming_providers/providers/hrti/provider.py @@ -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", diff --git a/lib/streaming_providers/providers/joyn/models.py b/lib/streaming_providers/providers/joyn/models.py index 2dd573f..f9ceb49 100644 --- a/lib/streaming_providers/providers/joyn/models.py +++ b/lib/streaming_providers/providers/joyn/models.py @@ -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, diff --git a/lib/streaming_providers/providers/joyn/provider.py b/lib/streaming_providers/providers/joyn/provider.py index 5998be6..b994f97 100644 --- a/lib/streaming_providers/providers/joyn/provider.py +++ b/lib/streaming_providers/providers/joyn/provider.py @@ -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"]: diff --git a/lib/streaming_providers/providers/m3u/provider.py b/lib/streaming_providers/providers/m3u/provider.py index 5fe1bae..b36a7ac 100644 --- a/lib/streaming_providers/providers/m3u/provider.py +++ b/lib/streaming_providers/providers/m3u/provider.py @@ -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], diff --git a/lib/streaming_providers/providers/magenta2/models.py b/lib/streaming_providers/providers/magenta2/models.py index cfdd14e..fd2fb05 100644 --- a/lib/streaming_providers/providers/magenta2/models.py +++ b/lib/streaming_providers/providers/magenta2/models.py @@ -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, diff --git a/lib/streaming_providers/providers/magenta2/provider.py b/lib/streaming_providers/providers/magenta2/provider.py index 084df22..884c1c9 100644 --- a/lib/streaming_providers/providers/magenta2/provider.py +++ b/lib/streaming_providers/providers/magenta2/provider.py @@ -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: diff --git a/lib/streaming_providers/providers/magentaeu/provider.py b/lib/streaming_providers/providers/magentaeu/provider.py index 4baa2e9..6a0daf1 100644 --- a/lib/streaming_providers/providers/magentaeu/provider.py +++ b/lib/streaming_providers/providers/magentaeu/provider.py @@ -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]: diff --git a/lib/streaming_providers/providers/rtlplus/provider.py b/lib/streaming_providers/providers/rtlplus/provider.py index 52c7b61..b3121ad 100644 --- a/lib/streaming_providers/providers/rtlplus/provider.py +++ b/lib/streaming_providers/providers/rtlplus/provider.py @@ -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", diff --git a/lib/streaming_providers/providers/scripts/provider.py b/lib/streaming_providers/providers/scripts/provider.py index 0accb52..ca84944 100644 --- a/lib/streaming_providers/providers/scripts/provider.py +++ b/lib/streaming_providers/providers/scripts/provider.py @@ -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,