import datetime import logging from sqlalchemy import create_engine, event, Column, Integer, String, Boolean, DateTime, Text, text from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() logger = logging.getLogger("models") def utcnow() -> datetime.datetime: return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) def iso_utc(dt: datetime.datetime | None) -> str | None: if dt is None: return None if dt.tzinfo is None: dt = dt.replace(tzinfo=datetime.timezone.utc) return dt.astimezone(datetime.timezone.utc).isoformat() class Job(Base): __tablename__ = "jobs" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(200), nullable=False) stream_url = Column(String(1000), nullable=False) stream_type = Column(String(20), default="auto") output_format = Column(String(10), default="") max_duration = Column(Integer, nullable=True) extra_ffmpeg_args = Column(String(500), default="") segment_duration = Column(Integer, nullable=True) schedule_enabled = Column(Boolean, default=False) schedule_once = Column(Boolean, default=False) schedule_date = Column(String(10), default="") schedule_days = Column(String(50), default="*") schedule_start = Column(String(5), default="") schedule_stop = Column(String(5), default="") schedule_end_date = Column(String(10), default="") pre_buffer = Column(Integer, default=0) post_buffer = Column(Integer, default=0) description = Column(Text, default="") plik_enabled = Column(Boolean, default=False) delete_after_upload = Column(Boolean, default=False) ntfy_enabled = Column(Boolean, default=False) station_id = Column(Integer, nullable=True) created_at = Column(DateTime, default=utcnow) updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class Station(Base): __tablename__ = "stations" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(200), nullable=False) stream_url = Column(String(1000), nullable=False) stream_type = Column(String(20), default="auto") description = Column(Text, default="") created_at = Column(DateTime, default=utcnow) updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class Recording(Base): __tablename__ = "recordings" id = Column(Integer, primary_key=True, autoincrement=True) job_id = Column(Integer, nullable=True) job_name = Column(String(200), nullable=False) stream_url = Column(String(1000), nullable=False) stream_type = Column(String(20), default="") output_dir = Column(String(500), default="") output_file = Column(String(500), default="") pid = Column(Integer, nullable=True) status = Column(String(20), default="running") # running, stopped, completed, error started_at = Column(DateTime, default=utcnow) stopped_at = Column(DateTime, nullable=True) scheduled_stop = Column(DateTime, nullable=True) segment_mode = Column(Boolean, default=False) is_scheduled = Column(Boolean, default=False) error_message = Column(Text, nullable=True) file_size = Column(Integer, nullable=True) plik_url = Column(String(1000), nullable=True) class LogEntry(Base): __tablename__ = "logs" id = Column(Integer, primary_key=True, autoincrement=True) timestamp = Column(DateTime, default=utcnow) level = Column(String(10), default="INFO") message = Column(Text, nullable=False) job_name = Column(String(200), nullable=True) _engine = None _SessionLocal = None def init_db(db_path: str): global _engine, _SessionLocal _engine = create_engine( f"sqlite:///{db_path}", connect_args={"check_same_thread": False, "timeout": 30}, ) @event.listens_for(_engine, "connect") def _set_sqlite_pragmas(dbapi_conn, _record): cur = dbapi_conn.cursor() try: cur.execute("PRAGMA journal_mode=WAL") cur.execute("PRAGMA busy_timeout=5000") finally: cur.close() Base.metadata.create_all(_engine) _SessionLocal = sessionmaker(bind=_engine) _migrate(_engine) def _migrate(engine): with engine.connect() as conn: for stmt in [ "ALTER TABLE recordings ADD COLUMN file_size INTEGER", "ALTER TABLE jobs ADD COLUMN station_id INTEGER", "ALTER TABLE jobs ADD COLUMN pre_buffer INTEGER DEFAULT 0", "ALTER TABLE jobs ADD COLUMN post_buffer INTEGER DEFAULT 0", "ALTER TABLE recordings ADD COLUMN plik_url VARCHAR(1000)", ]: try: conn.execute(text(stmt)) conn.commit() except Exception: pass def get_session(): return _SessionLocal() def prune_logs(days: int) -> int: if days <= 0: return 0 cutoff = utcnow() - datetime.timedelta(days=days) session = get_session() try: count = session.query(LogEntry).filter(LogEntry.timestamp < cutoff).delete(synchronize_session=False) session.commit() if count: logger.info("%d alte Log-Einträge gelöscht (älter als %d Tage)", count, days) return count except Exception as e: session.rollback() logger.error("Log-Bereinigung fehlgeschlagen: %s", e) return 0 finally: session.close()