using System.Net; using System.Net.Http.Json; using Flaro.Api.Endpoints; using Flaro.Core.Playback; namespace Flaro.Api.Tests; /// Fortschritt, Gesehen-Status und die Startseite — je Benutzer. 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]); } /// Legt eine Bibliothek an, scannt sie und meldet den Client. private async Task 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())!; (await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode(); for (var versuch = 0; versuch < 100; versuch++) { var stand = await client.GetFromJsonAsync($"/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 ErsterFilm(HttpClient client) { var seite = await client.GetFromJsonAsync>("/api/v1/movies"); return seite!.Items[0].Id; } private static Task Melden(HttpClient c, Guid id, double stelle, double? laenge) => c.PostAsJsonAsync($"/api/v1/items/{id}/progress", new ProgressRequest(stelle, laenge)); // ------------------------------------------------------------------ /// Der Fall, um dessentwillen der Server existiert. /// /// /// Jellyfin verwirft bei zwei Stunden alles unterhalb von sechs Minuten — /// MinResumePct steht auf 5. Hier kommt die Stelle zurück. /// [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())!; 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>("/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())!; 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>("/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)); }); } /// „Weiterschauen" muss sagen, was abgespielt würde. /// /// /// Sonst kann der Aufmacher nur blättern. Er bot „Abspielen" an /// und öffnete die Detailseite — weil ihm die Datei fehlte. Und bei einer /// Folge führte er auf #/filme/<Folgenkennung>, wo nichts /// steht: die Detailseite gehört der Serie, der Fortschritt der Folge. /// [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>("/api/v1/series"); var serie = serien!.Items.Single(); var folgen = await client.GetFromJsonAsync>( $"/api/v1/series/{serie.Id}/episodes"); var folge = folgen!.Items.Single(); await Melden(client, folge.Id, 120, 1500); var reihe = await client.GetFromJsonAsync>("/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)); }); } /// Eine Folge muss sich für sich abrufen lassen. /// /// /// Die Apps reichen eine blanke Kennung durch. 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. /// [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>("/api/v1/series"); var serie = serien!.Items.Single(); var ausDerListe = (await client.GetFromJsonAsync>( $"/api/v1/series/{serie.Id}/episodes"))!.Items.Single(); var einzeln = await client.GetFromJsonAsync( $"/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>("/api/v1/home/continue"); Assert.That(reihe!.Items, Is.Empty); } /// Ein zweites Mal angefangen — mit Häkchen und Stelle zugleich. [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>("/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>("/api/v1/home/continue"); var seite = await client.GetFromJsonAsync>("/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>("/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())!; var reihe = await client.GetFromJsonAsync>("/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($"/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)); } /// Zwei Leute im selben Haushalt stehen an verschiedenen Stellen. [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>("/api/v1/movies"); var seiteB = await zweiter.GetFromJsonAsync>("/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)); } /// Ohne Angabe nimmt der Server die Laufzeit aus der Datei. /// /// /// 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. /// [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())!; 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>("/api/v1/series"); var serie = serien!.Items[0]; Assert.That(serie.UnwatchedCount, Is.EqualTo(3)); var folgen = await client.GetFromJsonAsync>($"/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($"/api/v1/series/{serie.Id}"); Assert.That(danach!.UnwatchedCount, Is.EqualTo(2)); } /// Eine Serie als gesehen markieren hakt jede Folge ab. /// /// /// Der Fall, der vorher stumm scheiterte. Der Server kannte zu /// einer Serienkennung keinen Zustand und antwortete mit 404; die /// Oberfläche verschluckte die Ablehnung, und der Klick tat sichtbar /// nichts. /// [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>("/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($"/api/v1/series/{serie.Id}"); var folgen = await client.GetFromJsonAsync>( $"/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"); }); } /// Und wieder zurück — ohne Rest. [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>("/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($"/api/v1/series/{serie.Id}"); var folgen = await client.GetFromJsonAsync>( $"/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); }); } /// Favorit an einer Serie — vorher ebenfalls eine stille 404. [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>("/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>("/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"); } /// Der Stand einer Serie gehört dem Benutzer, nicht dem Server. [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>("/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($"/api/v1/series/{serie.Id}"); Assert.Multiple(() => { Assert.That(seine!.State?.Watched, Is.Not.True); Assert.That(seine.UnwatchedCount, Is.EqualTo(1)); }); } /// Viele auf einmal — eine Anfrage, nicht zweihundert. [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>("/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(); var danach = await client.GetFromJsonAsync>("/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"); }); } /// Was es nicht gibt, wird benannt statt verschwiegen. [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(); Assert.Multiple(() => { Assert.That(ergebnis!.Changed, Is.EqualTo(1)); Assert.That(ergebnis.Unknown, Is.EqualTo(new[] { erfunden })); }); } /// Auch Serien gehören in eine Sammelmarkierung. [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>("/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($"/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(); Assert.Multiple(() => { Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(ergebnis!.Changed, Is.Zero); }); } /// Eine Folge in „Weiterschauen" nennt ihre Serie. /// /// /// Ohne diese Angabe führte die Kachel ins Leere. 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. /// [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>("/api/v1/series"); var serie = serien!.Items[0]; var folgen = await client.GetFromJsonAsync>( $"/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>("/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"); }); } /// Dasselbe für „Nächste Folge". [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>("/api/v1/series"); var serie = serien!.Items[0]; var folgen = await client.GetFromJsonAsync>( $"/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>("/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)); }); } /// Ein Film hat keine Serie — und behauptet es auch nicht. [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>("/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>("/api/v1/series"); var folgen = await client.GetFromJsonAsync>( $"/api/v1/series/{serien!.Items[0].Id}/episodes"); var naechste = await client.GetFromJsonAsync( $"/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"); }); } [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>("/api/v1/series"); var folgen = await client.GetFromJsonAsync>( $"/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>("/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"); }); } /// Wer bei Folge 2 steht, bekommt nicht Folge 1 angeboten. /// /// /// Der Fall aus dem Gebrauch. 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. /// [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>("/api/v1/series"); var folgen = await client.GetFromJsonAsync>( $"/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>("/api/v1/home/next"); Assert.That(naechste!.Items, Is.Empty, "Folge 2 läuft und steht in Weiterschauen, also hat Als Naechstes nichts anzubieten"); } /// Und abhaken holt die Serie nicht zurück. /// /// /// Wer Folge 1 nachträglich abhakt, hat damit nicht gesagt, dass er /// Folge 2 noch einmal anfangen will — die läuft ja. /// [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>("/api/v1/series"); var folgen = await client.GetFromJsonAsync>( $"/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>("/api/v1/home/next"); Assert.That(naechste!.Items, Is.Empty); } /// Nach der letzten angefassten Folge geht es weiter. /// /// /// 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. /// [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>("/api/v1/series"); var folgen = await client.GetFromJsonAsync>( $"/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>("/api/v1/home/next"); Assert.That(naechste!.Items.Single().Episode, Is.EqualTo(3)); } /// Was in „Weiterschauen" steht, steht nicht nochmal daneben. [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>("/api/v1/series"); var folgen = await client.GetFromJsonAsync>( $"/api/v1/series/{serien!.Items[0].Id}/episodes"); await Melden(client, folgen!.Items[0].Id, 400, 2700); var weiter = await client.GetFromJsonAsync>("/api/v1/home/continue"); var naechste = await client.GetFromJsonAsync>("/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); }); } /// Die Spalte und die Regel dürfen nicht auseinanderlaufen. /// /// /// InProgress ist das festgehaltene Ergebnis von /// . Wer die Regel /// ändert und das Nachführen vergisst, bekommt eine Startseite, die lügt — /// und zwar lautlos. Deshalb hier gegeneinandergehalten. /// [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>("/api/v1/home/continue"); Assert.That(reihe!.Items.Count == 1, Is.EqualTo(erwartet), $"bei {stelle}s von {laenge}s"); } } /// Eine neue Schwelle wirkt auch auf das, was schon dasteht. /// /// /// Der Grund, aus dem der Endpunkt eine Zahl zurückgibt. /// InProgress 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. /// [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>("/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())!; var nachher = await client.GetFromJsonAsync>("/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($"/api/v1/movies/{film}"); Assert.That(eintrag?.State?.PositionSeconds, Is.EqualTo(1).Within(0.001)); } }