from logging.handlers import RotatingFileHandler import os import time from flask_socketio import SocketIO import sys from flask import Flask, has_request_context from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from psycopg2 import OperationalError import redis import spotipy from spotipy.oauth2 import SpotifyClientCredentials from celery import Celery from celery.schedules import crontab from sqlalchemy import create_engine from config import Config from jellyfin.client import JellyfinClient import logging from spotdl.utils.config import DEFAULT_CONFIG from flask_caching import Cache from .version import __version__ def check_db_connection(db_uri, retries=5, delay=5): """ Check if the database is reachable. Args: db_uri (str): The database URI. retries (int): Number of retry attempts. delay (int): Delay between retries in seconds. Raises: SystemExit: If the database is not reachable after retries. """ for attempt in range(1, retries + 1): try: engine = create_engine(db_uri) connection = engine.connect() connection.close() app.logger.info("Successfully connected to the database.") return except OperationalError as e: app.logger.error(f"Database connection failed on attempt {attempt}/{retries}: {e}") if attempt < retries: app.logger.info(f"Retrying in {delay} seconds...") time.sleep(delay) else: app.logger.critical("Could not connect to the database. Exiting application.") sys.exit(1) # Celery setup def make_celery(app): celery = Celery( app.import_name, backend=app.config['result_backend'], broker=app.config['CELERY_BROKER_URL'], include=['app.tasks'] ) celery.conf.update(app.config) # Configure Celery Beat schedule celery.conf.beat_schedule = { 'download-missing-tracks-schedule': { 'task': 'app.tasks.download_missing_tracks', 'schedule': crontab(minute='30'), }, 'check-playlist-updates-schedule': { 'task': 'app.tasks.check_for_playlist_updates', 'schedule': crontab(minute='25'), }, 'update_all_playlists_track_status-schedule': { 'task': 'app.tasks.update_all_playlists_track_status', 'schedule': crontab(minute='*/5'), }, 'update_jellyfin_id_for_downloaded_tracks-schedule': { 'task': 'app.tasks.update_jellyfin_id_for_downloaded_tracks', 'schedule': crontab(minute='*/10'), } } if app.config['LIDARR_API_KEY']: celery.conf.beat_schedule['request-lidarr-schedule'] = { 'task': 'app.tasks.request_lidarr', 'schedule': crontab(minute='*/15') } celery.conf.timezone = 'UTC' return celery # Why this ? Because we are using the same admin login for web, worker and beat we need to distinguish the device_id´s device_id = f'JellyPlist_{'_'.join(sys.argv)}' # Initialize Flask app app = Flask(__name__, template_folder="../templates", static_folder='../static') app.config.from_object(Config) for handler in app.logger.handlers: app.logger.removeHandler(handler) log_level = getattr(logging, app.config['LOG_LEVEL'], logging.INFO) # Default to DEBUG if invalid app.logger.setLevel(log_level) FORMAT = "[%(asctime)s][%(filename)18s:%(lineno)4s - %(funcName)20s() ] %(levelname)7s - %(message)s" logging.basicConfig(format=FORMAT) Config.validate_env_vars() cache = Cache(app) redis_client = redis.StrictRedis(host=app.config['CACHE_REDIS_HOST'], port=app.config['CACHE_REDIS_PORT'], db=0, decode_responses=True) # Spotify, Jellyfin, and Spotdl setup app.logger.info(f"setting up spotipy") sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials( client_id=app.config['SPOTIFY_CLIENT_ID'], client_secret=app.config['SPOTIFY_CLIENT_SECRET'] )) app.logger.info(f"setting up jellyfin client, BaseUrl = {app.config['JELLYFIN_SERVER_URL']}, timeout = {app.config['JELLYFIN_REQUEST_TIMEOUT']}") jellyfin = JellyfinClient(app.config['JELLYFIN_SERVER_URL'], app.config['JELLYFIN_REQUEST_TIMEOUT']) jellyfin_admin_token, jellyfin_admin_id, jellyfin_admin_name, jellyfin_admin_is_admin = jellyfin.login_with_password( app.config['JELLYFIN_ADMIN_USER'], app.config['JELLYFIN_ADMIN_PASSWORD'], device_id= device_id ) # SQLAlchemy and Migrate setup app.logger.info(f"connecting to db: {app.config['JELLYPLIST_DB_HOST']}:{app.config['JELLYPLIST_DB_PORT']}") db_uri = f'postgresql://{app.config["JELLYPLIST_DB_USER"]}:{app.config["JELLYPLIST_DB_PASSWORD"]}@{app.config["JELLYPLIST_DB_HOST"]}:{app.config['JELLYPLIST_DB_PORT']}/jellyplist' check_db_connection(db_uri=db_uri,retries=5,delay=2) app.config['SQLALCHEMY_DATABASE_URI'] = db_uri app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) app.logger.info(f"applying db migrations") migrate = Migrate(app, db) # Celery Configuration (Updated) app.config.update( CELERY_BROKER_URL=app.config['REDIS_URL'], result_backend=app.config['REDIS_URL'] ) def read_dev_build_file(file_path="/jellyplist/DEV_BUILD"): if os.path.exists(file_path): with open(file_path, "r") as file: content = file.read().strip() return f"-{content}" else: return '' app.logger.info(f"initializing celery") celery = make_celery(app) socketio = SocketIO(app, message_queue=app.config['REDIS_URL'], async_mode='eventlet') celery.set_default() app.logger.info(f'Jellyplist {__version__}{read_dev_build_file()} started') app.logger.debug(f"Debug logging active") from app.routes import pl_bp, routes, jellyfin_routes app.register_blueprint(pl_bp) from app import filters # Import the filters dictionary # Register all filters for name, func in filters.filters.items(): app.jinja_env.filters[name] = func from .providers import SpotifyClient if app.config['SPOTIFY_COOKIE_FILE']: if os.path.exists(app.config['SPOTIFY_COOKIE_FILE']): spotify_client = SpotifyClient(app.config['SPOTIFY_COOKIE_FILE']) else: app.logger.error(f"Cookie file {app.config['SPOTIFY_COOKIE_FILE']} does not exist. Exiting.") sys.exit(1) else: spotify_client = SpotifyClient() spotify_client.authenticate() from .registry import MusicProviderRegistry MusicProviderRegistry.register_provider(spotify_client) if app.config['LIDARR_API_KEY'] and app.config['LIDARR_URL']: app.logger.info(f'Creating Lidarr Client with URL: {app.config["LIDARR_URL"]}') from lidarr.client import LidarrClient lidarr_client = LidarrClient(app.config['LIDARR_URL'], app.config['LIDARR_API_KEY'])