244 lines
11 KiB
Python
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
|