Korrektur-Flow + Freigaben-Auswahl + Schreibtest + Schnellwahl-Klartext
Ampel / ampel (push) Failing after 52s

Commander-Befunde (23.07. spaet):
- MANUELLE KORREKTUR (KONZEPT Schritt 5 endlich komplett): Metadaten-Seite
  hat jetzt Titelsuche ueber ALLE Quellen (GET /metadata/search) mit
  Poster-Kacheln; ein Klick uebernimmt den Treffer (POST /metadata/override)
  fuer Karte, Jobs UND merkt ihn sich 30 Tage per Disc-Fingerabdruck —
  dieselbe Disc wird beim naechsten Einlegen sofort richtig erkannt
- PC/NAS per Klick: SMB-Freigaben eines Rechners auflisten
  (GET /storage-mounts/shares via smbclient) statt //host/share raten
- Berechtigungs-PRUEFUNG beim Einhaengen: Schreibtest, Warnung bei
  nur-lesbaren Zielen (API + UI + Log)
- "Standard-Unterordner" verstaendlich gemacht: heisst jetzt "Schnellwahl
  beim Rippen" mit Erklaertext; kryptisches Basis-Feld raus
- Preview zeigt Poster; Fingerprint fuer Cache/Override vereinheitlicht
  (billiges Label+Groesse statt Mount-Titel)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-23 21:34:34 +02:00
parent 37711f87c1
commit 6b7d391737
9 changed files with 440 additions and 48 deletions
+122 -4
View File
@@ -421,7 +421,7 @@ async def create_storage_mount(request: MountRequest):
raise HTTPException(status_code=409, detail="Name bereits vergeben")
try:
await asyncio.to_thread(
schreibbar = await asyncio.to_thread(
mount_verwaltung.mounten,
request.name, request.type, request.source,
request.options or "", request.username or "", request.password or "",
@@ -435,10 +435,24 @@ async def create_storage_mount(request: MountRequest):
request.options or "", request.username or "", request.password or "",
)
await asyncio.to_thread(
db.add_log, "success", "mounts",
f"Speicherziel '{request.name}' ({request.type}) eingehängt: {request.source}",
db.add_log,
"success" if schreibbar else "warning", "mounts",
f"Speicherziel '{request.name}' ({request.type}) eingehängt: {request.source}"
+ ("" if schreibbar else " — ACHTUNG: NUR LESBAR (Schreibtest fehlgeschlagen)"),
)
return {"name": request.name, "mounted": True}
return {"name": request.name, "mounted": True, "writable": schreibbar}
@app.get("/storage-mounts/shares")
async def list_shares(host: str, username: str = "", password: str = ""):
"""SMB-Freigaben eines Rechners auflisten (PC/NAS per Klick wählen)."""
try:
freigaben = await asyncio.to_thread(
mount_verwaltung.liste_smb_freigaben, host, username, password
)
except RuntimeError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"host": host, "shares": freigaben}
@app.delete("/storage-mounts/{name}")
@@ -615,6 +629,110 @@ async def confirm_metadata(title: str, year: Optional[int] = None, metadata: Dic
return {"status": "confirmed", "key": cache_key}
@app.get("/metadata/search")
async def metadata_search(q: str):
"""Manuelle Korrektur: Titel-Kandidaten aus ALLEN Quellen (TMDB/Jikan/OMDb).
KONZEPT Schritt 5: „Commander bestätigt oder korrigiert manuell" — das
hier ist der Korrektur-Teil, wenn die Automatik danebenliegt.
"""
def sammle():
prescan = PreScan()
ergebnisse = []
for movie in (prescan.tmdb.search_movie(q) or [])[:4]:
ergebnisse.append({
"title": movie.get("title", ""),
"year": int(movie["release_date"][:4]) if movie.get("release_date") else None,
"poster": f"https://image.tmdb.org/t/p/w342{movie['poster_path']}" if movie.get("poster_path") else "",
"overview": (movie.get("overview") or "")[:200],
"type": "movie", "source": "tmdb", "id": str(movie.get("id", "")),
})
for show in (prescan.tmdb.search_tv(q) or [])[:3]:
ergebnisse.append({
"title": show.get("name", ""),
"year": int(show["first_air_date"][:4]) if show.get("first_air_date") else None,
"poster": f"https://image.tmdb.org/t/p/w342{show['poster_path']}" if show.get("poster_path") else "",
"overview": (show.get("overview") or "")[:200],
"type": "tv", "source": "tmdb", "id": str(show.get("id", "")),
})
ergebnisse += prescan.jikan.suche(q)
ergebnisse += prescan.omdb.suche(q)
# Mit Poster zuerst, Duplikate (Titel+Jahr) raus
gesehen, dedup = set(), []
for e in sorted(ergebnisse, key=lambda x: 0 if x.get("poster") else 1):
schluessel = ((e.get("title") or "").lower(), e.get("year"))
if schluessel in gesehen:
continue
gesehen.add(schluessel)
dedup.append(e)
return dedup[:12]
return await asyncio.to_thread(sammle)
class MetadataOverride(BaseModel):
device_path: str
title: str
year: Optional[int] = None
poster: Optional[str] = None
overview: Optional[str] = None
type: Optional[str] = "movie"
source: Optional[str] = None
id: Optional[str] = None
@app.post("/metadata/override")
async def metadata_override(request: MetadataOverride):
"""Nutzer-Wahl für DIESE Disc merken: Karte, Jobs und Cache (30 Tage).
Der Disc-Fingerabdruck (Label+Größe) macht die Korrektur wiedererkennbar —
dieselbe Disc wird beim nächsten Einlegen sofort richtig angezeigt.
"""
if request.device_path not in device_discovery.list_optical_devices():
raise HTTPException(status_code=404, detail="Laufwerk nicht gefunden")
def speichere():
from cache import set as cache_setter
from cache.keys import generate_prescan_key
from prescan.prescan import disc_fingerprint
ergebnis = {
"disc_type": (DISC_CACHE.get(request.device_path) or {}).get("disc_type", "Blu-ray"),
"title": request.title,
"year": request.year,
"confidence": 0.99,
"metadata": {
"type": request.type or "movie",
"id": request.id or "",
"title": request.title,
"year": request.year,
"overview": request.overview or "",
"poster_path": request.poster or "",
"backdrop_path": "",
"runtime": 0,
"genres": [],
"source": request.source or "manuell",
},
"tracks": [],
}
abdruck = disc_fingerprint(request.device_path)
cache_setter(
generate_prescan_key(request.device_path, False, abdruck),
ergebnis, expire=30 * 86400,
)
DISC_CACHE[request.device_path] = ergebnis
return ergebnis
ergebnis = await asyncio.to_thread(speichere)
await asyncio.to_thread(
db.add_log, "success", "api",
f"Metadaten manuell festgelegt: {request.title}"
+ (f" ({request.year})" if request.year else ""),
)
return ergebnis
# Pre-Scan Endpoint
class PreScanRequest(BaseModel):
device_path: str