Der Scan erfindet keine Serie mehr, wenn weder Ordner noch Dateiname eine nennen — richtig so. Nur wurde die Datei dann übersprungen, ohne Zeile, ohne Spur. In der Mediathek standen 58 Serien und niemand konnte sagen, wo die anderen geblieben sind. Jetzt bekommt jede solche Datei eine Zeile ohne Zuordnung. Damit ist sie zählbar, auflistbar und nach einer besseren Regel nachträglich zuzuordnen. Untersucht wird sie nicht: ffprobe auf 3559 Dateien kostet eine halbe Stunde für Angaben, die niemand zu sehen bekommt, und sobald die Zuordnung steht, holt der nächste Lauf es nach. Dazu ein Abschnitt "Nicht zugeordnet" in der Verwaltung, nach Grund gebündelt und mit den Pfaden zum Aufklappen. Zwei Gründe gibt es, und sie verlangen Verschiedenes: fehlt die Folgennummer, muss umbenannt werden; fehlt der Serienname, genügt ein Ordner darum herum. Die Liste lässt sich als Datei herunterladen — eine, die man markieren und kopieren muss, schickt niemand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
572 lines
25 KiB
C#
572 lines
25 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"));
|
||
}
|
||
|
||
[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));
|
||
}
|
||
}
|