jellyfin/MediaBrowser.Controller/Entities/TV/Series.cs
Mikal Stordal 8e1796f08a Add escape hatch for Series merging
This is an universal solution for plugins to override how series are merged.
The reason to override is so we can set the same provider id on multiple items without merging them, while using another id for merging them. Having an (optional) provider id not tied to any online database allows plugins to use their own rules for merging series.
2022-07-03 00:32:09 +02:00

517 lines
17 KiB
C#

#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
namespace MediaBrowser.Controller.Entities.TV
{
/// <summary>
/// Class Series.
/// </summary>
public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer
{
public Series()
{
AirDays = Array.Empty<DayOfWeek>();
}
public DayOfWeek[] AirDays { get; set; }
public string AirTime { get; set; }
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
[JsonIgnore]
public override bool IsPreSorted => true;
[JsonIgnore]
public override bool SupportsDateLastMediaAdded => true;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
public override bool SupportsPeople => true;
/// <inheritdoc />
[JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
/// <summary>
/// Gets or sets the display order.
/// </summary>
/// <remarks>
/// Valid options are airdate, dvd or absolute.
/// </remarks>
public string DisplayOrder { get; set; }
/// <summary>
/// Gets or sets the status.
/// </summary>
/// <value>The status.</value>
public SeriesStatus? Status { get; set; }
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
value /= 3;
return value;
}
public override string CreatePresentationUniqueKey()
{
if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
{
var userdatakeys = GetUserDataKeys();
if (userdatakeys.Count > 1)
{
return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
}
}
return base.CreatePresentationUniqueKey();
}
private string AddLibrariesToPresentationUniqueKey(string key)
{
var lang = GetPreferredMetadataLanguage();
if (!string.IsNullOrEmpty(lang))
{
key += "-" + lang;
}
var folders = LibraryManager.GetCollectionFolders(this)
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
.ToArray();
if (folders.Length == 0)
{
return key;
}
return key + "-" + string.Join('-', folders);
}
private static string GetUniqueSeriesKey(BaseItem series)
{
return series.GetPresentationUniqueKey();
}
public override int GetChildCount(User user)
{
var seriesKey = GetUniqueSeriesKey(this);
var result = LibraryManager.GetCount(new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Season },
IsVirtualItem = false,
Limit = 0,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
});
return result;
}
public override int GetRecursiveChildCount(User user)
{
var seriesKey = GetUniqueSeriesKey(this);
var query = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
};
if (query.IncludeItemTypes.Length == 0)
{
query.IncludeItemTypes = new[] { BaseItemKind.Episode };
}
query.IsVirtualItem = false;
query.Limit = 0;
var totalRecordCount = LibraryManager.GetCount(query);
return totalRecordCount;
}
/// <summary>
/// Gets the user data key.
/// </summary>
/// <returns>System.String.</returns>
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
if (this.TryGetProviderId(MetadataProvider.Imdb, out var key))
{
list.Insert(0, key);
}
if (this.TryGetProviderId(MetadataProvider.Tvdb, out key))
{
list.Insert(0, key);
}
if (this.TryGetProviderId(MetadataProvider.Custom, out key))
{
list.Insert(0, key);
}
return list;
}
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
return GetSeasons(user, new DtoOptions(true));
}
public List<BaseItem> GetSeasons(User user, DtoOptions options)
{
var query = new InternalItemsQuery(user)
{
DtoOptions = options
};
SetSeasonQueryOptions(query, user);
return LibraryManager.GetItemList(query);
}
private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
{
var seriesKey = GetUniqueSeriesKey(this);
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
query.IncludeItemTypes = new[] { BaseItemKind.Season };
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
if (user != null && !user.DisplayMissingEpisodes)
{
query.IsMissing = false;
}
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
var user = query.User;
if (query.Recursive)
{
var seriesKey = GetUniqueSeriesKey(this);
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
if (query.OrderBy.Count == 0)
{
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
}
if (query.IncludeItemTypes.Length == 0)
{
query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season };
}
query.IsVirtualItem = false;
return LibraryManager.GetItemsResult(query);
}
SetSeasonQueryOptions(query, user);
return LibraryManager.GetItemsResult(query);
}
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
{
var seriesKey = GetUniqueSeriesKey(this);
var query = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
if (!user.DisplayMissingEpisodes)
{
query.IsMissing = false;
}
var allItems = LibraryManager.GetItemList(query);
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
var allEpisodes = allItems.OfType<Season>()
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
.Reverse();
// Specials could appear twice based on above - once in season 0, once in the aired season
// This depends on settings for that series
// When this happens, remove the duplicate from season 0
return allEpisodes.GroupBy(i => i.Id).Select(x => x.First()).Reverse();
}
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
// Refresh bottom up, children first, then the boxset
// By then hopefully the movies within will have Tmdb collection values
var items = GetRecursiveChildren();
var totalItems = items.Count;
var numComplete = 0;
// Refresh seasons
foreach (var item in items)
{
if (item is not Season)
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
if (refreshOptions.RefreshItem(item))
{
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
}
numComplete++;
double percent = numComplete;
percent /= totalItems;
progress.Report(percent * 100);
}
// Refresh episodes and other children
foreach (var item in items)
{
if (item is Season)
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
bool skipItem = item is Episode episode
&& refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
&& !refreshOptions.ReplaceAllMetadata
&& episode.IsMissingEpisode
&& episode.LocationType == LocationType.Virtual
&& episode.PremiereDate.HasValue
&& (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30;
if (!skipItem)
{
if (refreshOptions.RefreshItem(item))
{
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
}
}
numComplete++;
double percent = numComplete;
percent /= totalItems;
progress.Report(percent * 100);
}
refreshOptions = new MetadataRefreshOptions(refreshOptions);
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
}
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
{
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
// add optimization when this setting is not enabled
var seriesKey = queryFromSeries ?
GetUniqueSeriesKey(this) :
GetUniqueSeriesKey(parentSeason);
var query = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
if (user != null)
{
if (!user.DisplayMissingEpisodes)
{
query.IsMissing = false;
}
}
var allItems = LibraryManager.GetItemList(query);
return GetSeasonEpisodes(parentSeason, user, allItems, options);
}
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
{
if (allSeriesEpisodes == null)
{
return GetSeasonEpisodes(parentSeason, user, options);
}
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList();
}
/// <summary>
/// Filters the episodes by season.
/// </summary>
/// <param name="episodes">The episodes.</param>
/// <param name="parentSeason">The season.</param>
/// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
/// <returns>The set of episodes.</returns>
public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials)
{
var seasonNumber = parentSeason.IndexNumber;
var seasonPresentationKey = GetUniqueSeriesKey(parentSeason);
var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0;
return episodes.Where(episode =>
{
var episodeItem = (Episode)episode;
var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexNumber;
if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.Value)
{
return true;
}
if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual)
{
return true;
}
var season = episodeItem.Season;
return season != null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase);
});
}
/// <summary>
/// Filters the episodes by season.
/// </summary>
/// <param name="episodes">The episodes.</param>
/// <param name="seasonNumber">The season.</param>
/// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
/// <returns>The set of episodes.</returns>
public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
{
if (!includeSpecials || seasonNumber < 1)
{
return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
}
return episodes.Where(i =>
{
var episode = i;
if (episode != null)
{
var currentSeasonNumber = episode.AiredSeasonNumber;
return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
}
return false;
});
}
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
}
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Series;
}
public SeriesInfo GetLookupInfo()
{
var info = GetItemLookupInfo<SeriesInfo>();
return info;
}
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
if (!ProductionYear.HasValue)
{
var info = LibraryManager.ParseName(Name);
var yearInName = info.Year;
if (yearInName.HasValue)
{
ProductionYear = yearInName;
hasChanges = true;
}
}
return hasChanges;
}
public override List<ExternalUrl> GetRelatedUrls()
{
var list = base.GetRelatedUrls();
var imdbId = this.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdbId))
{
list.Add(new ExternalUrl
{
Name = "Trakt",
Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId)
});
}
return list;
}
}
}