diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index abd37b9..b17a5ee 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -155,9 +155,11 @@ public sealed class Markenerkennung( (f, d) => new { f.Id, f.SeasonNumber, f.EpisodeNumber, d.Path, d.Duration }) .ToListAsync(ct); - var vorhanden = await db.Marken.AsNoTracking() - .Where(m => folgen.Select(f => f.Id).Contains(m.EpisodeId)) - .Select(m => new { m.EpisodeId, m.Art }) + // **Was schon einmal gesucht wurde, und mit wie vielen Folgen.** + // Nicht „was gefunden wurde": eine Staffel ohne Abspann sähe sonst + // bei jedem Scan aus wie eine, in der noch niemand nachgesehen hat. + var gesucht = await db.Markensuchen.AsNoTracking() + .Where(x => x.SeriesId == serieId) .ToListAsync(ct); var gefunden = 0; @@ -172,8 +174,9 @@ public sealed class Markenerkennung( // aus demselben Abdruck. Das ist nicht nur richtig, sondern // billiger als zweimal lesen. var fehlt = new[] { Markenart.Intro, Markenart.Abspann } - .Where(art => erneut || !geordnet.All(f => - vorhanden.Any(v => v.EpisodeId == f.Id && v.Art == art))) + .Where(art => erneut || gesucht.FirstOrDefault( + x => x.Staffel == staffel.Key && x.Art == art) is not { } lauf + || lauf.Folgen != geordnet.Count) .ToList(); if (fehlt.Count == 0) continue; @@ -206,6 +209,11 @@ public sealed class Markenerkennung( var treffer = Tonabgleich.ReiheAbgleichen( abdruecke, Tonabdruckleser.SekundenJeAbdruck); + // **Vermerken, dass gesucht wurde — vor dem Ergebnis.** + // Auch ein Lauf ohne Fund ist ein Lauf; ihn nicht zu + // vermerken hiesse, ihn beim nächsten Scan zu wiederholen. + await VermerkenAsync(db, serieId, staffel.Key, art, geordnet.Count, ct); + foreach (var (folgenId, stelle) in treffer) { if (stelle.Bis - stelle.Von > Hoechstlaenge) continue; @@ -222,7 +230,9 @@ public sealed class Markenerkennung( } } - if (gefunden > 0) await db.SaveChangesAsync(ct); + // Immer speichern: auch ein Lauf ohne Fund hinterlässt Vermerke, + // und genau die verhindern den nächsten vergeblichen Lauf. + await db.SaveChangesAsync(ct); log.LogInformation("Markenerkennung für {Serie}: {Anzahl} Marken.", serieId, gefunden); } @@ -243,6 +253,30 @@ public sealed class Markenerkennung( return art == Markenart.Intro ? (0, fenster) : (werte - fenster, fenster); } + /// Hält fest, dass in dieser Staffel gesucht wurde. + private async Task VermerkenAsync( + FlaroContext db, Guid serieId, int staffel, Markenart art, int folgen, CancellationToken ct) + { + var alt = await db.Markensuchen + .FirstOrDefaultAsync(x => x.SeriesId == serieId && x.Staffel == staffel && x.Art == art, ct); + + if (alt is not null) + { + alt.Folgen = folgen; + alt.GelaufenAm = clock.GetUtcNow(); + return; + } + + db.Markensuchen.Add(new Markensuche + { + SeriesId = serieId, + Staffel = staffel, + Art = art, + Folgen = folgen, + GelaufenAm = clock.GetUtcNow(), + }); + } + private async Task SchreibenAsync( FlaroContext db, Guid folgenId, Markenart art, double von, double bis, CancellationToken ct) { diff --git a/server/src/Flaro.Core/Library/CatalogEntities.cs b/server/src/Flaro.Core/Library/CatalogEntities.cs index 78aeb27..bd255dc 100644 --- a/server/src/Flaro.Core/Library/CatalogEntities.cs +++ b/server/src/Flaro.Core/Library/CatalogEntities.cs @@ -165,3 +165,35 @@ public sealed class Marke /// public DateTimeOffset ErkanntAm { get; set; } } + +/// Dass in einer Staffel schon einmal gesucht wurde. +/// +/// +/// +/// Weil „nichts gefunden" auch ein Ergebnis ist. Ohne diese Zeile +/// liess sich nur ablesen, was gefunden wurde. Eine Staffel ohne Abspann — +/// es gibt genug davon — sah damit bei jedem Scan aus wie eine, in der noch +/// niemand nachgesehen hat, und wurde wieder und wieder abgetastet. Das +/// heisst: bei jedem Scan jede Tonspur jeder Folge noch einmal von der +/// Platte lesen, für immer, ohne Ertrag. +/// +/// +/// Die Folgenzahl ist der Auslöser. Kommt eine Folge dazu, passt die +/// Zahl nicht mehr und es wird neu gerechnet — genau dann, wenn es etwas +/// Neues zu vergleichen gibt. Ein Zeitstempel allein täte das nicht: eine +/// Staffel, die sich nie ändert, hat nie einen Grund für einen zweiten +/// Lauf. +/// +/// +public sealed class Markensuche +{ + public Guid Id { get; init; } = Guid.CreateVersion7(); + public required Guid SeriesId { get; init; } + public required int Staffel { get; init; } + public required Markenart Art { get; init; } + + /// Wie viele Folgen die Staffel hatte, als gesucht wurde. + public required int Folgen { get; set; } + + public DateTimeOffset GelaufenAm { get; set; } +} diff --git a/server/src/Flaro.Data/FlaroContext.cs b/server/src/Flaro.Data/FlaroContext.cs index 472c2ca..2e5ef9c 100644 --- a/server/src/Flaro.Data/FlaroContext.cs +++ b/server/src/Flaro.Data/FlaroContext.cs @@ -77,6 +77,7 @@ public sealed class FlaroContext(DbContextOptions options) : DbCon public DbSet GenreLinks => Set(); public DbSet Fremdbewertungen => Set(); public DbSet Marken => Set(); + public DbSet Markensuchen => Set(); public DbSet Stichworte => Set(); public DbSet Stichwortlinks => Set(); public DbSet People => Set(); @@ -319,6 +320,14 @@ public sealed class FlaroContext(DbContextOptions options) : DbCon e.Property(x => x.Art).HasConversion(); }); + b.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.SeriesId, x.Staffel, x.Art }).IsUnique(); + e.HasOne().WithMany().HasForeignKey(x => x.SeriesId).OnDelete(DeleteBehavior.Cascade); + e.Property(x => x.Art).HasConversion(); + }); + b.Entity(e => { e.HasKey(x => x.Id); diff --git a/server/src/Flaro.Data/Migrations/20261013034531_Markensuche.Designer.cs b/server/src/Flaro.Data/Migrations/20261013034531_Markensuche.Designer.cs new file mode 100644 index 0000000..a24c7ff --- /dev/null +++ b/server/src/Flaro.Data/Migrations/20261013034531_Markensuche.Designer.cs @@ -0,0 +1,1337 @@ +// +using System; +using Flaro.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flaro.Data.Migrations +{ + [DbContext(typeof(FlaroContext))] + [Migration("20261013034531_Markensuche")] + partial class Markensuche + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("Flaro.Core.Auth.AuthCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApprovedByUserId") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("ConsumedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("INTEGER"); + + b.Property("HandleFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("HandleFingerprint") + .IsUnique(); + + b.ToTable("AuthCodes"); + }); + + modelBuilder.Entity("Flaro.Core.Auth.DeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("INTEGER"); + + b.Property("RevokedAt") + .HasColumnType("INTEGER"); + + b.Property("TokenFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TokenFingerprint") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("DeviceTokens"); + }); + + modelBuilder.Entity("Flaro.Core.Auth.Einladung", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("CreatedByUserId") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("INTEGER"); + + b.Property("MaxUses") + .HasColumnType("INTEGER"); + + b.Property("RevokedAt") + .HasColumnType("INTEGER"); + + b.Property("TokenFingerprint") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UsedCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("TokenFingerprint") + .IsUnique(); + + b.ToTable("Einladungen"); + }); + + modelBuilder.Entity("Flaro.Core.Libraries.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ReviewNewItems") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Credit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Character") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("PersonId"); + + b.HasIndex("SeriesId"); + + b.ToTable("Credits"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Episode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddedAt") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("MetadataLanguage") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("PublishAt") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("StillImage") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId", "SeasonNumber", "EpisodeNumber") + .IsUnique(); + + b.ToTable("Episodes"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Fremdbewertung", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Anzeige") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Quelle") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Wert") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("MovieId", "Quelle") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("SeriesId", "Quelle") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Fremdbewertungen"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("Flaro.Core.Library.GenreLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("GenreId") + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("GenreId", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("GenreId", "SeriesId") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("GenreLinks"); + }); + + modelBuilder.Entity("Flaro.Core.Library.ItemTranslation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("FetchedAt") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId", "Language") + .IsUnique() + .HasFilter("\"EpisodeId\" IS NOT NULL"); + + b.HasIndex("MovieId", "Language") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("SeriesId", "Language") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Marke", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("BisSekunden") + .HasColumnType("REAL"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("ErkanntAm") + .HasColumnType("INTEGER"); + + b.Property("VonSekunden") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId", "Art") + .IsUnique(); + + b.ToTable("Marken"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Markensuche", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("Folgen") + .HasColumnType("INTEGER"); + + b.Property("GelaufenAm") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Staffel") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId", "Staffel", "Art") + .IsUnique(); + + b.ToTable("Markensuchen"); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("Container") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.Property("MissingSince") + .HasColumnType("INTEGER"); + + b.Property("ModifiedAt") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ProbedAt") + .HasColumnType("INTEGER"); + + b.Property("SeenAt") + .HasColumnType("INTEGER"); + + b.Property("SizeBytes") + .HasColumnType("INTEGER"); + + b.Property("VersionLabel") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("LibraryId", "Path") + .IsUnique(); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("MediaFileId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileId", "Index") + .IsUnique(); + + b.ToTable("MediaStreams"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Meldung", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("ErledigtAm") + .HasColumnType("INTEGER"); + + b.Property("ErledigtVon") + .HasColumnType("TEXT"); + + b.Property("GemeldetAm") + .HasColumnType("INTEGER"); + + b.Property("Kommentar") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Stelle") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("ErledigtAm", "GemeldetAm"); + + b.HasIndex("UserId", "GemeldetAm"); + + b.ToTable("Meldungen"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddedAt") + .HasColumnType("INTEGER"); + + b.Property("Aufmacherbilder") + .HasColumnType("TEXT"); + + b.Property("BackdropImage") + .HasColumnType("TEXT"); + + b.Property("BackdropPinned") + .HasColumnType("INTEGER"); + + b.Property("ChangedSince") + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("FassungenGeprueftBei") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.Property("LogoImage") + .HasColumnType("TEXT"); + + b.Property("LogoPinned") + .HasColumnType("INTEGER"); + + b.Property("MatchLocked") + .HasColumnType("INTEGER"); + + b.Property("MetadataLanguage") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("ParsedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParsedYear") + .HasColumnType("INTEGER"); + + b.Property("PosterImage") + .HasColumnType("TEXT"); + + b.Property("PosterPinned") + .HasColumnType("INTEGER"); + + b.Property("PublishAt") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("RuntimeMinutes") + .HasColumnType("INTEGER"); + + b.Property("SortTitlePinned") + .HasColumnType("INTEGER"); + + b.Property("SortTitleValue") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("TitlePinned") + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId", "MetadataLanguage"); + + b.HasIndex("LibraryId", "SortTitleValue"); + + b.HasIndex("LibraryId", "TmdbId") + .IsUnique() + .HasFilter("\"TmdbId\" IS NOT NULL"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Image") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("TmdbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TmdbId") + .IsUnique(); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AddedAt") + .HasColumnType("INTEGER"); + + b.Property("Aufmacherbilder") + .HasColumnType("TEXT"); + + b.Property("BackdropImage") + .HasColumnType("TEXT"); + + b.Property("BackdropPinned") + .HasColumnType("INTEGER"); + + b.Property("ChangedSince") + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("ImdbId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.Property("LogoImage") + .HasColumnType("TEXT"); + + b.Property("LogoPinned") + .HasColumnType("INTEGER"); + + b.Property("MatchLocked") + .HasColumnType("INTEGER"); + + b.Property("MetadataLanguage") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("ParsedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParsedYear") + .HasColumnType("INTEGER"); + + b.Property("PosterImage") + .HasColumnType("TEXT"); + + b.Property("PosterPinned") + .HasColumnType("INTEGER"); + + b.Property("PublishAt") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("RuntimeMinutes") + .HasColumnType("INTEGER"); + + b.Property("SortTitlePinned") + .HasColumnType("INTEGER"); + + b.Property("SortTitleValue") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("TitlePinned") + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId", "MetadataLanguage"); + + b.HasIndex("LibraryId", "SortTitleValue"); + + b.HasIndex("LibraryId", "TmdbId") + .IsUnique() + .HasFilter("\"TmdbId\" IS NOT NULL"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Stichwort", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Stichworte"); + }); + + modelBuilder.Entity("Flaro.Core.Library.Stichwortlink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("StichwortId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("StichwortId", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("StichwortId", "SeriesId") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.ToTable("Stichwortlinks"); + }); + + modelBuilder.Entity("Flaro.Core.Playback.Sehtag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("Sekunden") + .HasColumnType("REAL"); + + b.Property("Tag") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("UserId", "Tag"); + + b.HasIndex("UserId", "Tag", "EpisodeId") + .IsUnique() + .HasFilter("\"EpisodeId\" IS NOT NULL"); + + b.HasIndex("UserId", "Tag", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.ToTable("Sehtage"); + }); + + modelBuilder.Entity("Flaro.Core.Playback.UserItemState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Dismissed") + .HasColumnType("INTEGER"); + + b.Property("EpisodeId") + .HasColumnType("TEXT"); + + b.Property("Favourite") + .HasColumnType("INTEGER"); + + b.Property("InProgress") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedAt") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("TEXT"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Runtime") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Watched") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("UserId", "EpisodeId") + .IsUnique() + .HasFilter("\"EpisodeId\" IS NOT NULL"); + + b.HasIndex("UserId", "MovieId") + .IsUnique() + .HasFilter("\"MovieId\" IS NOT NULL"); + + b.HasIndex("UserId", "SeriesId") + .IsUnique() + .HasFilter("\"SeriesId\" IS NOT NULL"); + + b.HasIndex("UserId", "InProgress", "UpdatedAt"); + + b.ToTable("UserItemStates"); + }); + + modelBuilder.Entity("Flaro.Core.ServerSetting", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("Flaro.Core.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarImage") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("IsAdministrator") + .HasColumnType("INTEGER"); + + b.Property("IsOwner") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Flaro.Core.Auth.DeviceToken", b => + { + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Auth.Einladung", b => + { + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Credit", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.Episode", b => + { + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Fremdbewertung", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.GenreLink", b => + { + b.HasOne("Flaro.Core.Library.Genre", null) + .WithMany() + .HasForeignKey("GenreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Flaro.Core.Library.Marke", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Markensuche", b => + { + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => + { + b.HasOne("Flaro.Core.Libraries.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.MediaStream", b => + { + b.HasOne("Flaro.Core.Library.MediaFile", null) + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Meldung", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Movie", b => + { + b.HasOne("Flaro.Core.Libraries.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Series", b => + { + b.HasOne("Flaro.Core.Libraries.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Library.Stichwortlink", b => + { + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Stichwort", null) + .WithMany() + .HasForeignKey("StichwortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Playback.Sehtag", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flaro.Core.Playback.UserItemState", b => + { + b.HasOne("Flaro.Core.Library.Episode", null) + .WithMany() + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Movie", null) + .WithMany() + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Flaro.Core.Users.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Flaro.Data/Migrations/20261013034531_Markensuche.cs b/server/src/Flaro.Data/Migrations/20261013034531_Markensuche.cs new file mode 100644 index 0000000..226a7c8 --- /dev/null +++ b/server/src/Flaro.Data/Migrations/20261013034531_Markensuche.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flaro.Data.Migrations +{ + /// + public partial class Markensuche : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Markensuchen", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + SeriesId = table.Column(type: "TEXT", nullable: false), + Staffel = table.Column(type: "INTEGER", nullable: false), + Art = table.Column(type: "INTEGER", nullable: false), + Folgen = table.Column(type: "INTEGER", nullable: false), + GelaufenAm = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Markensuchen", x => x.Id); + table.ForeignKey( + name: "FK_Markensuchen_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Markensuchen_SeriesId_Staffel_Art", + table: "Markensuchen", + columns: new[] { "SeriesId", "Staffel", "Art" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Markensuchen"); + } + } +} diff --git a/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs b/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs index e94e37b..4d49f06 100644 --- a/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs +++ b/server/src/Flaro.Data/Migrations/FlaroContextModelSnapshot.cs @@ -435,6 +435,35 @@ namespace Flaro.Data.Migrations b.ToTable("Marken"); }); + modelBuilder.Entity("Flaro.Core.Library.Markensuche", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Art") + .HasColumnType("INTEGER"); + + b.Property("Folgen") + .HasColumnType("INTEGER"); + + b.Property("GelaufenAm") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("Staffel") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId", "Staffel", "Art") + .IsUnique(); + + b.ToTable("Markensuchen"); + }); + modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => { b.Property("Id") @@ -1169,6 +1198,15 @@ namespace Flaro.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("Flaro.Core.Library.Markensuche", b => + { + b.HasOne("Flaro.Core.Library.Series", null) + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Flaro.Core.Library.MediaFile", b => { b.HasOne("Flaro.Core.Libraries.Library", null) diff --git a/server/tests/Flaro.Api.Tests/MarkenTests.cs b/server/tests/Flaro.Api.Tests/MarkenTests.cs index 8954f19..56d3cdd 100644 --- a/server/tests/Flaro.Api.Tests/MarkenTests.cs +++ b/server/tests/Flaro.Api.Tests/MarkenTests.cs @@ -168,4 +168,52 @@ public class MarkenTests Assert.That(unbekannt.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); }); } + + /// Eine abgesuchte Staffel wird nicht noch einmal abgetastet. + /// + /// + /// Der teuerste Fehler, den es hier gab. Gemerkt wurde nur, was + /// gefunden wurde. Eine Staffel ohne Abspann — es gibt genug davon — + /// sah damit bei jedem Scan aus wie eine, in der noch niemand + /// nachgesehen hat: jede Tonspur jeder Folge noch einmal von der + /// Platte, für immer, ohne Ertrag. + /// + /// Geprüft wird hier der Vermerk, nicht das Abtasten selbst — ohne + /// fpcalc und ohne echten Ton gibt es nichts zu finden, und genau + /// dieser Fall ist der, um den es geht. + /// + [Test] + public async Task Ein_Lauf_ohne_Fund_merkt_sich_das() + { + Datei("Dark/Season 01/Dark - S01E01 - Geheimnisse.mkv"); + Datei("Dark/Season 01/Dark - S01E02 - Luegen.mkv"); + + using var server = new FlaroServer(); + var client = await Eingelesen(server, "Shows"); + var serien = await client.GetFromJsonAsync>("/api/v1/series"); + var serie = serien!.Items[0].Id; + + (await client.PostAsync($"/api/v1/series/{serie}/marken", null)).EnsureSuccessStatusCode(); + + // Der Lauf hängt im Hintergrund; ohne fpcalc ist er sofort durch. + List vermerke = []; + for (var versuch = 0; versuch < 40; versuch++) + { + await server.InDerDatenbank(async db => + vermerke = await db.Markensuchen.AsNoTracking() + .Where(x => x.SeriesId == serie).ToListAsync()); + if (vermerke.Count > 0) break; + await Task.Delay(50); + } + + // Ohne fpcalc gibt es keinen Vermerk — dann ist nichts gelaufen und + // auch nichts zu merken. Mit fpcalc müssen beide Arten dastehen. + Assert.That(vermerke.Count, Is.AnyOf(0, 2), + "entweder gar nicht gelaufen oder beide Arten vermerkt"); + if (vermerke.Count == 2) + { + Assert.That(vermerke.Select(v => v.Folgen), Is.All.EqualTo(2), + "die Folgenzahl löst den nächsten Lauf aus"); + } + } } diff --git a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs b/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs index f0acbfb..258e12c 100644 --- a/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs +++ b/server/tests/Flaro.Media.Tests/TonabdruckleserTests.cs @@ -66,7 +66,7 @@ public class TonabdruckRateTests { // 21 786 Werte für 2700 Sekunden. var gemessen = 2700.0 / 21786; - Assert.That(Tonabdruckleser.SekundenJeAbdruck, Is.EqualTo(gemessen).Within(0.0005)); + Assert.That(Math.Abs(gemessen - Tonabdruckleser.SekundenJeAbdruck), Is.LessThan(0.0005)); } /// Eine Stunde Abdruck passt in eine überschaubare Reihe. @@ -80,6 +80,6 @@ public class TonabdruckRateTests public void Eine_Stunde_sind_rund_29000_Werte() { var werte = (int)(3600 / Tonabdruckleser.SekundenJeAbdruck); - Assert.That(werte, Is.EqualTo(29000).Within(500)); + Assert.That(werte, Is.InRange(28500, 29500)); } } diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts index 7990c81..09ee957 100644 --- a/web/app/src/views/detail.ts +++ b/web/app/src/views/detail.ts @@ -485,11 +485,19 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem // Auf Zuruf und nicht von selbst: der Lauf liest von jeder Folge ein // Stück Ton, und das dauert Minuten. Ihn bei jedem Scan mitlaufen zu // lassen hiesse, jeden Scan um ein Vielfaches zu verlängern. + // + // **Der Knopf sucht immer neu.** Von selbst läuft die Suche nach jedem + // Scan, und dort wird übersprungen, was schon abgesucht ist — sonst + // läse der Server bei jedem Scan jede Tonspur neu. Wer hier drückt, + // will aber genau das: noch einmal, und zwar jetzt. Ein Knopf, der je + // nach gespeichertem Zustand mal etwas tut und mal nicht, wäre der + // schlechtere Tausch. if (melden && istAdmin() && art === 'serien') { - reihe.appendChild(rundKnopf('uhr', false, 'Vorspann und Abspann suchen', + reihe.appendChild(rundKnopf('uhr', false, 'Vorspann und Abspann neu suchen', async () => { - await markenSuchen(eintrag.id); - hinweisZeigen('Vorspann und Abspann werden gesucht. Das dauert ein paar Minuten.'); + await markenSuchen(eintrag.id, true); + hinweisZeigen('Vorspann und Abspann werden gesucht. ' + + 'Der Server liest dafür jede Folge einmal durch — das dauert ein paar Minuten.'); })); }