dashcam-transfer/dashcam-transfer.py
Eddy de Vink c7e046f4ab fix: corrigeer WHATSAPP_API URL naar localhost:3001 (nextcloud)
- WHATSAPP_API gebruikte 100.97.199.84:3000 (Mac Mini Tailscale IP, verkeerde poort)
- Gewijzigd naar localhost:3001 (wwebjs-api container op nextcloud zelf)
- Comment toegevoegd om verwarring te voorkomen
- README herschreven met WhatsApp notificatie sectie en correcte architectuur
- Verouderd plan geüpdatet met juiste URL
2026-06-23 11:04:39 +02:00

221 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Transfer dashcam MP4 files from SD card to USB-SSD with date/camera structure."""
import argparse
import fcntl
import json
import logging
import os
import re
import shutil
import subprocess
import sys
import urllib.request
SD_CARD = "/mnt/sdcard"
SOURCE_DIR = os.path.join(SD_CARD, "Novatek", "Movie")
SSD = "/mnt/ultratouch"
TARGET_BASE = os.path.join(SSD, "dashcam")
LOG_FILE = "/tmp/dashcam-transfer.log"
LOCK_FILE = "/tmp/dashcam-transfer.lock"
CAMERA_MAP = {"F": "front", "B": "back"}
FILE_PATTERN = re.compile(r"^(\d{8})\d{6}_\d+([FB])\.MP4$")
def setup_logging():
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="[%(asctime)s] %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def acquire_lock():
lock_fd = open(LOCK_FILE, "w")
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
logging.warning("Lockfile actief — script al bezig")
sys.exit(1)
return lock_fd
def check_mount(path, name):
if not os.path.ismount(path):
logging.error("%s niet gemount: %s", name, path)
sys.exit(1)
def get_mp4_files(src_dir):
if not os.path.isdir(src_dir):
return []
result = []
for fname in os.listdir(src_dir):
match = FILE_PATTERN.match(fname)
if match:
result.append((fname, match.group(1), match.group(2).upper()))
return result
def ensure_free_space(needed_bytes, target_base):
stat = os.statvfs(target_base)
free = stat.f_frsize * stat.f_bavail
if free >= needed_bytes:
return
logging.warning(
"Onvoldoende ruimte: %d bytes nodig, %d bytes vrij", needed_bytes, free
)
date_dirs = sorted(
d
for d in os.listdir(target_base)
if re.match(r"^\d{8}$", d)
and os.path.isdir(os.path.join(target_base, d))
)
freed = 0
removed = []
for date_name in date_dirs:
if free + freed >= needed_bytes:
break
dir_path = os.path.join(target_base, date_name)
dir_size = sum(
os.path.getsize(os.path.join(dirpath, f))
for dirpath, _, filenames in os.walk(dir_path)
for f in filenames
)
shutil.rmtree(dir_path)
freed += dir_size
removed.append(date_name)
logging.info("Ruimte gemaakt: %s verwijderd (%d bytes)", date_name, dir_size)
if free + freed < needed_bytes:
logging.error(
"Nog steeds onvoldoende ruimte na verwijderen %d mappen: "
"%d bytes nodig, %d bytes vrij",
len(removed),
needed_bytes,
free + freed,
)
sys.exit(1)
logging.info("Ruimte vrijgemaakt: %d bytes door %d mappen", freed, len(removed))
WHATSAPP_API = "http://localhost:3001/client/sendMessage/EdGpt" # localhost = nextcloud server, niet Mac Mini!
WHATSAPP_CHAT = "31620248011@c.us"
def send_whatsapp_notification(ok, errors, total_bytes):
if errors == 0:
prefix = "✅"
status = "geen fouten"
else:
prefix = "❌"
status = f"{errors} fout(en)"
gb = total_bytes / (1024 ** 3)
body = f"{prefix} Dashcam transfer voltooid: {ok} bestanden ({gb:.1f} GB) overgezet, {status}"
payload = {"chatId": WHATSAPP_CHAT, "content": body, "contentType": "string"}
try:
data = json.dumps(payload).encode()
req = urllib.request.Request(
WHATSAPP_API, data=data, headers={"Content-Type": "application/json"}
)
urllib.request.urlopen(req, timeout=10)
logging.info("WhatsApp notificatie verstuurd")
except Exception as e:
logging.warning("WhatsApp notificatie mislukt: %s", e)
def parse_args():
parser = argparse.ArgumentParser(description="Dashcam transfer tool")
parser.add_argument(
"--limit", "-n", type=int, default=0,
help="Maximum aantal bestanden om over te zetten (0 = alle)"
)
return parser.parse_args()
def main():
args = parse_args()
setup_logging()
lock_fd = acquire_lock()
check_mount(SD_CARD, "SD-kaart")
check_mount(SSD, "Ultratouch SSD")
files = get_mp4_files(SOURCE_DIR)
if not files:
logging.info("Geen MP4 bestanden gevonden in %s", SOURCE_DIR)
return
if args.limit > 0:
files = files[:args.limit]
total_size = sum(
os.path.getsize(os.path.join(SOURCE_DIR, fname)) for fname, _, _ in files
)
logging.info(
"Start transfer: %d bestanden, %d bytes", len(files), total_size
)
ensure_free_space(total_size, TARGET_BASE)
ok = 0
errors = 0
for fname, date_str, cam in files:
target_dir = os.path.join(TARGET_BASE, date_str, CAMERA_MAP[cam])
os.makedirs(target_dir, exist_ok=True)
src = os.path.join(SOURCE_DIR, fname)
dst = os.path.join(target_dir, fname)
if os.path.exists(dst):
src_size = os.path.getsize(src)
dst_size = os.path.getsize(dst)
if src_size == dst_size:
logging.info(
"Bestaat al en klopt: %s → %s/%s/", fname, date_str, CAMERA_MAP[cam]
)
os.remove(src)
ok += 1
continue
else:
logging.warning(
"Doel %s bestaat maar is anders (%d vs %d bytes) — overschrijf",
dst, dst_size, src_size,
)
os.remove(dst)
try:
subprocess.run(
["cp", src, dst], check=True, capture_output=True, timeout=600
)
except (subprocess.CalledProcessError, OSError) as e:
logging.error("Fout bij kopiëren %s: %s", fname, e)
errors += 1
continue
dst_size = os.path.getsize(dst)
src_size = os.path.getsize(src)
if dst_size != src_size:
logging.error(
"Verificatie mislukt %s: %d vs %d bytes", fname, src_size, dst_size
)
errors += 1
continue
os.remove(src)
logging.info(
"Overgezet: %s → %s/%s/ (%d bytes)", fname, date_str, CAMERA_MAP[cam], src_size
)
ok += 1
logging.info("Transfer voltooid: %d ok, %d fouten", ok, errors)
send_whatsapp_notification(ok, errors, total_size)
if __name__ == "__main__":
main()