Files
buzzer/buzzer_tool/core/commands/put.py
2026-03-02 00:25:40 +01:00

220 lines
6.9 KiB
Python

import glob
import os
import sys
import time
from core.connection import BuzzerError
TAG_MAGIC = b"TAG!"
TAG_FOOTER_LEN = 7
TAG_VERSION_V1 = 0x01
TAG_TYPE_CRC32 = 0x10
def _split_audio_and_tag_blob(filepath: str) -> tuple[bytes, bytes | None]:
with open(filepath, "rb") as f:
data = f.read()
if len(data) < TAG_FOOTER_LEN:
return data, None
if data[-4:] != TAG_MAGIC:
return data, None
tag_total_len = int.from_bytes(data[-6:-4], byteorder="little", signed=False)
tag_version = data[-7]
if tag_version != TAG_VERSION_V1:
return data, None
if tag_total_len < TAG_FOOTER_LEN or tag_total_len > len(data):
return data, None
audio_end = len(data) - tag_total_len
tag_payload_len = tag_total_len - TAG_FOOTER_LEN
tag_payload = data[audio_end:audio_end + tag_payload_len]
audio_data = data[:audio_end]
return audio_data, tag_payload
def _upsert_crc32_tag(tag_blob: bytes | None, crc32: int) -> tuple[bytes, bool]:
crc_payload = int(crc32).to_bytes(4, byteorder="little", signed=False)
crc_tlv = bytes([TAG_TYPE_CRC32, 0x04, 0x00]) + crc_payload
if not tag_blob:
return crc_tlv, True
pos = 0
out = bytearray()
found_crc = False
while pos < len(tag_blob):
if pos + 3 > len(tag_blob):
return crc_tlv, True
tag_type = tag_blob[pos]
tag_len = tag_blob[pos + 1] | (tag_blob[pos + 2] << 8)
header = tag_blob[pos:pos + 3]
pos += 3
if pos + tag_len > len(tag_blob):
return crc_tlv, True
value = tag_blob[pos:pos + tag_len]
pos += tag_len
if tag_type == TAG_TYPE_CRC32:
if not found_crc:
out.extend(crc_tlv)
found_crc = True
continue
out.extend(header)
out.extend(value)
if not found_crc:
out.extend(crc_tlv)
return bytes(out), True
def _collect_source_files(sources: list[str], recursive: bool) -> list[dict]:
entries = []
for source in sources:
matches = glob.glob(source)
if not matches:
print(f"⚠️ Keine Treffer für Quelle: {source}")
continue
for match in matches:
if os.path.isfile(match):
entries.append({"local": match, "relative": os.path.basename(match)})
continue
if os.path.isdir(match):
if recursive:
for root, _, files in os.walk(match):
for name in sorted(files):
local_path = os.path.join(root, name)
rel = os.path.relpath(local_path, match)
entries.append({"local": local_path, "relative": rel.replace("\\", "/")})
else:
for name in sorted(os.listdir(match)):
local_path = os.path.join(match, name)
if os.path.isfile(local_path):
entries.append({"local": local_path, "relative": name})
return entries
def _remote_parent(path: str) -> str:
idx = path.rfind("/")
if idx <= 0:
return "/"
return path[:idx]
def _ensure_remote_dir(conn, remote_dir: str) -> None:
if not remote_dir or remote_dir == "/":
return
current = ""
for part in [p for p in remote_dir.split("/") if p]:
current = f"{current}/{part}"
try:
conn.mkdir(current)
except BuzzerError as e:
msg = str(e)
if "0x11" in msg or "existiert bereits" in msg:
continue
raise
def _build_upload_plan(entries: list[dict], target: str) -> list[dict]:
if not entries:
return []
needs_dir_semantics = target.endswith("/") or len(entries) > 1 or any("/" in e["relative"] for e in entries)
if not needs_dir_semantics:
return [{"local": entries[0]["local"], "remote": target}]
base = target.rstrip("/")
if not base:
base = "/"
plan = []
for entry in entries:
rel = entry["relative"].lstrip("/")
if base == "/":
remote = f"/{rel}"
else:
remote = f"{base}/{rel}"
plan.append({"local": entry["local"], "remote": remote})
return plan
def execute(conn, sources: list[str], target: str, recursive: bool = False):
uploads = _build_upload_plan(_collect_source_files(sources, recursive=recursive), target)
if not uploads:
print("Keine gültigen Dateien gefunden.")
return
total_size_all = sum(os.path.getsize(item["local"]) for item in uploads)
sent_all = 0
start_time_all = time.monotonic()
last_ui_update = start_time_all
for item in uploads:
local_path = item["local"]
remote_path = item["remote"]
filename = os.path.basename(local_path)
audio_data, tag_blob = _split_audio_and_tag_blob(local_path)
audio_size = len(audio_data)
_ensure_remote_dir(conn, _remote_parent(remote_path))
print(f"Sende 📄 {filename} ({audio_size / 1024:.1f} KB Audio) -> {remote_path}")
start_time_file = time.monotonic()
def progress_handler(chunk_len, sent_file, total_file):
nonlocal sent_all, last_ui_update
sent_all += chunk_len
now = time.monotonic()
if now - last_ui_update < 0.2 and sent_file < total_file:
return
last_ui_update = now
elapsed = now - start_time_file
speed = (sent_file / 1024.0) / elapsed if elapsed > 0 else 0.0
perc_file = (sent_file / total_file) * 100.0 if total_file > 0 else 100.0
perc_all = (sent_all / total_size_all) * 100.0 if total_size_all > 0 else 100.0
elapsed_all = now - start_time_all
avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0.0
eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0.0
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
sys.stdout.write(
f"\r \033[90mProg: {perc_file:3.0f}% | Gesamt: {perc_all:3.0f}% | "
f"{speed:6.1f} KB/s | ETA: {eta_str}\033[0m"
)
sys.stdout.flush()
try:
audio_crc32 = conn.put_file_data(remote_path, audio_data, progress_callback=progress_handler)
rewritten_blob, _ = _upsert_crc32_tag(tag_blob, audio_crc32)
conn.set_tag_blob(remote_path, rewritten_blob)
tag_note = " (CRC32-Tag gesetzt)"
print(f"\r \033[32mFertig: {filename} übertragen{tag_note}.{' ' * 20}\033[0m")
except Exception as e:
print(f"\n\033[31mFehler: {e}\033[0m")
total_duration = time.monotonic() - start_time_all
total_kb = total_size_all / 1024.0
avg_speed = total_kb / total_duration if total_duration > 0 else 0.0
print(f"\nÜbertragung abgeschlossen: {total_kb:.1f} KB in {total_duration:.1f}s ({avg_speed:.1f} KB/s)")