migrate Intro to Segment and make it generic (#268)
This commit is contained in:
parent
92d2a55c81
commit
2438ba79f2
@ -105,13 +105,13 @@ public class TestAudioFingerprinting
|
||||
rhsFingerprint);
|
||||
|
||||
Assert.True(lhs.Valid);
|
||||
Assert.Equal(0, lhs.IntroStart);
|
||||
Assert.Equal(17.208, lhs.IntroEnd, 3);
|
||||
Assert.Equal(0, lhs.Start);
|
||||
Assert.Equal(17.208, lhs.End, 3);
|
||||
|
||||
Assert.True(rhs.Valid);
|
||||
// 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(22.1602, rhs.IntroEnd);
|
||||
Assert.Equal(0, rhs.Start);
|
||||
Assert.Equal(22.1602, rhs.End);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -42,7 +42,7 @@ public class TestBlackFrames
|
||||
|
||||
var result = analyzer.AnalyzeMediaFile(episode, 240, 30, 85);
|
||||
Assert.NotNull(result);
|
||||
Assert.InRange(result.IntroStart, 300 - range, 300 + range);
|
||||
Assert.InRange(result.Start, 300 - range, 300 + range);
|
||||
}
|
||||
|
||||
private QueuedEpisode queueFile(string path)
|
||||
|
@ -23,8 +23,8 @@ public class TestChapterAnalyzer
|
||||
var introChapter = FindChapter(chapters, AnalysisMode.Introduction);
|
||||
|
||||
Assert.NotNull(introChapter);
|
||||
Assert.Equal(60, introChapter.IntroStart);
|
||||
Assert.Equal(90, introChapter.IntroEnd);
|
||||
Assert.Equal(60, introChapter.Start);
|
||||
Assert.Equal(90, introChapter.End);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -39,11 +39,11 @@ public class TestChapterAnalyzer
|
||||
var creditsChapter = FindChapter(chapters, AnalysisMode.Credits);
|
||||
|
||||
Assert.NotNull(creditsChapter);
|
||||
Assert.Equal(1890, creditsChapter.IntroStart);
|
||||
Assert.Equal(2000, creditsChapter.IntroEnd);
|
||||
Assert.Equal(1890, creditsChapter.Start);
|
||||
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 analyzer = new ChapterAnalyzer(logger);
|
||||
|
@ -38,8 +38,8 @@ public class TestEdl
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -34,12 +34,12 @@ public class AnalyzerHelper
|
||||
/// <param name="originalIntros">Original introductions.</param>
|
||||
/// <param name="mode">Analysis mode.</param>
|
||||
/// <returns>Modified Intro Timestamps.</returns>
|
||||
public Dictionary<Guid, Intro> AdjustIntroTimes(
|
||||
public Dictionary<Guid, Segment> AdjustIntroTimes(
|
||||
ReadOnlyCollection<QueuedEpisode> episodes,
|
||||
Dictionary<Guid, Intro> originalIntros,
|
||||
Dictionary<Guid, Segment> originalIntros,
|
||||
AnalysisMode mode)
|
||||
{
|
||||
var modifiedIntros = new Dictionary<Guid, Intro>();
|
||||
var modifiedIntros = new Dictionary<Guid, Segment>();
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
@ -58,15 +58,15 @@ public class AnalyzerHelper
|
||||
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 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 originalIntroEnd = new TimeRange((int)originalIntro.IntroEnd - 10, Math.Min(episode.Duration, (int)originalIntro.IntroEnd + 5));
|
||||
var originalIntroStart = new TimeRange(Math.Max(0, (int)originalIntro.Start - 5), (int)originalIntro.Start + 10);
|
||||
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)
|
||||
&& mode == AnalysisMode.Introduction)
|
||||
@ -84,7 +84,7 @@ public class AnalyzerHelper
|
||||
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)
|
||||
{
|
||||
@ -92,13 +92,13 @@ public class AnalyzerHelper
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
@ -107,7 +107,7 @@ public class AnalyzerHelper
|
||||
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);
|
||||
|
||||
@ -117,16 +117,16 @@ public class AnalyzerHelper
|
||||
|
||||
if (IsValidSilenceForIntroAdjustment(currentRange, originalIntroEnd, adjustedIntro))
|
||||
{
|
||||
adjustedIntro.IntroEnd = currentRange.Start;
|
||||
adjustedIntro.End = currentRange.Start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidSilenceForIntroAdjustment(TimeRange silenceRange, TimeRange originalIntroEnd, Intro adjustedIntro)
|
||||
private bool IsValidSilenceForIntroAdjustment(TimeRange silenceRange, TimeRange originalIntroEnd, Segment adjustedIntro)
|
||||
{
|
||||
return originalIntroEnd.Intersects(silenceRange) &&
|
||||
silenceRange.Duration >= _silenceDetectionMinimumDuration &&
|
||||
silenceRange.Start >= adjustedIntro.IntroStart;
|
||||
silenceRange.Start >= adjustedIntro.Start;
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
||||
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);
|
||||
|
||||
@ -113,7 +113,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
searchStart = episode.Duration - credit.IntroStart + (0.5 * searchDistance);
|
||||
searchStart = episode.Duration - credit.Start + (0.5 * searchDistance);
|
||||
|
||||
creditTimes.Add(episode.EpisodeId, credit);
|
||||
episode.State.SetAnalyzed(mode, true);
|
||||
@ -135,7 +135,7 @@ public class BlackFrameAnalyzer : IMediaFileAnalyzer
|
||||
/// <param name="searchDistance">Search Distance.</param>
|
||||
/// <param name="minimum">Percentage of the frame that must be black.</param>
|
||||
/// <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.
|
||||
var upperLimit = searchStart;
|
||||
|
@ -34,7 +34,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
||||
AnalysisMode mode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var skippableRanges = new Dictionary<Guid, Intro>();
|
||||
var skippableRanges = new Dictionary<Guid, Segment>();
|
||||
|
||||
// Episode analysis queue.
|
||||
var episodeAnalysisQueue = new List<QueuedEpisode>(analysisQueue);
|
||||
@ -84,7 +84,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
||||
/// <param name="expression">Regular expression pattern.</param>
|
||||
/// <param name="mode">Analysis mode.</param>
|
||||
/// <returns>Intro object containing skippable time range, or null if no chapter matched.</returns>
|
||||
public Intro? FindMatchingChapter(
|
||||
public Segment? FindMatchingChapter(
|
||||
QueuedEpisode episode,
|
||||
Collection<ChapterInfo> chapters,
|
||||
string expression,
|
||||
@ -165,7 +165,7 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
|
||||
}
|
||||
|
||||
_logger.LogTrace("{Base}: okay", baseMessage);
|
||||
return new Intro(episode.EpisodeId, currentRange);
|
||||
return new Segment(episode.EpisodeId, currentRange);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -56,7 +56,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// All intros for this season.
|
||||
var seasonIntros = new Dictionary<Guid, Intro>();
|
||||
var seasonIntros = new Dictionary<Guid, Segment>();
|
||||
|
||||
// Cache of all fingerprints for this season.
|
||||
var fingerprintCache = new Dictionary<Guid, uint[]>();
|
||||
@ -157,14 +157,14 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
||||
if (_analysisMode == AnalysisMode.Credits)
|
||||
{
|
||||
// Calculate new values for the current intro
|
||||
double currentOriginalIntroStart = currentIntro.IntroStart;
|
||||
currentIntro.IntroStart = currentEpisode.Duration - currentIntro.IntroEnd;
|
||||
currentIntro.IntroEnd = currentEpisode.Duration - currentOriginalIntroStart;
|
||||
double currentOriginalIntroStart = currentIntro.Start;
|
||||
currentIntro.Start = currentEpisode.Duration - currentIntro.End;
|
||||
currentIntro.End = currentEpisode.Duration - currentOriginalIntroStart;
|
||||
|
||||
// Calculate new values for the remaining intro
|
||||
double remainingIntroOriginalStart = remainingIntro.IntroStart;
|
||||
remainingIntro.IntroStart = remainingEpisode.Duration - remainingIntro.IntroEnd;
|
||||
remainingIntro.IntroEnd = remainingEpisode.Duration - remainingIntroOriginalStart;
|
||||
double remainingIntroOriginalStart = remainingIntro.Start;
|
||||
remainingIntro.Start = remainingEpisode.Duration - remainingIntro.End;
|
||||
remainingIntro.End = remainingEpisode.Duration - remainingIntroOriginalStart;
|
||||
}
|
||||
|
||||
// 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="rhsPoints">Second episode fingerprint points.</param>
|
||||
/// <returns>Intros for the first and second episodes.</returns>
|
||||
public (Intro Lhs, Intro Rhs) CompareEpisodes(
|
||||
public (Segment Lhs, Segment Rhs) CompareEpisodes(
|
||||
Guid lhsId,
|
||||
uint[] lhsPoints,
|
||||
Guid rhsId,
|
||||
@ -240,7 +240,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
||||
lhsId,
|
||||
rhsId);
|
||||
|
||||
return (new Intro(lhsId), new Intro(rhsId));
|
||||
return (new Segment(lhsId), new Segment(rhsId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -251,7 +251,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
||||
/// <param name="rhsId">Second episode id.</param>
|
||||
/// <param name="rhsRanges">Second episode shared timecodes.</param>
|
||||
/// <returns>Intros for the first and second episodes.</returns>
|
||||
private (Intro Lhs, Intro Rhs) GetLongestTimeRange(
|
||||
private (Segment Lhs, Segment Rhs) GetLongestTimeRange(
|
||||
Guid lhsId,
|
||||
List<TimeRange> lhsRanges,
|
||||
Guid rhsId,
|
||||
@ -276,7 +276,7 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
@ -128,8 +128,8 @@ public class AutoSkip(
|
||||
}
|
||||
|
||||
// Seek is unreliable if called at the very start of an episode.
|
||||
var adjustedStart = Math.Max(5, intro.IntroStart + Plugin.Instance.Configuration.SecondsOfIntroStartToPlay);
|
||||
var adjustedEnd = intro.IntroEnd - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
|
||||
var adjustedStart = Math.Max(5, intro.Start + Plugin.Instance.Configuration.SecondsOfIntroStartToPlay);
|
||||
var adjustedEnd = intro.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
|
||||
|
||||
_logger.LogTrace(
|
||||
"Playback position is {Position}, intro runs from {Start} to {End}",
|
||||
|
@ -128,8 +128,8 @@ public class AutoSkipCredits(
|
||||
}
|
||||
|
||||
// Seek is unreliable if called at the very end of an episode.
|
||||
var adjustedStart = credit.IntroStart + Plugin.Instance.Configuration.SecondsOfCreditsStartToPlay;
|
||||
var adjustedEnd = credit.IntroEnd - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
|
||||
var adjustedStart = credit.Start + Plugin.Instance.Configuration.SecondsOfCreditsStartToPlay;
|
||||
var adjustedEnd = credit.End - Plugin.Instance.Configuration.RemainingSecondsOfIntro;
|
||||
|
||||
_logger.LogTrace(
|
||||
"Playback position is {Position}, credits run from {Start} to {End}",
|
||||
|
@ -1024,16 +1024,16 @@
|
||||
// Update the editor for the first and second episodes
|
||||
timestampEditor.style.display = "unset";
|
||||
document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
|
||||
document.querySelector("#editLeftIntroEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Introduction.IntroStart));
|
||||
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("#editLeftIntroEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Introduction.Start));
|
||||
document.querySelector("#editLeftIntroEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Introduction.End));
|
||||
document.querySelector("#editLeftCreditEpisodeStart").value = setTime(Math.round(leftEpisodeJson.Credits.Start));
|
||||
document.querySelector("#editLeftCreditEpisodeEnd").value = setTime(Math.round(leftEpisodeJson.Credits.End));
|
||||
|
||||
document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
|
||||
document.querySelector("#editRightIntroEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Introduction.IntroStart));
|
||||
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));
|
||||
document.querySelector("#editRightIntroEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Introduction.Start));
|
||||
document.querySelector("#editRightIntroEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Introduction.End));
|
||||
document.querySelector("#editRightCreditEpisodeStart").value = setTime(Math.round(rightEpisodeJson.Credits.Start));
|
||||
document.querySelector("#editRightCreditEpisodeEnd").value = setTime(Math.round(rightEpisodeJson.Credits.End));
|
||||
}
|
||||
|
||||
// adds an item to a dropdown
|
||||
|
@ -364,10 +364,10 @@ const introSkipper = {
|
||||
},
|
||||
updateSkipperFields(skipperFields) {
|
||||
const { Introduction = {}, Credits = {} } = this.skipperData;
|
||||
skipperFields.querySelector('#introStartEdit').value = Introduction.IntroStart || 0;
|
||||
skipperFields.querySelector('#introEndEdit').value = Introduction.IntroEnd || 0;
|
||||
skipperFields.querySelector('#creditsStartEdit').value = Credits.IntroStart || 0;
|
||||
skipperFields.querySelector('#creditsEndEdit').value = Credits.IntroEnd || 0;
|
||||
skipperFields.querySelector('#introStartEdit').value = Introduction.Start || 0;
|
||||
skipperFields.querySelector('#introEndEdit').value = Introduction.End || 0;
|
||||
skipperFields.querySelector('#creditsStartEdit').value = Credits.Start || 0;
|
||||
skipperFields.querySelector('#creditsEndEdit').value = Credits.End || 0;
|
||||
},
|
||||
attachSaveListener(metadataFormFields) {
|
||||
const saveButton = metadataFormFields.querySelector('.formDialogFooter .btnSave');
|
||||
@ -414,19 +414,19 @@ const introSkipper = {
|
||||
async saveSkipperData() {
|
||||
const newTimestamps = {
|
||||
Introduction: {
|
||||
IntroStart: parseFloat(document.getElementById('introStartEdit').value || 0),
|
||||
IntroEnd: parseFloat(document.getElementById('introEndEdit').value || 0)
|
||||
Start: parseFloat(document.getElementById('introStartEdit').value || 0),
|
||||
End: parseFloat(document.getElementById('introEndEdit').value || 0)
|
||||
},
|
||||
Credits: {
|
||||
IntroStart: parseFloat(document.getElementById('creditsStartEdit').value || 0),
|
||||
IntroEnd: parseFloat(document.getElementById('creditsEndEdit').value || 0)
|
||||
Start: parseFloat(document.getElementById('creditsStartEdit').value || 0),
|
||||
End: parseFloat(document.getElementById('creditsEndEdit').value || 0)
|
||||
}
|
||||
};
|
||||
const { Introduction = {}, Credits = {} } = this.skipperData;
|
||||
if (newTimestamps.Introduction.IntroStart !== (Introduction.IntroStart || 0) ||
|
||||
newTimestamps.Introduction.IntroEnd !== (Introduction.IntroEnd || 0) ||
|
||||
newTimestamps.Credits.IntroStart !== (Credits.IntroStart || 0) ||
|
||||
newTimestamps.Credits.IntroEnd !== (Credits.IntroEnd || 0)) {
|
||||
if (newTimestamps.Introduction.Start !== (Introduction.Start || 0) ||
|
||||
newTimestamps.Introduction.End !== (Introduction.End || 0) ||
|
||||
newTimestamps.Credits.Start !== (Credits.Start || 0) ||
|
||||
newTimestamps.Credits.End !== (Credits.End || 0)) {
|
||||
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);
|
||||
} else {
|
||||
|
@ -68,16 +68,16 @@ public class SkipIntroController : ControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (timestamps?.Introduction.IntroEnd > 0.0)
|
||||
if (timestamps?.Introduction.End > 0.0)
|
||||
{
|
||||
var tr = new TimeRange(timestamps.Introduction.IntroStart, timestamps.Introduction.IntroEnd);
|
||||
Plugin.Instance!.Intros[id] = new Intro(id, tr);
|
||||
var tr = new TimeRange(timestamps.Introduction.Start, timestamps.Introduction.End);
|
||||
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);
|
||||
Plugin.Instance!.Credits[id] = new Intro(id, cr);
|
||||
var cr = new TimeRange(timestamps.Credits.Start, timestamps.Credits.End);
|
||||
Plugin.Instance!.Credits[id] = new Segment(id, cr);
|
||||
}
|
||||
|
||||
Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction);
|
||||
@ -208,45 +208,6 @@ public class SkipIntroController : ControllerBase
|
||||
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>
|
||||
/// Gets the user interface configuration.
|
||||
/// </summary>
|
||||
|
@ -171,7 +171,7 @@ public class VisualizationController : ControllerBase
|
||||
if (timestamps.IntroEnd > 0.0)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
||||
|
||||
@ -9,55 +7,18 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Data;
|
||||
/// 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>
|
||||
/// <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")]
|
||||
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>
|
||||
/// Gets or sets the Episode ID.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public Guid EpisodeId { get; set; }
|
||||
public Guid EpisodeId { get; set; } = intro.EpisodeId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this introduction is valid or not.
|
||||
@ -65,23 +26,17 @@ public class Intro
|
||||
/// </summary>
|
||||
public bool Valid => IntroEnd > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration of this intro.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double Duration => IntroEnd - IntroStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the introduction sequence start time.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public double IntroStart { get; set; }
|
||||
public double IntroStart { get; set; } = intro.Start;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the introduction sequence end time.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public double IntroEnd { get; set; }
|
||||
public double IntroEnd { get; set; } = intro.End;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
114
ConfusedPolarBear.Plugin.IntroSkipper/Data/Segment.cs
Normal file
114
ConfusedPolarBear.Plugin.IntroSkipper/Data/Segment.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -9,11 +9,11 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper.Data
|
||||
/// <summary>
|
||||
/// Gets or sets Introduction.
|
||||
/// </summary>
|
||||
public Intro Introduction { get; set; } = new Intro();
|
||||
public Segment Introduction { get; set; } = new Segment();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Credits.
|
||||
/// </summary>
|
||||
public Intro Credits { get; set; } = new Intro();
|
||||
public Segment Credits { get; set; } = new Segment();
|
||||
}
|
||||
}
|
||||
|
@ -147,12 +147,12 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// <summary>
|
||||
/// Gets the results of fingerprinting all episodes.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<Guid, Intro> Intros { get; } = new();
|
||||
public ConcurrentDictionary<Guid, Segment> Intros { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all discovered ending credits.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<Guid, Intro> Credits { get; } = new();
|
||||
public ConcurrentDictionary<Guid, Segment> Credits { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most recent media item queue.
|
||||
@ -201,7 +201,7 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// <param name="mode">Mode.</param>
|
||||
public void SaveTimestamps(AnalysisMode mode)
|
||||
{
|
||||
List<Intro> introList = [];
|
||||
List<Segment> introList = [];
|
||||
var filePath = mode == AnalysisMode.Introduction
|
||||
? _introPath
|
||||
: _creditsPath;
|
||||
@ -311,7 +311,7 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// <param name="id">Item id.</param>
|
||||
/// <param name="mode">Mode.</param>
|
||||
/// <returns>Intro.</returns>
|
||||
internal static Intro GetIntroByMode(Guid id, AnalysisMode mode)
|
||||
internal static Segment GetIntroByMode(Guid id, AnalysisMode mode)
|
||||
{
|
||||
return mode == AnalysisMode.Introduction
|
||||
? Instance!.Intros[id]
|
||||
@ -366,7 +366,7 @@ public partial class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// <returns>State of this item.</returns>
|
||||
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)
|
||||
{
|
||||
|
@ -20,9 +20,10 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
|
||||
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
|
||||
{
|
||||
// Create a FileStream to read the XML file
|
||||
@ -34,7 +35,39 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
|
||||
DataContractSerializer serializer = new DataContractSerializer(typeof(List<Intro>));
|
||||
|
||||
// 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
|
||||
reader.Close();
|
||||
@ -81,6 +114,12 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
|
||||
// Save the modified XML document
|
||||
xmlDoc.Save(filePath);
|
||||
}
|
||||
|
||||
// intro -> segment migration
|
||||
if (xmlDoc.DocumentElement.NamespaceURI == "http://schemas.datacontract.org/2004/07/ConfusedPolarBear.Plugin.IntroSkipper")
|
||||
{
|
||||
MigrateFromIntro(filePath);
|
||||
}
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user