Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix duplicate entries Missing and not Missing #106

Merged
merged 11 commits into from
Feb 27, 2024
7 changes: 4 additions & 3 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, C
if (string.IsNullOrEmpty(episodeTvdbId))
{
_logger.LogError(
"Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}:{Name}",
"Episode S{Season:00}E{Episode:00} not found for series {SeriesTvdbId}:{Name}",
searchInfo.ParentIndexNumber,
searchInfo.IndexNumber,
seriesTvdbId,
Expand Down Expand Up @@ -212,8 +212,9 @@ private static MetadataResult<Episode> MapEpisodeToResult(EpisodeInfo id, Episod
AirsBeforeSeasonNumber = episode.AirsBeforeSeason,
// Tvdb uses 3 letter code for language (prob ISO 639-2)
// Reverts to OriginalName if no translation is found
Name = episode.Translations.NameTranslations.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(id.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Name ?? episode.Name,
Overview = episode.Translations.OverviewTranslations?.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(id.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Overview ?? episode.Overview
Name = episode.Translations.GetTranslatedNamedOrDefault(id.MetadataLanguage) ?? episode.Name,
Overview = episode.Translations.GetTranslatedOverviewOrDefault(id.MetadataLanguage) ?? episode.Overview,
OriginalTitle = episode.Name,
}
};
result.ResetPeople();
Expand Down
102 changes: 75 additions & 27 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbMissingEpisodeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Libraries;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
Expand Down Expand Up @@ -99,9 +97,20 @@ protected virtual void Dispose(bool disposing)

private static bool EpisodeExists(EpisodeBaseRecord episodeRecord, IReadOnlyList<Episode> existingEpisodes)
{
return existingEpisodes.Any(ep => ep.ContainsEpisodeNumber(episodeRecord.Number) && ep.ParentIndexNumber == episodeRecord.Number);
return existingEpisodes.Any(episode => EpisodeEquals(episode, episodeRecord));
}

private static bool EpisodeEquals(Episode episode, EpisodeBaseRecord otherEpisodeRecord)
{
return episode.ContainsEpisodeNumber(otherEpisodeRecord.Number)
&& episode.ParentIndexNumber == otherEpisodeRecord.SeasonNumber;
}

/// <summary>
/// Is Metadata fetcher enabled for Series, Season or Episode.
/// </summary>
/// <param name="item">Series, Season or Episode.</param>
/// <returns>true if enabled.</returns>
private bool IsEnabledForLibrary(BaseItem item)
{
Series? series = item switch
Expand All @@ -113,6 +122,7 @@ private bool IsEnabledForLibrary(BaseItem item)

if (series == null)
{
_logger.LogDebug("Given input is not in {@ValidTypes}: {Type}", new[] { nameof(Series), nameof(Season), nameof(Episode) }, item.GetType());
return false;
}

Expand All @@ -125,16 +135,20 @@ private void OnProviderManagerRefreshComplete(object? sender, GenericEventArgs<B
{
if (!IsEnabledForLibrary(genericEventArgs.Argument))
{
_logger.LogDebug("{ProviderName} not enabled for {InputName}", ProviderName, genericEventArgs.Argument.Name);
return;
}

_logger.LogDebug("{MethodName}: Try Refreshing for Item {Name} {Type}", nameof(OnProviderManagerRefreshComplete), genericEventArgs.Argument.Name, genericEventArgs.Argument.GetType());
if (genericEventArgs.Argument is Series series)
{
_logger.LogDebug("{MethodName}: Refreshing Series {SeriesName}", nameof(OnProviderManagerRefreshComplete), series.Name);
HandleSeries(series).GetAwaiter().GetResult();
}

if (genericEventArgs.Argument is Season season)
{
_logger.LogDebug("{MethodName}: Refreshing {SeriesName} {SeasonName}", nameof(OnProviderManagerRefreshComplete), season.Series?.Name, season.Name);
HandleSeason(season).GetAwaiter().GetResult();
}
}
Expand All @@ -143,6 +157,7 @@ private async Task HandleSeries(Series series)
{
if (!series.TryGetProviderId(MetadataProvider.Tvdb.ToString(), out var tvdbIdTxt))
{
_logger.LogDebug("No TVDB Id available.");
return;
}

Expand Down Expand Up @@ -190,62 +205,88 @@ private async Task HandleSeason(Season season)
if (season.Series == null
|| !season.Series.TryGetProviderId(MetadataProvider.Tvdb.ToString(), out var tvdbIdTxt))
{
_logger.LogDebug("No TVDB Id available.");
return;
}

var tvdbId = Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture);
var allEpisodes = await GetAllEpisodes(tvdbId, season.GetPreferredMetadataLanguage()).ConfigureAwait(false);
var allEpisodes = await GetAllEpisodes(tvdbId, season.GetPreferredMetadataLanguage())
.ConfigureAwait(false);

var existingEpisodes = season.Children.OfType<Episode>().ToList();
var seasonEpisodes = allEpisodes.Where(e => e.SeasonNumber == season.IndexNumber).ToList();
var existingEpisodes = season.GetEpisodes().OfType<Episode>().ToHashSet();

for (var i = 0; i < allEpisodes.Count; i++)
foreach (var episodeRecord in seasonEpisodes)
{
var episode = allEpisodes[i];
if (EpisodeExists(episode, existingEpisodes))
var foundEpisodes = existingEpisodes.Where(episode => EpisodeEquals(episode, episodeRecord)).ToList();
if (foundEpisodes.Any())
{
// So we have at least one existing episode for our episodeRecord
var physicalEpisodes = foundEpisodes.Where(e => !e.IsVirtualItem);
if (physicalEpisodes.Any())
{
// if there is a physical episode we can delete existing virtual episode entries
var virtualEpisodes = foundEpisodes.Where(e => e.IsVirtualItem).ToList();
DeleteVirtualItems(virtualEpisodes);
existingEpisodes.ExceptWith(virtualEpisodes);
}

continue;
}

AddVirtualEpisode(episode, season);
AddVirtualEpisode(episodeRecord, season);
}

var orphanedEpisodes = existingEpisodes
.Where(e => e.IsVirtualItem)
.Where(e => !seasonEpisodes.Any(episodeRecord => EpisodeEquals(e, episodeRecord)))
.ToList();
DeleteVirtualItems(orphanedEpisodes);
}

private void OnLibraryManagerItemUpdated(object? sender, ItemChangeEventArgs itemChangeEventArgs)
{
_logger.LogDebug("{MethodName}: Refreshing Item {ItemName} [{Reason}]", nameof(OnLibraryManagerItemUpdated), itemChangeEventArgs.Item.Name, itemChangeEventArgs.UpdateReason);
// Only interested in real Season and Episode items
if (itemChangeEventArgs.Item.IsVirtualItem
|| !(itemChangeEventArgs.Item is Season || itemChangeEventArgs.Item is Episode))
{
_logger.LogDebug("Skip: Updated item is {ItemType}.", itemChangeEventArgs.Item.IsVirtualItem ? "Virtual" : "no Season or Episode");
return;
}

if (!IsEnabledForLibrary(itemChangeEventArgs.Item))
{
_logger.LogDebug("{ProviderName} not enabled for {InputName}", ProviderName, itemChangeEventArgs.Item.Name);
return;
}

var indexNumber = itemChangeEventArgs.Item.IndexNumber;

// If the item is an Episode, filter on ParentIndexNumber as well (season number)
int? parentIndexNumber = null;
if (itemChangeEventArgs.Item is Episode)
{
parentIndexNumber = itemChangeEventArgs.Item.ParentIndexNumber;
}
var existingVirtualItems = GetVirtualItems(itemChangeEventArgs.Item, itemChangeEventArgs.Parent);
DeleteVirtualItems(existingVirtualItems);
}

private List<BaseItem> GetVirtualItems(BaseItem item, BaseItem? parent)
{
var query = new InternalItemsQuery
{
IsVirtualItem = true,
IndexNumber = indexNumber,
ParentIndexNumber = parentIndexNumber,
IncludeItemTypes = new[] { itemChangeEventArgs.Item.GetBaseItemKind() },
Parent = itemChangeEventArgs.Parent,
IndexNumber = item.IndexNumber,
// If the item is an Episode, filter on ParentIndexNumber as well (season number)
ParentIndexNumber = item is Episode ? item.ParentIndexNumber : null,
IncludeItemTypes = new[] { item.GetBaseItemKind() },
Parent = parent,
Recursive = true,
GroupByPresentationUniqueKey = false,
DtoOptions = new DtoOptions(true)
};

var existingVirtualItems = _libraryManager.GetItemList(query);
return existingVirtualItems;
}

private void DeleteVirtualItems<T>(List<T> existingVirtualItems)
where T : BaseItem
{
var deleteOptions = new DeleteOptions
{
DeleteFileLocation = true
Expand All @@ -254,16 +295,20 @@ private void OnLibraryManagerItemUpdated(object? sender, ItemChangeEventArgs ite
// Remove the virtual season/episode that matches the newly updated item
for (var i = 0; i < existingVirtualItems.Count; i++)
{
_libraryManager.DeleteItem(existingVirtualItems[i], deleteOptions);
var currentItem = existingVirtualItems[i];
_logger.LogDebug("Delete VirtualItem {Name} - S{Season:00}E{Episode:00}", currentItem.Name, currentItem.ParentIndexNumber, currentItem.IndexNumber);
_libraryManager.DeleteItem(currentItem, deleteOptions);
}
}

// TODO use async events
private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs)
{
_logger.LogDebug("{MethodName}: Refreshing {ItemName} [{Reason}]", nameof(OnLibraryManagerItemRemoved), itemChangeEventArgs.Item.Name, itemChangeEventArgs.UpdateReason);
// No action needed if the item is virtual
if (itemChangeEventArgs.Item.IsVirtualItem || !IsEnabledForLibrary(itemChangeEventArgs.Item))
{
_logger.LogDebug("Skip: {Message}.", itemChangeEventArgs.Item.IsVirtualItem ? "Updated item is Virtual" : "Update not enabled");
return;
}

Expand All @@ -279,6 +324,7 @@ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs ite
if (episode.Series == null
|| !episode.Series.TryGetProviderId(MetadataProvider.Tvdb.ToString(), out var tvdbIdTxt))
{
_logger.LogDebug("No TVDB Id available.");
return;
}

Expand All @@ -289,7 +335,7 @@ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs ite
EpisodeBaseRecord? episodeRecord = null;
if (episodeRecords.Count > 0)
{
episodeRecord = episodeRecords[0];
episodeRecord = episodeRecords.FirstOrDefault(e => EpisodeEquals(episode, e));
}

AddVirtualEpisode(episodeRecord, episode.Season);
Expand All @@ -309,11 +355,12 @@ private async Task<IReadOnlyList<EpisodeBaseRecord>> GetAllEpisodes(int tvdbId,
return Array.Empty<EpisodeBaseRecord>();
}

_logger.LogDebug("{MethodName}: For TVDB Id '{TvdbId}' found #{Count} [{Episodes}]", nameof(GetAllEpisodes), tvdbId, allEpisodes.Count, string.Join(", ", allEpisodes.Select(e => $"S{e.SeasonNumber}E{e.Number}")));
return allEpisodes;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to get episodes from TVDB");
_logger.LogWarning(ex, "Unable to get episodes from TVDB for Id '{TvdbId}'", tvdbId);
return Array.Empty<EpisodeBaseRecord>();
}
}
Expand Down Expand Up @@ -341,6 +388,7 @@ private void AddMissingEpisodes(
if (existingEpisodes.TryGetValue(episodeRecord.SeasonNumber, out var episodes)
&& EpisodeExists(episodeRecord, episodes))
{
_logger.LogDebug("{MethodName}: Skip, already existing S{Season:00}E{Episode:00}", nameof(AddMissingEpisodes), episodeRecord.SeasonNumber, episodeRecord.Number);
continue;
}

Expand Down Expand Up @@ -387,15 +435,15 @@ private Season AddVirtualSeason(int season, Series series)

private void AddVirtualEpisode(EpisodeBaseRecord? episode, Season? season)
{
if (season == null)
if (episode == null || season == null)
{
return;
}

// Put as much metadata into it as possible
var newEpisode = new Episode
{
Name = episode!.Name,
Name = episode.Name,
IndexNumber = episode.Number,
ParentIndexNumber = episode.SeasonNumber,
Id = _libraryManager.GetNewItemId(
Expand All @@ -422,7 +470,7 @@ private void AddVirtualEpisode(EpisodeBaseRecord? episode, Season? season)
newEpisode.SetProviderId(MetadataProvider.Tvdb, episode.Id.ToString(CultureInfo.InvariantCulture));

_logger.LogDebug(
"Creating virtual episode {0} {1}x{2}",
"Creating virtual episode {SeriesName} S{Season:00}E{Episode:00}",
season.Series.Name,
episode.SeasonNumber,
episode.Number);
Expand Down
8 changes: 4 additions & 4 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ await _tvdbClientManager
.ConfigureAwait(false);
var resultData = result;

if (resultData == null || resultData.Count == 0)
if (resultData is null || resultData.Count == 0 || resultData[0] is null || resultData[0].Series is null)
{
_logger.LogWarning("TvdbSearch: No series found for id: {0}", id);
return null;
Expand All @@ -291,7 +291,7 @@ await _tvdbClientManager
/// <returns>Task{System.String}.</returns>
private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
{
_logger.LogDebug("TvdbSearch: Finding id for item: {0} ({1})", name, year);
_logger.LogDebug("TvdbSearch: Finding id for item: {Name} ({Year})", name, year);
var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false);

return results.Where(i =>
Expand Down Expand Up @@ -467,8 +467,8 @@ private static void MapSeriesToResult(MetadataResult<Series> result, SeriesExten
series.SetProviderId(TvdbPlugin.ProviderId, tvdbSeries.Id.ToString(CultureInfo.InvariantCulture));
// Tvdb uses 3 letter code for language (prob ISO 639-2)
// Reverts to OriginalName if no translation is found
series.Name = tvdbSeries.Translations.NameTranslations.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(info.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Name ?? tvdbSeries.Name;
series.Overview = tvdbSeries.Translations.OverviewTranslations.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(info.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Overview ?? tvdbSeries.Overview;
series.Name = tvdbSeries.Translations.GetTranslatedNamedOrDefault(info.MetadataLanguage) ?? tvdbSeries.Name;
series.Overview = tvdbSeries.Translations.GetTranslatedOverviewOrDefault(info.MetadataLanguage) ?? tvdbSeries.Overview;
series.OriginalTitle = tvdbSeries.Name;
result.ResultLanguage = info.MetadataLanguage;
series.AirDays = TvdbUtils.GetAirDays(tvdbSeries.AirsDays).ToArray();
Expand Down
41 changes: 41 additions & 0 deletions Jellyfin.Plugin.Tvdb/TvdbSdkExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Linq;

using Jellyfin.Extensions;

using Tvdb.Sdk;

namespace Jellyfin.Plugin.Tvdb;

/// <summary>
/// Extension Methods for Tvdb SDK.
/// </summary>
public static class TvdbSdkExtensions
{
/// <summary>
/// Get the translated Name, or <see langword="null"/>.
/// </summary>
/// <param name="translations">Available translations.</param>
/// <param name="language">Requested language.</param>
/// <returns>Translated Name, or <see langword="null"/>.</returns>
public static string? GetTranslatedNamedOrDefault(this TranslationExtended? translations, string? language)
{
return translations?
.NameTranslations?
.FirstOrDefault(translation => TvdbUtils.MatchLanguage(language, translation.Language))?
.Name;
}

/// <summary>
/// Get the translated Overview, or <see langword="null"/>.
/// </summary>
/// <param name="translations">Available translations.</param>
/// <param name="language">Requested language.</param>
/// <returns>Translated Overview, or <see langword="null"/>.</returns>
public static string? GetTranslatedOverviewOrDefault(this TranslationExtended? translations, string? language)
{
return translations?
.OverviewTranslations?
.FirstOrDefault(translation => TvdbUtils.MatchLanguage(language, translation.Language))?
.Overview;
}
}
Loading