from sqlalchemy import create_engine, Column, String, Integer, Boolean, Float, Text, DateTime, TypeDecorator from sqlalchemy.orm import declarative_base, sessionmaker from .security import encrypt_config_value, decrypt_config_value DATABASE_URL = "sqlite:///./data/radiosync.db" engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() class EncryptedString(TypeDecorator): impl = String cache_ok = True def process_bind_param(self, value, dialect): return encrypt_config_value(value) def process_result_value(self, value, dialect): return decrypt_config_value(value) class Config(Base): __tablename__ = "config" key = Column(String, primary_key=True, index=True) value = Column(EncryptedString) class Program(Base): __tablename__ = "programs" id = Column(Integer, primary_key=True, index=True) station = Column(String, index=True) # "radio357", "rns", "jazz" slug = Column(String, index=True) name = Column(String) description = Column(Text, nullable=True) image = Column(String, nullable=True) # Sync tracking backfill_page = Column(Integer, default=1) backfill_complete = Column(Boolean, default=False) total_pages = Column(Integer, default=0) last_catchup = Column(Float, default=0.0) next_catchup_after = Column(Float, default=0.0) # timestamp – nie sprawdzaj przed tym czasem class Episode(Base): __tablename__ = "episodes" id = Column(Integer, primary_key=True, index=True) station = Column(String, index=True) program_slug = Column(String, index=True) ep_id = Column(String, index=True) # ID from the radio API title = Column(String) authors = Column(String, nullable=True) url = Column(String, nullable=True) image = Column(String, nullable=True) pub_date = Column(String, nullable=True) # ISO format or YYYY-MM-DD duration_secs = Column(Integer, default=0) description = Column(Text, nullable=True) is_broken = Column(Boolean, default=False) class ErrorEvent(Base): __tablename__ = "error_events" id = Column(Integer, primary_key=True, index=True) station = Column(String, index=True, nullable=False) source = Column(String, nullable=False) message = Column(Text, nullable=False) severity = Column(String, default="error", nullable=False) fingerprint = Column(String, index=True, nullable=False) occurrences = Column(Integer, default=1, nullable=False) first_seen = Column(DateTime, nullable=False) last_seen = Column(DateTime, nullable=False) acknowledged = Column(Boolean, default=False, nullable=False) acknowledged_at = Column(DateTime, nullable=True) snapshot_path = Column(String, nullable=True) def init_db(): Base.metadata.create_all(bind=engine) import sqlite3, os db_path = DATABASE_URL.replace("sqlite:///", "") if os.path.exists(db_path): with sqlite3.connect(db_path) as conn: cursor = conn.cursor() existing_columns = { row[1] for row in cursor.execute("PRAGMA table_info(programs)") } for col, typedef in [ ("next_catchup_after", "REAL DEFAULT 0.0"), ("total_pages", "INTEGER DEFAULT 0"), ]: if col not in existing_columns: cursor.execute(f"ALTER TABLE programs ADD COLUMN {col} {typedef}") error_columns = {row[1] for row in cursor.execute("PRAGMA table_info(error_events)")} if "snapshot_path" not in error_columns: cursor.execute("ALTER TABLE error_events ADD COLUMN snapshot_path TEXT") # Encrypt legacy plaintext values using the raw SQLite connection; # assigning the same decrypted value through the ORM is not dirty. rows = cursor.execute("SELECT key, value FROM config").fetchall() for key, value in rows: if value is not None and not value.startswith("enc:v1:"): cursor.execute("UPDATE config SET value = ? WHERE key = ?", (encrypt_config_value(value), key)) def get_db(): db = SessionLocal() try: yield db finally: db.close()