#nullable disable #pragma warning disable CS1591, SA1401 using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Diacritics.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// /// Class BaseItem. /// public abstract class BaseItem : IHasProviderIds, IHasLookupInfo, IEquatable { private BaseItemKind? _baseItemKind; public const string ThemeSongFileName = "theme"; /// /// The supported image extensions. /// public static readonly string[] SupportedImageExtensions = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; private static readonly List _supportedExtensions = new List(SupportedImageExtensions) { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; /// /// Extra types that should be counted and displayed as "Special Features" in the UI. /// public static readonly IReadOnlyCollection DisplayExtraTypes = new HashSet { Model.Entities.ExtraType.Unknown, Model.Entities.ExtraType.BehindTheScenes, Model.Entities.ExtraType.Clip, Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, Model.Entities.ExtraType.Scene }; private string _sortName; private string _forcedSortName; private string _name; public const char SlugChar = '-'; protected BaseItem() { Tags = Array.Empty(); Genres = Array.Empty(); Studios = Array.Empty(); ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); LockedFields = Array.Empty(); ImageInfos = Array.Empty(); ProductionLocations = Array.Empty(); RemoteTrailers = Array.Empty(); ExtraIds = Array.Empty(); } [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } [JsonIgnore] public string PreferredMetadataLanguage { get; set; } public long? Size { get; set; } public string Container { get; set; } [JsonIgnore] public string Tagline { get; set; } [JsonIgnore] public virtual ItemImageInfo[] ImageInfos { get; set; } [JsonIgnore] public bool IsVirtualItem { get; set; } /// /// Gets or sets the album. /// /// The album. [JsonIgnore] public string Album { get; set; } /// /// Gets or sets the channel identifier. /// /// The channel identifier. [JsonIgnore] public Guid ChannelId { get; set; } [JsonIgnore] public virtual bool SupportsAddingToPlaylist => false; [JsonIgnore] public virtual bool AlwaysScanInternalMetadataPath => false; /// /// Gets or sets a value indicating whether this instance is in mixed folder. /// /// true if this instance is in mixed folder; otherwise, false. [JsonIgnore] public bool IsInMixedFolder { get; set; } [JsonIgnore] public virtual bool SupportsPlayedStatus => false; [JsonIgnore] public virtual bool SupportsPositionTicksResume => false; [JsonIgnore] public virtual bool SupportsRemoteImageDownloading => true; /// /// Gets or sets the name. /// /// The name. [JsonIgnore] public virtual string Name { get => _name; set { _name = value; // lazy load this again _sortName = null; } } [JsonIgnore] public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; [JsonIgnore] public int? TotalBitrate { get; set; } [JsonIgnore] public ExtraType? ExtraType { get; set; } [JsonIgnore] public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); [JsonIgnore] public string OriginalTitle { get; set; } /// /// Gets or sets the id. /// /// The id. [JsonIgnore] public Guid Id { get; set; } [JsonIgnore] public Guid OwnerId { get; set; } /// /// Gets or sets the audio. /// /// The audio. [JsonIgnore] public ProgramAudio? Audio { get; set; } /// /// Gets the id that should be used to key display prefs for this item. /// Default is based on the type for everything except actual generic folders. /// /// The display prefs id. [JsonIgnore] public virtual Guid DisplayPreferencesId { get { var thisType = GetType(); return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5(); } } /// /// Gets or sets the path. /// /// The path. [JsonIgnore] public virtual string Path { get; set; } [JsonIgnore] public virtual SourceType SourceType { get { if (!ChannelId.Equals(default)) { return SourceType.Channel; } return SourceType.Library; } } /// /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// [JsonIgnore] public virtual string ContainingFolderPath { get { if (IsFolder) { return Path; } return System.IO.Path.GetDirectoryName(Path); } } /// /// Gets or sets the name of the service. /// /// The name of the service. [JsonIgnore] public string ServiceName { get; set; } /// /// Gets or sets the external id. /// /// /// If this content came from an external service, the id of the content on that service. /// [JsonIgnore] public string ExternalId { get; set; } [JsonIgnore] public string ExternalSeriesId { get; set; } [JsonIgnore] public virtual bool IsHidden => false; /// /// Gets the type of the location. /// /// The type of the location. [JsonIgnore] public virtual LocationType LocationType { get { var path = Path; if (string.IsNullOrEmpty(path)) { if (SourceType == SourceType.Channel) { return LocationType.Remote; } return LocationType.Virtual; } return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote; } } [JsonIgnore] public MediaProtocol? PathProtocol { get { var path = Path; if (string.IsNullOrEmpty(path)) { return null; } return MediaSourceManager.GetPathProtocol(path); } } [JsonIgnore] public bool IsFileProtocol => PathProtocol == MediaProtocol.File; [JsonIgnore] public bool HasPathProtocol => PathProtocol.HasValue; [JsonIgnore] public virtual bool SupportsLocalMetadata { get { if (SourceType == SourceType.Channel) { return false; } return IsFileProtocol; } } [JsonIgnore] public virtual string FileNameWithoutExtension { get { if (IsFileProtocol) { return System.IO.Path.GetFileNameWithoutExtension(Path); } return null; } } [JsonIgnore] public virtual bool EnableAlphaNumericSorting => true; public virtual bool IsHD => Height >= 720; public bool IsShortcut { get; set; } public string ShortcutPath { get; set; } public int Width { get; set; } public int Height { get; set; } public Guid[] ExtraIds { get; set; } /// /// Gets the primary image path. /// /// /// This is just a helper for convenience. /// /// The primary image path. [JsonIgnore] public string PrimaryImagePath => this.GetImagePath(ImageType.Primary); /// /// Gets or sets the date created. /// /// The date created. [JsonIgnore] public DateTime DateCreated { get; set; } /// /// Gets or sets the date modified. /// /// The date modified. [JsonIgnore] public DateTime DateModified { get; set; } public DateTime DateLastSaved { get; set; } [JsonIgnore] public DateTime DateLastRefreshed { get; set; } [JsonIgnore] public bool IsLocked { get; set; } /// /// Gets or sets the locked fields. /// /// The locked fields. [JsonIgnore] public MetadataField[] LockedFields { get; set; } /// /// Gets the type of the media. /// /// The type of the media. [JsonIgnore] public virtual string MediaType => null; [JsonIgnore] public virtual string[] PhysicalLocations { get { if (!IsFileProtocol) { return Array.Empty(); } return new[] { Path }; } } [JsonIgnore] public bool EnableMediaSourceDisplay { get { if (SourceType == SourceType.Channel) { return ChannelManager.EnableMediaSourceDisplay(this); } return true; } } [JsonIgnore] public Guid ParentId { get; set; } /// /// Gets or sets the logger. /// public static ILogger Logger { get; set; } public static ILibraryManager LibraryManager { get; set; } public static IServerConfigurationManager ConfigurationManager { get; set; } public static IProviderManager ProviderManager { get; set; } public static ILocalizationManager LocalizationManager { get; set; } public static IItemRepository ItemRepository { get; set; } public static IFileSystem FileSystem { get; set; } public static IUserDataManager UserDataManager { get; set; } public static IChannelManager ChannelManager { get; set; } public static IMediaSourceManager MediaSourceManager { get; set; } /// /// Gets or sets the name of the forced sort. /// /// The name of the forced sort. [JsonIgnore] public string ForcedSortName { get => _forcedSortName; set { _forcedSortName = value; _sortName = null; } } /// /// Gets or sets the name of the sort. /// /// The name of the sort. [JsonIgnore] public string SortName { get { if (_sortName == null) { if (!string.IsNullOrEmpty(ForcedSortName)) { // Need the ToLower because that's what CreateSortName does _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant(); } else { _sortName = CreateSortName(); } } return _sortName; } set => _sortName = value; } [JsonIgnore] public virtual Guid DisplayParentId => ParentId; [JsonIgnore] public BaseItem DisplayParent { get { var id = DisplayParentId; if (id.Equals(default)) { return null; } return LibraryManager.GetItemById(id); } } /// /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be first aired. /// /// The premiere date. [JsonIgnore] public DateTime? PremiereDate { get; set; } /// /// Gets or sets the end date. /// /// The end date. [JsonIgnore] public DateTime? EndDate { get; set; } /// /// Gets or sets the official rating. /// /// The official rating. [JsonIgnore] public string OfficialRating { get; set; } [JsonIgnore] public int InheritedParentalRatingValue { get; set; } /// /// Gets or sets the critic rating. /// /// The critic rating. [JsonIgnore] public float? CriticRating { get; set; } /// /// Gets or sets the custom rating. /// /// The custom rating. [JsonIgnore] public string CustomRating { get; set; } /// /// Gets or sets the overview. /// /// The overview. [JsonIgnore] public string Overview { get; set; } /// /// Gets or sets the studios. /// /// The studios. [JsonIgnore] public string[] Studios { get; set; } /// /// Gets or sets the genres. /// /// The genres. [JsonIgnore] public string[] Genres { get; set; } /// /// Gets or sets the tags. /// /// The tags. [JsonIgnore] public string[] Tags { get; set; } [JsonIgnore] public string[] ProductionLocations { get; set; } /// /// Gets or sets the home page URL. /// /// The home page URL. [JsonIgnore] public string HomePageUrl { get; set; } /// /// Gets or sets the community rating. /// /// The community rating. [JsonIgnore] public float? CommunityRating { get; set; } /// /// Gets or sets the run time ticks. /// /// The run time ticks. [JsonIgnore] public long? RunTimeTicks { get; set; } /// /// Gets or sets the production year. /// /// The production year. [JsonIgnore] public int? ProductionYear { get; set; } /// /// Gets or sets the index number. If the item is part of a series, this is it's number in the series. /// This could be episode number, album track number, etc. /// /// The index number. [JsonIgnore] public int? IndexNumber { get; set; } /// /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this could be the disc number. /// /// The parent index number. [JsonIgnore] public int? ParentIndexNumber { get; set; } [JsonIgnore] public virtual bool HasLocalAlternateVersions => false; [JsonIgnore] public string OfficialRatingForComparison { get { var officialRating = OfficialRating; if (!string.IsNullOrEmpty(officialRating)) { return officialRating; } var parent = DisplayParent; if (parent != null) { return parent.OfficialRatingForComparison; } return null; } } [JsonIgnore] public string CustomRatingForComparison { get { var customRating = CustomRating; if (!string.IsNullOrEmpty(customRating)) { return customRating; } var parent = DisplayParent; if (parent != null) { return parent.CustomRatingForComparison; } return null; } } /// /// Gets or sets the provider ids. /// /// The provider ids. [JsonIgnore] public Dictionary ProviderIds { get; set; } [JsonIgnore] public virtual Folder LatestItemsIndexContainer => null; [JsonIgnore] public string PresentationUniqueKey { get; set; } [JsonIgnore] public virtual bool EnableRememberingTrackSelections => true; [JsonIgnore] public virtual bool IsTopParent { get { if (this is BasePluginFolder || this is Channel) { return true; } if (this is IHasCollectionType view) { if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) { return true; } } if (GetParent() is AggregateFolder) { return true; } return false; } } [JsonIgnore] public virtual bool SupportsAncestors => true; [JsonIgnore] public virtual bool StopRefreshIfLocalMetadataFound => true; [JsonIgnore] protected virtual bool SupportsOwnedItems => !ParentId.Equals(default) && IsFileProtocol; [JsonIgnore] public virtual bool SupportsPeople => false; [JsonIgnore] public virtual bool SupportsThemeMedia => false; [JsonIgnore] public virtual bool SupportsInheritedParentImages => false; /// /// Gets a value indicating whether this instance is folder. /// /// true if this instance is folder; otherwise, false. [JsonIgnore] public virtual bool IsFolder => false; [JsonIgnore] public virtual bool IsDisplayedAsFolder => false; /// /// Gets or sets the remote trailers. /// /// The remote trailers. public IReadOnlyList RemoteTrailers { get; set; } public virtual bool SupportsExternalTransfer => false; public virtual double GetDefaultPrimaryImageAspectRatio() { return 0; } public virtual string CreatePresentationUniqueKey() { return Id.ToString("N", CultureInfo.InvariantCulture); } private List> GetSortChunks(string s1) { var list = new List>(); int thisMarker = 0; while (thisMarker < s1.Length) { char thisCh = s1[thisMarker]; var thisChunk = new StringBuilder(); bool isNumeric = char.IsDigit(thisCh); while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric) { thisChunk.Append(thisCh); thisMarker++; if (thisMarker < s1.Length) { thisCh = s1[thisMarker]; } } list.Add(new Tuple(thisChunk, isNumeric)); } return list; } public virtual bool CanDelete() { if (SourceType == SourceType.Channel) { return ChannelManager.CanDelete(this); } return IsFileProtocol; } public virtual bool IsAuthorizedToDelete(User user, List allCollectionFolders) { if (user.HasPermission(PermissionKind.EnableContentDeletion)) { return true; } var allowed = user.GetPreferenceValues(PreferenceKind.EnableContentDeletionFromFolders); if (SourceType == SourceType.Channel) { return allowed.Contains(ChannelId); } else { var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); foreach (var folder in collectionFolders) { if (allowed.Contains(folder.Id)) { return true; } } } return false; } public BaseItem GetOwner() { var ownerId = OwnerId; return ownerId.Equals(default) ? null : LibraryManager.GetItemById(ownerId); } public bool CanDelete(User user, List allCollectionFolders) { return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); } public bool CanDelete(User user) { var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType().ToList(); return CanDelete(user, allCollectionFolders); } public virtual bool CanDownload() { return false; } public virtual bool IsAuthorizedToDownload(User user) { return user.HasPermission(PermissionKind.EnableContentDownloading); } public bool CanDownload(User user) { return CanDownload() && IsAuthorizedToDownload(user); } /// /// Returns a that represents this instance. /// /// A that represents this instance. public override string ToString() { return Name; } public string GetInternalMetadataPath() { var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; return GetInternalMetadataPath(basePath); } protected virtual string GetInternalMetadataPath(string basePath) { if (SourceType == SourceType.Channel) { return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } ReadOnlySpan idString = Id.ToString("N", CultureInfo.InvariantCulture); return System.IO.Path.Join(basePath, "library", idString[..2], idString); } /// /// Creates the name of the sort. /// /// System.String. protected virtual string CreateSortName() { if (Name == null) { return null; // some items may not have name filled in properly } if (!EnableAlphaNumericSorting) { return Name.TrimStart(); } var sortable = Name.Trim().ToLowerInvariant(); foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) { sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal); } foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) { sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal); } foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { // Remove from beginning if a space follows if (sortable.StartsWith(search + " ", StringComparison.Ordinal)) { sortable = sortable.Remove(0, search.Length + 1); } // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); // Remove from end if followed by a space if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); } } return ModifySortChunks(sortable); } private string ModifySortChunks(string name) { var chunks = GetSortChunks(name); var builder = new StringBuilder(); foreach (var chunk in chunks) { var chunkBuilder = chunk.Item1; // This chunk is numeric if (chunk.Item2) { while (chunkBuilder.Length < 10) { chunkBuilder.Insert(0, '0'); } } builder.Append(chunkBuilder); } // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); } public BaseItem GetParent() { var parentId = ParentId; if (parentId.Equals(default)) { return null; } return LibraryManager.GetItemById(parentId); } public IEnumerable GetParents() { var parent = GetParent(); while (parent != null) { yield return parent; parent = parent.GetParent(); } } /// /// Finds a parent of a given type. /// /// Type of parent. /// ``0. public T FindParent() where T : Folder { foreach (var parent in GetParents()) { if (parent is T item) { return item; } } return null; } /// /// Gets the play access. /// /// The user. /// PlayAccess. public PlayAccess GetPlayAccess(User user) { if (!user.HasPermission(PermissionKind.EnableMediaPlayback)) { return PlayAccess.None; } // if (!user.IsParentalScheduleAllowed()) // { // return PlayAccess.None; // } return PlayAccess.Full; } public virtual List GetMediaStreams() { return MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = Id }); } protected virtual bool IsActiveRecording() { return false; } public virtual List GetMediaSources(bool enablePathSubstitution) { if (SourceType == SourceType.Channel) { var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None) .ToList(); if (sources.Count > 0) { return sources; } } var list = GetAllItemsForMediaSources(); var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList(); if (IsActiveRecording()) { foreach (var mediaSource in result) { mediaSource.Type = MediaSourceType.Placeholder; } } return result.OrderBy(i => { if (i.VideoType == VideoType.VideoFile) { return 0; } return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => { var stream = i.VideoStream; return stream == null || stream.Width == null ? 0 : stream.Width.Value; }) .ToList(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() { return Enumerable.Empty<(BaseItem, MediaSourceType)>(); } private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type) { if (item == null) { throw new ArgumentNullException(nameof(item)); } var protocol = item.PathProtocol; var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), Protocol = protocol ?? MediaProtocol.File, MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, Type = type }; if (string.IsNullOrEmpty(info.Path)) { info.Type = MediaSourceType.Placeholder; } if (info.Protocol == MediaProtocol.File) { info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", CultureInfo.InvariantCulture); } var video = item as Video; if (video != null) { info.IsoType = video.IsoType; info.VideoType = video.VideoType; info.Video3DFormat = video.Video3DFormat; info.Timestamp = video.Timestamp; if (video.IsShortcut) { info.IsRemote = true; info.Path = video.ShortcutPath; info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); } if (string.IsNullOrEmpty(info.Container)) { if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) { if (protocol.HasValue && protocol.Value == MediaProtocol.File) { info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); } } } } if (string.IsNullOrEmpty(info.Container)) { if (protocol.HasValue && protocol.Value == MediaProtocol.File) { info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.'); } } if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path)) { info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol); } if (video != null && video.VideoType != VideoType.VideoFile) { info.SupportsDirectStream = false; } info.Bitrate = item.TotalBitrate; info.InferTotalBitrate(); return info; } private string GetMediaSourceName(BaseItem item) { var terms = new List(); var path = item.Path; if (item.IsFileProtocol && !string.IsNullOrEmpty(path)) { if (HasLocalAlternateVersions) { var displayName = System.IO.Path.GetFileNameWithoutExtension(path) .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) .TrimStart(new char[] { ' ', '-' }); if (!string.IsNullOrEmpty(displayName)) { terms.Add(displayName); } } if (terms.Count == 0) { var displayName = System.IO.Path.GetFileNameWithoutExtension(path); terms.Add(displayName); } } if (terms.Count == 0) { terms.Add(item.Name); } if (item is Video video) { if (video.Video3DFormat.HasValue) { terms.Add("3D"); } if (video.VideoType == VideoType.BluRay) { terms.Add("Bluray"); } else if (video.VideoType == VideoType.Dvd) { terms.Add("DVD"); } else if (video.VideoType == VideoType.Iso) { if (video.IsoType.HasValue) { if (video.IsoType.Value == IsoType.BluRay) { terms.Add("Bluray"); } else if (video.IsoType.Value == IsoType.Dvd) { terms.Add("DVD"); } } else { terms.Add("ISO"); } } } return string.Join('/', terms); } public Task RefreshMetadata(CancellationToken cancellationToken) { return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } protected virtual void TriggerOnRefreshStart() { } protected virtual void TriggerOnRefreshComplete() { } /// /// Overrides the base implementation to refresh metadata for local trailers. /// /// The options. /// The cancellation token. /// true if a provider reports we changed. public async Task RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) { TriggerOnRefreshStart(); var requiresSave = false; if (SupportsOwnedItems) { try { if (IsFileProtocol) { requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService), cancellationToken).ConfigureAwait(false); } await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh } catch (Exception ex) { Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name); } } try { var refreshOptions = requiresSave ? new MetadataRefreshOptions(options) { ForceSave = true } : options; return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false); } finally { TriggerOnRefreshComplete(); } } protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) { if (!IsVisible(user)) { return false; } if (GetParents().Any(i => !i.IsVisible(user))) { return false; } if (checkFolders) { var topParent = GetParents().LastOrDefault() ?? this; if (string.IsNullOrEmpty(topParent.Path)) { return true; } var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList(); if (itemCollectionFolders.Count > 0) { var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList(); if (!itemCollectionFolders.Any(userCollectionFolders.Contains)) { return false; } } } return true; } public void SetParent(Folder parent) { ParentId = parent == null ? Guid.Empty : parent.Id; } /// /// Refreshes owned items such as trailers, theme videos, special features, etc. /// Returns true or false indicating if changes were found. /// /// The metadata refresh options. /// The list of filesystem children. /// The cancellation token. /// true if any items have changed, else false. protected virtual async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) { if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder or AggregateFolder || this.GetType() == typeof(Folder)) { return false; } return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false); } protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { var path = ContainingFolderPath; return directoryService.GetFileSystemEntries(path); } private async Task RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) { var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray(); var newExtraIds = extras.Select(i => i.Id).ToArray(); var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds); if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh) { return false; } var ownerId = item.Id; var tasks = extras.Select(i => { var subOptions = new MetadataRefreshOptions(options); if (!i.OwnerId.Equals(ownerId) || !i.ParentId.Equals(default)) { i.OwnerId = ownerId; i.ParentId = Guid.Empty; subOptions.ForceSave = true; } return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); await Task.WhenAll(tasks).ConfigureAwait(false); item.ExtraIds = newExtraIds; return true; } public string GetPresentationUniqueKey() { return PresentationUniqueKey ?? CreatePresentationUniqueKey(); } public virtual bool RequiresRefresh() { return false; } public virtual List GetUserDataKeys() { var list = new List(); if (SourceType == SourceType.Channel) { if (!string.IsNullOrEmpty(ExternalId)) { list.Add(ExternalId); } } list.Add(Id.ToString()); return list; } internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) { var updateType = ItemUpdateType.None; if (IsInMixedFolder != newItem.IsInMixedFolder) { IsInMixedFolder = newItem.IsInMixedFolder; updateType |= ItemUpdateType.MetadataImport; } return updateType; } public void AfterMetadataRefresh() { _sortName = null; } /// /// Gets the preferred metadata language. /// /// System.String. public string GetPreferredMetadataLanguage() { string lang = PreferredMetadataLanguage; if (string.IsNullOrEmpty(lang)) { lang = GetParents() .Select(i => i.PreferredMetadataLanguage) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } if (string.IsNullOrEmpty(lang)) { lang = LibraryManager.GetCollectionFolders(this) .Select(i => i.PreferredMetadataLanguage) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } if (string.IsNullOrEmpty(lang)) { lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage; } if (string.IsNullOrEmpty(lang)) { lang = ConfigurationManager.Configuration.PreferredMetadataLanguage; } return lang; } /// /// Gets the preferred metadata language. /// /// System.String. public string GetPreferredMetadataCountryCode() { string lang = PreferredMetadataCountryCode; if (string.IsNullOrEmpty(lang)) { lang = GetParents() .Select(i => i.PreferredMetadataCountryCode) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } if (string.IsNullOrEmpty(lang)) { lang = LibraryManager.GetCollectionFolders(this) .Select(i => i.PreferredMetadataCountryCode) .FirstOrDefault(i => !string.IsNullOrEmpty(i)); } if (string.IsNullOrEmpty(lang)) { lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode; } if (string.IsNullOrEmpty(lang)) { lang = ConfigurationManager.Configuration.MetadataCountryCode; } return lang; } public virtual bool IsSaveLocalMetadataEnabled() { if (SourceType == SourceType.Channel) { return false; } var libraryOptions = LibraryManager.GetLibraryOptions(this); return libraryOptions.SaveLocalMetadata; } /// /// Determines if a given user has access to this item. /// /// The user. /// true if [is parental allowed] [the specified user]; otherwise, false. /// If user is null. public bool IsParentalAllowed(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } if (!IsVisibleViaTags(user)) { return false; } var maxAllowedRating = user.MaxParentalAgeRating; if (maxAllowedRating == null) { return true; } var rating = CustomRatingForComparison; if (string.IsNullOrEmpty(rating)) { rating = OfficialRatingForComparison; } if (string.IsNullOrEmpty(rating)) { return !GetBlockUnratedValue(user); } var value = LocalizationManager.GetRatingLevel(rating); // Could not determine the integer value if (!value.HasValue) { var isAllowed = !GetBlockUnratedValue(user); if (!isAllowed) { Logger.LogDebug("{0} has an unrecognized parental rating of {1}.", Name, rating); } return isAllowed; } return value.Value <= maxAllowedRating.Value; } public int? GetParentalRatingValue() { var rating = CustomRating; if (string.IsNullOrEmpty(rating)) { rating = OfficialRating; } if (string.IsNullOrEmpty(rating)) { return null; } return LocalizationManager.GetRatingLevel(rating); } public int? GetInheritedParentalRatingValue() { var rating = CustomRatingForComparison; if (string.IsNullOrEmpty(rating)) { rating = OfficialRatingForComparison; } if (string.IsNullOrEmpty(rating)) { return null; } return LocalizationManager.GetRatingLevel(rating); } public List GetInheritedTags() { var list = new List(); list.AddRange(Tags); foreach (var parent in GetParents()) { list.AddRange(parent.Tags); } return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } private bool IsVisibleViaTags(User user) { if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase))) { return false; } return true; } protected virtual bool IsAllowTagFilterEnforced() { return true; } public virtual UnratedItem GetBlockUnratedType() { if (SourceType == SourceType.Channel) { return UnratedItem.ChannelContent; } return UnratedItem.Other; } /// /// Gets the block unrated value. /// /// The configuration. /// true if XXXX, false otherwise. protected virtual bool GetBlockUnratedValue(User user) { // Don't block plain folders that are unrated. Let the media underneath get blocked // Special folders like series and albums will override this method. if (IsFolder || this is IItemByName) { return false; } return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType()); } /// /// Determines if this folder should be visible to a given user. /// Default is just parental allowed. Can be overridden for more functionality. /// /// The user. /// true if the specified user is visible; otherwise, false. /// is null. public virtual bool IsVisible(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } return IsParentalAllowed(user); } public virtual bool IsVisibleStandalone(User user) { if (SourceType == SourceType.Channel) { return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user); } return IsVisibleStandaloneInternal(user, true); } public virtual string GetClientTypeName() { if (IsFolder && SourceType == SourceType.Channel && this is not Channel) { return "ChannelFolderItem"; } return GetType().Name; } public BaseItemKind GetBaseItemKind() { return _baseItemKind ??= Enum.Parse(GetClientTypeName()); } /// /// Gets the linked child. /// /// The info. /// BaseItem. protected BaseItem GetLinkedChild(LinkedChild info) { // First get using the cached Id if (info.ItemId.HasValue) { if (info.ItemId.Value.Equals(default)) { return null; } var itemById = LibraryManager.GetItemById(info.ItemId.Value); if (itemById != null) { return itemById; } } var item = FindLinkedChild(info); // If still null, log if (item == null) { // Don't keep searching over and over info.ItemId = Guid.Empty; } else { // Cache the id for next time info.ItemId = item.Id; } return item; } private BaseItem FindLinkedChild(LinkedChild info) { var path = info.Path; if (!string.IsNullOrEmpty(path)) { path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path); var itemByPath = LibraryManager.FindByPath(path, null); if (itemByPath == null) { Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return itemByPath; } if (!string.IsNullOrEmpty(info.LibraryItemId)) { var item = LibraryManager.GetItemById(info.LibraryItemId); if (item == null) { Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return item; } return null; } /// /// Adds a studio to the item. /// /// The name. /// Throws if name is null. public void AddStudio(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } var current = Studios; if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) { int curLen = current.Length; if (curLen == 0) { Studios = new[] { name }; } else { var newArr = new string[curLen + 1]; current.CopyTo(newArr, 0); newArr[curLen] = name; Studios = newArr; } } } public void SetStudios(IEnumerable names) { Studios = names.Distinct().ToArray(); } /// /// Adds a genre to the item. /// /// The name. /// Throwns if name is null. public void AddGenre(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } var genres = Genres; if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase)) { var list = genres.ToList(); list.Add(name); Genres = list.ToArray(); } } /// /// Marks the played. /// /// The user. /// The date played. /// if set to true [reset position]. /// Throws if user is null. public virtual void MarkPlayed( User user, DateTime? datePlayed, bool resetPosition) { if (user == null) { throw new ArgumentNullException(nameof(user)); } var data = UserDataManager.GetUserData(user, this); if (datePlayed.HasValue) { // Increment data.PlayCount++; } // Ensure it's at least one data.PlayCount = Math.Max(data.PlayCount, 1); if (resetPosition) { data.PlaybackPositionTicks = 0; } data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow; data.Played = true; UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None); } /// /// Marks the unplayed. /// /// The user. /// Throws if user is null. public virtual void MarkUnplayed(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } var data = UserDataManager.GetUserData(user, this); // I think it is okay to do this here. // if this is only called when a user is manually forcing something to un-played // then it probably is what we want to do... data.PlayCount = 0; data.PlaybackPositionTicks = 0; data.LastPlayedDate = null; data.Played = false; UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None); } /// /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// public virtual void ChangedExternally() { ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriority.High); } /// /// Gets an image. /// /// The type. /// Index of the image. /// true if the specified type has image; otherwise, false. /// Backdrops should be accessed using Item.Backdrops. public bool HasImage(ImageType type, int imageIndex) { return GetImageInfo(type, imageIndex) != null; } public void SetImage(ItemImageInfo image, int index) { if (image.Type == ImageType.Chapter) { throw new ArgumentException("Cannot set chapter images using SetImagePath"); } var existingImage = GetImageInfo(image.Type, index); if (existingImage == null) { AddImage(image); } else { existingImage.Path = image.Path; existingImage.DateModified = image.DateModified; existingImage.Width = image.Width; existingImage.Height = image.Height; existingImage.BlurHash = image.BlurHash; } } public void SetImagePath(ImageType type, int index, FileSystemMetadata file) { if (type == ImageType.Chapter) { throw new ArgumentException("Cannot set chapter images using SetImagePath"); } var image = GetImageInfo(type, index); if (image == null) { AddImage(GetImageInfo(file, type)); } else { var imageInfo = GetImageInfo(file, type); image.Path = file.FullName; image.DateModified = imageInfo.DateModified; // reset these values image.Width = 0; image.Height = 0; } } /// /// Deletes the image. /// /// The type. /// The index. /// A task. public async Task DeleteImageAsync(ImageType type, int index) { var info = GetImageInfo(type, index); if (info == null) { // Nothing to do return; } // Remove it from the item RemoveImage(info); if (info.IsLocalFile) { FileSystem.DeleteFile(info.Path); } await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); } public void RemoveImage(ItemImageInfo image) { RemoveImages(new[] { image }); } public void RemoveImages(IEnumerable deletedImages) { ImageInfos = ImageInfos.Except(deletedImages).ToArray(); } public void AddImage(ItemImageInfo image) { var current = ImageInfos; var currentCount = current.Length; var newArr = new ItemImageInfo[currentCount + 1]; current.CopyTo(newArr, 0); newArr[currentCount] = image; ImageInfos = newArr; } public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); /// /// Validates that images within the item are still on the filesystem. /// /// true if the images validate, false if not. public bool ValidateImages() { List deletedImages = null; foreach (var imageInfo in ImageInfos) { if (!imageInfo.IsLocalFile) { continue; } if (File.Exists(imageInfo.Path)) { continue; } (deletedImages ??= new List()).Add(imageInfo); } var anyImagesRemoved = deletedImages?.Count > 0; if (anyImagesRemoved) { RemoveImages(deletedImages); } return anyImagesRemoved; } /// /// Gets the image path. /// /// Type of the image. /// Index of the image. /// System.String. /// Item is null. public string GetImagePath(ImageType imageType, int imageIndex) => GetImageInfo(imageType, imageIndex)?.Path; /// /// Gets the image information. /// /// Type of the image. /// Index of the image. /// ItemImageInfo. public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex) { if (imageType == ImageType.Chapter) { var chapter = ItemRepository.GetChapter(this, imageIndex); if (chapter == null) { return null; } var path = chapter.ImagePath; if (string.IsNullOrEmpty(path)) { return null; } return new ItemImageInfo { Path = path, DateModified = chapter.ImageDateModified, Type = imageType }; } // Music albums usually don't have dedicated backdrops, so return one from the artist instead if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop) { var artist = FindParent(); if (artist != null) { return artist.GetImages(imageType).ElementAtOrDefault(imageIndex); } } return GetImages(imageType) .ElementAtOrDefault(imageIndex); } /// /// Computes image index for given image or raises if no matching image found. /// /// Image to compute index for. /// Image index cannot be computed as no matching image found. /// /// Image index. public int GetImageIndex(ItemImageInfo image) { if (image == null) { throw new ArgumentNullException(nameof(image)); } if (image.Type == ImageType.Chapter) { var chapters = ItemRepository.GetChapters(this); for (var i = 0; i < chapters.Count; i++) { if (chapters[i].ImagePath == image.Path) { return i; } } throw new ArgumentException("No chapter index found for image path", image.Path); } var images = GetImages(image.Type).ToArray(); for (var i = 0; i < images.Length; i++) { if (images[i].Path == image.Path) { return i; } } throw new ArgumentException("No image index found for image path", image.Path); } public IEnumerable GetImages(ImageType imageType) { if (imageType == ImageType.Chapter) { throw new ArgumentException("No image info for chapter images"); } // Yield return is more performant than LINQ Where on an Array for (var i = 0; i < ImageInfos.Length; i++) { var imageInfo = ImageInfos[i]; if (imageInfo.Type == imageType) { yield return imageInfo; } } } /// /// Adds the images, updating metadata if they already are part of this item. /// /// Type of the image. /// The images. /// true if images were added or updated, false otherwise. /// Cannot call AddImages with chapter images. public bool AddImages(ImageType imageType, List images) { if (imageType == ImageType.Chapter) { throw new ArgumentException("Cannot call AddImages with chapter images"); } var existingImages = GetImages(imageType) .ToList(); var newImageList = new List(); var imageUpdated = false; foreach (var newImage in images) { if (newImage == null) { throw new ArgumentException("null image found in list"); } var existing = existingImages .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); if (existing == null) { newImageList.Add(newImage); } else { if (existing.IsLocalFile) { var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage); // If date changed then we need to reset saved image dimensions if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0)) { existing.Width = 0; existing.Height = 0; imageUpdated = true; } existing.DateModified = newDateModified; } } } if (newImageList.Count > 0) { ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray(); } return imageUpdated || newImageList.Count > 0; } private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type) { return new ItemImageInfo { Path = file.FullName, Type = type, DateModified = FileSystem.GetLastWriteTimeUtc(file) }; } /// /// Gets the file system path to delete when the item is to be deleted. /// /// The metadata for the deleted paths. public virtual IEnumerable GetDeletePaths() { return new[] { new FileSystemMetadata { FullName = Path, IsDirectory = IsFolder } }.Concat(GetLocalMetadataFilesToDelete()); } protected List GetLocalMetadataFilesToDelete() { if (IsFolder || !IsInMixedFolder) { return new List(); } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false) .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) .ToList(); } public bool AllowsMultipleImages(ImageType type) { return type == ImageType.Backdrop || type == ImageType.Chapter; } public Task SwapImagesAsync(ImageType type, int index1, int index2) { if (!AllowsMultipleImages(type)) { throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots"); } var info1 = GetImageInfo(type, index1); var info2 = GetImageInfo(type, index2); if (info1 == null || info2 == null) { // Nothing to do return Task.CompletedTask; } if (!info1.IsLocalFile || !info2.IsLocalFile) { // TODO: Not supported yet return Task.CompletedTask; } var path1 = info1.Path; var path2 = info2.Path; FileSystem.SwapFiles(path1, path2); // Refresh these values info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path); info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path); info1.Width = 0; info1.Height = 0; info2.Width = 0; info2.Height = 0; return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None); } public virtual bool IsPlayed(User user) { var userdata = UserDataManager.GetUserData(user, this); return userdata != null && userdata.Played; } public bool IsFavoriteOrLiked(User user) { var userdata = UserDataManager.GetUserData(user, this); return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false)); } public virtual bool IsUnplayed(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } var userdata = UserDataManager.GetUserData(user, this); return userdata == null || !userdata.Played; } ItemLookupInfo IHasLookupInfo.GetLookupInfo() { return GetItemLookupInfo(); } protected T GetItemLookupInfo() where T : ItemLookupInfo, new() { return new T { Path = Path, MetadataCountryCode = GetPreferredMetadataCountryCode(), MetadataLanguage = GetPreferredMetadataLanguage(), Name = GetNameForMetadataLookup(), OriginalTitle = OriginalTitle, ProviderIds = ProviderIds, IndexNumber = IndexNumber, ParentIndexNumber = ParentIndexNumber, Year = ProductionYear, PremiereDate = PremiereDate }; } protected virtual string GetNameForMetadataLookup() { return Name; } /// /// This is called before any metadata refresh and returns true if changes were made. /// /// Whether to replace all metadata. /// true if the item has change, else false. public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata) { _sortName = null; var hasChanges = false; if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path)) { Name = System.IO.Path.GetFileNameWithoutExtension(Path); hasChanges = true; } return hasChanges; } protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol) { if (protocol == MediaProtocol.File) { return LibraryManager.GetPathAfterNetworkSubstitution(path, item); } return path; } public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) { if (RunTimeTicks.HasValue) { double pct = RunTimeTicks.Value; if (pct > 0) { pct = userData.PlaybackPositionTicks / pct; if (pct > 0) { dto.PlayedPercentage = 100 * pct; } } } } protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { SearchResult = null }; var item = this; if (copyTitleMetadata) { // Take some data from the main item, for querying purposes if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.Genres = item.Genres; } if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.Studios = item.Studios; } if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.ProductionLocations = item.ProductionLocations; } if (item.CommunityRating != ownedItem.CommunityRating) { ownedItem.CommunityRating = item.CommunityRating; newOptions.ForceSave = true; } if (item.CriticRating != ownedItem.CriticRating) { ownedItem.CriticRating = item.CriticRating; newOptions.ForceSave = true; } if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) { ownedItem.Overview = item.Overview; newOptions.ForceSave = true; } if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) { ownedItem.OfficialRating = item.OfficialRating; newOptions.ForceSave = true; } if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) { ownedItem.CustomRating = item.CustomRating; newOptions.ForceSave = true; } } return ownedItem.RefreshMetadata(newOptions, cancellationToken); } protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { SearchResult = null }; var id = LibraryManager.GetNewItemId(path, typeof(Video)); // Try to retrieve it from the db. If we don't find it, use the resolved version var video = LibraryManager.GetItemById(id) as Video; if (video == null) { video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; newOptions.ForceSave = true; } if (video == null) { return Task.FromResult(true); } return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); } public string GetEtag(User user) { var list = GetEtagValues(user); return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List GetEtagValues(User user) { return new List { DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) }; } public virtual IEnumerable GetAncestorIds() { return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id)); } public BaseItem GetTopParent() { if (IsTopParent) { return this; } return GetParents().FirstOrDefault(parent => parent.IsTopParent); } public virtual IEnumerable GetIdsForAncestorQuery() { return new[] { Id }; } public virtual List GetRelatedUrls() { return new List(); } public virtual double? GetRefreshProgress() { return null; } public virtual ItemUpdateType OnMetadataChanged() { var updateType = ItemUpdateType.None; var item = this; var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0; if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) { item.InheritedParentalRatingValue = inheritedParentalRatingValue; updateType |= ItemUpdateType.MetadataImport; } return updateType; } /// /// Updates the official rating based on content and returns true or false indicating if it changed. /// /// Media children. /// true if the rating was updated; otherwise false. public bool UpdateRatingToItems(IList children) { var currentOfficialRating = OfficialRating; // Gather all possible ratings var ratings = children .Select(i => i.OfficialRating) .Where(i => !string.IsNullOrEmpty(i)) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(rating => (rating, LocalizationManager.GetRatingLevel(rating))) .OrderBy(i => i.Item2 ?? 1000) .Select(i => i.rating); OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; return !string.Equals( currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty, StringComparison.OrdinalIgnoreCase); } public IReadOnlyList GetThemeSongs() { return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray(); } public IReadOnlyList GetThemeVideos() { return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray(); } /// /// Get all extras associated with this item, sorted by . /// /// An enumerable containing the items. public IEnumerable GetExtras() { return ExtraIds .Select(LibraryManager.GetItemById) .Where(i => i != null) .OrderBy(i => i.SortName); } /// /// Get all extras with specific types that are associated with this item. /// /// The types of extras to retrieve. /// An enumerable containing the extras. public IEnumerable GetExtras(IReadOnlyCollection extraTypes) { return ExtraIds .Select(LibraryManager.GetItemById) .Where(i => i != null) .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)); } public virtual long GetRunTimeTicksForPlayState() { return RunTimeTicks ?? 0; } /// public override bool Equals(object obj) { return obj is BaseItem baseItem && this.Equals(baseItem); } /// public bool Equals(BaseItem other) => other is not null && other.Id.Equals(Id); /// public override int GetHashCode() => HashCode.Combine(Id); } }