parent
bcb5e5ea30
commit
a68914ca8f
@ -0,0 +1,83 @@
|
|||||||
|
namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class TestChapterAnalyzer
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Opening")]
|
||||||
|
[InlineData("OP")]
|
||||||
|
[InlineData("Intro")]
|
||||||
|
[InlineData("Intro Start")]
|
||||||
|
[InlineData("Introduction")]
|
||||||
|
public void TestIntroductionExpression(string chapterName)
|
||||||
|
{
|
||||||
|
var chapters = CreateChapters(chapterName, AnalysisMode.Introduction);
|
||||||
|
var introChapter = FindChapter(chapters, AnalysisMode.Introduction);
|
||||||
|
|
||||||
|
Assert.NotNull(introChapter);
|
||||||
|
Assert.Equal(60, introChapter.IntroStart);
|
||||||
|
Assert.Equal(90, introChapter.IntroEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("End Credits")]
|
||||||
|
[InlineData("Ending")]
|
||||||
|
[InlineData("Credit start")]
|
||||||
|
[InlineData("Closing Credits")]
|
||||||
|
[InlineData("Credits")]
|
||||||
|
public void TestEndCreditsExpression(string chapterName)
|
||||||
|
{
|
||||||
|
var chapters = CreateChapters(chapterName, AnalysisMode.Credits);
|
||||||
|
var creditsChapter = FindChapter(chapters, AnalysisMode.Credits);
|
||||||
|
|
||||||
|
Assert.NotNull(creditsChapter);
|
||||||
|
Assert.Equal(1890, creditsChapter.IntroStart);
|
||||||
|
Assert.Equal(2000, creditsChapter.IntroEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intro? FindChapter(Collection<ChapterInfo> chapters, AnalysisMode mode)
|
||||||
|
{
|
||||||
|
var logger = new LoggerFactory().CreateLogger<ChapterAnalyzer>();
|
||||||
|
var analyzer = new ChapterAnalyzer(logger);
|
||||||
|
|
||||||
|
var config = new Configuration.PluginConfiguration();
|
||||||
|
var expression = mode == AnalysisMode.Introduction ?
|
||||||
|
config.ChapterAnalyzerIntroductionPattern :
|
||||||
|
config.ChapterAnalyzerEndCreditsPattern;
|
||||||
|
|
||||||
|
return analyzer.FindMatchingChapter(Guid.Empty, 2000, chapters, expression, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<ChapterInfo> CreateChapters(string name, AnalysisMode mode)
|
||||||
|
{
|
||||||
|
var chapters = new[]{
|
||||||
|
CreateChapter("Cold Open", 0),
|
||||||
|
CreateChapter(mode == AnalysisMode.Introduction ? name : "Introduction", 60),
|
||||||
|
CreateChapter("Main Episode", 90),
|
||||||
|
CreateChapter(mode == AnalysisMode.Credits ? name : "Credits", 1890)
|
||||||
|
};
|
||||||
|
|
||||||
|
return new(new List<ChapterInfo>(chapters));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a ChapterInfo object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Chapter name.</param>
|
||||||
|
/// <param name="position">Chapter position (in seconds).</param>
|
||||||
|
/// <returns>ChapterInfo.</returns>
|
||||||
|
private ChapterInfo CreateChapter(string name, int position)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
StartPositionTicks = TimeSpan.FromSeconds(position).Ticks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chapter name analyzer.
|
||||||
|
/// </summary>
|
||||||
|
public class ChapterAnalyzer : IMediaFileAnalyzer
|
||||||
|
{
|
||||||
|
private ILogger<ChapterAnalyzer> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ChapterAnalyzer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger.</param>
|
||||||
|
public ChapterAnalyzer(ILogger<ChapterAnalyzer> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ReadOnlyCollection<QueuedEpisode> AnalyzeMediaFiles(
|
||||||
|
ReadOnlyCollection<QueuedEpisode> analysisQueue,
|
||||||
|
AnalysisMode mode,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var skippableRanges = new Dictionary<Guid, Intro>();
|
||||||
|
var expression = mode == AnalysisMode.Introduction ?
|
||||||
|
Plugin.Instance!.Configuration.ChapterAnalyzerIntroductionPattern :
|
||||||
|
Plugin.Instance!.Configuration.ChapterAnalyzerEndCreditsPattern;
|
||||||
|
|
||||||
|
foreach (var episode in analysisQueue)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var skipRange = FindMatchingChapter(
|
||||||
|
episode.EpisodeId,
|
||||||
|
episode.Duration,
|
||||||
|
new(Plugin.Instance!.GetChapters(episode.EpisodeId)),
|
||||||
|
expression,
|
||||||
|
mode);
|
||||||
|
|
||||||
|
if (skipRange is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
skippableRanges.Add(episode.EpisodeId, skipRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin.Instance!.UpdateTimestamps(skippableRanges, mode);
|
||||||
|
|
||||||
|
return analysisQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches a list of chapter names for one that matches the provided regular expression.
|
||||||
|
/// Only public to allow for unit testing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Item id.</param>
|
||||||
|
/// <param name="duration">Duration of media file in seconds.</param>
|
||||||
|
/// <param name="chapters">Media item chapters.</param>
|
||||||
|
/// <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(
|
||||||
|
Guid id,
|
||||||
|
int duration,
|
||||||
|
Collection<ChapterInfo> chapters,
|
||||||
|
string expression,
|
||||||
|
AnalysisMode mode)
|
||||||
|
{
|
||||||
|
Intro? matchingChapter = null;
|
||||||
|
|
||||||
|
var config = Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration();
|
||||||
|
var minDuration = config.MinimumIntroDuration;
|
||||||
|
int maxDuration = mode == AnalysisMode.Introduction ?
|
||||||
|
config.MaximumIntroDuration :
|
||||||
|
config.MaximumEpisodeCreditsDuration;
|
||||||
|
|
||||||
|
if (mode == AnalysisMode.Credits)
|
||||||
|
{
|
||||||
|
// Since the ending credits chapter may be the last chapter in the file, append a virtual
|
||||||
|
// chapter at the very end of the file.
|
||||||
|
chapters.Add(new ChapterInfo()
|
||||||
|
{
|
||||||
|
StartPositionTicks = TimeSpan.FromSeconds(duration).Ticks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all chapters
|
||||||
|
for (int i = 0; i < chapters.Count - 1; i++)
|
||||||
|
{
|
||||||
|
// Calculate chapter position and duration
|
||||||
|
var current = chapters[i];
|
||||||
|
var next = chapters[i + 1];
|
||||||
|
|
||||||
|
var currentRange = new TimeRange(
|
||||||
|
TimeSpan.FromTicks(current.StartPositionTicks).TotalSeconds,
|
||||||
|
TimeSpan.FromTicks(next.StartPositionTicks).TotalSeconds);
|
||||||
|
|
||||||
|
// Skip chapters with that don't have a name or are too short/long
|
||||||
|
if (string.IsNullOrEmpty(current.Name) ||
|
||||||
|
currentRange.Duration < minDuration ||
|
||||||
|
currentRange.Duration > maxDuration)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex.IsMatch() is used here in order to allow the runtime to cache the compiled regex
|
||||||
|
// between function invocations.
|
||||||
|
var match = Regex.IsMatch(
|
||||||
|
current.Name,
|
||||||
|
expression,
|
||||||
|
RegexOptions.None,
|
||||||
|
TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
if (!match)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingChapter = new Intro(id, currentRange);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingChapter;
|
||||||
|
}
|
||||||
|
}
|
@ -82,7 +82,6 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
|
|||||||
}
|
}
|
||||||
catch (FingerprintException ex)
|
catch (FingerprintException ex)
|
||||||
{
|
{
|
||||||
// TODO: FIXME: move to debug level?
|
|
||||||
_logger.LogWarning("Caught fingerprint error: {Ex}", ex);
|
_logger.LogWarning("Caught fingerprint error: {Ex}", ex);
|
||||||
|
|
||||||
// Fallback to an empty fingerprint on any error
|
// Fallback to an empty fingerprint on any error
|
||||||
|
@ -73,15 +73,27 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
public int MaximumIntroDuration { get; set; } = 120;
|
public int MaximumIntroDuration { get; set; } = 120;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the upper limit (in minutes) on the length of each episode's audio track that will be analyzed when searching for ending credits.
|
/// Gets or sets the upper limit (in seconds) on the length of each episode's audio track that will be analyzed when searching for ending credits.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaximumEpisodeCreditsDuration { get; set; } = 4;
|
public int MaximumEpisodeCreditsDuration { get; set; } = 240;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame.
|
/// Gets or sets the minimum percentage of a frame that must consist of black pixels before it is considered a black frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int BlackFrameMinimumPercentage { get; set; } = 85;
|
public int BlackFrameMinimumPercentage { get; set; } = 85;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the regular expression used to detect introduction chapters.
|
||||||
|
/// </summary>
|
||||||
|
public string ChapterAnalyzerIntroductionPattern { get; set; } =
|
||||||
|
@"(^|\s)(Intro|Introduction|OP|Opening)(\s|$)";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the regular expression used to detect ending credit chapters.
|
||||||
|
/// </summary>
|
||||||
|
public string ChapterAnalyzerEndCreditsPattern { get; set; } =
|
||||||
|
@"(^|\s)(Credits?|Ending)(\s|$)";
|
||||||
|
|
||||||
// ===== Playback settings =====
|
// ===== Playback settings =====
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -73,18 +73,24 @@ public class SkipIntroController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all introductions. Only used by the end to end testing script.
|
/// Get all introductions or credits. Only used by the end to end testing script.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">All introductions have been returned.</response>
|
/// <param name="mode">Mode.</param>
|
||||||
|
/// <response code="200">All timestamps have been returned.</response>
|
||||||
/// <returns>List of IntroWithMetadata objects.</returns>
|
/// <returns>List of IntroWithMetadata objects.</returns>
|
||||||
[Authorize(Policy = "RequiresElevation")]
|
[Authorize(Policy = "RequiresElevation")]
|
||||||
[HttpGet("Intros/All")]
|
[HttpGet("Intros/All")]
|
||||||
public ActionResult<List<IntroWithMetadata>> GetAllIntros()
|
public ActionResult<List<IntroWithMetadata>> GetAllTimestamps(
|
||||||
|
[FromQuery] AnalysisMode mode = AnalysisMode.Introduction)
|
||||||
{
|
{
|
||||||
List<IntroWithMetadata> intros = new();
|
List<IntroWithMetadata> intros = new();
|
||||||
|
|
||||||
|
var timestamps = mode == AnalysisMode.Introduction ?
|
||||||
|
Plugin.Instance!.Intros :
|
||||||
|
Plugin.Instance!.Credits;
|
||||||
|
|
||||||
// Get metadata for all intros
|
// Get metadata for all intros
|
||||||
foreach (var intro in Plugin.Instance!.Intros)
|
foreach (var intro in timestamps)
|
||||||
{
|
{
|
||||||
// Get the details of the item from Jellyfin
|
// Get the details of the item from Jellyfin
|
||||||
var rawItem = Plugin.Instance!.GetItem(intro.Key);
|
var rawItem = Plugin.Instance!.GetItem(intro.Key);
|
||||||
|
@ -8,6 +8,8 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -23,9 +25,10 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
private readonly object _introsLock = new();
|
private readonly object _introsLock = new();
|
||||||
private IXmlSerializer _xmlSerializer;
|
private IXmlSerializer _xmlSerializer;
|
||||||
private ILibraryManager _libraryManager;
|
private ILibraryManager _libraryManager;
|
||||||
|
private IItemRepository _itemRepository;
|
||||||
private ILogger<Plugin> _logger;
|
private ILogger<Plugin> _logger;
|
||||||
private string _introPath;
|
private string _introPath;
|
||||||
private string _creditsPath; // TODO: FIXME: remove this
|
private string _creditsPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
@ -34,12 +37,14 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||||
/// <param name="serverConfiguration">Server configuration manager.</param>
|
/// <param name="serverConfiguration">Server configuration manager.</param>
|
||||||
/// <param name="libraryManager">Library manager.</param>
|
/// <param name="libraryManager">Library manager.</param>
|
||||||
|
/// <param name="itemRepository">Item repository.</param>
|
||||||
/// <param name="logger">Logger.</param>
|
/// <param name="logger">Logger.</param>
|
||||||
public Plugin(
|
public Plugin(
|
||||||
IApplicationPaths applicationPaths,
|
IApplicationPaths applicationPaths,
|
||||||
IXmlSerializer xmlSerializer,
|
IXmlSerializer xmlSerializer,
|
||||||
IServerConfigurationManager serverConfiguration,
|
IServerConfigurationManager serverConfiguration,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
|
IItemRepository itemRepository,
|
||||||
ILogger<Plugin> logger)
|
ILogger<Plugin> logger)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
@ -47,14 +52,13 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
|
|
||||||
_xmlSerializer = xmlSerializer;
|
_xmlSerializer = xmlSerializer;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
_itemRepository = itemRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
FingerprintCachePath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "cache");
|
FingerprintCachePath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "cache");
|
||||||
FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
|
FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
|
||||||
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
|
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
|
||||||
|
_creditsPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "credits.xml");
|
||||||
// TODO: FIXME: remove this
|
|
||||||
_creditsPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "credits.csv");
|
|
||||||
|
|
||||||
// Create the base & cache directories (if needed).
|
// Create the base & cache directories (if needed).
|
||||||
if (!Directory.Exists(FingerprintCachePath))
|
if (!Directory.Exists(FingerprintCachePath))
|
||||||
@ -73,12 +77,6 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to load introduction timestamps: {Exception}", ex);
|
_logger.LogWarning("Unable to load introduction timestamps: {Exception}", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: FIXME: remove this
|
|
||||||
if (File.Exists(_creditsPath))
|
|
||||||
{
|
|
||||||
File.Delete(_creditsPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -91,6 +89,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<Guid, Intro> Intros { get; } = new();
|
public Dictionary<Guid, Intro> Intros { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all discovered ending credits.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<Guid, Intro> Credits { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the most recent media item queue.
|
/// Gets the most recent media item queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -131,12 +134,23 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
{
|
{
|
||||||
var introList = new List<Intro>();
|
var introList = new List<Intro>();
|
||||||
|
|
||||||
|
// Serialize intros
|
||||||
foreach (var intro in Plugin.Instance!.Intros)
|
foreach (var intro in Plugin.Instance!.Intros)
|
||||||
{
|
{
|
||||||
introList.Add(intro.Value);
|
introList.Add(intro.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
_xmlSerializer.SerializeToFile(introList, _introPath);
|
_xmlSerializer.SerializeToFile(introList, _introPath);
|
||||||
|
|
||||||
|
// Serialize credits
|
||||||
|
introList.Clear();
|
||||||
|
|
||||||
|
foreach (var intro in Plugin.Instance!.Credits)
|
||||||
|
{
|
||||||
|
introList.Add(intro.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_xmlSerializer.SerializeToFile(introList, _creditsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,13 +159,12 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RestoreTimestamps()
|
public void RestoreTimestamps()
|
||||||
{
|
{
|
||||||
if (!File.Exists(_introPath))
|
if (File.Exists(_introPath))
|
||||||
{
|
{
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since dictionaries can't be easily serialized, analysis results are stored on disk as a list.
|
// Since dictionaries can't be easily serialized, analysis results are stored on disk as a list.
|
||||||
var introList = (List<Intro>)_xmlSerializer.DeserializeFromFile(typeof(List<Intro>), _introPath);
|
var introList = (List<Intro>)_xmlSerializer.DeserializeFromFile(
|
||||||
|
typeof(List<Intro>),
|
||||||
|
_introPath);
|
||||||
|
|
||||||
foreach (var intro in introList)
|
foreach (var intro in introList)
|
||||||
{
|
{
|
||||||
@ -159,6 +172,19 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (File.Exists(_creditsPath))
|
||||||
|
{
|
||||||
|
var creditList = (List<Intro>)_xmlSerializer.DeserializeFromFile(
|
||||||
|
typeof(List<Intro>),
|
||||||
|
_creditsPath);
|
||||||
|
|
||||||
|
foreach (var credit in creditList)
|
||||||
|
{
|
||||||
|
Plugin.Instance!.Credits[credit.EpisodeId] = credit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal BaseItem GetItem(Guid id)
|
internal BaseItem GetItem(Guid id)
|
||||||
{
|
{
|
||||||
return _libraryManager.GetItemById(id);
|
return _libraryManager.GetItemById(id);
|
||||||
@ -174,53 +200,34 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
return GetItem(id).Path;
|
return GetItem(id).Path;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void UpdateTimestamps(Dictionary<Guid, Intro> newIntros, AnalysisMode mode)
|
/// <summary>
|
||||||
|
/// Gets all chapters for this item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Item id.</param>
|
||||||
|
/// <returns>List of chapters.</returns>
|
||||||
|
internal List<ChapterInfo> GetChapters(Guid id)
|
||||||
{
|
{
|
||||||
switch (mode)
|
return _itemRepository.GetChapters(GetItem(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UpdateTimestamps(Dictionary<Guid, Intro> newTimestamps, AnalysisMode mode)
|
||||||
{
|
{
|
||||||
case AnalysisMode.Introduction:
|
|
||||||
lock (_introsLock)
|
lock (_introsLock)
|
||||||
{
|
{
|
||||||
foreach (var intro in newIntros)
|
foreach (var intro in newTimestamps)
|
||||||
|
{
|
||||||
|
if (mode == AnalysisMode.Introduction)
|
||||||
{
|
{
|
||||||
Plugin.Instance!.Intros[intro.Key] = intro.Value;
|
Plugin.Instance!.Intros[intro.Key] = intro.Value;
|
||||||
}
|
}
|
||||||
|
else if (mode == AnalysisMode.Credits)
|
||||||
|
{
|
||||||
|
Plugin.Instance!.Credits[intro.Key] = intro.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Plugin.Instance!.SaveTimestamps();
|
Plugin.Instance!.SaveTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AnalysisMode.Credits:
|
|
||||||
// TODO: FIXME: implement properly
|
|
||||||
|
|
||||||
lock (_introsLock)
|
|
||||||
{
|
|
||||||
foreach (var credit in newIntros)
|
|
||||||
{
|
|
||||||
var item = GetItem(credit.Value.EpisodeId) as Episode;
|
|
||||||
if (item is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format: series, season number, episode number, title, start, end
|
|
||||||
var contents = string.Format(
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
|
||||||
"{0},{1},{2},{3},{4},{5}\n",
|
|
||||||
item.SeriesName.Replace(",", string.Empty, StringComparison.Ordinal),
|
|
||||||
item.AiredSeasonNumber ?? 0,
|
|
||||||
item.IndexNumber ?? 0,
|
|
||||||
item.Name.Replace(",", string.Empty, StringComparison.Ordinal),
|
|
||||||
Math.Round(credit.Value.IntroStart, 2),
|
|
||||||
Math.Round(credit.Value.IntroEnd, 2));
|
|
||||||
|
|
||||||
File.AppendAllText(_creditsPath, contents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -98,9 +98,6 @@ public class DetectCreditsTask : IScheduledTask
|
|||||||
MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism
|
MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: FIXME: if the queue is modified while the task is running, the task will fail.
|
|
||||||
// clone the queue before running the task to prevent this.
|
|
||||||
|
|
||||||
// Analyze all episodes in the queue using the degrees of parallelism the user specified.
|
// Analyze all episodes in the queue using the degrees of parallelism the user specified.
|
||||||
Parallel.ForEach(queue, options, (season) =>
|
Parallel.ForEach(queue, options, (season) =>
|
||||||
{
|
{
|
||||||
|
@ -252,6 +252,10 @@ public class DetectIntroductionsTask : IScheduledTask
|
|||||||
first.SeriesName,
|
first.SeriesName,
|
||||||
first.SeasonNumber);
|
first.SeasonNumber);
|
||||||
|
|
||||||
|
// Chapter analyzer
|
||||||
|
var chapter = new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>());
|
||||||
|
chapter.AnalyzeMediaFiles(episodes, AnalysisMode.Introduction, cancellationToken);
|
||||||
|
|
||||||
// Analyze the season with Chromaprint
|
// Analyze the season with Chromaprint
|
||||||
var chromaprint = new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>());
|
var chromaprint = new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>());
|
||||||
chromaprint.AnalyzeMediaFiles(episodes, AnalysisMode.Introduction, cancellationToken);
|
chromaprint.AnalyzeMediaFiles(episodes, AnalysisMode.Introduction, cancellationToken);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user