2024-10-25 14:31:50 -04:00
|
|
|
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only.
|
2024-10-25 14:15:12 -04:00
|
|
|
|
2022-05-01 00:33:22 -05:00
|
|
|
using System;
|
2022-06-13 01:52:41 -05:00
|
|
|
using System.Collections.Generic;
|
2024-10-30 16:57:23 +01:00
|
|
|
using System.Linq;
|
2022-05-01 00:33:22 -05:00
|
|
|
using System.Net.Mime;
|
2024-10-30 16:57:23 +01:00
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
2024-10-19 23:50:41 +02:00
|
|
|
using IntroSkipper.Configuration;
|
|
|
|
using IntroSkipper.Data;
|
2024-11-02 18:17:22 +01:00
|
|
|
using IntroSkipper.Db;
|
2024-10-30 16:57:23 +01:00
|
|
|
using IntroSkipper.Manager;
|
2024-05-13 23:50:51 +02:00
|
|
|
using MediaBrowser.Common.Api;
|
2024-10-18 14:15:09 +02:00
|
|
|
using MediaBrowser.Controller.Entities.Movies;
|
2022-07-29 03:34:55 -05:00
|
|
|
using MediaBrowser.Controller.Entities.TV;
|
2022-05-01 00:33:22 -05:00
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2024-11-02 18:17:22 +01:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
2022-05-01 00:33:22 -05:00
|
|
|
|
2024-10-19 23:50:41 +02:00
|
|
|
namespace IntroSkipper.Controllers;
|
2022-05-01 00:33:22 -05:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Skip intro controller.
|
|
|
|
/// </summary>
|
|
|
|
[Authorize]
|
|
|
|
[ApiController]
|
|
|
|
[Produces(MediaTypeNames.Application.Json)]
|
2024-10-30 16:57:23 +01:00
|
|
|
public class SkipIntroController(MediaSegmentUpdateManager mediaSegmentUpdateManager) : ControllerBase
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
2024-10-31 11:58:56 +01:00
|
|
|
private readonly MediaSegmentUpdateManager _mediaSegmentUpdateManager = mediaSegmentUpdateManager;
|
|
|
|
|
2022-05-01 00:33:22 -05:00
|
|
|
/// <summary>
|
2022-06-12 21:28:24 -05:00
|
|
|
/// Returns the timestamps of the introduction in a television episode. Responses are in API version 1 format.
|
2022-05-01 00:33:22 -05:00
|
|
|
/// </summary>
|
2022-05-01 01:24:57 -05:00
|
|
|
/// <param name="id">ID of the episode. Required.</param>
|
2022-11-25 00:40:02 -06:00
|
|
|
/// <param name="mode">Timestamps to return. Optional. Defaults to Introduction for backwards compatibility.</param>
|
2022-05-01 00:33:22 -05:00
|
|
|
/// <response code="200">Episode contains an intro.</response>
|
|
|
|
/// <response code="404">Failed to find an intro in the provided episode.</response>
|
2022-05-09 22:50:41 -05:00
|
|
|
/// <returns>Detected intro.</returns>
|
2022-05-01 00:33:22 -05:00
|
|
|
[HttpGet("Episode/{id}/IntroTimestamps")]
|
2022-06-12 21:28:24 -05:00
|
|
|
[HttpGet("Episode/{id}/IntroTimestamps/v1")]
|
2022-11-25 00:40:02 -06:00
|
|
|
public ActionResult<Intro> GetIntroTimestamps(
|
|
|
|
[FromRoute] Guid id,
|
2024-10-16 16:05:59 +02:00
|
|
|
[FromQuery] AnalysisMode mode = AnalysisMode.Introduction)
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
var intros = GetIntros(id);
|
|
|
|
if (!intros.TryGetValue(mode, out var intro))
|
2022-05-01 00:33:22 -05:00
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
return intro;
|
|
|
|
}
|
2022-06-07 12:18:03 -05:00
|
|
|
|
2024-07-27 21:11:01 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Updates the timestamps for the provided episode.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="id">Episode ID to update timestamps for.</param>
|
|
|
|
/// <param name="timestamps">New timestamps Introduction/Credits start and end times.</param>
|
2024-10-31 11:58:56 +01:00
|
|
|
/// <param name="cancellationToken">Cancellation Token.</param>
|
2024-07-27 21:11:01 +00:00
|
|
|
/// <response code="204">New timestamps saved.</response>
|
2024-07-28 19:37:46 +02:00
|
|
|
/// <response code="404">Given ID is not an Episode.</response>
|
2024-07-27 21:11:01 +00:00
|
|
|
/// <returns>No content.</returns>
|
|
|
|
[Authorize(Policy = Policies.RequiresElevation)]
|
|
|
|
[HttpPost("Episode/{Id}/Timestamps")]
|
2024-10-31 11:58:56 +01:00
|
|
|
public async Task<ActionResult> UpdateTimestampsAsync([FromRoute] Guid id, [FromBody] TimeStamps timestamps, CancellationToken cancellationToken = default)
|
2024-07-27 21:11:01 +00:00
|
|
|
{
|
2024-07-28 19:37:46 +02:00
|
|
|
// only update existing episodes
|
|
|
|
var rawItem = Plugin.Instance!.GetItem(id);
|
2024-11-02 18:17:22 +01:00
|
|
|
if (rawItem is not Episode and not Movie)
|
2024-07-28 19:37:46 +02:00
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2024-09-12 08:37:47 +00:00
|
|
|
if (timestamps?.Introduction.End > 0.0)
|
2024-07-27 21:11:01 +00:00
|
|
|
{
|
2024-11-06 10:30:00 +01:00
|
|
|
var seg = new Segment(id, new TimeRange(timestamps.Introduction.Start, timestamps.Introduction.End));
|
|
|
|
await Plugin.Instance!.UpdateTimestamps([seg], AnalysisMode.Introduction).ConfigureAwait(false);
|
2024-07-27 21:11:01 +00:00
|
|
|
}
|
|
|
|
|
2024-09-12 08:37:47 +00:00
|
|
|
if (timestamps?.Credits.End > 0.0)
|
2024-07-27 21:11:01 +00:00
|
|
|
{
|
2024-11-06 10:30:00 +01:00
|
|
|
var seg = new Segment(id, new TimeRange(timestamps.Credits.Start, timestamps.Credits.End));
|
|
|
|
await Plugin.Instance!.UpdateTimestamps([seg], AnalysisMode.Credits).ConfigureAwait(false);
|
2024-07-27 21:11:01 +00:00
|
|
|
}
|
|
|
|
|
2024-10-30 16:57:23 +01:00
|
|
|
if (Plugin.Instance.Configuration.UpdateMediaSegments)
|
|
|
|
{
|
2024-10-31 11:58:56 +01:00
|
|
|
var episode = Plugin.Instance!.QueuedMediaItems[rawItem is Episode e ? e.SeasonId : rawItem.Id]
|
|
|
|
.FirstOrDefault(q => q.EpisodeId == rawItem.Id);
|
2024-10-30 16:57:23 +01:00
|
|
|
|
|
|
|
if (episode is not null)
|
|
|
|
{
|
2024-10-31 11:58:56 +01:00
|
|
|
await _mediaSegmentUpdateManager.UpdateMediaSegmentsAsync([episode], cancellationToken).ConfigureAwait(false);
|
2024-10-30 16:57:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-27 21:11:01 +00:00
|
|
|
return NoContent();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the timestamps for the provided episode.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="id">Episode ID.</param>
|
2024-07-28 19:37:46 +02:00
|
|
|
/// <response code="200">Sucess.</response>
|
|
|
|
/// <response code="404">Given ID is not an Episode.</response>
|
2024-07-27 21:11:01 +00:00
|
|
|
/// <returns>Episode Timestamps.</returns>
|
|
|
|
[HttpGet("Episode/{Id}/Timestamps")]
|
|
|
|
[ActionName("UpdateTimestamps")]
|
|
|
|
public ActionResult<TimeStamps> GetTimestamps([FromRoute] Guid id)
|
|
|
|
{
|
2024-07-28 18:53:23 +02:00
|
|
|
// only get return content for episodes
|
|
|
|
var rawItem = Plugin.Instance!.GetItem(id);
|
2024-11-02 18:17:22 +01:00
|
|
|
if (rawItem is not Episode and not Movie)
|
2024-07-28 18:53:23 +02:00
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2024-07-27 21:11:01 +00:00
|
|
|
var times = new TimeStamps();
|
2024-11-02 18:17:22 +01:00
|
|
|
var segments = Plugin.Instance!.GetSegmentsById(id);
|
|
|
|
|
|
|
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var introSegment))
|
2024-07-27 21:11:01 +00:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
times.Introduction = introSegment;
|
2024-07-27 21:11:01 +00:00
|
|
|
}
|
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
if (segments.TryGetValue(AnalysisMode.Credits, out var creditSegment))
|
2024-07-27 21:11:01 +00:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
times.Credits = creditSegment;
|
2024-07-27 21:11:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return times;
|
|
|
|
}
|
|
|
|
|
2023-03-04 00:15:26 -06:00
|
|
|
/// <summary>
|
|
|
|
/// Gets a dictionary of all skippable segments.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="id">Media ID.</param>
|
|
|
|
/// <response code="200">Skippable segments dictionary.</response>
|
|
|
|
/// <returns>Dictionary of skippable segments.</returns>
|
|
|
|
[HttpGet("Episode/{id}/IntroSkipperSegments")]
|
2024-10-16 16:05:59 +02:00
|
|
|
public ActionResult<Dictionary<AnalysisMode, Intro>> GetSkippableSegments([FromRoute] Guid id)
|
2023-03-04 00:15:26 -06:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
var segments = GetIntros(id);
|
2023-03-04 00:15:26 -06:00
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var introSegment))
|
2023-03-04 00:15:26 -06:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
segments[AnalysisMode.Introduction] = introSegment;
|
2023-03-04 00:15:26 -06:00
|
|
|
}
|
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
if (segments.TryGetValue(AnalysisMode.Introduction, out var creditSegment))
|
2023-03-04 00:15:26 -06:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
segments[AnalysisMode.Credits] = creditSegment;
|
2023-03-04 00:15:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return segments;
|
|
|
|
}
|
|
|
|
|
2022-11-25 00:40:02 -06:00
|
|
|
/// <summary>Lookup and return the skippable timestamps for the provided item.</summary>
|
2022-06-12 21:28:24 -05:00
|
|
|
/// <param name="id">Unique identifier of this episode.</param>
|
|
|
|
/// <returns>Intro object if the provided item has an intro, null otherwise.</returns>
|
2024-11-02 18:17:22 +01:00
|
|
|
private static Dictionary<AnalysisMode, Intro> GetIntros(Guid id)
|
2022-06-12 21:28:24 -05:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
var timestamps = Plugin.Instance!.GetSegmentsById(id);
|
|
|
|
var intros = new Dictionary<AnalysisMode, Intro>();
|
|
|
|
|
|
|
|
foreach (var (mode, timestamp) in timestamps)
|
2022-11-25 00:40:02 -06:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
if (!timestamp.Valid)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
2022-11-25 00:40:02 -06:00
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
// Create new Intro to avoid mutating the original stored in dictionary
|
2023-03-04 00:15:26 -06:00
|
|
|
var segment = new Intro(timestamp);
|
2024-11-02 18:17:22 +01:00
|
|
|
var config = Plugin.Instance.Configuration;
|
2023-03-04 00:15:26 -06:00
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
// Calculate intro end time based on mode
|
2024-10-23 15:10:37 +02:00
|
|
|
segment.IntroEnd = mode == AnalysisMode.Credits
|
|
|
|
? GetAdjustedIntroEnd(id, segment.IntroEnd, config)
|
|
|
|
: segment.IntroEnd - config.RemainingSecondsOfIntro;
|
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
// Set skip button prompt visibility times
|
|
|
|
const double MIN_REMAINING_TIME = 3.0; // Minimum seconds before end to hide prompt
|
2024-03-01 09:19:12 -05:00
|
|
|
if (config.PersistSkipButton)
|
2024-03-01 18:29:37 +01:00
|
|
|
{
|
2024-04-20 12:29:40 +02:00
|
|
|
segment.ShowSkipPromptAt = segment.IntroStart;
|
2024-11-02 18:17:22 +01:00
|
|
|
segment.HideSkipPromptAt = segment.IntroEnd - MIN_REMAINING_TIME;
|
2024-03-01 18:29:37 +01:00
|
|
|
}
|
|
|
|
else
|
2024-03-01 09:19:12 -05:00
|
|
|
{
|
2024-04-20 12:29:40 +02:00
|
|
|
segment.ShowSkipPromptAt = Math.Max(0, segment.IntroStart - config.ShowPromptAdjustment);
|
|
|
|
segment.HideSkipPromptAt = Math.Min(
|
|
|
|
segment.IntroStart + config.HidePromptAdjustment,
|
2024-11-02 18:17:22 +01:00
|
|
|
segment.IntroEnd - MIN_REMAINING_TIME);
|
2024-03-01 09:19:12 -05:00
|
|
|
}
|
|
|
|
|
2024-11-02 18:17:22 +01:00
|
|
|
intros[mode] = segment;
|
2022-11-25 00:40:02 -06:00
|
|
|
}
|
2024-11-02 18:17:22 +01:00
|
|
|
|
|
|
|
return intros;
|
2022-06-12 21:28:24 -05:00
|
|
|
}
|
|
|
|
|
2024-10-23 15:10:37 +02:00
|
|
|
private static double GetAdjustedIntroEnd(Guid id, double segmentEnd, PluginConfiguration config)
|
|
|
|
{
|
|
|
|
var runTime = TimeSpan.FromTicks(Plugin.Instance!.GetItem(id)?.RunTimeTicks ?? 0).TotalSeconds;
|
|
|
|
return runTime > 0 && runTime < segmentEnd + 1
|
|
|
|
? runTime
|
|
|
|
: segmentEnd - config.RemainingSecondsOfIntro;
|
|
|
|
}
|
|
|
|
|
2022-06-07 12:18:03 -05:00
|
|
|
/// <summary>
|
|
|
|
/// Erases all previously discovered introduction timestamps.
|
|
|
|
/// </summary>
|
2022-11-29 02:31:24 -06:00
|
|
|
/// <param name="mode">Mode.</param>
|
2024-06-07 17:09:48 +02:00
|
|
|
/// <param name="eraseCache">Erase cache.</param>
|
2022-06-07 12:18:03 -05:00
|
|
|
/// <response code="204">Operation successful.</response>
|
|
|
|
/// <returns>No content.</returns>
|
2024-05-13 23:50:51 +02:00
|
|
|
[Authorize(Policy = Policies.RequiresElevation)]
|
2022-06-07 12:18:03 -05:00
|
|
|
[HttpPost("Intros/EraseTimestamps")]
|
2024-11-02 18:17:22 +01:00
|
|
|
public async Task<ActionResult> ResetIntroTimestamps([FromQuery] AnalysisMode mode, [FromQuery] bool eraseCache = false)
|
2022-06-07 12:18:03 -05:00
|
|
|
{
|
2024-11-02 18:17:22 +01:00
|
|
|
using var db = new IntroSkipperDbContext(Plugin.Instance!.DbPath);
|
|
|
|
var segments = await db.DbSegment
|
|
|
|
.Where(s => s.Type == mode)
|
|
|
|
.ToListAsync()
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
|
|
db.DbSegment.RemoveRange(segments);
|
|
|
|
await db.SaveChangesAsync().ConfigureAwait(false);
|
2022-11-29 02:31:24 -06:00
|
|
|
|
2024-06-07 17:09:48 +02:00
|
|
|
if (eraseCache)
|
|
|
|
{
|
|
|
|
FFmpegWrapper.DeleteCacheFiles(mode);
|
|
|
|
}
|
|
|
|
|
2022-06-07 12:18:03 -05:00
|
|
|
return NoContent();
|
|
|
|
}
|
2022-06-13 01:52:41 -05:00
|
|
|
|
2022-11-06 21:20:52 -06:00
|
|
|
/// <summary>
|
|
|
|
/// Gets the user interface configuration.
|
|
|
|
/// </summary>
|
|
|
|
/// <response code="200">UserInterfaceConfiguration returned.</response>
|
|
|
|
/// <returns>UserInterfaceConfiguration.</returns>
|
2024-03-10 19:47:38 +01:00
|
|
|
[HttpGet]
|
2022-11-06 21:20:52 -06:00
|
|
|
[Route("Intros/UserInterfaceConfiguration")]
|
|
|
|
public ActionResult<UserInterfaceConfiguration> GetUserInterfaceConfiguration()
|
|
|
|
{
|
|
|
|
var config = Plugin.Instance!.Configuration;
|
2023-03-04 00:15:26 -06:00
|
|
|
return new UserInterfaceConfiguration(
|
2024-10-27 05:08:22 -04:00
|
|
|
config.SkipButtonEnabled,
|
2023-03-04 00:15:26 -06:00
|
|
|
config.SkipButtonIntroText,
|
2024-10-11 10:11:22 -04:00
|
|
|
config.SkipButtonEndCreditsText,
|
|
|
|
config.AutoSkip,
|
|
|
|
config.AutoSkipCredits,
|
|
|
|
config.ClientList);
|
2022-11-06 21:20:52 -06:00
|
|
|
}
|
2022-05-01 00:33:22 -05:00
|
|
|
}
|