Spider-Man ist nicht 720p, er ist breit
Die Auflösung wurde allein aus der Höhe gebildet. Ein Kinoformat von 2.39:1 liegt als 1920×800 vor — nach der Höhe gerechnet kam „720p" heraus, und das stand so in der Oberfläche. Jetzt zählt die grössere von Breite und Höhe; Hochformat kippt damit nicht, und 4K-Breitbild heisst nicht mehr 1440p. Die Detailansicht hat zwei Leserschaften, und bisher bekamen beide dasselbe. Wer schauen will, sieht jetzt Laufzeit, Auflösung und die Sprachen — jede einmal, statt „Englisch · Englisch · Englisch · Englisch · Deutsch". Wer die Mediathek betreut, sieht darunter Dateiformat, Grösse und den Ordner. Der Ordner verrät eine falsche Zuordnung in einer Sekunde: steht über der Beschreibung „Hanni & Nanni 3" und der Ordner heisst „Hanni und Nanni", ist die Sache klar. Er kommt nur für Administratoren überhaupt vom Server — versteckt im Client wäre er weiterhin auslesbar. Ein geplanter Titel war unsichtbar. Das ist die Hälfte, die nichts bringt: dass er kommt, ist der Sinn des Planens. Er steht jetzt mit „Verfügbar ab …" in der Mediathek, und die Datei geht trotzdem nicht heraus — Download wie Strom lehnen bis zum Termin ab, auch für Administratoren. Wer vorziehen will, hat „Jetzt veröffentlichen"; eine Hintertür für eine Rolle wäre eine Regel, die manchmal gilt. Was ein Auffrischen geändert hat, steht jetzt unter „Zu prüfen" — in einer eigenen Gruppe mit „Zur Kenntnis" statt „Veröffentlichen". Diese Titel sind sichtbar; es gibt nichts freizugeben, nur etwas abzuhaken. Und das Protokoll sagt endlich, wo es knallt. „InvalidOperationException: Nullable object must have a value." ohne eine einzige Zeile Herkunft ist für niemanden eine Diagnose — weder für den Betreiber noch für mich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f44779c899
commit
255eb7c2f0
@@ -1,3 +1,4 @@
|
||||
using Flaro.Api.Auth;
|
||||
using Flaro.Core.Library;
|
||||
using Flaro.Core.Media;
|
||||
using System.Security.Claims;
|
||||
@@ -90,21 +91,70 @@ public static class LibraryContentEndpoints
|
||||
s.IsDefault, s.IsForced, s.Channels, s.Width, s.Height,
|
||||
Display: s.Kind switch
|
||||
{
|
||||
MediaStreamKind.Video => Benennung.Bildspur(s.Codec, s.Height),
|
||||
MediaStreamKind.Video => Benennung.Bildspur(s.Codec, s.Width, s.Height),
|
||||
MediaStreamKind.Audio => Benennung.Tonspur(s.Language, s.Codec, s.Channels, s.Title),
|
||||
_ => Benennung.Untertitelspur(s.Language, s.IsForced, fuerHoergeschaedigte: false),
|
||||
}));
|
||||
},
|
||||
LanguageName: Benennung.Sprache(s.Language)));
|
||||
}
|
||||
|
||||
private static MediaFileResponse Datei(
|
||||
Guid id, string pfad, long bytes, string? schachtel, TimeSpan? dauer,
|
||||
ILookup<Guid, MediaStreamResponse> spuren, string? fassung = null) =>
|
||||
ILookup<Guid, MediaStreamResponse> spuren, string? fassung = null,
|
||||
string? mediathekspfad = null) =>
|
||||
new(id, System.IO.Path.GetFileName(pfad), bytes, schachtel, dauer?.TotalSeconds,
|
||||
spuren[id].ToList(),
|
||||
// Steht sie nicht in der Datenbank, wird sie aus dem Pfad gelesen:
|
||||
// ein Bestand von vor dieser Änderung soll nicht erst neu gescannt
|
||||
// werden müssen, um „Mobil" zu zeigen.
|
||||
fassung ?? FileNames.Fassung(pfad));
|
||||
fassung ?? FileNames.Fassung(pfad),
|
||||
Ordner(pfad, mediathekspfad));
|
||||
|
||||
/// <summary>Der Pfad der Mediathek — nur für Administratoren.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Hier entscheidet sich, wer den Ordner sieht.</b> Ohne Wurzel bleibt
|
||||
/// <see cref="Ordner"/> bei <c>null</c>, und das Feld kommt erst gar nicht
|
||||
/// in der Antwort an — statt es im Client zu verstecken, wo es jeder
|
||||
/// auslesen könnte.
|
||||
/// </remarks>
|
||||
private static async Task<string?> MediathekspfadFuer(
|
||||
FlaroContext db, ClaimsPrincipal wer, Guid bibliothek)
|
||||
{
|
||||
if (!wer.IsInRole(FlaroRoles.Administrator)) return null;
|
||||
|
||||
return await db.Libraries.AsNoTracking()
|
||||
.Where(l => l.Id == bibliothek)
|
||||
.Select(l => l.Path)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>Der Ordner unterhalb der Mediathek — oder <c>null</c>.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Nur der Teil unterhalb, nie der volle Pfad.</b> Der verriete jedem
|
||||
/// Angemeldeten den Aufbau des Servers; was zum Beurteilen einer Zuordnung
|
||||
/// nötig ist, ist der Ordnername — „Hanni und Nanni" neben dem Titel
|
||||
/// „Hanni & Nanni 3" sagt in einer Sekunde, dass etwas nicht stimmt.
|
||||
///
|
||||
/// <c>null</c> für alle, die nicht verwalten dürfen: dort wird der
|
||||
/// Mediathekspfad gar nicht erst hereingereicht.
|
||||
/// </remarks>
|
||||
private static string? Ordner(string pfad, string? mediathekspfad)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mediathekspfad)) return null;
|
||||
|
||||
var ordner = System.IO.Path.GetDirectoryName(pfad);
|
||||
if (string.IsNullOrEmpty(ordner)) return null;
|
||||
|
||||
var wurzel = mediathekspfad.TrimEnd('/', '\\');
|
||||
if (!ordner.StartsWith(wurzel, StringComparison.Ordinal)) return null;
|
||||
|
||||
var darunter = ordner[wurzel.Length..].Trim('/', '\\');
|
||||
// Liegt die Datei unmittelbar in der Mediathek, gibt es keinen Ordner
|
||||
// — und „" wäre eine Zeile, die nichts sagt.
|
||||
return darunter.Length > 0 ? darunter : null;
|
||||
}
|
||||
|
||||
/// <summary>Die Genres eines Titels, in der Reihenfolge der Quelle.</summary>
|
||||
private static async Task<IReadOnlyList<string>> GenresZu(FlaroContext db, Guid? film, Guid? serie) =>
|
||||
@@ -559,6 +609,7 @@ public static class LibraryContentEndpoints
|
||||
.ToListAsync();
|
||||
var spuren = await SpurenZu(db, dateien.Select(f => f.Id).ToList());
|
||||
var stände = await ZuständeZu(db, wer, filme: [id]);
|
||||
var wurzel = await MediathekspfadFuer(db, wer, film.LibraryId);
|
||||
|
||||
var sprache = await SpracheVon(wer, db);
|
||||
var uebersetzt = sprache == null ? null : await db.Translations.AsNoTracking()
|
||||
@@ -567,7 +618,7 @@ public static class LibraryContentEndpoints
|
||||
return Results.Ok(new MovieResponse(
|
||||
film.Id, uebersetzt?.Title ?? film.Title, film.Year,
|
||||
uebersetzt?.Overview ?? film.Overview, film.PosterImage, film.BackdropImage,
|
||||
dateien.Select(f => Datei(f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, spuren, f.VersionLabel)).ToList(),
|
||||
dateien.Select(f => Datei(f.Id, f.Path, f.SizeBytes, f.Container, f.Duration, spuren, f.VersionLabel, wurzel)).ToList(),
|
||||
stände.GetValueOrDefault(id),
|
||||
film.LogoImage, film.RuntimeMinutes, film.Rating,
|
||||
uebersetzt?.Tagline ?? film.Tagline, film.ContentRating,
|
||||
@@ -671,6 +722,10 @@ public static class LibraryContentEndpoints
|
||||
.Select(f => new { f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview, f.StillImage })
|
||||
.ToListAsync();
|
||||
|
||||
var wurzel = await MediathekspfadFuer(db, wer,
|
||||
await db.Series.AsNoTracking().Where(x => x.Id == id)
|
||||
.Select(x => x.LibraryId).FirstAsync());
|
||||
|
||||
var ids = folgen.Select(f => f.Id).ToList();
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.Where(d => d.EpisodeId != null && ids.Contains(d.EpisodeId!.Value))
|
||||
@@ -687,7 +742,7 @@ public static class LibraryContentEndpoints
|
||||
f.Id, f.SeasonNumber, f.EpisodeNumber, f.Title, f.Overview,
|
||||
f.StillImage ?? serienBild,
|
||||
dateien.Where(d => d.EpisodeId == f.Id)
|
||||
.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel)).ToList(),
|
||||
.Select(d => Datei(d.Id, d.Path, d.SizeBytes, d.Container, d.Duration, spuren, d.VersionLabel, wurzel)).ToList(),
|
||||
stände.GetValueOrDefault(f.Id))).ToList();
|
||||
|
||||
return Results.Ok(new Page<EpisodeResponse>(items, items.Count));
|
||||
|
||||
Reference in New Issue
Block a user