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 }