"""Konvertierungs-Job-Modell mit Status-Management""" import time import asyncio from dataclasses import dataclass, field from enum import IntEnum from typing import Optional from app.models.media import MediaFile # Globaler Zaehler fuer eindeutige IDs _id_counter = 0 class JobStatus(IntEnum): QUEUED = 0 ACTIVE = 1 FINISHED = 2 FAILED = 3 CANCELLED = 4 @dataclass class ConversionJob: """Einzelner Konvertierungs-Auftrag""" media: MediaFile preset_name: str = "" # Wird in __post_init__ gesetzt id: int = field(init=False) status: JobStatus = field(default=JobStatus.QUEUED) # Ziel-Informationen target_path: str = "" target_filename: str = "" target_container: str = "webm" # Optionen delete_source: bool = False # Quelldatei nach Konvertierung loeschen # ffmpeg Prozess ffmpeg_cmd: list[str] = field(default_factory=list) process: Optional[asyncio.subprocess.Process] = field(default=None, repr=False) task: Optional[asyncio.Task] = field(default=None, repr=False) # Fortschritt progress_percent: float = 0.0 progress_fps: float = 0.0 progress_speed: float = 0.0 progress_bitrate: int = 0 progress_size_bytes: int = 0 progress_time_sec: float = 0.0 progress_frames: int = 0 progress_eta_sec: float = 0.0 # Zeitstempel created_at: float = field(default_factory=time.time) started_at: Optional[float] = None finished_at: Optional[float] = None # Statistik-Akkumulation (Summe, Anzahl) _stat_fps: list = field(default_factory=lambda: [0.0, 0]) _stat_speed: list = field(default_factory=lambda: [0.0, 0]) _stat_bitrate: list = field(default_factory=lambda: [0, 0]) def __post_init__(self) -> None: global _id_counter self.id = int(time.time() * 1000) * 1000 + _id_counter _id_counter += 1 def build_target_path(self, config) -> None: """Baut Ziel-Pfad basierend auf Config""" files_cfg = config.files_config container = files_cfg.get("target_container", "webm") self.target_container = container # Dateiname ohne Extension + neue Extension base_name = self.media.source_filename.rsplit(".", 1)[0] self.target_filename = f"{base_name}.{container}" # Ziel-Ordner target_folder = files_cfg.get("target_folder", "same") if target_folder == "same": target_dir = self.media.source_dir else: target_dir = target_folder self.target_path = f"{target_dir}/{self.target_filename}" # Konfliktvermeidung: Wenn Ziel = Quelle (gleiche Extension) if self.target_path == self.media.source_path: base = self.target_path.rsplit(".", 1)[0] self.target_path = f"{base}_converted.{container}" self.target_filename = f"{base_name}_converted.{container}" def update_stats(self, fps: float, speed: float, bitrate: int) -> None: """Akkumuliert Statistik-Werte fuer Durchschnittsberechnung""" if fps > 0: self._stat_fps[0] += fps self._stat_fps[1] += 1 if speed > 0: self._stat_speed[0] += speed self._stat_speed[1] += 1 if bitrate > 0: self._stat_bitrate[0] += bitrate self._stat_bitrate[1] += 1 @property def avg_fps(self) -> float: if self._stat_fps[1] > 0: return self._stat_fps[0] / self._stat_fps[1] return 0.0 @property def avg_speed(self) -> float: if self._stat_speed[1] > 0: return self._stat_speed[0] / self._stat_speed[1] return 0.0 @property def avg_bitrate(self) -> int: if self._stat_bitrate[1] > 0: return int(self._stat_bitrate[0] / self._stat_bitrate[1]) return 0 @property def duration_sec(self) -> float: """Konvertierungsdauer in Sekunden""" if self.started_at and self.finished_at: return self.finished_at - self.started_at return 0.0 def to_dict_active(self) -> dict: """Fuer WebSocket data_convert Nachricht""" size = MediaFile.format_size(self.media.source_size_bytes) return { "source_file_name": self.media.source_filename, "source_file": self.media.source_path, "source_path": self.media.source_dir, "source_duration": self.media.source_duration_sec, "source_size": [size[0], size[1]], "source_frame_rate": self.media.frame_rate, "source_frames_total": self.media.total_frames, "target_file_name": self.target_filename, "target_file": self.target_path, "status": self.status.value, "preset": self.preset_name, } def to_dict_queue(self) -> dict: """Fuer WebSocket data_queue Nachricht""" return { "source_file_name": self.media.source_filename, "source_file": self.media.source_path, "source_path": self.media.source_dir, "status": self.status.value, "preset": self.preset_name, } def to_dict_progress(self) -> dict: """Fuer WebSocket data_flow Nachricht""" target_size = MediaFile.format_size(self.progress_size_bytes) return { "id": self.id, "frames": self.progress_frames, "fps": self.progress_fps, "speed": self.progress_speed, "quantizer": 0, "size": [target_size[0], target_size[1]], "time": MediaFile.format_time(self.progress_time_sec), "time_remaining": MediaFile.format_time(self.progress_eta_sec), "loading": round(self.progress_percent, 1), "bitrate": [self.progress_bitrate, "kbits/s"], } def to_dict_stats(self) -> dict: """Fuer Statistik-Datenbank""" return { "source_path": self.media.source_path, "source_filename": self.media.source_filename, "source_size_bytes": self.media.source_size_bytes, "source_duration_sec": self.media.source_duration_sec, "source_frame_rate": self.media.frame_rate, "source_frames_total": self.media.total_frames, "target_path": self.target_path, "target_filename": self.target_filename, "target_size_bytes": self.progress_size_bytes, "target_container": self.target_container, "preset_name": self.preset_name, "status": self.status.value, "started_at": self.started_at, "finished_at": self.finished_at, "duration_sec": self.duration_sec, "avg_fps": self.avg_fps, "avg_speed": self.avg_speed, "avg_bitrate": self.avg_bitrate, } def to_json(self) -> dict: """Fuer Queue-Persistierung""" return { "source_path": self.media.source_path, "preset_name": self.preset_name, "status": self.status.value, "created_at": self.created_at, "delete_source": self.delete_source, }