fix(windows): Cover, Arbeitsverzeichnis und die zurueckgelassenen _MEI-Ordner

Drei Befunde des Commanders vom 28.08.2026, alle drei gemessen.

## 1. „Was ist mit dem Cover auf Windows Rippy?"

Es gab keins, weil es keinen Treffer gab. `_scan_video` verlangte woertliche
Gleichheit:

    if movie.get("title", "").lower() == kandidat.lower():

An seiner Disc gemessen:

    'Evangelion 2.22'    1 Treffer   Evangelion: 2.0 You Can (Not) Advance
    'Evangelion'        20 Treffer   irgendein Evangelion

Der EINZIGE Treffer auf den vollen Disc-Titel war der richtige Film, mit
Poster — und wurde verworfen. Jetzt zaehlt die SPEZIFITAET der Anfrage: Wer
auf den vollen Disc-Titel hoechstens drei Treffer bekommt, hat gefragt wie
jemand, der weiss was er sucht. Ergebnis an derselben Disc:

    Evangelion: 2.0 You Can (Not) Advance / 2009 / 80 % / Poster + dt. Text

## 2. „Warum heisst das hier noch container platte? … Waere es moeglich das
##     Arbeitsverzeichnis zu aendern? momentan geht das nicht."

Beide Haelften gehen auf EINE Zeile zurueck: `MEDIA_ROOT = "/app/media"` war
zugleich Vorgabe UND Pfadgrenze. Auf Windows gibt es den Ordner nicht:

* `/storage-targets` fing den OSError und gab still [] zurueck — die Auswahl
  hatte genau einen Eintrag. Das ist „momentan geht das nicht".
* Dessen Text war fest verdrahtet „(Container-Platte)".
* `/browse` antwortete auf jeden Pfad mit 422.

Die Grenze faellt nicht weg, sie wird betriebsabhaengig: Container und
Kopflos-Betrieb bedienen ein Netz, die native App den Menschen davor.
Gemessen auf seinem PC: 7 Laufwerke zur Auswahl, X/Y/Z mit je 2,2 TB frei.

## 3. „Failed to remove temporary directory: …_MEI0000b0882"

GEMESSEN: 20 zurueckgelassene _MEI-Ordner mit 1,1 GB. Ursache: `Popen` ohne
`env=` reicht PyInstallers Auspack-Zeiger an die Kinder weiter (--dienst und
--oeffnen). Beide laufen dann im Ordner des Elternprozesses, der sich zuerst
beendet und ihn loeschen will. Waere das TEILWEISE geglueckt, haetten Dienst
und Fenster mitten im Betrieb ihre Dateien verloren.

Nebenbefund: test_setup_fenster scheiterte in jeder Umgebung ohne pywebview.

794 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-28 16:43:41 +02:00
co-authored by Claude Opus 5
parent 11001a443e
commit 35370fd552
15 changed files with 2728 additions and 1723 deletions
File diff suppressed because it is too large Load Diff
+141 -133
View File
@@ -1,133 +1,141 @@
/*
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
*
* ## Der Befund des Commanders (28.08.2026)
*
* Im Windows-Fenster stand auf der Server-Status-Kachel:
*
* Worker erreichbar: 0 von 1
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
* Container-Platte: unbekannt
* Freigaben: keine eingehängt
*
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
* installation für Windows gebaut."
*
* ## Warum ein Provider und kein Abruf je Seite
*
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
* Laufzeit nicht; er wird EINMAL geholt.
*
* ## Warum die Vorgabe „Docker" ist
*
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
* Docker-Annahme ist hier die richtige Vorgabe — sie zeigt MEHR, und ein kurz
* zu viel angezeigter Bereich ist harmloser als ein Bedienelement, das für
* einen Augenblick verschwindet und wieder auftaucht.
*
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
*/
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { api } from './api'
export interface BetriebsFaehigkeiten {
/** Gibt es andere Maschinen, die Jobs übernehmen? */
externe_worker: boolean
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
freigaben_einhaengen: boolean
/** Sind Pfade wie /app/media überhaupt gemeint? */
container_pfade: boolean
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
werkzeuge_verwalten: boolean
}
export interface Betrieb {
modus: 'standalone' | 'verteilt'
plattform: 'windows' | 'linux' | 'macos'
im_container: boolean
kann: BetriebsFaehigkeiten
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
ablage_vorgabe: string
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
* ausführen könnte — dann darf auch keiner dastehen. */
hilfe_befehl: string
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
* erreichbar" — nicht „es gibt keinen Betrieb". */
geladen: boolean
}
const VORGABE: Betrieb = {
modus: 'verteilt',
plattform: 'linux',
im_container: true,
kann: {
externe_worker: true,
freigaben_einhaengen: true,
container_pfade: true,
werkzeuge_verwalten: false,
},
ablage_vorgabe: '/app/media',
hilfe_befehl: 'docker compose -p rippy ps',
geladen: false,
}
const BetriebContext = createContext<Betrieb>(VORGABE)
export function BetriebProvider({ children }: { children: ReactNode }) {
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
useEffect(() => {
let abgemeldet = false
api.get('/betrieb')
.then(antwort => {
const d = antwort.data
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
// verschwände ein Bereich stillschweigend.
if (abgemeldet || !d || !d.kann) return
setBetrieb({
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
plattform: d.plattform || 'linux',
im_container: !!d.im_container,
kann: {
externe_worker: !!d.kann.externe_worker,
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
container_pfade: !!d.kann.container_pfade,
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
},
ablage_vorgabe: d.ablage_vorgabe || '',
hilfe_befehl: d.hilfe_befehl || '',
geladen: true,
})
})
.catch(() => {
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
})
return () => { abgemeldet = true }
}, [])
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
}
export function useBetrieb(): Betrieb {
return useContext(BetriebContext)
}
/**
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
* (Windows-Client oder Docker-All-in-One)?
*
* Bewusst NICHT „ist Windows": Ein Docker-All-in-One hat auch keinen zweiten
* Worker. Wer nach der Plattform fragt, obwohl er die Fähigkeit meint, baut
* die nächste falsche Annahme ein.
*/
export function useAlleinbetrieb(): boolean {
return !useBetrieb().kann.externe_worker
}
/*
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
*
* ## Der Befund des Commanders (28.08.2026)
*
* Im Windows-Fenster stand auf der Server-Status-Kachel:
*
* Worker erreichbar: 0 von 1
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
* Container-Platte: unbekannt
* Freigaben: keine eingehängt
*
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
* installation für Windows gebaut."
*
* ## Warum ein Provider und kein Abruf je Seite
*
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
* Laufzeit nicht; er wird EINMAL geholt.
*
* ## Warum die Vorgabe „Docker" ist
*
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
* Docker-Annahme ist hier die richtige Vorgabe — sie zeigt MEHR, und ein kurz
* zu viel angezeigter Bereich ist harmloser als ein Bedienelement, das für
* einen Augenblick verschwindet und wieder auftaucht.
*
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
*/
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { api } from './api'
export interface BetriebsFaehigkeiten {
/** Gibt es andere Maschinen, die Jobs übernehmen? */
externe_worker: boolean
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
freigaben_einhaengen: boolean
/** Sind Pfade wie /app/media überhaupt gemeint? */
container_pfade: boolean
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
werkzeuge_verwalten: boolean
/** Darf die Oberflaeche ausserhalb der Medien-Wurzel blaettern? */
frei_blaettern: boolean
}
export interface Betrieb {
modus: 'standalone' | 'verteilt'
plattform: 'windows' | 'linux' | 'macos'
im_container: boolean
kann: BetriebsFaehigkeiten
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
ablage_vorgabe: string
/** Wohin die Rohdaten wandern, wenn niemand etwas anderes waehlt. */
arbeits_vorgabe: string
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
* ausführen könnte — dann darf auch keiner dastehen. */
hilfe_befehl: string
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
* erreichbar" — nicht „es gibt keinen Betrieb". */
geladen: boolean
}
const VORGABE: Betrieb = {
modus: 'verteilt',
plattform: 'linux',
im_container: true,
kann: {
externe_worker: true,
freigaben_einhaengen: true,
container_pfade: true,
werkzeuge_verwalten: false,
frei_blaettern: false,
},
ablage_vorgabe: '/app/media',
arbeits_vorgabe: '/app/temp',
hilfe_befehl: 'docker compose -p rippy ps',
geladen: false,
}
const BetriebContext = createContext<Betrieb>(VORGABE)
export function BetriebProvider({ children }: { children: ReactNode }) {
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
useEffect(() => {
let abgemeldet = false
api.get('/betrieb')
.then(antwort => {
const d = antwort.data
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
// verschwände ein Bereich stillschweigend.
if (abgemeldet || !d || !d.kann) return
setBetrieb({
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
plattform: d.plattform || 'linux',
im_container: !!d.im_container,
kann: {
externe_worker: !!d.kann.externe_worker,
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
container_pfade: !!d.kann.container_pfade,
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
frei_blaettern: !!d.kann.frei_blaettern,
},
ablage_vorgabe: d.ablage_vorgabe || '',
arbeits_vorgabe: d.arbeits_vorgabe || '',
hilfe_befehl: d.hilfe_befehl || '',
geladen: true,
})
})
.catch(() => {
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
})
return () => { abgemeldet = true }
}, [])
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
}
export function useBetrieb(): Betrieb {
return useContext(BetriebContext)
}
/**
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
* (Windows-Client oder Docker-All-in-One)?
*
* Bewusst NICHT „ist Windows": Ein Docker-All-in-One hat auch keinen zweiten
* Worker. Wer nach der Plattform fragt, obwohl er die Fähigkeit meint, baut
* die nächste falsche Annahme ein.
*/
export function useAlleinbetrieb(): boolean {
return !useBetrieb().kann.externe_worker
}
+49 -3
View File
@@ -940,15 +940,40 @@ export default function SettingsPage() {
/storage-targets — dieselbe Liste wie bei den Speicherzielen,
inklusive freiem Platz.
*/}
{!betrieb.kann.container_pfade ? (
/*
* Ein echtes Pfadfeld statt einer Auswahl — dieselbe
* Entscheidung wie bei der Ablage darüber, und aus demselben
* Grund.
*
* Commander am 28.08.2026: „Wäre es möglich das
* Arbeitsverzeichnis zu ändern? momentan geht das nicht."
*
* Es ging nicht, weil `ziele` auf Windows LEER war: Die Liste
* kommt aus /storage-targets, und das las `/app/media` ein
* Ordner, den es dort nicht gibt. Übrig blieb eine Auswahl mit
* genau einem Eintrag, und der hieß Container-Platte".
*
* Die Liste ist inzwischen repariert (sie bringt jetzt die
* Laufwerke mit freiem Platz). Aber die richtige Antwort auf
* „wohin mit 100 GB" ist auf Windows oft ein Ort, den keine
* Liste kennt D:\Rippy-Arbeit oder eine UNC-Freigabe.
* Deshalb hier ein Feld, in das man ihn schreiben kann.
*/
<Input
label="Arbeitsverzeichnis für Roh-Rips (Standard)"
value={settings.workDir}
placeholder={betrieb.arbeits_vorgabe}
onChange={(e) => handleChange('workDir', e.target.value)}
/>
) : (
<Select
label="Arbeitsverzeichnis für Roh-Rips (Standard)"
value={settings.workDir}
onChange={(e) => handleChange('workDir', e.target.value)}
>
<option value="">
{betrieb.kann.container_pfade
? 'Container-Platte (Standard) — klein, nur für DVD/Blu-ray'
: `Standard (${betrieb.ablage_vorgabe || 'neben der Ablage'})`}
Container-Platte (Standard) klein, nur für DVD/Blu-ray
</option>
{ziele.map(z => (
<option key={z.path} value={z.path}>
@@ -957,6 +982,27 @@ export default function SettingsPage() {
</option>
))}
</Select>
)}
{!betrieb.kann.container_pfade && !!ziele.length && (
/* Die Laufwerke als Ein-Klick-Wahl. Wer den Platz sieht,
wählt anders — genau daran ist am 25.07. eine Platte
vollgelaufen. */
<div className="flex flex-wrap gap-1.5">
{ziele.map(z => (
<button
key={z.path}
onClick={() => handleChange('workDir', z.path)}
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
settings.workDir === z.path
? 'border-amber-500 bg-amber-500/10 text-amber-700 dark:text-amber-300'
: 'border-slate-200 dark:border-slate-700 text-slate-500 dark:text-slate-400 hover:border-slate-400'
}`}
>
{z.name}{z.free_gb != null ? `${z.free_gb} GB frei` : ''}
</button>
))}
</div>
)}
<p className="text-xs text-slate-500 dark:text-slate-400">
<strong className="text-slate-700 dark:text-slate-300">Der Standard</strong> beim
Rippen starten" kannst du für jede Disc etwas anderes wählen. <strong className="text-slate-700 dark:text-slate-300">