mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-23 17:42:19 +02:00
482 lines
20 KiB
Python
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)}"} |