322 lines
11 KiB
C#
322 lines
11 KiB
C#
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
|
// SPDX-License-Identifier: GPL-3.0-only.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net.Mime;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using IntroSkipper.Data;
|
|
using IntroSkipper.Manager;
|
|
using MediaBrowser.Common.Api;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace IntroSkipper.Controllers;
|
|
|
|
/// <summary>
|
|
/// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Initializes a new instance of the <see cref="VisualizationController"/> class.
|
|
/// </remarks>
|
|
/// <param name="logger">Logger.</param>
|
|
/// <param name="mediaSegmentUpdateManager">Media Segment Update Manager.</param>
|
|
[Authorize(Policy = Policies.RequiresElevation)]
|
|
[ApiController]
|
|
[Produces(MediaTypeNames.Application.Json)]
|
|
[Route("Intros")]
|
|
public class VisualizationController(ILogger<VisualizationController> logger, MediaSegmentUpdateManager mediaSegmentUpdateManager) : ControllerBase
|
|
{
|
|
/// <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<Guid, ShowInfos>> GetShowSeasons()
|
|
{
|
|
logger.LogDebug("Returning season IDs by series name");
|
|
|
|
var showSeasons = new Dictionary<Guid, ShowInfos>();
|
|
|
|
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), IsMovie = first.IsMovie, 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,
|
|
IsMovie = kvp.Value.IsMovie,
|
|
Seasons = kvp.Value.Seasons
|
|
.OrderBy(s => s.Value)
|
|
.ToDictionary(s => s.Key, s => s.Value)
|
|
});
|
|
|
|
return sortedShowSeasons;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the ignore list for the provided season.
|
|
/// </summary>
|
|
/// <param name="seasonId">Season ID.</param>
|
|
/// <returns>List of episode titles.</returns>
|
|
[HttpGet("IgnoreListSeason/{SeasonId}")]
|
|
public ActionResult<IgnoreListItem> 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]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the ignore list for the provided series.
|
|
/// </summary>
|
|
/// <param name="seriesId">Show ID.</param>
|
|
/// <returns>List of episode titles.</returns>
|
|
[HttpGet("IgnoreListSeries/{SeriesId}")]
|
|
public ActionResult<IgnoreListItem> 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))
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the names and unique identifiers of all episodes in the provided season.
|
|
/// </summary>
|
|
/// <param name="seriesId">Show ID.</param>
|
|
/// <param name="seasonId">Season ID.</param>
|
|
/// <returns>List of episode titles.</returns>
|
|
[HttpGet("Show/{SeriesId}/{SeasonId}")]
|
|
public ActionResult<List<EpisodeVisualization>> 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();
|
|
}
|
|
|
|
/// <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="seriesId">Show ID.</param>
|
|
/// <param name="seasonId">Season ID.</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/{SeriesId}/{SeasonId}")]
|
|
public async Task<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);
|
|
|
|
if (Plugin.Instance.Configuration.UpdateMediaSegments)
|
|
{
|
|
using var ct = new CancellationTokenSource();
|
|
await mediaSegmentUpdateManager.UpdateMediaSegmentsAsync(episodes, ct.Token).ConfigureAwait(false);
|
|
}
|
|
|
|
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="seriesId">Series ID.</param>
|
|
/// <param name="ignoreListItem">New ignore list items.</param>
|
|
/// <returns>No content.</returns>
|
|
[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();
|
|
}
|
|
|
|
/// <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 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";
|
|
}
|
|
}
|