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.
///
///
/// Initializes a new instance of the class.
///
/// Logger.
[Authorize(Policy = Policies.RequiresElevation)]
[ApiController]
[Produces(MediaTypeNames.Application.Json)]
[Route("Intros")]
public class VisualizationController(ILogger logger) : ControllerBase
{
private readonly ILogger _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 IDs by series name");
var showSeasons = new Dictionary();
foreach (var kvp in Plugin.Instance!.QueuedMediaItems)
{
if (kvp.Value.FirstOrDefault() is QueuedEpisode first)
{
var seriesId = first.SeriesId;
var seasonId = kvp.Key;
var seasonNumber = first.SeasonNumber;
if (!showSeasons.TryGetValue(seriesId, out var showInfo))
{
showInfo = new ShowInfos { SeriesName = first.SeriesName, ProductionYear = GetProductionYear(seriesId), LibraryName = GetLibraryName(seriesId), Seasons = [] };
showSeasons[seriesId] = showInfo;
}
showInfo.Seasons[seasonId] = seasonNumber;
}
}
// Sort the dictionary by SeriesName and the seasons by SeasonName
var sortedShowSeasons = showSeasons
.OrderBy(kvp => kvp.Value.SeriesName)
.ToDictionary(
kvp => kvp.Key,
kvp => new ShowInfos
{
SeriesName = kvp.Value.SeriesName,
ProductionYear = kvp.Value.ProductionYear,
LibraryName = kvp.Value.LibraryName,
Seasons = kvp.Value.Seasons
.OrderBy(s => s.Value)
.ToDictionary(s => s.Key, s => s.Value)
});
return sortedShowSeasons;
}
///
/// Returns the ignore list for the provided season.
///
/// Season ID.
/// List of episode titles.
[HttpGet("IgnoreListSeason/{SeasonId}")]
public ActionResult GetIgnoreListSeason([FromRoute] Guid seasonId)
{
if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(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 ID.
/// List of episode titles.
[HttpGet("IgnoreListSeries/{SeriesId}")]
public ActionResult GetIgnoreListSeries([FromRoute] Guid seriesId)
{
var seasonIds = Plugin.Instance!.QueuedMediaItems
.Where(kvp => kvp.Value.Any(e => e.SeriesId == seriesId))
.Select(kvp => kvp.Key)
.ToList();
if (seasonIds.Count == 0)
{
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 ID.
/// Season ID.
/// List of episode titles.
[HttpGet("Show/{SeriesId}/{SeasonId}")]
public ActionResult> GetSeasonEpisodes([FromRoute] Guid seriesId, [FromRoute] Guid seasonId)
{
if (!Plugin.Instance!.QueuedMediaItems.TryGetValue(seasonId, out var episodes))
{
return NotFound();
}
if (!episodes.Any(e => e.SeriesId == seriesId))
{
return NotFound();
}
var showName = episodes.FirstOrDefault()?.SeriesName!;
return episodes.Select(e => new EpisodeVisualization(e.EpisodeId, e.Name)).ToList();
}
///
/// 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 ID.
/// Season ID.
/// Erase cache.
/// Season timestamps erased.
/// Unable to find season in provided series.
/// No content.
[HttpDelete("Show/{SeriesId}/{SeasonId}")]
public ActionResult EraseSeason([FromRoute] Guid seriesId, [FromRoute] Guid seasonId, [FromQuery] bool eraseCache = false)
{
var episodes = Plugin.Instance!.QueuedMediaItems
.Where(kvp => kvp.Key == seasonId)
.SelectMany(kvp => kvp.Value.Where(e => e.SeriesId == seriesId))
.ToList();
if (episodes.Count == 0)
{
return NotFound();
}
_logger.LogInformation("Erasing timestamps for series {SeriesId} season {SeasonId} at user request", seriesId, seasonId);
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 | 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 ID.
/// New ignore list items.
/// No content.
[HttpPost("IgnoreList/UpdateSeries/{SeriesId}")]
public ActionResult UpdateIgnoreListSeries([FromRoute] Guid seriesId, [FromBody] IgnoreListItem ignoreListItem)
{
var seasonIds = Plugin.Instance!.QueuedMediaItems
.Where(kvp => kvp.Value.Any(e => e.SeriesId == seriesId))
.Select(kvp => kvp.Key)
.ToList();
if (seasonIds.Count == 0)
{
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 GetProductionYear(Guid seriesId)
{
return seriesId == Guid.Empty
? "Unknown"
: Plugin.Instance?.GetItem(seriesId)?.ProductionYear?.ToString(CultureInfo.InvariantCulture) ?? "Unknown";
}
private static string GetLibraryName(Guid seriesId)
{
if (seriesId == Guid.Empty)
{
return "Unknown";
}
var collectionFolders = Plugin.Instance?.GetCollectionFolders(seriesId);
return collectionFolders?.Count > 0
? string.Join(", ", collectionFolders.Select(folder => folder.Name))
: "Unknown";
}
}