using System;
using System.Collections.Generic;
using System.Globalization;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.RecentlyAddedGuard.Core;
///
/// Maps Jellyfin items to snapshots for the core logic.
///
public sealed class IdentityResolver
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// The library manager.
/// The logger.
public IdentityResolver(ILibraryManager libraryManager, ILogger logger)
{
_libraryManager = libraryManager;
_logger = logger;
}
///
/// Checks whether the item is a real movie or episode: no missing (virtual) episode, no extra.
///
/// The item.
/// true if the plugin tracks items of this kind.
public static bool IsTracked(BaseItem item)
=> item is Movie or Episode && !item.IsVirtualItem && item.ExtraType is null;
///
/// Resolves the identity of a tracked item.
///
/// The item.
/// Optional cached series lookup (used by the synchronization).
/// The identity (its keys may be empty if provider ids are still missing),
/// or null if the item is not tracked or its kind is disabled.
public MediaIdentity? Resolve(BaseItem item, Func? seriesLookup = null)
{
ArgumentNullException.ThrowIfNull(item);
if (!IsTracked(item))
{
return null;
}
var configuration = Plugin.CurrentConfiguration;
switch (item)
{
case Movie movie when configuration.EnableMovies:
return new MediaIdentity(movie.Id, MediaKind.Movie, movie.DateCreated, MovieKeys(movie), MovieName(movie));
case Episode episode when configuration.EnableEpisodes:
var seriesId = FindSeriesId(episode);
var series = FindSeries(seriesId, seriesLookup);
return new MediaIdentity(
episode.Id,
MediaKind.Episode,
episode.DateCreated,
EpisodeKeys(episode, seriesId, series),
EpisodeName(episode, series));
default:
return null;
}
}
///
/// Resolves the keys of a removed item. The provider ids are still attached to the object,
/// but the series may already be gone, so every lookup is defensive.
///
/// The removed item.
/// The keys; empty if they cannot be determined.
public IReadOnlyList ResolveKeysForRemoval(BaseItem item)
{
ArgumentNullException.ThrowIfNull(item);
switch (item)
{
case Movie movie:
return MovieKeys(movie);
case Episode episode:
var seriesId = FindSeriesId(episode);
return EpisodeKeys(episode, seriesId, FindSeries(seriesId, null));
default:
return [];
}
}
private static IReadOnlyList MovieKeys(Movie movie)
=> MediaKeys.ForMovie(
movie.GetProviderId(MetadataProvider.Tmdb),
movie.GetProviderId(MetadataProvider.Imdb),
movie.GetProviderId(MetadataProvider.Tvdb));
private static IReadOnlyList EpisodeKeys(Episode episode, Guid seriesId, Series? series)
{
var part = MediaKeys.EpisodePart(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd);
if (part is null)
{
return [];
}
return MediaKeys.ForEpisode(
part,
series?.GetProviderId(MetadataProvider.Tvdb),
series?.GetProviderId(MetadataProvider.Tmdb),
series?.GetProviderId(MetadataProvider.Imdb),
seriesId);
}
private static string MovieName(Movie movie)
{
var name = string.IsNullOrWhiteSpace(movie.Name) ? movie.Id.ToString("N", CultureInfo.InvariantCulture) : movie.Name;
return movie.ProductionYear is { } year
? string.Create(CultureInfo.InvariantCulture, $"{name} ({year})")
: name;
}
private static string EpisodeName(Episode episode, Series? series)
{
var seriesName = episode.SeriesName ?? series?.Name ?? "?";
var number = string.Create(CultureInfo.InvariantCulture, $"S{episode.ParentIndexNumber ?? 0:00}E{episode.IndexNumber ?? 0:00}");
if (episode.IndexNumberEnd is { } end && end > (episode.IndexNumber ?? 0))
{
number += string.Create(CultureInfo.InvariantCulture, $"-E{end:00}");
}
return string.IsNullOrWhiteSpace(episode.Name)
? $"{seriesName} {number}"
: $"{seriesName} {number} – {episode.Name}";
}
#pragma warning disable CA1031 // Lookups on half-deleted items may fail in many ways; a missing key is acceptable.
private Guid FindSeriesId(Episode episode)
{
if (episode.SeriesId != Guid.Empty)
{
return episode.SeriesId;
}
try
{
return episode.FindSeriesId();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Serie von Episode {ItemId} nicht ermittelbar", episode.Id);
return Guid.Empty;
}
}
private Series? FindSeries(Guid seriesId, Func? seriesLookup)
{
if (seriesId == Guid.Empty)
{
return null;
}
try
{
return seriesLookup is not null ? seriesLookup(seriesId) : _libraryManager.GetItemById(seriesId) as Series;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Serie {SeriesId} nicht auffindbar", seriesId);
return null;
}
}
#pragma warning restore CA1031
}