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