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>
128 lines
4.8 KiB
C#
128 lines
4.8 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Halo.Metadata.Tests;
|
|
|
|
/// <summary>Womit sich Halo bei TMDB ausweist.</summary>
|
|
public class ZugangTests
|
|
{
|
|
private const string LeseToken = "eyJhbGciOiJIUzI1NiJ9.beispiel.unterschrift";
|
|
private const string AelterSchluessel = "0123456789abcdef0123456789abcdef";
|
|
|
|
private static (TmdbClient Client, Attrappe Attrappe) Mit(string zugang)
|
|
{
|
|
var attrappe = Attrappe.MitJson("""{"results":[{"id":1,"title":"Egal"}]}""");
|
|
return (new TmdbClient(
|
|
new HttpClient(attrappe) { BaseAddress = new Uri(TmdbClient.BaseAddress) },
|
|
new FesteEinstellungen(new TmdbSettings(zugang, "de-DE")),
|
|
NullLogger<TmdbClient>.Instance), attrappe);
|
|
}
|
|
|
|
/// <summary>Ein Lese-Token gehört in die Kopfzeile, nicht in die Adresse.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eine Adresse steht in jedem Zugriffsprotokoll.</b> In jedem Reverse
|
|
/// Proxy, in jeder Fehlermeldung, die eine URL zitiert. Eine Kopfzeile tut
|
|
/// das nicht — und bei einem Schlüssel, der in einem offenen Projekt
|
|
/// mitgeliefert wird, zählt jede Stelle, an der er nicht auftaucht.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Der_Lese_Token_geht_als_Bearer_und_nicht_in_die_Adresse()
|
|
{
|
|
var (client, attrappe) = Mit(LeseToken);
|
|
await client.FindMovieAsync("Egal", null);
|
|
|
|
// **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>
|
|
[Test]
|
|
public async Task Der_aeltere_Schluessel_geht_weiterhin_als_Abfrageteil()
|
|
{
|
|
var (client, attrappe) = Mit(AelterSchluessel);
|
|
await client.FindMovieAsync("Egal", null);
|
|
|
|
Assert.That(attrappe.Kopfzeilen, Is.All.Null);
|
|
Assert.That(attrappe.Aufrufe, Is.All.Contains($"api_key={AelterSchluessel}"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Auch_bei_Folgen_wird_der_Bearer_mitgeschickt()
|
|
{
|
|
var (client, attrappe) = Mit(LeseToken);
|
|
await client.FindEpisodeAsync(70523, 1, 1);
|
|
|
|
Assert.That(attrappe.Kopfzeilen, Is.All.StartsWith("Bearer "));
|
|
Assert.That(attrappe.Aufrufe, Is.All.Not.Contains("api_key"));
|
|
}
|
|
}
|
|
|
|
/// <summary>Welcher Schlüssel gilt, wenn mehrere in Frage kommen.</summary>
|
|
public class SchluesselwahlTests
|
|
{
|
|
[Test]
|
|
public void Ein_eigener_Schluessel_sticht_den_mitgelieferten()
|
|
{
|
|
var gewählt = TmdbSettings.Resolve("mein-eigener", "de-DE");
|
|
|
|
Assert.That(gewählt.ApiKey, Is.EqualTo("mein-eigener"));
|
|
Assert.That(gewählt.UsingProjectKey, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void Ohne_eigenen_greift_der_mitgelieferte()
|
|
{
|
|
var gewählt = TmdbSettings.Resolve(null, null);
|
|
|
|
if (TmdbSettings.ProjectApiKey.Length == 0)
|
|
{
|
|
Assert.That(gewählt.ApiKey, Is.Null, "Ohne mitgelieferten Schlüssel bleibt es dabei");
|
|
Assert.That(gewählt.UsingProjectKey, Is.False);
|
|
return;
|
|
}
|
|
|
|
Assert.That(gewählt.ApiKey, Is.EqualTo(TmdbSettings.ProjectApiKey));
|
|
Assert.That(gewählt.UsingProjectKey, Is.True);
|
|
}
|
|
|
|
[TestCase("")]
|
|
[TestCase(" ")]
|
|
public void Ein_leerer_eigener_Schluessel_zaehlt_als_keiner(string leer)
|
|
{
|
|
Assert.That(TmdbSettings.Resolve(leer, null).UsingProjectKey,
|
|
Is.EqualTo(TmdbSettings.ProjectApiKey.Length > 0));
|
|
}
|
|
|
|
[Test]
|
|
public void Leerraum_um_den_Schluessel_wird_abgeschnitten()
|
|
{
|
|
Assert.That(TmdbSettings.Resolve(" meiner ", " de-DE ").ApiKey, Is.EqualTo("meiner"));
|
|
Assert.That(TmdbSettings.Resolve("meiner", " de-DE ").Language, Is.EqualTo("de-DE"));
|
|
}
|
|
|
|
[Test]
|
|
public void Ohne_Sprachangabe_gilt_Englisch()
|
|
{
|
|
Assert.That(TmdbSettings.Resolve("meiner", null).Language, Is.EqualTo("en-US"));
|
|
}
|
|
|
|
/// <summary>Der mitgelieferte Schlüssel darf nur lesen dürfen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Er steht öffentlich im Quellcode. Ein Lese-Token kann nichts ändern —
|
|
/// ein älterer API-Schlüssel schon. Diese Prüfung hält fest, dass hier
|
|
/// keiner der falschen Art hineingerät.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Der_mitgelieferte_Schluessel_ist_ein_Lese_Token()
|
|
{
|
|
if (TmdbSettings.ProjectApiKey.Length == 0) Assert.Pass("Kein Schlüssel mitgeliefert.");
|
|
|
|
Assert.That(TmdbSettings.ProjectApiKey, Does.StartWith("eyJ"),
|
|
"Erwartet wird ein API Read Access Token, kein älterer API-Schlüssel");
|
|
}
|
|
}
|