`laden()` setzte `innerHTML = ''` und baute die Mediathekenliste neu — während eines Scans alle drei Sekunden. Der ganze Abschnitt flackerte sichtbar, und wer gerade auf einen Knopf zielte, klickte ins Leere. Schlimmer: jeder Neuaufbau startete über `verfolgen(false)` einen weiteren Takt. Nach einer Minute liefen zwanzig davon nebeneinander. Jetzt wird fortgeschrieben statt neu gezeichnet: eine Zeile je Mediathek, die bleibt, und nur Kopfzeile, Prüfungsknopf und Freigabeknopf ändern sich. Belegt im Browser — nach einem Auffrischen sind es dieselben Knoten, nur die Beschriftung ist eine andere. Dieselbe Behandlung für die Seitenleiste. Und weil Seitenleiste und Verwaltung getrennt nachfragten, zeigten sie zwei Momente — 95 hier, 84 dort, während im Hintergrund Doppelte zusammengelegt wurden. Die Verwaltung reicht ihren Stand jetzt weiter: ein Abruf, zwei Anzeigen. Dazu zwei Dinge, die beim Prüfen auffielen. Zurücksetzen und Entfernen prallen jetzt serverseitig mit 409 ab, solange ein Lauf läuft — die Oberfläche sperrt die Knöpfe, aber darauf darf sich der Server nicht verlassen; ein Aufruf von Hand während eines Scans liess den Lauf an einer DbUpdateConcurrencyException sterben. Und in der Fortschrittszeile steht beim Holen der Folgenmetadaten jetzt der Serienname: „Folge 2×13" allein sagt bei 3815 Folgen nichts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
598 lines
26 KiB
C#
598 lines
26 KiB
C#
using System.Net;
|
||
using System.Net.Http.Json;
|
||
using Flaro.Api.Endpoints;
|
||
|
||
namespace Flaro.Api.Tests;
|
||
|
||
/// <summary>Scan gegen einen echten Ordner mit echten Dateien.</summary>
|
||
public class ScanTests
|
||
{
|
||
private string _bestand = "";
|
||
|
||
[SetUp]
|
||
public void Anlegen() => _bestand = Directory.CreateDirectory(
|
||
Path.Combine(Path.GetTempPath(), "flaro-medien-" + Guid.CreateVersion7().ToString("N"))).FullName;
|
||
|
||
[TearDown]
|
||
public void Aufraeumen()
|
||
{
|
||
try { Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
|
||
}
|
||
|
||
private void Datei(string relativ, int bytes = 2048)
|
||
{
|
||
var voll = Path.Combine(_bestand, relativ);
|
||
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
|
||
File.WriteAllBytes(voll, new byte[bytes]);
|
||
}
|
||
|
||
private async Task<(HttpClient Admin, Guid LibraryId)> MitBibliothek(FlaroServer server, string art)
|
||
{
|
||
var admin = await server.SetUpAndSignIn();
|
||
var client = server.AlsAngemeldeter(admin.Token);
|
||
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
|
||
new CreateLibraryRequest("Prüfbestand", art, _bestand));
|
||
angelegt.EnsureSuccessStatusCode();
|
||
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
|
||
return (client, bibliothek.Id);
|
||
}
|
||
|
||
/// <summary>Stösst an und wartet, bis der Hintergrundlauf fertig ist.</summary>
|
||
private static async Task<ScanStatus> Scannen(HttpClient client, Guid bibliothek)
|
||
{
|
||
var angestossen = await client.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null);
|
||
Assert.That(angestossen.StatusCode, Is.EqualTo(HttpStatusCode.Accepted));
|
||
|
||
// **Dreissig Sekunden, nicht fünf.** Die alte Grenze war für zwei,
|
||
// drei Dateien bemessen. Der Test über 120 Dateien schaffte sie
|
||
// allein, fiel aber durch, sobald die ganze Suite nebenher lief —
|
||
// und das war kein Fehler im Scan, sondern eine zu knappe Geduld.
|
||
// Teurer wird es nur, wenn wirklich etwas hängt.
|
||
const int schritte = 600, abstand = 50;
|
||
|
||
for (var versuch = 0; versuch < schritte; versuch++)
|
||
{
|
||
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
|
||
if (stand!.State is "finished") return stand;
|
||
if (stand.State is "failed") Assert.Fail($"Der Scan ist gescheitert: {stand.Error}");
|
||
await Task.Delay(abstand);
|
||
}
|
||
Assert.Fail($"Der Scan wurde in {schritte * abstand / 1000} Sekunden nicht fertig.");
|
||
return null!;
|
||
}
|
||
|
||
[Test]
|
||
public async Task Filme_werden_gefunden_und_benannt()
|
||
{
|
||
Datei("The.Matrix.1999.1080p.BluRay.x264-GRP.mkv");
|
||
Datei("Der Untergang (2004).mkv");
|
||
Datei("The Matrix (1999).srt"); // zählt nicht
|
||
Datei("@eaDir/The.Matrix.1999.mkv"); // zählt auch nicht
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
|
||
var stand = await Scannen(client, bibliothek);
|
||
Assert.That(stand.FilesFound, Is.EqualTo(2));
|
||
Assert.That(stand.Added, Is.EqualTo(2));
|
||
|
||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
Assert.That(filme!.Total, Is.EqualTo(2));
|
||
// Sortiert ohne Artikel: „Matrix" vor „Untergang".
|
||
Assert.That(filme.Items.Select(m => m.Title),
|
||
Is.EqualTo(new[] { "The Matrix", "Der Untergang" }));
|
||
Assert.That(filme.Items[0].Year, Is.EqualTo(1999));
|
||
}
|
||
|
||
/// <summary>Zweimal scannen muss dasselbe ergeben wie einmal.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Sonst wächst die Mediathek bei jedem Lauf.</b> Das ist der Fehler,
|
||
/// den man erst nach Wochen bemerkt — wenn jeder Film fünfmal dasteht.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Ein_zweiter_Lauf_aendert_nichts()
|
||
{
|
||
Datei("The Matrix (1999).mkv");
|
||
Datei("Inception (2010).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
|
||
await Scannen(client, bibliothek);
|
||
var zweiter = await Scannen(client, bibliothek);
|
||
|
||
Assert.That(zweiter.Added, Is.EqualTo(0));
|
||
Assert.That(zweiter.Removed, Is.EqualTo(0));
|
||
Assert.That((await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!.Total, Is.EqualTo(2));
|
||
}
|
||
|
||
/// <summary>Eine verschwundene Datei wird gemeldet, nicht stillschweigend gelöscht.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Vorher fiel hier der Titel samt Sehfortschritt.</b> Das ist für eine
|
||
/// wirklich gelöschte Datei richtig — und falsch für jeden anderen Grund,
|
||
/// aus dem eine Datei kurz nicht da ist. Beides sieht für den Scan gleich
|
||
/// aus, also entscheidet jetzt der Administrator.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Eine_verschwundene_Datei_wird_gemeldet_statt_geloescht()
|
||
{
|
||
Datei("The Matrix (1999).mkv");
|
||
Datei("Inception (2010).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
File.Delete(Path.Combine(_bestand, "Inception (2010).mkv"));
|
||
var danach = await Scannen(client, bibliothek);
|
||
|
||
Assert.That(danach.Missing, Is.EqualTo(1));
|
||
Assert.That((await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!.Total,
|
||
Is.EqualTo(2), "Der Titel bleibt stehen, bis jemand ihn entfernt.");
|
||
|
||
var vermisst = await client.GetFromJsonAsync<List<VermissteDatei>>("/api/v1/libraries/missing");
|
||
Assert.That(vermisst, Has.Count.EqualTo(1));
|
||
Assert.That(vermisst![0].Title, Is.EqualTo("Inception"));
|
||
|
||
// Und erst auf Ansage ist er wirklich weg.
|
||
(await client.DeleteAsync("/api/v1/libraries/missing")).EnsureSuccessStatusCode();
|
||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
Assert.That(filme!.Items.Select(m => m.Title), Is.EqualTo(new[] { "The Matrix" }));
|
||
}
|
||
|
||
/// <summary>Kommt die Datei zurück, verschwindet die Markierung von selbst.</summary>
|
||
[Test]
|
||
public async Task Eine_zurueckgekehrte_Datei_gilt_wieder_als_da()
|
||
{
|
||
// Zwei Dateien, damit der Ordner nicht leer wird — sonst greift die
|
||
// Notbremse aus dem Test darunter, und zwar zu Recht.
|
||
Datei("Inception (2010).mkv");
|
||
Datei("The Matrix (1999).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var pfad = Path.Combine(_bestand, "Inception (2010).mkv");
|
||
var inhalt = await File.ReadAllBytesAsync(pfad);
|
||
File.Delete(pfad);
|
||
await Scannen(client, bibliothek);
|
||
Assert.That(await client.GetFromJsonAsync<List<VermissteDatei>>("/api/v1/libraries/missing"),
|
||
Has.Count.EqualTo(1));
|
||
|
||
await File.WriteAllBytesAsync(pfad, inhalt);
|
||
await Scannen(client, bibliothek);
|
||
|
||
Assert.That(await client.GetFromJsonAsync<List<VermissteDatei>>("/api/v1/libraries/missing"),
|
||
Is.Empty);
|
||
}
|
||
|
||
/// <summary>Die Notbremse: ein leerer Ordner bricht ab, statt alles zu verwerfen.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Der teuerste denkbare Fehler.</b> Ist das Volume beim Start des NAS
|
||
/// noch nicht eingehängt, liegt der Ordner leer da — und ohne diese Prüfung
|
||
/// gälte der gesamte Bestand als verschwunden.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Ein_ploetzlich_leerer_Ordner_bricht_den_Scan_ab()
|
||
{
|
||
Datei("The Matrix (1999).mkv");
|
||
Datei("Inception (2010).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
foreach (var d in Directory.EnumerateFiles(_bestand, "*", SearchOption.AllDirectories))
|
||
File.Delete(d);
|
||
|
||
var angestossen = await client.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null);
|
||
Assert.That(angestossen.StatusCode, Is.EqualTo(HttpStatusCode.Accepted));
|
||
|
||
ScanStatus? stand = null;
|
||
for (var versuch = 0; versuch < 600; versuch++)
|
||
{
|
||
stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
|
||
if (stand!.State is "failed" or "finished") break;
|
||
await Task.Delay(50);
|
||
}
|
||
|
||
Assert.That(stand!.State, Is.EqualTo("failed"));
|
||
Assert.That(stand.Error, Does.Contain("abgebrochen"));
|
||
Assert.That((await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!.Total,
|
||
Is.EqualTo(2), "Es darf nichts verändert worden sein.");
|
||
Assert.That(await client.GetFromJsonAsync<List<VermissteDatei>>("/api/v1/libraries/missing"),
|
||
Is.Empty, "Auch nicht als vermisst markiert.");
|
||
}
|
||
|
||
/// <summary>Das Aufräumen darf nicht wegräumen, was derselbe Lauf schrieb.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Mehr als fünfzig Dateien, und das ist der Punkt.</b> Seit der Scan
|
||
/// alle fünfzig Dateien zwischenspeichert, liegt ein Teil des Bestands
|
||
/// schon in der Datenbank, während der Lauf noch läuft. Direkt danach
|
||
/// löscht er alles, was eine ältere <c>SeenAt</c>-Zeit trägt als dieser
|
||
/// Lauf — würde die Zeit irgendwo neu genommen statt vor der Schleife
|
||
/// festgehalten, risse dieser Schritt den halben Bestand mit.
|
||
///
|
||
/// Die Dateien hier sind Nullbytes, an denen ffprobe scheitert. Damit
|
||
/// greift das Zwischenspeichern aus der Schleife und nicht das aus der
|
||
/// Untersuchung — also genau der Weg, der sonst ungeprüft bliebe.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Ein_grosser_Bestand_ueberlebt_das_Aufraeumen()
|
||
{
|
||
for (var i = 1; i <= 120; i++) Datei($"Prüffilm {i:000} (2024).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
|
||
var erster = await Scannen(client, bibliothek);
|
||
|
||
Assert.That(erster.Added, Is.EqualTo(120));
|
||
Assert.That(erster.Removed, Is.Zero, "Ein erster Lauf darf nichts entfernen.");
|
||
Assert.That((await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!.Total,
|
||
Is.EqualTo(120));
|
||
|
||
// Und ein zweiter Lauf bleibt ebenso vollständig.
|
||
var zweiter = await Scannen(client, bibliothek);
|
||
Assert.That(zweiter.Removed, Is.Zero);
|
||
Assert.That((await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!.Total,
|
||
Is.EqualTo(120));
|
||
}
|
||
|
||
/// <summary>Schreibvarianten desselben Titels ergeben einen Eintrag.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Der Fall, der die Mediathek doppelt aussehen liess.</b> Der Scan
|
||
/// verglich den Titel aus dem Dateinamen zeichengenau — ein Punkt
|
||
/// Unterschied, und es wurden zwei Filme, die anschliessend beide
|
||
/// denselben TMDB-Eintrag bekamen und ununterscheidbar nebeneinander
|
||
/// standen.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Schreibvarianten_ergeben_einen_Film()
|
||
{
|
||
Datei("Dr. Dolittle 2 (2001).mkv");
|
||
Datei("Dr Dolittle 2.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
Assert.That(filme!.Total, Is.EqualTo(1));
|
||
Assert.That(filme.Items[0].Files, Has.Count.EqualTo(2));
|
||
Assert.That(filme.Items[0].Year, Is.EqualTo(2001),
|
||
"Das Jahr der einen Datei gilt auch, wenn die andere keines trägt.");
|
||
}
|
||
|
||
/// <summary>Eine mobile Fassung ist kein eigener Film.</summary>
|
||
[Test]
|
||
public async Task Eine_optimierte_Fassung_bleibt_beim_Film()
|
||
{
|
||
Datei("The Matrix (1999).mkv");
|
||
Datei("The Matrix (1999) - Mobil.mp4");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
Assert.That(filme!.Total, Is.EqualTo(1), "Sonst stünde ein zweiter Film ‚The Matrix - Mobil‘ daneben.");
|
||
Assert.That(filme.Items[0].Title, Is.EqualTo("The Matrix"));
|
||
Assert.That(filme.Items[0].Files, Has.Count.EqualTo(2));
|
||
}
|
||
|
||
/// <summary>Gleicher Name, anderes Jahr — zwei Filme, und das muss so bleiben.</summary>
|
||
[Test]
|
||
public async Task Zwei_Verfilmungen_bleiben_zwei_Filme()
|
||
{
|
||
Datei("Dune (1984).mkv");
|
||
Datei("Dune (2021).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
Assert.That(filme!.Total, Is.EqualTo(2));
|
||
}
|
||
|
||
/// <summary>Eine Serie, die ihren Namen verliert, bleibt nicht als Leiche stehen.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Der Weg, den ein vorhandener Bestand nach dem Update geht.</b> Ohne
|
||
/// Ordner benennt der Teil vor der Folgennummer die Serie. Wandert die
|
||
/// Datei in einen Ordner, gehört sie zu diesem — und der alte Eintrag muss
|
||
/// verschwinden, statt als leere Serie in der Mediathek zu bleiben.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Eine_verwaiste_Serie_verschwindet_beim_naechsten_Lauf()
|
||
{
|
||
Datei("Tatortreiniger S01E01 Kapitel 1.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var vorher = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(vorher!.Total, Is.EqualTo(1), "Ohne Ordner zählt der Titel vor der Folgennummer.");
|
||
var alterName = vorher.Items[0].Title;
|
||
|
||
// **Verschieben, nicht löschen und neu anlegen.** Beim Verschieben
|
||
// bleibt der Zeitstempel erhalten — und genau daran erkennt der Scan,
|
||
// dass es dieselbe Datei ist. Ein Test, der sie neu anlegt, prüft
|
||
// etwas anderes als das, was ein Mensch tut.
|
||
var alt = Path.Combine(_bestand, "Tatortreiniger S01E01 Kapitel 1.mkv");
|
||
var neuerPfad = Path.Combine(_bestand, "Nachtstreife", "Tatortreiniger S01E01 Kapitel 1.mkv");
|
||
Directory.CreateDirectory(Path.GetDirectoryName(neuerPfad)!);
|
||
File.Move(alt, neuerPfad);
|
||
|
||
await Scannen(client, bibliothek);
|
||
|
||
var nachher = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(nachher!.Total, Is.EqualTo(1), "Die alte Serie darf nicht danebenstehen.");
|
||
Assert.That(nachher.Items[0].Title, Is.EqualTo("Nachtstreife"));
|
||
Assert.That(nachher.Items[0].Title, Is.Not.EqualTo(alterName));
|
||
Assert.That(nachher.Items[0].EpisodeCount, Is.EqualTo(1));
|
||
}
|
||
|
||
/// <summary>Aus einem nackten Dateinamen wird keine Serie erfunden.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Der teuerste Fehler des Projekts.</b> Lag eine Datei direkt in der
|
||
/// Wurzel und trug vor der Folgennummer nichts, wurde der ganze Dateiname
|
||
/// zum Serientitel — samt Folgentitel. Aus 95 Serien wurden 737, jede mit
|
||
/// genau einer Folge, und das bei jedem Lauf aufs Neue. Lieber nichts
|
||
/// behaupten als eine Geisterserie anlegen.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Ein_nackter_Dateiname_erfindet_keine_Serie()
|
||
{
|
||
Datei("S01E01.mkv");
|
||
Datei("S01E02.mkv");
|
||
Datei("S02E01.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(serien!.Total, Is.EqualTo(0),
|
||
"Drei namenlose Dateien ergaben vorher drei Serien.");
|
||
}
|
||
|
||
/// <summary>Lose Dateien derselben Serie landen unter einer Serie.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// Ohne Ordner trägt der Teil vor der Folgennummer den Serientitel. Dass
|
||
/// dahinter bei jeder Datei ein anderer Folgentitel steht, darf daran
|
||
/// nichts ändern — genau daraus entstanden die Dutzende Einträge
|
||
/// „In höchster Not · …", jeder mit einer Folge.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Lose_Dateien_mit_Folgentiteln_ergeben_eine_Serie()
|
||
{
|
||
Datei("In höchster Not - S01E01 - Bergrettung am Grat.mkv");
|
||
Datei("In höchster Not - S01E02 - Lawine im Tal.mkv");
|
||
Datei("In höchster Not - S01E03 - Sturz in die Klamm.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(serien!.Total, Is.EqualTo(1));
|
||
Assert.That(serien.Items[0].EpisodeCount, Is.EqualTo(3));
|
||
}
|
||
|
||
/// <summary>Derselbe Pfad zweimal bringt den ganzen Lauf nicht mehr um.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// Ein doppelt eingehängter Ordner oder eine Verknüpfung zurück in die
|
||
/// Mediathek liefert denselben Pfad ein zweites Mal. Vorher entstand dabei
|
||
/// eine zweite Zeile mit gleichem <c>(LibraryId, Path)</c>, der
|
||
/// Eindeutigkeitsindex brach, und <b>der ganze Scan</b> endete mit
|
||
/// „An error occurred while saving the entity changes".
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Ein_doppelter_Pfad_bricht_den_Scan_nicht_ab()
|
||
{
|
||
Datei("Pingu/Pingu - S01E01.mkv");
|
||
|
||
// Eine Verknüpfung auf den Serienordner — derselbe Inhalt, zweiter Weg.
|
||
Directory.CreateSymbolicLink(
|
||
Path.Combine(_bestand, "Pingu-nochmal"), Path.Combine(_bestand, "Pingu"));
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var zustand = await client.GetFromJsonAsync<ScanStatus>(
|
||
$"/api/v1/libraries/{bibliothek}/scan");
|
||
Assert.That(zustand!.State, Is.Not.EqualTo("failed"), zustand.Error ?? "");
|
||
|
||
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(serien!.Total, Is.EqualTo(1), "Zwei Wege, eine Serie.");
|
||
}
|
||
|
||
/// <summary>4K und 1080p sind ein Film mit zwei Dateien, nicht zwei Filme.</summary>
|
||
[Test]
|
||
public async Task Zwei_Fassungen_desselben_Films_bleiben_ein_Eintrag()
|
||
{
|
||
Datei("The Matrix (1999) - 1080p.mkv");
|
||
Datei("The Matrix (1999) - 2160p.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
Assert.That(filme!.Total, Is.EqualTo(1));
|
||
Assert.That(filme.Items[0].Files, Has.Count.EqualTo(2));
|
||
}
|
||
|
||
[Test]
|
||
public async Task Serien_werden_in_Staffeln_und_Folgen_zerlegt()
|
||
{
|
||
Datei("Dark/Staffel 1/Dark - S01E01 - Geheimnisse.mkv");
|
||
Datei("Dark/Staffel 1/Dark - S01E02 - Lügen.mkv");
|
||
Datei("Dark/Staffel 2/Dark - S02E01 - Anfänge.mkv");
|
||
Datei("Notruf.Hafenkante.S01E01.German.1080p.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(serien!.Total, Is.EqualTo(2));
|
||
|
||
var dark = serien.Items.Single(s => s.Title == "Dark");
|
||
Assert.That(dark.EpisodeCount, Is.EqualTo(3));
|
||
Assert.That(dark.Seasons, Is.EqualTo(new[] { 1, 2 }));
|
||
|
||
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>($"/api/v1/series/{dark.Id}/episodes");
|
||
Assert.That(folgen!.Items.Select(f => f.Title),
|
||
Is.EqualTo(new[] { "Geheimnisse", "Lügen", "Anfänge" }));
|
||
|
||
var nurStaffel2 = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
|
||
$"/api/v1/series/{dark.Id}/episodes?season=2");
|
||
Assert.That(nurStaffel2!.Items, Has.Count.EqualTo(1));
|
||
}
|
||
|
||
/// <summary>Was keine Folgennummer trägt, wird nicht geraten — aber auch nicht verschwiegen.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Beide Hälften gehören zusammen.</b> Eine erfundene Staffel 1 wäre
|
||
/// falsch; die Datei lautlos zu überspringen war aber genauso falsch. Bei
|
||
/// 3559 von 5884 Dateien verschwand so die halbe Mediathek, ohne dass
|
||
/// irgendwo eine Zahl davon erzählte. <c>Added</c> zählt Dateizeilen, und
|
||
/// eine bekommt sie jetzt — zugeordnet ist sie deshalb nicht.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Dateien_ohne_Folgennummer_werden_gezaehlt_aber_nicht_zugeordnet()
|
||
{
|
||
Datei("Dark/Dark - S01E01.mkv");
|
||
Datei("Dark/Irgendein Mitschnitt.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
var stand = await Scannen(client, bibliothek);
|
||
|
||
Assert.That(stand.FilesFound, Is.EqualTo(2));
|
||
Assert.That(stand.Unrecognised, Is.EqualTo(1));
|
||
Assert.That(stand.Added, Is.EqualTo(2), "Beide Dateien bekommen eine Zeile.");
|
||
|
||
// Die Serie hat genau eine Folge — der Mitschnitt hängt an keiner.
|
||
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
|
||
Assert.That(serien!.Total, Is.EqualTo(1));
|
||
Assert.That(serien.Items[0].EpisodeCount, Is.EqualTo(1));
|
||
|
||
// Und er ist auffindbar, statt lautlos zu verschwinden.
|
||
var offen = await client.GetFromJsonAsync<Unzugeordnet>(
|
||
$"/api/v1/libraries/{bibliothek}/unassigned");
|
||
Assert.That(offen!.Gesamt, Is.EqualTo(1));
|
||
Assert.That(offen.Dateien[0].Pfad, Does.EndWith("Irgendein Mitschnitt.mkv"));
|
||
Assert.That(offen.Dateien[0].Grund, Does.Contain("keine Folgennummer"));
|
||
}
|
||
|
||
/// <summary>Zurücksetzen und Entfernen prallen ab, solange ein Lauf läuft.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Die Oberfläche sperrt die Knöpfe, aber darauf darf sich der Server nicht
|
||
/// verlassen.</b> Beim Prüfen von Hand ist genau das passiert: ein
|
||
/// „Zurücksetzen" per curl während eines Scans, und der Lauf starb an einer
|
||
/// DbUpdateConcurrencyException. Ein zweiter Administrator genügt dafür auch.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Zuruecksetzen_waehrend_eines_Scans_wird_abgelehnt()
|
||
{
|
||
for (var i = 1; i <= 400; i++) Datei($"Dark/Staffel 1/Dark - S01E{i:D3}.mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Shows");
|
||
|
||
var gestartet = await client.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null);
|
||
Assert.That(gestartet.StatusCode, Is.EqualTo(HttpStatusCode.Accepted));
|
||
|
||
var zurueck = await client.PostAsync($"/api/v1/libraries/{bibliothek}/reset", null);
|
||
var weg = await client.DeleteAsync($"/api/v1/libraries/{bibliothek}");
|
||
|
||
Assert.That(zurueck.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
|
||
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
|
||
}
|
||
|
||
[Test]
|
||
public async Task Die_Suche_grenzt_ein()
|
||
{
|
||
Datei("The Matrix (1999).mkv");
|
||
Datei("The Matrix Reloaded (2003).mkv");
|
||
Datei("Inception (2010).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var treffer = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies?search=matrix");
|
||
Assert.That(treffer!.Total, Is.EqualTo(2));
|
||
}
|
||
|
||
/// <summary>Eine Anfrage darf sich die Seitengrösse nicht selbst aussuchen.</summary>
|
||
[Test]
|
||
public async Task Die_Seitengroesse_ist_gedeckelt()
|
||
{
|
||
for (var i = 1; i <= 5; i++) Datei($"Film {i} (200{i}).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await Scannen(client, bibliothek);
|
||
|
||
var seite = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies?take=100000");
|
||
Assert.That(seite!.Total, Is.EqualTo(5), "Die Gesamtzahl bleibt richtig");
|
||
Assert.That(seite.Items, Has.Count.LessThanOrEqualTo(200));
|
||
|
||
var zweiteSeite = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies?skip=2&take=2");
|
||
Assert.That(zweiteSeite!.Items, Has.Count.EqualTo(2));
|
||
Assert.That(zweiteSeite.Total, Is.EqualTo(5));
|
||
}
|
||
|
||
[Test]
|
||
public async Task Vor_dem_ersten_Lauf_steht_der_Scan_auf_untaetig()
|
||
{
|
||
using var server = new FlaroServer();
|
||
var (client, bibliothek) = await MitBibliothek(server, "Movies");
|
||
|
||
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
|
||
Assert.That(stand!.State, Is.EqualTo("idle"));
|
||
}
|
||
|
||
[Test]
|
||
public async Task Ein_gewoehnlicher_Benutzer_stoesst_keinen_Scan_an()
|
||
{
|
||
using var server = new FlaroServer();
|
||
var (admin, bibliothek) = await MitBibliothek(server, "Movies");
|
||
await admin.PostAsJsonAsync("/api/v1/users",
|
||
new CreateUserRequest("gast", "auch-ein-langes-passwort", false));
|
||
var gast = await server.SignIn("gast", "auch-ein-langes-passwort", "iPad", "g-9");
|
||
|
||
var versuch = await server.AlsAngemeldeter(gast.Token)
|
||
.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null);
|
||
|
||
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
||
}
|
||
|
||
[Test]
|
||
public async Task Eine_Bibliothek_die_es_nicht_gibt_laesst_sich_nicht_scannen()
|
||
{
|
||
using var server = new FlaroServer();
|
||
var (client, _) = await MitBibliothek(server, "Movies");
|
||
|
||
var versuch = await client.PostAsync($"/api/v1/libraries/{Guid.CreateVersion7()}/scan", null);
|
||
Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
||
}
|
||
}
|