Files
script.service.ultimate/routes/m3u.py
T
2026-01-29 19:02:31 +01:00

482 lines
20 KiB
Python

#!/usr/bin/env python3
"""
M3U playlist route handlers
"""
from bottle import request, response
from streaming_providers.base.utils import logger
def setup_m3u_routes(app, manager, service):
"""Setup M3U playlist-related routes"""
@app.route("/api/m3u")
def get_m3u_all():
"""
Generates M3U playlist for all configured providers.
Returns cached version if available, otherwise generates new one.
Example: http://localhost:7777/api/m3u
"""
try:
cache_file = "playlist.m3u"
# Try to read cached file
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info("Serving cached M3U playlist for all providers")
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist.m3u8"'
)
return cached_content
# Cache doesn't exist or is corrupt, generate new M3U
logger.info(
"No valid cache found, generating M3U playlist for all providers"
)
return service._generate_m3u_all(save_to_cache=True)
except Exception as api_err:
logger.error(f"API Error in /api/m3u: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/m3u/generate")
def generate_m3u_all():
"""
Forces regeneration of M3U playlist for all providers and saves to cache.
Example: http://localhost:7777/api/m3u/generate
"""
try:
logger.info("Force generating M3U playlist for all providers")
return service._generate_m3u_all(save_to_cache=True)
except Exception as api_err:
logger.error(f"API Error in /api/m3u/generate: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/providers/<provider>/m3u")
def get_m3u_provider(provider):
"""
Generates M3U playlist for a specific provider.
Returns cached version if available, otherwise generates new one.
Example: http://localhost:7777/api/providers/rtlplus/m3u
"""
try:
cache_file = f"{provider}.m3u"
# Try to read cached file
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info(f"Serving cached M3U playlist for provider '{provider}'")
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
f'attachment; filename="{provider}_playlist.m3u8"'
)
return cached_content
# Cache doesn't exist or is corrupt, generate new M3U
logger.info(
f"No valid cache found, generating M3U playlist for provider '{provider}'"
)
return service._generate_m3u_provider(provider, save_to_cache=True)
except ValueError as val_err:
logger.error(f"API Error in /api/providers/{provider}/m3u: {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: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/providers/<provider>/m3u/generate")
def generate_m3u_provider(provider):
"""
Forces regeneration of M3U playlist for a specific provider and saves to cache.
Example: http://localhost:7777/api/providers/rtlplus/m3u/generate
"""
try:
logger.info(f"Force generating M3U playlist for provider '{provider}'")
return service._generate_m3u_provider(provider, save_to_cache=True)
except ValueError as val_err:
logger.error(
f"API Error in /api/providers/{provider}/m3u/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/generate: {str(api_err)}"
)
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/m3u/decrypted")
def get_m3u_decrypted():
"""
Generates decrypted M3U playlist for all configured providers.
Fast generation - includes ALL channels with decrypted stream URLs.
No filtering, no caching (very fast).
Example: http://localhost:7777/api/m3u/decrypted
"""
try:
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/filtered")
def get_m3u_decrypted_filtered():
"""
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/filtered
"""
try:
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/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)}"}
@app.route("/api/providers/<provider>/m3u/decrypted")
def get_m3u_decrypted_provider(provider):
"""
Generates decrypted M3U playlist for a specific provider.
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:
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(
f"API Error in /api/providers/{provider}/m3u/decrypted: {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: {str(api_err)}"
)
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/providers/<provider>/m3u/decrypted/ffmpeg")
def get_m3u_decrypted_ffmpeg_provider(provider):
"""
Generates decrypted M3U playlist for a specific provider with ffmpeg piping.
Fast generation - includes ALL channels with ffmpeg-piped decrypted stream URLs.
Streams are highest quality video only with all audio tracks copied.
No filtering, no caching (very fast).
Example: http://localhost:7777/api/providers/rtlplus/m3u/decrypted/ffmpeg
"""
try:
logger.info(f"Generating fast decrypted ffmpeg M3U playlist for provider '{provider}'")
return service._generate_m3u_decrypted_ffmpeg_fast(providers=provider)
except ValueError as val_err:
logger.error(
f"API Error in /api/providers/{provider}/m3u/decrypted/ffmpeg: {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/ffmpeg: {str(api_err)}"
)
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/providers/<provider>/m3u/decrypted/filtered")
def get_m3u_decrypted_filtered_provider(provider):
"""
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/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"No valid cache found, 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: {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: {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)}"}
@app.route("/api/m3u/subscribed")
def get_m3u_subscribed():
"""
Generate M3U playlist with only subscribed channels.
Note: This uses the same caching mechanism as regular M3U,
but with '_subscribed' suffix in cache filename.
"""
try:
cache_file = "playlist_subscribed.m3u"
# Try cached version
cached_content = service.vfs.read_text(cache_file)
if cached_content:
logger.info("Serving cached subscribed M3U playlist")
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_subscribed.m3u8"'
)
return cached_content
# Generate new M3U with subscribed channels
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)
for channel in channels:
# Generate M3U entry for each subscribed channel
channel_id = channel.channel_id
channel_name = channel.name
channel_logo = channel.logo_url or ""
# 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
# Build stream URL with /index.mpd
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/stream/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'
# Add DRM directives if available
try:
drm_configs = manager.get_channel_drm_configs(
provider_name, channel_id
)
if drm_configs:
drm_directives = service._generate_drm_directives(
drm_configs
)
m3u_content += drm_directives
except Exception as drm_err:
logger.debug(
f"Could not get DRM for {provider_name}/{channel_id}: {drm_err}"
)
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
# Cache the result
if service.vfs.write_text(cache_file, m3u_content):
logger.info(f"Subscribed M3U playlist cached to {cache_file}")
response.content_type = "audio/x-mpegurl; charset=utf-8"
response.headers["Content-Disposition"] = (
'attachment; filename="playlist_subscribed.m3u8"'
)
return m3u_content
except Exception as api_err:
logger.error(f"API Error in /api/m3u/subscribed: {str(api_err)}")
response.status = 500
return {"error": f"Internal server error: {str(api_err)}"}
@app.route("/api/m3u/subscribed/generate")
def generate_m3u_subscribed():
"""Force regenerate subscribed M3U playlist"""
try:
# Clear cache and regenerate
cache_file = "playlist_subscribed.m3u"
service.vfs.delete(cache_file) # Delete if exists
# Call the subscribed M3U endpoint which will regenerate
return get_m3u_subscribed()
except Exception as api_err:
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)}"}