Rework analysis queue
This commit is contained in:
parent
42b547e958
commit
cf1dc66970
@ -64,8 +64,7 @@ public class TestAudioFingerprinting
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void TestIntroDetection()
|
public void TestIntroDetection()
|
||||||
{
|
{
|
||||||
var logger = new Logger<FingerprinterTask>(new LoggerFactory());
|
var task = new FingerprinterTask(new LoggerFactory());
|
||||||
var task = new FingerprinterTask(logger);
|
|
||||||
|
|
||||||
var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3");
|
var lhsEpisode = queueEpisode("audio/big_buck_bunny_intro.mp3");
|
||||||
var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3");
|
var rhsEpisode = queueEpisode("audio/big_buck_bunny_clip.mp3");
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Library;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||||
@ -22,8 +16,7 @@ public class Entrypoint : IServerEntryPoint
|
|||||||
private readonly IUserViewManager _userViewManager;
|
private readonly IUserViewManager _userViewManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ILogger<Entrypoint> _logger;
|
private readonly ILogger<Entrypoint> _logger;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly object _queueLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Entrypoint"/> class.
|
/// 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="userViewManager">User view manager.</param>
|
||||||
/// <param name="libraryManager">Library manager.</param>
|
/// <param name="libraryManager">Library manager.</param>
|
||||||
/// <param name="logger">Logger.</param>
|
/// <param name="logger">Logger.</param>
|
||||||
|
/// <param name="loggerFactory">Logger factory.</param>
|
||||||
public Entrypoint(
|
public Entrypoint(
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IUserViewManager userViewManager,
|
IUserViewManager userViewManager,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
ILogger<Entrypoint> logger)
|
ILogger<Entrypoint> logger,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_userViewManager = userViewManager;
|
_userViewManager = userViewManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -56,40 +52,15 @@ public class Entrypoint : IServerEntryPoint
|
|||||||
LogVersion();
|
LogVersion();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Assert that ffmpeg with chromaprint is installed
|
// TODO: when a new item is added to the server, immediately analyze the season it belongs to
|
||||||
if (!Chromaprint.CheckFFmpegVersion())
|
// instead of waiting for the next task interval. The task start should be debounced by a few seconds.
|
||||||
{
|
|
||||||
_logger.LogError("ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// As soon as a new episode is added, queue it for later analysis.
|
// Enqueue all episodes at startup so the fingerprint visualizer works before the task is started.
|
||||||
_libraryManager.ItemAdded += ItemAdded;
|
_logger.LogInformation("Running startup enqueue");
|
||||||
|
var queueManager = new QueueManager(_loggerFactory.CreateLogger<QueueManager>(), _libraryManager);
|
||||||
// For all TV show libraries, enqueue all contained items.
|
queueManager.EnqueueAllEpisodes();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -101,114 +72,6 @@ public class Entrypoint : IServerEntryPoint
|
|||||||
return Task.CompletedTask;
|
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
|
#if DEBUG
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logs the exact commit that created this version of the plugin. Only used in unstable builds.
|
/// 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;
|
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.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -47,6 +48,10 @@ public class FingerprinterTask : IScheduledTask
|
|||||||
|
|
||||||
private readonly ILogger<FingerprinterTask> _logger;
|
private readonly ILogger<FingerprinterTask> _logger;
|
||||||
|
|
||||||
|
private readonly ILogger<QueueManager> _queueLogger;
|
||||||
|
|
||||||
|
private readonly ILibraryManager? _libraryManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock which guards the fingerprint cache dictionary.
|
/// Lock which guards the fingerprint cache dictionary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -66,10 +71,22 @@ public class FingerprinterTask : IScheduledTask
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FingerprinterTask"/> class.
|
/// Initializes a new instance of the <see cref="FingerprinterTask"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger.</param>
|
/// <param name="loggerFactory">Logger factory.</param>
|
||||||
public FingerprinterTask(ILogger<FingerprinterTask> logger)
|
/// <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>>();
|
_fingerprintCache = new Dictionary<Guid, ReadOnlyCollection<uint>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,13 +111,22 @@ public class FingerprinterTask : IScheduledTask
|
|||||||
public string Key => "CPBIntroSkipperRunFingerprinter";
|
public string Key => "CPBIntroSkipperRunFingerprinter";
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="progress">Task progress.</param>
|
/// <param name="progress">Task progress.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
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;
|
var queue = Plugin.Instance!.AnalysisQueue;
|
||||||
|
|
||||||
if (queue.Count == 0)
|
if (queue.Count == 0)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user