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>
147 lines
5.3 KiB
C#
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);
|
|
}
|
|
}
|