220 lines
6.9 KiB
Python
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)") |