add option to edit intros/credits (#223)

new API Endpoint: ´Episode/{Id}/Timestamps´

HTTP POST to update Timestamps.
HTTP GET to receive the unmodified Timestamps. If Intro/Outro not exists the API returns 0
This commit is contained in:
Kilian von Pflugk 2024-07-27 21:11:01 +00:00 committed by GitHub
parent d7ce2bdb6a
commit a7db712acf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 181 additions and 50 deletions

View File

@ -503,32 +503,54 @@
<summary>Manage Fingerprints</summary> <summary>Manage Fingerprints</summary>
<br /> <br />
<h3 style="margin:0">Select episodes to manage</h3> <label class="inputLabel" for="troubleshooterShow">Select TV Series to manage</label>
<br /> <select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
<select id="troubleshooterShow"></select> <label class="inputLabel" for="troubleshooterSeason">Select Season to manage</label>
<select id="troubleshooterSeason"></select> <select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
<br /> <br />
<select id="troubleshooterEpisode1"></select> <label class="inputLabel" for="troubleshooterEpisode1">Select the first episode</label>
<select id="troubleshooterEpisode2"></select> <select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
<br /> <label class="inputLabel" for="troubleshooterEpisode1">Select the seconds epsisode</label>
<select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
<br /> <br />
<div id="timestampEditor" style="display:none"> <div id="timestampEditor" style="display:none">
<h3 style="margin:0">Introduction timestamp editor</h3> <h3 style="margin:0">Introduction timestamp editor</h3>
<p style="margin:0">All times are in seconds.</p> <p style="margin:0">All times are displayed in the format (HH:MM:SS)</p>
<p id="editLeftEpisodeTitle" style="margin-bottom:0"></p>
<input style="width:4em" type="number" min="0" id="editLeftEpisodeStart"> to
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editLeftEpisodeEnd">
<p id="editRightEpisodeTitle" style="margin-top:0;margin-bottom:0"></p>
<input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
<input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
<br /> <br />
<table class="detailTable">
<tr>
<th scope="col" class="detailTableHeaderCell">Episode</th>
<th scope="col" class="detailTableHeaderCell">Section</th>
<th scope="col" class="detailTableHeaderCell">Start Time</th>
<th scope="col" class="detailTableHeaderCell">End Time</th>
</tr>
<tr>
<td rowspan="2" id="editLeftEpisodeTitle"></td>
<td>Intro</td>
<td><input type="time" step="1" id="editLeftIntroEpisodeStart"></td>
<td><input type="time" step="1" id="editLeftIntroEpisodeEnd"></td>
</tr>
<tr>
<td>Credits</td>
<td><input type="time" step="1" id="editLeftCreditEpisodeStart"></td>
<td><input type="time" step="1" id="editLeftCreditEpisodeEnd"></td>
</tr>
<tr>
<td rowspan="2" id="editRightEpisodeTitle"></td>
<td>Intro</td>
<td><input type="time" step="1" id="editRightIntroEpisodeStart"></td>
<td><input type="time" step="1" id="editRightIntroEpisodeEnd"></td>
</tr>
<tr>
<td>Credits</td>
<td><input type="time" step="1" id="editRightCreditEpisodeStart"></td>
<td><input type="time" step="1" id="editRightCreditEpisodeEnd"></td>
</tr>
</table>
<br /> <br />
<button is="emby-select" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">
<button id="btnUpdateTimestamps" type="button">
Update timestamps Update timestamps
</button> </button>
<br /> <br />
@ -548,8 +570,10 @@
</p> </p>
<table> <table>
<thead> <thead>
<td style="min-width: 100px; font-weight: bold">Key</td> <tr>
<td style="font-weight: bold">Function</td> <td style="min-width: 100px; font-weight: bold">Key</td>
<td style="font-weight: bold">Function</td>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
@ -882,27 +906,24 @@
// Get the title and ID of the left and right episodes // Get the title and ID of the left and right episodes
const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex]; const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex];
const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex]; const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex];
// Try to get the timestamps of each intro, falling back a default value of zero if no intro was found // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
let leftEpisodeIntro = await getJson("Episode/" + leftEpisode.value + "/IntroTimestamps/v1"); const leftEpisodeJson = await getJson("Episode/" + leftEpisode.value + "/Timestamps");
if (leftEpisodeIntro === null) { const rightEpisodeJson = await getJson("Episode/" + rightEpisode.value + "/Timestamps");
leftEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
}
let rightEpisodeIntro = await getJson("Episode/" + rightEpisode.value + "/IntroTimestamps/v1");
if (rightEpisodeIntro === null) {
rightEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
}
// Update the editor for the first and second episodes // Update the editor for the first and second episodes
timestampEditor.style.display = "unset"; timestampEditor.style.display = "unset";
document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text; document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart); document.querySelector("#editLeftIntroEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Introduction.IntroStart));
document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd); document.querySelector("#editLeftIntroEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Introduction.IntroEnd));
document.querySelector("#editLeftCreditEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Credits.IntroStart));
document.querySelector("#editLeftCreditEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Credits.IntroEnd));
document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text; document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
document.querySelector("#editRightEpisodeStart").value = Math.round(rightEpisodeIntro.IntroStart); document.querySelector("#editRightIntroEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Introduction.IntroStart));
document.querySelector("#editRightEpisodeEnd").value = Math.round(rightEpisodeIntro.IntroEnd); document.querySelector("#editRightIntroEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Introduction.IntroEnd));
document.querySelector("#editRightCreditEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Credits.IntroStart));
document.querySelector("#editRightCreditEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Credits.IntroEnd));
} }
// adds an item to a dropdown // adds an item to a dropdown
@ -1028,7 +1049,7 @@
// converts seconds to a readable timestamp (i.e. 127 becomes "02:07"). // converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
function secondsToString(seconds) { function secondsToString(seconds) {
return new Date(seconds * 1000).toISOString().substr(14, 5); return new Date(seconds * 1000).toISOString().slice(14, 19);
} }
// erase all intro/credits timestamps // erase all intro/credits timestamps
@ -1134,19 +1155,30 @@
}); });
btnUpdateTimestamps.addEventListener("click", () => { btnUpdateTimestamps.addEventListener("click", () => {
const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value; const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value;
const newLhsIntro = { const newLhs = {
IntroStart: document.querySelector("#editLeftEpisodeStart").value, Introduction: {
IntroEnd: document.querySelector("#editLeftEpisodeEnd").value, IntroStart: getTimeInSeconds(document.getElementById('editLeftIntroEpisodeStart').value),
IntroEnd: getTimeInSeconds(document.getElementById('editLeftIntroEpisodeEnd').value)
},
Credits: {
IntroStart: getTimeInSeconds(document.getElementById('editLeftCreditEpisodeStart').value),
IntroEnd: getTimeInSeconds(document.getElementById('editLeftCreditEpisodeEnd').value)
}
}; };
const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value; const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
const newRhsIntro = { const newRhs = {
IntroStart: document.querySelector("#editRightEpisodeStart").value, Introduction: {
IntroEnd: document.querySelector("#editRightEpisodeEnd").value, IntroStart: getTimeInSeconds(document.getElementById('editRightIntroEpisodeStart').value),
IntroEnd: getTimeInSeconds(document.getElementById('editRightIntroEpisodeEnd').value)
},
Credits: {
IntroStart: getTimeInSeconds(document.getElementById('editRightCreditEpisodeStart').value),
IntroEnd: getTimeInSeconds(document.getElementById('editRightCreditEpisodeEnd').value)
}
}; };
fetchWithAuth("Episode/" + lhsId + "/Timestamps", "POST", JSON.stringify(newLhs));
fetchWithAuth("Intros/Episode/" + lhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newLhsIntro)); fetchWithAuth("Episode/" + rhsId + "/Timestamps", "POST", JSON.stringify(newRhs));
fetchWithAuth("Intros/Episode/" + rhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newRhsIntro));
Dashboard.alert("New introduction timestamps saved"); Dashboard.alert("New introduction timestamps saved");
}); });
@ -1190,6 +1222,27 @@
timeContainer.style.left = "25px"; timeContainer.style.left = "25px";
timeContainer.style.top = (-1 * rect.height + y).toString() + "px"; timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
}); });
function setTime(seconds) {
// Calculate hours, minutes, and remaining seconds
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds % 3600) / 60);
let remainingSeconds = seconds % 60;
// Format as HH:MM:SS
let formattedTime =
String(hours).padStart(2, '0') + ':' +
String(minutes).padStart(2, '0') + ':' +
String(remainingSeconds).padStart(2, '0');
// Set the value of the time input
return formattedTime;
}
function getTimeInSeconds(time) {
let [hours, minutes, seconds] = time.split(':').map(Number);
return (hours * 3600) + (minutes * 60) + seconds;
}
</script> </script>
</div> </div>
</body> </body>

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Mime; using System.Net.Mime;
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration; using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -48,6 +49,59 @@ public class SkipIntroController : ControllerBase
return intro; return intro;
} }
/// <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>
/// <response code="204">New timestamps saved.</response>
/// <returns>No content.</returns>
[Authorize(Policy = Policies.RequiresElevation)]
[HttpPost("Episode/{Id}/Timestamps")]
public ActionResult UpdateTimestamps([FromRoute] Guid id, [FromBody] TimeStamps timestamps)
{
if (timestamps?.Introduction.IntroEnd > 0.0)
{
var tr = new TimeRange(timestamps.Introduction.IntroStart, timestamps.Introduction.IntroEnd);
Plugin.Instance!.Intros[id] = new Intro(id, tr);
}
if (timestamps?.Credits.IntroEnd > 0.0)
{
var cr = new TimeRange(timestamps.Credits.IntroStart, timestamps.Credits.IntroEnd);
Plugin.Instance!.Credits[id] = new Intro(id, cr);
}
Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction);
Plugin.Instance!.SaveTimestamps(AnalysisMode.Credits);
return NoContent();
}
/// <summary>
/// Gets the timestamps for the provided episode.
/// </summary>
/// <param name="id">Episode ID.</param>
/// <response code="204">Sucess.</response>
/// <returns>Episode Timestamps.</returns>
[HttpGet("Episode/{Id}/Timestamps")]
[ActionName("UpdateTimestamps")]
public ActionResult<TimeStamps> GetTimestamps([FromRoute] Guid id)
{
var times = new TimeStamps();
if (Plugin.Instance!.Intros.TryGetValue(id, out var introValue))
{
times.Introduction = introValue;
}
if (Plugin.Instance!.Credits.TryGetValue(id, out var creditValue))
{
times.Credits = creditValue;
}
return times;
}
/// <summary> /// <summary>
/// Gets a dictionary of all skippable segments. /// Gets a dictionary of all skippable segments.
/// </summary> /// </summary>

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Net.Mime; using System.Net.Mime;
using ConfusedPolarBear.Plugin.IntroSkipper.Data;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -157,18 +158,22 @@ public class VisualizationController : ControllerBase
} }
/// <summary> /// <summary>
/// Updates the timestamps for the provided episode. /// Updates the introduction timestamps for the provided episode.
/// </summary> /// </summary>
/// <param name="id">Episode ID to update timestamps for.</param> /// <param name="id">Episode ID to update timestamps for.</param>
/// <param name="timestamps">New introduction start and end times.</param> /// <param name="timestamps">New introduction start and end times.</param>
/// <response code="204">New introduction timestamps saved.</response> /// <response code="204">New introduction timestamps saved.</response>
/// <returns>No content.</returns> /// <returns>No content.</returns>
[HttpPost("Episode/{Id}/UpdateIntroTimestamps")] [HttpPost("Episode/{Id}/UpdateIntroTimestamps")]
public ActionResult UpdateTimestamps([FromRoute] Guid id, [FromBody] Intro timestamps) [Obsolete("deprecated use Episode/{Id}/Timestamps")]
public ActionResult UpdateIntroTimestamps([FromRoute] Guid id, [FromBody] Intro timestamps)
{ {
var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd); if (timestamps.IntroEnd > 0.0)
Plugin.Instance!.Intros[id] = new Intro(id, tr); {
Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction); var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd);
Plugin.Instance!.Intros[id] = new Intro(id, tr);
Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction);
}
return NoContent(); return NoContent();
} }

View File

@ -0,0 +1,19 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data
{
/// <summary>
/// Result of fingerprinting and analyzing two episodes in a season.
/// All times are measured in seconds relative to the beginning of the media file.
/// </summary>
public class TimeStamps
{
/// <summary>
/// Gets or sets Introduction.
/// </summary>
public Intro Introduction { get; set; } = new Intro();
/// <summary>
/// Gets or sets Credits.
/// </summary>
public Intro Credits { get; set; } = new Intro();
}
}