diff --git a/IntroSkipper/Analyzers/ChapterAnalyzer.cs b/IntroSkipper/Analyzers/ChapterAnalyzer.cs index 693bda9..ab9bc18 100644 --- a/IntroSkipper/Analyzers/ChapterAnalyzer.cs +++ b/IntroSkipper/Analyzers/ChapterAnalyzer.cs @@ -33,14 +33,19 @@ public class ChapterAnalyzer(ILogger logger) : IMediaFileAnalyz AnalysisMode mode, CancellationToken cancellationToken) { - var expression = mode switch + var expression = Plugin.Instance!.GetSeasonRegex(analysisQueue[0].SeasonId, mode); + + if (string.IsNullOrWhiteSpace(expression)) { - AnalysisMode.Introduction => _config.ChapterAnalyzerIntroductionPattern, - AnalysisMode.Credits => _config.ChapterAnalyzerEndCreditsPattern, - AnalysisMode.Recap => _config.ChapterAnalyzerRecapPattern, - AnalysisMode.Preview => _config.ChapterAnalyzerPreviewPattern, - _ => throw new ArgumentOutOfRangeException(nameof(mode), $"Unexpected analysis mode: {mode}") - }; + expression = mode switch + { + AnalysisMode.Introduction => _config.ChapterAnalyzerIntroductionPattern, + AnalysisMode.Credits => _config.ChapterAnalyzerEndCreditsPattern, + AnalysisMode.Recap => _config.ChapterAnalyzerRecapPattern, + AnalysisMode.Preview => _config.ChapterAnalyzerPreviewPattern, + _ => throw new ArgumentOutOfRangeException(nameof(mode), $"Unexpected analysis mode: {mode}") + }; + } if (string.IsNullOrWhiteSpace(expression)) { diff --git a/IntroSkipper/Configuration/configPage.html b/IntroSkipper/Configuration/configPage.html index 81b74dc..23911f7 100644 --- a/IntroSkipper/Configuration/configPage.html +++ b/IntroSkipper/Configuration/configPage.html @@ -469,6 +469,12 @@ + + +
+ +
+ +
+
@@ -800,9 +820,13 @@ // visualizer elements var analyzerActionsSection = document.querySelector("div#analyzerActionsSection"); var actionIntro = analyzerActionsSection.querySelector("select#actionIntro"); + var regexIntro = analyzerActionsSection.querySelector("input#regexIntro"); var actionCredits = analyzerActionsSection.querySelector("select#actionCredits"); + var regexCredits = analyzerActionsSection.querySelector("input#regexCredits"); var actionRecap = analyzerActionsSection.querySelector("select#actionRecap"); + var regexRecap = analyzerActionsSection.querySelector("input#regexRecap"); var actionPreview = analyzerActionsSection.querySelector("select#actionPreview"); + var regexPreview = analyzerActionsSection.querySelector("input#regexPreview"); var saveAnalyzerActionsButton = analyzerActionsSection.querySelector("button#saveAnalyzerActions"); var canvas = document.querySelector("canvas#troubleshooter"); var selectShow = document.querySelector("select#troubleshooterShow"); @@ -1085,6 +1109,11 @@ actionCredits.value = analyzerActions.Credits || "Default"; actionRecap.value = analyzerActions.Recap || "Default"; actionPreview.value = analyzerActions.Preview || "Default"; + const analyzerRegexs = await getJson("Intros/AnalyzerRegexs/" + encodeURI(selectSeason.value)); + regexIntro.value = analyzerRegexs.Introduction || ""; + regexCredits.value = analyzerRegexs.Credits || ""; + regexRecap.value = analyzerRegexs.Recap || ""; + regexPreview.value = analyzerRegexs.Preview || ""; analyzerActionsSection.style.display = "unset"; // show the erase season button @@ -1542,7 +1571,7 @@ saveAnalyzerActionsButton.addEventListener("click", () => { Dashboard.showLoadingMsg(); - var url = "Intros/AnalyzerActions/UpdateSeason"; + var url1 = "Intros/AnalyzerActions/UpdateSeason"; const actions = { id: selectSeason.value, analyzerActions: { @@ -1553,7 +1582,20 @@ }, }; - fetchWithAuth(url, "POST", JSON.stringify(actions)); + var url2 = "Intros/AnalyzerRegexs/UpdateSeason"; + + const regexs = { + id: selectSeason.value, + regexs: { + Introduction: regexIntro.value, + Credits: regexCredits.value, + Recap: regexRecap.value, + Preview: regexPreview.value, + }, + }; + + fetchWithAuth(url1, "POST", JSON.stringify(actions)); + fetchWithAuth(url2, "POST", JSON.stringify(regexs)); Dashboard.alert("Analyzer actions updated for " + selectSeason.value + " of " + selectShow.value); Dashboard.hideLoadingMsg(); diff --git a/IntroSkipper/Controllers/VisualizationController.cs b/IntroSkipper/Controllers/VisualizationController.cs index 6a8a0c1..f09a801 100644 --- a/IntroSkipper/Controllers/VisualizationController.cs +++ b/IntroSkipper/Controllers/VisualizationController.cs @@ -105,6 +105,28 @@ public class VisualizationController(ILogger logger, Me return Ok(analyzerActions); } + /// + /// Returns the analyzer actions for the provided season. + /// + /// Season ID. + /// List of episode titles. + [HttpGet("AnalyzerRegexs/{SeasonId}")] + public ActionResult> GetSeasonRegexs([FromRoute] Guid seasonId) + { + if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(seasonId)) + { + return NotFound(); + } + + var seasonRegexs = new Dictionary(); + foreach (var mode in Enum.GetValues()) + { + seasonRegexs[mode] = Plugin.Instance!.GetSeasonRegex(seasonId, mode); + } + + return Ok(seasonRegexs); + } + /// /// Returns the names and unique identifiers of all episodes in the provided season. /// @@ -227,6 +249,20 @@ public class VisualizationController(ILogger logger, Me return NoContent(); } + /// + /// Updates the analyzer regexs for the provided season. + /// + /// Update analyzer regexs request. + /// No content. + [HttpPost("AnalyzerRegexs/UpdateSeason")] + public async Task UpdateAnalyzerRegexs([FromBody] UpdateSeasonRegexRequest request) + { + _logger.LogInformation("Updating analyzer regexs for {SeasonId} with {SeasonRegexs}", request.Id, request.SeasonRegexs); + await Plugin.Instance!.SetSeasonRegexAsync(request.Id, request.SeasonRegexs).ConfigureAwait(false); + + return NoContent(); + } + private static string GetProductionYear(Guid seriesId) { return seriesId == Guid.Empty diff --git a/IntroSkipper/Data/UpdateSeasonRegexRequest.cs b/IntroSkipper/Data/UpdateSeasonRegexRequest.cs new file mode 100644 index 0000000..61beeab --- /dev/null +++ b/IntroSkipper/Data/UpdateSeasonRegexRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace IntroSkipper.Data +{ + /// + /// /// Update analyzer actions request. + /// + public class UpdateSeasonRegexRequest + { + /// + /// Gets or sets season ID. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets analyzer actions. + /// + public IReadOnlyDictionary SeasonRegexs { get; set; } = new Dictionary(); + } +} diff --git a/IntroSkipper/Db/DbSeasonInfo.cs b/IntroSkipper/Db/DbSeasonInfo.cs index 83dc8c6..196a420 100644 --- a/IntroSkipper/Db/DbSeasonInfo.cs +++ b/IntroSkipper/Db/DbSeasonInfo.cs @@ -22,12 +22,14 @@ public class DbSeasonInfo /// Analysis mode. /// Analyzer action. /// Episode IDs. - public DbSeasonInfo(Guid seasonId, AnalysisMode mode, AnalyzerAction action, IEnumerable? episodeIds = null) + /// Regex. + public DbSeasonInfo(Guid seasonId, AnalysisMode mode, AnalyzerAction action, IEnumerable? episodeIds = null, string? regex = null) { SeasonId = seasonId; Type = mode; Action = action; EpisodeIds = episodeIds ?? []; + Regex = regex ?? string.Empty; } /// @@ -56,4 +58,9 @@ public class DbSeasonInfo /// Gets the season number. /// public IEnumerable EpisodeIds { get; private set; } = []; + + /// + /// Gets the season number. + /// + public string Regex { get; private set; } = string.Empty; } diff --git a/IntroSkipper/Db/IntroSkipperDbContext.cs b/IntroSkipper/Db/IntroSkipperDbContext.cs index 920d926..d2aa116 100644 --- a/IntroSkipper/Db/IntroSkipperDbContext.cs +++ b/IntroSkipper/Db/IntroSkipperDbContext.cs @@ -99,6 +99,9 @@ public class IntroSkipperDbContext : DbContext (c1, c2) => (c1 ?? new List()).SequenceEqual(c2 ?? new List()), c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), c => c.ToList())); + + entity.Property(e => e.Regex) + .HasDefaultValue(string.Empty); }); base.OnModelCreating(modelBuilder); diff --git a/IntroSkipper/Migrations/20241125172633_SeasonRegex.Designer.cs b/IntroSkipper/Migrations/20241125172633_SeasonRegex.Designer.cs new file mode 100644 index 0000000..0f926a4 --- /dev/null +++ b/IntroSkipper/Migrations/20241125172633_SeasonRegex.Designer.cs @@ -0,0 +1,79 @@ +// +using System; +using IntroSkipper.Db; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IntroSkipper.Migrations +{ + [DbContext(typeof(IntroSkipperDbContext))] + [Migration("20241125172633_SeasonRegex")] + partial class SeasonRegex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); + + modelBuilder.Entity("IntroSkipper.Db.DbSeasonInfo", b => + { + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Action") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("EpisodeIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.HasKey("SeasonId", "Type"); + + b.HasIndex("SeasonId"); + + b.ToTable("DbSeasonInfo", (string)null); + }); + + modelBuilder.Entity("IntroSkipper.Db.DbSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("End") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(0.0); + + b.Property("Start") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(0.0); + + b.HasKey("ItemId", "Type"); + + b.HasIndex("ItemId"); + + b.ToTable("DbSegment", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/IntroSkipper/Migrations/20241125172633_SeasonRegex.cs b/IntroSkipper/Migrations/20241125172633_SeasonRegex.cs new file mode 100644 index 0000000..a28e280 --- /dev/null +++ b/IntroSkipper/Migrations/20241125172633_SeasonRegex.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IntroSkipper.Migrations +{ + /// + public partial class SeasonRegex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Regex", + table: "DbSeasonInfo", + type: "TEXT", + nullable: false, + defaultValue: string.Empty); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Regex", + table: "DbSeasonInfo"); + } + } +} diff --git a/IntroSkipper/Migrations/IntroSkipperDbContextModelSnapshot.cs b/IntroSkipper/Migrations/IntroSkipperDbContextModelSnapshot.cs index db60c7f..c006c3d 100644 --- a/IntroSkipper/Migrations/IntroSkipperDbContextModelSnapshot.cs +++ b/IntroSkipper/Migrations/IntroSkipperDbContextModelSnapshot.cs @@ -3,6 +3,7 @@ using System; using IntroSkipper.Db; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -14,7 +15,7 @@ namespace IntroSkipper.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); modelBuilder.Entity("IntroSkipper.Db.DbSeasonInfo", b => { @@ -33,6 +34,12 @@ namespace IntroSkipper.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("Regex") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + b.HasKey("SeasonId", "Type"); b.HasIndex("SeasonId"); diff --git a/IntroSkipper/Plugin.cs b/IntroSkipper/Plugin.cs index 7711b58..1bbd590 100644 --- a/IntroSkipper/Plugin.cs +++ b/IntroSkipper/Plugin.cs @@ -308,6 +308,35 @@ public class Plugin : BasePlugin, IHasWebPages .ToDictionary(s => s.Type, s => s.EpisodeIds); } + internal string GetSeasonRegex(Guid id, AnalysisMode mode) + { + using var db = new IntroSkipperDbContext(_dbPath); + return db.DbSeasonInfo.FirstOrDefault(s => s.SeasonId == id && s.Type == mode)?.Regex ?? string.Empty; + } + + internal async Task SetSeasonRegexAsync(Guid id, IReadOnlyDictionary regexs) + { + using var db = new IntroSkipperDbContext(_dbPath); + var existingEntries = await db.DbSeasonInfo + .Where(s => s.SeasonId == id) + .ToDictionaryAsync(s => s.Type) + .ConfigureAwait(false); + + foreach (var (mode, regex) in regexs) + { + if (existingEntries.TryGetValue(mode, out var existing)) + { + db.Entry(existing).Property(s => s.Regex).CurrentValue = regex; + } + else + { + db.DbSeasonInfo.Add(new DbSeasonInfo(id, mode, AnalyzerAction.Default, regex: regex)); + } + } + + await db.SaveChangesAsync().ConfigureAwait(false); + } + internal AnalyzerAction GetAnalyzerAction(Guid id, AnalysisMode mode) { using var db = new IntroSkipperDbContext(_dbPath);