fix(savetv): NAS-Sync nutzt /api/filelist statt veraltete downloads-IDs

This commit is contained in:
Homelab Cursor 2026-05-21 14:52:49 +02:00
parent 9e4cb33b08
commit 8f491ff36e

View file

@ -1,20 +1,31 @@
#!/usr/bin/env python3
"""
SaveTV NAS Sync
Wartet 12h ± 0-30min nach Download auf Hetzner bevor Datei auf NAS kommt.
Holt die echte MP4-Liste von /api/filelist (nicht /api/films downloads!).
Wartet 24h ± 0-30min nach Download auf Hetzner bevor Datei auf NAS kommt.
"""
import os, json, time, random, fcntl, urllib.request, urllib.parse, urllib.error, email.utils
import json
import os
import random
import time
import urllib.error
import urllib.parse
import urllib.request
import email.utils
import fcntl
from datetime import datetime
ZIEL = "/mnt/nas/Filme zum nachbearbeiten"
BASE = "http://138.201.84.95:9443/files"
API = "http://138.201.84.95:9443/api/films"
CALLBACK = "http://138.201.84.95:9443/api/nas_synced"
LOG = "/var/log/savetv_sync.log"
LOCKFILE = "/tmp/savetv_sync.lock"
ZIEL = "/mnt/nas/Filme zum nachbearbeiten"
BASE = "http://138.201.84.95:9443/files"
FILELIST_API = "http://138.201.84.95:9443/api/filelist"
CALLBACK = "http://138.201.84.95:9443/api/nas_synced"
LOG = "/var/log/savetv_sync.log"
LOCKFILE = "/tmp/savetv_sync.lock"
MIN_ALTER_H = 24
JITTER_MIN = 30 # ± bis zu 30 Minuten Zufall
MIN_SIZE_MB = 700
MIN_ALTER_H = 24
JITTER_MIN = 30 # ± bis zu 30 Minuten Zufall
def log(msg):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@ -23,22 +34,26 @@ def log(msg):
with open(LOG, "a") as f:
f.write(line + "\n")
def get_filmliste():
"""Echte MP4-Dateinamen die noch nicht ans NAS übertragen wurden."""
try:
with urllib.request.urlopen(API, timeout=15) as r:
with urllib.request.urlopen(FILELIST_API, timeout=20) as r:
d = json.loads(r.read())
films = d.get("downloads", [])
return [f if isinstance(f, str) else f.get("name", "") for f in films if f]
files = d.get("files", [])
log(f"filelist: {len(files)} Filme warten auf NAS")
return files
except Exception as e:
log(f"API-Fehler: {e}")
log(f"filelist API-Fehler: {e}")
return []
def get_file_age_hours(name):
"""Alter der Datei auf Hetzner in Stunden via Last-Modified Header."""
url = BASE + "/" + urllib.parse.quote(name)
try:
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req, timeout=10) as r:
with urllib.request.urlopen(req, timeout=15) as r:
lm = r.headers.get("Last-Modified")
if lm:
ts = email.utils.parsedate_to_datetime(lm).timestamp()
@ -47,55 +62,50 @@ def get_file_age_hours(name):
pass
return None
def sync():
filme = get_filmliste()
if not filme:
log("Keine Filme in API")
log("Keine Filme in filelist")
return
log(f"{len(filme)} Filme in API")
kopiert = 0
for name in filme:
if not name:
if not name or not name.endswith(".mp4"):
continue
ziel = os.path.join(ZIEL, name)
# Schon vorhanden und vollständig?
if os.path.exists(ziel) and os.path.getsize(ziel) > 700 * 1024 * 1024:
if os.path.exists(ziel) and os.path.getsize(ziel) > MIN_SIZE_MB * 1024 * 1024:
log(f"SKIP (schon auf NAS): {name}")
continue
# Dateigröße prüfen (HEAD)
url = BASE + "/" + urllib.parse.quote(name)
try:
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req, timeout=10) as r:
with urllib.request.urlopen(req, timeout=15) as r:
cl = int(r.headers.get("Content-Length", 0))
size_mb = cl / 1024 / 1024
except Exception:
size_mb = 0
if size_mb < 700:
except Exception as e:
log(f"SKIP (HEAD fehlgeschlagen): {name}{e}")
continue
if size_mb < MIN_SIZE_MB:
log(f"SKIP (zu klein, {size_mb:.0f} MB): {name}")
continue
# Alter prüfen
alter_h = get_file_age_hours(name)
if alter_h is None:
log(f"SKIP (kein Header): {name}")
log(f"SKIP (kein Last-Modified): {name}")
continue
# Wartezeit: 12h + zufällige 0-30min
warte_h = MIN_ALTER_H + random.uniform(0, JITTER_MIN / 60)
if alter_h < warte_h:
rest_min = (warte_h - alter_h) * 60
log(f"WARTE noch {rest_min:.0f} min: {name} (Alter: {alter_h:.1f}h)")
continue
# Kopieren
url = BASE + "/" + urllib.parse.quote(name)
log(f"LADE ({alter_h:.1f}h alt): {name}")
log(f"LADE ({alter_h:.1f}h alt, {size_mb:.0f} MB): {name}")
try:
urllib.request.urlretrieve(url, ziel)
size_mb = os.path.getsize(ziel) / 1024 / 1024
@ -103,18 +113,26 @@ def sync():
kopiert += 1
try:
body = json.dumps({"name": name}).encode()
req = urllib.request.Request(CALLBACK, data=body,
headers={"Content-Type": "application/json"}, method="POST")
urllib.request.urlopen(req, timeout=5)
except Exception:
pass
req = urllib.request.Request(
CALLBACK,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
urllib.request.urlopen(req, timeout=10)
except Exception as e:
log(f" CALLBACK warn: {e}")
except Exception as e:
log(f" FEHLER: {e}: {name}")
if os.path.exists(ziel):
os.remove(ziel)
try:
os.remove(ziel)
except OSError:
pass
log(f"Fertig. {kopiert} neue Filme kopiert.")
if __name__ == "__main__":
with open(LOCKFILE, "w") as lock:
try: