76f5d30db1
Co-authored-by: GitHub Copilot <noreply@github.com>
111 lines
4.3 KiB
Python
111 lines
4.3 KiB
Python
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()
|