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;
}
}