267 lines
9.2 KiB
C#
Raw Normal View History

2022-06-22 22:03:34 -05:00
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
2022-06-22 22:03:34 -05:00
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
2024-04-20 12:58:29 +02:00
namespace ConfusedPolarBear.Plugin.IntroSkipper;
2022-06-22 22:03:34 -05:00
/// <summary>
/// Manages enqueuing library items for analysis.
/// </summary>
public class QueueManager
{
private ILibraryManager _libraryManager;
private ILogger<QueueManager> _logger;
2022-06-26 22:54:47 -05:00
private double analysisPercent;
private List<string> selectedLibraries;
private Dictionary<Guid, List<QueuedEpisode>> _queuedEpisodes;
2022-06-26 22:54:47 -05:00
2022-06-22 22:03:34 -05:00
/// <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;
2022-07-08 00:57:12 -05:00
selectedLibraries = new();
_queuedEpisodes = new();
2022-06-22 22:03:34 -05:00
}
/// <summary>
/// Gets all media items on the server.
2022-06-22 22:03:34 -05:00
/// </summary>
/// <returns>Queued media items.</returns>
public ReadOnlyDictionary<Guid, List<QueuedEpisode>> GetMediaItems()
2022-06-22 22:03:34 -05:00
{
2022-06-27 00:34:57 -05:00
Plugin.Instance!.TotalQueued = 0;
2022-06-22 22:03:34 -05:00
2022-07-08 00:57:12 -05:00
LoadAnalysisSettings();
// For all selected libraries, enqueue all contained episodes.
2022-06-22 22:03:34 -05:00
foreach (var folder in _libraryManager.GetVirtualFolders())
{
// If libraries have been selected for analysis, ensure this library was selected.
2022-07-08 00:57:12 -05:00
if (selectedLibraries.Count > 0 && !selectedLibraries.Contains(folder.Name))
{
2022-07-25 00:27:24 -05:00
_logger.LogDebug("Not analyzing library \"{Name}\": not selected by user", folder.Name);
continue;
}
2024-03-30 09:43:45 -04:00
_logger.LogInformation("Running enqueue of items in library {Name}", folder.Name);
2022-06-22 22:03:34 -05:00
try
{
foreach (var location in folder.Locations)
{
QueueLibraryContents(_libraryManager.FindByPath(location, true).Id);
}
2022-06-22 22:03:34 -05:00
}
catch (Exception ex)
{
_logger.LogError("Failed to enqueue items from library {Name}: {Exception}", folder.Name, ex);
}
}
2024-04-20 12:21:07 +02:00
Plugin.Instance.TotalSeasons = _queuedEpisodes.Count;
Plugin.Instance.QueuedMediaItems.Clear();
foreach (var kvp in _queuedEpisodes)
{
2024-04-20 12:21:07 +02:00
Plugin.Instance.QueuedMediaItems[kvp.Key] = kvp.Value;
}
return new(_queuedEpisodes);
2022-06-22 22:03:34 -05:00
}
2022-07-08 00:57:12 -05:00
/// <summary>
/// Loads the list of libraries which have been selected for analysis and the minimum intro duration.
/// Settings which have been modified from the defaults are logged.
/// </summary>
private void LoadAnalysisSettings()
{
var config = Plugin.Instance!.Configuration;
// Store the analysis percent
analysisPercent = Convert.ToDouble(config.AnalysisPercent) / 100;
// Get the list of library names which have been selected for analysis, ignoring whitespace and empty entries.
selectedLibraries = config.SelectedLibraries
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
// If any libraries have been selected for analysis, log their names.
if (selectedLibraries.Count > 0)
{
_logger.LogInformation("Limiting analysis to the following libraries: {Selected}", selectedLibraries);
}
else
{
_logger.LogDebug("Not limiting analysis by library name");
}
// If analysis settings have been changed from the default, log the modified settings.
if (config.AnalysisLengthLimit != 10 || config.AnalysisPercent != 25 || config.MinimumIntroDuration != 15)
{
_logger.LogInformation(
2024-03-30 09:43:45 -04:00
"Analysis settings have been changed to: {Percent}% / {Minutes}m and a minimum of {Minimum}s",
2022-07-08 00:57:12 -05:00
config.AnalysisPercent,
config.AnalysisLengthLimit,
config.MinimumIntroDuration);
}
}
private void QueueLibraryContents(Guid id)
2022-06-22 22:03:34 -05:00
{
_logger.LogDebug("Constructing anonymous internal query");
2024-04-20 12:58:29 +02:00
var query = new InternalItemsQuery
2022-06-22 22:03:34 -05:00
{
// Order by series name, season, and then episode number so that status updates are logged in order
ParentId = id,
2024-04-20 12:37:20 +02:00
OrderBy = new[] { (ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending), },
2024-03-22 23:41:58 +01:00
IncludeItemTypes = [BaseItemKind.Episode],
2022-06-22 22:03:34 -05:00
Recursive = true,
2022-09-10 02:24:44 -05:00
IsVirtualItem = false
2022-06-22 22:03:34 -05:00
};
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.LogDebug("Item {Name} is not an episode", item.Name);
2022-06-22 22:03:34 -05:00
continue;
}
if (Plugin.Instance!.Configuration.PathRestrictions.Count > 0)
{
2024-04-20 12:21:07 +02:00
if (!Plugin.Instance.Configuration.PathRestrictions.Contains(item.ContainingFolderPath))
{
continue;
}
}
2022-06-22 22:03:34 -05:00
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))
{
2022-09-10 02:24:44 -05:00
_logger.LogWarning(
"Not queuing episode \"{Name}\" from series \"{Series}\" ({Id}) as no path was provided by Jellyfin",
episode.Name,
episode.SeriesName,
episode.Id);
2022-06-22 22:03:34 -05:00
return;
}
2022-06-26 22:54:47 -05:00
// Limit analysis to the first X% of the episode and at most Y minutes.
// X and Y default to 25% and 10 minutes.
2022-06-22 22:03:34 -05:00
var duration = TimeSpan.FromTicks(episode.RunTimeTicks ?? 0).TotalSeconds;
2022-10-31 01:00:39 -05:00
var fingerprintDuration = duration;
if (fingerprintDuration >= 5 * 60)
2022-06-22 22:03:34 -05:00
{
2022-10-31 01:00:39 -05:00
fingerprintDuration *= analysisPercent;
2022-06-22 22:03:34 -05:00
}
2022-10-31 01:00:39 -05:00
fingerprintDuration = Math.Min(
fingerprintDuration,
2024-04-20 12:21:07 +02:00
60 * Plugin.Instance.Configuration.AnalysisLengthLimit);
2022-06-22 22:03:34 -05:00
2022-07-03 02:47:48 -05:00
// Allocate a new list for each new season
_queuedEpisodes.TryAdd(episode.SeasonId, new List<QueuedEpisode>());
2022-07-03 02:47:48 -05:00
// Queue the episode for analysis
2024-04-20 12:21:07 +02:00
var maxCreditsDuration = Plugin.Instance.Configuration.MaximumCreditsDuration;
2024-04-20 12:58:29 +02:00
_queuedEpisodes[episode.SeasonId].Add(new QueuedEpisode
2022-06-22 22:03:34 -05:00
{
SeriesName = episode.SeriesName,
SeasonNumber = episode.AiredSeasonNumber ?? 0,
EpisodeId = episode.Id,
Name = episode.Name,
Path = episode.Path,
2022-10-31 01:00:39 -05:00
Duration = Convert.ToInt32(duration),
IntroFingerprintEnd = Convert.ToInt32(fingerprintDuration),
CreditsFingerprintStart = Convert.ToInt32(duration - maxCreditsDuration),
2022-06-22 22:03:34 -05:00
});
2024-04-20 12:21:07 +02:00
Plugin.Instance.TotalQueued++;
2022-06-22 22:03:34 -05:00
}
/// <summary>
/// Verify that a collection of queued media items still exist in Jellyfin and in storage.
/// This is done to ensure that we don't analyze items that were deleted between the call to GetMediaItems() and popping them from the queue.
/// </summary>
/// <param name="candidates">Queued media items.</param>
/// <param name="mode">Analysis mode.</param>
/// <returns>Media items that have been verified to exist in Jellyfin and in storage.</returns>
public (ReadOnlyCollection<QueuedEpisode> VerifiedItems, bool AnyUnanalyzed)
VerifyQueue(ReadOnlyCollection<QueuedEpisode> candidates, AnalysisMode mode)
{
var unanalyzed = false;
var verified = new List<QueuedEpisode>();
var timestamps = mode == AnalysisMode.Introduction ?
Plugin.Instance!.Intros :
Plugin.Instance!.Credits;
foreach (var candidate in candidates)
{
try
{
2024-04-20 12:21:07 +02:00
var path = Plugin.Instance.GetItemPath(candidate.EpisodeId);
if (File.Exists(path))
{
verified.Add(candidate);
}
if (!timestamps.ContainsKey(candidate.EpisodeId))
{
unanalyzed = true;
}
}
catch (Exception ex)
{
_logger.LogDebug(
"Skipping {Mode} analysis of {Name} ({Id}): {Exception}",
mode,
candidate.Name,
candidate.EpisodeId,
ex);
}
}
return (verified.AsReadOnly(), unanalyzed);
}
2022-06-22 22:03:34 -05:00
}