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"]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -221,10 +221,18 @@ class Bruecke:
|
|||||||
starten_danach=False,
|
starten_danach=False,
|
||||||
verknuepfen=bool(optionen.get("verknuepfungen", True)),
|
verknuepfen=bool(optionen.get("verknuepfungen", True)),
|
||||||
)
|
)
|
||||||
|
orte = {}
|
||||||
if optionen.get("ablage"):
|
if optionen.get("ablage"):
|
||||||
|
orte["outputDir"] = optionen["ablage"]
|
||||||
|
# Der Arbeitsordner gehoert mit ins Setup (Commander 28.08.2026).
|
||||||
|
# Leer heisst „nimm die Vorgabe" — dann steht hier nichts, und der
|
||||||
|
# Betrieb rechnet ihn sich neben der Ablage aus.
|
||||||
|
if optionen.get("arbeit"):
|
||||||
|
orte["workDir"] = optionen["arbeit"]
|
||||||
|
if orte:
|
||||||
self._lauf.beginne("ablage")
|
self._lauf.beginne("ablage")
|
||||||
melden("Ablage wird gespeichert …")
|
melden("Ordner werden gespeichert …")
|
||||||
_ablage_speichern(optionen["ablage"])
|
_einstellungen_schreiben(orte)
|
||||||
self._lauf.beginne("werkzeuge")
|
self._lauf.beginne("werkzeuge")
|
||||||
if optionen.get("werkzeuge_holen", True):
|
if optionen.get("werkzeuge_holen", True):
|
||||||
melden("Werkzeuge werden geprüft …")
|
melden("Werkzeuge werden geprüft …")
|
||||||
@@ -235,6 +243,7 @@ class Bruecke:
|
|||||||
aktualisieren=bool(optionen.get("aktualisieren", False)))
|
aktualisieren=bool(optionen.get("aktualisieren", False)))
|
||||||
melden(werkzeug_einrichtung.bericht(bericht))
|
melden(werkzeug_einrichtung.bericht(bericht))
|
||||||
bereit = bericht["bereit"]
|
bereit = bericht["bereit"]
|
||||||
|
_beta_key_holen(melden)
|
||||||
else:
|
else:
|
||||||
bereit = True
|
bereit = True
|
||||||
self._lauf.beginne("fertig")
|
self._lauf.beginne("fertig")
|
||||||
@@ -299,16 +308,73 @@ def _ablage_speichern(pfad: str) -> None:
|
|||||||
Der Store wird direkt benutzt. Ein Umweg über den Suchpfad der API war
|
Der Store wird direkt benutzt. Ein Umweg über den Suchpfad der API war
|
||||||
ohnehin überflüssig.
|
ohnehin überflüssig.
|
||||||
"""
|
"""
|
||||||
|
_einstellungen_schreiben({"outputDir": pfad})
|
||||||
|
|
||||||
|
|
||||||
|
def _einstellungen_schreiben(neu: dict) -> None:
|
||||||
|
"""Werte in die Einstellungen mischen — lesen, setzen, zurückschreiben.
|
||||||
|
|
||||||
|
`save_settings` überschreibt das GANZE JSON, deshalb erst lesen.
|
||||||
|
"""
|
||||||
from rippy import config, store
|
from rippy import config, store
|
||||||
|
|
||||||
werte = config.laden()
|
werte = config.laden()
|
||||||
store.verbinden(config.datenbank_url(werte))
|
store.verbinden(config.datenbank_url(werte))
|
||||||
store.init_db()
|
store.init_db()
|
||||||
aktuell = store.get_settings() or {}
|
aktuell = store.get_settings() or {}
|
||||||
aktuell["outputDir"] = pfad
|
aktuell.update(neu)
|
||||||
store.save_settings(aktuell)
|
store.save_settings(aktuell)
|
||||||
|
|
||||||
|
|
||||||
|
def _beta_key_holen(melden=None) -> str:
|
||||||
|
"""Den kostenlosen MakeMKV-Beta-Key aus dem Forum holen und ablegen.
|
||||||
|
|
||||||
|
## Warum das ins Setup gehört (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 hat recht, es kann: `docker/api/makemkv_key.py` holt den Key aus dem
|
||||||
|
Forum-Thread t=1053 und läuft seit jeher täglich mit. Nachgemessen am
|
||||||
|
28.08.2026: Antwort in 3,3 Sekunden, gültiger Key mit 62 Zeichen.
|
||||||
|
|
||||||
|
Nur half das beim ERSTEN Rip nicht: Die Schleife startet 60 Sekunden nach
|
||||||
|
dem Serverstart. Wer gleich nach dem Einrichten eine Blu-ray einlegt, rippt
|
||||||
|
ohne Key — und MakeMKV fällt in den 30-Tage-Testmodus.
|
||||||
|
|
||||||
|
Ohne Key ist Rippy nicht kaputt; DVDs gehen weiterhin. Deshalb ist ein
|
||||||
|
Fehlschlag hier auch kein Abbruch, sondern eine Zeile im Protokoll.
|
||||||
|
|
||||||
|
Rechtlich unverändert: Das ist die öffentliche Beta-LIZENZ der Software,
|
||||||
|
NICHT ein Disc-Schlüssel. Rippy liefert und verteilt keine Disc-Schlüssel.
|
||||||
|
"""
|
||||||
|
melden = melden or (lambda *a, **k: None)
|
||||||
|
try:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from rippy.daemon import _api_pfad
|
||||||
|
|
||||||
|
# `_api_pfad` gibt den Pfad NUR zurueck — eingehaengt wird er in
|
||||||
|
# daemon.starten(). Hier laeuft der Assistent, nicht der Dienst.
|
||||||
|
pfad = _api_pfad()
|
||||||
|
if pfad and pfad not in sys.path:
|
||||||
|
sys.path.insert(0, pfad)
|
||||||
|
import makemkv_key
|
||||||
|
|
||||||
|
key = makemkv_key.fetch_current_key()
|
||||||
|
except Exception as ex: # noqa: BLE001
|
||||||
|
melden(" Beta-Key nicht erreichbar (%s) — nachtragbar unter "
|
||||||
|
"Einstellungen." % type(ex).__name__)
|
||||||
|
return ""
|
||||||
|
if not key:
|
||||||
|
melden(" Kein Beta-Key im Forum gefunden — nachtragbar unter "
|
||||||
|
"Einstellungen.")
|
||||||
|
return ""
|
||||||
|
_einstellungen_schreiben({"makemkvAppKey": key})
|
||||||
|
melden(" MakeMKV-Beta-Key geholt (…%s)." % key[-6:])
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
def starten() -> dict:
|
def starten() -> dict:
|
||||||
"""Öffnet den Assistenten. Gibt zurück, was daraus geworden ist.
|
"""Öffnet den Assistenten. Gibt zurück, was daraus geworden ist.
|
||||||
|
|
||||||
@@ -426,6 +492,15 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
<button onclick="waehlen('ablage')">Durchsuchen …</button>
|
<button onclick="waehlen('ablage')">Durchsuchen …</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="feld"><span>Arbeitsordner für die Rohdaten
|
||||||
|
<div class="hinweis">Hier liegt die Disc zwischendurch am Stück — bei einer
|
||||||
|
4K-UHD bis zu 100 GB. Am besten dasselbe Laufwerk wie die Ablage: dann muss
|
||||||
|
Rippy die fertige Datei am Ende nur umhängen statt sie zu kopieren.</div></span>
|
||||||
|
<div class="mitknopf">
|
||||||
|
<input type="text" id="arbeit">
|
||||||
|
<button onclick="waehlen('arbeit')">Durchsuchen …</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<label class="feld" style="max-width:190px"><span>Port</span>
|
<label class="feld" style="max-width:190px"><span>Port</span>
|
||||||
<input type="number" id="port" min="1024" max="65535">
|
<input type="number" id="port" min="1024" max="65535">
|
||||||
</label>
|
</label>
|
||||||
@@ -532,6 +607,7 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
await pywebview.api.installieren({
|
await pywebview.api.installieren({
|
||||||
ziel: $('ziel').value,
|
ziel: $('ziel').value,
|
||||||
ablage: $('ablage').value,
|
ablage: $('ablage').value,
|
||||||
|
arbeit: $('arbeit').value,
|
||||||
port: Number($('port').value),
|
port: Number($('port').value),
|
||||||
autostart: $('autostart').checked,
|
autostart: $('autostart').checked,
|
||||||
verknuepfungen: $('verknuepfungen').checked,
|
verknuepfungen: $('verknuepfungen').checked,
|
||||||
@@ -578,6 +654,7 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
const v = await pywebview.api.vorgaben();
|
const v = await pywebview.api.vorgaben();
|
||||||
$('ziel').value = v.ziel;
|
$('ziel').value = v.ziel;
|
||||||
$('ablage').value = v.ablage;
|
$('ablage').value = v.ablage;
|
||||||
|
$('arbeit').value = v.arbeit;
|
||||||
$('port').value = v.port;
|
$('port').value = v.port;
|
||||||
$('autostart').checked = v.autostart;
|
$('autostart').checked = v.autostart;
|
||||||
$('verknuepfungen').checked = v.verknuepfungen;
|
$('verknuepfungen').checked = v.verknuepfungen;
|
||||||
|
|||||||
@@ -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