Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678fe276f9 | ||
|
|
d0281f7a1b |
@@ -2406,6 +2406,55 @@ async def get_keydb_status():
|
|||||||
return await asyncio.to_thread(sammle)
|
return await asyncio.to_thread(sammle)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/system/makemkv-key/holen")
|
||||||
|
async def makemkv_key_holen():
|
||||||
|
"""Holt den kostenlosen MakeMKV-Beta-Key JETZT aus dem Forum.
|
||||||
|
|
||||||
|
## Warum es diesen Knopf gibt (Commander 28.08.2026)
|
||||||
|
|
||||||
|
> „Bezüglich des MKV Beta Keys — der könnte theoretisch auch automatisch
|
||||||
|
> ausgelesen werden, ich glaube das web rippy kann das"
|
||||||
|
|
||||||
|
Er hatte recht: `makemkv_key.refresh_loop()` läuft seit jeher täglich mit
|
||||||
|
(main.py, Startereignis). Nachgemessen: Forum antwortet in 3,3 Sekunden,
|
||||||
|
Key mit 62 Zeichen.
|
||||||
|
|
||||||
|
Nur war davon **nichts zu sehen**. Kein Knopf, keine Zeitangabe, kein
|
||||||
|
Hinweis — im Einstellungsfeld stand ein Key, und ob der von Hand kam oder
|
||||||
|
von selbst, wusste niemand. Eine Automatik, die man nicht sehen kann, ist
|
||||||
|
für den Benutzer keine.
|
||||||
|
|
||||||
|
Der Key wechselt etwa monatlich. Wer nicht warten will, bis die Schleife
|
||||||
|
das nächste Mal nachsieht, drückt hier.
|
||||||
|
|
||||||
|
Rechtlich unverändert: öffentliche Beta-LIZENZ der Software, KEIN
|
||||||
|
Disc-Schlüssel.
|
||||||
|
"""
|
||||||
|
import makemkv_key as key_modul
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = await asyncio.to_thread(key_modul.fetch_current_key)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail=f"Das MakeMKV-Forum antwortet nicht ({type(e).__name__}). "
|
||||||
|
"Der Key lässt sich weiter von Hand eintragen.")
|
||||||
|
if not key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Im Forum-Beitrag stand kein Key im erwarteten Format. "
|
||||||
|
"Vermutlich hat sich die Seite geändert.")
|
||||||
|
geaendert = await asyncio.to_thread(key_modul._apply_key, key)
|
||||||
|
await asyncio.to_thread(
|
||||||
|
db.add_log, "success", "makemkv-key",
|
||||||
|
"Beta-Key aus dem Forum geholt (…%s)%s"
|
||||||
|
% (key[-6:], "" if geaendert else " — war schon der aktuelle"))
|
||||||
|
# Der Key selbst wird NICHT zurueckgegeben: Er steht in den Einstellungen,
|
||||||
|
# und ein zweiter Weg zu demselben Wert ist ein zweiter Weg, ihn zu
|
||||||
|
# verlieren.
|
||||||
|
return {"geholt": True, "geaendert": geaendert, "endet_auf": key[-6:]}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/system/keydb")
|
@app.post("/system/keydb")
|
||||||
async def set_keydb(request: KeydbRequest):
|
async def set_keydb(request: KeydbRequest):
|
||||||
"""Legt die vom Nutzer mitgebrachte KEYDB.cfg ab (atomar, ersetzt die alte).
|
"""Legt die vom Nutzer mitgebrachte KEYDB.cfg ab (atomar, ersetzt die alte).
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Tests für den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key)."""
|
"""Tests für den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key)."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
from makemkv_key import extract_key
|
from makemkv_key import extract_key
|
||||||
|
|
||||||
# Beispiel-Key im echten Format (T- + 64 Zeichen [A-Za-z0-9_]).
|
# Beispiel-Key im echten Format (T- + 64 Zeichen [A-Za-z0-9_]).
|
||||||
@@ -26,3 +28,71 @@ def test_extract_key_liefert_ganzen_key():
|
|||||||
treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text")
|
treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text")
|
||||||
assert treffer == _KEY
|
assert treffer == _KEY
|
||||||
assert treffer.startswith("T-")
|
assert treffer.startswith("T-")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Der Knopf „Jetzt aus dem Forum holen" (Commander 28.08.2026) ─────────
|
||||||
|
#
|
||||||
|
# > „Bezüglich des MKV Beta Keys - der könnte theoretisch auch automatisch
|
||||||
|
# > ausgelesen werden, ich glaube das web rippy kann das"
|
||||||
|
#
|
||||||
|
# Er hatte recht: `refresh_loop()` laeuft seit jeher taeglich mit (main.py,
|
||||||
|
# Startereignis). Gemessen am 28.08.2026: Forum antwortet in 3,3 s, Key mit 62
|
||||||
|
# Zeichen. Nur war davon NICHTS zu sehen — kein Knopf, keine Meldung. Eine
|
||||||
|
# Automatik, die man nicht sehen kann, ist fuer den Benutzer keine.
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def klient():
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_knopf_holt_und_meldet_was_passiert_ist(klient, monkeypatch):
|
||||||
|
import main as api
|
||||||
|
|
||||||
|
monkeypatch.setattr("makemkv_key.fetch_current_key", lambda *a, **k: _KEY)
|
||||||
|
monkeypatch.setattr("makemkv_key._apply_key", lambda key: True)
|
||||||
|
monkeypatch.setattr(api.db, "add_log", lambda *a, **k: None)
|
||||||
|
|
||||||
|
antwort = klient.post("/system/makemkv-key/holen")
|
||||||
|
assert antwort.status_code == 200
|
||||||
|
daten = antwort.json()
|
||||||
|
assert daten["geholt"] is True
|
||||||
|
assert daten["geaendert"] is True
|
||||||
|
assert daten["endet_auf"] == _KEY[-6:]
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_key_selbst_wandert_NICHT_ueber_die_leitung(klient, monkeypatch):
|
||||||
|
"""Er steht in den Einstellungen. Ein zweiter Weg zu demselben Wert ist
|
||||||
|
ein zweiter Weg, ihn zu verlieren."""
|
||||||
|
import main as api
|
||||||
|
|
||||||
|
monkeypatch.setattr("makemkv_key.fetch_current_key", lambda *a, **k: _KEY)
|
||||||
|
monkeypatch.setattr("makemkv_key._apply_key", lambda key: False)
|
||||||
|
monkeypatch.setattr(api.db, "add_log", lambda *a, **k: None)
|
||||||
|
|
||||||
|
text = klient.post("/system/makemkv-key/holen").text
|
||||||
|
assert _KEY not in text
|
||||||
|
assert _KEY[:20] not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_stummes_forum_ist_ein_klarer_fehler_kein_leerer_erfolg(klient,
|
||||||
|
monkeypatch):
|
||||||
|
"""Sonst hiesse „geholt: true" mit leerem Key, es sei alles in Ordnung."""
|
||||||
|
def wirft(*a, **k):
|
||||||
|
raise OSError("Verbindung abgelehnt")
|
||||||
|
|
||||||
|
monkeypatch.setattr("makemkv_key.fetch_current_key", wirft)
|
||||||
|
antwort = klient.post("/system/makemkv-key/holen")
|
||||||
|
assert antwort.status_code == 502
|
||||||
|
assert "Forum" in antwort.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_geaendertes_seitenformat_wird_benannt(klient, monkeypatch):
|
||||||
|
"""Kein Treffer heisst NICHT „kein Key noetig" — es heisst, die Seite hat
|
||||||
|
sich geaendert. Das muss dastehen, sonst sucht niemand danach."""
|
||||||
|
monkeypatch.setattr("makemkv_key.fetch_current_key", lambda *a, **k: None)
|
||||||
|
antwort = klient.post("/system/makemkv-key/holen")
|
||||||
|
assert antwort.status_code == 502
|
||||||
|
assert "geändert" in antwort.json()["detail"]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+141
-141
@@ -1,141 +1,141 @@
|
|||||||
/*
|
/*
|
||||||
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
|
* Worauf läuft Rippy — und was kann dieser Betrieb überhaupt?
|
||||||
*
|
*
|
||||||
* ## Der Befund des Commanders (28.08.2026)
|
* ## Der Befund des Commanders (28.08.2026)
|
||||||
*
|
*
|
||||||
* Im Windows-Fenster stand auf der Server-Status-Kachel:
|
* Im Windows-Fenster stand auf der Server-Status-Kachel:
|
||||||
*
|
*
|
||||||
* Worker erreichbar: 0 von 1
|
* Worker erreichbar: 0 von 1
|
||||||
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
|
* Kein Worker antwortet — ohne ihn läuft kein Rip. Prüfen: docker compose ps
|
||||||
* Container-Platte: unbekannt
|
* Container-Platte: unbekannt
|
||||||
* Freigaben: keine eingehängt
|
* Freigaben: keine eingehängt
|
||||||
*
|
*
|
||||||
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
|
* Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
|
||||||
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
|
* Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt dort
|
||||||
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
|
* selbst. Sein Urteil: „Du hast ja quasi nur rippy genommen und die docker
|
||||||
* installation für Windows gebaut."
|
* installation für Windows gebaut."
|
||||||
*
|
*
|
||||||
* ## Warum ein Provider und kein Abruf je Seite
|
* ## Warum ein Provider und kein Abruf je Seite
|
||||||
*
|
*
|
||||||
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
|
* Dieselbe Begründung wie beim Ereignis-Strom nebenan: Vier Seiten, die
|
||||||
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
|
* dasselbe abfragen, sind vier Abrufe — und vier Gelegenheiten, dass eine
|
||||||
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
|
* davon einen anderen Stand hat als die anderen. Der Betrieb ändert sich zur
|
||||||
* Laufzeit nicht; er wird EINMAL geholt.
|
* Laufzeit nicht; er wird EINMAL geholt.
|
||||||
*
|
*
|
||||||
* ## Warum die Vorgabe „Docker" ist
|
* ## Warum die Vorgabe „Docker" ist
|
||||||
*
|
*
|
||||||
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
|
* Solange die Antwort noch unterwegs ist, muss irgendetwas gelten. Die
|
||||||
* Docker-Annahme ist hier die richtige Vorgabe — sie zeigt MEHR, und ein kurz
|
* 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
|
* zu viel angezeigter Bereich ist harmloser als ein Bedienelement, das für
|
||||||
* einen Augenblick verschwindet und wieder auftaucht.
|
* einen Augenblick verschwindet und wieder auftaucht.
|
||||||
*
|
*
|
||||||
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
|
* Ein FEHLGESCHLAGENER Abruf ist etwas anderes als „noch unterwegs":
|
||||||
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
|
* `geladen` bleibt dann false, und wer das wissen will, kann es abfragen. Ein
|
||||||
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
|
* Verbindungsabriss ist keine Aussage über die Welt (siehe useEventStream).
|
||||||
*/
|
*/
|
||||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||||
import { api } from './api'
|
import { api } from './api'
|
||||||
|
|
||||||
export interface BetriebsFaehigkeiten {
|
export interface BetriebsFaehigkeiten {
|
||||||
/** Gibt es andere Maschinen, die Jobs übernehmen? */
|
/** Gibt es andere Maschinen, die Jobs übernehmen? */
|
||||||
externe_worker: boolean
|
externe_worker: boolean
|
||||||
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
|
/** Kann Rippy Netzwerk-Freigaben selbst einhängen? */
|
||||||
freigaben_einhaengen: boolean
|
freigaben_einhaengen: boolean
|
||||||
/** Sind Pfade wie /app/media überhaupt gemeint? */
|
/** Sind Pfade wie /app/media überhaupt gemeint? */
|
||||||
container_pfade: boolean
|
container_pfade: boolean
|
||||||
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
|
/** Kann Rippy MakeMKV/HandBrake selbst beschaffen? */
|
||||||
werkzeuge_verwalten: boolean
|
werkzeuge_verwalten: boolean
|
||||||
/** Darf die Oberflaeche ausserhalb der Medien-Wurzel blaettern? */
|
/** Darf die Oberflaeche ausserhalb der Medien-Wurzel blaettern? */
|
||||||
frei_blaettern: boolean
|
frei_blaettern: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Betrieb {
|
export interface Betrieb {
|
||||||
modus: 'standalone' | 'verteilt'
|
modus: 'standalone' | 'verteilt'
|
||||||
plattform: 'windows' | 'linux' | 'macos'
|
plattform: 'windows' | 'linux' | 'macos'
|
||||||
im_container: boolean
|
im_container: boolean
|
||||||
kann: BetriebsFaehigkeiten
|
kann: BetriebsFaehigkeiten
|
||||||
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
|
/** Womit ein Ablage-Feld vorbelegt wird — je Betrieb ein anderer Ort. */
|
||||||
ablage_vorgabe: string
|
ablage_vorgabe: string
|
||||||
/** Wohin die Rohdaten wandern, wenn niemand etwas anderes waehlt. */
|
/** Wohin die Rohdaten wandern, wenn niemand etwas anderes waehlt. */
|
||||||
arbeits_vorgabe: string
|
arbeits_vorgabe: string
|
||||||
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
|
/** Der Befehl zum Nachsehen. LEER heißt: es gibt keinen, den der Nutzer
|
||||||
* ausführen könnte — dann darf auch keiner dastehen. */
|
* ausführen könnte — dann darf auch keiner dastehen. */
|
||||||
hilfe_befehl: string
|
hilfe_befehl: string
|
||||||
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
|
/** Ist die Auskunft schon da? False heißt „noch unterwegs ODER nicht
|
||||||
* erreichbar" — nicht „es gibt keinen Betrieb". */
|
* erreichbar" — nicht „es gibt keinen Betrieb". */
|
||||||
geladen: boolean
|
geladen: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const VORGABE: Betrieb = {
|
const VORGABE: Betrieb = {
|
||||||
modus: 'verteilt',
|
modus: 'verteilt',
|
||||||
plattform: 'linux',
|
plattform: 'linux',
|
||||||
im_container: true,
|
im_container: true,
|
||||||
kann: {
|
kann: {
|
||||||
externe_worker: true,
|
externe_worker: true,
|
||||||
freigaben_einhaengen: true,
|
freigaben_einhaengen: true,
|
||||||
container_pfade: true,
|
container_pfade: true,
|
||||||
werkzeuge_verwalten: false,
|
werkzeuge_verwalten: false,
|
||||||
frei_blaettern: false,
|
frei_blaettern: false,
|
||||||
},
|
},
|
||||||
ablage_vorgabe: '/app/media',
|
ablage_vorgabe: '/app/media',
|
||||||
arbeits_vorgabe: '/app/temp',
|
arbeits_vorgabe: '/app/temp',
|
||||||
hilfe_befehl: 'docker compose -p rippy ps',
|
hilfe_befehl: 'docker compose -p rippy ps',
|
||||||
geladen: false,
|
geladen: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BetriebContext = createContext<Betrieb>(VORGABE)
|
const BetriebContext = createContext<Betrieb>(VORGABE)
|
||||||
|
|
||||||
export function BetriebProvider({ children }: { children: ReactNode }) {
|
export function BetriebProvider({ children }: { children: ReactNode }) {
|
||||||
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
|
const [betrieb, setBetrieb] = useState<Betrieb>(VORGABE)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let abgemeldet = false
|
let abgemeldet = false
|
||||||
api.get('/betrieb')
|
api.get('/betrieb')
|
||||||
.then(antwort => {
|
.then(antwort => {
|
||||||
const d = antwort.data
|
const d = antwort.data
|
||||||
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
|
// Nur übernehmen, was WIRKLICH ankommt. Ein halb gefülltes Objekt
|
||||||
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
|
// hieße in JavaScript `undefined` — und `undefined` ist falsch, also
|
||||||
// verschwände ein Bereich stillschweigend.
|
// verschwände ein Bereich stillschweigend.
|
||||||
if (abgemeldet || !d || !d.kann) return
|
if (abgemeldet || !d || !d.kann) return
|
||||||
setBetrieb({
|
setBetrieb({
|
||||||
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
|
modus: d.modus === 'verteilt' ? 'verteilt' : 'standalone',
|
||||||
plattform: d.plattform || 'linux',
|
plattform: d.plattform || 'linux',
|
||||||
im_container: !!d.im_container,
|
im_container: !!d.im_container,
|
||||||
kann: {
|
kann: {
|
||||||
externe_worker: !!d.kann.externe_worker,
|
externe_worker: !!d.kann.externe_worker,
|
||||||
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
|
freigaben_einhaengen: !!d.kann.freigaben_einhaengen,
|
||||||
container_pfade: !!d.kann.container_pfade,
|
container_pfade: !!d.kann.container_pfade,
|
||||||
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
|
werkzeuge_verwalten: !!d.kann.werkzeuge_verwalten,
|
||||||
frei_blaettern: !!d.kann.frei_blaettern,
|
frei_blaettern: !!d.kann.frei_blaettern,
|
||||||
},
|
},
|
||||||
ablage_vorgabe: d.ablage_vorgabe || '',
|
ablage_vorgabe: d.ablage_vorgabe || '',
|
||||||
arbeits_vorgabe: d.arbeits_vorgabe || '',
|
arbeits_vorgabe: d.arbeits_vorgabe || '',
|
||||||
hilfe_befehl: d.hilfe_befehl || '',
|
hilfe_befehl: d.hilfe_befehl || '',
|
||||||
geladen: true,
|
geladen: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
|
// Nichts tun. Ein misslungener Abruf ist keine Aussage über den
|
||||||
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
|
// Betrieb — die Vorgabe bleibt stehen, `geladen` bleibt false.
|
||||||
})
|
})
|
||||||
return () => { abgemeldet = true }
|
return () => { abgemeldet = true }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
|
return <BetriebContext.Provider value={betrieb}>{children}</BetriebContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBetrieb(): Betrieb {
|
export function useBetrieb(): Betrieb {
|
||||||
return useContext(BetriebContext)
|
return useContext(BetriebContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
|
* Kurzform für den häufigsten Fall: Läuft Rippy als eigenständige App
|
||||||
* (Windows-Client oder Docker-All-in-One)?
|
* (Windows-Client oder Docker-All-in-One)?
|
||||||
*
|
*
|
||||||
* Bewusst NICHT „ist Windows": Ein Docker-All-in-One hat auch keinen zweiten
|
* 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
|
* Worker. Wer nach der Plattform fragt, obwohl er die Fähigkeit meint, baut
|
||||||
* die nächste falsche Annahme ein.
|
* die nächste falsche Annahme ein.
|
||||||
*/
|
*/
|
||||||
export function useAlleinbetrieb(): boolean {
|
export function useAlleinbetrieb(): boolean {
|
||||||
return !useBetrieb().kann.externe_worker
|
return !useBetrieb().kann.externe_worker
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ export default function SettingsPage() {
|
|||||||
// Ablageziele unter /app/media inkl. eingehängter Freigaben — speist die
|
// Ablageziele unter /app/media inkl. eingehängter Freigaben — speist die
|
||||||
// Auswahl des Arbeitsverzeichnisses (vorher musste man den Pfad tippen).
|
// Auswahl des Arbeitsverzeichnisses (vorher musste man den Pfad tippen).
|
||||||
const [ziele, setZiele] = useState<StorageZiel[]>([])
|
const [ziele, setZiele] = useState<StorageZiel[]>([])
|
||||||
|
// Beta-Key: läuft gerade ein Abruf, und was kam zuletzt dabei heraus?
|
||||||
|
const [keyHolt, setKeyHolt] = useState(false)
|
||||||
|
const [keyMeldung, setKeyMeldung] = useState('')
|
||||||
const [keystore, setKeystore] = useState<KeystoreStatus | null>(null)
|
const [keystore, setKeystore] = useState<KeystoreStatus | null>(null)
|
||||||
const [keystoreBusy, setKeystoreBusy] = useState(false)
|
const [keystoreBusy, setKeystoreBusy] = useState(false)
|
||||||
const keystoreInput = useRef<HTMLInputElement>(null)
|
const keystoreInput = useRef<HTMLInputElement>(null)
|
||||||
@@ -253,6 +256,34 @@ export default function SettingsPage() {
|
|||||||
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
|
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Den Beta-Key JETZT holen, statt auf die tägliche Schleife zu warten.
|
||||||
|
*
|
||||||
|
* Der geholte Key wird nicht angezeigt — er wandert in die Einstellungen,
|
||||||
|
* und die werden hier ohnehin neu geladen. Zwei Wege zu demselben Wert sind
|
||||||
|
* zwei Wege, ihn zu verlieren.
|
||||||
|
*/
|
||||||
|
const keyJetztHolen = async () => {
|
||||||
|
setKeyHolt(true)
|
||||||
|
setKeyMeldung('')
|
||||||
|
try {
|
||||||
|
const r = await api.post('/system/makemkv-key/holen')
|
||||||
|
const antwort = await api.get('/settings')
|
||||||
|
setSettings({ ...defaultSettings, ...antwort.data })
|
||||||
|
setKeyMeldung(r.data?.geaendert
|
||||||
|
? `Neuer Key geholt (endet auf …${r.data.endet_auf}).`
|
||||||
|
: `Schon aktuell (endet auf …${r.data?.endet_auf || '?'}).`)
|
||||||
|
toast('success', 'MakeMKV-Beta-Key aus dem Forum geholt.')
|
||||||
|
} catch (e: any) {
|
||||||
|
const grund = e?.response?.data?.detail
|
||||||
|
|| 'Das Forum ist nicht erreichbar.'
|
||||||
|
setKeyMeldung('Fehlgeschlagen: ' + grund)
|
||||||
|
toast('error', grund)
|
||||||
|
} finally {
|
||||||
|
setKeyHolt(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const keydbHochladen = async (datei: File) => {
|
const keydbHochladen = async (datei: File) => {
|
||||||
setKeydbBusy(true)
|
setKeydbBusy(true)
|
||||||
try {
|
try {
|
||||||
@@ -1295,13 +1326,44 @@ export default function SettingsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input
|
{/*
|
||||||
type="password"
|
Der Beta-Key holt sich selbst — man sah es nur nirgends.
|
||||||
label="MakeMKV Beta-Key"
|
|
||||||
value={settings.makemkvAppKey}
|
Commander am 28.08.2026: „Bezüglich des MKV Beta Keys — der
|
||||||
onChange={(e) => handleChange('makemkvAppKey', e.target.value)}
|
könnte theoretisch auch automatisch ausgelesen werden, ich
|
||||||
placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)"
|
glaube das web rippy kann das." Er hatte recht:
|
||||||
/>
|
`makemkv_key.refresh_loop()` läuft täglich mit, seit es die
|
||||||
|
Datei gibt (gemessen: Forum antwortet in 3,3 s, Key mit 62
|
||||||
|
Zeichen). Hier stand aber nur ein Passwortfeld — ob der Wert
|
||||||
|
darin von Hand kam oder von selbst, war nicht zu erkennen.
|
||||||
|
Eine Automatik, die man nicht sehen kann, ist für den
|
||||||
|
Benutzer keine.
|
||||||
|
*/}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="MakeMKV Beta-Key"
|
||||||
|
value={settings.makemkvAppKey}
|
||||||
|
onChange={(e) => handleChange('makemkvAppKey', e.target.value)}
|
||||||
|
placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={keyHolt}
|
||||||
|
onClick={keyJetztHolen}
|
||||||
|
>
|
||||||
|
{keyHolt ? 'wird geholt …' : 'Jetzt aus dem Forum holen'}
|
||||||
|
</Button>
|
||||||
|
<span className={`text-xs ${keyMeldung.startsWith('Fehlgeschlagen')
|
||||||
|
? 'text-red-600 dark:text-red-400'
|
||||||
|
: 'text-slate-500 dark:text-slate-400'}`}>
|
||||||
|
{keyMeldung || 'Rippy holt ihn ohnehin täglich selbst — '
|
||||||
|
+ 'der Key wechselt etwa monatlich.'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Schlüsselspeicher — der Hauptweg für 4K-UHD (Befund 25.07.2026,
|
Schlüsselspeicher — der Hauptweg für 4K-UHD (Befund 25.07.2026,
|
||||||
|
|||||||
@@ -403,6 +403,13 @@ def vorgaben() -> dict:
|
|||||||
return {
|
return {
|
||||||
"ziel": reg.standard_installationsordner(),
|
"ziel": reg.standard_installationsordner(),
|
||||||
"ablage": betrieb.ablage_vorgabe({}, container=False, system="windows"),
|
"ablage": betrieb.ablage_vorgabe({}, container=False, system="windows"),
|
||||||
|
# Der Arbeitsordner gehoert ins Setup, nicht erst in die Einstellungen
|
||||||
|
# (Commander-Wunsch 28.08.2026). Grund: Der Roh-Rip einer 4K-UHD ist
|
||||||
|
# bis zu 100 GB gross. Wer das erst NACH der ersten vollen Platte
|
||||||
|
# erfaehrt, hat es zu spaet erfahren — genau so lief am 25.07.2026
|
||||||
|
# die VM-Platte voll.
|
||||||
|
"arbeit": betrieb.arbeits_vorgabe({}, container=False,
|
||||||
|
plattform_name="win32"),
|
||||||
"port": STANDARD_PORT,
|
"port": STANDARD_PORT,
|
||||||
"autostart": True,
|
"autostart": True,
|
||||||
"verknuepfungen": True,
|
"verknuepfungen": True,
|
||||||
|
|||||||
+224
-224
@@ -1,224 +1,224 @@
|
|||||||
"""Kind-Prozesse ohne Fenster — und die Ausgabe ohne Konsole.
|
"""Kind-Prozesse ohne Fenster — und die Ausgabe ohne Konsole.
|
||||||
|
|
||||||
Ein Konsolenfenster laesst sich in einem Test nicht ansehen. Geprueft wird
|
Ein Konsolenfenster laesst sich in einem Test nicht ansehen. Geprueft wird
|
||||||
deshalb das, woran es beim ersten Anlauf gescheitert ist: die ENTSCHEIDUNG,
|
deshalb das, woran es beim ersten Anlauf gescheitert ist: die ENTSCHEIDUNG,
|
||||||
wohin die Ausgabe geht, wenn es keine Konsole gibt.
|
wohin die Ausgabe geht, wenn es keine Konsole gibt.
|
||||||
|
|
||||||
## Warum das nicht Nebensache ist
|
## Warum das nicht Nebensache ist
|
||||||
|
|
||||||
Die EXE wird seit dem 28.08.2026 als Fenster-Programm gebaut, damit beim
|
Die EXE wird seit dem 28.08.2026 als Fenster-Programm gebaut, damit beim
|
||||||
Doppelklick keine schwarze Box aufgeht. Damit ist `sys.stdout` aber `None`.
|
Doppelklick keine schwarze Box aufgeht. Damit ist `sys.stdout` aber `None`.
|
||||||
Ohne Umleitung wuerde jede Bibliothek, die auf `stderr` schreibt, mit
|
Ohne Umleitung wuerde jede Bibliothek, die auf `stderr` schreibt, mit
|
||||||
`AttributeError: 'NoneType' object has no attribute 'write'` sterben — mitten
|
`AttributeError: 'NoneType' object has no attribute 'write'` sterben — mitten
|
||||||
im Start, und nirgends stuende etwas. Genau der stille Fehlschlag, vor dem
|
im Start, und nirgends stuende etwas. Genau der stille Fehlschlag, vor dem
|
||||||
AGENTS.md warnt.
|
AGENTS.md warnt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from rippy.platform import winlauf
|
from rippy.platform import winlauf
|
||||||
|
|
||||||
nur_windows = pytest.mark.skipif(os.name != "nt", reason="nur unter Windows")
|
nur_windows = pytest.mark.skipif(os.name != "nt", reason="nur unter Windows")
|
||||||
|
|
||||||
|
|
||||||
# ── Kind-Prozesse ───────────────────────────────────────────────────────
|
# ── Kind-Prozesse ───────────────────────────────────────────────────────
|
||||||
def test_flag_ist_auf_windows_gesetzt_und_sonst_null():
|
def test_flag_ist_auf_windows_gesetzt_und_sonst_null():
|
||||||
"""Auf Linux ist CREATE_NO_WINDOW nicht vorhanden; `creationflags=0` wird
|
"""Auf Linux ist CREATE_NO_WINDOW nicht vorhanden; `creationflags=0` wird
|
||||||
dort akzeptiert und ignoriert. So laeuft derselbe Code auf beiden Seiten."""
|
dort akzeptiert und ignoriert. So laeuft derselbe Code auf beiden Seiten."""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
assert winlauf.OHNE_FENSTER == subprocess.CREATE_NO_WINDOW
|
assert winlauf.OHNE_FENSTER == subprocess.CREATE_NO_WINDOW
|
||||||
else:
|
else:
|
||||||
assert winlauf.OHNE_FENSTER == 0
|
assert winlauf.OHNE_FENSTER == 0
|
||||||
|
|
||||||
|
|
||||||
# ── Gibt es ueberhaupt eine Ausgabe? ────────────────────────────────────
|
# ── Gibt es ueberhaupt eine Ausgabe? ────────────────────────────────────
|
||||||
def test_fehlende_standardausgabe_wird_erkannt(monkeypatch):
|
def test_fehlende_standardausgabe_wird_erkannt(monkeypatch):
|
||||||
"""Der Fall in der fertigen EXE: PyInstaller setzt `sys.stdout` auf None."""
|
"""Der Fall in der fertigen EXE: PyInstaller setzt `sys.stdout` auf None."""
|
||||||
monkeypatch.setattr(sys, "stdout", None)
|
monkeypatch.setattr(sys, "stdout", None)
|
||||||
assert winlauf.ohne_konsole() is True
|
assert winlauf.ohne_konsole() is True
|
||||||
|
|
||||||
|
|
||||||
def test_ein_strom_ohne_dateinummer_zaehlt_auch_als_ohne_konsole(monkeypatch):
|
def test_ein_strom_ohne_dateinummer_zaehlt_auch_als_ohne_konsole(monkeypatch):
|
||||||
"""pytest ersetzt stdout durch einen Auffang-Puffer. Der hat kein
|
"""pytest ersetzt stdout durch einen Auffang-Puffer. Der hat kein
|
||||||
`fileno` — und ein Meldungsfenster waere in einem Testlauf das Letzte,
|
`fileno` — und ein Meldungsfenster waere in einem Testlauf das Letzte,
|
||||||
was jemand gebrauchen kann."""
|
was jemand gebrauchen kann."""
|
||||||
class Puffer:
|
class Puffer:
|
||||||
write = staticmethod(lambda _: None)
|
write = staticmethod(lambda _: None)
|
||||||
|
|
||||||
monkeypatch.setattr(sys, "stdout", Puffer())
|
monkeypatch.setattr(sys, "stdout", Puffer())
|
||||||
assert winlauf.ohne_konsole() is True
|
assert winlauf.ohne_konsole() is True
|
||||||
|
|
||||||
|
|
||||||
def test_mit_echter_ausgabe_ist_alles_in_ordnung(monkeypatch, tmp_path):
|
def test_mit_echter_ausgabe_ist_alles_in_ordnung(monkeypatch, tmp_path):
|
||||||
datei = tmp_path / "echt.txt"
|
datei = tmp_path / "echt.txt"
|
||||||
with open(datei, "w", encoding="utf-8") as f:
|
with open(datei, "w", encoding="utf-8") as f:
|
||||||
monkeypatch.setattr(sys, "stdout", f)
|
monkeypatch.setattr(sys, "stdout", f)
|
||||||
assert winlauf.ohne_konsole() is False
|
assert winlauf.ohne_konsole() is False
|
||||||
|
|
||||||
|
|
||||||
# ── Die Protokolldatei ──────────────────────────────────────────────────
|
# ── Die Protokolldatei ──────────────────────────────────────────────────
|
||||||
def test_protokoll_liegt_bei_rippy():
|
def test_protokoll_liegt_bei_rippy():
|
||||||
"""Nicht im TEMP: Was Rippy ueber sich aufschreibt, gehoert zu Rippy und
|
"""Nicht im TEMP: Was Rippy ueber sich aufschreibt, gehoert zu Rippy und
|
||||||
verschwindet beim Deinstallieren mit."""
|
verschwindet beim Deinstallieren mit."""
|
||||||
pfad = winlauf.protokolldatei(os.path.join("C:" + os.sep, "Basis"))
|
pfad = winlauf.protokolldatei(os.path.join("C:" + os.sep, "Basis"))
|
||||||
assert pfad.endswith(os.path.join("Rippy", "rippy.log"))
|
assert pfad.endswith(os.path.join("Rippy", "rippy.log"))
|
||||||
assert pfad.startswith(os.path.join("C:" + os.sep, "Basis"))
|
assert pfad.startswith(os.path.join("C:" + os.sep, "Basis"))
|
||||||
|
|
||||||
|
|
||||||
def test_umleiten_schreibt_wirklich(tmp_path, monkeypatch):
|
def test_umleiten_schreibt_wirklich(tmp_path, monkeypatch):
|
||||||
ziel = str(tmp_path / "unterordner" / "rippy.log")
|
ziel = str(tmp_path / "unterordner" / "rippy.log")
|
||||||
vorher_out, vorher_err = sys.stdout, sys.stderr
|
vorher_out, vorher_err = sys.stdout, sys.stderr
|
||||||
try:
|
try:
|
||||||
assert winlauf.ausgabe_umleiten(ziel) == ziel
|
assert winlauf.ausgabe_umleiten(ziel) == ziel
|
||||||
print("eine Zeile mit Umlaut: ä")
|
print("eine Zeile mit Umlaut: ä")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
finally:
|
finally:
|
||||||
sys.stdout, sys.stderr = vorher_out, vorher_err
|
sys.stdout, sys.stderr = vorher_out, vorher_err
|
||||||
assert "eine Zeile mit Umlaut" in open(ziel, encoding="utf-8").read()
|
assert "eine Zeile mit Umlaut" in open(ziel, encoding="utf-8").read()
|
||||||
|
|
||||||
|
|
||||||
def test_umleiten_haengt_an_statt_zu_ueberschreiben(tmp_path):
|
def test_umleiten_haengt_an_statt_zu_ueberschreiben(tmp_path):
|
||||||
"""Sonst waere nach jedem Neustart das Protokoll des letzten Fehlers weg
|
"""Sonst waere nach jedem Neustart das Protokoll des letzten Fehlers weg
|
||||||
— also genau das, was man dann sucht."""
|
— also genau das, was man dann sucht."""
|
||||||
ziel = str(tmp_path / "rippy.log")
|
ziel = str(tmp_path / "rippy.log")
|
||||||
vorher_out, vorher_err = sys.stdout, sys.stderr
|
vorher_out, vorher_err = sys.stdout, sys.stderr
|
||||||
try:
|
try:
|
||||||
winlauf.ausgabe_umleiten(ziel)
|
winlauf.ausgabe_umleiten(ziel)
|
||||||
print("erster Start")
|
print("erster Start")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
winlauf.ausgabe_umleiten(ziel)
|
winlauf.ausgabe_umleiten(ziel)
|
||||||
print("zweiter Start")
|
print("zweiter Start")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
finally:
|
finally:
|
||||||
sys.stdout, sys.stderr = vorher_out, vorher_err
|
sys.stdout, sys.stderr = vorher_out, vorher_err
|
||||||
inhalt = open(ziel, encoding="utf-8").read()
|
inhalt = open(ziel, encoding="utf-8").read()
|
||||||
assert "erster Start" in inhalt and "zweiter Start" in inhalt
|
assert "erster Start" in inhalt and "zweiter Start" in inhalt
|
||||||
|
|
||||||
|
|
||||||
def test_unbeschreibbarer_ort_laesst_den_start_nicht_platzen(monkeypatch):
|
def test_unbeschreibbarer_ort_laesst_den_start_nicht_platzen(monkeypatch):
|
||||||
"""Lieber ins Nichts schreiben als beim Start sterben. Ein Programm, das
|
"""Lieber ins Nichts schreiben als beim Start sterben. Ein Programm, das
|
||||||
wegen seiner Protokolldatei nicht hochkommt, ist schlimmer als eines
|
wegen seiner Protokolldatei nicht hochkommt, ist schlimmer als eines
|
||||||
ohne Protokoll."""
|
ohne Protokoll."""
|
||||||
def geht_nicht(*a, **k):
|
def geht_nicht(*a, **k):
|
||||||
raise OSError("kein Platz")
|
raise OSError("kein Platz")
|
||||||
|
|
||||||
monkeypatch.setattr(winlauf.os, "makedirs", geht_nicht)
|
monkeypatch.setattr(winlauf.os, "makedirs", geht_nicht)
|
||||||
vorher_out, vorher_err = sys.stdout, sys.stderr
|
vorher_out, vorher_err = sys.stdout, sys.stderr
|
||||||
try:
|
try:
|
||||||
winlauf.ausgabe_umleiten("/gibt/es/nicht/rippy.log")
|
winlauf.ausgabe_umleiten("/gibt/es/nicht/rippy.log")
|
||||||
print("das darf nicht werfen")
|
print("das darf nicht werfen")
|
||||||
finally:
|
finally:
|
||||||
sys.stdout, sys.stderr = vorher_out, vorher_err
|
sys.stdout, sys.stderr = vorher_out, vorher_err
|
||||||
|
|
||||||
|
|
||||||
# ── Das Meldungsfenster ─────────────────────────────────────────────────
|
# ── Das Meldungsfenster ─────────────────────────────────────────────────
|
||||||
def test_ohne_windows_kein_meldungsfenster():
|
def test_ohne_windows_kein_meldungsfenster():
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
pytest.skip("prueft das Verhalten auf Nicht-Windows")
|
pytest.skip("prueft das Verhalten auf Nicht-Windows")
|
||||||
assert winlauf.meldung_zeigen("egal") is False
|
assert winlauf.meldung_zeigen("egal") is False
|
||||||
assert winlauf.an_elternkonsole_haengen() is False
|
assert winlauf.an_elternkonsole_haengen() is False
|
||||||
|
|
||||||
|
|
||||||
# ── Der Auspack-Ordner (Befund 28.08.2026) ──────────────────────────────
|
# ── Der Auspack-Ordner (Befund 28.08.2026) ──────────────────────────────
|
||||||
#
|
#
|
||||||
# Commander, mit Bildschirmfoto:
|
# Commander, mit Bildschirmfoto:
|
||||||
#
|
#
|
||||||
# Failed to remove temporary directory:
|
# Failed to remove temporary directory:
|
||||||
# C:\Users\TobisPC\AppData\Local\Temp\_MEI0000b0882
|
# C:\Users\TobisPC\AppData\Local\Temp\_MEI0000b0882
|
||||||
#
|
#
|
||||||
# „Und manchmal kommt dieser fehler."
|
# „Und manchmal kommt dieser fehler."
|
||||||
#
|
#
|
||||||
# GEMESSEN in seinem Temp-Ordner: 20 zurueckgelassene _MEI-Ordner, zusammen
|
# GEMESSEN in seinem Temp-Ordner: 20 zurueckgelassene _MEI-Ordner, zusammen
|
||||||
# 1,1 GB. Der aus der Meldung liess sich hinterher anstandslos loeschen — die
|
# 1,1 GB. Der aus der Meldung liess sich hinterher anstandslos loeschen — die
|
||||||
# Sperre war also voruebergehend, es ist ein Wettlauf.
|
# Sperre war also voruebergehend, es ist ein Wettlauf.
|
||||||
#
|
#
|
||||||
# Ursache: `starte_hintergrund` und `starte_fensterprozess` starten Rippy.exe
|
# Ursache: `starte_hintergrund` und `starte_fensterprozess` starten Rippy.exe
|
||||||
# erneut. `Popen` ohne `env=` reicht PyInstallers Auspack-Zeiger weiter, also
|
# erneut. `Popen` ohne `env=` reicht PyInstallers Auspack-Zeiger weiter, also
|
||||||
# laufen beide Kinder im Ordner des Elternprozesses. Der beendet sich zuerst,
|
# laufen beide Kinder im Ordner des Elternprozesses. Der beendet sich zuerst,
|
||||||
# will loeschen — und die Kinder halten die DLLs offen.
|
# will loeschen — und die Kinder halten die DLLs offen.
|
||||||
|
|
||||||
|
|
||||||
def test_der_auspack_zeiger_wird_dem_kind_NICHT_mitgegeben():
|
def test_der_auspack_zeiger_wird_dem_kind_NICHT_mitgegeben():
|
||||||
"""Der Test, der 1,1 GB Reste verhindert haette."""
|
"""Der Test, der 1,1 GB Reste verhindert haette."""
|
||||||
eltern = {"PATH": "/usr/bin", "_MEIPASS2": "/tmp/_MEI123",
|
eltern = {"PATH": "/usr/bin", "_MEIPASS2": "/tmp/_MEI123",
|
||||||
"_PYI_APPLICATION_HOME_DIR": "/tmp/_MEI123",
|
"_PYI_APPLICATION_HOME_DIR": "/tmp/_MEI123",
|
||||||
"_PYI_ARCHIVE_FILE": "/x/Rippy.exe",
|
"_PYI_ARCHIVE_FILE": "/x/Rippy.exe",
|
||||||
"_PYI_PARENT_PROCESS_LEVEL": "1"}
|
"_PYI_PARENT_PROCESS_LEVEL": "1"}
|
||||||
kind = winlauf.umgebung_ohne_bundle(eltern)
|
kind = winlauf.umgebung_ohne_bundle(eltern)
|
||||||
for name in winlauf.PYI_ZEIGER:
|
for name in winlauf.PYI_ZEIGER:
|
||||||
assert name not in kind, "%s wuerde das Kind in den Ordner der Eltern schicken" % name
|
assert name not in kind, "%s wuerde das Kind in den Ordner der Eltern schicken" % name
|
||||||
assert kind["PATH"] == "/usr/bin", "der Rest der Umgebung muss bleiben"
|
assert kind["PATH"] == "/usr/bin", "der Rest der Umgebung muss bleiben"
|
||||||
|
|
||||||
|
|
||||||
def test_die_elternumgebung_wird_nicht_veraendert():
|
def test_die_elternumgebung_wird_nicht_veraendert():
|
||||||
"""Ein `del os.environ[...]` haette den eigenen Prozess beschaedigt."""
|
"""Ein `del os.environ[...]` haette den eigenen Prozess beschaedigt."""
|
||||||
eltern = {"_MEIPASS2": "/tmp/_MEI123"}
|
eltern = {"_MEIPASS2": "/tmp/_MEI123"}
|
||||||
winlauf.umgebung_ohne_bundle(eltern)
|
winlauf.umgebung_ohne_bundle(eltern)
|
||||||
assert eltern == {"_MEIPASS2": "/tmp/_MEI123"}
|
assert eltern == {"_MEIPASS2": "/tmp/_MEI123"}
|
||||||
|
|
||||||
|
|
||||||
def test_beide_startwege_reichen_den_zeiger_nicht_weiter():
|
def test_beide_startwege_reichen_den_zeiger_nicht_weiter():
|
||||||
"""Der Waechter: Wer kuenftig einen dritten Startweg baut, muss `env=`
|
"""Der Waechter: Wer kuenftig einen dritten Startweg baut, muss `env=`
|
||||||
setzen. Sonst kommen die Reste zurueck, und zwar still."""
|
setzen. Sonst kommen die Reste zurueck, und zwar still."""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
quelle = os.path.join(os.path.dirname(__file__), "..", "windows_app.py")
|
quelle = os.path.join(os.path.dirname(__file__), "..", "windows_app.py")
|
||||||
with open(os.path.abspath(quelle), encoding="utf-8") as f:
|
with open(os.path.abspath(quelle), encoding="utf-8") as f:
|
||||||
text = f.read()
|
text = f.read()
|
||||||
stellen = [m.start() for m in re.finditer(r"subprocess\.Popen\(\[programm", text)]
|
stellen = [m.start() for m in re.finditer(r"subprocess\.Popen\(\[programm", text)]
|
||||||
assert len(stellen) == 2, "Startwege haben sich geaendert: %d gefunden" % len(stellen)
|
assert len(stellen) == 2, "Startwege haben sich geaendert: %d gefunden" % len(stellen)
|
||||||
for stelle in stellen:
|
for stelle in stellen:
|
||||||
aufruf = text[stelle:stelle + 300]
|
aufruf = text[stelle:stelle + 300]
|
||||||
assert "env=umgebung_ohne_bundle()" in aufruf, (
|
assert "env=umgebung_ohne_bundle()" in aufruf, (
|
||||||
"Dieser Start reicht den Auspack-Zeiger weiter:\n" + aufruf[:120])
|
"Dieser Start reicht den Auspack-Zeiger weiter:\n" + aufruf[:120])
|
||||||
|
|
||||||
|
|
||||||
def test_aufraeumen_laesst_einen_BENUTZTEN_ordner_unangetastet(tmp_path):
|
def test_aufraeumen_laesst_einen_BENUTZTEN_ordner_unangetastet(tmp_path):
|
||||||
"""Die wichtigste Eigenschaft: Ein laufender Rippy darf nichts verlieren.
|
"""Die wichtigste Eigenschaft: Ein laufender Rippy darf nichts verlieren.
|
||||||
|
|
||||||
Ein blindes `rmtree(ignore_errors=True)` haette ihm die halbe Bibliothek
|
Ein blindes `rmtree(ignore_errors=True)` haette ihm die halbe Bibliothek
|
||||||
weggeraeumt, bevor es an der gesperrten DLL scheitert.
|
weggeraeumt, bevor es an der gesperrten DLL scheitert.
|
||||||
"""
|
"""
|
||||||
lebt = tmp_path / "_MEI111111"
|
lebt = tmp_path / "_MEI111111"
|
||||||
lebt.mkdir()
|
lebt.mkdir()
|
||||||
(lebt / "python312.dll").write_bytes(b"MZ")
|
(lebt / "python312.dll").write_bytes(b"MZ")
|
||||||
(lebt / "wichtig.pyd").write_bytes(b"x")
|
(lebt / "wichtig.pyd").write_bytes(b"x")
|
||||||
|
|
||||||
def gesperrt(pfad):
|
def gesperrt(pfad):
|
||||||
raise OSError(32, "Datei wird von einem anderen Prozess verwendet")
|
raise OSError(32, "Datei wird von einem anderen Prozess verwendet")
|
||||||
|
|
||||||
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="",
|
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="",
|
||||||
jetzt_loeschen=gesperrt) == 0
|
jetzt_loeschen=gesperrt) == 0
|
||||||
assert (lebt / "wichtig.pyd").exists(), "an einem benutzten Ordner wird NICHTS angefasst"
|
assert (lebt / "wichtig.pyd").exists(), "an einem benutzten Ordner wird NICHTS angefasst"
|
||||||
|
|
||||||
|
|
||||||
def test_aufraeumen_entfernt_die_reste(tmp_path):
|
def test_aufraeumen_entfernt_die_reste(tmp_path):
|
||||||
for name in ("_MEI000035002", "_MEI0000b0882"):
|
for name in ("_MEI000035002", "_MEI0000b0882"):
|
||||||
ordner = tmp_path / name
|
ordner = tmp_path / name
|
||||||
ordner.mkdir()
|
ordner.mkdir()
|
||||||
(ordner / "python312.dll").write_bytes(b"MZ")
|
(ordner / "python312.dll").write_bytes(b"MZ")
|
||||||
(ordner / "base_library.zip").write_bytes(b"PK")
|
(ordner / "base_library.zip").write_bytes(b"PK")
|
||||||
|
|
||||||
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 2
|
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 2
|
||||||
assert not list(tmp_path.glob("_MEI*"))
|
assert not list(tmp_path.glob("_MEI*"))
|
||||||
|
|
||||||
|
|
||||||
def test_aufraeumen_raeumt_den_EIGENEN_ordner_nicht_weg(tmp_path):
|
def test_aufraeumen_raeumt_den_EIGENEN_ordner_nicht_weg(tmp_path):
|
||||||
"""Sonst saegte Rippy waehrend des Startens an seinem eigenen Ast."""
|
"""Sonst saegte Rippy waehrend des Startens an seinem eigenen Ast."""
|
||||||
eigen = tmp_path / "_MEI999999"
|
eigen = tmp_path / "_MEI999999"
|
||||||
eigen.mkdir()
|
eigen.mkdir()
|
||||||
(eigen / "python312.dll").write_bytes(b"MZ")
|
(eigen / "python312.dll").write_bytes(b"MZ")
|
||||||
|
|
||||||
assert winlauf.reste_aufraeumen(str(tmp_path), eigener=str(eigen)) == 0
|
assert winlauf.reste_aufraeumen(str(tmp_path), eigener=str(eigen)) == 0
|
||||||
assert eigen.exists()
|
assert eigen.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_aufraeumen_ignoriert_fremde_ordner(tmp_path):
|
def test_aufraeumen_ignoriert_fremde_ordner(tmp_path):
|
||||||
"""Was nicht `_MEI` heisst, geht uns nichts an."""
|
"""Was nicht `_MEI` heisst, geht uns nichts an."""
|
||||||
(tmp_path / "wichtige-daten").mkdir()
|
(tmp_path / "wichtige-daten").mkdir()
|
||||||
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 0
|
assert winlauf.reste_aufraeumen(str(tmp_path), eigener="") == 0
|
||||||
assert (tmp_path / "wichtige-daten").exists()
|
assert (tmp_path / "wichtige-daten").exists()
|
||||||
|
|||||||
+677
-600
File diff suppressed because it is too large
Load Diff
+193
-193
@@ -1,193 +1,193 @@
|
|||||||
"""Der Betriebsmodus und seine Faehigkeiten.
|
"""Der Betriebsmodus und seine Faehigkeiten.
|
||||||
|
|
||||||
## Warum es diese Tests gibt
|
## Warum es diese Tests gibt
|
||||||
|
|
||||||
Commander am 28.08.2026, zum Windows-Fenster:
|
Commander am 28.08.2026, zum Windows-Fenster:
|
||||||
|
|
||||||
Worker erreichbar: 0 von 1
|
Worker erreichbar: 0 von 1
|
||||||
Kein Worker antwortet — Pruefen: docker compose ps
|
Kein Worker antwortet — Pruefen: docker compose ps
|
||||||
Container-Platte: unbekannt
|
Container-Platte: unbekannt
|
||||||
Freigaben: keine eingehaengt
|
Freigaben: keine eingehaengt
|
||||||
|
|
||||||
Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Die Oberflaeche hatte
|
Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Die Oberflaeche hatte
|
||||||
nie erfahren, worauf sie laeuft — also hat sie angenommen.
|
nie erfahren, worauf sie laeuft — also hat sie angenommen.
|
||||||
|
|
||||||
Alle Pruefungen hier sind eingespritzt: Jeder Betriebsfall ist pruefbar, ohne
|
Alle Pruefungen hier sind eingespritzt: Jeder Betriebsfall ist pruefbar, ohne
|
||||||
ihn herzustellen. Das ist der Punkt — sonst waere „laeuft im Container" nur
|
ihn herzustellen. Das ist der Punkt — sonst waere „laeuft im Container" nur
|
||||||
auf einem Container-Rechner testbar, und genau dort faellt der Windows-Fehler
|
auf einem Container-Rechner testbar, und genau dort faellt der Windows-Fehler
|
||||||
nie auf.
|
nie auf.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rippy import betrieb
|
from rippy import betrieb
|
||||||
|
|
||||||
DOCKER = {"profil": "api", "queue": {"treiber": "celery", "broker": "redis://x"}}
|
DOCKER = {"profil": "api", "queue": {"treiber": "celery", "broker": "redis://x"}}
|
||||||
WINDOWS = {"profil": "standalone", "queue": {"treiber": "lokal"}}
|
WINDOWS = {"profil": "standalone", "queue": {"treiber": "lokal"}}
|
||||||
|
|
||||||
|
|
||||||
# ── Plattform ───────────────────────────────────────────────────────────
|
# ── Plattform ───────────────────────────────────────────────────────────
|
||||||
def test_plattform_wird_zu_einem_klaren_namen():
|
def test_plattform_wird_zu_einem_klaren_namen():
|
||||||
assert betrieb.plattform("win32") == "windows"
|
assert betrieb.plattform("win32") == "windows"
|
||||||
assert betrieb.plattform("linux") == "linux"
|
assert betrieb.plattform("linux") == "linux"
|
||||||
assert betrieb.plattform("darwin") == "macos"
|
assert betrieb.plattform("darwin") == "macos"
|
||||||
|
|
||||||
|
|
||||||
def test_unbekannte_plattform_gilt_als_linux():
|
def test_unbekannte_plattform_gilt_als_linux():
|
||||||
"""FreeBSD und Verwandte verhalten sich wie Linux — und ein Rueckfall auf
|
"""FreeBSD und Verwandte verhalten sich wie Linux — und ein Rueckfall auf
|
||||||
etwas Bekanntes ist besser als ein leeres Feld."""
|
etwas Bekanntes ist besser als ein leeres Feld."""
|
||||||
assert betrieb.plattform("freebsd14") == "linux"
|
assert betrieb.plattform("freebsd14") == "linux"
|
||||||
|
|
||||||
|
|
||||||
# ── Container ───────────────────────────────────────────────────────────
|
# ── Container ───────────────────────────────────────────────────────────
|
||||||
def test_container_wird_gemessen_nicht_geraten():
|
def test_container_wird_gemessen_nicht_geraten():
|
||||||
"""`/.dockerenv` legt Docker selbst an — verlaesslicher als eine
|
"""`/.dockerenv` legt Docker selbst an — verlaesslicher als eine
|
||||||
Umgebungsvariable, die jeder setzen und vergessen kann."""
|
Umgebungsvariable, die jeder setzen und vergessen kann."""
|
||||||
assert betrieb.im_container(lambda p: p == "/.dockerenv") is True
|
assert betrieb.im_container(lambda p: p == "/.dockerenv") is True
|
||||||
assert betrieb.im_container(lambda p: False) is False
|
assert betrieb.im_container(lambda p: False) is False
|
||||||
|
|
||||||
|
|
||||||
# ── Modus ───────────────────────────────────────────────────────────────
|
# ── Modus ───────────────────────────────────────────────────────────────
|
||||||
def test_celery_heisst_verteilt():
|
def test_celery_heisst_verteilt():
|
||||||
assert betrieb.modus(DOCKER) == "verteilt"
|
assert betrieb.modus(DOCKER) == "verteilt"
|
||||||
|
|
||||||
|
|
||||||
def test_lokale_queue_heisst_standalone():
|
def test_lokale_queue_heisst_standalone():
|
||||||
assert betrieb.modus(WINDOWS) == "standalone"
|
assert betrieb.modus(WINDOWS) == "standalone"
|
||||||
|
|
||||||
|
|
||||||
def test_ohne_angaben_gilt_standalone():
|
def test_ohne_angaben_gilt_standalone():
|
||||||
"""Der harmlosere Rueckfall: Ein standalone-Rippy zeigt nichts an, was es
|
"""Der harmlosere Rueckfall: Ein standalone-Rippy zeigt nichts an, was es
|
||||||
nicht gibt. Andersherum entstuenden die Phantom-Worker wieder."""
|
nicht gibt. Andersherum entstuenden die Phantom-Worker wieder."""
|
||||||
assert betrieb.modus({}) == "standalone"
|
assert betrieb.modus({}) == "standalone"
|
||||||
|
|
||||||
|
|
||||||
# ── Die Faehigkeiten — hier sass der gemeldete Fehler ───────────────────
|
# ── Die Faehigkeiten — hier sass der gemeldete Fehler ───────────────────
|
||||||
def test_windows_hat_KEINE_externen_worker():
|
def test_windows_hat_KEINE_externen_worker():
|
||||||
"""DER Befund: „Worker erreichbar: 0 von 1 — Kein Worker antwortet."
|
"""DER Befund: „Worker erreichbar: 0 von 1 — Kein Worker antwortet."
|
||||||
Unter Windows rippt Rippy selbst; es gibt keinen zweiten, auf den man
|
Unter Windows rippt Rippy selbst; es gibt keinen zweiten, auf den man
|
||||||
warten koennte. Die Meldung war nicht nur unpassend, sie war falsch."""
|
warten koennte. Die Meldung war nicht nur unpassend, sie war falsch."""
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert a["kann"]["externe_worker"] is False
|
assert a["kann"]["externe_worker"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_windows_kennt_KEINE_container_pfade():
|
def test_windows_kennt_KEINE_container_pfade():
|
||||||
"""`/app/media` ist ein Pfad IM Container. Auf Windows heisst der Ordner
|
"""`/app/media` ist ein Pfad IM Container. Auf Windows heisst der Ordner
|
||||||
anders und liegt woanders — ein damit vorbelegtes Feld ist schlicht
|
anders und liegt woanders — ein damit vorbelegtes Feld ist schlicht
|
||||||
falsch."""
|
falsch."""
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert a["kann"]["container_pfade"] is False
|
assert a["kann"]["container_pfade"] is False
|
||||||
assert "/app/" not in a["ablage_vorgabe"]
|
assert "/app/" not in a["ablage_vorgabe"]
|
||||||
|
|
||||||
|
|
||||||
def test_windows_haengt_keine_freigaben_ein():
|
def test_windows_haengt_keine_freigaben_ein():
|
||||||
"""Unter Windows gibt man einen UNC-Pfad an, fertig. Eine Maske zum
|
"""Unter Windows gibt man einen UNC-Pfad an, fertig. Eine Maske zum
|
||||||
Einhaengen waere eine Bedienung, die ins Leere fuehrt."""
|
Einhaengen waere eine Bedienung, die ins Leere fuehrt."""
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert a["kann"]["freigaben_einhaengen"] is False
|
assert a["kann"]["freigaben_einhaengen"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_windows_darf_seine_werkzeuge_selbst_pflegen():
|
def test_windows_darf_seine_werkzeuge_selbst_pflegen():
|
||||||
"""Im Image steckt alles fest, ein Update ist ein Rebuild. Nur die native
|
"""Im Image steckt alles fest, ein Update ist ein Rebuild. Nur die native
|
||||||
Installation kann MakeMKV/HandBrake selbst beschaffen."""
|
Installation kann MakeMKV/HandBrake selbst beschaffen."""
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert a["kann"]["werkzeuge_verwalten"] is True
|
assert a["kann"]["werkzeuge_verwalten"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_docker_behaelt_alles_was_es_hatte():
|
def test_docker_behaelt_alles_was_es_hatte():
|
||||||
"""Die Windows-Reparatur darf dem Docker-Betrieb nichts wegnehmen."""
|
"""Die Windows-Reparatur darf dem Docker-Betrieb nichts wegnehmen."""
|
||||||
a = betrieb.auskunft(DOCKER, container=True, plattform_name="linux")
|
a = betrieb.auskunft(DOCKER, container=True, plattform_name="linux")
|
||||||
assert a["kann"]["externe_worker"] is True
|
assert a["kann"]["externe_worker"] is True
|
||||||
assert a["kann"]["container_pfade"] is True
|
assert a["kann"]["container_pfade"] is True
|
||||||
assert a["kann"]["freigaben_einhaengen"] is True
|
assert a["kann"]["freigaben_einhaengen"] is True
|
||||||
assert a["ablage_vorgabe"] == "/app/media"
|
assert a["ablage_vorgabe"] == "/app/media"
|
||||||
assert a["hilfe_befehl"] == "docker compose -p rippy ps"
|
assert a["hilfe_befehl"] == "docker compose -p rippy ps"
|
||||||
|
|
||||||
|
|
||||||
def test_der_docker_hinweis_taucht_ausserhalb_von_docker_NICHT_auf():
|
def test_der_docker_hinweis_taucht_ausserhalb_von_docker_NICHT_auf():
|
||||||
"""„Pruefen: docker compose ps" auf einem Windows-PC ist ein Rat, den
|
"""„Pruefen: docker compose ps" auf einem Windows-PC ist ein Rat, den
|
||||||
niemand befolgen kann."""
|
niemand befolgen kann."""
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert a["hilfe_befehl"] == ""
|
assert a["hilfe_befehl"] == ""
|
||||||
|
|
||||||
|
|
||||||
def test_docker_allinone_hat_container_aber_keine_externen_worker():
|
def test_docker_allinone_hat_container_aber_keine_externen_worker():
|
||||||
"""Der Fall, an dem ein blosser Modus-Name zerbrechen wuerde: EIN
|
"""Der Fall, an dem ein blosser Modus-Name zerbrechen wuerde: EIN
|
||||||
Container, der alles selbst macht. Er hat Container-Pfade, aber niemanden,
|
Container, der alles selbst macht. Er hat Container-Pfade, aber niemanden,
|
||||||
der von aussen Jobs uebernimmt."""
|
der von aussen Jobs uebernimmt."""
|
||||||
a = betrieb.auskunft({"profil": "standalone", "queue": {"treiber": "lokal"}},
|
a = betrieb.auskunft({"profil": "standalone", "queue": {"treiber": "lokal"}},
|
||||||
container=True, plattform_name="linux")
|
container=True, plattform_name="linux")
|
||||||
assert a["kann"]["container_pfade"] is True
|
assert a["kann"]["container_pfade"] is True
|
||||||
assert a["kann"]["externe_worker"] is False
|
assert a["kann"]["externe_worker"] is False
|
||||||
|
|
||||||
|
|
||||||
# ── Die Ablage-Vorgabe ──────────────────────────────────────────────────
|
# ── Die Ablage-Vorgabe ──────────────────────────────────────────────────
|
||||||
def test_eingestellte_ablage_schlaegt_jede_vorgabe():
|
def test_eingestellte_ablage_schlaegt_jede_vorgabe():
|
||||||
a = betrieb.auskunft({"storage": {"medien": "D:\\Filme"}},
|
a = betrieb.auskunft({"storage": {"medien": "D:\\Filme"}},
|
||||||
container=False, plattform_name="win32")
|
container=False, plattform_name="win32")
|
||||||
assert a["ablage_vorgabe"] == "D:\\Filme"
|
assert a["ablage_vorgabe"] == "D:\\Filme"
|
||||||
|
|
||||||
|
|
||||||
def test_windows_vorgabe_liegt_beim_nutzer(monkeypatch):
|
def test_windows_vorgabe_liegt_beim_nutzer(monkeypatch):
|
||||||
monkeypatch.setenv("USERPROFILE", "C:\\Users\\Test")
|
monkeypatch.setenv("USERPROFILE", "C:\\Users\\Test")
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert a["ablage_vorgabe"].startswith("C:\\Users\\Test")
|
assert a["ablage_vorgabe"].startswith("C:\\Users\\Test")
|
||||||
assert a["ablage_vorgabe"].endswith("Rippy")
|
assert a["ablage_vorgabe"].endswith("Rippy")
|
||||||
|
|
||||||
|
|
||||||
# ── Der Vertrag als Ganzes ──────────────────────────────────────────────
|
# ── Der Vertrag als Ganzes ──────────────────────────────────────────────
|
||||||
def test_die_auskunft_ist_vollstaendig():
|
def test_die_auskunft_ist_vollstaendig():
|
||||||
"""Ein fehlendes Feld waere im UI ein `undefined` — und `undefined` ist
|
"""Ein fehlendes Feld waere im UI ein `undefined` — und `undefined` ist
|
||||||
in JavaScript falsch, also verschwaende die Oberflaeche stillschweigend
|
in JavaScript falsch, also verschwaende die Oberflaeche stillschweigend
|
||||||
einen Bereich. Lieber hier rot."""
|
einen Bereich. Lieber hier rot."""
|
||||||
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
a = betrieb.auskunft(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert set(a) == {"modus", "plattform", "im_container", "kann",
|
assert set(a) == {"modus", "plattform", "im_container", "kann",
|
||||||
"ablage_vorgabe", "arbeits_vorgabe", "hilfe_befehl"}
|
"ablage_vorgabe", "arbeits_vorgabe", "hilfe_befehl"}
|
||||||
assert set(a["kann"]) == {"externe_worker", "freigaben_einhaengen",
|
assert set(a["kann"]) == {"externe_worker", "freigaben_einhaengen",
|
||||||
"container_pfade", "werkzeuge_verwalten",
|
"container_pfade", "werkzeuge_verwalten",
|
||||||
"frei_blaettern"}
|
"frei_blaettern"}
|
||||||
assert all(isinstance(v, bool) for v in a["kann"].values())
|
assert all(isinstance(v, bool) for v in a["kann"].values())
|
||||||
|
|
||||||
|
|
||||||
# ── Wo Platz gemessen wird ──────────────────────────────────────────────
|
# ── Wo Platz gemessen wird ──────────────────────────────────────────────
|
||||||
def test_windows_misst_NICHT_in_app_media():
|
def test_windows_misst_NICHT_in_app_media():
|
||||||
"""DER Befund: Im Windows-Fenster stand „Platz fuer Rippy: unbekannt".
|
"""DER Befund: Im Windows-Fenster stand „Platz fuer Rippy: unbekannt".
|
||||||
|
|
||||||
`main.py` mass fest an `/app/media` und `/app/temp`. Beide gibt es unter
|
`main.py` mass fest an `/app/media` und `/app/temp`. Beide gibt es unter
|
||||||
Windows nicht, `shutil.disk_usage` warf, die Liste blieb leer — und aus
|
Windows nicht, `shutil.disk_usage` warf, die Liste blieb leer — und aus
|
||||||
einer Nichtauskunft wurde eine Anzeige, die wie eine Auskunft aussieht.
|
einer Nichtauskunft wurde eine Anzeige, die wie eine Auskunft aussieht.
|
||||||
"""
|
"""
|
||||||
orte = betrieb.platz_orte(WINDOWS, container=False, plattform_name="win32")
|
orte = betrieb.platz_orte(WINDOWS, container=False, plattform_name="win32")
|
||||||
assert orte, "es muss mindestens ein Ort gemessen werden"
|
assert orte, "es muss mindestens ein Ort gemessen werden"
|
||||||
assert all("/app/" not in o["pfad"] for o in orte), orte
|
assert all("/app/" not in o["pfad"] for o in orte), orte
|
||||||
|
|
||||||
|
|
||||||
def test_docker_misst_weiter_die_container_pfade():
|
def test_docker_misst_weiter_die_container_pfade():
|
||||||
orte = betrieb.platz_orte(DOCKER, container=True, plattform_name="linux")
|
orte = betrieb.platz_orte(DOCKER, container=True, plattform_name="linux")
|
||||||
assert [o["pfad"] for o in orte] == ["/app/media", "/app/temp"]
|
assert [o["pfad"] for o in orte] == ["/app/media", "/app/temp"]
|
||||||
|
|
||||||
|
|
||||||
def test_gleiches_laufwerk_wird_nicht_doppelt_gezaehlt():
|
def test_gleiches_laufwerk_wird_nicht_doppelt_gezaehlt():
|
||||||
"""Zweimal dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskuenfte
|
"""Zweimal dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskuenfte
|
||||||
und ist eine."""
|
und ist eine."""
|
||||||
orte = betrieb.platz_orte({"storage": {"medien": r"D:\Filme"}},
|
orte = betrieb.platz_orte({"storage": {"medien": r"D:\Filme"}},
|
||||||
container=False, plattform_name="win32")
|
container=False, plattform_name="win32")
|
||||||
assert len(orte) == 1, orte
|
assert len(orte) == 1, orte
|
||||||
|
|
||||||
|
|
||||||
def test_zweites_laufwerk_bekommt_eine_eigene_zeile():
|
def test_zweites_laufwerk_bekommt_eine_eigene_zeile():
|
||||||
orte = betrieb.platz_orte(
|
orte = betrieb.platz_orte(
|
||||||
{"storage": {"medien": r"D:\Filme", "temp": r"E:\Arbeit"}},
|
{"storage": {"medien": r"D:\Filme", "temp": r"E:\Arbeit"}},
|
||||||
container=False, plattform_name="win32")
|
container=False, plattform_name="win32")
|
||||||
assert len(orte) == 2, orte
|
assert len(orte) == 2, orte
|
||||||
|
|
||||||
|
|
||||||
def test_noch_nicht_angelegter_ordner_faellt_auf_das_laufwerk_zurueck():
|
def test_noch_nicht_angelegter_ordner_faellt_auf_das_laufwerk_zurueck():
|
||||||
"""Frisch installiert gibt es den Ablage-Ordner noch nicht. Der Nutzer
|
"""Frisch installiert gibt es den Ablage-Ordner noch nicht. Der Nutzer
|
||||||
will trotzdem wissen, ob auf dem LAUFWERK Platz ist."""
|
will trotzdem wissen, ob auf dem LAUFWERK Platz ist."""
|
||||||
da = r"C:\Users\Test"
|
da = r"C:\Users\Test"
|
||||||
assert betrieb.naechster_vorhandener(
|
assert betrieb.naechster_vorhandener(
|
||||||
r"C:\Users\Test\Videos\Rippy", existiert=lambda p: p == da) == da
|
r"C:\Users\Test\Videos\Rippy", existiert=lambda p: p == da) == da
|
||||||
|
|
||||||
|
|
||||||
def test_wenn_gar_nichts_existiert_wird_nichts_behauptet():
|
def test_wenn_gar_nichts_existiert_wird_nichts_behauptet():
|
||||||
assert betrieb.naechster_vorhandener(r"Z:\gibt\es\nicht",
|
assert betrieb.naechster_vorhandener(r"Z:\gibt\es\nicht",
|
||||||
existiert=lambda p: False) == ""
|
existiert=lambda p: False) == ""
|
||||||
|
|||||||
@@ -238,11 +238,12 @@ def test_vorgaben_sind_vollstaendig():
|
|||||||
import pytest
|
import pytest
|
||||||
pytest.skip("Registry-Vorgaben gibt es nur unter Windows")
|
pytest.skip("Registry-Vorgaben gibt es nur unter Windows")
|
||||||
v = e.vorgaben()
|
v = e.vorgaben()
|
||||||
assert set(v) == {"ziel", "ablage", "port", "autostart",
|
assert set(v) == {"ziel", "ablage", "arbeit", "port", "autostart",
|
||||||
"verknuepfungen", "werkzeuge_holen"}
|
"verknuepfungen", "werkzeuge_holen"}
|
||||||
assert v["port"] == 7788
|
assert v["port"] == 7788
|
||||||
assert v["ziel"] and v["ablage"]
|
assert v["ziel"] and v["ablage"] and v["arbeit"]
|
||||||
assert "/app/" not in v["ablage"], "Container-Pfad in einer Windows-Vorgabe"
|
for feld in ("ablage", "arbeit"):
|
||||||
|
assert "/app/" not in v[feld], "Container-Pfad in einer Windows-Vorgabe"
|
||||||
|
|
||||||
|
|
||||||
def test_netzwerkpfad_als_administrator_wird_erklaert():
|
def test_netzwerkpfad_als_administrator_wird_erklaert():
|
||||||
|
|||||||
@@ -284,3 +284,96 @@ def test_updates_melden_was_offen_ist(monkeypatch):
|
|||||||
assert [o["titel"] for o in stand["offen"]] == ["MakeMKV"]
|
assert [o["titel"] for o in stand["offen"]] == ["MakeMKV"]
|
||||||
assert stand["offen"][0]["installiert"] == "1.18.2"
|
assert stand["offen"][0]["installiert"] == "1.18.2"
|
||||||
assert stand["offen"][0]["neueste"] == "1.18.4"
|
assert stand["offen"][0]["neueste"] == "1.18.4"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Der Arbeitsordner im Setup (Commander 28.08.2026) ───────────────────
|
||||||
|
#
|
||||||
|
# > „kannst du noch einbauen das man den arbeitsordner usw. in den
|
||||||
|
# > Einstellungen setzen kann und direkt im setup?"
|
||||||
|
#
|
||||||
|
# Warum das nicht Kosmetik ist: Der Roh-Rip einer 4K-UHD ist bis zu 100 GB
|
||||||
|
# gross. Wer erst NACH der ersten vollen Platte erfaehrt, wo die liegen, hat
|
||||||
|
# es zu spaet erfahren — am 25.07.2026 lief genau so die VM-Platte voll.
|
||||||
|
|
||||||
|
|
||||||
|
def test_die_seite_fragt_nach_dem_arbeitsordner():
|
||||||
|
assert setup_fenster.seite_enthaelt('id="arbeit"')
|
||||||
|
assert setup_fenster.seite_enthaelt("Arbeitsordner für die Rohdaten")
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_arbeitsordner_hat_einen_durchsuchen_knopf():
|
||||||
|
"""Einen Pfad abzutippen ist die Sorte Bedienung, die niemand macht."""
|
||||||
|
assert setup_fenster.seite_enthaelt("waehlen('arbeit')")
|
||||||
|
|
||||||
|
|
||||||
|
def test_jede_vorgabe_hat_ein_feld_UND_wird_mitgeschickt():
|
||||||
|
"""Der Waechter gegen halb angeschlossene Felder.
|
||||||
|
|
||||||
|
Ein Feld kann auf drei Arten kaputt sein, und alle drei sehen im Fenster
|
||||||
|
gleich aus: kein Eingabefeld, keine Vorbelegung, oder es wird beim
|
||||||
|
Installieren nicht mitgeschickt. Der dritte Fall ist der gemeinste — man
|
||||||
|
waehlt etwas, es passiert nichts, und niemand sagt warum.
|
||||||
|
"""
|
||||||
|
from rippy import einrichtung
|
||||||
|
|
||||||
|
# Schalter heissen im HTML anders (checkbox) und werden separat geprueft.
|
||||||
|
pfade_und_zahlen = {"ziel", "ablage", "arbeit", "port"}
|
||||||
|
for name in pfade_und_zahlen:
|
||||||
|
assert name in einrichtung.vorgaben(), "%s fehlt in vorgaben()" % name
|
||||||
|
assert setup_fenster.seite_enthaelt('id="%s"' % name), \
|
||||||
|
"kein Eingabefeld fuer %s" % name
|
||||||
|
assert setup_fenster.seite_enthaelt("$('%s').value = v.%s" % (name, name)), \
|
||||||
|
"%s wird nicht vorbelegt" % name
|
||||||
|
assert setup_fenster.seite_enthaelt("%s: " % name), \
|
||||||
|
"%s wird beim Installieren nicht mitgeschickt" % name
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_arbeitsordner_landet_als_workDir_in_den_einstellungen(monkeypatch):
|
||||||
|
"""Der Name muss stimmen: Die Oberflaeche und der Worker lesen `workDir`."""
|
||||||
|
geschrieben = {}
|
||||||
|
monkeypatch.setattr(setup_fenster, "_einstellungen_schreiben",
|
||||||
|
lambda neu: geschrieben.update(neu))
|
||||||
|
monkeypatch.setattr(setup_fenster, "_beta_key_holen", lambda *a: "")
|
||||||
|
|
||||||
|
class StilleWerkzeuge:
|
||||||
|
@staticmethod
|
||||||
|
def sicherstellen(**k):
|
||||||
|
return {"bereit": True}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bericht(b):
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
modul = types.ModuleType("rippy.tools.einrichten")
|
||||||
|
modul.sicherstellen = StilleWerkzeuge.sicherstellen
|
||||||
|
modul.bericht = StilleWerkzeuge.bericht
|
||||||
|
monkeypatch.setitem(sys.modules, "rippy.tools.einrichten", modul)
|
||||||
|
|
||||||
|
from rippy import windows_app
|
||||||
|
monkeypatch.setattr(windows_app, "installieren", lambda **k: r"C:\Rippy")
|
||||||
|
|
||||||
|
b = setup_fenster.Bruecke()
|
||||||
|
b._installieren_wirklich({"ablage": r"D:\Filme", "arbeit": r"E:\Roh",
|
||||||
|
"werkzeuge_holen": True})
|
||||||
|
|
||||||
|
assert geschrieben.get("workDir") == r"E:\Roh"
|
||||||
|
assert geschrieben.get("outputDir") == r"D:\Filme"
|
||||||
|
assert b.ergebnis["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohne_angabe_wird_NICHTS_geschrieben(monkeypatch):
|
||||||
|
"""Leer heisst „nimm die Vorgabe" — dann darf kein leerer Pfad in die
|
||||||
|
Einstellungen wandern und die Berechnung ueberschreiben."""
|
||||||
|
geschrieben = {}
|
||||||
|
monkeypatch.setattr(setup_fenster, "_einstellungen_schreiben",
|
||||||
|
lambda neu: geschrieben.update(neu))
|
||||||
|
monkeypatch.setattr(setup_fenster, "_beta_key_holen", lambda *a: "")
|
||||||
|
from rippy import windows_app
|
||||||
|
monkeypatch.setattr(windows_app, "installieren", lambda **k: r"C:\Rippy")
|
||||||
|
|
||||||
|
b = setup_fenster.Bruecke()
|
||||||
|
b._installieren_wirklich({"ablage": "", "arbeit": "", "werkzeuge_holen": False})
|
||||||
|
assert geschrieben == {}
|
||||||
|
|||||||
Reference in New Issue
Block a user