35cfcbcb07
Ampel / ampel (push) Successful in 27s
Commander-Rueckmeldung: Umlaute fehlen. Ausloeser war sichtbar der Installer -
"Diese Maschine uebernimmt die Video-Kompression fuer Rippy" stand woertlich im
Screenshot.
## Die .ps1-Falle war loesbar, nicht unumgehbar
Bisher galt: ausgelieferte .ps1 MUESSEN ASCII sein, weil PowerShell 5.1 sie
ohne BOM als ANSI liest. Das ist nur die halbe Wahrheit - gemessen mit echtem
powershell.exe 5.1:
ohne BOM: $s = "Größe: äöü" + Unerwartetes Token -> Skript kaputt
mit BOM: Groesse: aeoeue + laeuft, Length 16 korrekt
Alle drei Skripte sind jetzt UTF-8 MIT BOM und tragen echte Umlaute; unter 5.1
gegengeprueft (BOM vorhanden, Parser fehlerfrei, Text korrekt gelesen). Der
Kopfkommentar sagt das jetzt richtig statt "ASCII-only".
## Umstellung: Text ja, Bezeichner nein
Umlaute gehoeren in Kommentare und Anzeigetexte, nicht in Funktionsnamen oder
Datenschluessel. Deshalb je Sprache das passende Werkzeug:
- Python: ueber den TOKENIZER - angefasst wurden ausschliesslich COMMENT- und
STRING-Tokens. 156 Stellen. Code ist damit garantiert unberuehrt.
- TypeScript: nur // und /* */ Kommentare sowie JSX-Text (kann per Definition
kein Bezeichner sein). 24 Stellen. `const waehlen`, `let laeuft`, `plaetze`,
`GeraetInfo` sind nachweislich unversehrt.
- install.sh: Anzeigetext, aber die Shell-Funktionen (gruen/rot/gelb/titel) und
der Schalter --nur-pruefen bleiben ASCII - das sind Schnittstellen.
- Markdown: 0 Aenderungen, die Doku hatte schon Umlaute.
ZWEI FEHLER MEINES KONVERTERS, beide von Werkzeugen gefangen:
1. In f-Strings steht in {...} CODE, kein Text. Aus f"{groesse}" wurde
f"{größe}", waehrend die Variable groesse hiess - Ruff meldete F821
"Undefined name". Der Konverter lagert Einsetzungen jetzt aus.
2. Ein Dict-Schluessel wurde umbenannt: die Wortliste enthaelt das PRAEFIX
"uebersprung", der Tabu-Schutz prueft aber ganze Woerter. bericht[...] ist
wieder ASCII - Umlaute in Datenschluesseln brechen JSON-Runden und DB-Felder.
## Installer im Rippy-Look
Statt hellgrau jetzt dieselben Toene wie das Web-UI (aus lib/design.ts
uebernommen): slate-900 Flaeche, dunkle Eingabefelder, Amber-Hauptknopf wie
"Los geht's" im Wizard. Oben ein Kopfbereich mit dem Farbverlauf
amber -> indigo -> purple und dem Disc-Symbol - in WinForms per Paint-Ereignis
gezeichnet, weil es dort keine Verlaeufe von der Stange gibt.
Geprueft, nicht gehofft: Das Fenster wurde headless in ein PNG gerendert
(DrawToBitmap) und angesehen - Verlauf, Symbol, Umlaute und Farben sitzen.
RippyWorkerSetup.exe neu gebaut (57344 -> 60416 Bytes); Umlaute und das
requireAdministrator-Manifest sind in der .exe verifiziert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
176 lines
7.0 KiB
Python
176 lines
7.0 KiB
Python
"""Tests für die Media-Server-Aufbereitung (Benennung, NFO, Serien-Matching)."""
|
|
|
|
import os
|
|
|
|
from medien import baue_nfo, matche_episoden, serien_ordner, sicherer_name, zielordner
|
|
|
|
|
|
def test_sicherer_name_entfernt_verbotene_zeichen():
|
|
assert sicherer_name('Alien: Die Wiedergeburt') == "Alien Die Wiedergeburt"
|
|
assert sicherer_name('Was/ist\\das?<>|*"') == "Wasistdas"
|
|
|
|
|
|
def test_sicherer_name_mit_jahr():
|
|
assert sicherer_name("Evangelion 2.22", 2009) == "Evangelion 2.22 (2009)"
|
|
|
|
|
|
def test_sicherer_name_kollabiert_leerraum_und_punkte():
|
|
assert sicherer_name(" Viel Raum ... ") == "Viel Raum"
|
|
|
|
|
|
def test_sicherer_name_leer_bleibt_leer():
|
|
assert sicherer_name("") == ""
|
|
assert sicherer_name("???") == ""
|
|
|
|
|
|
def test_zielordner_nutzt_titel_und_jahr(tmp_path):
|
|
pfad = zielordner(str(tmp_path), "Inception", 2010, "abc-123")
|
|
assert pfad == os.path.join(str(tmp_path), "Inception (2010)")
|
|
|
|
|
|
def test_zielordner_faellt_auf_job_id_zurueck(tmp_path):
|
|
pfad = zielordner(str(tmp_path), "", None, "abc-123")
|
|
assert pfad == os.path.join(str(tmp_path), "abc-123")
|
|
|
|
|
|
def test_zielordner_weicht_bei_kollision_aus(tmp_path):
|
|
os.makedirs(tmp_path / "Inception (2010)")
|
|
pfad = zielordner(str(tmp_path), "Inception", 2010, "abcdef12-3456")
|
|
assert pfad == os.path.join(str(tmp_path), "Inception (2010) [abcdef12]")
|
|
|
|
|
|
def test_baue_nfo_film_mit_plot_und_genres():
|
|
nfo = baue_nfo(
|
|
{"overview": "Ein Traum <im> Traum & so", "genres": ["Sci-Fi", "Action"]},
|
|
"Inception", 2010,
|
|
)
|
|
assert "<movie>" in nfo and "</movie>" in nfo
|
|
assert "<title>Inception</title>" in nfo
|
|
assert "<year>2010</year>" in nfo
|
|
# XML-Escaping: <, >, & dürfen den Parser nicht sprengen
|
|
assert "Ein Traum <im> Traum & so" in nfo
|
|
assert "<genre>Sci-Fi</genre>" in nfo
|
|
|
|
|
|
def test_baue_nfo_serie_bekommt_tvshow_wurzel():
|
|
nfo = baue_nfo({"type": "tv"}, "Neon Genesis Evangelion", 1995)
|
|
assert "<tvshow>" in nfo and "</tvshow>" in nfo
|
|
|
|
|
|
def test_sicherer_name_haengt_jahr_nicht_doppelt_an():
|
|
"""OMDb-Titel tragen das Jahr teils schon — kein 'X (2009) (2009)' mehr."""
|
|
assert sicherer_name("Summer Wars (2009)", 2009) == "Summer Wars (2009)"
|
|
assert sicherer_name("Summer Wars", 2009) == "Summer Wars (2009)"
|
|
|
|
|
|
def test_serien_ordner_jellyfin_schema():
|
|
assert serien_ordner("/app/media/series", "Neon Genesis: Evangelion", 2) == os.path.join(
|
|
"/app/media/series", "Neon Genesis Evangelion", "Season 02"
|
|
)
|
|
|
|
|
|
def test_matche_episoden_komplette_staffel_auch_bei_uniformen_laufzeiten():
|
|
# Anime-Standardfall: ganze Staffel auf der Disc, alle Episoden ~24 min
|
|
dateien = [1440, 1452, 1431]
|
|
episoden = [(1, 1440), (2, 1440), (3, 1440)]
|
|
assert matche_episoden(dateien, episoden) == [1, 2, 3]
|
|
|
|
|
|
def test_matche_episoden_teil_disc_mit_distinkten_laufzeiten():
|
|
# Disc 2 einer Staffel: Laufzeiten unterscheiden sich klar → zuordenbar
|
|
dateien = [1440, 2650]
|
|
episoden = [(1, 700), (2, 1440), (3, 2650)]
|
|
assert matche_episoden(dateien, episoden) == [2, 3]
|
|
|
|
|
|
def test_matche_episoden_haelt_reihenfolge():
|
|
# Datei 2 ist ein Doppellänger → muss auf die SPÄTERE lange Episode fallen
|
|
dateien = [1440, 2880]
|
|
episoden = [(1, 1440), (2, 2880)]
|
|
assert matche_episoden(dateien, episoden) == [1, 2]
|
|
|
|
|
|
def test_matche_episoden_mehrdeutig_gibt_none():
|
|
# Alle Episoden gleich lang und mehr Kandidaten als Dateien in Toleranz →
|
|
# Datei 1 passt auf Ep 1 UND Ep 2 gleich gut → keine eindeutige Wahl
|
|
dateien = [1440]
|
|
episoden = [(1, 1440), (2, 1440)]
|
|
assert matche_episoden(dateien, episoden) is None
|
|
|
|
|
|
def test_matche_episoden_ohne_treffer_gibt_none():
|
|
dateien = [600] # 10 min passt auf keine 24-min-Episode
|
|
episoden = [(1, 1440), (2, 1440)]
|
|
assert matche_episoden(dateien, episoden) is None
|
|
|
|
|
|
def test_pfad_lokal_uebersetzt_fuer_windows_worker():
|
|
from tasks import pfad_lokal
|
|
|
|
mapping = "/app/media=Z:\\media;/app/temp=Y:\\temp"
|
|
assert pfad_lokal("/app/temp/raw/abc", mapping) == "Y:\\temp\\raw\\abc"
|
|
assert pfad_lokal("/app/media/movies/Film (2020)", mapping) == "Z:\\media\\movies\\Film (2020)"
|
|
assert pfad_lokal("/woanders/x", mapping) == "/woanders/x"
|
|
assert pfad_lokal("/app/temp/raw/abc", "") == "/app/temp/raw/abc"
|
|
|
|
|
|
# Die beiden früheren Tests zu _original_aufheben sind nach
|
|
# test_original_aufheben.py gewandert und dort erweitert. Sie prüfen jetzt das
|
|
# richtige Kriterium: Vorher taeuschten sie per gefaelschtem os.stat
|
|
# „verschiedene Dateisysteme" vor — genau die Annahme, an der der Schutz in der
|
|
# Praxis scheiterte (st_dev war identisch, os.rename trotzdem EXDEV).
|
|
|
|
|
|
def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung():
|
|
"""Pro Rip wählbar (Commander 25.07.2026), Einstellung bleibt Standard.
|
|
|
|
Reihenfolge: Wahl dieses Rips -> Setting -> Container-Default. Der
|
|
Setting-Wert ist genau der, der bei Vollautomatik-Rips greift, weil dort
|
|
niemand gefragt wird.
|
|
"""
|
|
import tasks
|
|
|
|
einst = {"workDir": "/app/media/movies"}
|
|
assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy") == "/app/media/rippy"
|
|
assert tasks._arbeitsverzeichnis(einst) == "/app/media/movies"
|
|
assert tasks._arbeitsverzeichnis({}) == tasks.RAW_DIR
|
|
# Ausbruchsversuche und Pfade außerhalb /app/media fallen durch
|
|
assert tasks._arbeitsverzeichnis({}, "/etc") == tasks.RAW_DIR
|
|
assert tasks._arbeitsverzeichnis({}, "/app/media/../etc") == tasks.RAW_DIR
|
|
# Leere Wahl fällt sauber auf die Einstellung zurück
|
|
assert tasks._arbeitsverzeichnis(einst, " ") == "/app/media/movies"
|
|
|
|
|
|
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
|
"""Befund 25.07.2026: Elf Stellen prüften mit nacktem startswith().
|
|
„/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb."""
|
|
import tasks
|
|
|
|
assert tasks.unter_wurzel("/app/media", "/app/media") is True
|
|
assert tasks.unter_wurzel("/app/media/movies", "/app/media") is True
|
|
assert tasks.unter_wurzel("/app/media/movies/Film (2020)", "/app/media") is True
|
|
# Der Fall, den startswith() durchgelassen haette
|
|
assert tasks.unter_wurzel("/app/media-boese/x", "/app/media") is False
|
|
assert tasks.unter_wurzel("/app/mediaX", "/app/media") is False
|
|
assert tasks.unter_wurzel("/etc/passwd", "/app/media") is False
|
|
assert tasks.unter_wurzel("", "/app/media") is False
|
|
assert tasks.unter_wurzel("/app/media", "") is False
|
|
# Wurzel mit Schrägstrich am Ende darf nichts ändern
|
|
assert tasks.unter_wurzel("/app/media/movies", "/app/media/") is True
|
|
|
|
|
|
def test_zielbasis_lehnt_praefix_ausbruch_ab():
|
|
import tasks
|
|
|
|
assert tasks._zielbasis("/app/media/movies", "bluray") == "/app/media/movies"
|
|
# Ausbruch per Praefix-Namen fällt auf den Standard zurück
|
|
assert tasks._zielbasis("/app/media-boese", "bluray") != "/app/media-boese"
|
|
assert tasks._zielbasis("/etc", "bluray") != "/etc"
|
|
|
|
|
|
def test_arbeitsverzeichnis_lehnt_praefix_ausbruch_ab():
|
|
import tasks
|
|
|
|
assert tasks._arbeitsverzeichnis({}, "/app/media-boese") == tasks.RAW_DIR
|
|
assert tasks._arbeitsverzeichnis({"workDir": "/app/mediaX"}) == tasks.RAW_DIR
|