Files
Flaro/server/tests/Flaro.Api.Tests/PlaybackTests.cs
T
ScarriffleandClaude Opus 5 acba683c94 Eine Folge weiter, eine zurück — ohne den Spieler zu verlassen
Wer eine Serie schaut, will blättern können. Bisher gab es genau einen
Knopf dafür, und der erschien erst am Ende: „Nächste Folge", wenn der
Abspann durchgelaufen war. Zurück ging gar nicht — nur über die
Serienseite, und die beendet die Wiedergabe.

Jetzt stehen beide Knöpfe in der Bedienleiste, in der App wie im Web,
links und rechts vom Abspielknopf. Nur bei Serien: bei einem Film
antwortet der Server mit 404, bei der letzten Folge mit 204, und beides
heisst hier „kein Knopf". Sie erscheinen also von selbst nur dort, wo
sie etwas tun.

Der Server kannte nur den Weg vorwärts. Jetzt gibt es `/previous`
daneben, und beide Richtungen kommen aus derselben Rechnung — als zwei
Endpunkte abgeschrieben liefen sie auseinander, sobald einer etwas
dazubekommt. Jellyfin hat kein Gegenstück zu „NextUp"; dort entsteht
die vorige Folge aus der Folgenliste der Serie.

Beim Wechsel gilt weiter, was für die Serie an Sprache eingestellt ist.
Ohne das spränge die nächste Folge auf die erste Tonspur der Datei
zurück — bei deutschen Veröffentlichungen also auf Deutsch, mitten in
einer Serie, die man im Original schaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 21:11:09 +02:00

997 lines
41 KiB
C#

using System.Net;
using System.Net.Http.Json;
using Flaro.Api.Endpoints;
using Flaro.Core.Playback;
namespace Flaro.Api.Tests;
/// <summary>Fortschritt, Gesehen-Status und die Startseite — je Benutzer.</summary>
public class PlaybackTests
{
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)
{
var voll = Path.Combine(_bestand, relativ);
Directory.CreateDirectory(Path.GetDirectoryName(voll)!);
File.WriteAllBytes(voll, new byte[2048]);
}
/// <summary>Legt eine Bibliothek an, scannt sie und meldet den Client.</summary>
private async Task<HttpClient> Eingelesen(FlaroServer server, string art = "Movies")
{
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>())!;
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
for (var versuch = 0; versuch < 100; versuch++)
{
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek.Id}/scan");
if (stand!.State is "finished") return client;
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.");
return null!;
}
private static async Task<Guid> ErsterFilm(HttpClient client)
{
var seite = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
return seite!.Items[0].Id;
}
private static Task<HttpResponseMessage> Melden(HttpClient c, Guid id, double stelle, double? laenge) =>
c.PostAsJsonAsync($"/api/v1/items/{id}/progress", new ProgressRequest(stelle, laenge));
// ------------------------------------------------------------------
/// <summary>Der Fall, um dessentwillen der Server existiert.</summary>
///
/// <remarks>
/// Jellyfin verwirft bei zwei Stunden alles unterhalb von sechs Minuten —
/// <c>MinResumePct</c> steht auf 5. Hier kommt die Stelle zurück.
/// </remarks>
[Test]
public async Task Fuenf_Minuten_von_zwei_Stunden_bleiben_erhalten()
{
Datei("Der Untergang (2004)/Der Untergang (2004).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
var antwort = await Melden(client, film, 300, 7200);
var stand = (await antwort.Content.ReadFromJsonAsync<ItemStateResponse>())!;
Assert.Multiple(() =>
{
Assert.That(stand.PositionSeconds, Is.EqualTo(300));
Assert.That(stand.Watched, Is.False);
});
}
[Test]
public async Task Eine_Sekunde_wird_schon_gemerkt()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 1, 7200);
var seite = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.That(seite!.Items[0].State!.PositionSeconds, Is.EqualTo(1));
}
[Test]
public async Task Kurz_vor_Schluss_gilt_er_als_gesehen_und_behaelt_die_Stelle()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
var antwort = await Melden(client, film, 7000, 7200);
var stand = (await antwort.Content.ReadFromJsonAsync<ItemStateResponse>())!;
Assert.Multiple(() =>
{
Assert.That(stand.Watched, Is.True);
Assert.That(stand.PositionSeconds, Is.EqualTo(7000));
Assert.That(stand.PlayCount, Is.EqualTo(1));
});
}
[Test]
public async Task Angefangenes_steht_in_Weiterschauen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 600, 7200);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.Multiple(() =>
{
Assert.That(reihe!.Items, Has.Count.EqualTo(1));
Assert.That(reihe.Items[0].Title, Is.EqualTo("Matrix"));
Assert.That(reihe.Items[0].Kind, Is.EqualTo("movie"));
Assert.That(reihe.Items[0].State.PositionSeconds, Is.EqualTo(600));
});
}
/// <summary>„Weiterschauen" muss sagen, was abgespielt würde.</summary>
///
/// <remarks>
/// <b>Sonst kann der Aufmacher nur blättern.</b> Er bot „Abspielen" an
/// und öffnete die Detailseite — weil ihm die Datei fehlte. Und bei einer
/// Folge führte er auf <c>#/filme/&lt;Folgenkennung&gt;</c>, wo nichts
/// steht: die Detailseite gehört der Serie, der Fortschritt der Folge.
/// </remarks>
[Test]
public async Task Weiterschauen_nennt_Datei_und_Serie()
{
Datei("Dark (2017)/Season 01/Dark - S01E01 - Geheimnisse.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items.Single();
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes");
var folge = folgen!.Items.Single();
await Melden(client, folge.Id, 120, 1500);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
var zeile = reihe!.Items.Single();
Assert.Multiple(() =>
{
Assert.That(zeile.Kind, Is.EqualTo("episode"));
Assert.That(zeile.SeriesId, Is.EqualTo(serie.Id),
"ohne die Serie landet der Aufmacher auf einer Folge, die als Film nicht existiert");
Assert.That(zeile.FileId, Is.EqualTo(folge.Files.Single().Id),
"ohne die Datei kann „Abspielen\u201c nicht abspielen");
// Dieselbe Länge wie die Datei selbst. Im Prüfbestand liegen
// Attrappen ohne Spuren, also steht dort beidemal nichts — der
// Punkt ist, dass es derselbe Wert ist.
Assert.That(zeile.DurationSeconds, Is.EqualTo(folge.Files.Single().DurationSeconds));
});
}
/// <summary>Eine Folge muss sich für sich abrufen lassen.</summary>
///
/// <remarks>
/// <b>Die Apps reichen eine blanke Kennung durch.</b> Sie haben sie aus
/// „Weiterschauen" und wissen nicht, was dahintersteckt — also fragen sie
/// der Reihe nach. Fand sich weder Film noch Serie, meldeten sie „Dieser
/// Titel ist auf dem Server nicht mehr vorhanden", über eine Folge, die
/// sie selbst gerade anzeigten.
/// </remarks>
[Test]
public async Task Eine_Folge_laesst_sich_einzeln_abrufen()
{
Datei("Dark (2017)/Season 01/Dark - S01E01 - Geheimnisse.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items.Single();
var ausDerListe = (await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes"))!.Items.Single();
var einzeln = await client.GetFromJsonAsync<EpisodeResponse>(
$"/api/v1/episodes/{ausDerListe.Id}");
Assert.Multiple(() =>
{
Assert.That(einzeln!.Id, Is.EqualTo(ausDerListe.Id));
Assert.That(einzeln.Season, Is.EqualTo(ausDerListe.Season));
Assert.That(einzeln.Episode, Is.EqualTo(ausDerListe.Episode));
Assert.That(einzeln.SeriesId, Is.EqualTo(serie.Id),
"ohne die Serie fehlen der Folge Titel und Bild");
Assert.That(einzeln.Files, Has.Count.EqualTo(ausDerListe.Files.Count));
});
var unbekannt = await client.GetAsync($"/api/v1/episodes/{Guid.NewGuid()}");
Assert.That(unbekannt.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
[Test]
public async Task Durchgeschautes_verlaesst_Weiterschauen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 600, 7200);
await Melden(client, film, 7000, 7200);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.That(reihe!.Items, Is.Empty);
}
/// <summary>Ein zweites Mal angefangen — mit Häkchen und Stelle zugleich.</summary>
[Test]
public async Task Ein_gesehener_Film_kommt_beim_zweiten_Anlauf_zurueck()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 7000, 7200);
await Melden(client, film, 120, 7200);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.Multiple(() =>
{
Assert.That(reihe!.Items, Has.Count.EqualTo(1));
Assert.That(reihe.Items[0].State.Watched, Is.True, "gesehen hat er ihn ja");
Assert.That(reihe.Items[0].State.PositionSeconds, Is.EqualTo(120));
});
}
[Test]
public async Task Weglegen_nimmt_aus_der_Reihe_ohne_gesehen_zu_behaupten()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 1800, 7200);
(await client.PostAsync($"/api/v1/items/{film}/dismiss", null)).EnsureSuccessStatusCode();
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
var seite = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.Multiple(() =>
{
Assert.That(reihe!.Items, Is.Empty);
Assert.That(seite!.Items[0].State!.Watched, Is.False, "weggelegt heisst nicht gesehen");
Assert.That(seite.Items[0].State!.PositionSeconds, Is.EqualTo(1800), "die Stelle bleibt");
});
}
[Test]
public async Task Wer_doch_weiterschaut_holt_es_zurueck()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 1800, 7200);
await client.PostAsync($"/api/v1/items/{film}/dismiss", null);
await Melden(client, film, 1900, 7200);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.That(reihe!.Items, Has.Count.EqualTo(1));
}
[Test]
public async Task Von_Hand_als_gesehen_raeumt_die_Stelle_weg()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 1800, 7200);
var antwort = await client.PutAsJsonAsync($"/api/v1/items/{film}/watched", new WatchedRequest(true));
var stand = (await antwort.Content.ReadFromJsonAsync<ItemStateResponse>())!;
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.Multiple(() =>
{
Assert.That(stand.Watched, Is.True);
Assert.That(stand.PositionSeconds, Is.Zero);
Assert.That(reihe!.Items, Is.Empty);
});
}
[Test]
public async Task Favorit_und_Bewertung_bleiben_stehen()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await client.PutAsJsonAsync($"/api/v1/items/{film}/favourite", new FavouriteRequest(true));
await client.PutAsJsonAsync($"/api/v1/items/{film}/rating", new RatingRequest(9));
var einzeln = await client.GetFromJsonAsync<MovieResponse>($"/api/v1/movies/{film}");
Assert.Multiple(() =>
{
Assert.That(einzeln!.State!.Favourite, Is.True);
Assert.That(einzeln.State!.Rating, Is.EqualTo(9));
});
}
[Test]
public async Task Eine_Bewertung_ausserhalb_von_eins_bis_zehn_wird_abgelehnt()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
var antwort = await client.PutAsJsonAsync($"/api/v1/items/{film}/rating", new RatingRequest(11));
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
/// <summary>Zwei Leute im selben Haushalt stehen an verschiedenen Stellen.</summary>
[Test]
public async Task Der_Stand_gehoert_dem_Benutzer_nicht_dem_Server()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var admin = await Eingelesen(server);
var film = await ErsterFilm(admin);
(await admin.PostAsJsonAsync("/api/v1/users",
new CreateUserRequest("mitbewohner", "ein-langes-passwort", false))).EnsureSuccessStatusCode();
var zweiter = server.AlsAngemeldeter(
(await server.SignIn("mitbewohner", "ein-langes-passwort", "iPad", "geraet-2")).Token);
await Melden(admin, film, 600, 7200);
await Melden(zweiter, film, 3000, 7200);
var seiteA = await admin.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
var seiteB = await zweiter.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.Multiple(() =>
{
Assert.That(seiteA!.Items[0].State!.PositionSeconds, Is.EqualTo(600));
Assert.That(seiteB!.Items[0].State!.PositionSeconds, Is.EqualTo(3000));
});
}
[Test]
public async Task Einen_Titel_den_es_nicht_gibt_kann_man_nicht_melden()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var antwort = await Melden(client, Guid.CreateVersion7(), 60, 7200);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
/// <summary>Ohne Angabe nimmt der Server die Laufzeit aus der Datei.</summary>
///
/// <remarks>
/// Die Prüfdateien haben keine lesbare Laufzeit — ffprobe findet nichts in
/// zweitausend Nullbytes. Ohne Laufzeit darf nichts vermutet werden, und
/// genau das wird hier festgehalten.
/// </remarks>
[Test]
public async Task Ohne_Laufzeit_wird_nichts_als_gesehen_vermutet()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
var antwort = await Melden(client, film, 99999, null);
var stand = (await antwort.Content.ReadFromJsonAsync<ItemStateResponse>())!;
Assert.That(stand.Watched, Is.False);
}
// --- Serien --------------------------------------------------------
[Test]
public async Task Ungesehene_Folgen_werden_gezaehlt()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
Datei("Dark/Season 01/Dark - S01E03 - Gestern und heute.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
Assert.That(serie.UnwatchedCount, Is.EqualTo(3));
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>($"/api/v1/series/{serie.Id}/episodes");
await client.PutAsJsonAsync($"/api/v1/items/{folgen!.Items[0].Id}/watched", new WatchedRequest(true));
var danach = await client.GetFromJsonAsync<SeriesResponse>($"/api/v1/series/{serie.Id}");
Assert.That(danach!.UnwatchedCount, Is.EqualTo(2));
}
/// <summary>Eine Serie als gesehen markieren hakt jede Folge ab.</summary>
///
/// <remarks>
/// <b>Der Fall, der vorher stumm scheiterte.</b> Der Server kannte zu
/// einer Serienkennung keinen Zustand und antwortete mit 404; die
/// Oberfläche verschluckte die Ablehnung, und der Klick tat sichtbar
/// nichts.
/// </remarks>
[Test]
public async Task Eine_Serie_als_gesehen_markieren_hakt_alle_Folgen_ab()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
Datei("Dark/Season 02/Dark - S02E01 - Beginnings.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
var antwort = await client.PutAsJsonAsync(
$"/api/v1/items/{serie.Id}/watched", new WatchedRequest(true));
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK),
"eine Serienkennung ist eine gültige Titelkennung");
var danach = await client.GetFromJsonAsync<SeriesResponse>($"/api/v1/series/{serie.Id}");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes");
Assert.Multiple(() =>
{
Assert.That(danach!.UnwatchedCount, Is.Zero, "keine Folge steht mehr aus");
Assert.That(danach.State?.Watched, Is.True, "und die Serie selbst trägt das Häkchen");
Assert.That(folgen!.Items.Select(f => f.State?.Watched), Is.All.True,
"jede einzelne Folge ist abgehakt, nicht nur die Serie");
});
}
/// <summary>Und wieder zurück — ohne Rest.</summary>
[Test]
public async Task Eine_Serie_als_ungesehen_markieren_nimmt_jedes_Haekchen_weg()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
await client.PutAsJsonAsync($"/api/v1/items/{serie.Id}/watched", new WatchedRequest(true));
await client.PutAsJsonAsync($"/api/v1/items/{serie.Id}/watched", new WatchedRequest(false));
var danach = await client.GetFromJsonAsync<SeriesResponse>($"/api/v1/series/{serie.Id}");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes");
Assert.Multiple(() =>
{
Assert.That(danach!.UnwatchedCount, Is.EqualTo(2));
Assert.That(danach.State?.Watched, Is.False);
Assert.That(folgen!.Items.Select(f => f.State?.Watched), Is.All.False);
});
}
/// <summary>Favorit an einer Serie — vorher ebenfalls eine stille 404.</summary>
[Test]
public async Task Eine_Serie_laesst_sich_zu_den_Favoriten_legen()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
var antwort = await client.PutAsJsonAsync(
$"/api/v1/items/{serie.Id}/favourite", new FavouriteRequest(true));
antwort.EnsureSuccessStatusCode();
var liste = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
Assert.That(liste!.Items[0].State?.Favourite, Is.True,
"auch die Liste führt den Zustand mit — sonst wüsste die Kachel es nicht");
}
/// <summary>Der Stand einer Serie gehört dem Benutzer, nicht dem Server.</summary>
[Test]
public async Task Der_Serienstand_gehoert_dem_Benutzer()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
using var server = new FlaroServer();
var ich = await Eingelesen(server, "Shows");
var serien = await ich.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
await ich.PutAsJsonAsync($"/api/v1/items/{serie.Id}/watched", new WatchedRequest(true));
(await ich.PostAsJsonAsync("/api/v1/users",
new CreateUserRequest("mitbewohner", "ein-langes-passwort", false))).EnsureSuccessStatusCode();
var andere = server.AlsAngemeldeter(
(await server.SignIn("mitbewohner", "ein-langes-passwort", "iPad", "geraet-2")).Token);
var seine = await andere.GetFromJsonAsync<SeriesResponse>($"/api/v1/series/{serie.Id}");
Assert.Multiple(() =>
{
Assert.That(seine!.State?.Watched, Is.Not.True);
Assert.That(seine.UnwatchedCount, Is.EqualTo(1));
});
}
/// <summary>Viele auf einmal — eine Anfrage, nicht zweihundert.</summary>
[Test]
public async Task Eine_Sammelmarkierung_hakt_alles_Gewaehlte_ab()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
Datei("Alien (1979)/Alien (1979).mkv");
Datei("Heat (1995)/Heat (1995).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var seite = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
var zwei = seite!.Items.Take(2).Select(m => m.Id).ToList();
var antwort = await client.PutAsJsonAsync("/api/v1/items/watched",
new BulkWatchedRequest(zwei, true));
var ergebnis = await antwort.Content.ReadFromJsonAsync<BulkWatchedResponse>();
var danach = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
Assert.Multiple(() =>
{
Assert.That(ergebnis!.Changed, Is.EqualTo(2));
Assert.That(ergebnis.Unknown, Is.Empty);
Assert.That(danach!.Items.Count(m => m.State?.Watched == true), Is.EqualTo(2),
"genau die zwei gewählten, nicht der dritte");
});
}
/// <summary>Was es nicht gibt, wird benannt statt verschwiegen.</summary>
[Test]
public async Task Eine_Sammelmarkierung_nennt_was_sie_nicht_gefunden_hat()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
var erfunden = Guid.CreateVersion7();
var antwort = await client.PutAsJsonAsync("/api/v1/items/watched",
new BulkWatchedRequest([film, erfunden], true));
var ergebnis = await antwort.Content.ReadFromJsonAsync<BulkWatchedResponse>();
Assert.Multiple(() =>
{
Assert.That(ergebnis!.Changed, Is.EqualTo(1));
Assert.That(ergebnis.Unknown, Is.EqualTo(new[] { erfunden }));
});
}
/// <summary>Auch Serien gehören in eine Sammelmarkierung.</summary>
[Test]
public async Task Eine_Sammelmarkierung_zieht_die_Folgen_einer_Serie_mit()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
await client.PutAsJsonAsync("/api/v1/items/watched",
new BulkWatchedRequest([serie.Id], true));
var danach = await client.GetFromJsonAsync<SeriesResponse>($"/api/v1/series/{serie.Id}");
Assert.That(danach!.UnwatchedCount, Is.Zero);
}
[Test]
public async Task Eine_leere_Sammelmarkierung_aendert_nichts_und_scheitert_nicht()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var antwort = await client.PutAsJsonAsync("/api/v1/items/watched",
new BulkWatchedRequest([], true));
var ergebnis = await antwort.Content.ReadFromJsonAsync<BulkWatchedResponse>();
Assert.Multiple(() =>
{
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(ergebnis!.Changed, Is.Zero);
});
}
/// <summary>Eine Folge in „Weiterschauen" nennt ihre Serie.</summary>
///
/// <remarks>
/// <b>Ohne diese Angabe führte die Kachel ins Leere.</b> Sie trug die
/// Folgenkennung und öffnete damit eine Serienseite — die es unter dieser
/// Kennung nie gibt. Heraus kam „Diese Serie gibt es nicht mehr",
/// ausgerechnet unter „Nächste Folge", wo man gerade weiterschauen wollte.
/// Es lag nicht am Scan und nicht am Auffrischen: die Kachel hat von
/// Anfang an auf die falsche Kennung gezeigt.
/// </remarks>
[Test]
public async Task Eine_angefangene_Folge_nennt_ihre_Serie()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes");
// Mittendrin stehenbleiben — dann steht die Folge in „Weiterschauen".
await Melden(client, folgen!.Items[0].Id, 600, 7200);
var weiter = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
var eintrag = weiter!.Items.Single();
Assert.Multiple(() =>
{
Assert.That(eintrag.Id, Is.EqualTo(folgen.Items[0].Id), "die Kennung bleibt die der Folge");
Assert.That(eintrag.SeriesId, Is.EqualTo(serie.Id), "und daneben steht, wohin sie gehört");
});
}
/// <summary>Dasselbe für „Nächste Folge".</summary>
[Test]
public async Task Die_naechste_Folge_nennt_ihre_Serie()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var serie = serien!.Items[0];
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serie.Id}/episodes");
// Die erste ganz gesehen — dann ist die zweite „die nächste".
await client.PutAsJsonAsync($"/api/v1/items/{folgen!.Items[0].Id}/watched", new WatchedRequest(true));
var naechste = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/next");
var eintrag = naechste!.Items.Single();
Assert.Multiple(() =>
{
Assert.That(eintrag.Id, Is.EqualTo(folgen.Items[1].Id));
Assert.That(eintrag.SeriesId, Is.EqualTo(serie.Id));
});
}
/// <summary>Ein Film hat keine Serie — und behauptet es auch nicht.</summary>
[Test]
public async Task Ein_angefangener_Film_nennt_keine_Serie()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
await Melden(client, film, 600, 7200);
var weiter = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.That(weiter!.Items.Single().SeriesId, Is.Null);
}
[Test]
public async Task Die_naechste_Folge_richtet_sich_nach_Staffel_und_Nummer()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
var naechste = await client.GetFromJsonAsync<EpisodeResponse>(
$"/api/v1/episodes/{folgen!.Items[0].Id}/next");
var hinterDerLetzten = await client.GetAsync($"/api/v1/episodes/{folgen.Items[1].Id}/next");
Assert.Multiple(() =>
{
Assert.That(naechste!.Episode, Is.EqualTo(2));
Assert.That(hinterDerLetzten.StatusCode, Is.EqualTo(HttpStatusCode.NoContent),
"nach der letzten Folge kommt keine — das ist kein Fehler");
});
}
/// <summary>Und derselbe Weg rückwärts.</summary>
///
/// <remarks>
/// Für die Knöpfe im Spieler: wer eine Folge zu weit gesprungen ist,
/// soll zurück können, ohne über die Serienseite zu gehen.
/// </remarks>
[Test]
public async Task Die_vorige_Folge_gibt_es_auch()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
Datei("Dark/Season 02/Dark - S02E01 - Dunkel.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
var geordnet = folgen!.Items.OrderBy(f => f.Season).ThenBy(f => f.Episode).ToList();
var vorige = await client.GetFromJsonAsync<EpisodeResponse>(
$"/api/v1/episodes/{geordnet[1].Id}/previous");
// Über die Staffelgrenze: vor S02E01 steht S01E02.
var ueberDieGrenze = await client.GetFromJsonAsync<EpisodeResponse>(
$"/api/v1/episodes/{geordnet[2].Id}/previous");
var vorDerErsten = await client.GetAsync($"/api/v1/episodes/{geordnet[0].Id}/previous");
Assert.Multiple(() =>
{
Assert.That(vorige!.Episode, Is.EqualTo(1));
Assert.That(ueberDieGrenze!.Season, Is.EqualTo(1));
Assert.That(ueberDieGrenze.Episode, Is.EqualTo(2));
Assert.That(vorDerErsten.StatusCode, Is.EqualTo(HttpStatusCode.NoContent),
"vor der ersten Folge steht keine");
});
}
[Test]
public async Task Die_erste_ungesehene_Folge_steht_unter_Naechste_Folge()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
await client.PutAsJsonAsync($"/api/v1/items/{folgen!.Items[0].Id}/watched", new WatchedRequest(true));
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/next");
Assert.Multiple(() =>
{
Assert.That(reihe!.Items, Has.Count.EqualTo(1));
Assert.That(reihe.Items[0].Episode, Is.EqualTo(2));
Assert.That(reihe.Items[0].Title, Is.EqualTo("Dark"), "oben die Serie");
});
}
/// <summary>Wer bei Folge 2 steht, bekommt nicht Folge 1 angeboten.</summary>
///
/// <remarks>
/// <b>Der Fall aus dem Gebrauch.</b> Folge 1 halb gesehen und liegen
/// gelassen, dann bei Folge 2 weitergemacht — und „Als Nächstes" schlug
/// Folge 1 vor, weil sie die erste ohne Häkchen war. Die Reihe sagt aber
/// nicht, was man übersprungen hat, sondern was als Nächstes dran ist.
/// </remarks>
[Test]
public async Task Wer_bei_Folge_zwei_steht_bekommt_nicht_Folge_eins_vorgeschlagen()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
Datei("Dark/Season 01/Dark - S01E03 - Gestern.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
// Folge 1 angefangen und liegen gelassen, Folge 2 läuft gerade.
await Melden(client, folgen!.Items[0].Id, 300, 2700);
await Melden(client, folgen.Items[1].Id, 900, 2700);
var naechste = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/next");
Assert.That(naechste!.Items, Is.Empty,
"Folge 2 läuft und steht in Weiterschauen, also hat Als Naechstes nichts anzubieten");
}
/// <summary>Und abhaken holt die Serie nicht zurück.</summary>
///
/// <remarks>
/// Wer Folge 1 nachträglich abhakt, hat damit nicht gesagt, dass er
/// Folge 2 noch einmal anfangen will — die läuft ja.
/// </remarks>
[Test]
public async Task Folge_eins_abhaken_holt_die_Serie_nicht_zurueck()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
await Melden(client, folgen!.Items[1].Id, 900, 2700);
await client.PutAsJsonAsync($"/api/v1/items/{folgen.Items[0].Id}/watched",
new WatchedRequest(true));
var naechste = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/next");
Assert.That(naechste!.Items, Is.Empty);
}
/// <summary>Nach der letzten angefassten Folge geht es weiter.</summary>
///
/// <remarks>
/// Folge 1 und 2 durch, Folge 3 nie angefasst: dann ist Folge 3 dran —
/// und nicht Folge 1, nur weil dort irgendwann einmal ein Häkchen
/// fehlte.
/// </remarks>
[Test]
public async Task Nach_der_letzten_gesehenen_Folge_kommt_die_naechste()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
Datei("Dark/Season 01/Dark - S01E03 - Gestern.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
await client.PutAsJsonAsync($"/api/v1/items/{folgen!.Items[1].Id}/watched",
new WatchedRequest(true));
var naechste = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/next");
Assert.That(naechste!.Items.Single().Episode, Is.EqualTo(3));
}
/// <summary>Was in „Weiterschauen" steht, steht nicht nochmal daneben.</summary>
[Test]
public async Task Eine_angefangene_Folge_erscheint_nur_in_einer_Reihe()
{
Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv");
Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server, "Shows");
var serien = await client.GetFromJsonAsync<Page<SeriesResponse>>("/api/v1/series");
var folgen = await client.GetFromJsonAsync<Page<EpisodeResponse>>(
$"/api/v1/series/{serien!.Items[0].Id}/episodes");
await Melden(client, folgen!.Items[0].Id, 400, 2700);
var weiter = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
var naechste = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/next");
Assert.Multiple(() =>
{
Assert.That(weiter!.Items, Has.Count.EqualTo(1));
Assert.That(weiter.Items[0].Episode, Is.EqualTo(1));
Assert.That(naechste!.Items, Is.Empty);
});
}
/// <summary>Die Spalte und die Regel dürfen nicht auseinanderlaufen.</summary>
///
/// <remarks>
/// <c>InProgress</c> ist das festgehaltene Ergebnis von
/// <see cref="ProgressRule.BelongsInContinueWatching"/>. Wer die Regel
/// ändert und das Nachführen vergisst, bekommt eine Startseite, die lügt —
/// und zwar lautlos. Deshalb hier gegeneinandergehalten.
/// </remarks>
[Test]
public async Task Die_Reihe_zeigt_genau_das_was_die_Regel_sagt()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
foreach (var (stelle, laenge) in new (double, double?)[]
{ (1, 7200), (300, 7200), (7000, 7200), (120, 7200) })
{
await Melden(client, film, stelle, laenge);
var erwartet = ProgressRule.BelongsInContinueWatching(new UserItemState
{
UserId = Guid.Empty,
MovieId = film,
Position = TimeSpan.FromSeconds(stelle),
Runtime = laenge is { } l ? TimeSpan.FromSeconds(l) : null,
}, Wiedergaberegeln.Vorgabe);
var reihe = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.That(reihe!.Items.Count == 1, Is.EqualTo(erwartet),
$"bei {stelle}s von {laenge}s");
}
}
/// <summary>Eine neue Schwelle wirkt auch auf das, was schon dasteht.</summary>
///
/// <remarks>
/// <b>Der Grund, aus dem der Endpunkt eine Zahl zurückgibt.</b>
/// <c>InProgress</c> ist eine gespeicherte Spalte — die Bruchrechnung
/// lässt sich nicht nach SQL übersetzen. Ohne Durchlauf stünde der Titel,
/// in den man gestern eine Sekunde hineingeschaut hat, weiter in
/// „Weiterschauen", und die Einstellung sähe aus, als täte sie nichts.
/// </remarks>
[Test]
public async Task Eine_neue_Schwelle_raeumt_Weiterschauen_auf()
{
Datei("Matrix (1999)/Matrix (1999).mkv");
using var server = new FlaroServer();
var client = await Eingelesen(server);
var film = await ErsterFilm(client);
// Erst ohne Schwelle: eine Sekunde genügt.
await client.PutAsJsonAsync("/api/v1/settings/playback",
new PlaybackSettingsRequest(0, null, null));
await Melden(client, film, 1, 7200);
var vorher = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.That(vorher!.Items, Has.Count.EqualTo(1), "ohne Schwelle steht er da");
// Jetzt neunzig Sekunden — und zwar rückwirkend.
var antwort = await client.PutAsJsonAsync("/api/v1/settings/playback",
new PlaybackSettingsRequest(90, null, null));
var ergebnis = (await antwort.Content.ReadFromJsonAsync<PlaybackSettingsApplied>())!;
var nachher = await client.GetFromJsonAsync<Page<ContinueEntry>>("/api/v1/home/continue");
Assert.Multiple(() =>
{
Assert.That(ergebnis.ResumeAfterSeconds, Is.EqualTo(90));
Assert.That(ergebnis.Reevaluated, Is.EqualTo(1), "eine Zeile hat sich geändert");
Assert.That(nachher!.Items, Is.Empty, "und die Reihe ist leer");
});
// Die Stelle selbst überlebt — das ist der Punkt des ganzen Servers.
var eintrag = await client.GetFromJsonAsync<MovieResponse>($"/api/v1/movies/{film}");
Assert.That(eintrag?.State?.PositionSeconds, Is.EqualTo(1).Within(0.001));
}
}