This commit is contained in:
Nirvana
2026-01-24 20:19:47 +01:00
parent 348dac1b2a
commit e864dad8b6
2 changed files with 456 additions and 59 deletions
+164 -59
View File
@@ -124,49 +124,67 @@ def setup_m3u_routes(app, manager, service):
def get_m3u_decrypted():
"""
Generates decrypted M3U playlist for all configured providers.
Only includes channels with ClearKey DRM or unencrypted channels.
Returns cached version if available, otherwise generates new one.
Fast generation - includes ALL channels with decrypted stream URLs.
No filtering, no caching (very fast).
Example: http://localhost:7777/api/m3u/decrypted
"""
try:
cache_file = "playlist_decrypted.m3u"
# Try to read cached file
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info("Serving cached decrypted M3U playlist for all providers")
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_decrypted.m3u8"'
)
return cached_content
# Cache doesn't exist, generate new M3U
logger.info(
"No valid cache found, generating decrypted M3U playlist for all providers"
)
return service._generate_m3u_decrypted_all(save_to_cache=True)
logger.info("Generating fast decrypted M3U playlist for all providers")
return service._generate_m3u_decrypted_fast(providers=None)
except Exception as api_err:
logger.error(f"API Error in /api/m3u/decrypted: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/m3u/decrypted/generate")
def generate_m3u_decrypted():
@app.route("/api/m3u/decrypted/filtered")
def get_m3u_decrypted_filtered():
"""
Forces regeneration of decrypted M3U playlist for all providers and saves to cache.
Generates filtered decrypted M3U playlist for all configured providers.
Only includes channels with ClearKey DRM or unencrypted channels.
Returns cached version if available, otherwise generates new one.
Example: http://localhost:7777/api/m3u/decrypted/generate
Example: http://localhost:7777/api/m3u/decrypted/filtered
"""
try:
logger.info("Force generating decrypted M3U playlist for all providers")
return service._generate_m3u_decrypted_all(save_to_cache=True)
cache_file = "playlist_decrypted_filtered.m3u"
# Try to read cached file
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info("Serving cached filtered decrypted M3U playlist for all providers")
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_decrypted_filtered.m3u8"'
)
return cached_content
# Cache doesn't exist, generate new M3U
logger.info(
"No valid cache found, generating filtered decrypted M3U playlist for all providers"
)
return service._generate_m3u_decrypted_filtered_all(save_to_cache=True)
except Exception as api_err:
logger.error(f"API Error in /api/m3u/decrypted/generate: {str(api_err)}")
logger.error(f"API Error in /api/m3u/decrypted/filtered: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/m3u/decrypted/filtered/generate")
def generate_m3u_decrypted_filtered():
"""
Forces regeneration of filtered decrypted M3U playlist for all providers and saves to cache.
Example: http://localhost:7777/api/m3u/decrypted/filtered/generate
"""
try:
logger.info("Force generating filtered decrypted M3U playlist for all providers")
return service._generate_m3u_decrypted_filtered_all(save_to_cache=True)
except Exception as api_err:
logger.error(f"API Error in /api/m3u/decrypted/filtered/generate: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@@ -174,34 +192,14 @@ def setup_m3u_routes(app, manager, service):
def get_m3u_decrypted_provider(provider):
"""
Generates decrypted M3U playlist for a specific provider.
Only includes channels with ClearKey DRM or unencrypted channels.
Returns cached version if available, otherwise generates new one.
Fast generation - includes ALL channels with decrypted stream URLs.
No filtering, no caching (very fast).
Example: http://localhost:7777/api/providers/rtlplus/m3u/decrypted
"""
try:
cache_file = f"{provider}_decrypted.m3u"
# Try to read cached file
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info(
f"Serving cached decrypted M3U playlist for provider '{provider}'"
)
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
f'attachment; filename="{provider}_decrypted_playlist.m3u8"'
)
return cached_content
# Cache doesn't exist, generate new M3U
logger.info(
f"No valid cache found, generating decrypted M3U playlist for provider '{provider}'"
)
return service._generate_m3u_decrypted_provider(
provider, save_to_cache=True
)
logger.info(f"Generating fast decrypted M3U playlist for provider '{provider}'")
return service._generate_m3u_decrypted_fast(providers=provider)
except ValueError as val_err:
logger.error(
@@ -216,30 +214,76 @@ def setup_m3u_routes(app, manager, service):
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/providers/<provider>/m3u/decrypted/generate")
def generate_m3u_decrypted_provider(provider):
@app.route("/api/providers/<provider>/m3u/decrypted/filtered")
def get_m3u_decrypted_filtered_provider(provider):
"""
Forces regeneration of decrypted M3U playlist for a specific provider and saves to cache.
Generates filtered decrypted M3U playlist for a specific provider.
Only includes channels with ClearKey DRM or unencrypted channels.
Returns cached version if available, otherwise generates new one.
Example: http://localhost:7777/api/providers/rtlplus/m3u/decrypted/generate
Example: http://localhost:7777/api/providers/rtlplus/m3u/decrypted/filtered
"""
try:
cache_file = f"{provider}_decrypted_filtered.m3u"
# Try to read cached file
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info(
f"Serving cached filtered decrypted M3U playlist for provider '{provider}'"
)
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
f'attachment; filename="{provider}_decrypted_filtered_playlist.m3u8"'
)
return cached_content
# Cache doesn't exist, generate new M3U
logger.info(
f"Force generating decrypted M3U playlist for provider '{provider}'"
f"No valid cache found, generating filtered decrypted M3U playlist for provider '{provider}'"
)
return service._generate_m3u_decrypted_provider(
return service._generate_m3u_decrypted_filtered_provider(
provider, save_to_cache=True
)
except ValueError as val_err:
logger.error(
f"API Error in /api/providers/{provider}/m3u/decrypted/generate: {str(val_err)}"
f"API Error in /api/providers/{provider}/m3u/decrypted/filtered: {str(val_err)}"
)
response.status = 404
return {"error": str(val_err)}
except Exception as api_err:
logger.error(
f"API Error in /api/providers/{provider}/m3u/decrypted/generate: {str(api_err)}"
f"API Error in /api/providers/{provider}/m3u/decrypted/filtered: {str(api_err)}"
)
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/providers/<provider>/m3u/decrypted/filtered/generate")
def generate_m3u_decrypted_filtered_provider(provider):
"""
Forces regeneration of filtered decrypted M3U playlist for a specific provider and saves to cache.
Example: http://localhost:7777/api/providers/rtlplus/m3u/decrypted/filtered/generate
"""
try:
logger.info(
f"Force generating filtered decrypted M3U playlist for provider '{provider}'"
)
return service._generate_m3u_decrypted_filtered_provider(
provider, save_to_cache=True
)
except ValueError as val_err:
logger.error(
f"API Error in /api/providers/{provider}/m3u/decrypted/filtered/generate: {str(val_err)}"
)
response.status = 404
return {"error": str(val_err)}
except Exception as api_err:
logger.error(
f"API Error in /api/providers/{provider}/m3u/decrypted/filtered/generate: {str(api_err)}"
)
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@@ -348,3 +392,64 @@ def setup_m3u_routes(app, manager, service):
logger.error(f"API Error in /api/m3u/subscribed/generate: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/m3u/subscribed/decrypted")
def get_m3u_subscribed_decrypted():
"""
Generate decrypted M3U playlist with only subscribed channels.
Fast generation - no filtering, no caching.
"""
try:
logger.info("Generating fast decrypted M3U playlist for subscribed channels")
# Check if media proxy is configured
if not service.media_proxy_url:
response.status = 503
return {"error": "Media proxy not configured (MEDIA_PROXY_URL not set)"}
base_url = f"{request.urlparts.scheme}://{request.urlparts.netloc}"
m3u_content = "#EXTM3U\n"
provider_list = manager.list_providers()
for provider_name in provider_list:
try:
channels = manager.get_subscribed_channels(provider_name)
# Get provider label
try:
provider_instance = manager.get_provider(provider_name)
provider_label = getattr(
provider_instance, "provider_label", provider_name
)
except:
provider_label = provider_name
for channel in channels:
channel_id = channel.channel_id
channel_name = channel.name
channel_logo = channel.logo_url or ""
# Build decrypted stream URL
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/stream/decrypted/index.mpd"
# Add M3U entry
m3u_content += f'#EXTINF:-1 tvg-id="{channel_id}" tvg-logo="{channel_logo}" group-title="{provider_label}",{channel_name}\n'
m3u_content += "#KODIPROP:inputstream=inputstream.adaptive\n"
m3u_content += f"{stream_url}\n"
except Exception as provider_err:
logger.warning(
f"Failed to process subscribed channels for '{provider_name}': {str(provider_err)}"
)
continue
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_subscribed_decrypted.m3u8"'
)
return m3u_content
except Exception as api_err:
logger.error(f"API Error in /api/m3u/subscribed/decrypted: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
+292
View File
@@ -566,6 +566,298 @@ class UltimateService:
return m3u_content
def _generate_m3u_decrypted_fast(self, providers=None):
"""
Fast generation of decrypted M3U content for specified providers.
Includes ALL channels with decrypted stream URLs.
No DRM filtering, no caching - maximum speed.
Args:
providers: List of provider names, or None for all providers
Returns:
M3U content as string
"""
# Check if media proxy is configured
if not self.media_proxy_url:
logger.error("Cannot generate decrypted M3U: MEDIA_PROXY_URL not set")
response.status = 503
response.content_type = "application/json"
return json.dumps(
{"error": "Media proxy not configured (MEDIA_PROXY_URL not set)"}
)
# Get base URL for absolute stream URLs
base_url = f"{request.urlparts.scheme}://{request.urlparts.netloc}"
# Start M3U content
m3u_content = "#EXTM3U\n"
# Determine which providers to process
if providers is None:
providers_to_process = self.manager.list_providers()
else:
providers_to_process = (
[providers] if isinstance(providers, str) else providers
)
channels_included = 0
for provider_name in providers_to_process:
try:
# Get channels for this provider
channels = self.manager.get_channels(
provider_name=provider_name, fetch_manifests=False
)
# Get provider label
try:
provider_instance = self.manager.get_provider(provider_name)
provider_label = provider_instance.provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
# Process each channel - no DRM checks
for channel in channels:
channel_id = channel.channel_id
channel_name = channel.name
channel_logo = channel.logo_url or ""
# Build decrypted stream URL
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/stream/decrypted/index.mpd"
# Add M3U entry
m3u_content += f'#EXTINF:-1 tvg-id="{channel_id}" tvg-logo="{channel_logo}" group-title="{provider_label}",{channel_name}\n'
m3u_content += "#KODIPROP:inputstream=inputstream.adaptive\n"
m3u_content += f"{stream_url}\n"
channels_included += 1
except Exception as provider_err:
logger.warning(
f"Failed to process provider '{provider_name}': {str(provider_err)}"
)
continue
logger.info(f"Fast decrypted M3U: included {channels_included} channels")
# Set appropriate headers
response.content_type = "audio/x-mpegurl; charset=utf-8"
if providers and isinstance(providers, str):
# Single provider
response.headers["Content-Disposition"] = (
f'attachment; filename="{providers}_decrypted_playlist.m3u8"'
)
else:
# All providers
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_decrypted.m3u8"'
)
return m3u_content
def _generate_m3u_decrypted_filtered_content(
self, providers=None, save_to_cache=True, cache_filename=None
):
"""
Internal method to generate filtered decrypted M3U content for specified providers.
Only includes channels with ClearKey DRM or unencrypted channels.
Args:
providers: List of provider names, or None for all providers
save_to_cache: Whether to save to cache
cache_filename: Cache filename to use
Returns:
M3U content as string
"""
# Check if media proxy is configured
if not self.media_proxy_url:
logger.error("Cannot generate decrypted M3U: MEDIA_PROXY_URL not set")
return None
# Get base URL for absolute stream URLs
base_url = f"{request.urlparts.scheme}://{request.urlparts.netloc}"
# Start M3U content
m3u_content = "#EXTM3U\n"
# Determine which providers to process
if providers is None:
# All providers
providers_to_process = self.manager.list_providers()
cache_filename = cache_filename or "playlist_decrypted_filtered.m3u"
else:
# Specific provider(s)
providers_to_process = (
[providers] if isinstance(providers, str) else providers
)
cache_filename = (
cache_filename or f"{providers_to_process[0]}_decrypted_filtered.m3u"
)
channels_included = 0
channels_skipped = 0
for provider_name in providers_to_process:
try:
# Get channels for this provider
channels = self.manager.get_channels(
provider_name=provider_name, fetch_manifests=False
)
# Get provider instance for label and proxy config
try:
provider_instance = self.manager.get_provider(provider_name)
provider_label = provider_instance.provider_label
except (AttributeError, KeyError, ValueError):
provider_label = provider_name
# Get provider proxy URL if configured
http_manager = self.manager.get_provider_http_manager(provider_name)
provider_proxy_url = None
if http_manager and http_manager.config.proxy_config:
proxy_cfg = http_manager.config.proxy_config
provider_proxy_url = (
f"{proxy_cfg.proxy_type.value.lower()}://{proxy_cfg.host}:{proxy_cfg.port}"
)
# Process each channel
for channel in channels:
channel_id = channel.channel_id
channel_name = channel.name
channel_logo = channel.logo_url or ""
# Try to get DRM configs
try:
drm_configs = self.manager.get_channel_drm_configs(
provider_name=provider_name, channel_id=channel_id
)
# Convert list of DRM configs to dict
drm_dict = {}
if isinstance(drm_configs, list):
for config in drm_configs:
if hasattr(config, "to_dict"):
config_dict = config.to_dict()
drm_dict.update(config_dict)
elif isinstance(config, dict):
drm_dict.update(config)
elif isinstance(drm_configs, dict):
drm_dict = drm_configs
# Check if channel has ClearKey DRM or is unencrypted
has_clearkey = False
is_unencrypted = False
clearkey_data = None
if "org.w3.clearkey" in drm_dict:
clearkey_data = drm_dict["org.w3.clearkey"]
has_clearkey = True
elif "none" in drm_dict:
is_unencrypted = True
if has_clearkey and clearkey_data:
# Channel has ClearKey - generate decrypted entry
entry_content = self._generate_m3u_decrypted_channel_entry(
base_url=base_url,
provider_name=provider_name,
provider_label=provider_label,
channel=channel,
clearkey_data=clearkey_data,
provider_proxy_url=provider_proxy_url,
)
m3u_content += entry_content
channels_included += 1
elif is_unencrypted:
# Explicitly unencrypted channel - include with direct stream URL
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/stream/index.mpd"
m3u_content += f'#EXTINF:-1 tvg-id="{channel_id}" tvg-logo="{channel_logo}" group-title="{provider_label}",{channel_name}\n'
m3u_content += f"{stream_url}\n"
channels_included += 1
else:
# Channel has other DRM, is inaccessible, or unknown - skip
channels_skipped += 1
logger.debug(
f"Skipping {provider_name}/{channel_id} - unsupported DRM or no access"
)
except Exception as drm_err:
# Could not get DRM info - skip channel
logger.warning(
f"Could not get DRM for {provider_name}/{channel_id}: {drm_err}"
)
channels_skipped += 1
continue
except Exception as provider_err:
logger.warning(
f"Failed to process provider '{provider_name}': {str(provider_err)}"
)
continue
logger.info(
f"Filtered decrypted M3U: included {channels_included} channels, skipped {channels_skipped}"
)
# Save to cache if requested
if save_to_cache and cache_filename:
if self.vfs.write_text(cache_filename, m3u_content):
logger.info(f"Filtered decrypted M3U playlist cached to {cache_filename}")
else:
logger.warning(
f"Failed to cache filtered decrypted M3U playlist to {cache_filename}"
)
return m3u_content
def _generate_m3u_decrypted_filtered_all(self, save_to_cache: bool = False) -> str:
"""Internal method to generate filtered decrypted M3U for all providers."""
logger.info("Generating filtered decrypted M3U playlist for all providers")
m3u_content = self._generate_m3u_decrypted_filtered_content(
providers=None, save_to_cache=save_to_cache
)
if m3u_content is None:
response.status = 503
response.content_type = "application/json"
return json.dumps(
{"error": "Media proxy not configured (MEDIA_PROXY_URL not set)"}
)
# Set appropriate headers for M3U
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_decrypted_filtered.m3u8"'
)
return m3u_content
def _generate_m3u_decrypted_filtered_provider(
self, provider: str, save_to_cache: bool = False
) -> str:
"""Internal method to generate filtered decrypted M3U for a specific provider."""
logger.info(f"Generating filtered decrypted M3U playlist for provider '{provider}'")
m3u_content = self._generate_m3u_decrypted_filtered_content(
providers=provider, save_to_cache=save_to_cache
)
if m3u_content is None:
response.status = 503
response.content_type = "application/json"
return json.dumps(
{"error": "Media proxy not configured (MEDIA_PROXY_URL not set)"}
)
# Set appropriate headers for M3U
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
f'attachment; filename="{provider}_decrypted_filtered_playlist.m3u8"'
)
return m3u_content
def _generate_m3u_channel_entry(self, base_url, provider_name, channel):
"""
Generate M3U entry for a single channel.