Abspielen spielt ab
Im Aufmacher der Startseite stand „Abspielen", und wer draufdrückte, landete auf der Detailseite. Gemessen am laufenden Server: beide Knöpfe riefen dieselbe Funktion — `oeffnen` —, der eine hiess nur anders. Drei Dinge waren falsch, alle an derselben Stelle: **Der Knopf log.** Jetzt startet er den Spieler, mit Fortsetzen ab der Stelle, an der man war. Ohne Datei — eine ganze Serie — heisst er „Ansehen" und führt dorthin, wo man wählen kann. **Eine angefangene Folge war kein Film.** Sie lief durch `alsHero` mit `art: 'filme'` und ihrer eigenen Kennung; der Klick ging auf `#/filme/<Folgenkennung>` und endete in „Diesen Film gibt es nicht mehr". Nachgemessen im Browser, dann `angefangenAlsHero` dafür gebaut: die Detailseite gehört der Serie, der Fortschritt der Folge. **Ein geplanter Titel sah aus wie jeder andere.** Jetzt steht statt des Knopfes „Bald ist es soweit" mit dem Termin, wie auf der Detailseite. Dafür nennt `/home/continue` und `/home/next` nun Datei, Länge und Termin, und die Vorschläge bringen ihre Dateien mit — ohne die konnte der Aufmacher gar nicht abspielen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
343b0cdcfb
commit
f10fb4c1bc
@@ -618,7 +618,20 @@ public sealed record ContinueEntry(
|
||||
int? Season, int? Episode, int? Year,
|
||||
string? PosterImage, string? BackdropImage,
|
||||
ItemStateResponse State,
|
||||
Guid? SeriesId = null);
|
||||
Guid? SeriesId = null,
|
||||
|
||||
/// <summary>Die Datei, die abgespielt würde.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Damit „Abspielen" abspielt.</b> Der Aufmacher der Startseite hatte
|
||||
/// keine Datei zur Hand und konnte deshalb nur die Detailseite öffnen —
|
||||
/// ein Knopf, auf dem „Abspielen" steht und der etwas anderes tut.
|
||||
/// </remarks>
|
||||
Guid? FileId = null,
|
||||
double? DurationSeconds = null,
|
||||
|
||||
/// <summary>Ab wann abspielbar. In der Zukunft heisst: noch nicht.</summary>
|
||||
DateTimeOffset? PublishAt = null);
|
||||
|
||||
/// <summary>Eine laufende Wiedergabe — für das Dashboard.</summary>
|
||||
public sealed record SessionResponse(
|
||||
|
||||
@@ -286,7 +286,7 @@ public static class PlaybackEndpoints
|
||||
|
||||
var folgen = await db.Episodes.AsNoTracking()
|
||||
.Where(f => serienIds.Contains(f.SeriesId))
|
||||
.Select(f => new { f.Id, f.SeriesId, f.SeasonNumber, f.EpisodeNumber, f.Title, f.StillImage })
|
||||
.Select(f => new { f.Id, f.SeriesId, f.SeasonNumber, f.EpisodeNumber, f.Title, f.StillImage, f.PublishAt })
|
||||
.ToListAsync();
|
||||
|
||||
var folgenIds = folgen.Select(f => f.Id).ToList();
|
||||
@@ -319,9 +319,19 @@ public static class PlaybackEndpoints
|
||||
naechste.SeasonNumber, naechste.EpisodeNumber, null,
|
||||
serie.PosterImage, naechste.StillImage ?? serie.PosterImage,
|
||||
zustand is null ? Leer() : Abbild(zustand),
|
||||
SeriesId: x.SeriesId));
|
||||
SeriesId: x.SeriesId,
|
||||
PublishAt: naechste.PublishAt));
|
||||
}
|
||||
|
||||
// Die Dateien erst jetzt, für die ausgewählten Folgen — nicht für
|
||||
// alle Folgen aller angefassten Serien.
|
||||
var erste = await Erstdateien(db, [], liste.Select(e => e.Id).ToList());
|
||||
liste = liste.Select(e => e with
|
||||
{
|
||||
FileId = erste.GetValueOrDefault(e.Id).Id,
|
||||
DurationSeconds = erste.GetValueOrDefault(e.Id).Sekunden,
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(new Page<ContinueEntry>(liste, liste.Count));
|
||||
});
|
||||
|
||||
@@ -383,9 +393,24 @@ public static class PlaybackEndpoints
|
||||
.Select(x => x.Film)
|
||||
.ToListAsync();
|
||||
|
||||
// **Mit Datei und Termin.** Ein Vorschlag ohne Datei liesse den
|
||||
// Aufmacher „Abspielen" anbieten und dann doch nur blättern; ein
|
||||
// Vorschlag ohne Termin verschwiege, dass es ihn noch nicht gibt.
|
||||
var vorschlagIds = treffer.Select(m => m.Id).ToList();
|
||||
var vorschlagsdateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(d => d.MovieId != null && vorschlagIds.Contains(d.MovieId!.Value))
|
||||
.Select(d => new { d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, d.MovieId, d.VersionLabel })
|
||||
.ToListAsync();
|
||||
|
||||
var items = treffer.Select(m => new MovieResponse(
|
||||
m.Id, m.Title, m.Year, m.Overview, m.PosterImage, m.BackdropImage,
|
||||
[], null, m.LogoImage, m.RuntimeMinutes, m.Rating, null, m.ContentRating)).ToList();
|
||||
vorschlagsdateien.Where(d => d.MovieId == m.Id)
|
||||
.Select(d => new MediaFileResponse(
|
||||
d.Id, Path.GetFileName(d.Path), d.SizeBytes, d.Container,
|
||||
d.Duration?.TotalSeconds, [], d.VersionLabel))
|
||||
.ToList(),
|
||||
null, m.LogoImage, m.RuntimeMinutes, m.Rating, null, m.ContentRating,
|
||||
PublishAt: m.PublishAt)).ToList();
|
||||
|
||||
return Results.Ok(new Page<MovieResponse>(items, items.Count));
|
||||
});
|
||||
@@ -412,33 +437,67 @@ public static class PlaybackEndpoints
|
||||
var folgen = zustände.Where(z => z.EpisodeId != null).Select(z => z.EpisodeId!.Value).ToList();
|
||||
|
||||
var filmdaten = await db.Movies.AsNoTracking().Where(m => filme.Contains(m.Id))
|
||||
.Select(m => new { m.Id, m.Title, m.Year, m.PosterImage, m.BackdropImage })
|
||||
.Select(m => new { m.Id, m.Title, m.Year, m.PosterImage, m.BackdropImage, m.PublishAt })
|
||||
.ToDictionaryAsync(m => m.Id);
|
||||
|
||||
var folgendaten = await db.Episodes.AsNoTracking().Where(f => folgen.Contains(f.Id))
|
||||
.Join(db.Series.AsNoTracking(), f => f.SeriesId, s => s.Id, (f, s) => new
|
||||
{
|
||||
f.Id, f.SeasonNumber, f.EpisodeNumber, FolgenTitel = f.Title, f.StillImage,
|
||||
SerienId = s.Id, SerienTitel = s.Title, s.PosterImage,
|
||||
SerienId = s.Id, SerienTitel = s.Title, s.PosterImage, f.PublishAt,
|
||||
})
|
||||
.ToDictionaryAsync(f => f.Id);
|
||||
|
||||
var dateien = await Erstdateien(db, filme, folgen);
|
||||
|
||||
var liste = new List<ContinueEntry>(zustände.Count);
|
||||
foreach (var z in zustände)
|
||||
{
|
||||
if (z.MovieId is { } film && filmdaten.TryGetValue(film, out var m))
|
||||
liste.Add(new ContinueEntry(
|
||||
m.Id, "movie", m.Title, null, null, null, m.Year,
|
||||
m.PosterImage, m.BackdropImage ?? m.PosterImage, Abbild(z)));
|
||||
m.PosterImage, m.BackdropImage ?? m.PosterImage, Abbild(z),
|
||||
FileId: dateien.GetValueOrDefault(m.Id).Id,
|
||||
DurationSeconds: dateien.GetValueOrDefault(m.Id).Sekunden,
|
||||
PublishAt: m.PublishAt));
|
||||
else if (z.EpisodeId is { } folge && folgendaten.TryGetValue(folge, out var f))
|
||||
liste.Add(new ContinueEntry(
|
||||
f.Id, "episode", f.SerienTitel, f.FolgenTitel, f.SeasonNumber, f.EpisodeNumber, null,
|
||||
f.PosterImage, f.StillImage ?? f.PosterImage, Abbild(z),
|
||||
SeriesId: f.SerienId));
|
||||
SeriesId: f.SerienId,
|
||||
FileId: dateien.GetValueOrDefault(f.Id).Id,
|
||||
DurationSeconds: dateien.GetValueOrDefault(f.Id).Sekunden,
|
||||
PublishAt: f.PublishAt));
|
||||
}
|
||||
return liste;
|
||||
}
|
||||
|
||||
/// <summary>Je Titel die erste Datei — die, die abgespielt würde.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>In einer Abfrage für alle.</b> Zwanzig Zeilen auf der Startseite
|
||||
/// wären sonst zwanzig Abfragen, und SQLite führt die nacheinander aus.
|
||||
/// </remarks>
|
||||
private static async Task<Dictionary<Guid, (Guid? Id, double? Sekunden)>> Erstdateien(
|
||||
FlaroContext db, List<Guid> filme, List<Guid> folgen)
|
||||
{
|
||||
if (filme.Count == 0 && folgen.Count == 0) return [];
|
||||
|
||||
var roh = await db.MediaFiles.AsNoTracking()
|
||||
.Where(d => (d.MovieId != null && filme.Contains(d.MovieId!.Value))
|
||||
|| (d.EpisodeId != null && folgen.Contains(d.EpisodeId!.Value)))
|
||||
.Select(d => new { d.Id, d.MovieId, d.EpisodeId, d.Duration })
|
||||
.ToListAsync();
|
||||
|
||||
return roh
|
||||
.GroupBy(d => d.MovieId ?? d.EpisodeId!.Value)
|
||||
.ToDictionary(g => g.Key, g =>
|
||||
{
|
||||
var erste = g.First();
|
||||
return ((Guid?)erste.Id, erste.Duration?.TotalSeconds);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Gemeinsames
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -139,6 +139,46 @@ public class PlaybackTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <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/<Folgenkennung></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));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Durchgeschautes_verlaesst_Weiterschauen()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user