Initial commit

Co-authored-by: GitHub Copilot <noreply@github.com>
This commit is contained in:
2026-09-03 11:42:09 +02:00
commit 76f5d30db1
19 changed files with 2357 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
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()