// Copyright (C) 2024 Intro-Skipper contributors // 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.Db; using IntroSkipper.Manager; using MediaBrowser.Common.Api; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace IntroSkipper.Controllers; /// /// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis. /// /// /// Initializes a new instance of the class. /// /// Logger. /// Media Segment Update Manager. [Authorize(Policy = Policies.RequiresElevation)] [ApiController] [Produces(MediaTypeNames.Application.Json)] [Route("Intros")] public class VisualizationController(ILogger logger, MediaSegmentUpdateManager mediaSegmentUpdateManager) : ControllerBase { private readonly ILogger _logger = logger; private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager; /// /// 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), 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; } /// /// Returns the analyzer actions for the provided season. /// /// Season ID. /// List of episode titles. [HttpGet("AnalyzerActions/{SeasonId}")] public ActionResult> GetAnalyzerAction([FromRoute] Guid seasonId) { if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(seasonId)) { return NotFound(); } var analyzerActions = new Dictionary(); foreach (var mode in Enum.GetValues()) { analyzerActions[mode] = Plugin.Instance!.GetAnalyzerAction(seasonId, mode); } return Ok(analyzerActions); } /// /// 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. /// Cancellation Token. /// Season timestamps erased. /// Unable to find season in provided series. /// No content. [HttpDelete("Show/{SeriesId}/{SeasonId}")] public async Task EraseSeasonAsync([FromRoute] Guid seriesId, [FromRoute] Guid seasonId, [FromQuery] bool eraseCache = false, CancellationToken cancellationToken = default) { var episodes = Plugin.Instance!.QueuedMediaItems[seasonId]; if (episodes.Count == 0) { return NotFound(); } _logger.LogInformation("Erasing timestamps for series {SeriesId} season {SeasonId} at user request", seriesId, seasonId); try { using var db = new IntroSkipperDbContext(Plugin.Instance!.DbPath); foreach (var episode in episodes) { cancellationToken.ThrowIfCancellationRequested(); var existingSegments = db.DbSegment.Where(s => s.ItemId == episode.EpisodeId); db.DbSegment.RemoveRange(existingSegments); if (eraseCache) { await Task.Run(() => FFmpegWrapper.DeleteEpisodeCache(episode.EpisodeId), cancellationToken).ConfigureAwait(false); } } var seasonInfo = db.DbSeasonInfo.Where(s => s.SeasonId == seasonId); foreach (var info in seasonInfo) { db.Entry(info).Property(s => s.EpisodeIds).CurrentValue = []; } await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); if (Plugin.Instance.Configuration.UpdateMediaSegments) { await _mediaSegmentUpdateManager.UpdateMediaSegmentsAsync(episodes, cancellationToken).ConfigureAwait(false); } return NoContent(); } catch (Exception ex) { return StatusCode(500, ex.Message); } } /// /// Updates the analyzer actions for the provided season. /// /// Update analyzer actions request. /// No content. [HttpPost("AnalyzerActions/UpdateSeason")] public async Task UpdateAnalyzerActions([FromBody] UpdateAnalyzerActionsRequest request) { await Plugin.Instance!.SetAnalyzerActionAsync(request.Id, request.AnalyzerActions).ConfigureAwait(false); 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"; } }