Die Untertitel holen, bevor jemand sie einschaltet
Zwei Minuten „wird geholt", und noch immer kein Text. Die Ablage von vorhin
war richtig und reichte nicht: beim ersten Mal musste immer noch jemand
warten, und zwar der, der gerade einen Film anschaut.
Das Lesen selbst lässt sich nicht beschleunigen. Untertitelpakete liegen
über die ganze Datei verteilt; sie zu finden heisst, alles zu lesen. Bei
vierzig Gigabyte auf einem NAS sind das Minuten, und daran ändert kein
Schalter etwas. Also nicht schneller lesen, sondern früher.
Der Vorrat arbeitet die Mediathek jetzt von sich aus durch: beim Start des
Servers und nach jedem Scan, die zuletzt hinzugekommenen zuerst. Eine Datei
nach der anderen, mit zwei Sekunden Pause dazwischen — das Lesen ist von
der Platte begrenzt, und eine laufende Wiedergabe liest von derselben
Platte und ist wichtiger. Wer einen Film startet, drängelt sich vor: seine
Datei ist als Nächstes dran statt als Dreissigste.
Einmal je Datei, nie wieder: der Name der Ablage trägt den
Änderungszeitpunkt. Eine ersetzte Datei bekommt dadurch von selbst einen
neuen Namen, eine unveränderte wird nie zweimal gelesen.
Gemessen am Prüfserver mit leerer Ablage:
Start, ohne jeden Abruf -> „1 Datei(en) werden im Hintergrund
gelesen", danach „2 von 2 Spur(en)"
Klick auf „Deutsch" -> 153 ms bis zum Text, 10 Einblendungen,
die erste bei 0 bis 3 s
Landet jemand trotzdem im Wartefall, gibt der Spieler nicht mehr nach zwei
Minuten auf, sondern fragt zehn Minuten lang nach — und sagt dazu, dass es
einmalig ist.
Die Auswahl der Textspuren stand zweimal im Quelltext und steht jetzt
einmal: im Vorrat. Zwei Listen, von denen eine das Menü füllt und die
andere bestimmt, was geholt wird, laufen sonst irgendwann auseinander.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c2fd2a86fd
commit
1531163bc1
@@ -53,8 +53,7 @@ public static class StreamEndpoints
|
||||
// warten; der Vorrat ist bis dahin längst gefüllt. Wer sie sofort
|
||||
// einschaltet, wartet so lange wie vorher, bekommt aber eine
|
||||
// Antwort statt eines Fehlers.
|
||||
vorrat.Anstossen(gewuenschte.Id, gewuenschte.Path, gewuenschte.ModifiedAt,
|
||||
await Textspuren(db, id, CancellationToken.None));
|
||||
vorrat.Anstellen(gewuenschte.Id, vordraengeln: true);
|
||||
|
||||
var profil = Profil(anfrage);
|
||||
var hoehe = anfrage.Height is { } gewuenscht
|
||||
@@ -238,13 +237,12 @@ public static class StreamEndpoints
|
||||
if (datei is null || !File.Exists(datei.Path))
|
||||
return Einwand.Fehlt("Diese Datei liegt nicht mehr da.");
|
||||
|
||||
var spuren = await Textspuren(db, id, ct);
|
||||
var spuren = await Untertitelvorrat.TextspurenAsync(db, id, ct);
|
||||
if (!spuren.Contains(spur))
|
||||
return Einwand.Fehlt("Diese Spur enthält keinen Text, sondern Bilder.");
|
||||
|
||||
var (stand, text) = await vorrat.HolenAsync(
|
||||
datei.Id, datei.Path, datei.ModifiedAt, spuren, spur,
|
||||
TimeSpan.FromSeconds(20), ct);
|
||||
datei.Id, datei.ModifiedAt, spur, TimeSpan.FromSeconds(20), ct);
|
||||
|
||||
return stand switch
|
||||
{
|
||||
@@ -308,7 +306,7 @@ public static class StreamEndpoints
|
||||
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
|
||||
.ToList(),
|
||||
SubtitleStreams: spuren.Where(s => s.Kind == MediaStreamKind.Subtitle)
|
||||
.Where(s => s.Codec is null || !IstBild(s.Codec))
|
||||
.Where(s => s.Codec is null || !Untertitelvorrat.IstBild(s.Codec))
|
||||
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
|
||||
.ToList());
|
||||
}
|
||||
@@ -360,7 +358,7 @@ public static class StreamEndpoints
|
||||
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
|
||||
.ToList(),
|
||||
SubtitleStreams: spuren.Where(s => s.Kind == MediaStreamKind.Subtitle)
|
||||
.Where(s => s.Codec is null || !IstBild(s.Codec))
|
||||
.Where(s => s.Codec is null || !Untertitelvorrat.IstBild(s.Codec))
|
||||
.Select(s => new SpurAuskunft(s.Index, Benennung(s), s.Language, s.IsDefault))
|
||||
.ToList());
|
||||
}
|
||||
@@ -372,26 +370,6 @@ public static class StreamEndpoints
|
||||
/// PGS ist auf jedem Blu-ray-Rip die Regel — ein Eintrag im Menü, der zu
|
||||
/// nichts führt, ist schlechter als kein Eintrag.
|
||||
/// </remarks>
|
||||
private static bool IstBild(string codec) =>
|
||||
codec is "hdmv_pgs_subtitle" or "pgssub" or "dvd_subtitle" or "dvdsub" or "xsub";
|
||||
|
||||
/// <summary>Die Spurnummern, aus denen sich Text holen lässt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Dieselbe Auswahl, die auch im Menü steht — sonst böte der Spieler eine
|
||||
/// Spur an, die der Vorrat gar nicht erst zieht.
|
||||
/// </remarks>
|
||||
private static async Task<IReadOnlyList<int>> Textspuren(
|
||||
FlaroContext db, Guid dateiId, CancellationToken ct)
|
||||
{
|
||||
var alle = await db.MediaStreams.AsNoTracking()
|
||||
.Where(s => s.MediaFileId == dateiId && s.Kind == MediaStreamKind.Subtitle)
|
||||
.OrderBy(s => s.Index)
|
||||
.Select(s => new { s.Index, s.Codec })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return alle.Where(s => s.Codec is null || !IstBild(s.Codec)).Select(s => s.Index).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Wie eine Spur im Menü heisst.</summary>
|
||||
private static string Benennung(MediaStream spur)
|
||||
|
||||
@@ -312,7 +312,19 @@ if (Directory.Exists(oberfläche))
|
||||
var leben = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
leben.ApplicationStarted.Register(() =>
|
||||
tagebuch.Schreiben($"Server gestartet — {Buildkennung.Beides}"));
|
||||
{
|
||||
tagebuch.Schreiben($"Server gestartet — {Buildkennung.Beides}");
|
||||
|
||||
// **Die Untertitel holen, bevor jemand sie einschaltet.**
|
||||
//
|
||||
// Eine Spur aus einer Datei zu ziehen heisst, die Datei ganz zu
|
||||
// lesen — bei einem Dreistünder auf einem NAS sind das Minuten.
|
||||
// Das im Augenblick des Einschaltens zu tun hiess: zwei Minuten
|
||||
// „wird geholt", und noch immer kein Text. Hier läuft es einmal je
|
||||
// Datei durch, eine nach der anderen, während niemand wartet.
|
||||
_ = app.Services.GetRequiredService<Untertitelvorrat>()
|
||||
.NachtragenAsync(leben.ApplicationStopping);
|
||||
});
|
||||
leben.ApplicationStopping.Register(() =>
|
||||
tagebuch.Schreiben("Server fährt herunter."));
|
||||
}
|
||||
|
||||
@@ -152,6 +152,14 @@ public sealed class ScanCoordinator(
|
||||
zusammengelegt.Filme, zusammengelegt.Serien, libraryId);
|
||||
}
|
||||
|
||||
// **Untertitel gleich mitnehmen.** Sie aus einer Datei zu holen
|
||||
// heisst, die Datei ganz zu lesen; das im Augenblick des
|
||||
// Einschaltens zu tun heisst, den Zuschauer Minuten warten zu
|
||||
// lassen. Der Vorrat arbeitet das im Hintergrund ab, eine Datei
|
||||
// nach der anderen.
|
||||
_ = bereich.ServiceProvider.GetRequiredService<Untertitelvorrat>()
|
||||
.NachtragenAsync(CancellationToken.None);
|
||||
|
||||
_zustand[libraryId] = _zustand[libraryId] with
|
||||
{
|
||||
Status = "finished",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Flaro.Core.Library;
|
||||
using Flaro.Data;
|
||||
using Flaro.Media;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flaro.Api;
|
||||
|
||||
/// <summary>Herausgezogene Untertitel, abgelegt für den zweiten Abruf.</summary>
|
||||
/// <summary>Herausgezogene Untertitel — vorher geholt, nicht beim Einschalten.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -11,24 +13,33 @@ namespace Flaro.Api;
|
||||
/// laden", und auf der zweiten Spur kam überhaupt nichts. Der Grund lag nicht
|
||||
/// im Format, sondern in der Zeit: jeder Abruf startete ffmpeg, ffmpeg musste
|
||||
/// die ganze Datei durchlesen, und nach dreissig Sekunden wurde abgebrochen.
|
||||
/// Bei einem kleinen Prüffilm fällt das nie auf; bei einem Dreistünder auf
|
||||
/// einem NAS reicht es nie.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Und es wurde schlimmer, bevor es besser wurde.</b> Seit die Adresse den
|
||||
/// Einstiegspunkt mitführt (<c>?ab=</c>), ändert sie sich bei jedem Sprung —
|
||||
/// der Browser konnte nichts mehr aus seinem eigenen Zwischenspeicher
|
||||
/// nehmen, und jeder Sprung stiess einen neuen Durchlauf an. Deshalb liegt
|
||||
/// hier die ungeschobene Spur; das Verschieben ist eine Zeilenersetzung und
|
||||
/// kostet nichts.
|
||||
/// <b>Und das Lesen lässt sich nicht beschleunigen.</b> Untertitelpakete
|
||||
/// liegen über die ganze Datei verteilt; sie zu finden heisst, alles zu
|
||||
/// lesen. Bei vierzig Gigabyte auf einem NAS sind das Minuten, und daran
|
||||
/// ändert kein Schalter etwas. Auch mit Ablage blieb deshalb der erste
|
||||
/// Abruf eine Zumutung: zwei Minuten „wird geholt", und noch immer kein
|
||||
/// Text.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Warm, bevor jemand fragt.</b> Angestossen wird beim Start des Stroms,
|
||||
/// nicht erst beim Öffnen des Menüs. Wer nach fünf Minuten Untertitel
|
||||
/// einschaltet, wartet dann gar nicht mehr.
|
||||
/// <b>Also vorher.</b> Dieser Vorrat arbeitet die Mediathek von sich aus
|
||||
/// durch — beim Start des Servers und nach jedem Scan, eine Datei nach der
|
||||
/// anderen, damit die Platte nicht von zehn ffmpeg-Läufen zugleich belegt
|
||||
/// wird. Wer einen Film startet, drängelt sich vor: sein Film ist dann als
|
||||
/// Nächstes dran statt als Dreissigster.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Einmal je Datei, nie wieder.</b> Der Name der Ablage trägt den
|
||||
/// Änderungszeitpunkt der Datei. Eine ersetzte Datei bekommt dadurch von
|
||||
/// selbst einen neuen Namen und wird neu gelesen; eine unveränderte nie.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser)
|
||||
public sealed class Untertitelvorrat(
|
||||
FlaroPaths pfade,
|
||||
UntertitelLeser leser,
|
||||
IServiceScopeFactory scopes,
|
||||
ILogger<Untertitelvorrat> log)
|
||||
{
|
||||
/// <summary>Was ein Abruf vorfindet.</summary>
|
||||
public enum Stand
|
||||
@@ -39,38 +50,78 @@ public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser)
|
||||
/// <summary>Liegt da und ist leer — diese Spur hat keinen Text.</summary>
|
||||
Leer,
|
||||
|
||||
/// <summary>Wird gerade herausgezogen.</summary>
|
||||
/// <summary>Wird gerade geholt oder wartet in der Reihe.</summary>
|
||||
Laeuft,
|
||||
}
|
||||
|
||||
/// <summary>Ein Lauf je Datei, nicht je Spur — er holt ohnehin alle.</summary>
|
||||
private readonly ConcurrentDictionary<Guid, Task> _laeuft = new();
|
||||
/// <summary>Die Reihe der noch zu lesenden Dateien. Vorn wird genommen.</summary>
|
||||
private readonly LinkedList<Guid> _reihe = new();
|
||||
|
||||
private readonly Lock _schloss = new();
|
||||
private bool _arbeitet;
|
||||
|
||||
/// <summary>Welche Datei gerade gelesen wird — für das Protokoll.</summary>
|
||||
public Guid? Laufende { get; private set; }
|
||||
|
||||
/// <summary>Wie viele noch warten.</summary>
|
||||
public int Wartend { get { lock (_schloss) return _reihe.Count; } }
|
||||
|
||||
public string Pfad(Guid dateiId, DateTimeOffset geaendert, int spur) =>
|
||||
Path.Combine(pfade.Transcode, "untertitel",
|
||||
$"{dateiId:N}-{geaendert.ToUnixTimeMilliseconds()}-{spur}.vtt");
|
||||
|
||||
/// <summary>Stösst das Herausziehen an, wenn etwas fehlt — und kehrt sofort zurück.</summary>
|
||||
public void Anstossen(Guid dateiId, string quelle, DateTimeOffset geaendert, IReadOnlyList<int> spuren)
|
||||
/// <summary>Stellt eine Datei an — vorn, wenn jemand auf sie wartet.</summary>
|
||||
public void Anstellen(Guid dateiId, bool vordraengeln = false)
|
||||
{
|
||||
if (spuren.Count == 0) return;
|
||||
|
||||
var offen = spuren
|
||||
.Where(s => !File.Exists(Pfad(dateiId, geaendert, s)))
|
||||
.Select(s => new UntertitelLeser.Auftrag(s, Pfad(dateiId, geaendert, s)))
|
||||
.ToList();
|
||||
|
||||
if (offen.Count == 0) return;
|
||||
|
||||
// **Ohne das Abbruchzeichen des Abrufers.** Der Lauf gehört nicht dem
|
||||
// ersten, der gefragt hat: schaltet der sein Fenster zu, während die
|
||||
// Datei gelesen wird, wäre sonst alles umsonst und der nächste finge
|
||||
// von vorn an.
|
||||
_laeuft.GetOrAdd(dateiId, _ => Task.Run(async () =>
|
||||
lock (_schloss)
|
||||
{
|
||||
try { await leser.ZiehenAsync(quelle, offen, CancellationToken.None); }
|
||||
finally { _laeuft.TryRemove(dateiId, out Task? _); }
|
||||
}));
|
||||
if (Laufende == dateiId) return;
|
||||
|
||||
var vorhanden = _reihe.Find(dateiId);
|
||||
if (vorhanden is not null)
|
||||
{
|
||||
if (!vordraengeln) return;
|
||||
_reihe.Remove(vorhanden);
|
||||
}
|
||||
|
||||
if (vordraengeln) _reihe.AddFirst(dateiId);
|
||||
else _reihe.AddLast(dateiId);
|
||||
|
||||
if (_arbeitet) return;
|
||||
_arbeitet = true;
|
||||
}
|
||||
|
||||
_ = Task.Run(Abarbeiten);
|
||||
}
|
||||
|
||||
/// <summary>Stellt alles an, was noch keine Ablage hat.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Die zuletzt hinzugekommenen zuerst: was gerade in die Mediathek kam,
|
||||
/// wird am ehesten als Nächstes geschaut.
|
||||
/// </remarks>
|
||||
public async Task NachtragenAsync(CancellationToken ct = default)
|
||||
{
|
||||
using var raum = scopes.CreateScope();
|
||||
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
|
||||
|
||||
var dateien = await db.MediaFiles.AsNoTracking()
|
||||
.OrderByDescending(f => f.Id)
|
||||
.Select(f => new { f.Id, f.ModifiedAt })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var offen = 0;
|
||||
foreach (var datei in dateien)
|
||||
{
|
||||
var spuren = await TextspurenAsync(db, datei.Id, ct);
|
||||
if (spuren.Count == 0) continue;
|
||||
if (spuren.All(s => File.Exists(Pfad(datei.Id, datei.ModifiedAt, s)))) continue;
|
||||
|
||||
Anstellen(datei.Id);
|
||||
offen++;
|
||||
}
|
||||
|
||||
if (offen > 0) log.LogInformation("Untertitel: {Anzahl} Datei(en) werden im Hintergrund gelesen.", offen);
|
||||
}
|
||||
|
||||
/// <summary>Holt eine Spur — und wartet dabei höchstens <paramref name="geduld"/>.</summary>
|
||||
@@ -79,35 +130,127 @@ public sealed class Untertitelvorrat(FlaroPaths pfade, UntertitelLeser leser)
|
||||
/// <b>Warum überhaupt gewartet wird.</b> Ein Abruf, der sofort „noch
|
||||
/// nicht" sagt, zwingt den Spieler in eine Schleife, auch wenn die Datei
|
||||
/// in zwei Sekunden fertig gewesen wäre. Ein Abruf, der unbegrenzt
|
||||
/// wartet, hängt hinter jedem Reverse Proxy ins Zeitlimit. Zwanzig
|
||||
/// Sekunden decken den gewöhnlichen Fall ab; der seltene bekommt eine
|
||||
/// Antwort, mit der sich etwas anfangen lässt.
|
||||
/// wartet, hängt hinter jedem Reverse Proxy ins Zeitlimit.
|
||||
/// </remarks>
|
||||
public async Task<(Stand Stand, string Text)> HolenAsync(
|
||||
Guid dateiId, string quelle, DateTimeOffset geaendert,
|
||||
IReadOnlyList<int> spuren, int spur, TimeSpan geduld, CancellationToken ct)
|
||||
Guid dateiId, DateTimeOffset geaendert, int spur, TimeSpan geduld, CancellationToken ct)
|
||||
{
|
||||
var ziel = Pfad(dateiId, geaendert, spur);
|
||||
if (File.Exists(ziel)) return await Lesen(ziel, ct);
|
||||
|
||||
Anstossen(dateiId, quelle, geaendert, spuren);
|
||||
Anstellen(dateiId, vordraengeln: true);
|
||||
|
||||
var bis = DateTimeOffset.UtcNow + geduld;
|
||||
while (DateTimeOffset.UtcNow < bis)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(400), ct);
|
||||
if (File.Exists(ziel)) return await Lesen(ziel, ct);
|
||||
|
||||
// Durch und trotzdem nichts da: diese Spur liess sich nicht
|
||||
// herausziehen. Weiterzuwarten hiesse, auf etwas zu warten, das
|
||||
// niemand mehr tut.
|
||||
lock (_schloss)
|
||||
{
|
||||
if (Laufende != dateiId && _reihe.Find(dateiId) is null) return (Stand.Leer, "");
|
||||
}
|
||||
}
|
||||
|
||||
return (Stand.Laeuft, "");
|
||||
}
|
||||
|
||||
private static async Task<(Stand, string)> Lesen(string ziel, CancellationToken ct)
|
||||
{
|
||||
var text = await File.ReadAllTextAsync(ziel, ct);
|
||||
return (text.Length == 0 ? Stand.Leer : Stand.Fertig, text);
|
||||
}
|
||||
|
||||
/// <summary>Eine Datei nach der anderen, bis die Reihe leer ist.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Eine, nicht zehn.</b> Das Lesen ist von der Platte begrenzt, nicht
|
||||
/// vom Rechner. Zehn Läufe nebeneinander machen jeden einzelnen
|
||||
/// langsamer und bringen die laufende Wiedergabe zum Stocken — und die
|
||||
/// ist wichtiger als der Vorrat.
|
||||
/// </remarks>
|
||||
private async Task Abarbeiten()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (File.Exists(ziel))
|
||||
Guid naechste;
|
||||
lock (_schloss)
|
||||
{
|
||||
var text = await File.ReadAllTextAsync(ziel, ct);
|
||||
return (text.Length == 0 ? Stand.Leer : Stand.Fertig, text);
|
||||
if (_reihe.First is null)
|
||||
{
|
||||
_arbeitet = false;
|
||||
Laufende = null;
|
||||
return;
|
||||
}
|
||||
|
||||
naechste = _reihe.First.Value;
|
||||
_reihe.RemoveFirst();
|
||||
Laufende = naechste;
|
||||
}
|
||||
|
||||
// Der Lauf ist durch und die Datei fehlt trotzdem: diese Spur
|
||||
// liess sich nicht herausziehen. Weiterzuwarten hiesse, auf
|
||||
// etwas zu warten, das niemand mehr tut.
|
||||
if (!_laeuft.ContainsKey(dateiId)) return (Stand.Leer, "");
|
||||
try { await Ziehen(naechste); }
|
||||
catch (Exception fehler)
|
||||
{
|
||||
log.LogWarning(fehler, "Untertitel der Datei {Kennung} liessen sich nicht holen.", naechste);
|
||||
}
|
||||
|
||||
if (DateTimeOffset.UtcNow >= bis) return (Stand.Laeuft, "");
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(400), ct);
|
||||
// **Luft holen zwischen zwei Dateien.** Der Vorrat liest die
|
||||
// Platte am Stück leer; eine laufende Wiedergabe liest von
|
||||
// derselben Platte und ist wichtiger. Zwei Sekunden kosten bei
|
||||
// einem Lauf über Stunden nichts und nehmen den Druck heraus.
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Ziehen(Guid dateiId)
|
||||
{
|
||||
using var raum = scopes.CreateScope();
|
||||
var db = raum.ServiceProvider.GetRequiredService<FlaroContext>();
|
||||
|
||||
var datei = await db.MediaFiles.AsNoTracking().FirstOrDefaultAsync(f => f.Id == dateiId);
|
||||
if (datei is null || !File.Exists(datei.Path)) return;
|
||||
|
||||
var spuren = await TextspurenAsync(db, dateiId, CancellationToken.None);
|
||||
var offen = spuren
|
||||
.Where(s => !File.Exists(Pfad(dateiId, datei.ModifiedAt, s)))
|
||||
.Select(s => new UntertitelLeser.Auftrag(s, Pfad(dateiId, datei.ModifiedAt, s)))
|
||||
.ToList();
|
||||
|
||||
if (offen.Count == 0) return;
|
||||
|
||||
var begonnen = DateTimeOffset.UtcNow;
|
||||
var fertig = await leser.ZiehenAsync(datei.Path, offen, CancellationToken.None);
|
||||
|
||||
log.LogInformation(
|
||||
"Untertitel aus {Datei}: {Fertig} von {Offen} Spur(en) in {Dauer:0.0} s. Noch {Rest} Datei(en).",
|
||||
Path.GetFileName(datei.Path), fertig.Count, offen.Count,
|
||||
(DateTimeOffset.UtcNow - begonnen).TotalSeconds, Wartend);
|
||||
}
|
||||
|
||||
/// <summary>Die Spurnummern, aus denen sich Text holen lässt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Dieselbe Auswahl, die auch im Menü steht — sonst zöge der Vorrat
|
||||
/// Spuren, die niemand anbietet, oder liesse welche aus, die jemand
|
||||
/// einschaltet. Bildspuren wie PGS lassen sich nicht nach WebVTT
|
||||
/// wandeln; das hiesse, Schrift zu erkennen.
|
||||
/// </remarks>
|
||||
public static async Task<IReadOnlyList<int>> TextspurenAsync(
|
||||
FlaroContext db, Guid dateiId, CancellationToken ct)
|
||||
{
|
||||
var alle = await db.MediaStreams.AsNoTracking()
|
||||
.Where(s => s.MediaFileId == dateiId && s.Kind == MediaStreamKind.Subtitle)
|
||||
.OrderBy(s => s.Index)
|
||||
.Select(s => new { s.Index, s.Codec })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return alle.Where(s => s.Codec is null || !IstBild(s.Codec)).Select(s => s.Index).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Bildbasierte Untertitel lassen sich nicht nach WebVTT wandeln.</summary>
|
||||
public static bool IstBild(string codec) =>
|
||||
codec is "hdmv_pgs_subtitle" or "pgssub" or "dvd_subtitle" or "dvdsub" or "xsub";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user