Files
Flaro/server/tests/Halo.Media.Tests/SidecarWriterTests.cs
T
ScarriffleandClaude Opus 5 f98b995050 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>
2026-10-03 19:01:26 +02:00

147 lines
5.3 KiB
C#

using System.Text;
using Halo.Core.Library;
using Microsoft.Extensions.Logging.Abstractions;
namespace Halo.Media.Tests;
/// <summary>Was wirklich auf der Platte landet.</summary>
public class SidecarWriterTests
{
private string _ordner = "";
private string _film = "";
[SetUp]
public void Anlegen()
{
_ordner = Directory.CreateDirectory(Path.Combine(
Path.GetTempPath(), "halo-beiwerk-" + Guid.CreateVersion7().ToString("N"),
"Der Untergang (2004)")).FullName;
_film = Path.Combine(_ordner, "Der Untergang (2004).mkv");
File.WriteAllBytes(_film, new byte[2048]);
}
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(Path.GetDirectoryName(_ordner)!, recursive: true); } catch (IOException) { }
}
private static SidecarWriter Schreiber(bool an = true) =>
new(new SidecarOptions(an), NullLogger<SidecarWriter>.Instance);
[Test]
public async Task Eine_NFO_landet_neben_dem_Film()
{
await Schreiber().WriteTextAsync(_film, SidecarNames.MovieNfo, "<movie><tmdbid>613</tmdbid></movie>");
var ziel = Path.Combine(_ordner, "movie.nfo");
Assert.That(File.Exists(ziel), Is.True);
Assert.That(await File.ReadAllTextAsync(ziel), Does.Contain("613"));
}
[Test]
public async Task Ein_Bild_landet_neben_dem_Film()
{
using var inhalt = new MemoryStream(Encoding.UTF8.GetBytes("ein plakat"));
await Schreiber().WriteStreamAsync(_film, SidecarNames.MoviePoster, inhalt);
Assert.That(File.Exists(Path.Combine(_ordner, "poster.jpg")), Is.True);
}
/// <summary>Die Prüfung, an der der Schutz des Bestands hängt.</summary>
///
/// <remarks>
/// <b>Es muss laut scheitern, nicht still übergehen.</b> Käme hier ein
/// Name durch, der nicht auf der Liste steht, wäre der gesamte Bestand
/// offen — und ein stilles Übergehen verstecke den Fehler, bis jemand
/// seine Filme vermisst.
/// </remarks>
[TestCase("film.mkv")]
[TestCase("../anderswo.nfo")]
[TestCase("/etc/passwd")]
[TestCase("unterordner/poster.jpg")]
[TestCase("beliebig.txt")]
public void Was_nicht_auf_der_Liste_steht_wird_laut_abgelehnt(string versuch)
{
var fehler = Assert.ThrowsAsync<InvalidOperationException>(
() => Schreiber().WriteTextAsync(_film, versuch, "egal"));
Assert.That(fehler!.Message, Does.Contain("Liste"));
}
/// <summary>Die Mediendatei bleibt unberührt — auch beim Versuch.</summary>
[Test]
public void Die_Mediendatei_wird_nie_angefasst()
{
var vorher = new FileInfo(_film).Length;
Assert.ThrowsAsync<InvalidOperationException>(
() => Schreiber().WriteTextAsync(_film, "Der Untergang (2004).mkv", "kaputt"));
Assert.That(new FileInfo(_film).Length, Is.EqualTo(vorher));
Assert.That(File.ReadAllBytes(_film).All(b => b == 0), Is.True);
}
/// <summary>Abgeschaltet heisst abgeschaltet.</summary>
[Test]
public async Task Ohne_Erlaubnis_wird_nichts_geschrieben()
{
await Schreiber(an: false).WriteTextAsync(_film, SidecarNames.MovieNfo, "<movie/>");
Assert.That(File.Exists(Path.Combine(_ordner, "movie.nfo")), Is.False);
Assert.That(Schreiber(an: false).Enabled, Is.False);
}
/// <summary>Gelesen wird trotzdem.</summary>
///
/// <remarks>
/// Eine vorhandene NFO ist eine Zuordnung, die jemand getroffen hat. Sie
/// zu übergehen, bloss weil Halo selbst nicht schreiben darf, wäre
/// verkehrt herum.
/// </remarks>
[Test]
public async Task Gelesen_wird_auch_ohne_Schreiberlaubnis()
{
await File.WriteAllTextAsync(Path.Combine(_ordner, "movie.nfo"), "<movie><tmdbid>613</tmdbid></movie>");
var gelesen = await Schreiber(an: false).ReadTextAsync(_film, SidecarNames.MovieNfo);
Assert.That(Nfo.ReadIds(gelesen!).Tmdb, Is.EqualTo(613));
}
[Test]
public async Task Eine_fehlende_Datei_ergibt_null()
{
Assert.That(await Schreiber().ReadTextAsync(_film, SidecarNames.MovieNfo), Is.Null);
}
[Test]
public async Task Ein_zweites_Schreiben_ersetzt_das_erste()
{
await Schreiber().WriteTextAsync(_film, SidecarNames.MovieNfo, "<movie>alt</movie>");
await Schreiber().WriteTextAsync(_film, SidecarNames.MovieNfo, "<movie>neu</movie>");
Assert.That(await File.ReadAllTextAsync(Path.Combine(_ordner, "movie.nfo")), Is.EqualTo("<movie>neu</movie>"));
Assert.That(Directory.GetFiles(_ordner, "*.halo-tmp"), Is.Empty, "Keine Reste vom Umbenennen");
}
[Test]
public void Ein_Film_allein_im_Ordner_wird_erkannt()
{
Assert.That(Schreiber().IsAloneInFolder(_film), Is.True);
File.WriteAllBytes(Path.Combine(_ordner, "Noch ein Film (2005).mkv"), new byte[10]);
Assert.That(Schreiber().IsAloneInFolder(_film), Is.False);
}
/// <summary>Beiwerk zählt nicht als zweiter Film.</summary>
[Test]
public async Task Das_eigene_Beiwerk_macht_den_Film_nicht_unselbstaendig()
{
await Schreiber().WriteTextAsync(_film, SidecarNames.MovieNfo, "<movie/>");
using var bild = new MemoryStream([1, 2, 3]);
await Schreiber().WriteStreamAsync(_film, SidecarNames.MoviePoster, bild);
Assert.That(Schreiber().IsAloneInFolder(_film), Is.True);
}
}