""" API Integration Endpoints This module provides REST API endpoints for external integrations, webhooks, exports, and notifications. """ import json from flask import Blueprint, request, jsonify, make_response, current_app from auth import require_auth, optional_auth from error_handler import handle_api_errors, RetryableError, NonRetryableError from api_integration import ( api_key_manager, webhook_manager, export_manager, notification_service, require_api_key ) # Blueprint for API integration endpoints api_integration_bp = Blueprint('api_integration', __name__) # API Key Management Endpoints @api_integration_bp.route('/api/keys', methods=['GET']) @handle_api_errors @require_auth def list_api_keys(): """List all API keys.""" try: keys = api_key_manager.list_api_keys() return jsonify({ 'status': 'success', 'data': { 'api_keys': keys, 'count': len(keys) } }) except Exception as e: raise RetryableError(f"Failed to list API keys: {e}") @api_integration_bp.route('/api/keys', methods=['POST']) @handle_api_errors @require_auth def create_api_key(): """Create a new API key.""" try: data = request.get_json() name = data.get('name') permissions = data.get('permissions', []) rate_limit = data.get('rate_limit', 1000) if not name: return jsonify({ 'status': 'error', 'message': 'Name is required' }), 400 if not isinstance(permissions, list): return jsonify({ 'status': 'error', 'message': 'Permissions must be a list' }), 400 # Validate permissions valid_permissions = ['read', 'write', 'admin', 'download', 'export'] invalid_permissions = set(permissions) - set(valid_permissions) if invalid_permissions: return jsonify({ 'status': 'error', 'message': f'Invalid permissions: {", ".join(invalid_permissions)}' }), 400 api_key, key_id = api_key_manager.create_api_key(name, permissions, rate_limit) return jsonify({ 'status': 'success', 'message': 'API key created successfully', 'data': { 'api_key': api_key, # Only returned once! 'key_id': key_id, 'name': name, 'permissions': permissions, 'rate_limit': rate_limit } }), 201 except Exception as e: raise RetryableError(f"Failed to create API key: {e}") @api_integration_bp.route('/api/keys/', methods=['DELETE']) @handle_api_errors @require_auth def revoke_api_key(key_id): """Revoke an API key.""" try: success = api_key_manager.revoke_api_key(key_id) if success: return jsonify({ 'status': 'success', 'message': 'API key revoked successfully' }) else: return jsonify({ 'status': 'error', 'message': 'API key not found' }), 404 except Exception as e: raise RetryableError(f"Failed to revoke API key: {e}") # Webhook Management Endpoints @api_integration_bp.route('/api/webhooks', methods=['GET']) @handle_api_errors @require_auth def list_webhooks(): """List all webhook endpoints.""" try: webhooks = webhook_manager.list_webhooks() return jsonify({ 'status': 'success', 'data': { 'webhooks': webhooks, 'count': len(webhooks) } }) except Exception as e: raise RetryableError(f"Failed to list webhooks: {e}") @api_integration_bp.route('/api/webhooks', methods=['POST']) @handle_api_errors @require_auth def create_webhook(): """Create a new webhook endpoint.""" try: data = request.get_json() name = data.get('name') url = data.get('url') events = data.get('events', []) secret = data.get('secret') if not name or not url: return jsonify({ 'status': 'error', 'message': 'Name and URL are required' }), 400 if not isinstance(events, list) or not events: return jsonify({ 'status': 'error', 'message': 'At least one event must be specified' }), 400 # Validate events valid_events = [ 'download.started', 'download.completed', 'download.failed', 'scan.started', 'scan.completed', 'scan.failed', 'series.added', 'series.removed' ] invalid_events = set(events) - set(valid_events) if invalid_events: return jsonify({ 'status': 'error', 'message': f'Invalid events: {", ".join(invalid_events)}' }), 400 webhook_id = webhook_manager.create_webhook(name, url, events, secret) return jsonify({ 'status': 'success', 'message': 'Webhook created successfully', 'data': { 'webhook_id': webhook_id, 'name': name, 'url': url, 'events': events } }), 201 except Exception as e: raise RetryableError(f"Failed to create webhook: {e}") @api_integration_bp.route('/api/webhooks/', methods=['DELETE']) @handle_api_errors @require_auth def delete_webhook(webhook_id): """Delete a webhook endpoint.""" try: success = webhook_manager.delete_webhook(webhook_id) if success: return jsonify({ 'status': 'success', 'message': 'Webhook deleted successfully' }) else: return jsonify({ 'status': 'error', 'message': 'Webhook not found' }), 404 except Exception as e: raise RetryableError(f"Failed to delete webhook: {e}") @api_integration_bp.route('/api/webhooks/test', methods=['POST']) @handle_api_errors @require_auth def test_webhook(): """Test webhook delivery.""" try: data = request.get_json() webhook_id = data.get('webhook_id') if not webhook_id: return jsonify({ 'status': 'error', 'message': 'webhook_id is required' }), 400 # Send test event test_data = { 'message': 'This is a test webhook delivery', 'test': True } webhook_manager.trigger_event('test.webhook', test_data) return jsonify({ 'status': 'success', 'message': 'Test webhook triggered' }) except Exception as e: raise RetryableError(f"Failed to test webhook: {e}") # Export Endpoints @api_integration_bp.route('/api/export/anime-list') @handle_api_errors @require_api_key(['read', 'export']) def export_anime_list(): """Export anime list in JSON or CSV format.""" try: format_type = request.args.get('format', 'json').lower() include_missing_only = request.args.get('missing_only', 'false').lower() == 'true' if format_type not in ['json', 'csv']: return jsonify({ 'status': 'error', 'message': 'Format must be either "json" or "csv"' }), 400 if format_type == 'json': data = export_manager.export_anime_list_json(include_missing_only) response = make_response(jsonify({ 'status': 'success', 'data': data })) response.headers['Content-Type'] = 'application/json' else: # CSV csv_data = export_manager.export_anime_list_csv(include_missing_only) response = make_response(csv_data) response.headers['Content-Type'] = 'text/csv' response.headers['Content-Disposition'] = 'attachment; filename=anime_list.csv' return response except Exception as e: raise RetryableError(f"Failed to export anime list: {e}") @api_integration_bp.route('/api/export/statistics') @handle_api_errors @require_api_key(['read', 'export']) def export_statistics(): """Export download statistics.""" try: data = export_manager.export_download_statistics() return jsonify({ 'status': 'success', 'data': data }) except Exception as e: raise RetryableError(f"Failed to export statistics: {e}") # External API Endpoints (for API key authentication) @api_integration_bp.route('/api/v1/series') @handle_api_errors @require_api_key(['read']) def api_get_series(): """Get series list via API.""" try: # This would integrate with the main series app from app import series_app if not series_app or not series_app.List: return jsonify({ 'status': 'success', 'data': { 'series': [], 'count': 0 } }) series_list = [] for serie in series_app.List.GetList(): series_data = { 'name': serie.name or serie.folder, 'folder': serie.folder, 'key': getattr(serie, 'key', None), 'missing_episodes_count': sum(len(episodes) for episodes in serie.episodeDict.values()) if hasattr(serie, 'episodeDict') and serie.episodeDict else 0 } series_list.append(series_data) return jsonify({ 'status': 'success', 'data': { 'series': series_list, 'count': len(series_list) } }) except Exception as e: raise RetryableError(f"Failed to get series: {e}") @api_integration_bp.route('/api/v1/series//episodes') @handle_api_errors @require_api_key(['read']) def api_get_series_episodes(serie_folder): """Get episodes for a specific series via API.""" try: from app import series_app if not series_app or not series_app.List: return jsonify({ 'status': 'error', 'message': 'Series data not available' }), 404 # Find series by folder target_serie = None for serie in series_app.List.GetList(): if serie.folder == serie_folder: target_serie = serie break if not target_serie: return jsonify({ 'status': 'error', 'message': 'Series not found' }), 404 episodes_data = {} if hasattr(target_serie, 'episodeDict') and target_serie.episodeDict: for season, episodes in target_serie.episodeDict.items(): episodes_data[str(season)] = list(episodes) return jsonify({ 'status': 'success', 'data': { 'series_name': target_serie.name or target_serie.folder, 'folder': target_serie.folder, 'missing_episodes': episodes_data } }) except Exception as e: raise RetryableError(f"Failed to get series episodes: {e}") @api_integration_bp.route('/api/v1/download/start', methods=['POST']) @handle_api_errors @require_api_key(['download']) def api_start_download(): """Start download for specific episodes via API.""" try: data = request.get_json() serie_folder = data.get('serie_folder') season = data.get('season') episode = data.get('episode') if not all([serie_folder, season is not None, episode is not None]): return jsonify({ 'status': 'error', 'message': 'serie_folder, season, and episode are required' }), 400 # This would integrate with the download system # For now, trigger webhook event webhook_manager.trigger_event('download.started', { 'serie_folder': serie_folder, 'season': season, 'episode': episode, 'requested_via': 'api' }) return jsonify({ 'status': 'success', 'message': 'Download started', 'data': { 'serie_folder': serie_folder, 'season': season, 'episode': episode } }) except Exception as e: raise RetryableError(f"Failed to start download: {e}") # Notification Service Endpoints @api_integration_bp.route('/api/notifications/discord', methods=['POST']) @handle_api_errors @require_auth def setup_discord_notifications(): """Setup Discord webhook notifications.""" try: data = request.get_json() webhook_url = data.get('webhook_url') name = data.get('name', 'discord') if not webhook_url: return jsonify({ 'status': 'error', 'message': 'webhook_url is required' }), 400 notification_service.register_discord_webhook(webhook_url, name) return jsonify({ 'status': 'success', 'message': 'Discord notifications configured' }) except Exception as e: raise RetryableError(f"Failed to setup Discord notifications: {e}") @api_integration_bp.route('/api/notifications/telegram', methods=['POST']) @handle_api_errors @require_auth def setup_telegram_notifications(): """Setup Telegram bot notifications.""" try: data = request.get_json() bot_token = data.get('bot_token') chat_id = data.get('chat_id') name = data.get('name', 'telegram') if not bot_token or not chat_id: return jsonify({ 'status': 'error', 'message': 'bot_token and chat_id are required' }), 400 notification_service.register_telegram_bot(bot_token, chat_id, name) return jsonify({ 'status': 'success', 'message': 'Telegram notifications configured' }) except Exception as e: raise RetryableError(f"Failed to setup Telegram notifications: {e}") @api_integration_bp.route('/api/notifications/test', methods=['POST']) @handle_api_errors @require_auth def test_notifications(): """Test notification delivery.""" try: data = request.get_json() service_name = data.get('service_name') notification_service.send_notification( message="This is a test notification from AniWorld API", title="Test Notification", service_name=service_name ) return jsonify({ 'status': 'success', 'message': 'Test notification sent' }) except Exception as e: raise RetryableError(f"Failed to send test notification: {e}") # API Documentation Endpoint @api_integration_bp.route('/api/docs') def api_documentation(): """Get API documentation.""" docs = { 'title': 'AniWorld API Documentation', 'version': '1.0.0', 'description': 'REST API for AniWorld anime download management', 'authentication': { 'type': 'API Key', 'header': 'Authorization: Bearer ', 'note': 'API keys can be created through the web interface' }, 'endpoints': { 'GET /api/v1/series': { 'description': 'Get list of all series', 'permissions': ['read'], 'parameters': {}, 'response': 'List of series with basic information' }, 'GET /api/v1/series/{folder}/episodes': { 'description': 'Get episodes for specific series', 'permissions': ['read'], 'parameters': { 'folder': 'Series folder name' }, 'response': 'Missing episodes for the series' }, 'POST /api/v1/download/start': { 'description': 'Start download for specific episode', 'permissions': ['download'], 'parameters': { 'serie_folder': 'Series folder name', 'season': 'Season number', 'episode': 'Episode number' }, 'response': 'Download status' }, 'GET /api/export/anime-list': { 'description': 'Export anime list', 'permissions': ['read', 'export'], 'parameters': { 'format': 'json or csv', 'missing_only': 'true or false' }, 'response': 'Anime list in requested format' } }, 'webhook_events': [ 'download.started', 'download.completed', 'download.failed', 'scan.started', 'scan.completed', 'scan.failed', 'series.added', 'series.removed' ], 'rate_limits': { 'default': '1000 requests per hour per API key', 'note': 'Rate limits are configurable per API key' } } return jsonify(docs) # Export the blueprint __all__ = ['api_integration_bp']