podcastrr/application.py
2025-06-17 16:00:46 -07:00

244 lines
11 KiB
Python

"""
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