Rework analysis queue
This commit is contained in:
parent
42b547e958
commit
cf1dc66970
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
156
ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
Normal file
156
ConfusedPolarBear.Plugin.IntroSkipper/QueueManager.cs
Normal 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++;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user