Video-Aufräumsystem: Unterschied zwischen den Versionen

Aus Z-Brain
(Die Seite wurde neu angelegt: „__NOTOC__ Auf '''server3579''' läuft eine automatische Pipeline, die private Video-Downloads auf der Storage Box (<code>/mnt/storagebox/FlaviusPrivat</code>) bereinigt und sortiert. {| style="width:100%; border-spacing:10px; background:transparent; margin-top:-10px;" | style="width:50%; vertical-align:top;" | {| style="width:100%; border:1px solid #a7d7f9; background:#ffffff; padding:15px; margin-bottom:15px; border-radius:4px;" | style="background:#ce…“)
 
Keine Bearbeitungszusammenfassung
 
Zeile 77: Zeile 77:
<div style="font-weight:bold; background:#eaecf0; padding:5px;">📄 video-flach.py (Ausklappen)</div>
<div style="font-weight:bold; background:#eaecf0; padding:5px;">📄 video-flach.py (Ausklappen)</div>
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
<source lang="python">
<pre>
#!/usr/bin/env python3
#!/usr/bin/env python3
import argparse
import argparse
Zeile 154: Zeile 154:
if __name__ == "__main__":
if __name__ == "__main__":
     main()
     main()
</source>
</pre>
</div>
</div>
</div>
</div>
Zeile 161: Zeile 161:
<div style="font-weight:bold; background:#eaecf0; padding:5px;">📄 video-gruppieren.py (Ausklappen)</div>
<div style="font-weight:bold; background:#eaecf0; padding:5px;">📄 video-gruppieren.py (Ausklappen)</div>
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
<source lang="python">
<pre>
#!/usr/bin/env python3
#!/usr/bin/env python3
import argparse
import argparse
Zeile 237: Zeile 237:
if __name__ == "__main__":
if __name__ == "__main__":
     main()
     main()
</source>
</pre>
</div>
</div>
</div>
</div>
Zeile 244: Zeile 244:
<div style="font-weight:bold; background:#eaecf0; padding:5px;">📄 video-aufraeumen.sh (Ausklappen)</div>
<div style="font-weight:bold; background:#eaecf0; padding:5px;">📄 video-aufraeumen.sh (Ausklappen)</div>
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
<source lang="bash">
<pre>
#!/usr/bin/env bash
#!/usr/bin/env bash
set -euo pipefail
set -euo pipefail
Zeile 260: Zeile 260:
python3 /root/.local/bin/video-gruppieren.py "$ZIEL" --apply
python3 /root/.local/bin/video-gruppieren.py "$ZIEL" --apply
echo "=== $(date '+%F %T') : Aufraeumen fertig ==="
echo "=== $(date '+%F %T') : Aufraeumen fertig ==="
</source>
</pre>
</div>
</div>
</div>
</div>
Zeile 269: Zeile 269:
<div style="font-weight:bold; background:#eaecf0; padding:5px;">⚙️ video-aufraeumen.service (Ausklappen)</div>
<div style="font-weight:bold; background:#eaecf0; padding:5px;">⚙️ video-aufraeumen.service (Ausklappen)</div>
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
<source lang="ini">
<pre>
[Unit]
[Unit]
Description=Videos flach machen und gruppieren
Description=Videos flach machen und gruppieren
Zeile 278: Zeile 278:
Type=oneshot
Type=oneshot
ExecStart=/root/.local/bin/video-aufraeumen.sh
ExecStart=/root/.local/bin/video-aufraeumen.sh
</source>
</pre>
</div>
</div>
</div>
</div>


<div class="mw-collapsible mw-collapsed" style="important-border:1px solid #a7d7f9; padding:5px; margin-bottom:10px;">
<div class="mw-collapsible mw-collapsed" style="border:1px solid #a2a9b1; padding:5px; margin-bottom:10px;">
<div style="font-weight:bold; background:#eaecf0; padding:5px;">⏳ video-aufraeumen.timer (Ausklappen)</div>
<div style="font-weight:bold; background:#eaecf0; padding:5px;">⏳ video-aufraeumen.timer (Ausklappen)</div>
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
<source lang="ini">
<pre>
[Unit]
[Unit]
Description=Videos regelmaessig aufraeumen (flach + gruppieren)
Description=Videos regelmaessig aufraeumen (flach + gruppieren)
Zeile 296: Zeile 296:
[Install]
[Install]
WantedBy=timers.target
WantedBy=timers.target
</source>
</pre>
</div>
</div>
</div>
</div>
Zeile 302: Zeile 302:
=== 3. Einrichtung nach Wiederherstellung ===
=== 3. Einrichtung nach Wiederherstellung ===
Um die Pipeline auf einem neuen System zu aktivieren, folgende Befehle ausführen:
Um die Pipeline auf einem neuen System zu aktivieren, folgende Befehle ausführen:
<source lang="bash">
<pre>
chmod +x /root/.local/bin/video-flach.py
chmod +x /root/.local/bin/video-flach.py
chmod +x /root/.local/bin/video-gruppieren.py
chmod +x /root/.local/bin/video-gruppieren.py
Zeile 309: Zeile 309:
systemctl daemon-reload
systemctl daemon-reload
systemctl enable --now video-aufraeumen.timer
systemctl enable --now video-aufraeumen.timer
</source>
</pre>


<div style="font-size:0.9em; color:#54595d; text-align:right; margin-top:20px;">Stand der Dokumentation: Juni 2026</div>
<div style="font-size:0.9em; color:#54595d; text-align:right; margin-top:20px;">Stand der Dokumentation: Juni 2026</div>

Aktuelle Version vom 26. Juni 2026, 09:58 Uhr

Auf server3579 läuft eine automatische Pipeline, die private Video-Downloads auf der Storage Box (/mnt/storagebox/FlaviusPrivat) bereinigt und sortiert.

⚙️ Funktionsweise
  1. Flatten (video-flach.py): Zieht Videos aus überflüssigen Zwischenordnern eine Ebene hoch, falls die Struktur Name/Release-Ordner/film.mp4 statt Name/film.mp4 war.
  2. Gruppieren (video-gruppieren.py): Fasst mehrere Ordner mit gleichem Basis-Namen in einen gemeinsamen Hauptordner zusammen (z.B. mehrere Aufnahmen derselben Reihe).
  • Intervall: Läuft automatisch per systemd-Timer alle 30 Minuten (sowie 5 Minuten nach Server-Neustart).
  • Upload-Schutz: Ein Sicherheits-Check verhindert, dass Dateien angefasst werden, die sich aktuell noch im Schreibvorgang befinden.
💻 Bedienung & Befehle
Manuell als Vorschau (Dry-Run)
python3 /root/.local/bin/video-flach.py "/mnt/storagebox/FlaviusPrivat"
python3 /root/.local/bin/video-gruppieren.py "/mnt/storagebox/FlaviusPrivat"
Manuell wirklich ausführen
python3 /root/.local/bin/video-flach.py "/mnt/storagebox/FlaviusPrivat" --apply
python3 /root/.local/bin/video-gruppieren.py "/mnt/storagebox/FlaviusPrivat" --apply
Automatik-Status prüfen
systemctl list-timers video-aufraeumen.timer --no-pager
Letzte Protokolle ansehen
journalctl -u video-aufraeumen.service --no-pager -n 50
📂 Ablageorte
Datei / Zweck Pfad
Flatten-Skript /root/.local/bin/video-flach.py
Gruppier-Skript /root/.local/bin/video-gruppieren.py
Wrapper-Skript /root/.local/bin/video-aufraeumen.sh
systemd Service /etc/systemd/system/video-aufraeumen.service
systemd Timer /etc/systemd/system/video-aufraeumen.timer
Zielordner /mnt/storagebox/FlaviusPrivat

🧠 Entwicklung & Historie (Lessons Learned)[Bearbeiten]

Die Pipeline entstand iterativ über mehrere Zwischenschritte, bei denen typische Edge-Cases gelöst wurden:

  • Symptom „Keine Dateien gefunden“: Das Ursprungsskript suchte nur lose Dateien. Die echte Struktur nutzt jedoch immer eigene Unterordner pro Film.
  • Symptom „Ordner leer“ (Tiefe Strukturen): Videos lagen oft tief in Release-Unterordnern. video-flach.py zieht diese nun hoch. Gibt es Unklarheiten (mehrere oder keine Videos), wird der Ordner sicherheitshalber übersprungen.
  • Symptom „Keine Gruppenbildung“: Ordner nutzen oft das Datumsformat (.JJ.MM.TT.). Die Erkennung trennt nun strikt zwischen echten Datumsreihen und vierstelligen Jahreszahlen wie (2010), damit unterschiedliche Filme nicht fälschlich gruppiert werden.
  • Fehlerschutz bei parallelem Upload: Um Datenverlust während laufender Uploads zu vermeiden, prüfen die Skripte, ob die Dateigröße über einen kurzen Zeitraum absolut stabil bleibt und die Datei seit mindestens 2 Minuten nicht mehr verändert wurde (Settle-Phase).

---

💾 Quellcode & Wiederherstellung[Bearbeiten]

Um die Skripte im Falle eines Datenverlusts wiederherzustellen, können die folgenden Blöcke ausgeklappt und kopiert werden.

1. Skripte (`/root/.local/bin/`)[Bearbeiten]

📄 video-flach.py (Ausklappen)
#!/usr/bin/env python3
import argparse
import shutil
import time
from pathlib import Path

VIDEO_EXTENSIONS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".m4v", ".wmv", ".ts"}
SETTLE_SEC = 120
PROBE_SEC = 15

def find_videos(folder: Path):
    return [p for p in folder.rglob("*") if p.is_file() and p.suffix.lower() in VIDEO_EXTENSIONS]

def fertige_videos(videos):
    snapshot = {}
    for v in videos:
        try:
            snapshot[v] = v.stat().st_size
        except OSError:
            pass
    if not snapshot:
        return set()
    time.sleep(PROBE_SEC)
    fertig = set()
    for v, groesse_vorher in snapshot.items():
        try:
            st = v.stat()
        except OSError:
            continue
        if st.st_size == groesse_vorher and (time.time() - st.st_mtime) >= SETTLE_SEC:
            fertig.add(v)
    return fertig

def main():
    parser = argparse.ArgumentParser(description="Filme aus Zwischenordnern eine Ebene hochziehen")
    parser.add_argument("ordner", type=Path, help="Ordner mit den Namens-Unterordnern")
    parser.add_argument("--apply", action="store_true", help="Tatsaechlich verschieben")
    args = parser.parse_args()

    if args.apply:
        fertig = fertige_videos(find_videos(args.ordner))
    else:
        fertig = set(find_videos(args.ordner))

    geaendert = False
    for name_dir in sorted(args.ordner.iterdir()):
        if not name_dir.is_dir():
            continue

        videos = find_videos(name_dir)
        if len(videos) == 0 or len(videos) > 1:
            continue

        video = videos[0]
        if video not in fertig or video.parent == name_dir:
            continue

        geaendert = True
        inner = video.parent
        if args.apply:
            for f in sorted(inner.iterdir()):
                ziel = name_dir / f.name
                if ziel.exists():
                    continue
                shutil.move(str(f), str(ziel))
            current = inner
            while current != name_dir:
                eltern = current.parent
                try:
                    current.rmdir()
                except OSError:
                    break
                current = eltern

if __name__ == "__main__":
    main()
📄 video-gruppieren.py (Ausklappen)
#!/usr/bin/env python3
import argparse
import re
import shutil
import time
from collections import defaultdict
from pathlib import Path

VIDEO_EXTENSIONS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".m4v", ".wmv", ".ts"}
SETTLE_SEC = 120
PROBE_SEC = 15

DATE_PATTERN = re.compile(r"^(.+?)\.\d{2}\.\d{2}\.\d{2}(?:\.|\s|$)")
SUFFIX_PATTERN = re.compile(r"\s*[-_]?\s*[\(\[]?(?:teil|part|pt|folge)?\.?\s*(?<!\d)\d{1,3}[\)\]]?\s*$", re.IGNORECASE)

def base_name(name: str) -> str:
    m = DATE_PATTERN.match(name)
    if m:
        return m.group(1).strip()
    stripped = SUFFIX_PATTERN.sub("", name).strip()
    return stripped if stripped else name

def has_video(folder: Path) -> bool:
    return any(p.is_file() and p.suffix.lower() in VIDEO_EXTENSIONS for p in folder.iterdir())

def videos_in_folder(folder: Path):
    return [p for p in folder.iterdir() if p.is_file() and p.suffix.lower() in VIDEO_EXTENSIONS]

def fertige_videos(videos):
    snapshot = {}
    for v in videos:
        try: snapshot[v] = v.stat().st_size
        except OSError: pass
    if not snapshot: return set()
    time.sleep(PROBE_SEC)
    fertig = set()
    for v, groesse_vorher in snapshot.items():
        try: st = v.stat()
        except OSError: continue
        if st.st_size == groesse_vorher and (time.time() - st.st_mtime) >= SETTLE_SEC:
            fertig.add(v)
    return fertig

def main():
    parser = argparse.ArgumentParser(description="Video-Ordner nach Basis-Namen gruppieren")
    parser.add_argument("ordner", type=Path, help="Ordner mit den Film-Unterordnern")
    parser.add_argument("--apply", action="store_true", help="Tatsaechlich verschieben")
    args = parser.parse_args()

    alle_videos = [v for d in args.ordner.iterdir() if d.is_dir() for v in videos_in_folder(d)]
    fertig = fertige_videos(alle_videos) if args.apply else set(alle_videos)

    groups = defaultdict(list)
    for d in sorted(args.ordner.iterdir()):
        if d.is_dir() and has_video(d):
            groups[base_name(d.name)].append(d)

    for name, folders in groups.items():
        if len(folders) < 2: continue
        target_dir = args.ordner / name
        sources = [f for f in folders if f != target_dir]

        if args.apply:
            target_dir.mkdir(exist_ok=True)
            for src in sources:
                if not all(v in fertig for v in videos_in_folder(src)): continue
                for f in sorted(src.iterdir()):
                    ziel = target_dir / f.name
                    if ziel.exists(): continue
                    shutil.move(str(f), str(ziel))
                try: src.rmdir()
                except OSError: pass

if __name__ == "__main__":
    main()
📄 video-aufraeumen.sh (Ausklappen)
#!/usr/bin/env bash
set -euo pipefail

ZIEL="/mnt/storagebox/FlaviusPrivat"
MOUNT="/mnt/storagebox"

if ! mountpoint -q "$MOUNT"; then
    echo "Storage Box ($MOUNT) ist nicht gemountet – breche ab."
    exit 0
fi

echo "=== $(date '+%F %T') : Aufraeumen gestartet ==="
python3 /root/.local/bin/video-flach.py "$ZIEL" --apply
python3 /root/.local/bin/video-gruppieren.py "$ZIEL" --apply
echo "=== $(date '+%F %T') : Aufraeumen fertig ==="

2. systemd Konfiguration (`/etc/systemd/system/`)[Bearbeiten]

⚙️ video-aufraeumen.service (Ausklappen)
[Unit]
Description=Videos flach machen und gruppieren
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/root/.local/bin/video-aufraeumen.sh
⏳ video-aufraeumen.timer (Ausklappen)
[Unit]
Description=Videos regelmaessig aufraeumen (flach + gruppieren)

[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Persistent=true

[Install]
WantedBy=timers.target

3. Einrichtung nach Wiederherstellung[Bearbeiten]

Um die Pipeline auf einem neuen System zu aktivieren, folgende Befehle ausführen:

chmod +x /root/.local/bin/video-flach.py
chmod +x /root/.local/bin/video-gruppieren.py
chmod +x /root/.local/bin/video-aufraeumen.sh

systemctl daemon-reload
systemctl enable --now video-aufraeumen.timer
Stand der Dokumentation: Juni 2026