Eine Folge ist weder ein Film noch eine Serie
Die App meldete auf der Startseite „Dieser Titel ist auf dem Server nicht
mehr vorhanden" — über eine Folge, die sie selbst gerade anzeigte.
Gemessen im Quelltext: `item(id:)` probiert `/movies/{id}`, dann
`/series/{id}`, und wirft sonst `.notFound`. In „Weiterschauen" steht
eine Folge mit ihrer eigenen Kennung, und einen Endpunkt für eine
einzelne Folge gab es gar nicht. Jetzt gibt es `GET /api/v1/episodes/{id}`
— mit `SeriesId`, denn ohne die Serie fehlen der Folge Titel und Bild.
Im Simulator nachgestellt und nachgeprüft: die Seite steht.
**Zuletzt hinzugefügt.** Die App zeigte „Filme" und „Serien" — den Anfang
der Mediathek in der Reihenfolge der Datenbank, also immer denselben.
Jetzt je Mediathek eine Reihe „Neu in …", sortiert nach Zugang. Die
Kennungen beginnen nun mit `latest`, und daran hängen zwei Einstellungen,
die an einem Flaro-Server deshalb nichts taten: „Gesehenes in Neuzugängen
ausblenden" und „Aufmacher nur aus Neuzugängen".
**Geplante Titel.** `MediaItem` kennt jetzt `availableFrom`. Aufmacher und
Detailseite zeigen statt des Knopfes „Bald ist es soweit" mit dem Termin.
Die Kachel spielte trotzdem los — im Simulator kam der Spieler bis „Wird
vorbereitet …" —, also zählt ein geplanter Titel jetzt wie eine Sammlung:
nichts zu starten, der Tipp führt auf die Seite.
**Und das Bild im Aufmacher.** Bei einer angefangenen Folge stand dort ihr
Standbild statt des Bildes der Serie; wer der Serie eines ausgesucht
hatte, fand ein anderes vor. Die Kachel behält das Standbild — sie zeigt,
wo man steht —, der Aufmacher bekommt `SeriesBackdropImage`.
**Die Punkte im Karussell waren 26 × 3 Pixel.** Von drei Klicks auf
denselben Punkt traf nur der auf der Mittellinie; fünf Pixel daneben
geschah nichts. Der Knopf ist jetzt 27 Pixel hoch, sichtbar bleiben 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f10fb4c1bc
commit
10a5070e58
@@ -430,7 +430,18 @@ public sealed record EpisodeResponse(
|
||||
Guid Id, int Season, int Episode, string? Title, string? Overview, string? StillImage,
|
||||
IReadOnlyList<MediaFileResponse> Files,
|
||||
ItemStateResponse? State,
|
||||
DateTimeOffset? PublishAt = null);
|
||||
DateTimeOffset? PublishAt = null,
|
||||
|
||||
/// <summary>Zu welcher Serie sie gehört.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Damit eine Folge für sich geöffnet werden kann.</b> Die Apps
|
||||
/// reichen eine Kennung durch, ohne zu wissen, was dahintersteckt. War es
|
||||
/// eine Folge, fand sich weder ein Film noch eine Serie, und die
|
||||
/// Startseite meldete „Dieser Titel ist auf dem Server nicht mehr
|
||||
/// vorhanden" — über einen Titel, den sie selbst gerade angezeigt hatte.
|
||||
/// </remarks>
|
||||
Guid? SeriesId = null);
|
||||
|
||||
public sealed record Page<T>(IReadOnlyList<T> Items, int Total);
|
||||
|
||||
@@ -631,7 +642,17 @@ public sealed record ContinueEntry(
|
||||
double? DurationSeconds = null,
|
||||
|
||||
/// <summary>Ab wann abspielbar. In der Zukunft heisst: noch nicht.</summary>
|
||||
DateTimeOffset? PublishAt = null);
|
||||
DateTimeOffset? PublishAt = null,
|
||||
|
||||
/// <summary>Das breite Bild der Serie, zu der die Folge gehört.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Neben dem Folgenbild, nicht statt seiner.</b> Die Kachel will das
|
||||
/// Bild der Folge — es zeigt, wo man steht. Der Aufmacher füllt die halbe
|
||||
/// Seite und will das Bild der Serie; dort stand bisher das Folgenbild,
|
||||
/// und wer das Serienbild selbst ausgesucht hatte, fand ein anderes vor.
|
||||
/// </remarks>
|
||||
string? SeriesBackdropImage = null);
|
||||
|
||||
/// <summary>Eine laufende Wiedergabe — für das Dashboard.</summary>
|
||||
public sealed record SessionResponse(
|
||||
|
||||
@@ -666,6 +666,47 @@ public static class LibraryContentEndpoints
|
||||
PublishAt: folgen.Min(f => f.PublishAt)));
|
||||
}).RequireAuthorization();
|
||||
|
||||
// **Eine Folge für sich.**
|
||||
//
|
||||
// Es gab sie nur im Rudel — als Liste einer Serie. Die Apps reichen
|
||||
// aber eine blanke Kennung durch: sie haben sie aus „Weiterschauen"
|
||||
// und wissen nicht, ob ein Film, eine Serie oder eine Folge
|
||||
// dahintersteckt. Sie fragten der Reihe nach Film und Serie, fanden
|
||||
// beides nicht und meldeten „Dieser Titel ist auf dem Server nicht
|
||||
// mehr vorhanden" — über eine Folge, die sie selbst gerade zeigten.
|
||||
app.MapGet("/api/v1/episodes/{id:guid}", async (
|
||||
Guid id, ClaimsPrincipal wer, FlaroContext db) =>
|
||||
{
|
||||
var folge = await db.Episodes.AsNoTracking()
|
||||
.Where(f => f.Id == id)
|
||||
.Select(f => new
|
||||
{
|
||||
f.Id, f.SeriesId, f.SeasonNumber, f.EpisodeNumber,
|
||||
f.Title, f.Overview, f.StillImage, f.PublishAt,
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
if (folge is null) return Results.NotFound();
|
||||
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(d => d.EpisodeId == id)
|
||||
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, d.VersionLabel })
|
||||
.ToListAsync();
|
||||
var spuren = await SpurenZu(db, dateien.Select(d => d.Id).ToList());
|
||||
var stände = await ZuständeZu(db, wer, folgen: [id]);
|
||||
|
||||
var wurzel = await MediathekspfadFuer(db, wer,
|
||||
await db.Series.AsNoTracking().Where(x => x.Id == folge.SeriesId)
|
||||
.Select(x => x.LibraryId).FirstAsync());
|
||||
|
||||
return Results.Ok(new EpisodeResponse(
|
||||
folge.Id, folge.SeasonNumber, folge.EpisodeNumber,
|
||||
folge.Title, folge.Overview, folge.StillImage,
|
||||
dateien.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel, wurzel)).ToList(),
|
||||
stände.GetValueOrDefault(id),
|
||||
folge.PublishAt,
|
||||
SeriesId: folge.SeriesId));
|
||||
}).RequireAuthorization();
|
||||
|
||||
// **Die nächste Folge nach dieser.** Reihenfolge ist Staffel, dann
|
||||
// Folge — nicht die Kennung: die ist zeitlich geordnet und sagt, wann
|
||||
// der Scan sie fand, nicht wann sie läuft.
|
||||
@@ -709,7 +750,8 @@ public static class LibraryContentEndpoints
|
||||
naechste.Title, naechste.Overview, naechste.StillImage ?? serienBild,
|
||||
dateien.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel)).ToList(),
|
||||
stände.GetValueOrDefault(naechste.Id),
|
||||
naechste.PublishAt));
|
||||
naechste.PublishAt,
|
||||
SeriesId: diese.SeriesId));
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapGet("/api/v1/series/{id:guid}/episodes", async (
|
||||
@@ -747,7 +789,8 @@ public static class LibraryContentEndpoints
|
||||
dateien.Where(d => d.EpisodeId == f.Id)
|
||||
.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel, wurzel)).ToList(),
|
||||
stände.GetValueOrDefault(f.Id),
|
||||
f.PublishAt)).ToList();
|
||||
f.PublishAt,
|
||||
SeriesId: id)).ToList();
|
||||
|
||||
return Results.Ok(new Page<EpisodeResponse>(items, items.Count));
|
||||
}).RequireAuthorization();
|
||||
|
||||
@@ -296,7 +296,7 @@ public static class PlaybackEndpoints
|
||||
|
||||
var serien = await db.Series.AsNoTracking()
|
||||
.Where(x => serienIds.Contains(x.Id))
|
||||
.Select(x => new { x.Id, x.Title, x.PosterImage })
|
||||
.Select(x => new { x.Id, x.Title, x.PosterImage, x.BackdropImage })
|
||||
.ToDictionaryAsync(x => x.Id);
|
||||
|
||||
var liste = new List<ContinueEntry>();
|
||||
@@ -320,7 +320,8 @@ public static class PlaybackEndpoints
|
||||
serie.PosterImage, naechste.StillImage ?? serie.PosterImage,
|
||||
zustand is null ? Leer() : Abbild(zustand),
|
||||
SeriesId: x.SeriesId,
|
||||
PublishAt: naechste.PublishAt));
|
||||
PublishAt: naechste.PublishAt,
|
||||
SeriesBackdropImage: serie.BackdropImage));
|
||||
}
|
||||
|
||||
// Die Dateien erst jetzt, für die ausgewählten Folgen — nicht für
|
||||
@@ -445,6 +446,7 @@ public static class PlaybackEndpoints
|
||||
{
|
||||
f.Id, f.SeasonNumber, f.EpisodeNumber, FolgenTitel = f.Title, f.StillImage,
|
||||
SerienId = s.Id, SerienTitel = s.Title, s.PosterImage, f.PublishAt,
|
||||
SerienBild = s.BackdropImage,
|
||||
})
|
||||
.ToDictionaryAsync(f => f.Id);
|
||||
|
||||
@@ -467,7 +469,8 @@ public static class PlaybackEndpoints
|
||||
SeriesId: f.SerienId,
|
||||
FileId: dateien.GetValueOrDefault(f.Id).Id,
|
||||
DurationSeconds: dateien.GetValueOrDefault(f.Id).Sekunden,
|
||||
PublishAt: f.PublishAt));
|
||||
PublishAt: f.PublishAt,
|
||||
SeriesBackdropImage: f.SerienBild));
|
||||
}
|
||||
return liste;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,44 @@ public class PlaybackTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user