Dateigröße pro Aufnahme entkoppelt - feste Größe nach Abschluss
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:
co-authored by
Claude Opus 4.6
parent
1ad87c7186
commit
851ff086ac
+119
-80
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user