Dateigröße pro Aufnahme entkoppelt - feste Größe nach Abschluss
Build Dev Docker Image / build-amd64 (push) Successful in 10s
Build Dev Docker Image / build-arm64 (push) Successful in 16s
Build Dev Docker Image / publish-dev-manifest (push) Successful in 9s

Die Dateigröße wird jetzt pro Aufnahme korrekt berechnet statt das
gesamte Verzeichnis zu summieren. Bei abgeschlossenen Aufnahmen wird
die Größe einmalig gespeichert und nicht mehr durch laufende Aufnahmen
verändert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Asmus
2026-09-16 11:46:18 +02:00
co-authored by Claude Opus 4.6
parent 1ad87c7186
commit 851ff086ac
6 changed files with 600 additions and 124 deletions
+119 -80
View File
@@ -1,5 +1,6 @@
import asyncio
import datetime
import glob as globmod
import os
import re
import signal
@@ -68,6 +69,7 @@ class RecordingManager:
self._ntfy = None
self._plik = None
self._rec_options: dict[int, dict] = {}
self._output_files: dict[int, list[str]] = {}
def _get_metadata_monitor(self):
if self._metadata_monitor is None:
@@ -173,6 +175,15 @@ class RecordingManager:
finally:
session.close()
self._rec_options[rec_id] = {
"ntfy_enabled": ntfy_enabled,
"plik_enabled": plik_enabled,
"delete_after_upload": delete_after_upload,
"segment_mode": segment_mode,
"description": description,
}
self._output_files[rec_id] = [output_file]
stop_event = threading.Event()
self._stop_events[rec_id] = stop_event
@@ -185,14 +196,6 @@ class RecordingManager:
self._threads[rec_id] = thread
thread.start()
self._rec_options[rec_id] = {
"ntfy_enabled": ntfy_enabled,
"plik_enabled": plik_enabled,
"delete_after_upload": delete_after_upload,
"segment_mode": segment_mode,
"description": description,
}
self._log("INFO", f"Aufnahme gestartet: {name}", name)
self._notify("start", "Aufnahme gestartet", name, rec_id)
@@ -225,74 +228,90 @@ class RecordingManager:
retry_delay = self.config.get("recording", "retry_delay", default=5)
retry = 0
while not stop_event.is_set():
cmd = self._build_ffmpeg_cmd(
stream_url, stream_type, output_file,
extra_ffmpeg_args, segment_mode, segment_duration,
)
try:
log_path = os.path.join(output_dir, f".ffmpeg_{rec_id}.log")
log_fh = open(log_path, "a")
process = subprocess.Popen(
cmd,
stdout=log_fh,
stderr=log_fh,
stdin=subprocess.DEVNULL,
try:
while not stop_event.is_set():
cmd = self._build_ffmpeg_cmd(
stream_url, stream_type, output_file,
extra_ffmpeg_args, segment_mode, segment_duration,
)
except Exception as e:
self._update_recording(rec_id, status="error", error_message=str(e))
self._log("ERROR", f"ffmpeg konnte nicht gestartet werden: {e}", job_name)
self._notify("error", "Aufnahme fehlgeschlagen", f"{job_name}: {e}", rec_id, priority="high")
return
self._processes[rec_id] = process
self._update_recording(rec_id, status="running", pid=process.pid)
try:
log_path = os.path.join(output_dir, f".ffmpeg_{rec_id}.log")
log_fh = open(log_path, "a")
process = subprocess.Popen(
cmd,
stdout=log_fh,
stderr=log_fh,
stdin=subprocess.DEVNULL,
)
except Exception as e:
self._update_recording(rec_id, status="error", error_message=str(e),
file_size=self._finalize_file_size(rec_id, segment_mode))
self._log("ERROR", f"ffmpeg konnte nicht gestartet werden: {e}", job_name)
self._notify("error", "Aufnahme fehlgeschlagen", f"{job_name}: {e}", rec_id, priority="high")
return
process.wait()
retcode = process.returncode
log_fh.close()
self._processes.pop(rec_id, None)
self._processes[rec_id] = process
self._update_recording(rec_id, status="running", pid=process.pid)
if stop_event.is_set() or retcode == 0:
status = "completed" if retcode == 0 else "stopped"
status_de = "abgeschlossen" if status == "completed" else "gestoppt"
self._update_recording(rec_id, status=status)
self._log("INFO", f"Aufnahme {status_de}: {job_name}", job_name)
self._notify("stop", f"Aufnahme {status_de}", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
return
process.wait()
retcode = process.returncode
log_fh.close()
self._processes.pop(rec_id, None)
if retcode in (-2, 130, 143, 255):
self._update_recording(rec_id, status="stopped")
self._log("INFO", f"Aufnahme gestoppt: {job_name}", job_name)
self._notify("stop", "Aufnahme gestoppt", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
return
if stop_event.is_set() or retcode == 0:
status = "completed" if retcode == 0 else "stopped"
status_de = "abgeschlossen" if status == "completed" else "gestoppt"
self._update_recording(rec_id, status=status,
file_size=self._finalize_file_size(rec_id, segment_mode))
self._log("INFO", f"Aufnahme {status_de}: {job_name}", job_name)
self._notify("stop", f"Aufnahme {status_de}", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
return
retry += 1
if max_retries > 0 and retry >= max_retries:
self._update_recording(rec_id, status="error",
error_message=f"Max retries ({max_retries}) reached, exit code {retcode}")
self._log("ERROR", f"Max. Versuche ({max_retries}) erreicht: {job_name}", job_name)
self._notify("error", "Aufnahme fehlgeschlagen", f"{job_name}: Max. Versuche erreicht", rec_id, priority="high")
return
if retcode in (-2, 130, 143, 255):
self._update_recording(rec_id, status="stopped",
file_size=self._finalize_file_size(rec_id, segment_mode))
self._log("INFO", f"Aufnahme gestoppt: {job_name}", job_name)
self._notify("stop", "Aufnahme gestoppt", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
return
self._log("WARN", f"Verbindung verloren (Code: {retcode}), Reconnect in {retry_delay}s... [{job_name}]", job_name)
self._notify("error", "Stream abgerissen", f"{job_name}: Reconnect in {retry_delay}s...", rec_id)
if stop_event.wait(timeout=retry_delay):
self._update_recording(rec_id, status="stopped")
return
retry += 1
if max_retries > 0 and retry >= max_retries:
self._update_recording(rec_id, status="error",
error_message=f"Max retries ({max_retries}) reached, exit code {retcode}",
file_size=self._finalize_file_size(rec_id, segment_mode))
self._log("ERROR", f"Max. Versuche ({max_retries}) erreicht: {job_name}", job_name)
self._notify("error", "Aufnahme fehlgeschlagen", f"{job_name}: Max. Versuche erreicht", rec_id, priority="high")
return
sname = safe_name(job_name)
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
ext = Path(output_file).suffix.lstrip(".")
if segment_mode:
output_file = os.path.join(output_dir, f"{sname}_{ts}_seg%03d.{ext}")
else:
output_file = os.path.join(output_dir, f"{sname}_{ts}.{ext}")
self._log("WARN", f"Verbindung verloren (Code: {retcode}), Reconnect in {retry_delay}s... [{job_name}]", job_name)
self._notify("error", "Stream abgerissen", f"{job_name}: Reconnect in {retry_delay}s...", rec_id)
if stop_event.wait(timeout=retry_delay):
self._update_recording(rec_id, status="stopped",
file_size=self._finalize_file_size(rec_id, segment_mode))
self._log("INFO", f"Aufnahme gestoppt: {job_name}", job_name)
self._notify("stop", "Aufnahme gestoppt", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
return
self._update_recording(rec_id, status="stopped")
sname = safe_name(job_name)
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
ext = Path(output_file).suffix.lstrip(".")
if segment_mode:
output_file = os.path.join(output_dir, f"{sname}_{ts}_seg%03d.{ext}")
else:
output_file = os.path.join(output_dir, f"{sname}_{ts}.{ext}")
self._output_files.setdefault(rec_id, []).append(output_file)
self._update_recording(rec_id, status="stopped",
file_size=self._finalize_file_size(rec_id, segment_mode))
self._log("INFO", f"Aufnahme gestoppt: {job_name}", job_name)
self._notify("stop", "Aufnahme gestoppt", job_name, rec_id)
self._handle_post_recording(rec_id, output_dir, output_file, job_name, segment_mode)
finally:
self._cleanup(rec_id)
def _build_ffmpeg_cmd(self, url, stream_type, output_file, extra_args, segment_mode, segment_duration):
cmd = ["ffmpeg", "-nostdin", "-y", "-hide_banner", "-loglevel", "warning"]
@@ -330,9 +349,10 @@ class RecordingManager:
except (ProcessLookupError, OSError):
pass
self._update_recording(rec_id, status="stopped")
self._log("INFO", f"Aufnahme gestoppt (ID: {rec_id})")
self._cleanup(rec_id)
if not process and rec_id not in self._threads:
self._update_recording(rec_id, status="stopped")
self._log("INFO", f"Aufnahme gestoppt (ID: {rec_id})")
self._cleanup(rec_id)
return True
def extend_recording(self, rec_id: int, extra_seconds: int) -> bool:
@@ -433,16 +453,11 @@ class RecordingManager:
total_size = 0
segment_count = 0
if rec.output_dir and os.path.isdir(rec.output_dir):
for f in os.listdir(rec.output_dir):
if f.startswith("."):
continue
fp = os.path.join(rec.output_dir, f)
if os.path.isfile(fp):
total_size += os.path.getsize(fp)
segment_count += 1
elif rec.output_file and os.path.isfile(rec.output_file):
total_size = os.path.getsize(rec.output_file)
if rec.file_size is not None:
total_size = rec.file_size
else:
files = self._output_files.get(rec.id, [rec.output_file] if rec.output_file else [])
total_size, segment_count = self._calc_recording_size(files, rec.segment_mode)
remaining = None
if rec.scheduled_stop and rec.status == "running":
@@ -474,6 +489,29 @@ class RecordingManager:
"remaining_display": format_elapsed(remaining) if remaining is not None else None,
}
def _calc_recording_size(self, output_files: list[str], segment_mode: bool) -> tuple[int, int]:
total_size = 0
segment_count = 0
for of in output_files:
if segment_mode:
pattern = of.replace('%03d', '*')
for f in globmod.glob(pattern):
if os.path.isfile(f):
total_size += os.path.getsize(f)
segment_count += 1
else:
if os.path.isfile(of):
total_size += os.path.getsize(of)
segment_count += 1
return total_size, segment_count
def _finalize_file_size(self, rec_id: int, segment_mode: bool) -> int:
files = self._output_files.get(rec_id, [])
if not files:
return 0
total_size, _ = self._calc_recording_size(files, segment_mode)
return total_size
def _update_recording(self, rec_id: int, **kwargs):
session = get_session()
try:
@@ -505,3 +543,4 @@ class RecordingManager:
self._stop_events.pop(rec_id, None)
self._threads.pop(rec_id, None)
self._rec_options.pop(rec_id, None)
self._output_files.pop(rec_id, None)