Bisher gab es in der Seitenleiste „Filme" und „Serien" — zwei feste Zeilen, hinter denen alles zusammenfiel, was der Server kannte. Wer „Filme" und „Dokumentationen" getrennt angelegt hatte, sah sie wieder in einem Topf. Das nahm die Entscheidung zurück, sie zu trennen. Jetzt führt die Seitenleiste **jede Mediathek einzeln**, mit Namen und Zahl, wie bei Plex und Jellyfin. Die Startseite bekommt je Mediathek eine Reihe mit dem zuletzt Hinzugefügten; steht mehr drin, als die Reihe zeigt, führt „Alle zeigen" dorthin. Die Mediathekenseite trägt den Namen im Titel, sucht innerhalb der Mediathek und lässt sich sortieren. **Am Server dafür:** `?sort=` auf `/movies` und `/series` — `title`, `added`, `year`. Der Titel immer als letzter Vergleich, sonst hätten zwei Filme aus demselben Jahr keine feste Reihenfolge und Seite 2 zeigte denselben Film noch einmal oder gar nicht. Die Liste der Bibliotheken nennt je Zeile einen Zählwert. „Dokumentationen · 0" sagt mehr als ein Eintrag, hinter dem sich nach dem Klick nichts findet — da ist der Unterschied zwischen „noch nicht gescannt" und „falscher Pfad" wenigstens sichtbar. `null` heisst „nicht gezählt", nicht „leer". Umbenennen und Entfernen, beides nur für Administratoren. **Umbenennen, mehr nicht:** die Art zu ändern hiesse, Filme in Serien zu verwandeln; den Pfad zu ändern hiesse, einen Bestand gegen einen anderen zu tauschen, während die Einträge stehenbleiben. Beides ist kein Bearbeiten, sondern eine neue Bibliothek. **Beim Entfernen bleiben die Mediendateien liegen.** Weg sind der Eintrag, die Zuordnungen und der Sehfortschritt aller Benutzer dazu; die Oberfläche sagt das vorher, und eine Prüfung hält fest, dass die Datei danach noch da ist. Ein Server, der beim Aufräumen in der Oberfläche fremde Filme löscht, ist einer, den man nicht noch einmal benutzt. --- **Der Fehler, den der Browser zeigte, und er betrifft jeden Betrieb:** nach einem Server-Update lief weiter die alte Oberfläche. Die Dateien unter `assets/` tragen eine Prüfsumme im Namen — ändert sich der Inhalt, ändert sich der Name. `index.html` trägt keine; sie nennt nur deren Namen. Lag sie im Zwischenspeicher, bekam der Browser weiter die alte App: nicht kaputt, sondern veraltet, und das fällt niemandem auf. Nachgemessen: geladen war `index-C0K3V8eg.js`, ausgeliefert wurde längst `index-DomHN4_b.js`. Jetzt zwei Lebensdauern — `no-cache` für die Seite, ein Jahr und `immutable` für die Bausteine. Mit Prüfung, und am Container nachgemessen. Der Beispielbestand bekommt zwei weitere Mediatheken, „Dokumentationen" und „Konzerte". Mit nur Filme und Serien liesse sich gar nicht prüfen, ob mehrere wirklich getrennt bleiben — zwei Bestände **derselben Art** sind der Fall, in dem es auffällt. 358 Tests grün. Am Container nachgewiesen: vier Mediatheken in der Seitenleiste mit Zählwerten, je eine Reihe auf der Startseite, und „Dokumentationen" zeigt zwei Titel statt sieben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
248 lines
10 KiB
C#
248 lines
10 KiB
C#
using System.Net;
|
||
using System.Net.Http.Json;
|
||
using Flaro.Api.Endpoints;
|
||
|
||
namespace Flaro.Api.Tests;
|
||
|
||
/// <summary>Mehrere Mediatheken nebeneinander — wie bei Plex und Jellyfin.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Zwei Filmbibliotheken sind keine doppelte Filmbibliothek.</b> „Filme"
|
||
/// und „Dokumentationen" liegen in verschiedenen Ordnern, gehören in der
|
||
/// Seitenleiste getrennt und dürfen sich in keiner Liste vermischen. Dass
|
||
/// das Modell es kann, hiess bisher nicht, dass es geprüft war.
|
||
/// </remarks>
|
||
public class MultipleLibraryTests
|
||
{
|
||
private readonly List<string> _ordner = [];
|
||
|
||
[TearDown]
|
||
public void Aufraeumen()
|
||
{
|
||
foreach (var o in _ordner)
|
||
{
|
||
try { Directory.Delete(o, recursive: true); } catch (IOException) { /* Kür */ }
|
||
}
|
||
_ordner.Clear();
|
||
}
|
||
|
||
/// <summary>Ein Ordner mit den genannten Dateien darin.</summary>
|
||
private string Bestand(params string[] dateien)
|
||
{
|
||
var wurzel = Directory.CreateDirectory(
|
||
Path.Combine(Path.GetTempPath(), "flaro-mehrere-" + Guid.CreateVersion7().ToString("N"))).FullName;
|
||
_ordner.Add(wurzel);
|
||
|
||
foreach (var relativ in dateien)
|
||
{
|
||
var voll = Path.Combine(wurzel, relativ);
|
||
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
|
||
File.WriteAllBytes(voll, new byte[2048]);
|
||
}
|
||
return wurzel;
|
||
}
|
||
|
||
private static async Task<LibraryResponse> Anlegen(
|
||
HttpClient client, string name, string art, string pfad)
|
||
{
|
||
var antwort = await client.PostAsJsonAsync("/api/v1/libraries",
|
||
new CreateLibraryRequest(name, art, pfad));
|
||
antwort.EnsureSuccessStatusCode();
|
||
return (await antwort.Content.ReadFromJsonAsync<LibraryResponse>())!;
|
||
}
|
||
|
||
private static async Task Scannen(HttpClient client, Guid bibliothek)
|
||
{
|
||
(await client.PostAsync($"/api/v1/libraries/{bibliothek}/scan", null)).EnsureSuccessStatusCode();
|
||
for (var versuch = 0; versuch < 100; versuch++)
|
||
{
|
||
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek}/scan");
|
||
if (stand!.State is "finished") return;
|
||
if (stand.State is "failed") Assert.Fail($"Der Scan ist gescheitert: {stand.Error}");
|
||
await Task.Delay(50);
|
||
}
|
||
Assert.Fail("Der Scan wurde in fünf Sekunden nicht fertig.");
|
||
}
|
||
|
||
[Test]
|
||
public async Task Zwei_Filmbibliotheken_bleiben_getrennt()
|
||
{
|
||
var spielfilme = Bestand("Matrix (1999)/Matrix (1999).mkv", "Inception (2010)/Inception (2010).mkv");
|
||
var dokus = Bestand("Winged Migration (2001)/Winged Migration (2001).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var client = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
|
||
var a = await Anlegen(client, "Filme", "Movies", spielfilme);
|
||
var b = await Anlegen(client, "Dokumentationen", "Movies", dokus);
|
||
await Scannen(client, a.Id);
|
||
await Scannen(client, b.Id);
|
||
|
||
var inA = await client.GetFromJsonAsync<Page<MovieResponse>>($"/api/v1/movies?library={a.Id}");
|
||
var inB = await client.GetFromJsonAsync<Page<MovieResponse>>($"/api/v1/movies?library={b.Id}");
|
||
var alle = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(inA!.Total, Is.EqualTo(2));
|
||
Assert.That(inB!.Total, Is.EqualTo(1));
|
||
Assert.That(inB.Items[0].Title, Is.EqualTo("Winged Migration"));
|
||
// Ohne Einschränkung alle zusammen — für die Suche quer durch alles.
|
||
Assert.That(alle!.Total, Is.EqualTo(3));
|
||
});
|
||
}
|
||
|
||
/// <summary>Die Seitenleiste braucht zu jeder Zeile eine Zahl.</summary>
|
||
[Test]
|
||
public async Task Die_Liste_nennt_je_Bibliothek_die_Anzahl()
|
||
{
|
||
var filme = Bestand("Matrix (1999)/Matrix (1999).mkv");
|
||
var serien = Bestand("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv",
|
||
"Dark/Season 01/Dark - S01E02 - Luegen.mkv");
|
||
var leer = Bestand();
|
||
|
||
using var server = new FlaroServer();
|
||
var client = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
|
||
var a = await Anlegen(client, "Filme", "Movies", filme);
|
||
var b = await Anlegen(client, "Serien", "Shows", serien);
|
||
await Anlegen(client, "Noch nichts drin", "Movies", leer);
|
||
await Scannen(client, a.Id);
|
||
await Scannen(client, b.Id);
|
||
|
||
var liste = await client.GetFromJsonAsync<List<LibraryResponse>>("/api/v1/libraries");
|
||
var nachNamen = liste!.ToDictionary(l => l.Name);
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(nachNamen["Filme"].ItemCount, Is.EqualTo(1));
|
||
// **Serien, nicht Folgen.** Zwei Folgen einer Serie sind eine Zeile.
|
||
Assert.That(nachNamen["Serien"].ItemCount, Is.EqualTo(1));
|
||
Assert.That(nachNamen["Noch nichts drin"].ItemCount, Is.Zero);
|
||
});
|
||
}
|
||
|
||
[Test]
|
||
public async Task Eine_Bibliothek_laesst_sich_umbenennen()
|
||
{
|
||
using var server = new FlaroServer();
|
||
var client = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
var angelegt = await Anlegen(client, "Flime", "Movies", Bestand());
|
||
|
||
var antwort = await client.PutAsJsonAsync($"/api/v1/libraries/{angelegt.Id}",
|
||
new RenameLibraryRequest("Filme"));
|
||
var geaendert = await antwort.Content.ReadFromJsonAsync<LibraryResponse>();
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(geaendert!.Name, Is.EqualTo("Filme"));
|
||
Assert.That(geaendert.Path, Is.EqualTo(angelegt.Path), "der Pfad bleibt, wo er war");
|
||
});
|
||
}
|
||
|
||
[Test]
|
||
public async Task Ein_leerer_Name_wird_abgelehnt()
|
||
{
|
||
using var server = new FlaroServer();
|
||
var client = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
var angelegt = await Anlegen(client, "Filme", "Movies", Bestand());
|
||
|
||
var antwort = await client.PutAsJsonAsync($"/api/v1/libraries/{angelegt.Id}",
|
||
new RenameLibraryRequest(" "));
|
||
|
||
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
||
}
|
||
|
||
/// <summary>Entfernt wird der Eintrag — nicht der Film auf der Platte.</summary>
|
||
///
|
||
/// <remarks>
|
||
/// <b>Das ist die wichtigste Zusage dieses Servers.</b> Ein Server, der
|
||
/// beim Aufräumen in der Oberfläche fremde Mediendateien löscht, ist
|
||
/// einer, den man nicht noch einmal benutzt.
|
||
/// </remarks>
|
||
[Test]
|
||
public async Task Loeschen_nimmt_die_Eintraege_und_laesst_die_Dateien_liegen()
|
||
{
|
||
var ordner = Bestand("Matrix (1999)/Matrix (1999).mkv");
|
||
var datei = Path.Combine(ordner, "Matrix (1999)", "Matrix (1999).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var client = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
var angelegt = await Anlegen(client, "Filme", "Movies", ordner);
|
||
await Scannen(client, angelegt.Id);
|
||
|
||
Assert.That((await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies"))!.Total,
|
||
Is.EqualTo(1), "vorher ist er da");
|
||
|
||
var geloescht = await client.DeleteAsync($"/api/v1/libraries/{angelegt.Id}");
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(geloescht.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
|
||
Assert.That(File.Exists(datei), Is.True, "die Datei liegt noch da");
|
||
});
|
||
|
||
var danach = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
var liste = await client.GetFromJsonAsync<List<LibraryResponse>>("/api/v1/libraries");
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(danach!.Total, Is.Zero);
|
||
Assert.That(liste, Is.Empty);
|
||
});
|
||
}
|
||
|
||
[Test]
|
||
public async Task Nur_Administratoren_duerfen_loeschen_und_umbenennen()
|
||
{
|
||
using var server = new FlaroServer();
|
||
var admin = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
var angelegt = await Anlegen(admin, "Filme", "Movies", Bestand());
|
||
|
||
(await admin.PostAsJsonAsync("/api/v1/users",
|
||
new CreateUserRequest("mitbewohner", "ein-langes-passwort", false))).EnsureSuccessStatusCode();
|
||
var gast = server.AlsAngemeldeter(
|
||
(await server.SignIn("mitbewohner", "ein-langes-passwort", "iPad", "geraet-2")).Token);
|
||
|
||
var umbenannt = await gast.PutAsJsonAsync($"/api/v1/libraries/{angelegt.Id}",
|
||
new RenameLibraryRequest("Meins jetzt"));
|
||
var geloescht = await gast.DeleteAsync($"/api/v1/libraries/{angelegt.Id}");
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(umbenannt.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
||
Assert.That(geloescht.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
|
||
});
|
||
}
|
||
|
||
/// <summary>„Zuletzt hinzugefügt" braucht eine andere Reihenfolge als A–Z.</summary>
|
||
[Test]
|
||
public async Task Nach_Aufnahme_sortiert_steht_das_Neueste_oben()
|
||
{
|
||
var ordner = Bestand("Zzz Zuletzt (2024)/Zzz Zuletzt (2024).mkv");
|
||
|
||
using var server = new FlaroServer();
|
||
var client = server.AlsAngemeldeter((await server.SetUpAndSignIn()).Token);
|
||
var angelegt = await Anlegen(client, "Filme", "Movies", ordner);
|
||
await Scannen(client, angelegt.Id);
|
||
|
||
// Ein zweiter Film, nachweislich später aufgenommen.
|
||
var voll = Path.Combine(ordner, "Aaa Zuerst (1999)", "Aaa Zuerst (1999).mkv");
|
||
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
|
||
await File.WriteAllBytesAsync(voll, new byte[2048]);
|
||
await Task.Delay(1100);
|
||
await Scannen(client, angelegt.Id);
|
||
|
||
var alphabetisch = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||
var nachAufnahme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies?sort=added");
|
||
|
||
Assert.Multiple(() =>
|
||
{
|
||
Assert.That(alphabetisch!.Items.Select(m => m.Title),
|
||
Is.EqualTo(new[] { "Aaa Zuerst", "Zzz Zuletzt" }));
|
||
Assert.That(nachAufnahme!.Items[0].Title, Is.EqualTo("Aaa Zuerst"),
|
||
"zuletzt gefunden steht oben");
|
||
});
|
||
}
|
||
}
|