migrate Intro to Segment and make it generic (#268)

This commit is contained in:
Kilian von Pflugk 2024-09-12 08:37:47 +00:00 committed by GitHub
parent 92d2a55c81
commit 2438ba79f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 246 additions and 235 deletions

View File

@ -105,13 +105,13 @@ public class TestAudioFingerprinting
rhsFingerprint); rhsFingerprint);
Assert.True(lhs.Valid); Assert.True(lhs.Valid);
Assert.Equal(0, lhs.IntroStart); Assert.Equal(0, lhs.Start);
Assert.Equal(17.208, lhs.IntroEnd, 3); Assert.Equal(17.208, lhs.End, 3);
Assert.True(rhs.Valid); Assert.True(rhs.Valid);
// because we changed for 0.128 to 0.1238 its 4,952 now but that's too early (<= 5) // because we changed for 0.128 to 0.1238 its 4,952 now but that's too early (<= 5)
Assert.Equal(0, rhs.IntroStart); Assert.Equal(0, rhs.Start);
Assert.Equal(22.1602, rhs.IntroEnd); Assert.Equal(22.1602, rhs.End);
} }
/// <summary> /// <summary>

View File

@ -42,7 +42,7 @@ public class TestBlackFrames
var result = analyzer.AnalyzeMediaFile(episode, 240, 30, 85); var result = analyzer.AnalyzeMediaFile(episode, 240, 30, 85);
Assert.NotNull(result); Assert.NotNull(result);
Assert.InRange(result.IntroStart, 300 - range, 300 + range); Assert.InRange(result.Start, 300 - range, 300 + range);
} }
private QueuedEpisode queueFile(string path) private QueuedEpisode queueFile(string path)

View File

@ -23,8 +23,8 @@ public class TestChapterAnalyzer
var introChapter = FindChapter(chapters, AnalysisMode.Introduction); var introChapter = FindChapter(chapters, AnalysisMode.Introduction);
Assert.NotNull(introChapter); Assert.NotNull(introChapter);
Assert.Equal(60, introChapter.IntroStart); Assert.Equal(60, introChapter.Start);
Assert.Equal(90, introChapter.IntroEnd); Assert.Equal(90, introChapter.End);
} }
[Theory] [Theory]
@ -39,11 +39,11 @@ public class TestChapterAnalyzer
var creditsChapter = FindChapter(chapters, AnalysisMode.Credits); var creditsChapter = FindChapter(chapters, AnalysisMode.Credits);
Assert.NotNull(creditsChapter); Assert.NotNull(creditsChapter);
Assert.Equal(1890, creditsChapter.IntroStart); Assert.Equal(1890, creditsChapter.Start);
Assert.Equal(2000, creditsChapter.IntroEnd); Assert.Equal(2000, creditsChapter.End);
} }
private Intro? FindChapter(Collection<ChapterInfo> chapters, AnalysisMode mode) private Segment? FindChapter(Collection<ChapterInfo> chapters, AnalysisMode mode)
{ {
var logger = new LoggerFactory().CreateLogger<ChapterAnalyzer>(); var logger = new LoggerFactory().CreateLogger<ChapterAnalyzer>();
var analyzer = new ChapterAnalyzer(logger); var analyzer = new ChapterAnalyzer(logger);

View File

@ -38,8 +38,8 @@ public class TestEdl
Assert.Equal(edlPath, EdlManager.GetEdlPath(mediaPath)); Assert.Equal(edlPath, EdlManager.GetEdlPath(mediaPath));
} }
private Intro MakeIntro(double start, double end) private Segment MakeIntro(double start, double end)
{ {
return new Intro(Guid.Empty, new TimeRange(start, end)); return new Segment(Guid.Empty, new TimeRange(start, end));
} }
} }

View File

@ -34,12 +34,12 @@ public class AnalyzerHelper
/// <param name="originalIntros">Original introductions.</param> /// <param name="originalIntros">Original introductions.</param>
/// <param name="mode">Analysis mode.</param> /// <param name="mode">Analysis mode.</param>
/// <returns>Modified Intro Timestamps.</returns> /// <returns>Modified Intro Timestamps.</returns>
public Dictionary<Guid, Intro> AdjustIntroTimes( public Dictionary<Guid, Segment> AdjustIntroTimes(
ReadOnlyCollection<QueuedEpisode> episodes, ReadOnlyCollection<QueuedEpisode> episodes,
Dictionary<Guid, Intro> originalIntros, Dictionary<Guid, Segment> originalIntros,
AnalysisMode mode) AnalysisMode mode)
{ {
var modifiedIntros = new Dictionary<Guid, Intro>(); var modifiedIntros = new Dictionary<Guid, Segment>();
foreach (var episode in episodes) foreach (var episode in episodes)
{ {
@ -58,15 +58,15 @@ public class AnalyzerHelper
return modifiedIntros; return modifiedIntros;
} }
private Intro AdjustIntroForEpisode(QueuedEpisode episode, Intro originalIntro, AnalysisMode mode) private Segment AdjustIntroForEpisode(QueuedEpisode episode, Segment originalIntro, AnalysisMode mode)
{ {
var chapters = GetChaptersWithVirtualEnd(episode); var chapters = GetChaptersWithVirtualEnd(episode);
var adjustedIntro = new Intro(originalIntro); var adjustedIntro = new Segment(originalIntro);
var originalIntroStart = new TimeRange(Math.Max(0, (int)originalIntro.IntroStart - 5), (int)originalIntro.IntroStart + 10); var originalIntroStart = new TimeRange(Math.Max(0, (int)originalIntro.Start - 5), (int)originalIntro.Start + 10);
var originalIntroEnd = new TimeRange((int)originalIntro.IntroEnd - 10, Math.Min(episode.Duration, (int)originalIntro.IntroEnd + 5)); var originalIntroEnd = new TimeRange((int)originalIntro.End - 10, Math.Min(episode.Duration, (int)originalIntro.End + 5));
_logger.LogTrace("{Name} original intro: {Start} - {End}", episode.Name, originalIntro.IntroStart, originalIntro.IntroEnd); _logger.LogTrace("{Name} original intro: {Start} - {End}", episode.Name, originalIntro.Start, originalIntro.End);
if (!AdjustIntroBasedOnChapters(episode, chapters, adjustedIntro, originalIntroStart, originalIntroEnd) if (!AdjustIntroBasedOnChapters(episode, chapters, adjustedIntro, originalIntroStart, originalIntroEnd)
&& mode == AnalysisMode.Introduction) && mode == AnalysisMode.Introduction)
@ -84,7 +84,7 @@ public class AnalyzerHelper
return chapters; return chapters;
} }
private bool AdjustIntroBasedOnChapters(QueuedEpisode episode, List<ChapterInfo> chapters, Intro adjustedIntro, TimeRange originalIntroStart, TimeRange originalIntroEnd) private bool AdjustIntroBasedOnChapters(QueuedEpisode episode, List<ChapterInfo> chapters, Segment adjustedIntro, TimeRange originalIntroStart, TimeRange originalIntroEnd)
{ {
foreach (var chapter in chapters) foreach (var chapter in chapters)
{ {
@ -92,13 +92,13 @@ public class AnalyzerHelper
if (originalIntroStart.Start < chapterStartSeconds && chapterStartSeconds < originalIntroStart.End) if (originalIntroStart.Start < chapterStartSeconds && chapterStartSeconds < originalIntroStart.End)
{ {
adjustedIntro.IntroStart = chapterStartSeconds; adjustedIntro.Start = chapterStartSeconds;
_logger.LogTrace("{Name} chapter found close to intro start: {Start}", episode.Name, chapterStartSeconds); _logger.LogTrace("{Name} chapter found close to intro start: {Start}", episode.Name, chapterStartSeconds);
} }
if (originalIntroEnd.Start < chapterStartSeconds && chapterStartSeconds < originalIntroEnd.End) if (originalIntroEnd.Start < chapterStartSeconds && chapterStartSeconds < originalIntroEnd.End)
{ {
adjustedIntro.IntroEnd = chapterStartSeconds; adjustedIntro.End = chapterStartSeconds;
_logger.LogTrace("{Name} chapter found close to intro end: {End}", episode.Name, chapterStartSeconds); _logger.LogTrace("{Name} chapter found close to intro end: {End}", episode.Name, chapterStartSeconds);
return true; return true;
} }
@ -107,7 +107,7 @@ public class AnalyzerHelper
return false; return false;
} }
private void AdjustIntroBasedOnSilence(QueuedEpisode episode, Intro adjustedIntro, TimeRange originalIntroEnd) private void AdjustIntroBasedOnSilence(QueuedEpisode episode, Segment adjustedIntro, TimeRange originalIntroEnd)
{ {
var silence = FFmpegWrapper.DetectSilence(episode, originalIntroEnd); var silence = FFmpegWrapper.DetectSilence(episode, originalIntroEnd);
@ -117,16 +117,16 @@ public class AnalyzerHelper
if (IsValidSilenceForIntroAdjustment(currentRange, originalIntroEnd, adjustedIntro)) if (IsValidSilenceForIntroAdjustment(currentRange, originalIntroEnd, adjustedIntro))
{ {
adjustedIntro.IntroEnd = currentRange.Start; adjustedIntro.End = currentRange.Start;
break; break;
} }
} }
} }
private bool IsValidSilenceForIntroAdjustment(TimeRange silenceRange, TimeRange originalIntroEnd, Intro adjustedIntro) private bool IsValidSilenceForIntroAdjustment(TimeRange silenceRange, TimeRange originalIntroEnd, Segment adjustedIntro)
{ {
return originalIntroEnd.Intersects(silenceRange) && return originalIntroEnd.Intersects(silenceRange) &&
silenceRange.Duration >= _silenceDetectionMinimumDuration && silenceRange.Duration >= _silenceDetectionMinimumDuration &&
silenceRange.Start >= adjustedIntro.IntroStart; silenceRange.Start >= adjustedIntro.Start;
} }
} }

View File

@ -50,7 +50,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
throw new NotImplementedException("mode must equal Credits"); throw new NotImplementedException("mode must equal Credits");
} }
var creditTimes = new Dictionary<Guid, Intro>(); var creditTimes = new Dictionary<Guid, Segment>();
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue); var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
@ -113,7 +113,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
continue; continue;
} }
searchStart = episode.Duration - credit.IntroStart + (0.5 * searchDistance); searchStart = episode.Duration - credit.Start + (0.5 * searchDistance);
creditTimes.Add(episode.EpisodeId, credit); creditTimes.Add(episode.EpisodeId, credit);
episode.State.SetAnalyzed(mode, true); episode.State.SetAnalyzed(mode, true);
@ -135,7 +135,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
/// <param name="searchDistance">Search Distance.</param> /// <param name="searchDistance">Search Distance.</param>
/// <param name="minimum">Percentage of the frame that must be black.</param> /// <param name="minimum">Percentage of the frame that must be black.</param>
/// <returns>Credits timestamp.</returns> /// <returns>Credits timestamp.</returns>
public Intro? AnalyzeMediaFile(QueuedEpisode episode, double searchStart, int searchDistance, int minimum) public Segment? AnalyzeMediaFile(QueuedEpisode episode, double searchStart, int searchDistance, int minimum)
{ {
// Start by analyzing the last N minutes of the file. // Start by analyzing the last N minutes of the file.
var upperLimit = searchStart; var upperLimit = searchStart;

View File

@ -34,7 +34,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
AnalysisMode mode, AnalysisMode mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var skippableRanges = new Dictionary<Guid, Intro>(); var skippableRanges = new Dictionary<Guid, Segment>();
// Episode analysis queue. // Episode analysis queue.
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue); var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
@ -84,7 +84,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
/// <param name="expression">Regular expression pattern.</param> /// <param name="expression">Regular expression pattern.</param>
/// <param name="mode">Analysis mode.</param> /// <param name="mode">Analysis mode.</param>
/// <returns>Intro object containing skippable time range, or null if no chapter matched.</returns> /// <returns>Intro object containing skippable time range, or null if no chapter matched.</returns>
public Intro? FindMatchingChapter( public Segment? FindMatchingChapter(
QueuedEpisode episode, QueuedEpisode episode,
Collection<ChapterInfo> chapters, Collection<ChapterInfo> chapters,
string expression, string expression,
@ -165,7 +165,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
} }
_logger.LogTrace("{Base}: okay", baseMessage); _logger.LogTrace("{Base}: okay", baseMessage);
return new Intro(episode.EpisodeId, currentRange); return new Segment(episode.EpisodeId, currentRange);
} }
return null; return null;

View File

@ -56,7 +56,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// All intros for this season. // All intros for this season.
var seasonIntros = new Dictionary<Guid, Intro>(); var seasonIntros = new Dictionary<Guid, Segment>();
// Cache of all fingerprints for this season. // Cache of all fingerprints for this season.
var fingerprintCache = new Dictionary<Guid, uint[]>(); var fingerprintCache = new Dictionary<Guid, uint[]>();
@ -157,14 +157,14 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
if (_analysisMode == AnalysisMode.Credits) if (_analysisMode == AnalysisMode.Credits)
{ {
// Calculate new values for the current intro // Calculate new values for the current intro
double currentOriginalIntroStart = currentIntro.IntroStart; double currentOriginalIntroStart = currentIntro.Start;
currentIntro.IntroStart = currentEpisode.Duration - currentIntro.IntroEnd; currentIntro.Start = currentEpisode.Duration - currentIntro.End;
currentIntro.IntroEnd = currentEpisode.Duration - currentOriginalIntroStart; currentIntro.End = currentEpisode.Duration - currentOriginalIntroStart;
// Calculate new values for the remaining intro // Calculate new values for the remaining intro
double remainingIntroOriginalStart = remainingIntro.IntroStart; double remainingIntroOriginalStart = remainingIntro.Start;
remainingIntro.IntroStart = remainingEpisode.Duration - remainingIntro.IntroEnd; remainingIntro.Start = remainingEpisode.Duration - remainingIntro.End;
remainingIntro.IntroEnd = remainingEpisode.Duration - remainingIntroOriginalStart; remainingIntro.End = remainingEpisode.Duration - remainingIntroOriginalStart;
} }
// Only save the discovered intro if it is: // Only save the discovered intro if it is:
@ -218,7 +218,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
/// <param name="rhsId">Second episode id.</param> /// <param name="rhsId">Second episode id.</param>
/// <param name="rhsPoints">Second episode fingerprint points.</param> /// <param name="rhsPoints">Second episode fingerprint points.</param>
/// <returns>Intros for the first and second episodes.</returns> /// <returns>Intros for the first and second episodes.</returns>
public (Intro Lhs, Intro Rhs) CompareEpisodes( public (Segment Lhs, Segment Rhs) CompareEpisodes(
Guid lhsId, Guid lhsId,
uint[] lhsPoints, uint[] lhsPoints,
Guid rhsId, Guid rhsId,
@ -240,7 +240,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
lhsId, lhsId,
rhsId); rhsId);
return (new Intro(lhsId), new Intro(rhsId)); return (new Segment(lhsId), new Segment(rhsId));
} }
/// <summary> /// <summary>
@ -251,7 +251,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
/// <param name="rhsId">Second episode id.</param> /// <param name="rhsId">Second episode id.</param>
/// <param name="rhsRanges">Second episode shared timecodes.</param> /// <param name="rhsRanges">Second episode shared timecodes.</param>
/// <returns>Intros for the first and second episodes.</returns> /// <returns>Intros for the first and second episodes.</returns>
private (Intro Lhs, Intro Rhs) GetLongestTimeRange( private (Segment Lhs, Segment Rhs) GetLongestTimeRange(
Guid lhsId, Guid lhsId,
List<TimeRange> lhsRanges, List<TimeRange> lhsRanges,
Guid rhsId, Guid rhsId,
@ -276,7 +276,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
} }
// Create Intro classes for each time range. // Create Intro classes for each time range.
return (new Intro(lhsId, lhsIntro), new Intro(rhsId, rhsIntro)); return (new Segment(lhsId, lhsIntro), new Segment(rhsId, rhsIntro));
} }
/// <summary> /// <summary>

View File

@ -128,8 +128,8 @@ public class AutoSkip(
} }
// Seek is unreliable if called at the very start of an episode. // Seek is unreliable if called at the very start of an episode.
var adjustedStart = Math.Max(5, intro.IntroStart + Plugin.Instance.Configuration.SecondsOfIntroStartToPlay); var adjustedStart = Math.Max(5, intro.Start + Plugin.Instance.Configuration.SecondsOfIntroStartToPlay);
var adjustedEnd = intro.IntroEnd - Plugin.Instance.Configuration.RemainingSecondsOfIntro; var adjustedEnd = intro.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
_logger.LogTrace( _logger.LogTrace(
"Playback position is {Position}, intro runs from {Start} to {End}", "Playback position is {Position}, intro runs from {Start} to {End}",

View File

@ -128,8 +128,8 @@ public class AutoSkipCredits(
} }
// Seek is unreliable if called at the very end of an episode. // Seek is unreliable if called at the very end of an episode.
var adjustedStart = credit.IntroStart + Plugin.Instance.Configuration.SecondsOfCreditsStartToPlay; var adjustedStart = credit.Start + Plugin.Instance.Configuration.SecondsOfCreditsStartToPlay;
var adjustedEnd = credit.IntroEnd - Plugin.Instance.Configuration.RemainingSecondsOfIntro; var adjustedEnd = credit.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
_logger.LogTrace( _logger.LogTrace(
"Playback position is {Position}, credits run from {Start} to {End}", "Playback position is {Position}, credits run from {Start} to {End}",

View File

@ -1024,16 +1024,16 @@
// 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("#editLeftIntroEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Introduction.IntroStart)); document.querySelector("#editLeftIntroEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Introduction.Start));
document.querySelector("#editLeftIntroEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Introduction.IntroEnd)); document.querySelector("#editLeftIntroEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Introduction.End));
document.querySelector("#editLeftCreditEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Credits.IntroStart)); document.querySelector("#editLeftCreditEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Credits.Start));
document.querySelector("#editLeftCreditEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Credits.IntroEnd)); document.querySelector("#editLeftCreditEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Credits.End));
document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text; document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
document.querySelector("#editRightIntroEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Introduction.IntroStart)); document.querySelector("#editRightIntroEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Introduction.Start));
document.querySelector("#editRightIntroEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Introduction.IntroEnd)); document.querySelector("#editRightIntroEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Introduction.End));
document.querySelector("#editRightCreditEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Credits.IntroStart)); document.querySelector("#editRightCreditEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Credits.Start));
document.querySelector("#editRightCreditEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Credits.IntroEnd)); document.querySelector("#editRightCreditEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Credits.End));
} }
// adds an item to a dropdown // adds an item to a dropdown

View File

@ -364,10 +364,10 @@ const introSkipper = {
}, },
updateSkipperFields(skipperFields) { updateSkipperFields(skipperFields) {
const { Introduction = {}, Credits = {} } = this.skipperData; const { Introduction = {}, Credits = {} } = this.skipperData;
skipperFields.querySelector('#introStartEdit').value = Introduction.IntroStart || 0; skipperFields.querySelector('#introStartEdit').value = Introduction.Start || 0;
skipperFields.querySelector('#introEndEdit').value = Introduction.IntroEnd || 0; skipperFields.querySelector('#introEndEdit').value = Introduction.End || 0;
skipperFields.querySelector('#creditsStartEdit').value = Credits.IntroStart || 0; skipperFields.querySelector('#creditsStartEdit').value = Credits.Start || 0;
skipperFields.querySelector('#creditsEndEdit').value = Credits.IntroEnd || 0; skipperFields.querySelector('#creditsEndEdit').value = Credits.End || 0;
}, },
attachSaveListener(metadataFormFields) { attachSaveListener(metadataFormFields) {
const saveButton = metadataFormFields.querySelector('.formDialogFooter .btnSave'); const saveButton = metadataFormFields.querySelector('.formDialogFooter .btnSave');
@ -414,19 +414,19 @@ const introSkipper = {
async saveSkipperData() { async saveSkipperData() {
const newTimestamps = { const newTimestamps = {
Introduction: { Introduction: {
IntroStart: parseFloat(document.getElementById('introStartEdit').value || 0), Start: parseFloat(document.getElementById('introStartEdit').value || 0),
IntroEnd: parseFloat(document.getElementById('introEndEdit').value || 0) End: parseFloat(document.getElementById('introEndEdit').value || 0)
}, },
Credits: { Credits: {
IntroStart: parseFloat(document.getElementById('creditsStartEdit').value || 0), Start: parseFloat(document.getElementById('creditsStartEdit').value || 0),
IntroEnd: parseFloat(document.getElementById('creditsEndEdit').value || 0) End: parseFloat(document.getElementById('creditsEndEdit').value || 0)
} }
}; };
const { Introduction = {}, Credits = {} } = this.skipperData; const { Introduction = {}, Credits = {} } = this.skipperData;
if (newTimestamps.Introduction.IntroStart !== (Introduction.IntroStart || 0) || if (newTimestamps.Introduction.Start !== (Introduction.Start || 0) ||
newTimestamps.Introduction.IntroEnd !== (Introduction.IntroEnd || 0) || newTimestamps.Introduction.End !== (Introduction.End || 0) ||
newTimestamps.Credits.IntroStart !== (Credits.IntroStart || 0) || newTimestamps.Credits.Start !== (Credits.Start || 0) ||
newTimestamps.Credits.IntroEnd !== (Credits.IntroEnd || 0)) { newTimestamps.Credits.End !== (Credits.End || 0)) {
const response = await this.secureFetch(`Episode/${this.currentEpisodeId}/Timestamps`, "POST", JSON.stringify(newTimestamps)); const response = await this.secureFetch(`Episode/${this.currentEpisodeId}/Timestamps`, "POST", JSON.stringify(newTimestamps));
this.d(response.ok ? 'Timestamps updated successfully' : 'Failed to update timestamps:', response.status); this.d(response.ok ? 'Timestamps updated successfully' : 'Failed to update timestamps:', response.status);
} else { } else {

View File

@ -68,16 +68,16 @@ public class SkipIntroController : ControllerBase
return NotFound(); return NotFound();
} }
if (timestamps?.Introduction.IntroEnd > 0.0) if (timestamps?.Introduction.End > 0.0)
{ {
var tr = new TimeRange(timestamps.Introduction.IntroStart, timestamps.Introduction.IntroEnd); var tr = new TimeRange(timestamps.Introduction.Start, timestamps.Introduction.End);
Plugin.Instance!.Intros[id] = new Intro(id, tr); Plugin.Instance!.Intros[id] = new Segment(id, tr);
} }
if (timestamps?.Credits.IntroEnd > 0.0) if (timestamps?.Credits.End > 0.0)
{ {
var cr = new TimeRange(timestamps.Credits.IntroStart, timestamps.Credits.IntroEnd); var cr = new TimeRange(timestamps.Credits.Start, timestamps.Credits.End);
Plugin.Instance!.Credits[id] = new Intro(id, cr); Plugin.Instance!.Credits[id] = new Segment(id, cr);
} }
Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction); Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction);
@ -208,45 +208,6 @@ public class SkipIntroController : ControllerBase
return NoContent(); return NoContent();
} }
/// <summary>
/// Get all introductions or credits. Only used by the end to end testing script.
/// </summary>
/// <param name="mode">Mode.</param>
/// <response code="200">All timestamps have been returned.</response>
/// <returns>List of IntroWithMetadata objects.</returns>
[Authorize(Policy = Policies.RequiresElevation)]
[HttpGet("Intros/All")]
public ActionResult<List<IntroWithMetadata>> GetAllTimestamps(
[FromQuery] AnalysisMode mode = AnalysisMode.Introduction)
{
List<IntroWithMetadata> intros = [];
var timestamps = mode == AnalysisMode.Introduction ?
Plugin.Instance!.Intros :
Plugin.Instance!.Credits;
// Get metadata for all intros
foreach (var intro in timestamps)
{
// Get the details of the item from Jellyfin
var rawItem = Plugin.Instance.GetItem(intro.Key);
if (rawItem == null || rawItem is not Episode episode)
{
throw new InvalidCastException("Unable to cast item id " + intro.Key + " to an Episode");
}
// Associate the metadata with the intro
intros.Add(
new IntroWithMetadata(
episode.SeriesName,
episode.AiredSeasonNumber ?? 0,
episode.Name,
intro.Value));
}
return intros;
}
/// <summary> /// <summary>
/// Gets the user interface configuration. /// Gets the user interface configuration.
/// </summary> /// </summary>

View File

@ -171,7 +171,7 @@ public class VisualizationController : ControllerBase
if (timestamps.IntroEnd > 0.0) if (timestamps.IntroEnd > 0.0)
{ {
var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd); var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd);
Plugin.Instance!.Intros[id] = new Intro(id, tr); Plugin.Instance!.Intros[id] = new Segment(id, tr);
Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction); Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction);
} }

View File

@ -1,7 +1,5 @@
using System; using System;
using System.Globalization;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data; namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
@ -9,55 +7,18 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
/// Result of fingerprinting and analyzing two episodes in a season. /// Result of fingerprinting and analyzing two episodes in a season.
/// All times are measured in seconds relative to the beginning of the media file. /// All times are measured in seconds relative to the beginning of the media file.
/// </summary> /// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="Intro"/> class.
/// </remarks>
/// <param name="intro">intro.</param>
[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/ConfusedPolarBear.Plugin.IntroSkipper")] [DataContract(Namespace = "http://schemas.datacontract.org/2004/07/ConfusedPolarBear.Plugin.IntroSkipper")]
public class Intro public class Intro(Segment intro)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Intro"/> class.
/// </summary>
/// <param name="episode">Episode.</param>
/// <param name="intro">Introduction time range.</param>
public Intro(Guid episode, TimeRange intro)
{
EpisodeId = episode;
IntroStart = intro.Start;
IntroEnd = intro.End;
}
/// <summary>
/// Initializes a new instance of the <see cref="Intro"/> class.
/// </summary>
/// <param name="episode">Episode.</param>
public Intro(Guid episode)
{
EpisodeId = episode;
IntroStart = 0;
IntroEnd = 0;
}
/// <summary>
/// Initializes a new instance of the <see cref="Intro"/> class.
/// </summary>
/// <param name="intro">intro.</param>
public Intro(Intro intro)
{
EpisodeId = intro.EpisodeId;
IntroStart = intro.IntroStart;
IntroEnd = intro.IntroEnd;
}
/// <summary>
/// Initializes a new instance of the <see cref="Intro"/> class.
/// </summary>
public Intro()
{
}
/// <summary> /// <summary>
/// Gets or sets the Episode ID. /// Gets or sets the Episode ID.
/// </summary> /// </summary>
[DataMember] [DataMember]
public Guid EpisodeId { get; set; } public Guid EpisodeId { get; set; } = intro.EpisodeId;
/// <summary> /// <summary>
/// Gets a value indicating whether this introduction is valid or not. /// Gets a value indicating whether this introduction is valid or not.
@ -65,23 +26,17 @@ public class Intro
/// </summary> /// </summary>
public bool Valid => IntroEnd > 0; public bool Valid => IntroEnd > 0;
/// <summary>
/// Gets the duration of this intro.
/// </summary>
[JsonIgnore]
public double Duration => IntroEnd - IntroStart;
/// <summary> /// <summary>
/// Gets or sets the introduction sequence start time. /// Gets or sets the introduction sequence start time.
/// </summary> /// </summary>
[DataMember] [DataMember]
public double IntroStart { get; set; } public double IntroStart { get; set; } = intro.Start;
/// <summary> /// <summary>
/// Gets or sets the introduction sequence end time. /// Gets or sets the introduction sequence end time.
/// </summary> /// </summary>
[DataMember] [DataMember]
public double IntroEnd { get; set; } public double IntroEnd { get; set; } = intro.End;
/// <summary> /// <summary>
/// Gets or sets the recommended time to display the skip intro prompt. /// Gets or sets the recommended time to display the skip intro prompt.
@ -92,22 +47,4 @@ public class Intro
/// Gets or sets the recommended time to hide the skip intro prompt. /// Gets or sets the recommended time to hide the skip intro prompt.
/// </summary> /// </summary>
public double HideSkipPromptAt { get; set; } public double HideSkipPromptAt { get; set; }
/// <summary>
/// Convert this Intro object to a Kodi compatible EDL entry.
/// </summary>
/// <param name="action">User specified configuration EDL action.</param>
/// <returns>String.</returns>
public string ToEdl(EdlAction action)
{
if (action == EdlAction.None)
{
throw new ArgumentException("Cannot serialize an EdlAction of None");
}
var start = Math.Round(IntroStart, 2);
var end = Math.Round(IntroEnd, 2);
return string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", start, end, (int)action);
}
} }

View File

@ -1,40 +0,0 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
/// <summary>
/// An Intro class with episode metadata. Only used in end to end testing programs.
/// </summary>
public class IntroWithMetadata : Intro
{
/// <summary>
/// Initializes a new instance of the <see cref="IntroWithMetadata"/> class.
/// </summary>
/// <param name="series">Series name.</param>
/// <param name="season">Season number.</param>
/// <param name="title">Episode title.</param>
/// <param name="intro">Intro timestamps.</param>
public IntroWithMetadata(string series, int season, string title, Intro intro)
{
Series = series;
Season = season;
Title = title;
EpisodeId = intro.EpisodeId;
IntroStart = intro.IntroStart;
IntroEnd = intro.IntroEnd;
}
/// <summary>
/// Gets or sets the series name of the TV episode associated with this intro.
/// </summary>
public string Series { get; set; }
/// <summary>
/// Gets or sets the season number of the TV episode associated with this intro.
/// </summary>
public int Season { get; set; }
/// <summary>
/// Gets or sets the title of the TV episode associated with this intro.
/// </summary>
public string Title { get; set; }
}

View File

@ -0,0 +1,114 @@
using System;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
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>
[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/ConfusedPolarBear.Plugin.IntroSkipper.Segment")]
public class Segment
{
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class.
/// </summary>
/// <param name="episode">Episode.</param>
/// <param name="segment">Introduction time range.</param>
public Segment(Guid episode, TimeRange segment)
{
EpisodeId = episode;
Start = segment.Start;
End = segment.End;
}
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class.
/// </summary>
/// <param name="episode">Episode.</param>
public Segment(Guid episode)
{
EpisodeId = episode;
Start = 0;
End = 0;
}
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class.
/// </summary>
/// <param name="intro">intro.</param>
public Segment(Segment intro)
{
EpisodeId = intro.EpisodeId;
Start = intro.Start;
End = intro.End;
}
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class.
/// </summary>
/// <param name="intro">intro.</param>
public Segment(Intro intro)
{
EpisodeId = intro.EpisodeId;
Start = intro.IntroStart;
End = intro.IntroEnd;
}
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class.
/// </summary>
public Segment()
{
}
/// <summary>
/// Gets or sets the Episode ID.
/// </summary>
[DataMember]
public Guid EpisodeId { get; set; }
/// <summary>
/// Gets or sets the introduction sequence start time.
/// </summary>
[DataMember]
public double Start { get; set; }
/// <summary>
/// Gets or sets the introduction sequence end time.
/// </summary>
[DataMember]
public double End { get; set; }
/// <summary>
/// Gets a value indicating whether this introduction is valid or not.
/// Invalid results must not be returned through the API.
/// </summary>
public bool Valid => End > 0;
/// <summary>
/// Gets the duration of this intro.
/// </summary>
[JsonIgnore]
public double Duration => End - Start;
/// <summary>
/// Convert this Intro object to a Kodi compatible EDL entry.
/// </summary>
/// <param name="action">User specified configuration EDL action.</param>
/// <returns>String.</returns>
public string ToEdl(EdlAction action)
{
if (action == EdlAction.None)
{
throw new ArgumentException("Cannot serialize an EdlAction of None");
}
var start = Math.Round(Start, 2);
var end = Math.Round(End, 2);
return string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", start, end, (int)action);
}
}

View File

@ -9,11 +9,11 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Data
/// <summary> /// <summary>
/// Gets or sets Introduction. /// Gets or sets Introduction.
/// </summary> /// </summary>
public Intro Introduction { get; set; } = new Intro(); public Segment Introduction { get; set; } = new Segment();
/// <summary> /// <summary>
/// Gets or sets Credits. /// Gets or sets Credits.
/// </summary> /// </summary>
public Intro Credits { get; set; } = new Intro(); public Segment Credits { get; set; } = new Segment();
} }
} }

View File

@ -147,12 +147,12 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <summary> /// <summary>
/// Gets the results of fingerprinting all episodes. /// Gets the results of fingerprinting all episodes.
/// </summary> /// </summary>
public ConcurrentDictionary<Guid, Intro> Intros { get; } = new(); public ConcurrentDictionary<Guid, Segment> Intros { get; } = new();
/// <summary> /// <summary>
/// Gets all discovered ending credits. /// Gets all discovered ending credits.
/// </summary> /// </summary>
public ConcurrentDictionary<Guid, Intro> Credits { get; } = new(); public ConcurrentDictionary<Guid, Segment> Credits { get; } = new();
/// <summary> /// <summary>
/// Gets the most recent media item queue. /// Gets the most recent media item queue.
@ -201,7 +201,7 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
public void SaveTimestamps(AnalysisMode mode) public void SaveTimestamps(AnalysisMode mode)
{ {
List<Intro> introList = []; List<Segment> introList = [];
var filePath = mode == AnalysisMode.Introduction var filePath = mode == AnalysisMode.Introduction
? _introPath ? _introPath
: _creditsPath; : _creditsPath;
@ -311,7 +311,7 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <param name="id">Item id.</param> /// <param name="id">Item id.</param>
/// <param name="mode">Mode.</param> /// <param name="mode">Mode.</param>
/// <returns>Intro.</returns> /// <returns>Intro.</returns>
internal static Intro GetIntroByMode(Guid id, AnalysisMode mode) internal static Segment GetIntroByMode(Guid id, AnalysisMode mode)
{ {
return mode == AnalysisMode.Introduction return mode == AnalysisMode.Introduction
? Instance!.Intros[id] ? Instance!.Intros[id]
@ -366,7 +366,7 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// <returns>State of this item.</returns> /// <returns>State of this item.</returns>
internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState()); internal EpisodeState GetState(Guid id) => EpisodeStates.GetOrAdd(id, _ => new EpisodeState());
internal void UpdateTimestamps(Dictionary<Guid, Intro> newTimestamps, AnalysisMode mode) internal void UpdateTimestamps(Dictionary<Guid, Segment> newTimestamps, AnalysisMode mode)
{ {
foreach (var intro in newTimestamps) foreach (var intro in newTimestamps)
{ {

View File

@ -20,9 +20,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
serializer.WriteObject(fileStream, obj); serializer.WriteObject(fileStream, obj);
} }
public static List<Intro> DeserializeFromXml(string filePath) public static void MigrateFromIntro(string filePath)
{ {
var result = new List<Intro>(); var intros = new List<Intro>();
var segments = new List<Segment>();
try try
{ {
// Create a FileStream to read the XML file // Create a FileStream to read the XML file
@ -34,7 +35,39 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
DataContractSerializer serializer = new DataContractSerializer(typeof(List<Intro>)); DataContractSerializer serializer = new DataContractSerializer(typeof(List<Intro>));
// Deserialize the object from the XML // Deserialize the object from the XML
result = serializer.ReadObject(reader) as List<Intro>; intros = serializer.ReadObject(reader) as List<Intro>;
// Close the reader
reader.Close();
}
catch (Exception ex)
{
Console.WriteLine($"Error deserializing XML: {ex.Message}");
}
ArgumentNullException.ThrowIfNull(intros);
intros.ForEach(delegate(Intro name)
{
segments.Add(new Segment(name));
});
SerializeToXml(segments, filePath);
}
public static List<Segment> DeserializeFromXml(string filePath)
{
var result = new List<Segment>();
try
{
// Create a FileStream to read the XML file
using FileStream fileStream = new FileStream(filePath, FileMode.Open);
// Create an XmlDictionaryReader to read the XML
XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(fileStream, new XmlDictionaryReaderQuotas());
// Create a DataContractSerializer for type T
DataContractSerializer serializer = new DataContractSerializer(typeof(List<Segment>));
// Deserialize the object from the XML
result = serializer.ReadObject(reader) as List<Segment>;
// Close the reader // Close the reader
reader.Close(); reader.Close();
@ -81,6 +114,12 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
// Save the modified XML document // Save the modified XML document
xmlDoc.Save(filePath); xmlDoc.Save(filePath);
} }
// intro -> segment migration
if (xmlDoc.DocumentElement.NamespaceURI == "http://schemas.datacontract.org/2004/07/ConfusedPolarBear.Plugin.IntroSkipper")
{
MigrateFromIntro(filePath);
}
} }
catch (XmlException ex) catch (XmlException ex)
{ {