#!/usr/bin/env python3 """ API Documentation route handlers """ import html as html_lib import re from bottle import response from streaming_providers.base.utils import logger # HTML template with placeholders for dynamic content PAGE_TEMPLATE = """ Ultimate Backend API Documentation

📚 API Documentation

Total Endpoints: {{TOTAL}}
Categories: {{CATEGORIES_COUNT}}
{{ENDPOINTS}}
🔍 No endpoints match your search. Try a different term.
""" def setup_docs_routes(app, manager=None, service=None): """ Setup API documentation routes. Args: app: Bottle application instance manager: Provider manager (unused but kept for consistent signature) service: Ultimate service instance (unused but kept for consistent signature) """ # These are intentionally unused but kept for consistent API with other route modules # manager and service may be used in future versions for live API testing _ = manager, service # Suppress lint warnings @app.route("/api/docs") def api_docs_html(): """ Display all available API routes with descriptions (HTML version) Example: http://localhost:7777/api/docs """ try: # Collect all routes from the Bottle app routes = [] for route in app.routes: # Skip internal routes if route.rule.startswith('/_') or route.rule.startswith('/static'): continue # Skip OPTIONS and HEAD (usually auto-generated) if route.method in ('OPTIONS', 'HEAD'): continue # Get route info method = route.method rule = route.rule # Try to get the docstring from the route's callback docstring = None if hasattr(route, 'callback'): callback = route.callback if hasattr(callback, '__doc__'): docstring = callback.__doc__ elif hasattr(callback, '__wrapped__') and hasattr(callback.__wrapped__, '__doc__'): docstring = callback.__wrapped__.__doc__ # Clean up docstring if docstring: # Remove leading/trailing whitespace and get first line docstring = docstring.strip().split('\n')[0].strip() else: docstring = 'No description available' routes.append({ 'method': method, 'path': rule, 'description': docstring }) # Sort routes by path then method routes.sort(key=lambda r: (r['path'], r['method'])) # Group routes by category categories = { 'Providers': [], 'Channels': [], 'Streams': [], 'M3U Playlists': [], 'DRM & PSSH': [], 'EPG': [], 'VOD': [], 'Events': [], 'Recordings': [], 'Timers': [], 'Bookmarks': [], 'Favorites': [], 'Cache': [], 'Configuration': [], 'Other': [] } # Categorize routes for route in routes: path = route['path'] # Check for specific route patterns (order matters - most specific first) if '/m3u' in path: categories['M3U Playlists'].append(route) elif '/drm' in path or '/pssh' in path: categories['DRM & PSSH'].append(route) elif '/epg' in path: categories['EPG'].append(route) elif '/cache' in path: categories['Cache'].append(route) elif '/config' in path: categories['Configuration'].append(route) elif '/bookmarks' in path: categories['Bookmarks'].append(route) elif '/favorites' in path: categories['Favorites'].append(route) elif '/providers' in path: # Provider-relative routes if '/channels' in path: if '/stream' in path: categories['Streams'].append(route) elif '/epg' in path: categories['EPG'].append(route) else: categories['Channels'].append(route) elif '/vod' in path: categories['VOD'].append(route) elif '/events' in path: categories['Events'].append(route) elif '/recordings' in path: categories['Recordings'].append(route) elif '/timers' in path: categories['Timers'].append(route) elif '/m3u' in path: categories['M3U Playlists'].append(route) else: categories['Providers'].append(route) else: categories['Other'].append(route) # Remove empty categories categories = {k: v for k, v in categories.items() if v} # Build endpoints HTML with proper escaping endpoints_html = "" total_routes = 0 for category, routes_list in categories.items(): total_routes += len(routes_list) category_escaped = html_lib.escape(category) # No inline onclick or IDs - uses event delegation in JS endpoints_html += f'
' endpoints_html += f'
' endpoints_html += f'{category_escaped}' endpoints_html += f'{len(routes_list)} endpoints' endpoints_html += '
' endpoints_html += '
' for route in routes_list: method = html_lib.escape(route['method']) path = html_lib.escape(route['path']) desc = html_lib.escape(route['description']) # Replace path parameters with styled spans # Match escaped angle brackets from html.escape path_display = re.sub( r'<([^:>]+):([^>]+)>', r':\2', path ) path_display = re.sub( r'<([^>]+)>', r'\1', path_display ) endpoints_html += ( f'
' ) endpoints_html += f'{method}' endpoints_html += f'
{path_display}
' endpoints_html += f'{desc}' endpoints_html += '
' endpoints_html += '
' # Build final page using replace to avoid format() brace issues page = PAGE_TEMPLATE page = page.replace("{{ENDPOINTS}}", endpoints_html) page = page.replace("{{TOTAL}}", str(total_routes)) page = page.replace("{{CATEGORIES_COUNT}}", str(len(categories))) response.content_type = "text/html; charset=utf-8" return page except Exception as e: logger.error(f"Error generating API documentation: {e}") response.status = 500 response.content_type = "application/json" return {"error": "Failed to generate API documentation", "message": str(e)} @app.route("/api/docs/json") def api_docs_json(): """ Get API documentation as JSON (machine-readable format) Example: http://localhost:7777/api/docs/json """ try: routes = [] for route in app.routes: # Skip internal routes if route.rule.startswith('/_') or route.rule.startswith('/static'): continue if route.method in ('OPTIONS', 'HEAD'): continue docstring = None if hasattr(route, 'callback'): callback = route.callback if hasattr(callback, '__doc__'): docstring = callback.__doc__ elif hasattr(callback, '__wrapped__') and hasattr(callback.__wrapped__, '__doc__'): docstring = callback.__wrapped__.__doc__ if docstring: docstring = docstring.strip().split('\n')[0].strip() routes.append({ 'method': route.method, 'path': route.rule, 'description': docstring or 'No description available' }) routes.sort(key=lambda r: (r['path'], r['method'])) response.content_type = "application/json; charset=utf-8" return { 'total': len(routes), 'routes': routes } except Exception as e: logger.error(f"Error generating JSON API docs: {e}") response.status = 500 response.content_type = "application/json" return {"error": "Failed to generate API documentation", "message": str(e)}