using Flaro.Api.Auth;
using Flaro.Core;
using Flaro.Core.Delivery;
using Flaro.Core.Playback;
using Flaro.Data;
using Flaro.Metadata;
using Microsoft.EntityFrameworkCore;
namespace Flaro.Api.Endpoints;
public static class SettingsEndpoints
{
public static void MapSettings(this IEndpointRouteBuilder app)
{
app.MapGet("/api/v1/settings/metadata", async (FlaroContext db) =>
{
var zeilen = await db.Settings.AsNoTracking()
.Where(s => s.Key == DatabaseTmdbSettings.ApiKeyName || s.Key == DatabaseTmdbSettings.LanguageName)
.ToDictionaryAsync(s => s.Key, s => s.Value);
// **Der Schlüssel selbst kommt nicht zurück.** Er gäbe jedem
// Angemeldeten das TMDB-Kontingent des Betreibers in die Hand, und
// zum Anzeigen genügt die Auskunft, dass einer da ist.
var aufgelöst = TmdbSettings.Resolve(
zeilen.GetValueOrDefault(DatabaseTmdbSettings.ApiKeyName),
zeilen.GetValueOrDefault(DatabaseTmdbSettings.LanguageName));
return Results.Ok(new MetadataSettingsResponse(
HasApiKey: !string.IsNullOrEmpty(aufgelöst.ApiKey),
Language: aufgelöst.Language,
// Damit in der Oberfläche steht, ob der mitgelieferte Schlüssel
// benutzt wird — und dass ein eigener ihn ablösen würde.
UsingProjectKey: aufgelöst.UsingProjectKey));
}).RequireAuthorization(FlaroPolicies.Administrator);
// **In welcher Sprache der Bestand vorliegt.**
//
// Der eigentliche Fehler war nicht, dass Titel englisch blieben —
// sondern dass niemand feststellen konnte, ob sie es sind. „Alles ist
// noch auf Englisch, obwohl Deutsch eingestellt ist" liess sich nicht
// beantworten, nur bestreiten. Hier steht die Zahl.
app.MapGet("/api/v1/settings/metadata/languages", async (FlaroContext db) =>
{
var eingestellt = await db.Settings.AsNoTracking()
.Where(x => x.Key == DatabaseTmdbSettings.LanguageName)
.Select(x => x.Value).FirstOrDefaultAsync();
var serversprache = TmdbSettings.Resolve(null, eingestellt).Language;
// Nur zugeordnete Titel zählen: was keine Kennung hat, hat auch
// keine Sprache — das ist ein Zuordnungsproblem, kein Sprachproblem.
var filme = await db.Movies.AsNoTracking().Where(m => m.TmdbId != null)
.GroupBy(m => m.MetadataLanguage)
.Select(g => new { Sprache = g.Key, Anzahl = g.Count() }).ToListAsync();
var serien = await db.Series.AsNoTracking().Where(x => x.TmdbId != null)
.GroupBy(x => x.MetadataLanguage)
.Select(g => new { Sprache = g.Key, Anzahl = g.Count() }).ToListAsync();
var zusammen = filme.Concat(serien)
.GroupBy(x => x.Sprache)
.Select(g => new Sprachanteil(g.Key, g.Sum(x => x.Anzahl)))
.OrderByDescending(x => x.Anzahl)
.ToList();
return Results.Ok(new Sprachverteilung(
serversprache,
zusammen,
Abweichend: zusammen.Where(x => x.Sprache != serversprache).Sum(x => x.Anzahl)));
}).RequireAuthorization(FlaroPolicies.Administrator);
// Frischt genau die auf, die nicht zur Serversprache passen.
app.MapPost("/api/v1/settings/metadata/languages/refresh", async (
FlaroContext db, ScanCoordinator koordinator) =>
{
var eingestellt = await db.Settings.AsNoTracking()
.Where(x => x.Key == DatabaseTmdbSettings.LanguageName)
.Select(x => x.Value).FirstOrDefaultAsync();
var serversprache = TmdbSettings.Resolve(null, eingestellt).Language;
var mediatheken = await db.Libraries.AsNoTracking().Select(l => l.Id).ToListAsync();
var gestartet = mediatheken.Count(id => koordinator.TryStart(id, erneut: true, serversprache));
return gestartet > 0
? Results.Accepted(value: new { started = gestartet, language = serversprache })
: Einwand.Belegt("In jeder Mediathek läuft bereits etwas. Warte, bis es durch ist.");
}).RequireAuthorization(FlaroPolicies.Administrator);
app.MapPut("/api/v1/settings/metadata", async (
MetadataSettingsRequest anfrage, FlaroContext db) =>
{
if (anfrage.ApiKey is not null) await Setzen(db, DatabaseTmdbSettings.ApiKeyName, anfrage.ApiKey.Trim());
if (anfrage.Language is not null)
{
var sprache = anfrage.Language.Trim();
// Form wie „de-DE". Alles andere nähme TMDB stillschweigend als
// Englisch an, und niemand wüsste, warum.
if (!System.Text.RegularExpressions.Regex.IsMatch(sprache, "^[a-z]{2}(-[A-Z]{2})?$"))
return Einwand.Bitte($"„{sprache}“ ist keine Sprachkennung. Erwartet wird etwa „de-DE“ oder „en“.");
await Setzen(db, DatabaseTmdbSettings.LanguageName, sprache);
}
await db.SaveChangesAsync();
return Results.NoContent();
}).RequireAuthorization(FlaroPolicies.Administrator);
// ------------------------------------------------------------------
// Wiedergabe
// ------------------------------------------------------------------
app.MapGet("/api/v1/settings/playback", async (Wiedergabeeinstellungen einstellungen) =>
{
var regeln = await einstellungen.RegelnAsync();
var stufe = await einstellungen.StufeAsync();
return Results.Ok(new PlaybackSettingsResponse(
regeln.FortsetzenAb.TotalSeconds, regeln.GesehenAnteil, stufe.AlsWort()));
}).RequireAuthorization(FlaroPolicies.Administrator);
app.MapPut("/api/v1/settings/playback", async (
PlaybackSettingsRequest anfrage, FlaroContext db, TimeProvider clock) =>
{
var vorher = Wiedergabeeinstellungen.Lesen(
(await db.Settings.AsNoTracking()
.FirstOrDefaultAsync(x => x.Key == Wiedergabeeinstellungen.FortsetzenAbName))?.Value,
(await db.Settings.AsNoTracking()
.FirstOrDefaultAsync(x => x.Key == Wiedergabeeinstellungen.GesehenAnteilName))?.Value);
var nachher = Wiedergabeeinstellungen.Lesen(
anfrage.ResumeAfterSeconds?.ToString(System.Globalization.CultureInfo.InvariantCulture)
?? vorher.FortsetzenAb.TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture),
anfrage.WatchedFraction?.ToString(System.Globalization.CultureInfo.InvariantCulture)
?? vorher.GesehenAnteil.ToString(System.Globalization.CultureInfo.InvariantCulture));
await Setzen(db, Wiedergabeeinstellungen.FortsetzenAbName,
nachher.FortsetzenAb.TotalSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture));
await Setzen(db, Wiedergabeeinstellungen.GesehenAnteilName,
nachher.GesehenAnteil.ToString(System.Globalization.CultureInfo.InvariantCulture));
if (anfrage.Encoder is not null)
await Setzen(db, Wiedergabeeinstellungen.TranscoderName, Kodierstufen.Lesen(anfrage.Encoder).AlsWort());
await db.SaveChangesAsync();
// **Was schon dasteht, muss neu bewertet werden.**
//
// `UserItemState.InProgress` ist eine gespeicherte Spalte — die
// Bruchrechnung lässt sich nicht nach SQL übersetzen. Eine neue
// Schwelle wirkte deshalb nur auf künftige Meldungen: wer gestern
// eine Sekunde hineingeschaut hat, stünde bis in alle Ewigkeit in
// „Weiterschauen". Also einmal durch, in Stapeln.
var geaendert = nachher == vorher ? 0 : await NeuBewerten(db, nachher, clock);
return Results.Ok(new PlaybackSettingsApplied(
nachher.FortsetzenAb.TotalSeconds, nachher.GesehenAnteil,
Kodierstufen.Lesen(anfrage.Encoder).AlsWort(), geaendert));
}).RequireAuthorization(FlaroPolicies.Administrator);
}
/// Rechnet „steht in Weiterschauen" für alle Zeilen neu.
///
///
/// In Stapeln zu fünfhundert: ein Haushalt mit drei Leuten und
/// fünftausend Titeln hat höchstens ein paar tausend Zeilen, aber alle
/// auf einmal zu laden ist eine Zahl, die man nicht kennt.
///
private static async Task NeuBewerten(
FlaroContext db, Wiedergaberegeln regeln, TimeProvider clock)
{
const int Stapel = 500;
var geaendert = 0;
var jetzt = clock.GetUtcNow();
for (var sprung = 0; ; sprung += Stapel)
{
var zeilen = await db.UserItemStates
.OrderBy(z => z.Id).Skip(sprung).Take(Stapel).ToListAsync();
if (zeilen.Count == 0) break;
foreach (var z in zeilen)
{
var soll = ProgressRule.BelongsInContinueWatching(z, regeln);
if (soll == z.InProgress) continue;
z.InProgress = soll;
z.UpdatedAt = jetzt;
geaendert++;
}
await db.SaveChangesAsync();
if (zeilen.Count < Stapel) break;
}
return geaendert;
}
private static async Task Setzen(FlaroContext db, string schlüssel, string wert)
{
var vorhanden = await db.Settings.FindAsync(schlüssel);
if (vorhanden is null) db.Settings.Add(new ServerSetting { Key = schlüssel, Value = wert });
else vorhanden.Value = wert;
}
}