fixed file naming thingy
This commit is contained in:
@@ -47,13 +47,102 @@ Base = declarative_base()
|
||||
def init_database():
|
||||
"""Initialize database - called on container startup"""
|
||||
try:
|
||||
# Check if database file exists
|
||||
if DB_PATH.exists():
|
||||
logger.info(f"Database file exists at {DB_PATH}, checking for schema upgrades")
|
||||
upgrade_database_schema()
|
||||
else:
|
||||
logger.info(f"Creating new database at {DB_PATH}")
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
logger.info(f"Database initialized at {DB_PATH}")
|
||||
|
||||
# Verify the schema after creation/upgrade
|
||||
verify_database_schema()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def verify_database_schema():
|
||||
"""Verify that the database schema has all required columns"""
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(text("PRAGMA table_info(scheduled_tasks)"))
|
||||
columns = [row[1] for row in result.fetchall()]
|
||||
|
||||
required_columns = ['upvoted', 'saved']
|
||||
missing_columns = [col for col in required_columns if col not in columns]
|
||||
|
||||
if missing_columns:
|
||||
logger.error(f"Database schema verification failed. Missing columns: {missing_columns}")
|
||||
logger.error(f"Available columns: {columns}")
|
||||
else:
|
||||
logger.info("Database schema verification passed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to verify database schema: {e}")
|
||||
|
||||
|
||||
def upgrade_database_schema():
|
||||
"""Check and upgrade database schema for new columns"""
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
|
||||
logger.info("Starting database schema upgrade check...")
|
||||
|
||||
# Check if scheduled_tasks table exists first
|
||||
with engine.connect() as conn:
|
||||
# Check if table exists
|
||||
result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='scheduled_tasks'"))
|
||||
table_exists = result.fetchone() is not None
|
||||
|
||||
if not table_exists:
|
||||
logger.info("scheduled_tasks table does not exist, will be created by create_all")
|
||||
return
|
||||
|
||||
logger.info("scheduled_tasks table exists, checking columns...")
|
||||
|
||||
# Get table info
|
||||
result = conn.execute(text("PRAGMA table_info(scheduled_tasks)"))
|
||||
columns = [row[1] for row in result.fetchall()]
|
||||
|
||||
logger.info(f"Existing columns in scheduled_tasks: {columns}")
|
||||
|
||||
# Check for missing columns
|
||||
missing_columns = []
|
||||
if 'upvoted' not in columns:
|
||||
missing_columns.append('upvoted BOOLEAN DEFAULT 0')
|
||||
if 'saved' not in columns:
|
||||
missing_columns.append('saved BOOLEAN DEFAULT 0')
|
||||
|
||||
# Add missing columns
|
||||
for column in missing_columns:
|
||||
logger.info(f"Adding missing column: {column}")
|
||||
try:
|
||||
conn.execute(text(f"ALTER TABLE scheduled_tasks ADD COLUMN {column}"))
|
||||
conn.commit()
|
||||
logger.info(f"Successfully added column: {column}")
|
||||
except Exception as alter_error:
|
||||
logger.error(f"Failed to add column {column}: {alter_error}")
|
||||
raise
|
||||
|
||||
if missing_columns:
|
||||
logger.info(f"Database schema upgraded with columns: {missing_columns}")
|
||||
else:
|
||||
logger.info("Database schema is up to date")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upgrade database schema: {e}")
|
||||
logger.error(f"Error type: {type(e)}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
# Don't raise here - let the app continue with create_all
|
||||
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
Dependency for FastAPI to get database session.
|
||||
|
||||
@@ -83,15 +83,20 @@ async def startup_event():
|
||||
"""Initialize services on application startup"""
|
||||
try:
|
||||
logger.info("Starting up BDFR Web Interface...")
|
||||
|
||||
|
||||
# Initialize database
|
||||
init_database()
|
||||
logger.info("Database initialized")
|
||||
|
||||
|
||||
# Ensure database schema is up to date before starting scheduler
|
||||
from .database import upgrade_database_schema
|
||||
upgrade_database_schema()
|
||||
logger.info("Database schema check complete")
|
||||
|
||||
# Start scheduler
|
||||
start_scheduler()
|
||||
logger.info("Scheduler started")
|
||||
|
||||
|
||||
logger.info("Startup complete!")
|
||||
except Exception as e:
|
||||
logger.error(f"Startup error: {e}", exc_info=True)
|
||||
|
||||
@@ -56,7 +56,11 @@ class ScheduledTask(Base):
|
||||
|
||||
# Authentication
|
||||
auth_state = Column(String(255), nullable=True)
|
||||
|
||||
|
||||
# User-specific download options
|
||||
upvoted = Column(Boolean, default=False, nullable=False)
|
||||
saved = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Relationships
|
||||
executions = relationship("TaskExecutionHistory", back_populates="task", cascade="all, delete-orphan")
|
||||
|
||||
@@ -82,7 +86,9 @@ class ScheduledTask(Base):
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
"last_run_at": self.last_run_at.isoformat() if self.last_run_at else None,
|
||||
"next_run_at": self.next_run_at.isoformat() if self.next_run_at else None,
|
||||
"auth_state": self.auth_state
|
||||
"auth_state": self.auth_state,
|
||||
"upvoted": getattr(self, 'upvoted', False),
|
||||
"saved": getattr(self, 'saved', False)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from .database import get_db
|
||||
from .models import ScheduledTask, TaskExecutionHistory
|
||||
from .scheduler import schedule_task, unschedule_task, calculate_next_run
|
||||
from .task_queue import task_queue
|
||||
from .auth import get_oauth_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,6 +35,8 @@ class ScheduledTaskCreate(BaseModel):
|
||||
run_time: str = Field(..., pattern="^([01]?[0-9]|2[0-3]):[0-5][0-9]$") # HH:MM format
|
||||
timezone: str = Field(default="UTC")
|
||||
auth_state: Optional[str] = None
|
||||
upvoted: bool = False
|
||||
saved: bool = False
|
||||
|
||||
|
||||
class ScheduledTaskUpdate(BaseModel):
|
||||
@@ -67,7 +70,10 @@ class ScheduledTaskResponse(BaseModel):
|
||||
updated_at: str
|
||||
last_run_at: Optional[str]
|
||||
next_run_at: Optional[str]
|
||||
|
||||
auth_state: Optional[str]
|
||||
upvoted: bool
|
||||
saved: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -124,7 +130,9 @@ async def create_scheduled_task(task_data: ScheduledTaskCreate, db: Session = De
|
||||
schedule_frequency="daily",
|
||||
run_time=run_time_obj,
|
||||
timezone=task_data.timezone,
|
||||
auth_state=task_data.auth_state
|
||||
auth_state=task_data.auth_state,
|
||||
upvoted=task_data.upvoted,
|
||||
saved=task_data.saved
|
||||
)
|
||||
|
||||
# Calculate next run time
|
||||
@@ -162,6 +170,10 @@ async def list_scheduled_tasks(db: Session = Depends(get_db)):
|
||||
tasks = db.query(ScheduledTask).order_by(ScheduledTask.created_at.desc()).all()
|
||||
return [task.to_dict() for task in tasks]
|
||||
except Exception as e:
|
||||
if "no such column" in str(e):
|
||||
logger.warning(f"Database schema is outdated: {e}")
|
||||
# Return empty list if schema is outdated
|
||||
return []
|
||||
logger.error(f"Failed to list scheduled tasks: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -444,7 +456,7 @@ async def get_queue_status():
|
||||
"""Get current task queue status"""
|
||||
try:
|
||||
status = task_queue.get_queue_status()
|
||||
|
||||
|
||||
# Get details of current task if any
|
||||
current_task_info = None
|
||||
if status['current_task']:
|
||||
@@ -459,16 +471,208 @@ async def get_queue_status():
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
return {
|
||||
'queue_size': status['queue_size'],
|
||||
'is_processing': status['is_processing'],
|
||||
'current_task': current_task_info
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get queue status: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get queue status"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/create-likes", response_model=ScheduledTaskResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_likes_task(
|
||||
limit: int = 25,
|
||||
sort: str = "hot",
|
||||
download_mode: str = "download",
|
||||
run_now: bool = False,
|
||||
run_time: str = "02:00",
|
||||
timezone: str = "UTC",
|
||||
auth_state: str = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a scheduled task to download the user's liked posts.
|
||||
"""
|
||||
try:
|
||||
# Get current username from auth
|
||||
if not auth_state:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
oauth_manager = get_oauth_manager()
|
||||
auth_status = oauth_manager.get_auth_status(auth_state)
|
||||
if not auth_status["authenticated"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication"
|
||||
)
|
||||
|
||||
username = auth_status["username"]
|
||||
task_name = f"{username} - Liked posts"
|
||||
|
||||
# Validate timezone
|
||||
try:
|
||||
pytz.timezone(timezone)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid timezone: {timezone}"
|
||||
)
|
||||
|
||||
# Parse run_time
|
||||
hour, minute = map(int, run_time.split(':'))
|
||||
run_time_obj = time_type(hour=hour, minute=minute)
|
||||
|
||||
# Create task
|
||||
task = ScheduledTask(
|
||||
name=task_name,
|
||||
enabled=True,
|
||||
source_type="user",
|
||||
source_name=username,
|
||||
download_mode=download_mode,
|
||||
limit=limit,
|
||||
sort=sort,
|
||||
time_filter="day",
|
||||
no_dupes=True,
|
||||
simple_check=False,
|
||||
schedule_frequency="daily",
|
||||
run_time=run_time_obj,
|
||||
timezone=timezone,
|
||||
auth_state=auth_state,
|
||||
upvoted=True,
|
||||
saved=False
|
||||
)
|
||||
|
||||
# Calculate next run time
|
||||
task.next_run_at = calculate_next_run(task)
|
||||
|
||||
# Save to database
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
|
||||
# Schedule the task
|
||||
schedule_task(task)
|
||||
|
||||
# If run_now is True, queue it immediately
|
||||
if run_now:
|
||||
await task_queue.add_task(task.id, priority=1)
|
||||
|
||||
logger.info(f"Created likes task {task.id}: {task.name}")
|
||||
|
||||
return task.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create likes task: {e}", exc_info=True)
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create likes task: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/create-saved", response_model=ScheduledTaskResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_saved_task(
|
||||
limit: int = 25,
|
||||
sort: str = "hot",
|
||||
download_mode: str = "download",
|
||||
run_now: bool = False,
|
||||
run_time: str = "02:00",
|
||||
timezone: str = "UTC",
|
||||
auth_state: str = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a scheduled task to download the user's saved posts.
|
||||
"""
|
||||
try:
|
||||
# Get current username from auth
|
||||
if not auth_state:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
oauth_manager = get_oauth_manager()
|
||||
auth_status = oauth_manager.get_auth_status(auth_state)
|
||||
if not auth_status["authenticated"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication"
|
||||
)
|
||||
|
||||
username = auth_status["username"]
|
||||
task_name = f"{username} - Saved posts"
|
||||
|
||||
# Validate timezone
|
||||
try:
|
||||
pytz.timezone(timezone)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid timezone: {timezone}"
|
||||
)
|
||||
|
||||
# Parse run_time
|
||||
hour, minute = map(int, run_time.split(':'))
|
||||
run_time_obj = time_type(hour=hour, minute=minute)
|
||||
|
||||
# Create task
|
||||
task = ScheduledTask(
|
||||
name=task_name,
|
||||
enabled=True,
|
||||
source_type="user",
|
||||
source_name=username,
|
||||
download_mode=download_mode,
|
||||
limit=limit,
|
||||
sort=sort,
|
||||
time_filter="day",
|
||||
no_dupes=True,
|
||||
simple_check=False,
|
||||
schedule_frequency="daily",
|
||||
run_time=run_time_obj,
|
||||
timezone=timezone,
|
||||
auth_state=auth_state,
|
||||
upvoted=False,
|
||||
saved=True
|
||||
)
|
||||
|
||||
# Calculate next run time
|
||||
task.next_run_at = calculate_next_run(task)
|
||||
|
||||
# Save to database
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
|
||||
# Schedule the task
|
||||
schedule_task(task)
|
||||
|
||||
# If run_now is True, queue it immediately
|
||||
if run_now:
|
||||
await task_queue.add_task(task.id, priority=1)
|
||||
|
||||
logger.info(f"Created saved task {task.id}: {task.name}")
|
||||
|
||||
return task.to_dict()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create saved task: {e}", exc_info=True)
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create saved task: {str(e)}"
|
||||
)
|
||||
@@ -132,12 +132,38 @@ def load_scheduled_tasks():
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
tasks = db.query(ScheduledTask).filter(ScheduledTask.enabled == True).all()
|
||||
# Check if the required columns exist before querying
|
||||
try:
|
||||
# First, check if we can access the table at all
|
||||
db.query(ScheduledTask).first()
|
||||
|
||||
# Try to query with the new columns - this will fail if columns don't exist
|
||||
tasks = db.query(ScheduledTask).filter(ScheduledTask.enabled == True).all()
|
||||
except Exception as column_error:
|
||||
if "no such column" in str(column_error):
|
||||
logger.warning(f"Database schema is outdated. Required columns missing: {column_error}")
|
||||
logger.info("Attempting to upgrade database schema...")
|
||||
|
||||
# Try to upgrade the database
|
||||
try:
|
||||
from .database import upgrade_database_schema
|
||||
upgrade_database_schema()
|
||||
|
||||
# Now try again to load tasks
|
||||
tasks = db.query(ScheduledTask).filter(ScheduledTask.enabled == True).all()
|
||||
logger.info("Database upgraded successfully, continuing with task loading")
|
||||
except Exception as upgrade_error:
|
||||
logger.error(f"Failed to upgrade database: {upgrade_error}")
|
||||
logger.info("Skipping scheduled task loading until database is manually upgraded")
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
logger.info(f"Loading {len(tasks)} enabled scheduled tasks")
|
||||
|
||||
|
||||
for task in tasks:
|
||||
schedule_task(task)
|
||||
|
||||
|
||||
logger.info(f"Loaded {len(tasks)} scheduled tasks")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load scheduled tasks: {e}", exc_info=True)
|
||||
@@ -198,8 +224,15 @@ async def execute_scheduled_task(task_id: str):
|
||||
'time_filter': 'day', # Always "day" for daily scheduled tasks
|
||||
'no_dupes': True, # Always enabled for scheduled tasks
|
||||
'simple_check': task.simple_check,
|
||||
'auth_state': task.auth_state
|
||||
'auth_state': task.auth_state,
|
||||
'upvoted': task.upvoted,
|
||||
'saved': task.saved
|
||||
}
|
||||
|
||||
# For user downloads, set submitted appropriately
|
||||
if task.source_type == 'user':
|
||||
# For likes or saved, don't download submitted posts
|
||||
kwargs['submitted'] = not (task.upvoted or task.saved)
|
||||
|
||||
# Create download using existing API
|
||||
download_id = await create_download_with_bdfr_api(
|
||||
|
||||
Reference in New Issue
Block a user