diff --git a/api.py b/api.py new file mode 100644 index 0000000..80404ab --- /dev/null +++ b/api.py @@ -0,0 +1,68 @@ +import argparse + +from flask import ( + Flask, +) + +import modules.logs as logging +from modules.errors import determine_exit_code +from consts import ( + DEFAULT_LOG_DIR, + DEFAULT_DATABASE_PATH, + CONSOLE_LOG_LEVEL, + FILE_LOG_LEVEL, + FLASK_ADDRESS, + FLASK_PORT, + FLASK_DATABASE_PATH, +) +from api.routes.index import index +from api.routes.webhooks.tautulli.index import webhooks_tautulli + +APP_NAME = "API" + +# Parse CLI arguments +parser = argparse.ArgumentParser(description="Tauticord API - API for Tauticord") +""" +Bot will use config, in order: +1. Explicit config file path provided as CLI argument, if included, or +2. Default config file path, if exists, or +3. Environmental variables +""" +parser.add_argument("-l", "--log", help="Log file directory", default=DEFAULT_LOG_DIR) +parser.add_argument("-d", "--database", help="Path to database file", default=DEFAULT_DATABASE_PATH) +args = parser.parse_args() + + +def run_with_potential_exit_on_error(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + logging.fatal(f"Fatal error occurred. Shutting down: {e}") + exit_code = determine_exit_code(exception=e) + logging.fatal(f"Exiting with code {exit_code}") + exit(exit_code) + + return wrapper + + +@run_with_potential_exit_on_error +def set_up_logging(): + logging.init(app_name=APP_NAME, + console_log_level=CONSOLE_LOG_LEVEL, + log_to_file=True, + log_file_dir=args.log, + file_log_level=FILE_LOG_LEVEL) + + +# Register Flask blueprints +application = Flask(APP_NAME) +application.config[FLASK_DATABASE_PATH] = args.database + +application.register_blueprint(index) +application.register_blueprint(webhooks_tautulli) + +if __name__ == "__main__": + set_up_logging() + + application.run(host=FLASK_ADDRESS, port=FLASK_PORT, debug=False, use_reloader=False) diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/controllers/__init__.py b/api/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/webhook_processor.py b/api/controllers/webhook_processor.py similarity index 85% rename from modules/webhook_processor.py rename to api/controllers/webhook_processor.py index 2f44a92..e184a54 100644 --- a/modules/webhook_processor.py +++ b/api/controllers/webhook_processor.py @@ -4,10 +4,9 @@ jsonify, request as flask_request, ) -from sqlalchemy.testing.plugin.plugin_base import logging +import modules.logs as logging import modules.database.repository as db -from modules.discord.bot import Bot from modules.webhooks import RecentlyAddedWebhook @@ -16,7 +15,7 @@ def __init__(self): pass @staticmethod - def process_tautulli_recently_added_webhook(request: flask_request, bot: Bot, database_path: str) -> [Union[str, None], int]: + def process_tautulli_recently_added_webhook(request: flask_request, database_path: str) -> [Union[str, None], int]: """ Process a configured recently-added webhook from Tautulli. Return an empty response and a 200 status code back to Tautulli as confirmation. diff --git a/api/routes/index.py b/api/routes/index.py new file mode 100644 index 0000000..84b3a7d --- /dev/null +++ b/api/routes/index.py @@ -0,0 +1,27 @@ +from flask import ( + Blueprint, + request, + Response as FlaskResponse, +) + +from consts import ( + FLASK_POST, + FLASK_GET, +) + +index = Blueprint("index", __name__, url_prefix="") + + +@index.route("/ping", methods=[FLASK_GET]) +def ping(): + return 'Pong!', 200 + + +@index.route("/hello", methods=[FLASK_GET]) +def hello_world(): + return 'Hello, World!', 200 + + +@index.route("/health", methods=[FLASK_GET]) +def health_check(): + return 'OK', 200 diff --git a/api/routes/webhooks/tautulli/index.py b/api/routes/webhooks/tautulli/index.py new file mode 100644 index 0000000..fc07491 --- /dev/null +++ b/api/routes/webhooks/tautulli/index.py @@ -0,0 +1,22 @@ +from flask import ( + Blueprint, + request as flask_request, + Response as FlaskResponse, + current_app, +) + +from api.controllers.webhook_processor import WebhookProcessor +from consts import ( + FLASK_POST, + FLASK_GET, + FLASK_DATABASE_PATH, +) + +webhooks_tautulli = Blueprint("tautulli", __name__, url_prefix="/webhooks/tautulli") + + +@webhooks_tautulli.route("/recently_added", methods=[FLASK_POST]) +def tautulli_webhook(): + database_path = current_app.config[FLASK_DATABASE_PATH] + return WebhookProcessor.process_tautulli_recently_added_webhook(request=flask_request, + database_path=database_path) diff --git a/consts.py b/consts.py index a378398..cb5458b 100644 --- a/consts.py +++ b/consts.py @@ -10,3 +10,6 @@ GITHUB_REPO_MASTER_BRANCH = "master" FLASK_ADDRESS = "0.0.0.0" FLASK_PORT = 8283 +FLASK_POST = "POST" +FLASK_GET = "GET" +FLASK_DATABASE_PATH = "FLASK_DATABASE_PATH" diff --git a/ecosystem.config.json b/ecosystem.config.json index 4344ecb..d0d8947 100644 --- a/ecosystem.config.json +++ b/ecosystem.config.json @@ -8,6 +8,15 @@ "exec_mode": "fork", "instances": 1 }, + { + "name": "api", + "interpreter": "/app/venv/bin/python", + "script": "api.py", + "autorestart": true, + "exec_mode": "fork", + "instances": 1, + "stop_exit_codes": [302] + }, { "name": "tauticord", "interpreter": "/app/venv/bin/python", @@ -22,7 +31,7 @@ // 101 - Discord login failed // 102 - Missing privileged intents // 301 - Migrations failed - "stop_exit_codes": [101, 102, 301] + "stop_exit_codes": [101, 102, 301, 302] } ] } diff --git a/modules/errors.py b/modules/errors.py index 5f7e385..e75271c 100644 --- a/modules/errors.py +++ b/modules/errors.py @@ -39,6 +39,15 @@ def __init__(self, message: str): super().__init__(code=303, message=message) +class TauticordAPIFailure(TauticordException): + """ + Raised when an error occurs during an API call + """ + + def __init__(self, message: str): + super().__init__(code=304, message=message) + + def determine_exit_code(exception: Exception) -> int: """ Determine the exit code based on the exception that was thrown diff --git a/run.py b/run.py index 3831755..21ac7bc 100644 --- a/run.py +++ b/run.py @@ -1,11 +1,6 @@ import argparse import os -import threading -from flask import ( - Flask, - request as flask_request, -) from pydantic import ValidationError as PydanticValidationError import modules.logs as logging @@ -18,8 +13,6 @@ DEFAULT_DATABASE_PATH, CONSOLE_LOG_LEVEL, FILE_LOG_LEVEL, - FLASK_ADDRESS, - FLASK_PORT, ) from migrations.migration_manager import MigrationManager from modules import versioning @@ -42,7 +35,6 @@ KEY_RUN_ARGS_MONITOR_PATH, KEY_RUN_ARGS_DATABASE_PATH, ) -from modules.webhook_processor import WebhookProcessor # Parse CLI arguments parser = argparse.ArgumentParser(description="Tauticord - Discord bot for Tautulli") @@ -201,36 +193,6 @@ def set_up_discord_bot(config: Config, ) -@run_with_potential_exit_on_error -def start_api(config: Config, discord_bot: Bot, database_path: str) -> [Flask, threading.Thread]: - api = Flask(APP_NAME) - - @api.route('/ping', methods=['GET']) - def ping(): - return 'Pong!', 200 - - @api.route('/hello', methods=['GET']) - def hello_world(): - return 'Hello, World!', 200 - - @api.route('/health', methods=['GET']) - def health_check(): - return 'OK', 200 - - @api.route('/webhooks/tautulli/recently_added', methods=['POST']) - def tautulli_webhook(): - return WebhookProcessor.process_tautulli_recently_added_webhook(request=flask_request, - bot=discord_bot, - database_path=database_path) - - flask_thread = threading.Thread( - target=lambda: api.run(host=FLASK_ADDRESS, port=FLASK_PORT, debug=False, use_reloader=False)) - logging.info("Starting Flask server") - flask_thread.start() - - return api, flask_thread - - @run_with_potential_exit_on_error def start(discord_bot: Bot) -> None: # Connect the bot to Discord (last step, since it will block and trigger all the sub-services) @@ -251,5 +213,4 @@ def start(discord_bot: Bot) -> None: tautulli_connector=_tautulli_connector, emoji_manager=_emoji_manager, analytics=_analytics) - _api, _flask_thread = start_api(config=_config, discord_bot=_discord_bot, database_path=args.database) start(discord_bot=_discord_bot)