using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Mime; using ConfusedPolarBear.Plugin.IntroSkipper.Data; using MediaBrowser.Common.Api; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace ConfusedPolarBear.Plugin.IntroSkipper.Controllers; /// /// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis. /// [Authorize(Policy = Policies.RequiresElevation)] [ApiController] [Produces(MediaTypeNames.Application.Json)] [Route("Intros")] public class VisualizationController : ControllerBase { private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Logger. public VisualizationController(ILogger logger) { _logger = logger; } /// /// Returns all show names and seasons. /// /// Dictionary of show names to a list of season names. [HttpGet("Shows")] public ActionResult>> GetShowSeasons() { _logger.LogDebug("Returning season names by series"); var showSeasons = new Dictionary>(); // Loop through all seasons in the analysis queue foreach (var kvp in Plugin.Instance!.QueuedMediaItems) { // Check that this season contains at least one episode. var episodes = kvp.Value; if (episodes is null || episodes.Count == 0) { _logger.LogDebug("Skipping season {Id} (null or empty)", kvp.Key); continue; } // Peek at the top episode from this season and store the series name and season number. var first = episodes[0]; var series = first.SeriesName; var season = GetSeasonName(first); // Validate the series and season before attempting to store it. if (string.IsNullOrWhiteSpace(series) || string.IsNullOrWhiteSpace(season)) { _logger.LogDebug("Skipping season {Id} (no name or number)", kvp.Key); continue; } // TryAdd is used when adding the HashSet since it is a no-op if one was already created for this series. showSeasons.TryAdd(series, new HashSet()); showSeasons[series].Add(season); } return showSeasons; } /// /// Returns the ignore list for the provided season. /// /// Show name. /// Season name. /// List of episode titles. [HttpGet("IgnoreList/{Series}/{Season}")] public ActionResult GetIgnoreListSeason([FromRoute] string series, [FromRoute] string season) { if (!LookupSeasonIdByName(series, season, out var seasonId)) { return NotFound(); } if (!Plugin.Instance!.IgnoreList.TryGetValue(seasonId, out _)) { return new IgnoreListItem(seasonId); } return new IgnoreListItem(Plugin.Instance!.IgnoreList[seasonId]); } /// /// Returns the ignore list for the provided series. /// /// Show name. /// List of episode titles. [HttpGet("IgnoreList/{Series}")] public ActionResult GetIgnoreListSeries([FromRoute] string series) { if (!LookupSeasonIdsByName(series, out var seasonIds)) { return NotFound(); } return new IgnoreListItem(Guid.Empty) { IgnoreIntro = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Introduction)), IgnoreCredits = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Credits)) }; } /// /// Returns the names and unique identifiers of all episodes in the provided season. /// /// Show name. /// Season name. /// List of episode titles. [HttpGet("Show/{Series}/{Season}")] public ActionResult> GetSeasonEpisodes([FromRoute] string series, [FromRoute] string season) { var visualEpisodes = new List(); if (!LookupSeasonByName(series, season, out var episodes)) { return NotFound(); } foreach (var e in episodes) { visualEpisodes.Add(new EpisodeVisualization(e.EpisodeId, e.Name)); } return visualEpisodes; } /// /// Fingerprint the provided episode and returns the uncompressed fingerprint data points. /// /// Episode id. /// Read only collection of fingerprint points. [HttpGet("Episode/{Id}/Chromaprint")] public ActionResult GetEpisodeFingerprint([FromRoute] Guid id) { // Search through all queued episodes to find the requested id foreach (var season in Plugin.Instance!.QueuedMediaItems) { foreach (var needle in season.Value) { if (needle.EpisodeId == id) { return FFmpegWrapper.Fingerprint(needle, AnalysisMode.Introduction); } } } return NotFound(); } /// /// Erases all timestamps for the provided season. /// /// Show name. /// Season name. /// Erase cache. /// Season timestamps erased. /// Unable to find season in provided series. /// No content. [HttpDelete("Show/{Series}/{Season}")] public ActionResult EraseSeason([FromRoute] string series, [FromRoute] string season, [FromQuery] bool eraseCache = false) { if (!LookupSeasonByName(series, season, out var episodes)) { return NotFound(); } _logger.LogInformation("Erasing timestamps for {Series} {Season} at user request", series, season); foreach (var e in episodes) { Plugin.Instance!.Intros.TryRemove(e.EpisodeId, out _); Plugin.Instance!.Credits.TryRemove(e.EpisodeId, out _); e.State.ResetStates(); if (eraseCache) { FFmpegWrapper.DeleteEpisodeCache(e.EpisodeId); } } Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction); Plugin.Instance!.SaveTimestamps(AnalysisMode.Credits); return NoContent(); } /// /// Updates the ignore list for the provided season. /// /// New ignore list items. /// Save the ignore list. /// No content. [HttpPost("IgnoreList/UpdateSeason")] public ActionResult UpdateIgnoreListSeason([FromBody] IgnoreListItem ignoreListItem, bool save = true) { if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(ignoreListItem.SeasonId)) { return NotFound(); } if (ignoreListItem.IgnoreIntro || ignoreListItem.IgnoreCredits) { Plugin.Instance!.IgnoreList.AddOrUpdate(ignoreListItem.SeasonId, ignoreListItem, (_, _) => ignoreListItem); } else { Plugin.Instance!.IgnoreList.TryRemove(ignoreListItem.SeasonId, out _); } if (save) { Plugin.Instance!.SaveIgnoreList(); } return NoContent(); } /// /// Updates the ignore list for the provided series. /// /// Series name. /// New ignore list items. /// No content. [HttpPost("IgnoreList/UpdateSeries/{Series}")] public ActionResult UpdateIgnoreListSeries([FromRoute] string series, [FromBody] IgnoreListItem ignoreListItem) { if (!LookupSeasonIdsByName(series, out var seasonIds)) { return NotFound(); } foreach (var seasonId in seasonIds) { UpdateIgnoreListSeason(new IgnoreListItem(ignoreListItem) { SeasonId = seasonId }, false); } Plugin.Instance!.SaveIgnoreList(); return NoContent(); } /// /// Updates the introduction timestamps for the provided episode. /// /// Episode ID to update timestamps for. /// New introduction start and end times. /// New introduction timestamps saved. /// No content. [HttpPost("Episode/{Id}/UpdateIntroTimestamps")] [Obsolete("deprecated use Episode/{Id}/Timestamps")] public ActionResult UpdateIntroTimestamps([FromRoute] Guid id, [FromBody] Intro timestamps) { if (timestamps.IntroEnd > 0.0) { var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd); Plugin.Instance!.Intros[id] = new Segment(id, tr); Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction); } return NoContent(); } private static string GetSeasonName(QueuedEpisode episode) { return "Season " + episode.SeasonNumber.ToString(CultureInfo.InvariantCulture); } /// /// Lookup a named season of a series and return all queued episodes. /// /// Series name. /// Season name. /// Episodes. /// Boolean indicating if the requested season was found. private static bool LookupSeasonByName(string series, string season, out List episodes) { foreach (var queuedEpisodes in Plugin.Instance!.QueuedMediaItems) { var first = queuedEpisodes.Value[0]; var firstSeasonName = GetSeasonName(first); // Assert that the queued episode series and season are equal to what was requested if ( !string.Equals(first.SeriesName, series, StringComparison.OrdinalIgnoreCase) || !string.Equals(firstSeasonName, season, StringComparison.OrdinalIgnoreCase)) { continue; } episodes = queuedEpisodes.Value; return true; } episodes = []; return false; } /// /// Lookup a named season of a series and return its season id. /// /// Series name. /// Season name. /// Season id. /// Boolean indicating if the requested season was found. private bool LookupSeasonIdByName(string series, string season, out Guid seasonId) { foreach (var queuedEpisodes in Plugin.Instance!.QueuedMediaItems) { var first = queuedEpisodes.Value[0]; var firstSeasonName = GetSeasonName(first); // Assert that the queued episode series and season are equal to what was requested if ( !string.Equals(first.SeriesName, series, StringComparison.OrdinalIgnoreCase) || !string.Equals(firstSeasonName, season, StringComparison.OrdinalIgnoreCase)) { continue; } seasonId = queuedEpisodes.Key; return true; } seasonId = Guid.Empty; return false; } /// /// Lookup a named series and return all the season ids. /// /// Series name. /// Seasons. /// Boolean indicating if the requested series was found. private bool LookupSeasonIdsByName(string series, out List seasons) { seasons = new List(); foreach (var queuedEpisodes in Plugin.Instance!.QueuedMediaItems) { var first = queuedEpisodes.Value[0]; // Assert that the queued episode series is equal to what was requested if (!string.Equals(first.SeriesName, series, StringComparison.OrdinalIgnoreCase)) { continue; } seasons.Add(queuedEpisodes.Key); } return seasons.Count > 0; } }