""" Flask application factory for Podcastrr. """ import os from flask import Flask from flask_migrate import Migrate import jinja2 def create_app(config=None): """ Create and configure the Flask application. Args: config: Configuration object or path to configuration file. Returns: Flask application instance. """ app = Flask(__name__, template_folder='templates', static_folder='static') # Load default configuration app.config.from_mapping( SECRET_KEY=os.environ.get('SECRET_KEY', 'dev'), SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URI', f'sqlite:///{os.path.abspath(os.path.join(os.path.dirname(__file__), "instance", "podcastrr.db"))}'), SQLALCHEMY_TRACK_MODIFICATIONS=False, DOWNLOAD_PATH=os.environ.get('DOWNLOAD_PATH', os.path.join(os.getcwd(), 'downloads')), ) # Load additional configuration if provided if config: app.config.from_mapping(config) # Import and initialize database from app.models.database import db db.init_app(app) Migrate(app, db) # Import and register blueprints from app.web.routes.main import main_bp from app.web.routes.podcasts import podcasts_bp from app.web.routes.settings import settings_bp from app.web.routes.api import api_bp from app.web.routes.tasks import tasks_bp from app.web.routes.calendar import calendar_bp app.register_blueprint(main_bp) app.register_blueprint(podcasts_bp, url_prefix='/podcasts') app.register_blueprint(settings_bp, url_prefix='/settings') app.register_blueprint(api_bp, url_prefix='/api') app.register_blueprint(tasks_bp) app.register_blueprint(calendar_bp, url_prefix='/calendar') # Ensure the download directory exists os.makedirs(app.config['DOWNLOAD_PATH'], exist_ok=True) # Ensure the instance directory exists instance_path = os.path.join(os.path.dirname(__file__), 'instance') os.makedirs(instance_path, exist_ok=True) # Create an empty file in the instance directory to ensure it's writable try: test_file_path = os.path.join(instance_path, '.test_write') with open(test_file_path, 'w') as f: f.write('test') os.remove(test_file_path) except Exception as e: app.logger.error(f"Error writing to instance directory: {str(e)}") app.logger.error("This may indicate a permissions issue with the instance directory.") # Add custom Jinja2 filters @app.template_filter('isdigit') def isdigit_filter(s): """Check if a string contains only digits.""" if s is None: return False return str(s).isdigit() # Add a context processor to make the fallback warning available to all templates @app.context_processor def inject_db_fallback_warning(): """Inject the database fallback warning into all templates.""" return { 'db_fallback_warning': app.config.get('DB_FALLBACK_WARNING', False) } # Create database tables and run migrations with app.app_context(): try: # Get the database path from the config db_uri = app.config['SQLALCHEMY_DATABASE_URI'] app.logger.info(f"Database URI: {db_uri}") # Extract the file path from the URI if db_uri.startswith('sqlite:///'): db_path = db_uri.replace('sqlite:///', '') app.logger.info(f"Database path: {db_path}") # Ensure the directory for the database file exists db_dir = os.path.dirname(db_path) if db_dir: # Only create directory if path has a directory component os.makedirs(db_dir, exist_ok=True) app.logger.info(f"Created database directory: {db_dir}") # Test if we can write to the database directory try: test_file_path = os.path.join(db_dir if db_dir else '.', '.test_db_write') with open(test_file_path, 'w') as f: f.write('test') os.remove(test_file_path) app.logger.info("Database directory is writable") except Exception as e: app.logger.error(f"Error writing to database directory: {str(e)}") app.logger.error("This may indicate a permissions issue with the database directory.") # Check if the database file exists if os.path.exists(db_path): app.logger.info(f"Database file exists: {db_path}") # Check if the database file is readable and writable try: # Check if readable with open(db_path, 'r') as f: pass app.logger.info("Database file is readable") # Check if writable with open(db_path, 'a') as f: pass app.logger.info("Database file is writable") except Exception as e: app.logger.error(f"Error accessing database file: {str(e)}") app.logger.error("This may indicate a permissions issue with the database file.") # Try to fix permissions try: import stat os.chmod(db_path, stat.S_IRUSR | stat.S_IWUSR) app.logger.info("Updated database file permissions") except Exception as e: app.logger.error(f"Error updating database file permissions: {str(e)}") else: app.logger.info(f"Database file does not exist: {db_path}") # Try to create the database file directly try: with open(db_path, 'a') as f: pass app.logger.info(f"Created empty database file: {db_path}") # Set appropriate permissions try: import stat os.chmod(db_path, stat.S_IRUSR | stat.S_IWUSR) app.logger.info("Set database file permissions") except Exception as e: app.logger.error(f"Error setting database file permissions: {str(e)}") except Exception as e: app.logger.error(f"Error creating database file: {str(e)}") app.logger.error("This may indicate a permissions issue with the database file.") else: app.logger.info("Using a non-SQLite database, skipping file checks") # Create all tables first app.logger.info("Creating database tables...") # Try to create the database tables with retries max_retries = 3 retry_count = 0 fallback_used = False while retry_count < max_retries: try: # Test the database connection first app.logger.info("Testing database connection...") with db.engine.connect() as conn: app.logger.info("Database connection successful") # Create the tables db.create_all() app.logger.info("Database tables created successfully") break except Exception as e: retry_count += 1 app.logger.error(f"Error creating database tables (attempt {retry_count}/{max_retries}): {str(e)}") # If we've tried multiple times and still failing, try a fallback approach if retry_count >= max_retries and not fallback_used: app.logger.warning("Maximum retry attempts reached. Trying fallback approach...") try: # Create a fallback database in memory app.logger.info("Attempting to use in-memory SQLite database as fallback") app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' # Reinitialize the database with the new connection string db.init_app(app) # Reset retry counter and set fallback flag retry_count = 0 fallback_used = True app.logger.info("Fallback database configured, retrying...") # Add a warning message that will be displayed in the application app.config['DB_FALLBACK_WARNING'] = True app.logger.warning("WARNING: Using in-memory database as fallback. Data will not be persisted between application restarts!") continue except Exception as fallback_error: app.logger.error(f"Error setting up fallback database: {str(fallback_error)}") if retry_count >= max_retries: if fallback_used: app.logger.error("Maximum retry attempts reached with fallback. Could not create database tables.") else: app.logger.error("Maximum retry attempts reached. Could not create database tables.") raise import time time.sleep(1) # Wait a second before retrying # Then run all migration scripts import importlib migrations_dir = os.path.join(os.path.dirname(__file__), 'migrations') for filename in os.listdir(migrations_dir): if filename.endswith('.py') and filename != '__init__.py': module_name = f"migrations.{filename[:-3]}" try: migration_module = importlib.import_module(module_name) if hasattr(migration_module, 'run_migration'): app.logger.info(f"Running migration: {filename}") migration_module.run_migration() except Exception as e: app.logger.error(f"Error running migration {filename}: {str(e)}") except Exception as e: app.logger.error(f"Error during database initialization: {str(e)}") # Log more detailed error information import traceback app.logger.error(traceback.format_exc()) return app