mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-05 07:22:24 +02:00
687 lines
24 KiB
Python
687 lines
24 KiB
Python
# streaming_providers/providers/joyn/provider.py
|
|
# -*- coding: utf-8 -*-
|
|
import hashlib
|
|
import json
|
|
import time
|
|
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.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
|
|
from .constants import (
|
|
CONTENT_TYPE_LIVE,
|
|
CONTENT_TYPE_VOD,
|
|
COUNTRY_TENANT_MAPPING,
|
|
DEFAULT_EPG_WINDOW_HOURS,
|
|
DEFAULT_LIVESTREAM_TYPES,
|
|
DEFAULT_MAX_RETRIES,
|
|
DEFAULT_PLATFORM,
|
|
DEFAULT_REQUEST_TIMEOUT,
|
|
DEFAULT_VIDEO_CONFIG,
|
|
DRM_REQUEST_HEADERS,
|
|
DRM_SYSTEM_WIDEVINE,
|
|
ERROR_CODES,
|
|
GRAPHQL_LIVE_CHANNELS_FILTER,
|
|
GRAPHQL_MAX_RESULTS,
|
|
GRAPHQL_OFFSET,
|
|
GRAPHQL_PERSISTED_QUERY_VERSION,
|
|
GRAPHQL_QUERY_HASHES,
|
|
JOYN_API_BASE_HEADERS,
|
|
JOYN_CLIENT_VERSION,
|
|
JOYN_DOMAINS,
|
|
JOYN_GRAPHQL_BASE_HEADERS,
|
|
JOYN_GRAPHQL_ENDPOINTS,
|
|
JOYN_LOGO,
|
|
JOYN_STREAMING_ENDPOINTS,
|
|
JOYN_USER_AGENT,
|
|
MODE_LIVE,
|
|
MODE_VOD,
|
|
SIGNATURE_SECRET_KEY,
|
|
SUPPORTED_COUNTRIES,
|
|
)
|
|
from .models import JoynChannel, PlaybackRestrictedException
|
|
|
|
|
|
def create_video_payload(config: Optional[Dict] = None, compact: bool = True) -> str:
|
|
"""Create video data payload for requests"""
|
|
video_config = config or DEFAULT_VIDEO_CONFIG
|
|
payload = dumps(video_config)
|
|
return payload.replace(" ", "") if compact else payload
|
|
|
|
|
|
def build_signature(
|
|
entitlement_token: str,
|
|
video_payload: Optional[str] = None,
|
|
secret_key: Optional[str] = None,
|
|
) -> str:
|
|
"""Build signature for video data requests"""
|
|
if video_payload is None:
|
|
video_payload = create_video_payload()
|
|
|
|
if secret_key is None:
|
|
secret_key = b64decode(SIGNATURE_SECRET_KEY).decode("utf-8")
|
|
|
|
signature_input = f"{video_payload},{entitlement_token}{secret_key}"
|
|
return hashlib.sha1(signature_input.encode("utf-8")).hexdigest()
|
|
|
|
|
|
class JoynProvider(StreamingProvider):
|
|
"""
|
|
Joyn streaming provider implementation with centralized HTTP management
|
|
"""
|
|
|
|
# ============================================================================
|
|
# STATIC METADATA (NEW)
|
|
# ============================================================================
|
|
PROVIDER_LABEL: ClassVar[str] = "Joyn"
|
|
SUPPORTED_AUTH_TYPES: ClassVar[List[str]] = [
|
|
"client_credentials",
|
|
"user_credentials",
|
|
]
|
|
PROVIDER_LOGO: ClassVar[str] = JOYN_LOGO
|
|
SUPPORTED_COUNTRIES: ClassVar[List[str]] = SUPPORTED_COUNTRIES
|
|
|
|
def __init__(
|
|
self,
|
|
country: str = "de",
|
|
platform: str = DEFAULT_PLATFORM,
|
|
config_dir: Optional[str] = None,
|
|
proxy_config: Optional[ProxyConfig] = None,
|
|
proxy_url: Optional[str] = None,
|
|
):
|
|
"""
|
|
Initialize Joyn provider
|
|
|
|
Args:
|
|
country: Country code ('de', 'at', 'ch')
|
|
platform: Platform identifier
|
|
config_dir: Optional config directory override
|
|
proxy_config: Optional proxy configuration (highest priority)
|
|
proxy_url: Optional proxy URL string (medium priority)
|
|
"""
|
|
if not self.validate_country(country):
|
|
supported = ", ".join(self.SUPPORTED_COUNTRIES)
|
|
raise ValueError(f"Unsupported country: {country}. " f"Joyn supports: {supported}")
|
|
|
|
super().__init__(country=country)
|
|
|
|
if country not in SUPPORTED_COUNTRIES:
|
|
raise ValueError(
|
|
f"Unsupported country: {country}. Must be one of: {SUPPORTED_COUNTRIES}"
|
|
)
|
|
|
|
self.distribution_tenant = COUNTRY_TENANT_MAPPING[country]
|
|
self.platform = platform
|
|
|
|
# Setup HTTP manager using abstraction
|
|
self.http_manager = self._setup_http_manager(
|
|
provider_name="joyn",
|
|
proxy_config=proxy_config,
|
|
proxy_url=proxy_url,
|
|
config_dir=config_dir,
|
|
user_agent=JOYN_USER_AGENT,
|
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
max_retries=DEFAULT_MAX_RETRIES,
|
|
)
|
|
|
|
# Create authenticator with shared HTTP manager
|
|
self.authenticator = JoynAuthenticator(
|
|
country=country,
|
|
platform=platform,
|
|
config_dir=config_dir,
|
|
http_manager=self.http_manager,
|
|
proxy_config=self.http_manager.config.proxy_config, # Use resolved proxy
|
|
)
|
|
|
|
# Authenticate
|
|
try:
|
|
self.bearer_token = self.authenticator.get_bearer_token()
|
|
except Exception as e:
|
|
logger.warning(f"Could not authenticate during initialization: {e}")
|
|
self.bearer_token = None
|
|
|
|
@property
|
|
def provider_name(self) -> str:
|
|
return "joyn"
|
|
|
|
# Override provider_label to provide custom country formatting
|
|
@property
|
|
def provider_label(self) -> str:
|
|
"""Return country-specific label"""
|
|
country_map = {
|
|
"de": "Joyn Germany",
|
|
"at": "Joyn Austria",
|
|
"ch": "Joyn Switzerland",
|
|
}
|
|
return country_map.get(self.country, f"Joyn ({self.country.upper()})")
|
|
|
|
@property
|
|
def provider_logo(self) -> str:
|
|
return self.PROVIDER_LOGO # Use class attribute
|
|
|
|
@property
|
|
def supported_auth_types(self) -> List[str]:
|
|
return self.SUPPORTED_AUTH_TYPES # Use class attribute
|
|
|
|
@property
|
|
def uses_dynamic_manifests(self) -> bool:
|
|
return False
|
|
|
|
@property
|
|
def implements_epg(self) -> bool:
|
|
return False
|
|
|
|
def authenticate(self, **kwargs) -> str:
|
|
"""Authenticate and return bearer token"""
|
|
self.bearer_token = self.authenticator.get_bearer_token(
|
|
force_refresh=kwargs.get("force_refresh", False)
|
|
)
|
|
return self.bearer_token
|
|
|
|
def get_dynamic_manifest_params(self, channel: StreamingChannel, **kwargs) -> Optional[str]:
|
|
return None
|
|
|
|
def _get_graphql_headers(self) -> Dict[str, str]:
|
|
"""Get headers for GraphQL requests"""
|
|
return self._build_provider_headers(
|
|
base_headers=JOYN_GRAPHQL_BASE_HEADERS,
|
|
auth_type=AuthType.NONE, # GraphQL queries don't need auth
|
|
provider_headers={
|
|
"joyn-client-version": JOYN_CLIENT_VERSION,
|
|
"joyn-country": self.country.upper(),
|
|
"joyn-distribution-tenant": self.distribution_tenant,
|
|
"joyn-platform": self.platform,
|
|
"joyn-user-state": "code=R_A",
|
|
},
|
|
)
|
|
|
|
def refresh_authentication(self) -> str:
|
|
"""Force refresh authentication"""
|
|
self.bearer_token = self.authenticator.get_bearer_token(force_refresh=True)
|
|
return self.bearer_token
|
|
|
|
def get_channels(
|
|
self,
|
|
time_window_hours: int = DEFAULT_EPG_WINDOW_HOURS,
|
|
fetch_manifests: bool = False,
|
|
populate_streaming_data: bool = True,
|
|
**kwargs,
|
|
) -> List[StreamingChannel]:
|
|
"""
|
|
Fetch available channels from Joyn GraphQL API
|
|
|
|
Args:
|
|
time_window_hours: EPG time window in hours
|
|
fetch_manifests: Whether to immediately populate streaming data
|
|
populate_streaming_data: Whether to populate streaming data when fetch_manifests is True
|
|
**kwargs: Additional parameters
|
|
|
|
Returns:
|
|
List of StreamingChannel objects
|
|
"""
|
|
try:
|
|
headers = self._get_graphql_headers()
|
|
|
|
current_time = int(time.time())
|
|
end_time = current_time + (time_window_hours * 3600)
|
|
|
|
variables = {
|
|
"liveStreamGroupFilter": GRAPHQL_LIVE_CHANNELS_FILTER,
|
|
"first": GRAPHQL_MAX_RESULTS,
|
|
"offset": GRAPHQL_OFFSET,
|
|
"livestreamTypes": DEFAULT_LIVESTREAM_TYPES,
|
|
"from": current_time,
|
|
"to": end_time,
|
|
}
|
|
|
|
variables_encoded = urllib.parse.quote(json.dumps(variables))
|
|
extensions = {
|
|
"persistedQuery": {
|
|
"version": GRAPHQL_PERSISTED_QUERY_VERSION,
|
|
"sha256Hash": GRAPHQL_QUERY_HASHES["LIVE_CHANNELS"],
|
|
}
|
|
}
|
|
extensions_encoded = urllib.parse.quote(json.dumps(extensions))
|
|
|
|
url = f"{JOYN_GRAPHQL_ENDPOINTS['LIVE_CHANNELS']}&variables={variables_encoded}&extensions={extensions_encoded}"
|
|
|
|
response = self.http_manager.get(
|
|
url, operation="api", headers=headers, timeout=DEFAULT_REQUEST_TIMEOUT
|
|
)
|
|
response.raise_for_status()
|
|
channel_data = response.json()
|
|
|
|
channels = self._process_graphql_response(channel_data)
|
|
|
|
# Populate streaming data immediately after fetching channels
|
|
if fetch_manifests and populate_streaming_data:
|
|
channels = self.populate_streaming_data(channels)
|
|
|
|
logger.info(f"Successfully fetched {len(channels)} channels for country {self.country}")
|
|
return channels
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error fetching channels from GraphQL: {e}")
|
|
|
|
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"]:
|
|
raise Exception("Invalid GraphQL response structure")
|
|
|
|
live_streams = response_data["data"]["liveStreams"]
|
|
channels = []
|
|
|
|
for stream_data in live_streams:
|
|
try:
|
|
channel_id = stream_data.get("id", "")
|
|
title = stream_data.get("title", "Unknown Channel")
|
|
stream_type = stream_data.get("type", "LINEAR")
|
|
quality = stream_data.get("quality", "")
|
|
|
|
logo_url = None
|
|
if "logo" in stream_data and "url" in stream_data["logo"]:
|
|
logo_url = stream_data["logo"]["url"]
|
|
|
|
content_type = CONTENT_TYPE_LIVE if stream_type == "LINEAR" else CONTENT_TYPE_VOD
|
|
mode = MODE_LIVE if stream_type == "LINEAR" else MODE_VOD
|
|
|
|
joyn_channel = JoynChannel(
|
|
name=title,
|
|
channel_id=channel_id,
|
|
logo_url=logo_url,
|
|
mode=mode,
|
|
content_type=content_type,
|
|
country=self.country,
|
|
raw_data=stream_data,
|
|
)
|
|
|
|
if quality:
|
|
joyn_channel.name = f"{title} ({quality})"
|
|
|
|
if "brand" in stream_data:
|
|
brand_data = stream_data["brand"]
|
|
if "brand_id" in brand_data:
|
|
joyn_channel.raw_data["brand_id"] = brand_data["brand_id"]
|
|
|
|
if stream_data.get("eventStream", False):
|
|
joyn_channel.raw_data["is_event_stream"] = True
|
|
|
|
streaming_channel = joyn_channel.to_streaming_channel(
|
|
provider_name=self.provider_name
|
|
)
|
|
channels.append(streaming_channel)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error processing channel data: {e}")
|
|
|
|
return channels
|
|
|
|
def get_entitlement_token(self, content_id: str, content_type: str = CONTENT_TYPE_LIVE) -> str:
|
|
"""
|
|
Get entitlement token for content
|
|
|
|
Args:
|
|
content_id: Content ID
|
|
content_type: Content type ('LIVE' or 'VOD')
|
|
|
|
Returns:
|
|
Entitlement token string
|
|
"""
|
|
headers = self._build_provider_headers(
|
|
base_headers=JOYN_API_BASE_HEADERS,
|
|
auth_type=AuthType.BEARER,
|
|
provider_headers={
|
|
"joyn-client-version": JOYN_CLIENT_VERSION,
|
|
"joyn-country": self.country.upper(),
|
|
"joyn-distribution-tenant": self.distribution_tenant,
|
|
"joyn-platform": self.platform,
|
|
"joyn-b2b-context": "UNKNOWN",
|
|
"joyn-client-os": "UNKNOWN",
|
|
"origin": JOYN_DOMAINS.get(self.country, JOYN_DOMAINS["de"]),
|
|
},
|
|
)
|
|
|
|
payload = {"content_id": content_id, "content_type": content_type}
|
|
|
|
try:
|
|
response = self.http_manager.post(
|
|
JOYN_STREAMING_ENDPOINTS["ENTITLEMENT"],
|
|
operation="auth",
|
|
headers=headers,
|
|
json_data=payload,
|
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
)
|
|
|
|
if response.status_code == 400:
|
|
try:
|
|
error_data = response.json()
|
|
if isinstance(error_data, list) and len(error_data) > 0:
|
|
error = error_data[0]
|
|
code = error.get("code", "UNKNOWN")
|
|
msg = error.get("msg", "No error message provided")
|
|
if code == ERROR_CODES["PLAYBACK_RESTRICTED"]:
|
|
raise PlaybackRestrictedException(
|
|
f"Playback restricted for {content_id}: {msg}"
|
|
)
|
|
else:
|
|
raise Exception(f"Entitlement error for {content_id} ({code}): {msg}")
|
|
except (json.JSONDecodeError, KeyError, IndexError) as e:
|
|
raise Exception(
|
|
f"Bad response for {content_id} (400), failed to parse error: {e}"
|
|
)
|
|
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
return data["entitlement_token"]
|
|
|
|
except PlaybackRestrictedException:
|
|
raise
|
|
except KeyError:
|
|
raise Exception(f"No entitlement_token in response for {content_id}")
|
|
except Exception as e:
|
|
raise Exception(f"Error getting entitlement token for {content_id}: {e}")
|
|
|
|
def get_channel_playlist(
|
|
self,
|
|
channel_id: str,
|
|
entitlement_token: str,
|
|
video_config: Optional[Dict] = None,
|
|
) -> Dict:
|
|
"""
|
|
Get channel playlist data
|
|
|
|
Args:
|
|
channel_id: Channel ID
|
|
entitlement_token: Entitlement token
|
|
video_config: Optional video configuration override
|
|
|
|
Returns:
|
|
Playlist data dictionary
|
|
"""
|
|
video_payload = create_video_payload(video_config)
|
|
signature = build_signature(entitlement_token, video_payload)
|
|
|
|
url = JOYN_STREAMING_ENDPOINTS["PLAYLIST"].format(channel_id=channel_id)
|
|
url += f"?signature={signature}"
|
|
|
|
headers = JOYN_API_BASE_HEADERS.copy()
|
|
headers["Authorization"] = f"Bearer {entitlement_token}"
|
|
|
|
try:
|
|
response = self.http_manager.post(
|
|
url,
|
|
operation="manifest",
|
|
headers=headers,
|
|
data=video_payload,
|
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error getting playlist for {channel_id}: {e}")
|
|
|
|
def populate_streaming_data(
|
|
self,
|
|
channels: List[StreamingChannel],
|
|
video_config: Optional[Dict] = None,
|
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
) -> List[StreamingChannel]:
|
|
"""
|
|
Populate streaming data (manifest, DRM) for all channels
|
|
|
|
Args:
|
|
channels: List of channels to populate
|
|
video_config: Optional video configuration
|
|
max_retries: Maximum retry attempts per channel
|
|
|
|
Returns:
|
|
List of successfully populated channels
|
|
"""
|
|
successful_channels = []
|
|
|
|
for channel in channels:
|
|
retries = 0
|
|
success = False
|
|
is_restricted = False
|
|
|
|
while retries < max_retries and not success and not is_restricted:
|
|
try:
|
|
logger.debug(
|
|
f"Getting entitlement token for: {channel.name} (attempt {retries + 1})"
|
|
)
|
|
|
|
entitlement_token = self.get_entitlement_token(
|
|
content_id=channel.channel_id, content_type=channel.content_type
|
|
)
|
|
|
|
logger.debug(f"Getting playlist data for: {channel.name}")
|
|
|
|
playlist_data = self.get_channel_playlist(
|
|
channel.channel_id, entitlement_token, video_config
|
|
)
|
|
|
|
manifest_url = playlist_data.get("manifestUrl")
|
|
license_url = playlist_data.get("licenseUrl")
|
|
certificate_url = playlist_data.get("certificateUrl")
|
|
streaming_format = playlist_data.get("streamingFormat", "dash")
|
|
|
|
if manifest_url:
|
|
channel.manifest = manifest_url
|
|
channel.cdm_type = DRM_SYSTEM_WIDEVINE
|
|
channel.cdm = f"pid={channel.channel_id}"
|
|
channel.license_url = license_url
|
|
channel.certificate_url = certificate_url
|
|
channel.streaming_format = streaming_format
|
|
|
|
logger.info(f"Streaming data populated for: {channel.name}")
|
|
successful_channels.append(channel)
|
|
success = True
|
|
else:
|
|
raise Exception("No manifestUrl in response")
|
|
|
|
except PlaybackRestrictedException as e:
|
|
logger.warning(f"Playback restricted for {channel.name}: {e}")
|
|
is_restricted = True
|
|
|
|
except Exception as e:
|
|
retries += 1
|
|
if retries < max_retries:
|
|
logger.debug(f"Retry {retries}/{max_retries} for {channel.name}: {e}")
|
|
time.sleep(1)
|
|
else:
|
|
logger.error(f"Failed to get streaming data for {channel.name}: {e}")
|
|
|
|
logger.info(f"Streaming data population complete:")
|
|
logger.info(f" Successful: {len(successful_channels)}")
|
|
logger.info(f" Restricted: {len([c for c in channels if c not in successful_channels])}")
|
|
logger.info(f" Total: {len(channels)}")
|
|
|
|
return successful_channels
|
|
|
|
def enrich_channel_data(
|
|
self, channel: StreamingChannel, video_config: Optional[Dict] = None, **kwargs
|
|
) -> Optional[StreamingChannel]:
|
|
"""
|
|
Get manifest URL for a specific channel and properly configure DRM
|
|
|
|
Args:
|
|
channel: StreamingChannel to enrich
|
|
video_config: Optional video configuration dictionary
|
|
**kwargs: Additional parameters
|
|
|
|
Returns:
|
|
The enriched StreamingChannel with manifest and proper DRM config, or None if failed
|
|
"""
|
|
try:
|
|
entitlement_token = self.get_entitlement_token(
|
|
content_id=channel.channel_id, content_type=channel.content_type
|
|
)
|
|
|
|
playlist_data = self.get_channel_playlist(
|
|
channel.channel_id, entitlement_token, video_config
|
|
)
|
|
|
|
manifest_url = playlist_data.get("manifestUrl")
|
|
if not manifest_url:
|
|
return None
|
|
|
|
channel.manifest = manifest_url
|
|
channel.streaming_format = playlist_data.get("streamingFormat", "dash")
|
|
|
|
license_url = playlist_data.get("licenseUrl")
|
|
if license_url:
|
|
drm_config = DRMConfig(
|
|
system=DRMSystem.WIDEVINE,
|
|
priority=1,
|
|
license=LicenseConfig(
|
|
server_url=license_url,
|
|
server_certificate=playlist_data.get("certificateUrl"),
|
|
req_headers=json.dumps(
|
|
{
|
|
"User-Agent": JOYN_USER_AGENT,
|
|
"Content-Type": DRM_REQUEST_HEADERS["Content-Type"],
|
|
}
|
|
),
|
|
req_data="{CHA-RAW}",
|
|
use_http_get_request=False,
|
|
),
|
|
)
|
|
channel.drm_config = drm_config
|
|
channel.cdm_type = DRM_SYSTEM_WIDEVINE
|
|
channel.cdm = f"pid={channel.channel_id}"
|
|
|
|
return channel
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting manifest for {channel.name}: {e}")
|
|
return None
|
|
|
|
def get_manifest(
|
|
self,
|
|
channel_id: str,
|
|
content_type: str = CONTENT_TYPE_LIVE,
|
|
video_config: Optional[Dict] = None,
|
|
**kwargs,
|
|
) -> Optional[str]:
|
|
"""
|
|
Get manifest URL for a specific channel by ID
|
|
|
|
Args:
|
|
channel_id: ID of the channel to get manifest for
|
|
content_type: Content type ('LIVE' or 'VOD')
|
|
video_config: Optional video configuration dictionary
|
|
**kwargs: Additional parameters
|
|
|
|
Returns:
|
|
Manifest URL string, or None if not available
|
|
"""
|
|
try:
|
|
entitlement_token = self.get_entitlement_token(
|
|
content_id=channel_id, content_type=content_type
|
|
)
|
|
|
|
playlist_data = self.get_channel_playlist(channel_id, entitlement_token, video_config)
|
|
|
|
return playlist_data.get("manifestUrl")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting manifest for channel {channel_id}: {e}")
|
|
return None
|
|
|
|
def get_drm(
|
|
self,
|
|
channel_id: str,
|
|
content_type: str = CONTENT_TYPE_LIVE,
|
|
video_config: Optional[Dict] = None,
|
|
**kwargs,
|
|
) -> List[DRMConfig]:
|
|
"""
|
|
Get all DRM configurations for a channel by ID
|
|
|
|
Args:
|
|
channel_id: ID of the channel to get DRM configs
|
|
content_type: Content type ('LIVE' or 'VOD')
|
|
video_config: Optional video configuration dictionary
|
|
**kwargs: Additional parameters
|
|
|
|
Returns:
|
|
List of DRMConfig objects (can be empty if no DRM is used)
|
|
"""
|
|
try:
|
|
entitlement_token = self.get_entitlement_token(
|
|
content_id=channel_id, content_type=content_type
|
|
)
|
|
|
|
playlist_data = self.get_channel_playlist(channel_id, entitlement_token, video_config)
|
|
|
|
license_url = playlist_data.get("licenseUrl")
|
|
if not license_url:
|
|
return []
|
|
|
|
drm_config = DRMConfig(
|
|
system=DRMSystem.WIDEVINE,
|
|
priority=1,
|
|
license=LicenseConfig.create_with_req_data(
|
|
req_data_template="{CHA-RAW}",
|
|
server_url=license_url,
|
|
server_certificate=playlist_data.get("certificateUrl"),
|
|
req_headers=json.dumps(
|
|
{
|
|
"Authorization": f"Bearer {self.bearer_token}",
|
|
"Content-Type": DRM_REQUEST_HEADERS["Content-Type"],
|
|
"User-Agent": JOYN_USER_AGENT,
|
|
}
|
|
),
|
|
use_http_get_request=False,
|
|
),
|
|
)
|
|
|
|
return [drm_config]
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting DRM configs for channel {channel_id}: {e}")
|
|
return []
|
|
|
|
def get_epg(
|
|
self,
|
|
channel_id: str,
|
|
start_time: Optional[datetime] = None,
|
|
end_time: Optional[datetime] = None,
|
|
**kwargs,
|
|
) -> List[Dict]:
|
|
"""
|
|
Get EPG data for a channel
|
|
|
|
Args:
|
|
channel_id: Channel ID to get EPG for
|
|
start_time: Optional start time for EPG window
|
|
end_time: Optional end time for EPG window
|
|
**kwargs: Additional parameters
|
|
|
|
Returns:
|
|
List of EPG entries
|
|
"""
|
|
try:
|
|
if start_time is None:
|
|
start_time = datetime.now()
|
|
if end_time is None:
|
|
end_time = datetime.now() + timedelta(hours=DEFAULT_EPG_WINDOW_HOURS)
|
|
|
|
headers = self._get_graphql_headers()
|
|
|
|
# Joyn EPG would require additional GraphQL queries
|
|
logger.info(f"EPG data requested for channel {channel_id} - not yet implemented")
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting EPG for channel {channel_id}: {e}")
|
|
return []
|