theMasterpc d867ede882
add ignore list (#281)
* add block list

* better block list system.
todo:
-change the name blocklist.
-fixing small bugs.
-maybe moving from Dictionary to List<BlockListItem>.

* - moving from ConcurrentDictionary to List<BlackListItem>, for better xml file.
- changing block to black.
- small fixes.

todo:
- maybe changing the blacklist naming.

* moving to ignorelist.
moving the blacklisting to Manage Fingerprints.
changing the object BlackListItem.

todo:
- moving to the naming "ignorelist", instead of "blacklist".
- adding "save for series" button.
- improving the ui of the blacklist section".
- fixing some more bugs.
- changing the "Manage Fingerprints" to "Manage Timestamps & Fingerprints".

* adding the option to apply ignorelist changes into a series.
moving to ignorelist naming.
changing "Manage Fingerprints" to "Manage Timestamps & Fingerprints".
improving the ui of the ignorelist editor

* small fixes

* fix some bugs. improving the ignore feature

* fix some stuff

* Refactor CSS styles for ignore list checkboxes

* small fixes

* small changes

* small changes

* big changes

* small fixes

* Refactor IgnoreListItem to use SeasonId instead of Id

* Refactor IgnoreListItem to use SeasonId instead of Id.

changes to the ExecuteAsync function and to its documentation
2024-09-20 07:18:04 -04:00

368 lines
13 KiB
C#

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;
/// <summary>
/// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis.
/// </summary>
[Authorize(Policy = Policies.RequiresElevation)]
[ApiController]
[Produces(MediaTypeNames.Application.Json)]
[Route("Intros")]
public class VisualizationController : ControllerBase
{
private readonly ILogger<VisualizationController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="VisualizationController"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
public VisualizationController(ILogger<VisualizationController> logger)
{
_logger = logger;
}
/// <summary>
/// Returns all show names and seasons.
/// </summary>
/// <returns>Dictionary of show names to a list of season names.</returns>
[HttpGet("Shows")]
public ActionResult<Dictionary<string, HashSet<string>>> GetShowSeasons()
{
_logger.LogDebug("Returning season names by series");
var showSeasons = new Dictionary<string, HashSet<string>>();
// 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<string>());
showSeasons[series].Add(season);
}
return showSeasons;
}
/// <summary>
/// Returns the ignore list for the provided season.
/// </summary>
/// <param name="series">Show name.</param>
/// <param name="season">Season name.</param>
/// <returns>List of episode titles.</returns>
[HttpGet("IgnoreList/{Series}/{Season}")]
public ActionResult<IgnoreListItem> 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]);
}
/// <summary>
/// Returns the ignore list for the provided series.
/// </summary>
/// <param name="series">Show name.</param>
/// <returns>List of episode titles.</returns>
[HttpGet("IgnoreList/{Series}")]
public ActionResult<IgnoreListItem> 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))
};
}
/// <summary>
/// Returns the names and unique identifiers of all episodes in the provided season.
/// </summary>
/// <param name="series">Show name.</param>
/// <param name="season">Season name.</param>
/// <returns>List of episode titles.</returns>
[HttpGet("Show/{Series}/{Season}")]
public ActionResult<List<EpisodeVisualization>> GetSeasonEpisodes([FromRoute] string series, [FromRoute] string season)
{
var visualEpisodes = new List<EpisodeVisualization>();
if (!LookupSeasonByName(series, season, out var episodes))
{
return NotFound();
}
foreach (var e in episodes)
{
visualEpisodes.Add(new EpisodeVisualization(e.EpisodeId, e.Name));
}
return visualEpisodes;
}
/// <summary>
/// Fingerprint the provided episode and returns the uncompressed fingerprint data points.
/// </summary>
/// <param name="id">Episode id.</param>
/// <returns>Read only collection of fingerprint points.</returns>
[HttpGet("Episode/{Id}/Chromaprint")]
public ActionResult<uint[]> 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();
}
/// <summary>
/// Erases all timestamps for the provided season.
/// </summary>
/// <param name="series">Show name.</param>
/// <param name="season">Season name.</param>
/// <param name="eraseCache">Erase cache.</param>
/// <response code="204">Season timestamps erased.</response>
/// <response code="404">Unable to find season in provided series.</response>
/// <returns>No content.</returns>
[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();
}
/// <summary>
/// Updates the ignore list for the provided season.
/// </summary>
/// <param name="ignoreListItem">New ignore list items.</param>
/// <param name="save">Save the ignore list.</param>
/// <returns>No content.</returns>
[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();
}
/// <summary>
/// Updates the ignore list for the provided series.
/// </summary>
/// <param name="series">Series name.</param>
/// <param name="ignoreListItem">New ignore list items.</param>
/// <returns>No content.</returns>
[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();
}
/// <summary>
/// Updates the introduction timestamps for the provided episode.
/// </summary>
/// <param name="id">Episode ID to update timestamps for.</param>
/// <param name="timestamps">New introduction start and end times.</param>
/// <response code="204">New introduction timestamps saved.</response>
/// <returns>No content.</returns>
[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);
}
/// <summary>
/// Lookup a named season of a series and return all queued episodes.
/// </summary>
/// <param name="series">Series name.</param>
/// <param name="season">Season name.</param>
/// <param name="episodes">Episodes.</param>
/// <returns>Boolean indicating if the requested season was found.</returns>
private static bool LookupSeasonByName(string series, string season, out List<QueuedEpisode> 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;
}
/// <summary>
/// Lookup a named season of a series and return its season id.
/// </summary>
/// <param name="series">Series name.</param>
/// <param name="season">Season name.</param>
/// <param name="seasonId">Season id.</param>
/// <returns>Boolean indicating if the requested season was found.</returns>
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;
}
/// <summary>
/// Lookup a named series and return all the season ids.
/// </summary>
/// <param name="series">Series name.</param>
/// <param name="seasons">Seasons.</param>
/// <returns>Boolean indicating if the requested series was found.</returns>
private bool LookupSeasonIdsByName(string series, out List<Guid> seasons)
{
seasons = new List<Guid>();
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;
}
}