Files
rippy/docker/worker/test_medien.py
T
HitonabiandClaude Opus 5 423374e65a fix(windows): Roh- und Zielordner landeten in einem Ordner namens „app"
Beim Nachstellen des leeren Bildschirms lief ein echter Test-Rip durch. Die
Rohdaten landeten in:

    F:\app\temp\raw\<job>\Evangelion 2.22_t00.mkv   (436 MB)

Also in einem Ordner namens `app` auf dem Laufwerk, von dem Rippy gerade lief.

## Zwei Container-Wurzeln im Worker

    RAW_DIR    = /app/temp/raw
    MEDIA_ROOT = /app/media

Unter Windows sind das keine Pfade, sondern Unfaelle. Schlimmer: Die Pruefung
`unter_wurzel(wahl, MEDIA_ROOT)` verwarf auch eine AUSDRUECKLICHE Wahl — ein
Arbeitsordner wie `D:\Roh` liegt nicht unter `/app/media`, also fiel er still
auf den Container-Standard zurueck.

**Damit kam der Arbeitsordner, den der Commander am 28.08.2026 ausdruecklich
bestellt hat, unter Windows nie an.** Der Dialog zeigte ihn, das Setzen ging,
und der Worker ignorierte ihn — ohne ein Wort. Dasselbe galt fuer das Ziel:
Eine UNC-Freigabe liegt unter gar keiner lokalen Wurzel, also waere die
fertige Datei in `X:\app\media\bluray` gelandet.

## Die Wurzeln kommen jetzt aus dem Betrieb

Im Container aendert sich NICHTS: dort ist `/app/media` die Wurzel und `frei`
falsch. Nativ zaehlt die Wahl des Nutzers — dort IST sein Laufwerk die Grenze.

Die drei bestehenden Tests wurden rot, und zwar zu Recht: Sie pruefen die
Container-Regel, liefen aber unter Windows, wo `frei` gilt. Die Wurzeln sind
deshalb einspritzbar — beide Betriebsfaelle sind jetzt auf jedem Rechner
pruefbar, statt vom laufenden abzuhaengen.

837 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 14:15:17 +02:00

236 lines
9.4 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 &lt;im&gt; Traum &amp; 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 ablauf 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).
# Die Wurzeln je Betrieb — eingespritzt, damit BEIDE Faelle ueberall pruefbar
# sind. Vorher hingen diese Tests am laufenden Rechner: Unter Windows ist
# `frei` wahr, und die Container-Regeln galten dort nicht mehr (am 29.08.2026
# prompt rot geworden).
CONTAINER = ("/app/media", "/app/temp/raw", False)
NATIV = (r"C:\Users\Tobi\Videos\Rippy", r"C:\Users\Tobi\Videos\Rippy\_arbeit", True)
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 -> Vorgabe des Betriebs. Der
Setting-Wert ist genau der, der bei Vollautomatik-Rips greift, weil dort
niemand gefragt wird.
"""
import ablauf as tasks
einst = {"workDir": "/app/media/movies"}
w = CONTAINER
assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy", w) == "/app/media/rippy"
assert tasks._arbeitsverzeichnis(einst, "", w) == "/app/media/movies"
assert tasks._arbeitsverzeichnis({}, "", w) == "/app/temp/raw"
# Ausbruchsversuche und Pfade außerhalb /app/media fallen durch
assert tasks._arbeitsverzeichnis({}, "/etc", w) == "/app/temp/raw"
assert tasks._arbeitsverzeichnis({}, "/app/media/../etc", w) == "/app/temp/raw"
# Leere Wahl fällt sauber auf die Einstellung zurück
assert tasks._arbeitsverzeichnis(einst, " ", w) == "/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 ablauf as 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():
"""Im CONTAINER bleibt die Wurzel eine Wurzel — daran ändert die
Windows-Reparatur nichts."""
import ablauf as tasks
w = CONTAINER
assert tasks._zielbasis("/app/media/movies", "bluray", w) == "/app/media/movies"
# Ausbruch per Praefix-Namen fällt auf den Standard zurück
assert tasks._zielbasis("/app/media-boese", "bluray", w) != "/app/media-boese"
assert tasks._zielbasis("/etc", "bluray", w) != "/etc"
def test_arbeitsverzeichnis_lehnt_praefix_ausbruch_ab():
import ablauf as tasks
w = CONTAINER
assert tasks._arbeitsverzeichnis({}, "/app/media-boese", w) == "/app/temp/raw"
assert tasks._arbeitsverzeichnis({"workDir": "/app/mediaX"}, "", w) == "/app/temp/raw"
# ── Nativ: der Befund vom 29.08.2026 ────────────────────────────────────
#
# Beim Nachstellen des leeren Bildschirms lief ein echter Test-Rip durch, und
# die Rohdaten landeten in `F:\app\temp\raw` — einem Ordner namens `app` auf
# dem Laufwerk, von dem Rippy gerade lief. Ursache: `RAW_DIR` ist
# `/app/temp/raw`, und die Pruefung `unter_wurzel(wahl, "/app/media")` verwarf
# sogar eine AUSDRUECKLICHE Wahl.
#
# Damit kam der Arbeitsordner, den der Commander am 28.08.2026 bestellt hat,
# unter Windows nie an: Der Dialog zeigte ihn, das Setzen ging, der Worker
# ignorierte ihn — ohne ein Wort.
def test_nativ_zaehlt_die_wahl_des_nutzers():
"""DER Befund. `D:\\Roh` liegt unter keiner Container-Wurzel und wurde
deshalb still verworfen."""
import ablauf as tasks
assert tasks._arbeitsverzeichnis({}, r"D:\Roh", NATIV) == r"D:\Roh"
assert tasks._arbeitsverzeichnis({"workDir": r"E:\Arbeit"}, "", NATIV) == r"E:\Arbeit"
def test_nativ_faellt_auf_den_ort_aus_der_installation_zurueck():
"""Nicht auf `/app/temp/raw` — das wurde unter Windows zu `X:\\app\\temp`."""
import ablauf as tasks
assert tasks._arbeitsverzeichnis({}, "", NATIV) == NATIV[1]
assert "/app/" not in tasks._arbeitsverzeichnis({}, "", NATIV)
def test_nativ_nimmt_auch_eine_freigabe_als_ziel():
"""Sein Ziel ist eine UNC-Freigabe — die liegt unter gar keiner lokalen
Wurzel."""
import ablauf as tasks
unc = r"\\192.168.179.62\rippy\movies"
assert tasks._zielbasis(unc, "bluray", NATIV) == unc
def test_nativ_ohne_wahl_landet_unter_der_eigenen_ablage():
import ablauf as tasks
ziel = tasks._zielbasis("", "bluray", NATIV)
assert ziel.startswith(NATIV[0])
assert "/app/" not in ziel