From 80ebd79451ab34a7646b84931d7ce90a48614888 Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Fri, 29 May 2026 19:37:50 +0200 Subject: [PATCH] feat: add dashcam-transfer.py script --- dashcam-transfer.py | 148 +++++++++++ ...6-05-29-dashcam-transfer-implementation.md | 241 ++++++++++++++++++ 2 files changed, 389 insertions(+) create mode 100644 dashcam-transfer.py create mode 100644 docs/superpowers/plans/2026-05-29-dashcam-transfer-implementation.md diff --git a/dashcam-transfer.py b/dashcam-transfer.py new file mode 100644 index 0000000..ceb54dd --- /dev/null +++ b/dashcam-transfer.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Transfer dashcam MP4 files from SD card to USB-SSD with date/camera structure.""" + +import fcntl +import logging +import os +import re +import shutil +import sys + +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 = "/var/log/dashcam-transfer.log" +LOCK_FILE = "/var/run/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)) + + +def main(): + 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 + + 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) + try: + shutil.move(src, dst) + logging.info( + "Verplaatst: %s → %s/%s/", fname, date_str, CAMERA_MAP[cam] + ) + ok += 1 + except OSError as e: + logging.error("Fout bij verplaatsen %s: %s", fname, e) + errors += 1 + + logging.info( + "Transfer voltooid: %d ok, %d fouten, %d bytes", ok, errors, total_size + ) + + +if __name__ == "__main__": + main() diff --git a/docs/superpowers/plans/2026-05-29-dashcam-transfer-implementation.md b/docs/superpowers/plans/2026-05-29-dashcam-transfer-implementation.md new file mode 100644 index 0000000..9f69566 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-dashcam-transfer-implementation.md @@ -0,0 +1,241 @@ +# Dashcam Transfer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Automatic transfer of dashcam MP4 files from SD card to USB-SSD. + +**Architecture:** Single Python script (`dashcam-transfer.py`) using stdlib only, triggered by udev rule via systemd oneshot service. + +**Tech Stack:** Python 3 (stdlib), systemd, udev + +--- + +### Task 1: Write dashcam-transfer.py + +**Files:** +- Create: `dashcam-transfer.py` (local repo) +- Deploy to: `/usr/local/bin/dashcam-transfer` on nextcloud + +The script must: + +1. Parse all `.MP4` files from `/mnt/sdcard/Novatek/Movie/` +2. Extract date (first 8 chars of filename) → `YYYYMMDD/` dir +3. Extract camera suffix (last char before `.MP4`): + - `F` → `front/` + - `B` → `back/` +4. Create target dirs: `/mnt/ultratouch/dashcam/YYYYMMDD/{front,back}/` +5. Calculate total space needed, check available space +6. If insufficient: sort date dirs on ultratouch by name, delete oldest dirs until enough space +7. `mv` each file (not copy) +8. Log every action to `/var/log/dashcam-transfer.log` +9. Use `flock` lockfile to prevent concurrent runs +10. Error handling: skip unreadable files, abort if SD not mounted + +- [ ] **Write dashcam-transfer.py** + +```python +#!/usr/bin/env python3 +"""Transfer dashcam MP4 files from SD card to USB-SSD with date/camera structure.""" + +import fcntl +import logging +import os +import re +import shutil +import sys + +# Config +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 = "/var/log/dashcam-transfer.log" +LOCK_FILE = "/var/run/dashcam-transfer.lock" + +CAMERA_MAP = {"F": "front", "B": "back"} +FILE_PATTERN = re.compile(r"^(\d{8})\d{6}_\d+([FB])\.MP4$") + +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): + logging.info("Geen bronmap: %s", src_dir) + return [] + files = [] + for fname in os.listdir(src_dir): + match = FILE_PATTERN.match(fname) + if match: + files.append((fname, match.group(1), match.group(2).upper())) + return files + + +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.info("Onvoldoende ruimte: %d nodig, %d 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 d in date_dirs: + if free + freed >= needed_bytes: + break + dir_path = os.path.join(target_base, d) + 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(d) + logging.info("Verwijderd (ruimte gebrek): %s (%d bytes)", d, dir_size) + + if free + freed < needed_bytes: + logging.error( + "Nog steeds onvoldoende ruimte na verwijderen %d mappen", len(removed) + ) + sys.exit(1) + + logging.info("Ruimte vrijgemaakt: %d bytes door %d mappen", freed, len(removed)) + + +def main(): + 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 + + 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) + try: + shutil.move(src, dst) + logging.info("Verplaatst: %s → %s/%s/", fname, date_str, CAMERA_MAP[cam]) + ok += 1 + except OSError as e: + logging.error("Fout bij %s: %s", fname, e) + errors += 1 + + logging.info( + "Transfer voltooid: %d ok, %d fouten, %d bytes", ok, errors, total_size + ) + + +if __name__ == "__main__": + main() +``` + +- [ ] **Commit to repo** + +```bash +git add dashcam-transfer.py +git commit -m "feat: dashcam-transfer.py script" +git push +``` + +--- + +### Task 2: Deploy and test on nextcloud + +**Files:** +- Modify: `/usr/local/bin/dashcam-transfer` on nextcloud + +- [ ] **Copy script to nextcloud** + +```bash +scp dashcam-transfer.py aiaiaiaiagent@nextcloud:/tmp/ +ssh aiaiaiaiagent@nextcloud 'sudo -A install -m 755 /tmp/dashcam-transfer.py /usr/local/bin/dashcam-transfer' +``` + +- [ ] **Dry-run test (check detection, no transfer yet)** + +```bash +ssh aiaiaiaiagent@nextcloud '/usr/local/bin/dashcam-transfer' +``` + +Expected: Script should find 338 MP4 files, log the start, and proceed with transfer. + +--- + +### Task 3: Add udev rule + systemd service (if needed) + +**Files:** +- Create: `/etc/udev/rules.d/99-dashcam-sdcard.rules` on nextcloud +- Create: `/etc/systemd/system/dashcam-transfer.service` on nextcloud +- Create: `dashcam-transfer-trigger` script on nextcloud + +- [ ] **Create trigger script** + +```bash +ssh aiaiaiaiagent@nextcloud 'sudo tee /usr/local/bin/dashcam-transfer-trigger << "TRIGGER" +#!/bin/bash +sleep 5 +/usr/local/bin/dashcam-transfer +TRIGGER' +ssh aiaiaiaiagent@nextcloud 'sudo -A chmod +x /usr/local/bin/dashcam-transfer-trigger' +``` + +- [ ] **Create systemd service** + +```ini +[Unit] +Description=Dashcam SD card transfer +After=mnt-sdcard.mount + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/dashcam-transfer +``` + +- [ ] **Create udev rule** + +```udev +ACTION=="add", KERNEL=="sdd1", RUN+="/usr/local/bin/dashcam-transfer-trigger" +```