"""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 Traum & so", "genres": ["Sci-Fi", "Action"]}, "Inception", 2010, ) assert "" in nfo and "" in nfo assert "Inception" in nfo assert "2010" in nfo # XML-Escaping: <, >, & dürfen den Parser nicht sprengen assert "Ein Traum <im> Traum & so" in nfo assert "Sci-Fi" in nfo def test_baue_nfo_serie_bekommt_tvshow_wurzel(): nfo = baue_nfo({"type": "tv"}, "Neon Genesis Evangelion", 1995) assert "" in nfo and "" 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