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:
co-authored by
Claude Opus 5
parent
11001a443e
commit
35370fd552
File diff suppressed because it is too large
Load Diff
+141
-133
@@ -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
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user