Beiwerk im Medienordner, und eine Zuordnung lässt sich korrigieren

Plakate, Hintergründe und NFO-Dateien landen jetzt neben dem Film — und eine
falsche Zuordnung lässt sich richtigstellen, ohne dass der nächste Scan sie
wieder überschreibt.

    Der Untergang (2004)/
    ├── Der Untergang (2004).mkv
    ├── poster.jpg
    ├── fanart.jpg
    └── movie.nfo

Bei Serien liegt `tvshow.nfo` eine Ebene über dem Staffelordner, je Folge
kommen NFO und Standbild daneben. Liegen mehrere Filme in einem Ordner, heisst
das Beiwerk `Titel (Jahr)-poster.jpg` — sonst überschrieben sie sich
gegenseitig.

**Die Erlaubnisliste ist der ganze Schutz.** Damit das geht, muss die
Medieneinhängung ohne `:ro` auskommen, und damit könnte der Server dort alles
anstellen. Dass er es nicht tut, hängt an `SidecarNames`: sechs Arten von
Datei, nichts sonst. Ein Name, der nicht daraufsteht, wirft — laut, nicht
still, denn das wäre ein Fehler im Programm. Mediendateien werden nie
geändert, umbenannt oder gelöscht; es gibt keine Stelle, die das könnte.
Geprüft an echten Dateien, einschliesslich der Versuche mit `../`, mit
absoluten Pfaden und mit einer Mediendatei als Ziel.

Standardmässig **aus**. In fremde Medienordner zu schreiben ist nichts, was
ein Server ungefragt tun sollte.

**Gelesen wird auch ohne Schreiberlaubnis.** Eine vorhandene NFO ist eine
Zuordnung, die jemand getroffen hat — Halo nimmt sie und sucht gar nicht
erst. Dasselbe gilt für Ordnernamen der Form `[tmdbid-613]`. Halo liest sie
und benennt **nichts** um: Umbenennen ist der einzige Eingriff, der eine
Bibliothek wirklich zerlegen kann.

Nebenbei: eine NFO aus dem Medienordner hat Halo nicht geschrieben. DTDs sind
beim Lesen abgeschaltet — eine untergeschobene Datei mit externer Entität
könnte sonst den Server dazu bringen, `/config/halo.db` einzulesen und als
Titel auszuliefern. Mit Prüfung.

---

**Der Fehler, den erst der echte Lauf zeigte** — und er ist die zweite
Auflage eines schon einmal behobenen:

Nach der Korrektur stand im Ordner „Dark (2017)", in der Datenbank aber 2004.
Der nächste Scan suchte nach *(Dark, 2017)*, fand nichts, legte den Film neu
an und löschte den korrigierten als verwaist. Überlebt hat die Zuordnung nur,
weil sie in der NFO stand — das System hat sich selbst gerettet, aber der
Eintrag war ein anderer und das Jahr wieder falsch.

Dieselbe Klasse wie bei `ParsedTitle`: zugeordnet wurde über einen Wert, den
die Metadatenquelle verändert. Jetzt gibt es `ParsedYear`, und das ändert sich
nie. Mit Prüfung, die genau diesen Ablauf festhält.

---

Dazu von TMDB: die IMDb-Kennung wird nachgeschlagen, damit sie in der NFO
steht — wer später auf ein anderes Werkzeug wechselt, findet denselben Titel
wieder, auch wenn das TMDB gar nicht kennt.

308 Tests grün. Im Container nachgewiesen: Zuordnung falsch, korrigiert,
zweiter Scan — derselbe Eintrag, dieselbe Kennung, Jahr bleibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-03 19:01:26 +02:00
co-authored by Claude Opus 5
parent 79b6001f86
commit f98b995050
30 changed files with 2911 additions and 39 deletions
@@ -48,7 +48,9 @@ public class TmdbClientTests
var attrappe = Attrappe.MitJson(FilmTreffer);
await Client(attrappe, FesteEinstellungen.MitSchluessel("de-DE")).FindMovieAsync("Der Untergang", 2004);
var aufruf = attrappe.Aufrufe.Single();
// Die erste Anfrage ist die Suche; danach folgt der Nachschlag der
// IMDb-Kennung für die NFO-Datei.
var aufruf = attrappe.Aufrufe[0];
Assert.That(aufruf, Does.Contain("query=Der%20Untergang"));
Assert.That(aufruf, Does.Contain("year=2004"));
Assert.That(aufruf, Does.Contain("language=de-DE"));
@@ -59,7 +61,7 @@ public class TmdbClientTests
{
var attrappe = Attrappe.MitJson(FilmTreffer);
await Client(attrappe).FindMovieAsync("Matrix", null);
Assert.That(attrappe.Aufrufe.Single(), Does.Not.Contain("year="));
Assert.That(attrappe.Aufrufe[0], Does.Not.Contain("year="));
}
[Test]
@@ -31,9 +31,11 @@ public class ZugangTests
var (client, attrappe) = Mit(LeseToken);
await client.FindMovieAsync("Egal", null);
Assert.That(attrappe.Kopfzeilen.Single(), Is.EqualTo($"Bearer {LeseToken}"));
Assert.That(attrappe.Aufrufe.Single(), Does.Not.Contain("api_key"));
Assert.That(attrappe.Aufrufe.Single(), Does.Not.Contain(LeseToken));
// **Jede** Anfrage, nicht nur die erste: seit der IMDb-Nachschlag
// dazukam, gehen zwei hinaus, und beide müssen es richtig machen.
Assert.That(attrappe.Kopfzeilen, Is.All.EqualTo($"Bearer {LeseToken}"));
Assert.That(attrappe.Aufrufe, Is.All.Not.Contains("api_key"));
Assert.That(attrappe.Aufrufe, Is.All.Not.Contains(LeseToken));
}
/// <summary>Der ältere Schlüssel kann nur in die Adresse — also tut er das.</summary>
@@ -43,8 +45,8 @@ public class ZugangTests
var (client, attrappe) = Mit(AelterSchluessel);
await client.FindMovieAsync("Egal", null);
Assert.That(attrappe.Kopfzeilen.Single(), Is.Null);
Assert.That(attrappe.Aufrufe.Single(), Does.Contain($"api_key={AelterSchluessel}"));
Assert.That(attrappe.Kopfzeilen, Is.All.Null);
Assert.That(attrappe.Aufrufe, Is.All.Contains($"api_key={AelterSchluessel}"));
}
[Test]
@@ -53,8 +55,8 @@ public class ZugangTests
var (client, attrappe) = Mit(LeseToken);
await client.FindEpisodeAsync(70523, 1, 1);
Assert.That(attrappe.Kopfzeilen.Single(), Does.StartWith("Bearer "));
Assert.That(attrappe.Aufrufe.Single(), Does.Not.Contain("api_key"));
Assert.That(attrappe.Kopfzeilen, Is.All.StartsWith("Bearer "));
Assert.That(attrappe.Aufrufe, Is.All.Not.Contains("api_key"));
}
}