570 lines
18 KiB
Python
570 lines
18 KiB
Python
"""
|
|
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/<key_id>', 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/<webhook_id>', 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/<serie_folder>/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 <api_key>',
|
|
'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'] |