Add discovery

This commit is contained in:
Nirvana
2026-02-27 12:14:43 +01:00
parent 9d0d037cd4
commit b8ffc364e5
19 changed files with 521 additions and 374 deletions
@@ -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
+25 -5
View File
@@ -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,