""" Initial database schema migration. This migration creates the base tables for the Aniworld application, including users, anime, downloads, and configuration tables. Version: 20250124_001 Created: 2025-01-24 """ import logging from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from ..migrations.base import Migration, MigrationError logger = logging.getLogger(__name__) class InitialSchemaMigration(Migration): """ Creates initial database schema. This migration sets up all core tables needed for the application: - users: User accounts and authentication - anime: Anime series metadata - episodes: Episode information - downloads: Download queue and history - config: Application configuration """ def __init__(self): """Initialize the initial schema migration.""" super().__init__( version="20250124_001", description="Create initial database schema", ) async def upgrade(self, session: AsyncSession) -> None: """ Create all initial tables. Args: session: Database session Raises: MigrationError: If table creation fails """ try: # Create users table await session.execute( text( """ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, email TEXT, password_hash TEXT NOT NULL, is_active BOOLEAN DEFAULT 1, is_admin BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) ) # Create anime table await session.execute( text( """ CREATE TABLE IF NOT EXISTS anime ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, original_title TEXT, description TEXT, genres TEXT, release_year INTEGER, status TEXT, total_episodes INTEGER, cover_image_url TEXT, aniworld_url TEXT, mal_id INTEGER, anilist_id INTEGER, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) ) # Create episodes table await session.execute( text( """ CREATE TABLE IF NOT EXISTS episodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, anime_id INTEGER NOT NULL, episode_number INTEGER NOT NULL, season_number INTEGER DEFAULT 1, title TEXT, description TEXT, duration_minutes INTEGER, air_date DATE, stream_url TEXT, download_url TEXT, file_path TEXT, file_size_bytes INTEGER, is_downloaded BOOLEAN DEFAULT 0, download_progress REAL DEFAULT 0.0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (anime_id) REFERENCES anime(id) ON DELETE CASCADE, UNIQUE (anime_id, season_number, episode_number) ) """ ) ) # Create downloads table await session.execute( text( """ CREATE TABLE IF NOT EXISTS downloads ( id INTEGER PRIMARY KEY AUTOINCREMENT, episode_id INTEGER NOT NULL, user_id INTEGER, status TEXT NOT NULL DEFAULT 'pending', priority INTEGER DEFAULT 5, progress REAL DEFAULT 0.0, download_speed_mbps REAL, eta_seconds INTEGER, started_at TIMESTAMP, completed_at TIMESTAMP, failed_at TIMESTAMP, error_message TEXT, retry_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (episode_id) REFERENCES episodes(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ) """ ) ) # Create config table await session.execute( text( """ CREATE TABLE IF NOT EXISTS config ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL UNIQUE, value TEXT NOT NULL, category TEXT DEFAULT 'general', description TEXT, is_secret BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) ) # Create indexes for better performance await session.execute( text( "CREATE INDEX IF NOT EXISTS idx_anime_title " "ON anime(title)" ) ) await session.execute( text( "CREATE INDEX IF NOT EXISTS idx_episodes_anime_id " "ON episodes(anime_id)" ) ) await session.execute( text( "CREATE INDEX IF NOT EXISTS idx_downloads_status " "ON downloads(status)" ) ) await session.execute( text( "CREATE INDEX IF NOT EXISTS " "idx_downloads_episode_id ON downloads(episode_id)" ) ) logger.info("Initial schema created successfully") except Exception as e: logger.error(f"Failed to create initial schema: {e}") raise MigrationError( f"Initial schema creation failed: {e}" ) from e async def downgrade(self, session: AsyncSession) -> None: """ Drop all initial tables. Args: session: Database session Raises: MigrationError: If table dropping fails """ try: # Drop tables in reverse order to respect foreign keys tables = [ "downloads", "episodes", "anime", "users", "config", ] for table in tables: await session.execute(text(f"DROP TABLE IF EXISTS {table}")) logger.debug(f"Dropped table: {table}") logger.info("Initial schema rolled back successfully") except Exception as e: logger.error(f"Failed to rollback initial schema: {e}") raise MigrationError( f"Initial schema rollback failed: {e}" ) from e