Rework analysis queue

This commit is contained in:
ConfusedPolarBear 2022-06-22 22:03:34 -05:00
parent 42b547e958
commit cf1dc66970
4 changed files with 198 additions and 156 deletions

View File

@ -64,8 +64,7 @@ public class TestAudioFingerprinting
[Fact]
public void TestIntroDetection()
{
var logger = new Logger<FingerprinterTask>(new LoggerFactory());
var task = new FingerprinterTask(logger);
var task = new FingerprinterTask(new LoggerFactory());
var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3");
var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3");

View File

@ -1,14 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library;
using Microsoft.Extensions.Logging;
namespace ConfusedPolarBear.Plugin.IntroSkipper;
@ -22,8 +16,7 @@ public class Entrypoint : IServerEntryPoint
private readonly IUserViewManager _userViewManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<Entrypoint> _logger;
private readonly object _queueLock = new object();
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes a new instance of the <see cref="Entrypoint"/> class.
@ -32,16 +25,19 @@ public class Entrypoint : IServerEntryPoint
/// <param name="userViewManager">User view manager.</param>
/// <param name="libraryManager">Library manager.</param>
/// <param name="logger">Logger.</param>
/// <param name="loggerFactory">Logger factory.</param>
public Entrypoint(
IUserManager userManager,
IUserViewManager userViewManager,
ILibraryManager libraryManager,
ILogger<Entrypoint> logger)
ILogger<Entrypoint> logger,
ILoggerFactory loggerFactory)
{
_userManager = userManager;
_userViewManager = userViewManager;
_libraryManager = libraryManager;
_logger = logger;
_loggerFactory = loggerFactory;
}
/// <summary>
@ -56,40 +52,15 @@ public class Entrypoint : IServerEntryPoint
LogVersion();
#endif
// Assert that ffmpeg with chromaprint is installed
if (!Chromaprint.CheckFFmpegVersion())
{
_logger.LogError("ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed");
return Task.CompletedTask;
}
// TODO: when a new item is added to the server, immediately analyze the season it belongs to
// instead of waiting for the next task interval. The task start should be debounced by a few seconds.
try
{
// As soon as a new episode is added, queue it for later analysis.
_libraryManager.ItemAdded += ItemAdded;
// For all TV show libraries, enqueue all contained items.
foreach (var folder in _libraryManager.GetVirtualFolders())
{
if (folder.CollectionType != CollectionTypeOptions.TvShows)
{
continue;
}
_logger.LogInformation(
"Running startup enqueue of items in library {Name} ({ItemId})",
folder.Name,
folder.ItemId);
try
{
QueueLibraryContents(folder.ItemId);
}
catch (Exception ex)
{
_logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex);
}
}
// Enqueue all episodes at startup so the fingerprint visualizer works before the task is started.
_logger.LogInformation("Running startup enqueue");
var queueManager = new QueueManager(_loggerFactory.CreateLogger<QueueManager>(), _libraryManager);
queueManager.EnqueueAllEpisodes();
}
catch (Exception ex)
{
@ -101,114 +72,6 @@ public class Entrypoint : IServerEntryPoint
return Task.CompletedTask;
}
private void QueueLibraryContents(string rawId)
{
_logger.LogDebug("Constructing anonymous internal query");
var query = new InternalItemsQuery()
{
// Order by series name, season, and then episode number so that status updates are logged in order
ParentId = Guid.Parse(rawId),
OrderBy = new[]
{
("SeriesSortName", SortOrder.Ascending),
("ParentIndexNumber", SortOrder.Ascending),
("IndexNumber", SortOrder.Ascending),
},
IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode },
Recursive = true,
};
_logger.LogDebug("Getting items");
var items = _libraryManager.GetItemList(query, false);
if (items is null)
{
_logger.LogError("Library query result is null");
return;
}
// Queue all episodes on the server for fingerprinting.
_logger.LogDebug("Iterating through library items");
foreach (var item in items)
{
if (item is not Episode episode)
{
_logger.LogError("Item {Name} is not an episode", item.Name);
continue;
}
QueueEpisode(episode);
}
_logger.LogDebug("Queued {Count} episodes", items.Count);
}
/// <summary>
/// Called when an item is added to the server.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">ItemChangeEventArgs.</param>
private void ItemAdded(object? sender, ItemChangeEventArgs e)
{
if (e.Item is not Episode episode)
{
return;
}
_logger.LogDebug("Queuing fingerprint of new episode {Name}", episode.Name);
QueueEpisode(episode);
}
private void QueueEpisode(Episode episode)
{
if (Plugin.Instance is null)
{
throw new InvalidOperationException("plugin instance was null");
}
if (string.IsNullOrEmpty(episode.Path))
{
_logger.LogWarning("Not queuing episode {Id} as no path was provided by Jellyfin", episode.Id);
return;
}
lock (_queueLock)
{
var queue = Plugin.Instance.AnalysisQueue;
// Allocate a new list for each new season
if (!queue.ContainsKey(episode.SeasonId))
{
Plugin.Instance.AnalysisQueue[episode.SeasonId] = new List<QueuedEpisode>();
}
// Only fingerprint up to 25% of the episode and at most 10 minutes.
var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds;
if (duration >= 5 * 60)
{
duration /= 4;
}
duration = Math.Min(duration, 10 * 60);
Plugin.Instance.AnalysisQueue[episode.SeasonId].Add(new QueuedEpisode()
{
SeriesName = episode.SeriesName,
SeasonNumber = episode.AiredSeasonNumber ?? 0,
EpisodeId = episode.Id,
Name = episode.Name,
Path = episode.Path,
FingerprintDuration = Convert.ToInt32(duration)
});
Plugin.Instance!.TotalQueued++;
}
}
#if DEBUG
/// <summary>
/// Logs the exact commit that created this version of the plugin. Only used in unstable builds.
@ -262,7 +125,5 @@ public class Entrypoint : IServerEntryPoint
{
return;
}
_libraryManager.ItemAdded -= ItemAdded;
}
}

View File

@ -0,0 +1,156 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper;
using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
/// <summary>
/// Manages enqueuing library items for analysis.
/// </summary>
public class QueueManager
{
private ILibraryManager _libraryManager;
private ILogger<QueueManager> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="QueueManager"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="libraryManager">Library manager.</param>
public QueueManager(ILogger<QueueManager> logger, ILibraryManager libraryManager)
{
_logger = logger;
_libraryManager = libraryManager;
}
/// <summary>
/// Iterates through all libraries on the server and queues all episodes for analysis.
/// </summary>
public void EnqueueAllEpisodes()
{
// Assert that ffmpeg with chromaprint is installed
if (!Chromaprint.CheckFFmpegVersion())
{
throw new FingerprintException(
"ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed");
}
Plugin.Instance!.AnalysisQueue.Clear();
// For all TV show libraries, enqueue all contained items.
foreach (var folder in _libraryManager.GetVirtualFolders())
{
if (folder.CollectionType != CollectionTypeOptions.TvShows)
{
continue;
}
_logger.LogInformation(
"Running enqueue of items in library {Name} ({ItemId})",
folder.Name,
folder.ItemId);
try
{
QueueLibraryContents(folder.ItemId);
}
catch (Exception ex)
{
_logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex);
}
}
}
private void QueueLibraryContents(string rawId)
{
_logger.LogDebug("Constructing anonymous internal query");
var query = new InternalItemsQuery()
{
// Order by series name, season, and then episode number so that status updates are logged in order
ParentId = Guid.Parse(rawId),
OrderBy = new[]
{
("SeriesSortName", SortOrder.Ascending),
("ParentIndexNumber", SortOrder.Ascending),
("IndexNumber", SortOrder.Ascending),
},
IncludeItemTypes = new BaseItemKind[] { BaseItemKind.Episode },
Recursive = true,
};
_logger.LogDebug("Getting items");
var items = _libraryManager.GetItemList(query, false);
if (items is null)
{
_logger.LogError("Library query result is null");
return;
}
// Queue all episodes on the server for fingerprinting.
_logger.LogDebug("Iterating through library items");
foreach (var item in items)
{
if (item is not Episode episode)
{
_logger.LogError("Item {Name} is not an episode", item.Name);
continue;
}
QueueEpisode(episode);
}
_logger.LogDebug("Queued {Count} episodes", items.Count);
}
private void QueueEpisode(Episode episode)
{
if (Plugin.Instance is null)
{
throw new InvalidOperationException("plugin instance was null");
}
if (string.IsNullOrEmpty(episode.Path))
{
_logger.LogWarning("Not queuing episode {Id} as no path was provided by Jellyfin", episode.Id);
return;
}
var queue = Plugin.Instance.AnalysisQueue;
// Allocate a new list for each new season
if (!queue.ContainsKey(episode.SeasonId))
{
Plugin.Instance.AnalysisQueue[episode.SeasonId] = new List<QueuedEpisode>();
}
// Only fingerprint up to 25% of the episode and at most 10 minutes.
var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds;
if (duration >= 5 * 60)
{
duration /= 4;
}
duration = Math.Min(duration, 10 * 60);
Plugin.Instance.AnalysisQueue[episode.SeasonId].Add(new QueuedEpisode()
{
SeriesName = episode.SeriesName,
SeasonNumber = episode.AiredSeasonNumber ?? 0,
EpisodeId = episode.Id,
Name = episode.Name,
Path = episode.Path,
FingerprintDuration = Convert.ToInt32(duration)
});
Plugin.Instance!.TotalQueued++;
}
}

View File

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@ -47,6 +48,10 @@ public class FingerprinterTask : IScheduledTask
private readonly ILogger<FingerprinterTask> _logger;
private readonly ILogger<QueueManager> _queueLogger;
private readonly ILibraryManager? _libraryManager;
/// <summary>
/// Lock which guards the fingerprint cache dictionary.
/// </summary>
@ -66,10 +71,22 @@ public class FingerprinterTask : IScheduledTask
/// <summary>
/// Initializes a new instance of the <see cref="FingerprinterTask"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
public FingerprinterTask(ILogger<FingerprinterTask> logger)
/// <param name="loggerFactory">Logger factory.</param>
/// <param name="libraryManager">Library manager.</param>
public FingerprinterTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager) : this(loggerFactory)
{
_logger = logger;
_libraryManager = libraryManager;
}
/// <summary>
/// Initializes a new instance of the <see cref="FingerprinterTask"/> class.
/// </summary>
/// <param name="loggerFactory">Logger factory.</param>
public FingerprinterTask(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<FingerprinterTask>();
_queueLogger = loggerFactory.CreateLogger<QueueManager>();
_fingerprintCache = new Dictionary<Guid, ReadOnlyCollection<uint>>();
}
@ -94,13 +111,22 @@ public class FingerprinterTask : IScheduledTask
public string Key => "CPBIntroSkipperRunFingerprinter";
/// <summary>
/// Analyze all episodes in the queue.
/// Analyze all episodes in the queue. Only one instance of this task should be run at a time.
/// </summary>
/// <param name="progress">Task progress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task.</returns>
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
if (_libraryManager is null)
{
throw new InvalidOperationException("Library manager must not be null");
}
// Make sure the analysis queue matches what's currently in Jellyfin.
var queueManager = new QueueManager(_queueLogger, _libraryManager);
queueManager.EnqueueAllEpisodes();
var queue = Plugin.Instance!.AnalysisQueue;
if (queue.Count == 0)