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;
///
/// Server entrypoint.
///
public class Entrypoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
private readonly object _queueLock = new object();
///
/// Initializes a new instance of the class.
///
/// User manager.
/// User view manager.
/// Library manager.
/// Logger.
public Entrypoint(
IUserManager userManager,
IUserViewManager userViewManager,
ILibraryManager libraryManager,
ILogger logger)
{
_userManager = userManager;
_userViewManager = userViewManager;
_libraryManager = libraryManager;
_logger = logger;
}
///
/// Registers event handler.
///
/// Task.
public Task RunAsync()
{
Chromaprint.Logger = _logger;
#if DEBUG
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;
}
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);
}
}
}
catch (Exception ex)
{
_logger.LogError("Unable to run startup enqueue: {Exception}", ex);
}
_logger.LogDebug("Total enqueued seasons: {Count}", Plugin.Instance!.AnalysisQueue.Count);
return Task.CompletedTask;
}
private void QueueLibraryContents(string rawId)
{
_logger.LogDebug("Constructing anonymous internal query");
var query = new InternalItemsQuery()
{
ParentId = Guid.Parse(rawId),
OrderBy = new[] { ("SortName", 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);
}
///
/// Called when an item is added to the server.
///
/// Sender.
/// ItemChangeEventArgs.
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");
}
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();
}
// 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
///
/// Logs the exact commit that created this version of the plugin. Only used in unstable builds.
///
private void LogVersion()
{
var assembly = GetType().Assembly;
var path = GetType().Namespace + ".Configuration.version.txt";
using (var stream = assembly.GetManifestResourceStream(path))
{
if (stream is null)
{
_logger.LogWarning("Unable to read embedded version information");
return;
}
var version = string.Empty;
using (var reader = new StreamReader(stream))
{
version = reader.ReadToEnd().TrimEnd();
}
if (version == "unknown")
{
_logger.LogTrace("Embedded version information was not valid, ignoring");
return;
}
_logger.LogInformation("Unstable version built from commit {Version}", version);
}
}
#endif
///
/// Dispose.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Protected dispose.
///
/// Dispose.
protected virtual void Dispose(bool dispose)
{
if (!dispose)
{
return;
}
_libraryManager.ItemAdded -= ItemAdded;
}
}