diff options
Diffstat (limited to 'MediaBrowser.Controller/Entities')
60 files changed, 3206 insertions, 2799 deletions
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 054df21e5..9589f5245 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -1,43 +1,40 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> /// Specialized folder that can have items added to it's children by external entities. - /// Used for our RootFolder so plug-ins can add items. + /// Used for our RootFolder so plugins can add items. /// </summary> public class AggregateFolder : Folder { - public AggregateFolder() - { - PhysicalLocationsList = Array.Empty<string>(); - } - - [IgnoreDataMember] - public override bool IsPhysicalRoot => true; - - public override bool CanDelete() - { - return false; - } - - [IgnoreDataMember] - public override bool SupportsPlayedStatus => false; + private readonly object _childIdsLock = new object(); /// <summary> - /// The _virtual children + /// The _virtual children. /// </summary> private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>(); + private bool _requiresRefresh; + private Guid[] _childrenIds = null; + + public AggregateFolder() + { + PhysicalLocationsList = Array.Empty<string>(); + } /// <summary> /// Gets the virtual children. @@ -45,18 +42,27 @@ namespace MediaBrowser.Controller.Entities /// <value>The virtual children.</value> public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren; - [IgnoreDataMember] + [JsonIgnore] + public override bool IsPhysicalRoot => true; + + [JsonIgnore] + public override bool SupportsPlayedStatus => false; + + [JsonIgnore] public override string[] PhysicalLocations => PhysicalLocationsList; public string[] PhysicalLocationsList { get; set; } + public override bool CanDelete() + { + return false; + } + protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } - private Guid[] _childrenIds = null; - private readonly object _childIdsLock = new object(); protected override List<BaseItem> LoadChildren() { lock (_childIdsLock) @@ -80,7 +86,6 @@ namespace MediaBrowser.Controller.Entities } } - private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; @@ -89,7 +94,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { @@ -100,11 +105,11 @@ namespace MediaBrowser.Controller.Entities return changed; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { ClearCache(); - var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } @@ -117,8 +122,7 @@ namespace MediaBrowser.Controller.Entities var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { - FileInfo = FileSystem.GetDirectoryInfo(path), - Path = path + FileInfo = FileSystem.GetDirectoryInfo(path) }; // Gather child folder and files @@ -150,11 +154,11 @@ namespace MediaBrowser.Controller.Entities return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); } - protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { ClearCache(); - await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken) .ConfigureAwait(false); ClearCache(); @@ -164,7 +168,7 @@ namespace MediaBrowser.Controller.Entities /// Adds the virtual child. /// </summary> /// <param name="child">The child.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if child is null.</exception> public void AddVirtualChild(BaseItem child) { if (child == null) @@ -180,7 +184,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="id">The id.</param> /// <returns>BaseItem.</returns> - /// <exception cref="ArgumentNullException">id</exception> + /// <exception cref="ArgumentNullException">The id is empty.</exception> public BaseItem FindVirtualChild(Guid id) { if (id.Equals(Guid.Empty)) @@ -195,6 +199,7 @@ namespace MediaBrowser.Controller.Entities return child; } } + return null; } } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 13a6fe44a..536668e50 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,16 +1,22 @@ +#nullable disable + +#pragma warning disable CA1002, CA1724, CA1826, CS1591 + using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class Audio + /// Class Audio. /// </summary> public class Audio : BaseItem, IHasAlbumArtist, @@ -19,105 +25,79 @@ namespace MediaBrowser.Controller.Entities.Audio IHasLookupInfo<SongInfo>, IHasMediaSources { - /// <summary> - /// Gets or sets the artist. - /// </summary> - /// <value>The artist.</value> - [IgnoreDataMember] - public string[] Artists { get; set; } - - [IgnoreDataMember] - public string[] AlbumArtists { get; set; } - public Audio() { Artists = Array.Empty<string>(); AlbumArtists = Array.Empty<string>(); } - public override double GetDefaultPrimaryImageAspectRatio() - { - return 1; - } + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } + + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> AlbumArtists { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] - public override bool SupportsPeople => false; + [JsonIgnore] + public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => false; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; - public override bool CanDownload() - { - return IsFileProtocol; - } - - [IgnoreDataMember] - public string[] AllArtists - { - get - { - var list = new string[AlbumArtists.Length + Artists.Length]; - - var index = 0; - foreach (var artist in AlbumArtists) - { - list[index] = artist; - index++; - } - foreach (var artist in Artists) - { - list[index] = artist; - index++; - } - - return list; - - } - } - - [IgnoreDataMember] + [JsonIgnore] public MusicAlbum AlbumEntity => FindParent<MusicAlbum>(); /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Audio; + public override double GetDefaultPrimaryImageAspectRatio() + { + return 1; + } + + public override bool CanDownload() + { + return IsFileProtocol; + } + /// <summary> /// Creates the name of the sort. /// </summary> /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "") - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); - var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty; - + var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty; if (ParentIndexNumber.HasValue) { - songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey; + songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey; } + songKey += Name; if (!string.IsNullOrEmpty(Album)) @@ -125,7 +105,7 @@ namespace MediaBrowser.Controller.Entities.Audio songKey = Album + "-" + songKey; } - var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0]; + var albumArtist = AlbumArtists.FirstOrDefault(); if (!string.IsNullOrEmpty(albumArtist)) { songKey = albumArtist + "-" + songKey; @@ -142,6 +122,7 @@ namespace MediaBrowser.Controller.Entities.Audio { return UnratedItem.Music; } + return base.GetBlockUnratedType(); } diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index a269b3486..1625c748a 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -1,14 +1,33 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Library; + namespace MediaBrowser.Controller.Entities.Audio { public interface IHasAlbumArtist { - string[] AlbumArtists { get; set; } + IReadOnlyList<string> AlbumArtists { get; set; } } public interface IHasArtist { - string[] AllArtists { get; } + /// <summary> + /// Gets or sets the artists. + /// </summary> + /// <value>The artists.</value> + IReadOnlyList<string> Artists { get; set; } + } - string[] Artists { get; set; } + public static class Extentions + { + public static IEnumerable<string> GetAllArtists<T>(this T item) + where T : IHasArtist, IHasAlbumArtist + { + return item.AlbumArtists.Concat(item.Artists).DistinctNames(); + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs index 5ae056050..c2dae5a2d 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + namespace MediaBrowser.Controller.Entities.Audio { public interface IHasMusicGenres diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 5b2939b75..03d1f3304 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,48 +1,74 @@ +#nullable disable + +#pragma warning disable CA1721, CA1826, CS1591 + using System; using System.Collections.Generic; 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.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class MusicAlbum + /// Class MusicAlbum. /// </summary> public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer { - public string[] AlbumArtists { get; set; } - public string[] Artists { get; set; } - public MusicAlbum() { Artists = Array.Empty<string>(); AlbumArtists = Array.Empty<string>(); } - [IgnoreDataMember] + /// <inheritdoc /> + public IReadOnlyList<string> AlbumArtists { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<string> Artists { get; set; } + + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true)); + [JsonIgnore] + public override bool SupportsPlayedStatus => false; + + [JsonIgnore] + public override bool SupportsCumulativeRunTimeTicks => true; + + [JsonIgnore] + public string AlbumArtist => AlbumArtists.FirstOrDefault(); + + [JsonIgnore] + public override bool SupportsPeople => false; + + /// <summary> + /// Gets the tracks. + /// </summary> + /// <value>The tracks.</value> + [JsonIgnore] + public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>(); + public MusicArtist GetMusicArtist(DtoOptions options) { var parents = GetParents(); foreach (var parent in parents) { - var artist = parent as MusicArtist; - if (artist != null) + if (parent is MusicArtist artist) { return artist; } @@ -53,51 +79,10 @@ namespace MediaBrowser.Controller.Entities.Audio { return LibraryManager.GetArtist(name, options); } - return null; - } - - [IgnoreDataMember] - public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] - public override bool SupportsCumulativeRunTimeTicks => true; - - [IgnoreDataMember] - public string[] AllArtists - { - get - { - var list = new string[AlbumArtists.Length + Artists.Length]; - - var index = 0; - foreach (var artist in AlbumArtists) - { - list[index] = artist; - index++; - } - foreach (var artist in Artists) - { - list[index] = artist; - index++; - } - - return list; - } + return null; } - [IgnoreDataMember] - public string AlbumArtist => AlbumArtists.Length == 0 ? null : AlbumArtists[0]; - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - /// <summary> - /// Gets the tracks. - /// </summary> - /// <value>The tracks.</value> - [IgnoreDataMember] - public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>(); - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return Tracks; @@ -118,14 +103,14 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, albumArtist + "-" + Name); } - var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum); + var id = this.GetProviderId(MetadataProvider.MusicBrainzAlbum); if (!string.IsNullOrEmpty(id)) { list.Insert(0, "MusicAlbum-Musicbrainz-" + id); } - id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + id = this.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); if (!string.IsNullOrEmpty(id)) { @@ -135,9 +120,9 @@ namespace MediaBrowser.Controller.Entities.Audio return list; } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Music); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music); } public override UnratedItem GetBlockUnratedType() @@ -216,8 +201,7 @@ namespace MediaBrowser.Controller.Entities.Audio private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { - var all = AllArtists; - foreach (var i in all) + foreach (var i in this.GetAllArtists()) { // This should not be necessary but we're seeing some cases of it if (string.IsNullOrEmpty(i)) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 2d464bd32..f30f8ce7f 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,44 +1,79 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Extensions; +using Diacritics.Extensions; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class MusicArtist + /// Class MusicArtist. /// </summary> public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { - [IgnoreDataMember] + [JsonIgnore] public bool IsAccessedByName => ParentId.Equals(Guid.Empty); - [IgnoreDataMember] + [JsonIgnore] public override bool IsFolder => !IsAccessedByName; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsCumulativeRunTimeTicks => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override IEnumerable<BaseItem> Children + { + get + { + if (IsAccessedByName) + { + return new List<BaseItem>(); + } + + return base.Children; + } + } + + [JsonIgnore] + public override bool SupportsPeople => false; + + public static string GetPath(string name) + { + return GetPath(name, true); + } + public override double GetDefaultPrimaryImageAspectRatio() { return 1; @@ -53,34 +88,16 @@ namespace MediaBrowser.Controller.Entities.Audio { if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name }; + query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) }; query.ArtistIds = new[] { Id }; } return LibraryManager.GetItemList(query); } - [IgnoreDataMember] - public override IEnumerable<BaseItem> Children - { - get - { - if (IsAccessedByName) - { - return new List<BaseItem>(); - } - - return base.Children; - } - } - public override int GetChildCount(User user) { - if (IsAccessedByName) - { - return 0; - } - return base.GetChildCount(user); + return IsAccessedByName ? 0 : base.GetChildCount(user); } public override bool IsSaveLocalMetadataEnabled() @@ -93,7 +110,7 @@ namespace MediaBrowser.Controller.Entities.Audio return base.IsSaveLocalMetadataEnabled(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (IsAccessedByName) { @@ -101,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.Audio return Task.CompletedTask; } - return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken); } public override List<string> GetUserDataKeys() @@ -113,14 +130,6 @@ namespace MediaBrowser.Controller.Entities.Audio } /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - /// <summary> /// Gets the user data key. /// </summary> /// <param name="item">The item.</param> @@ -128,7 +137,7 @@ namespace MediaBrowser.Controller.Entities.Audio private static List<string> GetUserDataKeys(MusicArtist item) { var list = new List<string>(); - var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); + var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (!string.IsNullOrEmpty(id)) { @@ -138,13 +147,15 @@ namespace MediaBrowser.Controller.Entities.Audio list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); } - protected override bool GetBlockUnratedValue(UserPolicy config) + + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Music); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music); } public override UnratedItem GetBlockUnratedType() @@ -164,14 +175,6 @@ namespace MediaBrowser.Controller.Entities.Audio return info; } - [IgnoreDataMember] - public override bool SupportsPeople => false; - - public static string GetPath(string name) - { - return GetPath(name, true); - } - public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that @@ -198,15 +201,18 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Option to replace metadata.</param> + /// <returns>True if metadata changed.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (IsAccessedByName) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index d26aaf2bb..dc6fcc55a 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -1,16 +1,40 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.Audio { /// <summary> - /// Class MusicGenre + /// Class MusicGenre. /// </summary> public class MusicGenre : BaseItem, IItemByName { + [JsonIgnore] + public override bool SupportsAddingToPlaylist => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -18,28 +42,12 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; } - [IgnoreDataMember] - public override bool SupportsAddingToPlaylist => true; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - - [IgnoreDataMember] - public override bool IsDisplayedAsFolder => true; - - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - public override double GetDefaultPrimaryImageAspectRatio() { return 1; @@ -55,13 +63,10 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; - query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) }; return LibraryManager.GetItemList(query); } @@ -94,15 +99,18 @@ namespace MediaBrowser.Controller.Entities.Audio Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Option to replace metadata.</param> + /// <returns>True if metadata changed.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 65c8a5fdd..782481fbc 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -1,33 +1,41 @@ +#nullable disable + +#pragma warning disable CA1724, CS1591 + using System; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesSortName() { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 43fee79a1..838a9f2f8 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,141 +1,223 @@ +#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.Extensions; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; 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 MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class BaseItem + /// Class BaseItem. /// </summary> - public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo> + public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem> { - protected static MetadataFields[] EmptyMetadataFieldsArray = Array.Empty<MetadataFields>(); - protected static MediaUrl[] EmptyMediaUrlArray = Array.Empty<MediaUrl>(); - protected static ItemImageInfo[] EmptyItemImageInfoArray = Array.Empty<ItemImageInfo>(); - public static readonly LinkedChild[] EmptyLinkedChildArray = Array.Empty<LinkedChild>(); + /// <summary> + /// The trailer folder name. + /// </summary> + public const string TrailersFolderName = "trailers"; + public const string ThemeSongsFolderName = "theme-music"; + public const string ThemeSongFileName = "theme"; + public const string ThemeVideosFolderName = "backdrops"; + + /// <summary> + /// The supported image extensions. + /// </summary> + public static readonly string[] SupportedImageExtensions + = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + + private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions) + { + ".nfo", + ".xml", + ".srt", + ".vtt", + ".sub", + ".idx", + ".txt", + ".edl", + ".bif", + ".smi", + ".ttml" + }; + + /// <summary> + /// Extra types that should be counted and displayed as "Special Features" in the UI. + /// </summary> + public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType> + { + 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 + }; + + public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; + + /// <summary> + /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />. + /// </summary> + public static readonly Dictionary<string, ExtraType> AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase) + { + ["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown, + ["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes, + ["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene, + ["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview, + ["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene, + ["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample, + ["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip, + ["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip + }; + + private string _sortName; + private Guid[] _themeSongIds; + private Guid[] _themeVideoIds; + + private string _forcedSortName; + + private string _name; + + public const char SlugChar = '-'; protected BaseItem() { - ThemeSongIds = Array.Empty<Guid>(); - ThemeVideoIds = Array.Empty<Guid>(); Tags = Array.Empty<string>(); Genres = Array.Empty<string>(); Studios = Array.Empty<string>(); ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - LockedFields = EmptyMetadataFieldsArray; - ImageInfos = EmptyItemImageInfoArray; + LockedFields = Array.Empty<MetadataField>(); + ImageInfos = Array.Empty<ItemImageInfo>(); ProductionLocations = Array.Empty<string>(); RemoteTrailers = Array.Empty<MediaUrl>(); ExtraIds = Array.Empty<Guid>(); } - public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; - public static char SlugChar = '-'; + [JsonIgnore] + public Guid[] ThemeSongIds + { + get + { + return _themeSongIds ??= GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong) + .Select(song => song.Id) + .ToArray(); + } - /// <summary> - /// The supported image extensions - /// </summary> - public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + private set + { + _themeSongIds = value; + } + } - /// <summary> - /// The trailer folder name - /// </summary> - public static string TrailerFolderName = "trailers"; - public static string ThemeSongsFolderName = "theme-music"; - public static string ThemeSongFilename = "theme"; - public static string ThemeVideosFolderName = "backdrops"; + [JsonIgnore] + public Guid[] ThemeVideoIds + { + get + { + return _themeVideoIds ??= GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo) + .Select(song => song.Id) + .ToArray(); + } - [IgnoreDataMember] - public Guid[] ThemeSongIds { get; set; } - [IgnoreDataMember] - public Guid[] ThemeVideoIds { get; set; } + private set + { + _themeVideoIds = value; + } + } - [IgnoreDataMember] + [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public string PreferredMetadataLanguage { get; set; } public long? Size { get; set; } + public string Container { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string Tagline { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual ItemImageInfo[] ImageInfos { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsVirtualItem { get; set; } /// <summary> /// Gets or sets the album. /// </summary> /// <value>The album.</value> - [IgnoreDataMember] + [JsonIgnore] public string Album { get; set; } /// <summary> /// Gets or sets the channel identifier. /// </summary> /// <value>The channel identifier.</value> - [IgnoreDataMember] + [JsonIgnore] public Guid ChannelId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsAddingToPlaylist => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool AlwaysScanInternalMetadataPath => false; /// <summary> - /// Gets a value indicating whether this instance is in mixed folder. + /// Gets or sets a value indicating whether this instance is in mixed folder. /// </summary> /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsInMixedFolder { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPositionTicksResume => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsRemoteImageDownloading => true; - private string _name; /// <summary> /// Gets or sets the name. /// </summary> /// <value>The name.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string Name { get => _name; @@ -148,43 +230,44 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; - [IgnoreDataMember] + [JsonIgnore] public int? TotalBitrate { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public ExtraType? ExtraType { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); - [IgnoreDataMember] + [JsonIgnore] public string OriginalTitle { get; set; } /// <summary> /// Gets or sets the id. /// </summary> /// <value>The id.</value> - [IgnoreDataMember] + [JsonIgnore] public Guid Id { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid OwnerId { get; set; } /// <summary> /// Gets or sets the audio. /// </summary> /// <value>The audio.</value> - [IgnoreDataMember] + [JsonIgnore] public ProgramAudio? Audio { get; set; } /// <summary> - /// Return the id that should be used to key display prefs for this item. + /// 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. /// </summary> /// <value>The display prefs id.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual Guid DisplayPreferencesId { get @@ -198,10 +281,10 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string Path { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual SourceType SourceType { get @@ -216,10 +299,10 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. /// </summary> - [IgnoreDataMember] + [JsonIgnore] public virtual string ContainingFolderPath { get @@ -237,47 +320,44 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name of the service. /// </summary> /// <value>The name of the service.</value> - [IgnoreDataMember] + [JsonIgnore] public string ServiceName { get; set; } /// <summary> - /// If this content came from an external service, the id of the content on that service + /// Gets or sets the external id. /// </summary> - [IgnoreDataMember] + /// <remarks> + /// If this content came from an external service, the id of the content on that service. + /// </remarks> + [JsonIgnore] public string ExternalId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string ExternalSeriesId { get; set; } /// <summary> /// Gets or sets the etag. /// </summary> /// <value>The etag.</value> - [IgnoreDataMember] + [JsonIgnore] public string ExternalEtag { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsHidden => false; - public BaseItem GetOwner() - { - var ownerId = OwnerId; - return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); - } - /// <summary> - /// Gets or sets the type of the location. + /// Gets the type of the location. /// </summary> /// <value>The type of the location.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual LocationType LocationType { get { - //if (IsOffline) - //{ + // if (IsOffline) + // { // return LocationType.Offline; - //} + // } var path = Path; if (string.IsNullOrEmpty(path)) @@ -294,7 +374,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public MediaProtocol? PathProtocol { get @@ -310,20 +390,13 @@ namespace MediaBrowser.Controller.Entities } } - public bool IsPathProtocol(MediaProtocol protocol) - { - var current = PathProtocol; - - return current.HasValue && current.Value == protocol; - } - - [IgnoreDataMember] + [JsonIgnore] public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File); - [IgnoreDataMember] + [JsonIgnore] public bool HasPathProtocol => PathProtocol.HasValue; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsLocalMetadata { get @@ -337,7 +410,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual string FileNameWithoutExtension { get @@ -351,452 +424,165 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual bool EnableAlphaNumericSorting => true; - private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) - { - var list = new List<Tuple<StringBuilder, bool>>(); - - int thisMarker = 0; - - while (thisMarker < s1.Length) - { - if (thisMarker >= s1.Length) - { - break; - } - char thisCh = s1[thisMarker]; + public virtual bool IsHD => Height >= 720; - var thisChunk = new StringBuilder(); + public bool IsShortcut { get; set; } - while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0]))) - { - thisChunk.Append(thisCh); - thisMarker++; + public string ShortcutPath { get; set; } - if (thisMarker < s1.Length) - { - thisCh = s1[thisMarker]; - } - } + public int Width { get; set; } - var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]); - list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric)); - } + public int Height { get; set; } - return list; - } + public Guid[] ExtraIds { get; set; } /// <summary> - /// This is just a helper for convenience + /// Gets the primary image path. /// </summary> + /// <remarks> + /// This is just a helper for convenience. + /// </remarks> /// <value>The primary image path.</value> - [IgnoreDataMember] + [JsonIgnore] public string PrimaryImagePath => this.GetImagePath(ImageType.Primary); - public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name) - { - if (SourceType == SourceType.Channel) - { - // hack alert - return !EnableMediaSourceDisplay; - } - - var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); - if (typeOptions != null) - { - return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - if (!libraryOptions.EnableInternetProviders) - { - return false; - } - - var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name) - { - if (this is Channel) - { - // hack alert - return true; - } - if (SourceType == SourceType.Channel) - { - // hack alert - return !EnableMediaSourceDisplay; - } - - var typeOptions = libraryOptions.GetTypeOptions(GetType().Name); - if (typeOptions != null) - { - return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - if (!libraryOptions.EnableInternetProviders) - { - return false; - } - - var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - - return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); - } - - public virtual bool CanDelete() - { - if (SourceType == SourceType.Channel) - { - return ChannelManager.CanDelete(this); - } - - return IsFileProtocol; - } - - public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) - { - if (user.Policy.EnableContentDeletion) - { - return true; - } - - var allowed = user.Policy.EnableContentDeletionFromFolders; - - if (SourceType == SourceType.Channel) - { - return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase); - } - else - { - var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders); - - foreach (var folder in collectionFolders) - { - if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - public bool CanDelete(User user, List<Folder> allCollectionFolders) - { - return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); - } - - public bool CanDelete(User user) - { - var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); - - return CanDelete(user, allCollectionFolders); - } - - public virtual bool CanDownload() - { - return false; - } - - public virtual bool IsAuthorizedToDownload(User user) - { - return user.Policy.EnableContentDownloading; - } - - public bool CanDownload(User user) - { - return CanDownload() && IsAuthorizedToDownload(user); - } - /// <summary> /// Gets or sets the date created. /// </summary> /// <value>The date created.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime DateCreated { get; set; } /// <summary> /// Gets or sets the date modified. /// </summary> /// <value>The date modified.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime DateModified { get; set; } - [IgnoreDataMember] public DateTime DateLastSaved { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime DateLastRefreshed { get; set; } - /// <summary> - /// The logger - /// </summary> - 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; } - - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> - public override string ToString() - { - return Name; - } - - [IgnoreDataMember] + [JsonIgnore] public bool IsLocked { get; set; } /// <summary> /// Gets or sets the locked fields. /// </summary> /// <value>The locked fields.</value> - [IgnoreDataMember] - public MetadataFields[] LockedFields { get; set; } + [JsonIgnore] + public MetadataField[] LockedFields { get; set; } /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string MediaType => null; - [IgnoreDataMember] + [JsonIgnore] public virtual string[] PhysicalLocations { get { if (!IsFileProtocol) { - return new string[] { }; + return Array.Empty<string>(); } return new[] { Path }; } } - private string _forcedSortName; - /// <summary> - /// Gets or sets the name of the forced sort. - /// </summary> - /// <value>The name of the forced sort.</value> - [IgnoreDataMember] - public string ForcedSortName - { - get => _forcedSortName; - set { _forcedSortName = value; _sortName = null; } - } - - private string _sortName; - /// <summary> - /// Gets the name of the sort. - /// </summary> - /// <value>The name of the sort.</value> - [IgnoreDataMember] - public string SortName + [JsonIgnore] + public bool EnableMediaSourceDisplay { get { - if (_sortName == null) + if (SourceType == SourceType.Channel) { - if (!string.IsNullOrEmpty(ForcedSortName)) - { - // Need the ToLower because that's what CreateSortName does - _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant(); - } - else - { - _sortName = CreateSortName(); - } + return ChannelManager.EnableMediaSourceDisplay(this); } - return _sortName; - } - set => _sortName = value; - } - - 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.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N")); + return true; } - - var idString = Id.ToString("N"); - - basePath = System.IO.Path.Combine(basePath, "library"); - - return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); } + [JsonIgnore] + public Guid ParentId { get; set; } + /// <summary> - /// Creates the name of the sort. + /// Gets or sets the logger. /// </summary> - /// <returns>System.String.</returns> - 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(); + public static ILogger<BaseItem> Logger { get; set; } - foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) - { - sortable = sortable.Replace(removeChar, string.Empty); - } - - foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) - { - sortable = sortable.Replace(replaceChar, " "); - } - - foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) - { - // Remove from beginning if a space follows - if (sortable.StartsWith(search + " ")) - { - sortable = sortable.Remove(0, search.Length + 1); - } - // Remove from middle if surrounded by spaces - sortable = sortable.Replace(" " + search + " ", " "); - - // Remove from end if followed by a space - if (sortable.EndsWith(" " + search)) - { - sortable = sortable.Remove(sortable.Length - (search.Length + 1)); - } - } - - return ModifySortChunks(sortable); - } + public static ILibraryManager LibraryManager { get; set; } - private string ModifySortChunks(string name) - { - var chunks = GetSortChunks(name); + public static IServerConfigurationManager ConfigurationManager { get; set; } - var builder = new StringBuilder(); + public static IProviderManager ProviderManager { get; set; } - foreach (var chunk in chunks) - { - var chunkBuilder = chunk.Item1; + public static ILocalizationManager LocalizationManager { get; set; } - // This chunk is numeric - if (chunk.Item2) - { - while (chunkBuilder.Length < 10) - { - chunkBuilder.Insert(0, '0'); - } - } + public static IItemRepository ItemRepository { get; set; } - builder.Append(chunkBuilder); - } - //logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); - return builder.ToString().RemoveDiacritics(); - } + public static IFileSystem FileSystem { get; set; } - [IgnoreDataMember] - public bool EnableMediaSourceDisplay - { - get - { - if (SourceType == SourceType.Channel) - { - return ChannelManager.EnableMediaSourceDisplay(this); - } + public static IUserDataManager UserDataManager { get; set; } - return true; - } - } + public static IChannelManager ChannelManager { get; set; } - [IgnoreDataMember] - public Guid ParentId { get; set; } + public static IMediaSourceManager MediaSourceManager { get; set; } /// <summary> - /// Gets or sets the parent. + /// Gets or sets the name of the forced sort. /// </summary> - /// <value>The parent.</value> - [IgnoreDataMember] - public Folder Parent + /// <value>The name of the forced sort.</value> + [JsonIgnore] + public string ForcedSortName { - get => GetParent() as Folder; + get => _forcedSortName; set { - - } - } - - public void SetParent(Folder parent) - { - ParentId = parent == null ? Guid.Empty : parent.Id; - } - - public BaseItem GetParent() - { - var parentId = ParentId; - if (!parentId.Equals(Guid.Empty)) - { - return LibraryManager.GetItemById(parentId); - } - - return null; - } - - public IEnumerable<BaseItem> GetParents() - { - var parent = GetParent(); - - while (parent != null) - { - yield return parent; - - parent = parent.GetParent(); + _forcedSortName = value; + _sortName = null; } } /// <summary> - /// Finds a parent of a given type + /// Gets or sets the name of the sort. /// </summary> - /// <typeparam name="T"></typeparam> - /// <returns>``0.</returns> - public T FindParent<T>() - where T : Folder + /// <value>The name of the sort.</value> + [JsonIgnore] + public string SortName { - foreach (var parent in GetParents()) + get { - var item = parent as T; - if (item != null) + if (_sortName == null) { - return item; + if (!string.IsNullOrEmpty(ForcedSortName)) + { + // Need the ToLower because that's what CreateSortName does + _sortName = ModifySortChunks(ForcedSortName).ToLowerInvariant(); + } + else + { + _sortName = CreateSortName(); + } } + + return _sortName; } - return null; + + set => _sortName = value; } - [IgnoreDataMember] + [JsonIgnore] public virtual Guid DisplayParentId { get @@ -806,7 +592,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public BaseItem DisplayParent { get @@ -816,126 +602,127 @@ namespace MediaBrowser.Controller.Entities { return null; } + return LibraryManager.GetItemById(id); } } /// <summary> - /// When the item first debuted. For movies this could be premiere date, episodes would be first aired + /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be first aired. /// </summary> /// <value>The premiere date.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime? PremiereDate { get; set; } /// <summary> /// Gets or sets the end date. /// </summary> /// <value>The end date.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime? EndDate { get; set; } /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> - [IgnoreDataMember] + [JsonIgnore] public string OfficialRating { get; set; } - [IgnoreDataMember] + [JsonIgnore] public int InheritedParentalRatingValue { get; set; } /// <summary> /// Gets or sets the critic rating. /// </summary> /// <value>The critic rating.</value> - [IgnoreDataMember] + [JsonIgnore] public float? CriticRating { get; set; } /// <summary> /// Gets or sets the custom rating. /// </summary> /// <value>The custom rating.</value> - [IgnoreDataMember] + [JsonIgnore] public string CustomRating { get; set; } /// <summary> /// Gets or sets the overview. /// </summary> /// <value>The overview.</value> - [IgnoreDataMember] + [JsonIgnore] public string Overview { get; set; } /// <summary> /// Gets or sets the studios. /// </summary> /// <value>The studios.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Studios { get; set; } /// <summary> /// Gets or sets the genres. /// </summary> /// <value>The genres.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Genres { get; set; } /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Tags { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string[] ProductionLocations { get; set; } /// <summary> /// Gets or sets the home page URL. /// </summary> /// <value>The home page URL.</value> - [IgnoreDataMember] + [JsonIgnore] public string HomePageUrl { get; set; } /// <summary> /// Gets or sets the community rating. /// </summary> /// <value>The community rating.</value> - [IgnoreDataMember] + [JsonIgnore] public float? CommunityRating { get; set; } /// <summary> /// Gets or sets the run time ticks. /// </summary> /// <value>The run time ticks.</value> - [IgnoreDataMember] + [JsonIgnore] public long? RunTimeTicks { get; set; } /// <summary> /// Gets or sets the production year. /// </summary> /// <value>The production year.</value> - [IgnoreDataMember] + [JsonIgnore] public int? ProductionYear { get; set; } /// <summary> - /// If the item is part of a series, this is it's number in the series. + /// 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. /// </summary> /// <value>The index number.</value> - [IgnoreDataMember] + [JsonIgnore] public int? IndexNumber { get; set; } /// <summary> - /// For an episode this could be the season number, or for a song this could be the disc number. + /// 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. /// </summary> /// <value>The parent index number.</value> - [IgnoreDataMember] + [JsonIgnore] public int? ParentIndexNumber { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool HasLocalAlternateVersions => false; - [IgnoreDataMember] + [JsonIgnore] public string OfficialRatingForComparison { get @@ -956,7 +743,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public string CustomRatingForComparison { get @@ -978,21 +765,364 @@ namespace MediaBrowser.Controller.Entities } /// <summary> + /// Gets or sets the provider ids. + /// </summary> + /// <value>The provider ids.</value> + [JsonIgnore] + public Dictionary<string, string> 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(Guid.Empty) && IsFileProtocol; + + [JsonIgnore] + public virtual bool SupportsPeople => false; + + [JsonIgnore] + public virtual bool SupportsThemeMedia => false; + + [JsonIgnore] + public virtual bool SupportsInheritedParentImages => false; + + /// <summary> + /// Gets a value indicating whether this instance is folder. + /// </summary> + /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> + [JsonIgnore] + public virtual bool IsFolder => false; + + [JsonIgnore] + public virtual bool IsDisplayedAsFolder => false; + + /// <summary> + /// Gets or sets the remote trailers. + /// </summary> + /// <value>The remote trailers.</value> + public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } + + public virtual bool SupportsExternalTransfer => false; + + public virtual double GetDefaultPrimaryImageAspectRatio() + { + return 0; + } + + public virtual string CreatePresentationUniqueKey() + { + return Id.ToString("N", CultureInfo.InvariantCulture); + } + + public bool IsPathProtocol(MediaProtocol protocol) + { + var current = PathProtocol; + + return current.HasValue && current.Value == protocol; + } + + private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) + { + var list = new List<Tuple<StringBuilder, bool>>(); + + 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<StringBuilder, bool>(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<Folder> allCollectionFolders) + { + if (user.HasPermission(PermissionKind.EnableContentDeletion)) + { + return true; + } + + var allowed = user.GetPreferenceValues<Guid>(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(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId); + } + + public bool CanDelete(User user, List<Folder> allCollectionFolders) + { + return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders); + } + + public bool CanDelete(User user) + { + var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().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); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="string" /> that represents this instance.</returns> + 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<char> idString = Id.ToString("N", CultureInfo.InvariantCulture); + + return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString); + } + + /// <summary> + /// Creates the name of the sort. + /// </summary> + /// <returns>System.String.</returns> + 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(Guid.Empty)) + { + return LibraryManager.GetItemById(parentId); + } + + return null; + } + + public IEnumerable<BaseItem> GetParents() + { + var parent = GetParent(); + + while (parent != null) + { + yield return parent; + + parent = parent.GetParent(); + } + } + + /// <summary> + /// Finds a parent of a given type. + /// </summary> + /// <typeparam name="T">Type of parent.</typeparam> + /// <returns>``0.</returns> + public T FindParent<T>() + where T : Folder + { + foreach (var parent in GetParents()) + { + if (parent is T item) + { + return item; + } + } + + return null; + } + + /// <summary> /// Gets the play access. /// </summary> /// <param name="user">The user.</param> /// <returns>PlayAccess.</returns> public PlayAccess GetPlayAccess(User user) { - if (!user.Policy.EnableMediaPlayback) + if (!user.HasPermission(PermissionKind.EnableMediaPlayback)) { return PlayAccess.None; } - //if (!user.IsParentalScheduleAllowed()) - //{ + // if (!user.IsParentalScheduleAllowed()) + // { // return PlayAccess.None; - //} + // } return PlayAccess.Full; } @@ -1042,7 +1172,6 @@ namespace MediaBrowser.Controller.Entities } return 1; - }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => { @@ -1069,9 +1198,10 @@ namespace MediaBrowser.Controller.Entities var info = new MediaSourceInfo { - Id = item.Id.ToString("N"), + 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, @@ -1087,7 +1217,7 @@ namespace MediaBrowser.Controller.Entities if (info.Protocol == MediaProtocol.File) { - info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", CultureInfo.InvariantCulture); } var video = item as Video; @@ -1192,11 +1322,11 @@ namespace MediaBrowser.Controller.Entities { if (video.IsoType.HasValue) { - if (video.IsoType.Value == Model.Entities.IsoType.BluRay) + if (video.IsoType.Value == IsoType.BluRay) { terms.Add("Bluray"); } - else if (video.IsoType.Value == Model.Entities.IsoType.Dvd) + else if (video.IsoType.Value == IsoType.Dvd) { terms.Add("DVD"); } @@ -1208,7 +1338,7 @@ namespace MediaBrowser.Controller.Entities } } - return string.Join("/", terms.ToArray()); + return string.Join('/', terms.ToArray()); } /// <summary> @@ -1224,8 +1354,7 @@ namespace MediaBrowser.Controller.Entities // Support plex/xbmc convention files.AddRange(fileSystemChildren - .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) - ); + .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFileName, StringComparison.OrdinalIgnoreCase))); return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType<Audio.Audio>() @@ -1265,16 +1394,15 @@ namespace MediaBrowser.Controller.Entities .Select(item => { // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(item.Id) as Video; - if (dbItem != null) + if (LibraryManager.GetItemById(item.Id) is Video dbItem) { item = dbItem; } else { // item is new - item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; + item.ExtraType = Model.Entities.ExtraType.ThemeVideo; } return item; @@ -1285,54 +1413,45 @@ namespace MediaBrowser.Controller.Entities protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var files = fileSystemChildren.Where(i => i.IsDirectory) - .SelectMany(i => FileSystem.GetFiles(i.FullName)); - - return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) - .OfType<Video>() - .Select(item => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(item.Id) as Video; - - if (dbItem != null) - { - item = dbItem; - } - else + return fileSystemChildren + .Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name)) + .SelectMany(folder => LibraryManager + .ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions()) + .OfType<Video>() + .Select(video => { - // item is new - item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip; - } - - return item; + // Try to retrieve it from the db. If we don't find it, use the resolved version + if (LibraryManager.GetItemById(video.Id) is Video dbItem) + { + video = dbItem; + } - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToArray(); + video.ExtraType = AllExtrasTypesFolderNames[folder.Name]; + return video; + }) + .OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes + .ToArray(); } - public Task RefreshMetadata(CancellationToken cancellationToken) { - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } protected virtual void TriggerOnRefreshStart() { - } protected virtual void TriggerOnRefreshComplete() { - } /// <summary> - /// Overrides the base implementation to refresh metadata for local trailers + /// Overrides the base implementation to refresh metadata for local trailers. /// </summary> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>true if a provider reports we changed</returns> + /// <returns>true if a provider reports we changed.</returns> public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken) { TriggerOnRefreshStart(); @@ -1348,6 +1467,7 @@ namespace MediaBrowser.Controller.Entities new List<FileSystemMetadata>(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); + await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh if (ownedItemsChanged) { @@ -1377,23 +1497,55 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] - protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol; + protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) + { + if (!IsVisible(user)) + { + return false; + } - [IgnoreDataMember] - public virtual bool SupportsPeople => false; + if (GetParents().Any(i => !i.IsVisible(user))) + { + return false; + } - [IgnoreDataMember] - public virtual bool SupportsThemeMedia => 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; + } /// <summary> /// Refreshes owned items such as trailers, theme videos, special features, etc. /// Returns true or false indicating if changes were found. /// </summary> - /// <param name="options"></param> - /// <param name="fileSystemChildren"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="options">The metadata refresh options.</param> + /// <param name="fileSystemChildren">The list of filesystem children.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns> protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var themeSongsChanged = false; @@ -1470,7 +1622,13 @@ namespace MediaBrowser.Controller.Entities private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { - var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService).Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)).Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService)); + var extras = LoadExtras(fileSystemChildren, options.DirectoryService); + var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService); + var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService); + var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length]; + extras.CopyTo(newExtras, 0); + themeVideos.CopyTo(newExtras, extras.Length); + themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length); var newExtraIds = newExtras.Select(i => i.Id).ToArray(); @@ -1482,7 +1640,15 @@ namespace MediaBrowser.Controller.Entities var tasks = newExtras.Select(i => { - return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken); + var subOptions = new MetadataRefreshOptions(options); + if (i.OwnerId != ownerId || i.ParentId != Guid.Empty) + { + i.OwnerId = ownerId; + i.ParentId = Guid.Empty; + subOptions.ForceSave = true; + } + + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); await Task.WhenAll(tasks).ConfigureAwait(false); @@ -1523,7 +1689,8 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - item.ThemeVideoIds = newThemeVideoIds; + // They are expected to be sorted by SortName + item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); return themeVideosChanged; } @@ -1560,34 +1727,12 @@ namespace MediaBrowser.Controller.Entities await Task.WhenAll(tasks).ConfigureAwait(false); - item.ThemeSongIds = newThemeSongIds; + // They are expected to be sorted by SortName + item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray(); return themeSongsChanged; } - /// <summary> - /// Gets or sets the provider ids. - /// </summary> - /// <value>The provider ids.</value> - [IgnoreDataMember] - public Dictionary<string, string> ProviderIds { get; set; } - - [IgnoreDataMember] - public virtual Folder LatestItemsIndexContainer => null; - - public virtual double GetDefaultPrimaryImageAspectRatio() - { - return 0; - } - - public virtual string CreatePresentationUniqueKey() - { - return Id.ToString("N"); - } - - [IgnoreDataMember] - public string PresentationUniqueKey { get; set; } - public string GetPresentationUniqueKey() { return PresentationUniqueKey ?? CreatePresentationUniqueKey(); @@ -1715,11 +1860,11 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Determines if a given user has access to this item + /// Determines if a given user has access to this item. /// </summary> /// <param name="user">The user.</param> /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">user</exception> + /// <exception cref="ArgumentNullException">If user is null.</exception> public bool IsParentalAllowed(User user) { if (user == null) @@ -1732,7 +1877,7 @@ namespace MediaBrowser.Controller.Entities return false; } - var maxAllowedRating = user.Policy.MaxParentalRating; + var maxAllowedRating = user.MaxParentalAgeRating; if (maxAllowedRating == null) { @@ -1748,7 +1893,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { - return !GetBlockUnratedValue(user.Policy); + return !GetBlockUnratedValue(user); } var value = LocalizationManager.GetRatingLevel(rating); @@ -1756,7 +1901,7 @@ namespace MediaBrowser.Controller.Entities // Could not determine the integer value if (!value.HasValue) { - var isAllowed = !GetBlockUnratedValue(user.Policy); + var isAllowed = !GetBlockUnratedValue(user); if (!isAllowed) { @@ -1818,8 +1963,7 @@ namespace MediaBrowser.Controller.Entities private bool IsVisibleViaTags(User user) { - var policy = user.Policy; - if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) + if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -1845,22 +1989,18 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Gets the block unrated value. /// </summary> - /// <param name="config">The configuration.</param> + /// <param name="user">The configuration.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - protected virtual bool GetBlockUnratedValue(UserPolicy config) + 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) - { - return false; - } - if (this is IItemByName) + if (IsFolder || this is IItemByName) { return false; } - return config.BlockUnratedItems.Contains(GetBlockUnratedType()); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType()); } /// <summary> @@ -1869,7 +2009,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="user">The user.</param> /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">user</exception> + /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception> public virtual bool IsVisible(User user) { if (user == null) @@ -1890,58 +2030,9 @@ namespace MediaBrowser.Controller.Entities return IsVisibleStandaloneInternal(user, true); } - [IgnoreDataMember] - public virtual bool SupportsInheritedParentImages => false; - - 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; - } - - /// <summary> - /// Gets a value indicating whether this instance is folder. - /// </summary> - /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public virtual bool IsFolder => false; - - [IgnoreDataMember] - public virtual bool IsDisplayedAsFolder => false; - public virtual string GetClientTypeName() { - if (IsFolder && SourceType == SourceType.Channel && !(this is Channel)) + if (IsFolder && SourceType == SourceType.Channel && this is not Channel) { return "ChannelFolderItem"; } @@ -1949,6 +2040,11 @@ namespace MediaBrowser.Controller.Entities return GetType().Name; } + public BaseItemKind GetBaseItemKind() + { + return Enum.Parse<BaseItemKind>(GetClientTypeName()); + } + /// <summary> /// Gets the linked child. /// </summary> @@ -2001,7 +2097,7 @@ namespace MediaBrowser.Controller.Entities if (itemByPath == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return itemByPath; @@ -2013,7 +2109,7 @@ namespace MediaBrowser.Controller.Entities if (item == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return item; @@ -2022,14 +2118,11 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] - public virtual bool EnableRememberingTrackSelections => true; - /// <summary> - /// Adds a studio to the item + /// Adds a studio to the item. /// </summary> /// <param name="name">The name.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if name is null.</exception> public void AddStudio(string name) { if (string.IsNullOrEmpty(name)) @@ -2041,14 +2134,17 @@ namespace MediaBrowser.Controller.Entities if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) { - if (current.Length == 0) + int curLen = current.Length; + if (curLen == 0) { Studios = new[] { name }; } else { - var list = - Studios = current.Concat(new[] { name }).ToArray(); + var newArr = new string[curLen + 1]; + current.CopyTo(newArr, 0); + newArr[curLen] = name; + Studios = newArr; } } } @@ -2059,10 +2155,10 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Adds a genre to the item + /// Adds a genre to the item. /// </summary> /// <param name="name">The name.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throwns if name is null.</exception> public void AddGenre(string name) { if (string.IsNullOrEmpty(name)) @@ -2085,9 +2181,9 @@ namespace MediaBrowser.Controller.Entities /// <param name="user">The user.</param> /// <param name="datePlayed">The date played.</param> /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> - public virtual void MarkPlayed(User user, + /// <exception cref="ArgumentNullException">Throws if user is null.</exception> + public virtual void MarkPlayed( + User user, DateTime? datePlayed, bool resetPosition) { @@ -2122,8 +2218,7 @@ namespace MediaBrowser.Controller.Entities /// Marks the unplayed. /// </summary> /// <param name="user">The user.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException">Throws if user is null.</exception> public virtual void MarkUnplayed(User user) { if (user == null) @@ -2133,7 +2228,7 @@ namespace MediaBrowser.Controller.Entities var data = UserDataManager.GetUserData(user, this); - //I think it is okay to do this here. + // 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; @@ -2147,22 +2242,18 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// </summary> - /// <returns>Task.</returns> public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - - }, RefreshPriority.High); + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriority.High); } /// <summary> - /// Gets an image + /// Gets an image. /// </summary> /// <param name="type">The type.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> + /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception> public bool HasImage(ImageType type, int imageIndex) { return GetImageInfo(type, imageIndex) != null; @@ -2183,12 +2274,16 @@ namespace MediaBrowser.Controller.Entities existingImage.DateModified = image.DateModified; existingImage.Width = image.Width; existingImage.Height = image.Height; + existingImage.BlurHash = image.BlurHash; } - else { - var currentCount = ImageInfos.Length; - ImageInfos = ImageInfos.Concat(new[] { image }).ToArray(); + var current = ImageInfos; + var currentCount = current.Length; + var newArr = new ItemImageInfo[currentCount + 1]; + current.CopyTo(newArr, 0); + newArr[currentCount] = image; + ImageInfos = newArr; } } @@ -2223,8 +2318,8 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="type">The type.</param> /// <param name="index">The index.</param> - /// <returns>Task.</returns> - public void DeleteImage(ImageType type, int index) + /// <returns>A task.</returns> + public async Task DeleteImageAsync(ImageType type, int index) { var info = GetImageInfo(type, index); @@ -2242,7 +2337,7 @@ namespace MediaBrowser.Controller.Entities FileSystem.DeleteFile(info.Path); } - UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); } public void RemoveImage(ItemImageInfo image) @@ -2255,21 +2350,21 @@ namespace MediaBrowser.Controller.Entities ImageInfos = ImageInfos.Except(deletedImages).ToArray(); } - public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) - { - LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken); - } + public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) + => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); /// <summary> - /// Validates that images within the item are still on the file system + /// Validates that images within the item are still on the filesystem. /// </summary> + /// <param name="directoryService">The directory service to use.</param> + /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns> public bool ValidateImages(IDirectoryService directoryService) { var allFiles = ImageInfos .Where(i => i.IsLocalFile) .Select(i => System.IO.Path.GetDirectoryName(i.Path)) .Distinct(StringComparer.OrdinalIgnoreCase) - .SelectMany(i => directoryService.GetFilePaths(i)) + .SelectMany(path => directoryService.GetFilePaths(path)) .ToList(); var deletedImages = ImageInfos @@ -2290,9 +2385,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="imageType">Type of the image.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns>System.String.</returns> - /// <exception cref="InvalidOperationException"> - /// </exception> - /// <exception cref="ArgumentNullException">item</exception> + /// <exception cref="ArgumentNullException">Item is null.</exception> public string GetImagePath(ImageType imageType, int imageIndex) => GetImageInfo(imageType, imageIndex)?.Path; @@ -2328,10 +2421,61 @@ namespace MediaBrowser.Controller.Entities }; } + // 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<MusicArtist>(); + + if (artist != null) + { + return artist.GetImages(imageType).ElementAtOrDefault(imageIndex); + } + } + return GetImages(imageType) .ElementAtOrDefault(imageIndex); } + /// <summary> + /// Computes image index for given image or raises if no matching image found. + /// </summary> + /// <param name="image">Image to compute index for.</param> + /// <exception cref="ArgumentException">Image index cannot be computed as no matching image found. + /// </exception> + /// <returns>Image index.</returns> + 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<ItemImageInfo> GetImages(ImageType imageType) { if (imageType == ImageType.Chapter) @@ -2339,7 +2483,15 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentException("No image info for chapter images"); } - return ImageInfos.Where(i => i.Type == imageType); + // 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; + } + } } /// <summary> @@ -2348,7 +2500,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="imageType">Type of the image.</param> /// <param name="images">The images.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception> + /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception> public bool AddImages(ImageType imageType, List<FileSystemMetadata> images) { if (imageType == ImageType.Chapter) @@ -2371,7 +2523,7 @@ namespace MediaBrowser.Controller.Entities } var existing = existingImages - .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); + .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); if (existing == null) { @@ -2402,8 +2554,7 @@ namespace MediaBrowser.Controller.Entities var newImagePaths = images.Select(i => i.FullName).ToList(); var deleted = existingImages - .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path)) - .ToList(); + .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path)); if (deleted.Count > 0) { @@ -2430,12 +2581,13 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets the file system path to delete when the item is to be deleted + /// Gets the file system path to delete when the item is to be deleted. /// </summary> - /// <returns></returns> + /// <returns>The metadata for the deleted paths.</returns> public virtual IEnumerable<FileSystemMetadata> GetDeletePaths() { - return new[] { + return new[] + { new FileSystemMetadata { FullName = Path, @@ -2452,10 +2604,8 @@ namespace MediaBrowser.Controller.Entities } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); - var extensions = new List<string> { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; - extensions.AddRange(SupportedImageExtensions); - return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), extensions.ToArray(), false, false) + return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false) .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -2465,11 +2615,11 @@ namespace MediaBrowser.Controller.Entities return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter; } - public void SwapImages(ImageType type, int index1, int index2) + 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 screenshots"); + throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots"); } var info1 = GetImageInfo(type, index1); @@ -2478,13 +2628,13 @@ namespace MediaBrowser.Controller.Entities if (info1 == null || info2 == null) { // Nothing to do - return; + return Task.CompletedTask; } if (!info1.IsLocalFile || !info2.IsLocalFile) { // TODO: Not supported yet - return; + return Task.CompletedTask; } var path1 = info1.Path; @@ -2501,7 +2651,7 @@ namespace MediaBrowser.Controller.Entities info2.Width = 0; info2.Height = 0; - UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None); } public virtual bool IsPlayed(User user) @@ -2540,9 +2690,11 @@ namespace MediaBrowser.Controller.Entities { return new T { + Path = Path, MetadataCountryCode = GetPreferredMetadataCountryCode(), MetadataLanguage = GetPreferredMetadataLanguage(), Name = GetNameForMetadataLookup(), + OriginalTitle = OriginalTitle, ProviderIds = ProviderIds, IndexNumber = IndexNumber, ParentIndexNumber = ParentIndexNumber, @@ -2557,9 +2709,11 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata) { _sortName = null; @@ -2617,36 +2771,43 @@ namespace MediaBrowser.Controller.Entities 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; @@ -2674,13 +2835,13 @@ namespace MediaBrowser.Controller.Entities newOptions.ForceSave = true; } - //var parentId = Id; - //if (!video.IsOwnedItem || video.ParentId != parentId) - //{ + // var parentId = Id; + // if (!video.IsOwnedItem || video.ParentId != parentId) + // { // video.IsOwnedItem = true; // video.ParentId = parentId; // newOptions.ForceSave = true; - //} + // } if (video == null) { @@ -2694,7 +2855,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N"); + return string.Join('|', list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List<string> GetEtagValues(User user) @@ -2717,50 +2878,9 @@ namespace MediaBrowser.Controller.Entities return this; } - foreach (var parent in GetParents()) - { - if (parent.IsTopParent) - { - return parent; - } - } - return null; + return GetParents().FirstOrDefault(parent => parent.IsTopParent); } - [IgnoreDataMember] - public virtual bool IsTopParent - { - get - { - if (this is BasePluginFolder || this is Channel) - { - return true; - } - - var view = this as IHasCollectionType; - if (view != null) - { - if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - if (GetParent() is AggregateFolder) - { - return true; - } - - return false; - } - } - - [IgnoreDataMember] - public virtual bool SupportsAncestors => true; - - [IgnoreDataMember] - public virtual bool StopRefreshIfLocalMetadataFound => true; - public virtual IEnumerable<Guid> GetIdsForAncestorQuery() { return new[] { Id }; @@ -2795,7 +2915,8 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Updates the official rating based on content and returns true or false indicating if it changed. /// </summary> - /// <returns></returns> + /// <param name="children">Media children.</param> + /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns> public bool UpdateRatingToItems(IList<BaseItem> children) { var currentOfficialRating = OfficialRating; @@ -2811,55 +2932,74 @@ namespace MediaBrowser.Controller.Entities OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; - return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty, + return !string.Equals( + currentOfficialRating ?? string.Empty, + OfficialRating ?? string.Empty, StringComparison.OrdinalIgnoreCase); } public IEnumerable<BaseItem> GetThemeSongs() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName); + return ThemeSongIds.Select(LibraryManager.GetItemById); } public IEnumerable<BaseItem> GetThemeVideos() { - return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName); + return ThemeVideoIds.Select(LibraryManager.GetItemById); } /// <summary> - /// Gets or sets the remote trailers. + /// Get all extras associated with this item, sorted by <see cref="SortName"/>. /// </summary> - /// <value>The remote trailers.</value> - public MediaUrl[] RemoteTrailers { get; set; } - + /// <returns>An enumerable containing the items.</returns> public IEnumerable<BaseItem> GetExtras() { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .OrderBy(i => i.SortName); } - public IEnumerable<BaseItem> GetExtras(ExtraType[] extraTypes) + /// <summary> + /// Get all extras with specific types that are associated with this item. + /// </summary> + /// <param name="extraTypes">The types of extras to retrieve.</param> + /// <returns>An enumerable containing the extras.</returns> + public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes) { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)).OrderBy(i => i.SortName); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)); } - public IEnumerable<BaseItem> GetDisplayExtras() + public IEnumerable<BaseItem> GetTrailers() { - return GetExtras(DisplayExtraTypes); + if (this is IHasTrailers) + { + return ((IHasTrailers)this).LocalTrailerIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + } + else + { + return Array.Empty<BaseItem>(); + } } - 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; } public virtual long GetRunTimeTicksForPlayState() { return RunTimeTicks ?? 0; } - // Possible types of extra videos - public static ExtraType[] DisplayExtraTypes = new[] { Model.Entities.ExtraType.BehindTheScenes, Model.Entities.ExtraType.Clip, Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, Model.Entities.ExtraType.Scene }; + /// <inheritdoc /> + public override bool Equals(object obj) + { + return obj is BaseItem baseItem && this.Equals(baseItem); + } - public virtual bool SupportsExternalTransfer => false; + /// <inheritdoc /> + public bool Equals(BaseItem other) => Equals(Id, other?.Id); + + /// <inheritdoc /> + public override int GetHashCode() => HashCode.Combine(Id); } } diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 9c955a724..33870e2fb 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -1,3 +1,6 @@ +#pragma warning disable CS1591 + +using System; using System.Linq; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -41,9 +44,10 @@ namespace MediaBrowser.Controller.Entities /// <param name="file">The file.</param> public static void SetImagePath(this BaseItem item, ImageType imageType, string file) { - if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase)) + if (file.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - item.SetImage(new ItemImageInfo + item.SetImage( + new ItemImageInfo { Path = file, Type = imageType @@ -60,40 +64,63 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="source">The source object.</param> /// <param name="dest">The destination object.</param> + /// <typeparam name="T">Source type.</typeparam> + /// <typeparam name="TU">Destination type.</typeparam> public static void DeepCopy<T, TU>(this T source, TU dest) - where T : BaseItem - where TU : BaseItem + where T : BaseItem + where TU : BaseItem { - var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList(); - var destProps = typeof(TU).GetProperties() - .Where(x => x.CanWrite) - .ToList(); + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } - foreach (var sourceProp in sourceProps) + var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList(); + + foreach (var sourceProp in typeof(T).GetProperties()) { - if (destProps.Any(x => x.Name == sourceProp.Name)) + // We should be able to write to the property + // for both the source and destination type + // This is only false when the derived type hides the base member + // (which we shouldn't copy anyway) + if (!sourceProp.CanRead || !sourceProp.CanWrite) { - var p = destProps.First(x => x.Name == sourceProp.Name); - p.SetValue(dest, sourceProp.GetValue(source, null), null); + continue; } - } + var v = sourceProp.GetValue(source); + if (v == null) + { + continue; + } + var p = destProps.Find(x => x.Name == sourceProp.Name); + if (p != null) + { + p.SetValue(dest, v); + } + } } /// <summary> /// Copies all properties on newly created object. Skips properties that do not exist. /// </summary> /// <param name="source">The source object.</param> + /// <typeparam name="T">Source type.</typeparam> + /// <typeparam name="TU">Destination type.</typeparam> + /// <returns>Destination object.</returns> public static TU DeepCopy<T, TU>(this T source) - where T : BaseItem - where TU : BaseItem, new() + where T : BaseItem + where TU : BaseItem, new() { var dest = new TU(); source.DeepCopy(dest); return dest; } - - } } diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index 8cdb9695c..272a37df1 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,4 +1,8 @@ -using MediaBrowser.Model.Serialization; +#nullable disable + +#pragma warning disable CS1591 + +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { @@ -8,9 +12,15 @@ namespace MediaBrowser.Controller.Entities /// </summary> public abstract class BasePluginFolder : Folder, ICollectionFolder { - [IgnoreDataMember] + [JsonIgnore] public virtual string CollectionType => null; + [JsonIgnore] + public override bool SupportsInheritedParentImages => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override bool CanDelete() { return false; @@ -20,19 +30,5 @@ namespace MediaBrowser.Controller.Entities { return true; } - - [IgnoreDataMember] - public override bool SupportsInheritedParentImages => false; - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - //public override double? GetDefaultPrimaryImageAspectRatio() - //{ - // double value = 16; - // value /= 9; - - // return value; - //} } } diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 7a23d9a66..d75beb06d 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,31 +1,48 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries { - [IgnoreDataMember] + public Book() + { + this.RunTimeTicks = TimeSpan.TicksPerSecond; + } + + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Book; - [IgnoreDataMember] + public override bool SupportsPlayedStatus => true; + + public override bool SupportsPositionTicksResume => true; + + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesSortName() { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; @@ -36,11 +53,13 @@ namespace MediaBrowser.Controller.Entities return SeriesId; } + /// <inheritdoc /> public override bool CanDownload() { return IsFileProtocol; } + /// <inheritdoc /> public override UnratedItem GetBlockUnratedType() { return UnratedItem.Book; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 275052d48..7dc7f774d 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -1,15 +1,20 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; - +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -19,34 +24,70 @@ namespace MediaBrowser.Controller.Entities { /// <summary> /// Specialized Folder class that points to a subset of the physical folders in the system. - /// It is created from the user-specific folders within the system root + /// It is created from the user-specific folders within the system root. /// </summary> public class CollectionFolder : Folder, ICollectionFolder { - public static IXmlSerializer XmlSerializer { get; set; } - public static IJsonSerializer JsonSerializer { get; set; } - public static IServerApplicationHost ApplicationHost { get; set; } + private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>(); + private bool _requiresRefresh; + /// <summary> + /// Initializes a new instance of the <see cref="CollectionFolder"/> class. + /// </summary> public CollectionFolder() { PhysicalLocationsList = Array.Empty<string>(); PhysicalFolderIds = Array.Empty<Guid>(); } - [IgnoreDataMember] + /// <summary> + /// Gets the display preferences id. + /// </summary> + /// <remarks> + /// Allow different display preferences for each collection folder. + /// </remarks> + /// <value>The display prefs id.</value> + [JsonIgnore] + public override Guid DisplayPreferencesId => Id; + + [JsonIgnore] + public override string[] PhysicalLocations => PhysicalLocationsList; + + public string[] PhysicalLocationsList { get; set; } + + public Guid[] PhysicalFolderIds { get; set; } + + public static IXmlSerializer XmlSerializer { get; set; } + + public static IServerApplicationHost ApplicationHost { get; set; } + + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; + public string CollectionType { get; set; } + + /// <summary> + /// Gets the item's children. + /// </summary> + /// <remarks> + /// Our children are actually just references to the ones in the physical root... + /// </remarks> + /// <value>The actual children.</value> + [JsonIgnore] + public override IEnumerable<BaseItem> Children => GetActualChildren(); + + [JsonIgnore] + public override bool SupportsPeople => false; + public override bool CanDelete() { return false; } - public string CollectionType { get; set; } - - private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>(); public LibraryOptions GetLibraryOptions() { return GetLibraryOptions(Path); @@ -56,9 +97,7 @@ namespace MediaBrowser.Controller.Entities { try { - var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; - - if (result == null) + if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) is not LibraryOptions result) { return new LibraryOptions(); } @@ -101,12 +140,12 @@ namespace MediaBrowser.Controller.Entities public static LibraryOptions GetLibraryOptions(string path) { - lock (LibraryOptions) + lock (_libraryOptions) { - if (!LibraryOptions.TryGetValue(path, out var options)) + if (!_libraryOptions.TryGetValue(path, out var options)) { options = LoadLibraryOptions(path); - LibraryOptions[path] = options; + _libraryOptions[path] = options; } return options; @@ -115,11 +154,11 @@ namespace MediaBrowser.Controller.Entities public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (LibraryOptions) + lock (_libraryOptions) { - LibraryOptions[path] = options; + _libraryOptions[path] = options; - var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options)); + var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); foreach (var mediaPath in clone.PathInfos) { if (!string.IsNullOrEmpty(mediaPath.Path)) @@ -134,36 +173,22 @@ namespace MediaBrowser.Controller.Entities public static void OnCollectionFolderChange() { - lock (LibraryOptions) + lock (_libraryOptions) { - LibraryOptions.Clear(); + _libraryOptions.Clear(); } } - /// <summary> - /// Allow different display preferences for each collection folder - /// </summary> - /// <value>The display prefs id.</value> - [IgnoreDataMember] - public override Guid DisplayPreferencesId => Id; - - [IgnoreDataMember] - public override string[] PhysicalLocations => PhysicalLocationsList; - public override bool IsSaveLocalMetadataEnabled() { return true; } - public string[] PhysicalLocationsList { get; set; } - public Guid[] PhysicalFolderIds { get; set; } - protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } - private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; @@ -172,7 +197,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { @@ -195,9 +220,9 @@ namespace MediaBrowser.Controller.Entities return changed; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } @@ -223,7 +248,7 @@ namespace MediaBrowser.Controller.Entities return null; } - return (totalProgresses / foldersWithProgress); + return totalProgresses / foldersWithProgress; } protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) @@ -266,7 +291,6 @@ namespace MediaBrowser.Controller.Entities var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { FileInfo = FileSystem.GetDirectoryInfo(path), - Path = path, Parent = GetParent() as Folder, CollectionType = CollectionType }; @@ -293,27 +317,20 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes - /// ***Currently does not contain logic to maintain items that are unavailable in the file system*** + /// ***Currently does not contain logic to maintain items that are unavailable in the file system***. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { return Task.CompletedTask; } - /// <summary> - /// Our children are actually just references to the ones in the physical root... - /// </summary> - /// <value>The actual children.</value> - [IgnoreDataMember] - public override IEnumerable<BaseItem> Children => GetActualChildren(); - public IEnumerable<BaseItem> GetActualChildren() { return GetPhysicalFolders(true).SelectMany(c => c.Children); @@ -350,9 +367,7 @@ namespace MediaBrowser.Controller.Entities if (result.Count == 0) { - var folder = LibraryManager.FindByPath(path, true) as Folder; - - if (folder != null) + if (LibraryManager.FindByPath(path, true) is Folder folder) { result.Add(folder); } @@ -360,8 +375,5 @@ namespace MediaBrowser.Controller.Entities return result; } - - [IgnoreDataMember] - public override bool SupportsPeople => false; } } diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs deleted file mode 100644 index 8a79e0783..000000000 --- a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Configuration; - -namespace MediaBrowser.Controller.Entities -{ - public static class DayOfWeekHelper - { - public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day) - { - return GetDaysOfWeek(new List<DynamicDayOfWeek> { day }); - } - - public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days) - { - var list = new List<DayOfWeek>(); - - if (days.Contains(DynamicDayOfWeek.Sunday) || - days.Contains(DynamicDayOfWeek.Weekend) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Sunday); - } - - if (days.Contains(DynamicDayOfWeek.Saturday) || - days.Contains(DynamicDayOfWeek.Weekend) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Saturday); - } - - if (days.Contains(DynamicDayOfWeek.Monday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Monday); - } - - if (days.Contains(DynamicDayOfWeek.Tuesday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Tuesday - ); - } - - if (days.Contains(DynamicDayOfWeek.Wednesday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Wednesday); - } - - if (days.Contains(DynamicDayOfWeek.Thursday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Thursday); - } - - if (days.Contains(DynamicDayOfWeek.Friday) || - days.Contains(DynamicDayOfWeek.Weekday) || - days.Contains(DynamicDayOfWeek.Everyday)) - { - list.Add(DayOfWeek.Friday); - } - - return list; - } - } -} diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index f3bddd29c..9ce8eebe3 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -1,17 +1,22 @@ +#nullable disable + using System; using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Extensions + /// Class Extensions. /// </summary> public static class Extensions { /// <summary> /// Adds the trailer URL. /// </summary> + /// <param name="item">Media item.</param> + /// <param name="url">Trailer URL.</param> public static void AddTrailerUrl(this BaseItem item, string url) { if (string.IsNullOrEmpty(url)) @@ -28,13 +33,17 @@ namespace MediaBrowser.Controller.Entities Url = url }; - if (item.RemoteTrailers.Length == 0) + if (item.RemoteTrailers.Count == 0) { item.RemoteTrailers = new[] { mediaUrl }; } else { - item.RemoteTrailers = item.RemoteTrailers.Concat(new[] { mediaUrl }).ToArray(); + var oldIds = item.RemoteTrailers; + var newIds = new MediaUrl[oldIds.Count + 1]; + oldIds.CopyTo(newIds); + newIds[oldIds.Count] = mediaUrl; + item.RemoteTrailers = newIds; } } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8bfadbee6..18b4ec3c6 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,9 +1,17 @@ +#nullable disable + +#pragma warning disable CA1002, CA1721, CA1819, CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -11,23 +19,29 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Folder + /// Class Folder. /// </summary> public class Folder : BaseItem { - public static IUserManager UserManager { get; set; } + public Folder() + { + LinkedChildren = Array.Empty<LinkedChild>(); + } + public static IUserViewManager UserViewManager { get; set; } /// <summary> @@ -38,45 +52,121 @@ namespace MediaBrowser.Controller.Entities public LinkedChild[] LinkedChildren { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime? DateLastMediaAdded { get; set; } - public Folder() - { - LinkedChildren = EmptyLinkedChildArray; - } - - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsThemeMedia => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsPreSorted => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsPhysicalRoot => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; /// <summary> /// Gets a value indicating whether this instance is folder. /// </summary> /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public override bool IsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsCumulativeRunTimeTicks => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsDateLastMediaAdded => false; + [JsonIgnore] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileName(Path); + } + + return null; + } + } + + /// <summary> + /// Gets the actual children. + /// </summary> + /// <value>The actual children.</value> + [JsonIgnore] + public virtual IEnumerable<BaseItem> Children => LoadChildren(); + + /// <summary> + /// Gets thread-safe access to all recursive children of this folder - without regard to user. + /// </summary> + /// <value>The recursive children.</value> + [JsonIgnore] + public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); + + [JsonIgnore] + protected virtual bool SupportsShortcutChildren => false; + + protected virtual bool FilterLinkedChildrenPerUser => false; + + [JsonIgnore] + protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; + + [JsonIgnore] + public virtual bool SupportsUserDataFromChildren + { + get + { + // These are just far too slow. + if (this is ICollectionFolder) + { + return false; + } + + if (this is UserView) + { + return false; + } + + if (this is UserRootFolder) + { + return false; + } + + if (this is Channel) + { + return false; + } + + if (SourceType != SourceType.Library) + { + return false; + } + + if (this is IItemByName) + { + if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName) + { + return false; + } + } + + return true; + } + } + + public static ICollectionManager CollectionManager { get; set; } + public override bool CanDelete() { if (IsRoot) @@ -99,44 +189,27 @@ namespace MediaBrowser.Controller.Entities return baseResult; } - [IgnoreDataMember] - public override string FileNameWithoutExtension - { - get - { - if (IsFileProtocol) - { - return System.IO.Path.GetFileName(Path); - } - - return null; - } - } - protected override bool IsAllowTagFilterEnforced() { if (this is ICollectionFolder) { return false; } + if (this is UserView) { return false; } + return true; } - [IgnoreDataMember] - protected virtual bool SupportsShortcutChildren => false; - /// <summary> /// Adds the child. /// </summary> /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception> - public void AddChild(BaseItem item, CancellationToken cancellationToken) + /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception> + public void AddChild(BaseItem item) { item.SetParent(this); @@ -149,6 +222,7 @@ namespace MediaBrowser.Controller.Entities { item.DateCreated = DateTime.UtcNow; } + if (item.DateModified == DateTime.MinValue) { item.DateModified = DateTime.UtcNow; @@ -157,37 +231,22 @@ namespace MediaBrowser.Controller.Entities LibraryManager.CreateItem(item, this); } - /// <summary> - /// Gets the actual children. - /// </summary> - /// <value>The actual children.</value> - [IgnoreDataMember] - public virtual IEnumerable<BaseItem> Children => LoadChildren(); - - /// <summary> - /// thread-safe access to all recursive children of this folder - without regard to user - /// </summary> - /// <value>The recursive children.</value> - [IgnoreDataMember] - public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); - public override bool IsVisible(User user) { - if (this is ICollectionFolder && !(this is BasePluginFolder)) + if (this is ICollectionFolder && this is not BasePluginFolder) { - if (user.Policy.BlockedMediaFolders != null) + var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders); + if (blockedMediaFolders.Length > 0) { - if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || - - // Backwards compatibility - user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) + if (blockedMediaFolders.Contains(Id)) { return false; } } else { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (!user.HasPermission(PermissionKind.EnableAllFolders) + && !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(Id)) { return false; } @@ -199,12 +258,13 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Loads our children. Validation will occur externally. - /// We want this sychronous. + /// We want this synchronous. /// </summary> + /// <returns>Returns children.</returns> protected virtual List<BaseItem> LoadChildren() { - //logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); - //just load our children from the repo - the library will be validated and maintained in other processes + // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); + // just load our children from the repo - the library will be validated and maintained in other processes return GetCachedChildren(); } @@ -215,20 +275,20 @@ namespace MediaBrowser.Controller.Entities public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) { - return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); + return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken); } /// <summary> - /// Validates that the children of the folder still exist + /// Validates that the children of the folder still exist. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="metadataRefreshOptions">The metadata refresh options.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) + public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default) { - return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); + return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken); } private Dictionary<Guid, BaseItem> GetActualChildrenDictionary() @@ -242,7 +302,8 @@ namespace MediaBrowser.Controller.Entities var id = child.Id; if (dictionary.ContainsKey(id)) { - Logger.LogError("Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", + Logger.LogError( + "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", Path ?? Name, child.Path ?? child.Name); } @@ -267,13 +328,13 @@ namespace MediaBrowser.Controller.Entities /// Validates the children internal. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (recursive) { @@ -282,7 +343,7 @@ namespace MediaBrowser.Controller.Entities try { - await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false); + await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } finally { @@ -293,7 +354,7 @@ namespace MediaBrowser.Controller.Entities } } - private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -314,17 +375,17 @@ namespace MediaBrowser.Controller.Entities return; } - progress.Report(5); + progress.Report(ProgressHelpers.RetrievedChildren); if (recursive) { - ProviderManager.OnRefreshProgress(this, 5); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren); } - //build a dictionary of the current children we have now by Id so we can compare quickly and easily + // Build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = GetActualChildrenDictionary(); - //create a list for our validated children + // Create a list for our validated children var newItems = new List<BaseItem>(); cancellationToken.ThrowIfCancellationRequested(); @@ -337,7 +398,12 @@ namespace MediaBrowser.Controller.Entities if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None) { - currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } + else + { + // metadata is up-to-date; make sure DB has correct images dimensions and hash + await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); } continue; @@ -374,11 +440,11 @@ namespace MediaBrowser.Controller.Entities validChildrenNeedGeneration = true; } - progress.Report(10); + progress.Report(ProgressHelpers.UpdatedChildItems); if (recursive) { - ProviderManager.OnRefreshProgress(this, 10); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems); } cancellationToken.ThrowIfCancellationRequested(); @@ -388,11 +454,13 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress<double>(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = .80 * p + 10; - progress.Report(newPct); - ProviderManager.OnRefreshProgress(folder, newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent); + + progress.Report(percent); + + ProviderManager.OnRefreshProgress(folder, percent); }); if (validChildrenNeedGeneration) @@ -406,11 +474,11 @@ namespace MediaBrowser.Controller.Entities if (refreshChildMetadata) { - progress.Report(90); + progress.Report(ProgressHelpers.ScannedSubfolders); if (recursive) { - ProviderManager.OnRefreshProgress(this, 90); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders); } var container = this as IMetadataContainer; @@ -418,13 +486,15 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress<double>(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = .10 * p + 90; - progress.Report(newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent); + + progress.Report(percent); + if (recursive) { - ProviderManager.OnRefreshProgress(folder, newPct); + ProviderManager.OnRefreshProgress(folder, percent); } }); @@ -439,55 +509,35 @@ namespace MediaBrowser.Controller.Entities validChildren = Children.ToList(); } - await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken); + await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken).ConfigureAwait(false); } } } - private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; - - foreach (var child in children) - { - cancellationToken.ThrowIfCancellationRequested(); - - var innerProgress = new ActionableProgress<double>(); - - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; - - innerProgress.RegisterAction(p => - { - double innerPercent = currentInnerPercent; - innerPercent += p / (count); - progress.Report(innerPercent); - }); - - await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken) - .ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; - - progress.Report(percent); - } + return RunTasks( + (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), + children, + progress, + cancellationToken); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) { - var series = container as Series; - if (series != null) - { - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + // limit the amount of concurrent metadata refreshes + await ProviderManager.RunMetadataRefresh( + async () => + { + var series = container as Series; + if (series != null) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } - } - await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + }, + cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) @@ -502,12 +552,15 @@ namespace MediaBrowser.Controller.Entities { if (refreshOptions.RefreshItem(child)) { - await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + // limit the amount of concurrent metadata refreshes + await ProviderManager.RunMetadataRefresh( + async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), + cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) { - await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken); + await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } } @@ -520,45 +573,80 @@ namespace MediaBrowser.Controller.Entities /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) + private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; + return RunTasks( + (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken), + children, + progress, + cancellationToken); + } - foreach (var child in children) + /// <summary> + /// Runs an action block on a list of children. + /// </summary> + /// <param name="task">The task to run for each child.</param> + /// <param name="children">The list of children.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken) + { + var childrenCount = children.Count; + var childrenProgress = new double[childrenCount]; + + void UpdateProgress() { - cancellationToken.ThrowIfCancellationRequested(); + progress.Report(childrenProgress.Average()); + } - var innerProgress = new ActionableProgress<double>(); + var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency; + + var actionBlock = new ActionBlock<int>( + async i => + { + var innerProgress = new ActionableProgress<double>(); + + innerProgress.RegisterAction(innerPercent => + { + // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls + var innerPercentRounded = Math.Round(innerPercent); + if (childrenProgress[i] != innerPercentRounded) + { + childrenProgress[i] = innerPercentRounded; + UpdateProgress(); + } + }); + + await task(children[i], innerProgress).ConfigureAwait(false); - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; + childrenProgress[i] = 100; - innerProgress.RegisterAction(p => + UpdateProgress(); + }, + new ExecutionDataflowBlockOptions { - double innerPercent = currentInnerPercent; - innerPercent += p / (count); - progress.Report(innerPercent); + MaxDegreeOfParallelism = parallelism, + CancellationToken = cancellationToken, }); - await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService) - .ConfigureAwait(false); + for (var i = 0; i < childrenCount; i++) + { + actionBlock.Post(i); + } - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; + actionBlock.Complete(); - progress.Report(percent); - } + await actionBlock.Completion.ConfigureAwait(false); } /// <summary> - /// Get the children of this folder from the actual file system + /// Get the children of this folder from the actual file system. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> + /// <param name="directoryService">The directory service to use for operation.</param> + /// <returns>Returns set of base items.</returns> protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { var collectionType = LibraryManager.GetContentType(this); @@ -568,7 +656,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Get our children from the repo - stubbed for now + /// Get our children from the repo - stubbed for now. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> protected List<BaseItem> GetCachedChildren() @@ -585,7 +673,7 @@ namespace MediaBrowser.Controller.Entities { if (LinkedChildren.Length > 0) { - if (!(this is ICollectionFolder)) + if (this is not ICollectionFolder) { return GetChildren(user, true).Count; } @@ -600,7 +688,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }); return result.TotalRecordCount; @@ -619,7 +706,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }).TotalRecordCount; } @@ -644,7 +730,7 @@ namespace MediaBrowser.Controller.Entities return PostFilterAndSort(items, query, true); } - if (!(this is UserRootFolder) && !(this is AggregateFolder) && query.ParentId == Guid.Empty) + if (this is not UserRootFolder && this is not AggregateFolder && query.ParentId == Guid.Empty) { query.Parent = this; } @@ -665,36 +751,36 @@ namespace MediaBrowser.Controller.Entities query.StartIndex = null; query.Limit = null; - var itemsList = LibraryManager.GetItemList(query); + IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query); var user = query.User; if (user != null) { // needed for boxsets - itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList(); + itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)); } - BaseItem[] returnItems; + IEnumerable<BaseItem> returnItems; int totalCount = 0; if (query.EnableTotalRecordCount) { - var itemsArray = itemsList.ToArray(); - totalCount = itemsArray.Length; - returnItems = itemsArray; + var itemArray = itemsList.ToArray(); + totalCount = itemArray.Length; + returnItems = itemArray; } else { - returnItems = itemsList.ToArray(); + returnItems = itemsList; } if (limit.HasValue) { - returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray(); + returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value); } else if (startIndex.HasValue) { - returnItems = returnItems.Skip(startIndex.Value).ToArray(); + returnItems = returnItems.Skip(startIndex.Value); } return new QueryResult<BaseItem> @@ -706,7 +792,7 @@ namespace MediaBrowser.Controller.Entities private bool RequiresPostFiltering2(InternalItemsQuery query) { - if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)) { Logger.LogDebug("Query requires post-filtering due to BoxSet query"); return true; @@ -719,7 +805,7 @@ namespace MediaBrowser.Controller.Entities { if (LinkedChildren.Length > 0) { - if (!(this is ICollectionFolder)) + if (this is not ICollectionFolder) { Logger.LogDebug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name); return true; @@ -796,7 +882,7 @@ namespace MediaBrowser.Controller.Entities if (query.IsPlayed.HasValue) { - if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name)) + if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series))) { Logger.LogDebug("Query requires post-filtering due to IsPlayed"); return true; @@ -806,26 +892,51 @@ namespace MediaBrowser.Controller.Entities return false; } + private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items) + { + var ids = query.ItemIds; + int size = items.Count; + + // ids can potentially contain non-unique guids, but query result cannot, + // so we include only first occurrence of each guid + var positions = new Dictionary<Guid, int>(size); + int index = 0; + for (int i = 0; i < ids.Length; i++) + { + if (positions.TryAdd(ids[i], index)) + { + index++; + } + } + + var newItems = new BaseItem[size]; + for (int i = 0; i < size; i++) + { + var item = items[i]; + newItems[positions[item.Id]] = item; + } + + return newItems; + } + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { if (query.ItemIds.Length > 0) { var result = LibraryManager.GetItemsResult(query); - if (query.OrderBy.Length == 0) + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + result.Items = SortItemsByRequest(query, result.Items); } + return result; } return GetItemsInternal(query); } - public BaseItem[] GetItemList(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query) { query.EnableTotalRecordCount = false; @@ -833,14 +944,12 @@ namespace MediaBrowser.Controller.Entities { var result = LibraryManager.GetItemList(query); - if (query.OrderBy.Length == 0) + if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1) { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); + return SortItemsByRequest(query, result); } - return result.ToArray(); + + return result; } return GetItemsInternal(query).Items; @@ -853,7 +962,7 @@ namespace MediaBrowser.Controller.Entities try { query.Parent = this; - query.ChannelIds = new Guid[] { ChannelId }; + query.ChannelIds = new[] { ChannelId }; // Don't blow up here because it could cause parent screens with other content to fail return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result; @@ -882,14 +991,18 @@ namespace MediaBrowser.Controller.Entities } else { - items = GetChildren(user, true).Where(filter); + // need to pass this param to the children. + var childQuery = new InternalItemsQuery + { + DisplayAlbumFolders = query.DisplayAlbumFolders + }; + + items = GetChildren(user, true, childQuery).Where(filter); } return PostFilterAndSort(items, query, true); } - public static ICollectionManager CollectionManager { get; set; } - protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting) { var user = query.User; @@ -902,16 +1015,17 @@ namespace MediaBrowser.Controller.Entities if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) { - items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1); + items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1); } + if (!string.IsNullOrEmpty(query.NameStartsWith)) { - items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase)); } if (!string.IsNullOrEmpty(query.NameLessThan)) { - items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1); + items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1); } // This must be the last filter @@ -923,11 +1037,13 @@ namespace MediaBrowser.Controller.Entities return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); } - private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items, + private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded( + IEnumerable<BaseItem> items, InternalItemsQuery query, BaseItem queryParent, User user, - IServerConfigurationManager configurationManager, ICollectionManager collectionManager) + IServerConfigurationManager configurationManager, + ICollectionManager collectionManager) { if (items == null) { @@ -942,7 +1058,8 @@ namespace MediaBrowser.Controller.Entities return items; } - private static bool CollapseBoxSetItems(InternalItemsQuery query, + private static bool CollapseBoxSetItems( + InternalItemsQuery query, BaseItem queryParent, User user, IServerConfigurationManager configurationManager) @@ -952,18 +1069,22 @@ namespace MediaBrowser.Controller.Entities { return false; } + if (queryParent is Series) { return false; } + if (queryParent is Season) { return false; } + if (queryParent is MusicAlbum) { return false; } + if (queryParent is MusicArtist) { return false; @@ -993,33 +1114,38 @@ namespace MediaBrowser.Controller.Entities { return false; } + if (request.IsFavoriteOrLiked.HasValue) { return false; } + if (request.IsLiked.HasValue) { return false; } + if (request.IsPlayed.HasValue) { return false; } + if (request.IsResumable.HasValue) { return false; } + if (request.IsFolder.HasValue) { return false; } - if (request.Genres.Length > 0) + if (request.Genres.Count > 0) { return false; } - if (request.GenreIds.Length > 0) + if (request.GenreIds.Count > 0) { return false; } @@ -1124,7 +1250,7 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.GenreIds.Length > 0) + if (request.GenreIds.Count > 0) { return false; } @@ -1169,6 +1295,11 @@ namespace MediaBrowser.Controller.Entities public List<BaseItem> GetChildren(User user, bool includeLinkedChildren) { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return GetChildren(user, includeLinkedChildren, null); } @@ -1179,8 +1310,11 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentNullException(nameof(user)); } - //the true root should return our users root folder children - if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + // the true root should return our users root folder children + if (IsPhysicalRoot) + { + return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren); + } var result = new Dictionary<Guid, BaseItem>(); @@ -1197,10 +1331,23 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Adds the children to list. /// </summary> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query) { - foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) + // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums. + IEnumerable<BaseItem> children = null; + if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum)) + { + children = Children; + query = null; + } + + // If there are not sub-folders, proceed as normal. + if (children == null) + { + children = GetEligibleChildrenForRecursiveChildren(user); + } + + foreach (var child in children) { bool? isVisibleToUser = null; @@ -1240,18 +1387,6 @@ namespace MediaBrowser.Controller.Entities } } - /// <summary> - /// Gets allowed recursive children of an item - /// </summary> - /// <param name="user">The user.</param> - /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> - /// <returns>IEnumerable{BaseItem}.</returns> - /// <exception cref="ArgumentNullException"></exception> - public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true) - { - return GetRecursiveChildren(user, null); - } - public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { if (user == null) @@ -1327,7 +1462,6 @@ namespace MediaBrowser.Controller.Entities } } - /// <summary> /// Gets the linked children. /// </summary> @@ -1346,19 +1480,23 @@ namespace MediaBrowser.Controller.Entities list.Add(child); } } + return list; } - protected virtual bool FilterLinkedChildrenPerUser => false; - public bool ContainsLinkedChildByItemId(Guid itemId) { var linkedChildren = LinkedChildren; foreach (var i in linkedChildren) { - if (i.ItemId.HasValue && i.ItemId.Value == itemId) + if (i.ItemId.HasValue) { - return true; + if (i.ItemId.Value == itemId) + { + return true; + } + + continue; } var child = GetLinkedChild(i); @@ -1368,6 +1506,7 @@ namespace MediaBrowser.Controller.Entities return true; } } + return false; } @@ -1406,7 +1545,7 @@ namespace MediaBrowser.Controller.Entities var childOwner = child.GetOwner() ?? child; - if (childOwner != null && !(child is IItemByName)) + if (child is not IItemByName) { var childProtocol = childOwner.PathProtocol; if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File) @@ -1445,9 +1584,6 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.Item2 != null); } - [IgnoreDataMember] - protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; @@ -1468,7 +1604,8 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Refreshes the linked children. /// </summary> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <param name="fileSystemChildren">The enumerable of file system metadata.</param> + /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns> protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) { if (SupportsShortcutChildren) @@ -1532,8 +1669,8 @@ namespace MediaBrowser.Controller.Entities /// <param name="user">The user.</param> /// <param name="datePlayed">The date played.</param> /// <param name="resetPosition">if set to <c>true</c> [reset position].</param> - /// <returns>Task.</returns> - public override void MarkPlayed(User user, + public override void MarkPlayed( + User user, DateTime? datePlayed, bool resetPosition) { @@ -1545,7 +1682,7 @@ namespace MediaBrowser.Controller.Entities EnableTotalRecordCount = false }; - if (!user.Configuration.DisplayMissingEpisodes) + if (!user.DisplayMissingEpisodes) { query.IsVirtualItem = false; } @@ -1573,7 +1710,6 @@ namespace MediaBrowser.Controller.Entities /// Marks the unplayed. /// </summary> /// <param name="user">The user.</param> - /// <returns>Task.</returns> public override void MarkUnplayed(User user) { var itemsResult = GetItemList(new InternalItemsQuery @@ -1582,7 +1718,6 @@ namespace MediaBrowser.Controller.Entities Recursive = true, IsFolder = false, EnableTotalRecordCount = false - }); // Sweep through recursively and update status @@ -1600,7 +1735,6 @@ namespace MediaBrowser.Controller.Entities IsFolder = false, IsVirtualItem = false, EnableTotalRecordCount = false - }); return itemsResult @@ -1612,46 +1746,6 @@ namespace MediaBrowser.Controller.Entities return !IsPlayed(user); } - [IgnoreDataMember] - public virtual bool SupportsUserDataFromChildren - { - get - { - // These are just far too slow. - if (this is ICollectionFolder) - { - return false; - } - if (this is UserView) - { - return false; - } - if (this is UserRootFolder) - { - return false; - } - if (this is Channel) - { - return false; - } - if (SourceType != SourceType.Library) - { - return false; - } - var iItemByName = this as IItemByName; - if (iItemByName != null) - { - var hasDualAccess = this as IHasDualAccess; - if (hasDualAccess == null || hasDualAccess.IsAccessedByName) - { - return false; - } - } - - return true; - } - } - public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) { if (!SupportsUserDataFromChildren) @@ -1681,21 +1775,15 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } + }).TotalRecordCount; - }); - - double unplayedCount = unplayedQueryResult.TotalRecordCount; - - dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; + dto.UnplayedItemCount = unplayedQueryResult; - if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + if (itemDto?.RecursiveItemCount > 0) { - if (itemDto.RecursiveItemCount.Value > 0) - { - var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100; - dto.PlayedPercentage = 100 - unplayedPercentage; - dto.Played = dto.PlayedPercentage.Value >= 100; - } + var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; } else { @@ -1703,5 +1791,45 @@ namespace MediaBrowser.Controller.Entities } } } + + /// <summary> + /// Contains constants used when reporting scan progress. + /// </summary> + private static class ProgressHelpers + { + /// <summary> + /// Reported after the folders immediate children are retrieved. + /// </summary> + public const int RetrievedChildren = 5; + + /// <summary> + /// Reported after add, updating, or deleting child items from the LibraryManager. + /// </summary> + public const int UpdatedChildItems = 10; + + /// <summary> + /// Reported once subfolders are scanned. + /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. + /// </summary> + public const int ScannedSubfolders = 50; + + /// <summary> + /// Reported once metadata is refreshed. + /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. + /// </summary> + public const int RefreshedMetadata = 100; + + /// <summary> + /// Gets the current progress given the previous step, next step, and progress in between. + /// </summary> + /// <param name="previousProgressStep">The previous progress step.</param> + /// <param name="nextProgressStep">The next progress step.</param> + /// <param name="currentProgress">The current progress step.</param> + /// <returns>The progress.</returns> + public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) + { + return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); + } + } } } diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 44cb62d22..338f96204 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,17 +1,38 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Genre + /// Class Genre. /// </summary> public class Genre : BaseItem, IItemByName { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -19,6 +40,7 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; @@ -29,20 +51,6 @@ namespace MediaBrowser.Controller.Entities return 1; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - [IgnoreDataMember] - public override bool IsDisplayedAsFolder => true; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - public override bool IsSaveLocalMetadataEnabled() { return true; @@ -56,14 +64,17 @@ namespace MediaBrowser.Controller.Entities public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; - query.ExcludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name }; + query.ExcludeItemTypes = new[] + { + nameof(MusicVideo), + nameof(Entities.Audio.Audio), + nameof(MusicAlbum), + nameof(MusicArtist) + }; return LibraryManager.GetItemList(query); } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -92,15 +103,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index a6a9a0783..89e494ebc 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -1,15 +1,22 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; namespace MediaBrowser.Controller.Entities { /// <summary> - /// This is just a marker interface to denote top level folders + /// This is just a marker interface to denote top level folders. /// </summary> public interface ICollectionFolder : IHasCollectionType { string Path { get; } + string Name { get; } + Guid Id { get; } + string[] PhysicalLocations { get; } } diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs index 149c1e5ab..3aeb7468f 100644 --- a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs +++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs @@ -1,7 +1,9 @@ +#nullable disable + namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasAspectRatio + /// Interface IHasAspectRatio. /// </summary> public interface IHasAspectRatio { diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs index abee75a28..14459624e 100644 --- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -1,7 +1,9 @@ +#nullable disable + namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasDisplayOrder + /// Interface IHasDisplayOrder. /// </summary> public interface IHasDisplayOrder { diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 4635b9062..90d9bdd2d 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Dto; @@ -7,13 +11,19 @@ namespace MediaBrowser.Controller.Entities { public interface IHasMediaSources { + Guid Id { get; set; } + + long? RunTimeTicks { get; set; } + + string Path { get; } + /// <summary> /// Gets the media sources. /// </summary> + /// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param> + /// <returns>A list of media sources.</returns> List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); + List<MediaStream> GetMediaStreams(); - Guid Id { get; set; } - long? RunTimeTicks { get; set; } - string Path { get; } } } diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs index 777b40828..f80f7c304 100644 --- a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs +++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.Entities @@ -5,13 +9,21 @@ namespace MediaBrowser.Controller.Entities public interface IHasProgramAttributes { bool IsMovie { get; set; } + bool IsSports { get; } + bool IsNews { get; } + bool IsKids { get; } + bool IsRepeat { get; set; } + bool IsSeries { get; set; } + ProgramAudio? Audio { get; set; } + string EpisodeTitle { get; set; } + string ServiceName { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs index 0975242f5..ae01c223e 100644 --- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasScreenshots + /// The item has screenshots. /// </summary> public interface IHasScreenshots { diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs index 7da53f730..5f774bbde 100644 --- a/MediaBrowser.Controller/Entities/IHasSeries.cs +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -1,3 +1,7 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities @@ -5,15 +9,21 @@ namespace MediaBrowser.Controller.Entities public interface IHasSeries { /// <summary> - /// Gets the name of the series. + /// Gets or sets the name of the series. /// </summary> /// <value>The name of the series.</value> string SeriesName { get; set; } + + Guid SeriesId { get; set; } + + string SeriesPresentationUniqueKey { get; set; } + string FindSeriesName(); + string FindSeriesSortName(); - Guid SeriesId { get; set; } + Guid FindSeriesId(); - string SeriesPresentationUniqueKey { get; set; } + string FindSeriesPresentationUniqueKey(); } } diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs new file mode 100644 index 000000000..dca5af873 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasShares.cs @@ -0,0 +1,11 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasShares + { + Share[] Shares { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs index 688439e6c..f317a02ff 100644 --- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -1,4 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { @@ -8,6 +13,6 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the special feature ids. /// </summary> /// <value>The special feature ids.</value> - Guid[] SpecialFeatureIds { get; set; } + IReadOnlyList<Guid> SpecialFeatureIds { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IHasStartDate.cs b/MediaBrowser.Controller/Entities/IHasStartDate.cs index 1ecde9af4..dab15eb01 100644 --- a/MediaBrowser.Controller/Entities/IHasStartDate.cs +++ b/MediaBrowser.Controller/Entities/IHasStartDate.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs index 3bdb9b64a..f4271678d 100644 --- a/MediaBrowser.Controller/Entities/IHasTrailers.cs +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -1,6 +1,9 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using System.Linq; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -11,29 +14,85 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the remote trailers. /// </summary> /// <value>The remote trailers.</value> - MediaUrl[] RemoteTrailers { get; set; } + IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } /// <summary> /// Gets or sets the local trailer ids. /// </summary> /// <value>The local trailer ids.</value> - Guid[] LocalTrailerIds { get; set; } - Guid[] RemoteTrailerIds { get; set; } + IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <summary> + /// Gets or sets the remote trailer ids. + /// </summary> + /// <value>The remote trailer ids.</value> + IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + Guid Id { get; set; } } + /// <summary> + /// Class providing extension methods for working with <see cref="IHasTrailers" />. + /// </summary> public static class HasTrailerExtensions { /// <summary> + /// Gets the trailer count. + /// </summary> + /// <param name="item">Media item.</param> + /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> + public static int GetTrailerCount(this IHasTrailers item) + => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count; + + /// <summary> /// Gets the trailer ids. /// </summary> - /// <returns>List<Guid>.</returns> - public static List<Guid> GetTrailerIds(this IHasTrailers item) + /// <param name="item">Media item.</param> + /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> + public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item) { - var list = item.LocalTrailerIds.ToList(); - list.AddRange(item.RemoteTrailerIds); - return list; + var localIds = item.LocalTrailerIds; + var remoteIds = item.RemoteTrailerIds; + + var all = new Guid[localIds.Count + remoteIds.Count]; + var index = 0; + foreach (var id in localIds) + { + all[index++] = id; + } + + foreach (var id in remoteIds) + { + all[index++] = id; + } + + return all; } + /// <summary> + /// Gets the trailers. + /// </summary> + /// <param name="item">Media item.</param> + /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns> + public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item) + { + var localIds = item.LocalTrailerIds; + var remoteIds = item.RemoteTrailerIds; + var libraryManager = BaseItem.LibraryManager; + + var all = new BaseItem[localIds.Count + remoteIds.Count]; + var index = 0; + foreach (var id in localIds) + { + all[index++] = libraryManager.GetItemById(id); + } + + foreach (var id in remoteIds) + { + all[index++] = libraryManager.GetItemById(id); + } + + return all; + } } } diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 89b5dfee3..cac8aa61a 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -1,9 +1,11 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Marker interface + /// Marker interface. /// </summary> public interface IItemByName { diff --git a/MediaBrowser.Controller/Entities/IMetadataContainer.cs b/MediaBrowser.Controller/Entities/IMetadataContainer.cs index a384c0df3..77f5cfb79 100644 --- a/MediaBrowser.Controller/Entities/IMetadataContainer.cs +++ b/MediaBrowser.Controller/Entities/IMetadataContainer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs index 3e96771c3..cdda8ea39 100644 --- a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs +++ b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public interface ISupportsPlaceHolders diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 78f859069..0baa7725e 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,254 +1,365 @@ +#pragma warning disable CA1044, CA1819, CA2227, CS1591 + using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities { public class InternalItemsQuery { + public InternalItemsQuery() + { + AlbumArtistIds = Array.Empty<Guid>(); + AlbumIds = Array.Empty<Guid>(); + AncestorIds = Array.Empty<Guid>(); + ArtistIds = Array.Empty<Guid>(); + BlockUnratedItems = Array.Empty<UnratedItem>(); + BoxSetLibraryFolders = Array.Empty<Guid>(); + ChannelIds = Array.Empty<Guid>(); + ContributingArtistIds = Array.Empty<Guid>(); + DtoOptions = new DtoOptions(); + EnableTotalRecordCount = true; + ExcludeArtistIds = Array.Empty<Guid>(); + ExcludeInheritedTags = Array.Empty<string>(); + ExcludeItemIds = Array.Empty<Guid>(); + ExcludeItemTypes = Array.Empty<string>(); + ExcludeTags = Array.Empty<string>(); + GenreIds = Array.Empty<Guid>(); + Genres = Array.Empty<string>(); + GroupByPresentationUniqueKey = true; + ImageTypes = Array.Empty<ImageType>(); + IncludeItemTypes = Array.Empty<string>(); + ItemIds = Array.Empty<Guid>(); + MediaTypes = Array.Empty<string>(); + MinSimilarityScore = 20; + OfficialRatings = Array.Empty<string>(); + OrderBy = Array.Empty<ValueTuple<string, SortOrder>>(); + PersonIds = Array.Empty<Guid>(); + PersonTypes = Array.Empty<string>(); + PresetViews = Array.Empty<string>(); + SeriesStatuses = Array.Empty<SeriesStatus>(); + SourceTypes = Array.Empty<SourceType>(); + StudioIds = Array.Empty<Guid>(); + Tags = Array.Empty<string>(); + TopParentIds = Array.Empty<Guid>(); + TrailerTypes = Array.Empty<TrailerType>(); + VideoTypes = Array.Empty<VideoType>(); + Years = Array.Empty<int>(); + } + + public InternalItemsQuery(User? user) + : this() + { + if (user != null) + { + SetUser(user); + } + } + public bool Recursive { get; set; } public int? StartIndex { get; set; } public int? Limit { get; set; } - public User User { get; set; } + public User? User { get; set; } - public BaseItem SimilarTo { get; set; } + public BaseItem? SimilarTo { get; set; } public bool? IsFolder { get; set; } + public bool? IsFavorite { get; set; } + public bool? IsFavoriteOrLiked { get; set; } + public bool? IsLiked { get; set; } + public bool? IsPlayed { get; set; } + public bool? IsResumable { get; set; } + public bool? IncludeItemsByName { get; set; } public string[] MediaTypes { get; set; } + public string[] IncludeItemTypes { get; set; } + public string[] ExcludeItemTypes { get; set; } + public string[] ExcludeTags { get; set; } + public string[] ExcludeInheritedTags { get; set; } - public string[] Genres { get; set; } + + public IReadOnlyList<string> Genres { get; set; } public bool? IsSpecialSeason { get; set; } + public bool? IsMissing { get; set; } + public bool? IsUnaired { get; set; } + public bool? CollapseBoxSetItems { get; set; } - public string NameStartsWithOrGreater { get; set; } - public string NameStartsWith { get; set; } - public string NameLessThan { get; set; } - public string NameContains { get; set; } - public string MinSortName { get; set; } + public string? NameStartsWithOrGreater { get; set; } + + public string? NameStartsWith { get; set; } + + public string? NameLessThan { get; set; } + + public string? NameContains { get; set; } + + public string? MinSortName { get; set; } + + public string? PresentationUniqueKey { get; set; } - public string PresentationUniqueKey { get; set; } - public string Path { get; set; } - public string Name { get; set; } + public string? Path { get; set; } + + public string? Name { get; set; } + + public string? Person { get; set; } - public string Person { get; set; } public Guid[] PersonIds { get; set; } + public Guid[] ItemIds { get; set; } + public Guid[] ExcludeItemIds { get; set; } - public string AdjacentTo { get; set; } + + public string? AdjacentTo { get; set; } + public string[] PersonTypes { get; set; } public bool? Is3D { get; set; } + public bool? IsHD { get; set; } + public bool? IsLocked { get; set; } + public bool? IsPlaceHolder { get; set; } public bool? HasImdbId { get; set; } + public bool? HasOverview { get; set; } + public bool? HasTmdbId { get; set; } + public bool? HasOfficialRating { get; set; } + public bool? HasTvdbId { get; set; } + public bool? HasThemeSong { get; set; } + public bool? HasThemeVideo { get; set; } + public bool? HasSubtitles { get; set; } + public bool? HasSpecialFeature { get; set; } + public bool? HasTrailer { get; set; } + public bool? HasParentalRating { get; set; } public Guid[] StudioIds { get; set; } - public Guid[] GenreIds { get; set; } + + public IReadOnlyList<Guid> GenreIds { get; set; } + public ImageType[] ImageTypes { get; set; } + public VideoType[] VideoTypes { get; set; } + public UnratedItem[] BlockUnratedItems { get; set; } + public int[] Years { get; set; } + public string[] Tags { get; set; } + public string[] OfficialRatings { get; set; } public DateTime? MinPremiereDate { get; set; } + public DateTime? MaxPremiereDate { get; set; } + public DateTime? MinStartDate { get; set; } + public DateTime? MaxStartDate { get; set; } + public DateTime? MinEndDate { get; set; } + public DateTime? MaxEndDate { get; set; } + public bool? IsAiring { get; set; } public bool? IsMovie { get; set; } + public bool? IsSports { get; set; } + public bool? IsKids { get; set; } + public bool? IsNews { get; set; } + public bool? IsSeries { get; set; } + public int? MinIndexNumber { get; set; } + public int? AiredDuringSeason { get; set; } + public double? MinCriticRating { get; set; } + public double? MinCommunityRating { get; set; } - public Guid[] ChannelIds { get; set; } + public IReadOnlyList<Guid> ChannelIds { get; set; } public int? ParentIndexNumber { get; set; } + public int? ParentIndexNumberNotEquals { get; set; } + public int? IndexNumber { get; set; } + public int? MinParentalRating { get; set; } + public int? MaxParentalRating { get; set; } public bool? HasDeadParentId { get; set; } + public bool? IsVirtualItem { get; set; } public Guid ParentId { get; set; } - public string ParentType { get; set; } + + public string? ParentType { get; set; } + public Guid[] AncestorIds { get; set; } - public Guid[] TopParentIds { get; set; } - public BaseItem Parent - { - set - { - if (value == null) - { - ParentId = Guid.Empty; - ParentType = null; - } - else - { - ParentId = value.Id; - ParentType = value.GetType().Name; - } - } - } + public Guid[] TopParentIds { get; set; } public string[] PresetViews { get; set; } + public TrailerType[] TrailerTypes { get; set; } + public SourceType[] SourceTypes { get; set; } public SeriesStatus[] SeriesStatuses { get; set; } - public string ExternalSeriesId { get; set; } - public string ExternalId { get; set; } + + public string? ExternalSeriesId { get; set; } + + public string? ExternalId { get; set; } public Guid[] AlbumIds { get; set; } + public Guid[] ArtistIds { get; set; } + public Guid[] ExcludeArtistIds { get; set; } - public string AncestorWithPresentationUniqueKey { get; set; } - public string SeriesPresentationUniqueKey { get; set; } + + public string? AncestorWithPresentationUniqueKey { get; set; } + + public string? SeriesPresentationUniqueKey { get; set; } public bool GroupByPresentationUniqueKey { get; set; } + public bool GroupBySeriesPresentationUniqueKey { get; set; } + public bool EnableTotalRecordCount { get; set; } + public bool ForceDirect { get; set; } - public Dictionary<string, string> ExcludeProviderIds { get; set; } + + public Dictionary<string, string>? ExcludeProviderIds { get; set; } + public bool EnableGroupByMetadataKey { get; set; } + public bool? HasChapterImages { get; set; } - public ValueTuple<string, SortOrder>[] OrderBy { get; set; } + public IReadOnlyList<(string, SortOrder)> OrderBy { get; set; } public DateTime? MinDateCreated { get; set; } + public DateTime? MinDateLastSaved { get; set; } + public DateTime? MinDateLastSavedForUser { get; set; } public DtoOptions DtoOptions { get; set; } + public int MinSimilarityScore { get; set; } - public string HasNoAudioTrackWithLanguage { get; set; } - public string HasNoInternalSubtitleTrackWithLanguage { get; set; } - public string HasNoExternalSubtitleTrackWithLanguage { get; set; } - public string HasNoSubtitleTrackWithLanguage { get; set; } + + public string? HasNoAudioTrackWithLanguage { get; set; } + + public string? HasNoInternalSubtitleTrackWithLanguage { get; set; } + + public string? HasNoExternalSubtitleTrackWithLanguage { get; set; } + + public string? HasNoSubtitleTrackWithLanguage { get; set; } + public bool? IsDeadArtist { get; set; } + public bool? IsDeadStudio { get; set; } - public bool? IsDeadPerson { get; set; } - public InternalItemsQuery() - { - AlbumArtistIds = Array.Empty<Guid>(); - AlbumIds = Array.Empty<Guid>(); - AncestorIds = Array.Empty<Guid>(); - ArtistIds = Array.Empty<Guid>(); - BlockUnratedItems = Array.Empty<UnratedItem>(); - BoxSetLibraryFolders = Array.Empty<Guid>(); - ChannelIds = Array.Empty<Guid>(); - ContributingArtistIds = Array.Empty<Guid>(); - DtoOptions = new DtoOptions(); - EnableTotalRecordCount = true; - ExcludeArtistIds = Array.Empty<Guid>(); - ExcludeInheritedTags = Array.Empty<string>(); - ExcludeItemIds = Array.Empty<Guid>(); - ExcludeItemTypes = Array.Empty<string>(); - ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - ExcludeTags = Array.Empty<string>(); - GenreIds = Array.Empty<Guid>(); - Genres = Array.Empty<string>(); - GroupByPresentationUniqueKey = true; - HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - ImageTypes = Array.Empty<ImageType>(); - IncludeItemTypes = Array.Empty<string>(); - ItemIds = Array.Empty<Guid>(); - MediaTypes = Array.Empty<string>(); - MinSimilarityScore = 20; - OfficialRatings = Array.Empty<string>(); - OrderBy = Array.Empty<ValueTuple<string, SortOrder>>(); - PersonIds = Array.Empty<Guid>(); - PersonTypes = Array.Empty<string>(); - PresetViews = Array.Empty<string>(); - SeriesStatuses = Array.Empty<SeriesStatus>(); - SourceTypes = Array.Empty<SourceType>(); - StudioIds = Array.Empty<Guid>(); - Tags = Array.Empty<string>(); - TopParentIds = Array.Empty<Guid>(); - TrailerTypes = Array.Empty<TrailerType>(); - VideoTypes = Array.Empty<VideoType>(); - Years = Array.Empty<int>(); - } + public bool? IsDeadPerson { get; set; } - public InternalItemsQuery(User user) - : this() - { - SetUser(user); - } + /// <summary> + /// Gets or sets a value indicating whether album sub-folders should be returned if they exist. + /// </summary> + public bool? DisplayAlbumFolders { get; set; } - public void SetUser(User user) + public BaseItem? Parent { - if (user != null) + set { - var policy = user.Policy; - MaxParentalRating = policy.MaxParentalRating; - - if (policy.MaxParentalRating.HasValue) + if (value == null) + { + ParentId = Guid.Empty; + ParentType = null; + } + else { - BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray(); + ParentId = value.Id; + ParentType = value.GetType().Name; } - - ExcludeInheritedTags = policy.BlockedTags; - - User = user; } } - public Dictionary<string, string> HasAnyProviderId { get; set; } + public Dictionary<string, string>? HasAnyProviderId { get; set; } + public Guid[] AlbumArtistIds { get; set; } + public Guid[] BoxSetLibraryFolders { get; set; } + public Guid[] ContributingArtistIds { get; set; } + public bool? HasAired { get; set; } + public bool? HasOwnerId { get; set; } + public bool? Is4K { get; set; } + public int? MaxHeight { get; set; } + public int? MaxWidth { get; set; } + public int? MinHeight { get; set; } + public int? MinWidth { get; set; } - public string SearchTerm { get; set; } - public string SeriesTimerId { get; set; } + + public string? SearchTerm { get; set; } + + public string? SeriesTimerId { get; set; } + + public void SetUser(User user) + { + MaxParentalRating = user.MaxParentalAgeRating; + + if (MaxParentalRating.HasValue) + { + string other = UnratedItem.Other.ToString(); + BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) + .Where(i => i != other) + .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); + } + + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); + + User = user; + } } } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 1613531b5..3e1d89274 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -1,20 +1,45 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; +using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Entities { public class InternalPeopleQuery { + public InternalPeopleQuery() + : this(Array.Empty<string>(), Array.Empty<string>()) + { + } + + public InternalPeopleQuery(IReadOnlyList<string> personTypes, IReadOnlyList<string> excludePersonTypes) + { + PersonTypes = personTypes; + ExcludePersonTypes = excludePersonTypes; + } + + /// <summary> + /// Gets or sets the maximum number of items the query should return. + /// </summary> + public int Limit { get; set; } + public Guid ItemId { get; set; } - public string[] PersonTypes { get; set; } - public string[] ExcludePersonTypes { get; set; } + + public IReadOnlyList<string> PersonTypes { get; } + + public IReadOnlyList<string> ExcludePersonTypes { get; } + public int? MaxListOrder { get; set; } + public Guid AppearsInItemId { get; set; } + public string NameContains { get; set; } - public InternalPeopleQuery() - { - PersonTypes = Array.Empty<string>(); - ExcludePersonTypes = Array.Empty<string>(); - } + public User User { get; set; } + + public bool? IsFavorite { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index ff6b13398..ea8555dbf 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,6 +1,10 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Text.Json.Serialization; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -25,22 +29,16 @@ namespace MediaBrowser.Controller.Entities public DateTime DateModified { get; set; } public int Width { get; set; } + public int Height { get; set; } - [IgnoreDataMember] - public bool IsLocalFile - { - get - { - if (Path != null) - { - if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - return true; - } - } + /// <summary> + /// Gets or sets the blurhash. + /// </summary> + /// <value>The blurhash.</value> + public string BlurHash { get; set; } + + [JsonIgnore] + public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index bb2d03246..fd5fef3dc 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -1,21 +1,31 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; -using System.Collections.Generic; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; +using System.Globalization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { public class LinkedChild { + public LinkedChild() + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + } + public string Path { get; set; } + public LinkedChildType Type { get; set; } + public string LibraryItemId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string Id { get; set; } /// <summary> - /// Serves as a cache + /// Gets or sets the linked item id. /// </summary> public Guid? ItemId { get; set; } @@ -29,45 +39,10 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(child.Path)) { - child.LibraryItemId = item.Id.ToString("N"); + child.LibraryItemId = item.Id.ToString("N", CultureInfo.InvariantCulture); } return child; } - - public LinkedChild() - { - Id = Guid.NewGuid().ToString("N"); - } - } - - public enum LinkedChildType - { - Manual = 0, - Shortcut = 1 - } - - public class LinkedChildComparer : IEqualityComparer<LinkedChild> - { - private readonly IFileSystem _fileSystem; - - public LinkedChildComparer(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - public bool Equals(LinkedChild x, LinkedChild y) - { - if (x.Type == y.Type) - { - return _fileSystem.AreEqual(x.Path, y.Path); - } - return false; - } - - public int GetHashCode(LinkedChild obj) - { - return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(); - } } } diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs new file mode 100644 index 000000000..4e58e2942 --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs @@ -0,0 +1,35 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Entities +{ + public class LinkedChildComparer : IEqualityComparer<LinkedChild> + { + private readonly IFileSystem _fileSystem; + + public LinkedChildComparer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public bool Equals(LinkedChild x, LinkedChild y) + { + if (x.Type == y.Type) + { + return _fileSystem.AreEqual(x.Path, y.Path); + } + + return false; + } + + public int GetHashCode(LinkedChild obj) + { + return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/LinkedChildType.cs b/MediaBrowser.Controller/Entities/LinkedChildType.cs new file mode 100644 index 000000000..9ddb7b620 --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChildType.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// The linked child type. + /// </summary> + public enum LinkedChildType + { + /// <summary> + /// Manually linked child. + /// </summary> + Manual = 0, + + /// <summary> + /// Shortcut linked child. + /// </summary> + Shortcut = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 124a943ef..e46f99cd5 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,40 +1,47 @@ +#nullable disable + +#pragma warning disable CA1721, CA1819, CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.Movies { /// <summary> - /// Class BoxSet + /// Class BoxSet. /// </summary> public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo> { public BoxSet() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty<MediaUrl>(); LocalTrailerIds = Array.Empty<Guid>(); RemoteTrailerIds = Array.Empty<Guid>(); DisplayOrder = ItemSortBy.PremiereDate; } - [IgnoreDataMember] + [JsonIgnore] protected override bool FilterLinkedChildrenPerUser => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets or sets the display order. @@ -42,9 +49,33 @@ namespace MediaBrowser.Controller.Entities.Movies /// <value>The display order.</value> public string DisplayOrder { get; set; } - protected override bool GetBlockUnratedValue(UserPolicy config) + [JsonIgnore] + private bool IsLegacyBoxSet + { + get + { + if (string.IsNullOrEmpty(Path)) + { + return false; + } + + if (LinkedChildren.Length > 0) + { + return false; + } + + return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); + } + } + + [JsonIgnore] + public override bool IsPreSorted => true; + + public Guid[] LibraryFolderIds { get; set; } + + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Movie); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie); } public override double GetDefaultPrimaryImageAspectRatio() @@ -61,7 +92,8 @@ namespace MediaBrowser.Controller.Entities.Movies { return base.GetNonCachedChildren(directoryService); } - return new List<BaseItem>(); + + return Enumerable.Empty<BaseItem>(); } protected override List<BaseItem> LoadChildren() @@ -75,28 +107,6 @@ namespace MediaBrowser.Controller.Entities.Movies return new List<BaseItem>(); } - [IgnoreDataMember] - private bool IsLegacyBoxSet - { - get - { - if (string.IsNullOrEmpty(Path)) - { - return false; - } - - if (LinkedChildren.Length > 0) - { - return false; - } - - return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path); - } - } - - [IgnoreDataMember] - public override bool IsPreSorted => true; - public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) { return true; @@ -183,8 +193,6 @@ namespace MediaBrowser.Controller.Entities.Movies return IsVisible(user); } - public Guid[] LibraryFolderIds { get; set; } - private Guid[] GetLibraryFolderIds(User user) { return LibraryManager.GetUserRootFolder().GetChildren(user, true) @@ -194,7 +202,7 @@ namespace MediaBrowser.Controller.Entities.Movies public Guid[] GetLibraryFolderIds() { - var expandedFolders = new List<Guid>() { }; + var expandedFolders = new List<Guid>(); return FlattenItems(this, expandedFolders) .SelectMany(i => LibraryManager.GetCollectionFolders(i)) @@ -211,8 +219,7 @@ namespace MediaBrowser.Controller.Entities.Movies private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders) { - var boxset = item as BoxSet; - if (boxset != null) + if (item is BoxSet boxset) { if (!expandedFolders.Contains(item.Id)) { diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 232d11624..b54bbf5eb 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,34 +1,43 @@ +#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.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities.Movies { /// <summary> - /// Class Movie + /// Class Movie. /// </summary> public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping { - public Guid[] SpecialFeatureIds { get; set; } - public Movie() { - SpecialFeatureIds = new Guid[] { }; - RemoteTrailers = EmptyMediaUrlArray; - LocalTrailerIds = new Guid[] { }; - RemoteTrailerIds = new Guid[] { }; + SpecialFeatureIds = Array.Empty<Guid>(); + RemoteTrailers = Array.Empty<MediaUrl>(); + LocalTrailerIds = Array.Empty<Guid>(); + RemoteTrailerIds = Array.Empty<Guid>(); } - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> SpecialFeatureIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets or sets the name of the TMDB collection. @@ -36,13 +45,16 @@ namespace MediaBrowser.Controller.Entities.Movies /// <value>The name of the TMDB collection.</value> public string TmdbCollectionName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string CollectionName { get => TmdbCollectionName; set => TmdbCollectionName = value; } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public override double GetDefaultPrimaryImageAspectRatio() { // hack for tv plugins @@ -102,6 +114,7 @@ namespace MediaBrowser.Controller.Entities.Movies return itemsChanged; } + /// <inheritdoc /> public override UnratedItem GetBlockUnratedType() { return UnratedItem.Movie; @@ -130,9 +143,10 @@ namespace MediaBrowser.Controller.Entities.Movies return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <inheritdoc /> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { @@ -166,24 +180,22 @@ namespace MediaBrowser.Controller.Entities.Movies return hasChanges; } + /// <inheritdoc /> public override List<ExternalUrl> GetRelatedUrls() { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } return list; } - - [IgnoreDataMember] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 5bf082b7e..237ad5198 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,23 +1,26 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo> { - [IgnoreDataMember] - public string[] Artists { get; set; } - public MusicVideo() { Artists = Array.Empty<string>(); } - [IgnoreDataMember] - public string[] AllArtists => Artists; + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } public override UnratedItem GetBlockUnratedType() { @@ -33,9 +36,9 @@ namespace MediaBrowser.Controller.Entities return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 2fb613768..687ce1ec8 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; @@ -98,22 +100,5 @@ namespace MediaBrowser.Controller.Entities existing.SetProviderId(id.Key, id.Value); } } - - public static bool ContainsPerson(List<PersonInfo> people, string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - foreach (var i in people) - { - if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; - } } } diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index dd0183489..045c1b89f 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -1,9 +1,12 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -13,6 +16,26 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo> { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + /// <summary> + /// Gets a value indicating whether to enable alpha numeric sorting. + /// </summary> + [JsonIgnore] + public override bool EnableAlphaNumericSorting => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + + [JsonIgnore] + public override bool SupportsAncestors => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -20,6 +43,7 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; @@ -45,14 +69,6 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - public override bool CanDelete() { return false; @@ -63,15 +79,6 @@ namespace MediaBrowser.Controller.Entities return true; } - [IgnoreDataMember] - public override bool EnableAlphaNumericSorting => false; - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -115,15 +122,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param> + /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) @@ -135,57 +145,4 @@ namespace MediaBrowser.Controller.Entities return hasChanges; } } - - /// <summary> - /// This is the small Person stub that is attached to BaseItems - /// </summary> - public class PersonInfo : IHasProviderIds - { - public PersonInfo() - { - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - public Guid ItemId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - /// <summary> - /// Gets or sets the role. - /// </summary> - /// <value>The role.</value> - public string Role { get; set; } - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public string Type { get; set; } - - /// <summary> - /// Gets or sets the sort order - ascending - /// </summary> - /// <value>The sort order.</value> - public int? SortOrder { get; set; } - - public string ImageUrl { get; set; } - - public Dictionary<string, string> ProviderIds { get; set; } - - /// <summary> - /// Returns a <see cref="string" /> that represents this instance. - /// </summary> - /// <returns>A <see cref="string" /> that represents this instance.</returns> - public override string ToString() - { - return Name; - } - - public bool IsType(string type) - { - return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); - } - } } diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs new file mode 100644 index 000000000..2b689ae7e --- /dev/null +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -0,0 +1,66 @@ +#nullable disable + +#pragma warning disable CA2227, CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// This is a small Person stub that is attached to BaseItems. + /// </summary> + public sealed class PersonInfo : IHasProviderIds + { + public PersonInfo() + { + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the role. + /// </summary> + /// <value>The role.</value> + public string Role { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the ascending sort order. + /// </summary> + /// <value>The sort order.</value> + public int? SortOrder { get; set; } + + public string ImageUrl { get; set; } + + public Dictionary<string, string> ProviderIds { get; set; } + + /// <summary> + /// Returns a <see cref="string" /> that represents this instance. + /// </summary> + /// <returns>A <see cref="string" /> that represents this instance.</returns> + public override string ToString() + { + return Name; + } + + public bool IsType(string type) + { + return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) + || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 60c832189..ba6ce189a 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -1,21 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Text.Json.Serialization; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class Photo : BaseItem { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsLocalMetadata => false; - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Photo; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; - - [IgnoreDataMember] + [JsonIgnore] public PhotoAlbum AlbumEntity { get @@ -23,16 +26,40 @@ namespace MediaBrowser.Controller.Entities var parents = GetParents(); foreach (var parent in parents) { - var photoAlbum = parent as PhotoAlbum; - if (photoAlbum != null) + if (parent is PhotoAlbum photoAlbum) { return photoAlbum; } } + return null; } } + public string CameraMake { get; set; } + + public string CameraModel { get; set; } + + public string Software { get; set; } + + public double? ExposureTime { get; set; } + + public double? FocalLength { get; set; } + + public ImageOrientation? Orientation { get; set; } + + public double? Aperture { get; set; } + + public double? ShutterSpeed { get; set; } + + public double? Latitude { get; set; } + + public double? Longitude { get; set; } + + public double? Altitude { get; set; } + + public int? IsoSpeedRating { get; set; } + public override bool CanDownload() { return true; @@ -41,10 +68,10 @@ namespace MediaBrowser.Controller.Entities public override double GetDefaultPrimaryImageAspectRatio() { // REVIEW: @bond - if (Width.HasValue && Height.HasValue) + if (Width != 0 && Height != 0) { - double width = Width.Value; - double height = Height.Value; + double width = Width; + double height = Height; if (Orientation.HasValue) { @@ -66,21 +93,5 @@ namespace MediaBrowser.Controller.Entities return base.GetDefaultPrimaryImageAspectRatio(); } - - public new int? Width { get; set; } - public new int? Height { get; set; } - public string CameraMake { get; set; } - public string CameraModel { get; set; } - public string Software { get; set; } - public double? ExposureTime { get; set; } - public double? FocalLength { get; set; } - public ImageOrientation? Orientation { get; set; } - public double? Aperture { get; set; } - public double? ShutterSpeed { get; set; } - - public double? Latitude { get; set; } - public double? Longitude { get; set; } - public double? Altitude { get; set; } - public int? IsoSpeedRating { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index 4cd0c8b66..a7ecb9061 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -1,16 +1,18 @@ -using MediaBrowser.Model.Serialization; +#pragma warning disable CS1591 + +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { public class PhotoAlbum : Folder { - [IgnoreDataMember] + [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; } } diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs index c17789ccc..64f446eef 100644 --- a/MediaBrowser.Controller/Entities/Share.cs +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -1,13 +1,13 @@ +#nullable disable + +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { - public interface IHasShares - { - Share[] Shares { get; set; } - } - public class Share { public string UserId { get; set; } + public bool CanEdit { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/SourceType.cs b/MediaBrowser.Controller/Entities/SourceType.cs index 927483b93..be19e1bda 100644 --- a/MediaBrowser.Controller/Entities/SourceType.cs +++ b/MediaBrowser.Controller/Entities/SourceType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public enum SourceType diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index d6da0d48c..c8feb1c94 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -1,16 +1,37 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; +using Diacritics.Extensions; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Studio + /// Class Studio. /// </summary> public class Studio : BaseItem, IItemByName { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -18,25 +39,12 @@ namespace MediaBrowser.Controller.Entities list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } + public override string CreatePresentationUniqueKey() { return GetUserDataKeys()[0]; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - [IgnoreDataMember] - public override bool IsDisplayedAsFolder => true; - - [IgnoreDataMember] - public override bool SupportsAncestors => false; - public override double GetDefaultPrimaryImageAspectRatio() { double value = 16; @@ -62,9 +70,6 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -93,15 +98,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param> + /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 072b1d89a..27c3ff81b 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -1,107 +1,80 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.TV { /// <summary> - /// Class Episode + /// Class Episode. /// </summary> public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries { public Episode() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty<MediaUrl>(); LocalTrailerIds = Array.Empty<Guid>(); RemoteTrailerIds = Array.Empty<Guid>(); } - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> - /// Gets the season in which it aired. + /// Gets or sets the season in which it aired. /// </summary> /// <value>The aired season.</value> public int? AirsBeforeSeasonNumber { get; set; } + public int? AirsAfterSeasonNumber { get; set; } + public int? AirsBeforeEpisodeNumber { get; set; } /// <summary> - /// This is the ending episode number for double episodes. + /// Gets or sets the ending episode number for double episodes. /// </summary> /// <value>The index number.</value> public int? IndexNumberEnd { get; set; } - public string FindSeriesSortName() - { - var series = Series; - return series == null ? SeriesName : series.SortName; - } - - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public int? AiredSeasonNumber => AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => Series; - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayParentId => SeasonId; - [IgnoreDataMember] + [JsonIgnore] protected override bool EnableDefaultVideoUserDataKeys => false; - public override double GetDefaultPrimaryImageAspectRatio() - { - // hack for tv plugins - if (SourceType == SourceType.Channel) - { - return 0; - } - - return 16.0 / 9; - } - - public override List<string> GetUserDataKeys() - { - var list = base.GetUserDataKeys(); - - var series = Series; - if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) - { - var seriesUserDataKeys = series.GetUserDataKeys(); - var take = seriesUserDataKeys.Count; - if (seriesUserDataKeys.Count > 1) - { - take--; - } - list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"))); - } - - return list; - } - /// <summary> - /// This Episode's Series Instance + /// Gets the Episode's Series Instance. /// </summary> /// <value>The series.</value> - [IgnoreDataMember] + [JsonIgnore] public Series Series { get @@ -111,11 +84,12 @@ namespace MediaBrowser.Controller.Entities.TV { seriesId = FindSeriesId(); } + return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null; } } - [IgnoreDataMember] + [JsonIgnore] public Season Season { get @@ -125,22 +99,91 @@ namespace MediaBrowser.Controller.Entities.TV { seasonId = FindSeasonId(); } + return !seasonId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seasonId) as Season) : null; } } - [IgnoreDataMember] + [JsonIgnore] public bool IsInSeasonFolder => FindParent<Season>() != null; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeasonName { get; set; } + [JsonIgnore] + public override bool SupportsRemoteImageDownloading + { + get + { + if (IsMissingEpisode) + { + return false; + } + + return true; + } + } + + [JsonIgnore] + public bool IsMissingEpisode => LocationType == LocationType.Virtual; + + [JsonIgnore] + public Guid SeasonId { get; set; } + + [JsonIgnore] + public Guid SeriesId { get; set; } + + public string FindSeriesSortName() + { + var series = Series; + return series == null ? SeriesName : series.SortName; + } + + public override double GetDefaultPrimaryImageAspectRatio() + { + // hack for tv plugins + if (SourceType == SourceType.Channel) + { + return 0; + } + + return 16.0 / 9; + } + + public override List<string> GetUserDataKeys() + { + var list = base.GetUserDataKeys(); + + var series = Series; + if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue) + { + var seriesUserDataKeys = series.GetUserDataKeys(); + var take = seriesUserDataKeys.Count; + if (seriesUserDataKeys.Count > 1) + { + take--; + } + + var newList = seriesUserDataKeys.GetRange(0, take); + var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture); + for (int i = 0; i < take; i++) + { + newList[i] = newList[i] + suffix; + } + + newList.AddRange(list); + list = newList; + } + + return list; + } + public string FindSeriesPresentationUniqueKey() { var series = Series; @@ -157,6 +200,7 @@ namespace MediaBrowser.Controller.Entities.TV { return "Season " + ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture); } + return "Season Unknown"; } @@ -197,8 +241,8 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "") - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } /// <summary> @@ -221,28 +265,6 @@ namespace MediaBrowser.Controller.Entities.TV return false; } - [IgnoreDataMember] - public override bool SupportsRemoteImageDownloading - { - get - { - if (IsMissingEpisode) - { - return false; - } - - return true; - } - } - - [IgnoreDataMember] - public bool IsMissingEpisode => LocationType == LocationType.Virtual; - - [IgnoreDataMember] - public Guid SeasonId { get; set; } - [IgnoreDataMember] - public Guid SeriesId { get; set; } - public Guid FindSeriesId() { var series = FindParent<Series>(); @@ -265,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV public override IEnumerable<FileSystemMetadata> GetDeletePaths() { - return new[] { + return new[] + { new FileSystemMetadata { FullName = Path, @@ -297,9 +320,9 @@ namespace MediaBrowser.Controller.Entities.TV return id; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!IsLocked) { @@ -307,7 +330,7 @@ namespace MediaBrowser.Controller.Entities.TV { try { - if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata)) + if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata)) { hasChanges = true; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 5d7c260d1..926c7b045 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,38 +1,87 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.TV { /// <summary> - /// Class Season + /// Class Season. /// </summary> public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsDateLastMediaAdded => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayParentId => SeriesId; + /// <summary> + /// Gets this Episode's Series Instance. + /// </summary> + /// <value>The series.</value> + [JsonIgnore] + public Series Series + { + get + { + var seriesId = SeriesId; + if (seriesId == Guid.Empty) + { + seriesId = FindSeriesId(); + } + + return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series); + } + } + + [JsonIgnore] + public string SeriesPath + { + get + { + var series = Series; + + if (series != null) + { + return series.Path; + } + + return System.IO.Path.GetDirectoryName(Path); + } + } + + [JsonIgnore] + public string SeriesPresentationUniqueKey { get; set; } + + [JsonIgnore] + public string SeriesName { get; set; } + + [JsonIgnore] + public Guid SeriesId { get; set; } + public override double GetDefaultPrimaryImageAspectRatio() { double value = 2; @@ -54,7 +103,15 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series != null) { - list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000"))); + var newList = series.GetUserDataKeys(); + var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); + for (int i = 0; i < newList.Count; i++) + { + newList[i] = newList[i] + suffix; + } + + newList.AddRange(list); + list = newList; } return list; @@ -67,40 +124,6 @@ namespace MediaBrowser.Controller.Entities.TV return result; } - /// <summary> - /// This Episode's Series Instance - /// </summary> - /// <value>The series.</value> - [IgnoreDataMember] - public Series Series - { - get - { - var seriesId = SeriesId; - if (seriesId == Guid.Empty) - { - seriesId = FindSeriesId(); - } - return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series); - } - } - - [IgnoreDataMember] - public string SeriesPath - { - get - { - var series = Series; - - if (series != null) - { - return series.Path; - } - - return System.IO.Path.GetDirectoryName(Path); - } - } - public override string CreatePresentationUniqueKey() { if (IndexNumber.HasValue) @@ -108,7 +131,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series != null) { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); + return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); } } @@ -121,7 +144,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name; + return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name; } protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) @@ -143,6 +166,9 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Gets the episodes. /// </summary> + /// <param name="user">The user.</param> + /// <param name="options">The options to use.</param> + /// <returns>Set of episodes.</returns> public List<BaseItem> GetEpisodes(User user, DtoOptions options) { return GetEpisodes(Series, user, options); @@ -168,7 +194,7 @@ namespace MediaBrowser.Controller.Entities.TV return GetEpisodes(user, new DtoOptions(true)); } - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(User user) { // Don't block. Let either the entire series rating or episode rating determine it return false; @@ -179,15 +205,6 @@ namespace MediaBrowser.Controller.Entities.TV return UnratedItem.Series; } - [IgnoreDataMember] - public string SeriesPresentationUniqueKey { get; set; } - - [IgnoreDataMember] - public string SeriesName { get; set; } - - [IgnoreDataMember] - public Guid SeriesId { get; set; } - public string FindSeriesPresentationUniqueKey() { var series = Series; @@ -203,7 +220,7 @@ namespace MediaBrowser.Controller.Entities.TV public Guid FindSeriesId() { var series = FindParent<Series>(); - return series == null ? Guid.Empty : series.Id; + return series?.Id ?? Guid.Empty; } /// <summary> @@ -225,16 +242,17 @@ namespace MediaBrowser.Controller.Entities.TV } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> + /// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { - IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path); + IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); // If a change was made record it if (IndexNumber.HasValue) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 570e9389e..e4933e968 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,57 +1,69 @@ +#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.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace MediaBrowser.Controller.Entities.TV { /// <summary> - /// Class Series + /// Class Series. /// </summary> public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer { public Series() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty<MediaUrl>(); LocalTrailerIds = Array.Empty<Guid>(); RemoteTrailerIds = Array.Empty<Guid>(); AirDays = Array.Empty<DayOfWeek>(); } public DayOfWeek[] AirDays { get; set; } + public string AirTime { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsDateLastMediaAdded => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> - /// airdate, dvd or absolute + /// Gets or sets the display order. /// </summary> + /// <remarks> + /// Valid options are airdate, dvd or absolute. + /// </remarks> public string DisplayOrder { get; set; } /// <summary> @@ -60,6 +72,9 @@ namespace MediaBrowser.Controller.Entities.TV /// <value>The status.</value> public SeriesStatus? Status { get; set; } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public override double GetDefaultPrimaryImageAspectRatio() { double value = 2; @@ -92,7 +107,7 @@ namespace MediaBrowser.Controller.Entities.TV } var folders = LibraryManager.GetCollectionFolders(this) - .Select(i => i.Id.ToString("N")) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .ToArray(); if (folders.Length == 0) @@ -100,7 +115,7 @@ namespace MediaBrowser.Controller.Entities.TV return key; } - return key + "-" + string.Join("-", folders); + return key + "-" + string.Join('-', folders); } private static string GetUniqueSeriesKey(BaseItem series) @@ -116,7 +131,7 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Season).Name }, + IncludeItemTypes = new[] { nameof(Season) }, IsVirtualItem = false, Limit = 0, DtoOptions = new DtoOptions(false) @@ -144,8 +159,9 @@ namespace MediaBrowser.Controller.Entities.TV if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; } + query.IsVirtualItem = false; query.Limit = 0; var totalRecordCount = LibraryManager.GetCount(query); @@ -161,14 +177,12 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetUserDataKeys(); - var key = this.GetProviderId(MetadataProviders.Imdb); - if (!string.IsNullOrEmpty(key)) + if (this.TryGetProviderId(MetadataProvider.Imdb, out var key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tvdb); - if (!string.IsNullOrEmpty(key)) + if (this.TryGetProviderId(MetadataProvider.Tvdb, out key)) { list.Insert(0, key); } @@ -199,17 +213,12 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - query.IncludeItemTypes = new[] { typeof(Season).Name }; - query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + query.IncludeItemTypes = new[] { nameof(Season) }; + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; - if (user != null) + if (user != null && !user.DisplayMissingEpisodes) { - var config = user.Configuration; - - if (!config.DisplayMissingEpisodes) - { - query.IsMissing = false; - } + query.IsMissing = false; } } @@ -223,14 +232,16 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - if (query.OrderBy.Length == 0) + if (query.OrderBy.Count == 0) { - query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; } + if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }; + query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }; } + query.IsVirtualItem = false; return LibraryManager.GetItemsResult(query); } @@ -248,12 +259,12 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name }, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; - var config = user.Configuration; - if (!config.DisplayMissingEpisodes) + + if (!user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -285,7 +296,7 @@ namespace MediaBrowser.Controller.Entities.TV // Refresh seasons foreach (var item in items) { - if (!(item is Season)) + if (item is not Season) { continue; } @@ -306,27 +317,20 @@ namespace MediaBrowser.Controller.Entities.TV // Refresh episodes and other children foreach (var item in items) { - if ((item is Season)) + if (item is Season) { continue; } cancellationToken.ThrowIfCancellationRequested(); - var skipItem = false; - - var episode = item as Episode; - - if (episode != null + 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) - { - skipItem = true; - } + && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30; if (!skipItem) { @@ -359,14 +363,13 @@ namespace MediaBrowser.Controller.Entities.TV { AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey, SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null, - IncludeItemTypes = new[] { typeof(Episode).Name }, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + IncludeItemTypes = new[] { nameof(Episode) }, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; if (user != null) { - var config = user.Configuration; - if (!config.DisplayMissingEpisodes) + if (!user.DisplayMissingEpisodes) { query.IsMissing = false; } @@ -394,6 +397,10 @@ namespace MediaBrowser.Controller.Entities.TV /// <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; @@ -424,6 +431,10 @@ namespace MediaBrowser.Controller.Entities.TV /// <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) @@ -446,10 +457,9 @@ namespace MediaBrowser.Controller.Entities.TV }); } - - protected override bool GetBlockUnratedValue(UserPolicy config) + protected override bool GetBlockUnratedValue(User user) { - return config.BlockUnratedItems.Contains(UnratedItem.Series); + return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series); } public override UnratedItem GetBlockUnratedType() @@ -488,20 +498,17 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/shows/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId) }); } return list; } - - [IgnoreDataMember] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs index 97f590635..2ce396daf 100644 --- a/MediaBrowser.Controller/Entities/TagExtensions.cs +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 5bf22d7bc..1c558d419 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,15 +1,20 @@ +#nullable disable + +#pragma warning disable CA1819, CS1591 + using System; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Trailer + /// Class Trailer. /// </summary> public class Trailer : Video, IHasLookupInfo<TrailerInfo> { @@ -18,6 +23,9 @@ namespace MediaBrowser.Controller.Entities TrailerTypes = Array.Empty<TrailerType>(); } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public TrailerType[] TrailerTypes { get; set; } public override double GetDefaultPrimaryImageAspectRatio() @@ -40,9 +48,9 @@ namespace MediaBrowser.Controller.Entities return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { @@ -80,20 +88,17 @@ namespace MediaBrowser.Controller.Entities { var list = base.GetRelatedUrls(); - var imdbId = this.GetProviderId(MetadataProviders.Imdb); + var imdbId = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(imdbId)) { list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } return list; } - - [IgnoreDataMember] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs deleted file mode 100644 index 0d5f508dd..000000000 --- a/MediaBrowser.Controller/Entities/User.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace MediaBrowser.Controller.Entities -{ - /// <summary> - /// Class User - /// </summary> - public class User : BaseItem - { - public static IUserManager UserManager { get; set; } - public static IXmlSerializer XmlSerializer { get; set; } - - /// <summary> - /// From now on all user paths will be Id-based. - /// This is for backwards compatibility. - /// </summary> - public bool UsesIdForConfigurationPath { get; set; } - - /// <summary> - /// Gets or sets the password. - /// </summary> - /// <value>The password.</value> - public string Password { get; set; } - public string EasyPassword { get; set; } - public string Salt { get; set; } - - // Strictly to remove IgnoreDataMember - public override ItemImageInfo[] ImageInfos - { - get => base.ImageInfos; - set => base.ImageInfos = value; - } - - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - [IgnoreDataMember] - public override string Path - { - get => ConfigurationDirectoryPath; - set => base.Path = value; - } - - private string _name; - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public override string Name - { - get => _name; - set - { - _name = value; - - // lazy load this again - SortName = null; - } - } - - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - - /// <summary> - /// Gets the root folder. - /// </summary> - /// <value>The root folder.</value> - [IgnoreDataMember] - public Folder RootFolder => LibraryManager.GetUserRootFolder(); - - /// <summary> - /// Gets or sets the last login date. - /// </summary> - /// <value>The last login date.</value> - public DateTime? LastLoginDate { get; set; } - /// <summary> - /// Gets or sets the last activity date. - /// </summary> - /// <value>The last activity date.</value> - public DateTime? LastActivityDate { get; set; } - - private volatile UserConfiguration _config; - private readonly object _configSyncLock = new object(); - [IgnoreDataMember] - public UserConfiguration Configuration - { - get - { - if (_config == null) - { - lock (_configSyncLock) - { - if (_config == null) - { - _config = UserManager.GetUserConfiguration(this); - } - } - } - - return _config; - } - set => _config = value; - } - - private volatile UserPolicy _policy; - private readonly object _policySyncLock = new object(); - [IgnoreDataMember] - public UserPolicy Policy - { - get - { - if (_policy == null) - { - lock (_policySyncLock) - { - if (_policy == null) - { - _policy = UserManager.GetUserPolicy(this); - } - } - } - - return _policy; - } - set => _policy = value; - } - - /// <summary> - /// Renames the user. - /// </summary> - /// <param name="newName">The new name.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException"></exception> - public Task Rename(string newName) - { - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - // If only the casing is changing, leave the file system alone - if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) - { - UsesIdForConfigurationPath = true; - - // Move configuration - var newConfigDirectory = GetConfigurationDirectoryPath(newName); - var oldConfigurationDirectory = ConfigurationDirectoryPath; - - // Exceptions will be thrown if these paths already exist - if (Directory.Exists(newConfigDirectory)) - { - Directory.Delete(newConfigDirectory, true); - } - - if (Directory.Exists(oldConfigurationDirectory)) - { - Directory.Move(oldConfigurationDirectory, newConfigDirectory); - } - else - { - Directory.CreateDirectory(newConfigDirectory); - } - } - - Name = newName; - - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - ReplaceAllMetadata = true, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = true - - }, CancellationToken.None); - } - - public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) - { - UserManager.UpdateUser(this); - } - - /// <summary> - /// Gets the path to the user's configuration directory - /// </summary> - /// <value>The configuration directory path.</value> - [IgnoreDataMember] - public string ConfigurationDirectoryPath => GetConfigurationDirectoryPath(Name); - - public override double GetDefaultPrimaryImageAspectRatio() - { - return 1; - } - - /// <summary> - /// Gets the configuration directory path. - /// </summary> - /// <param name="username">The username.</param> - /// <returns>System.String.</returns> - private string GetConfigurationDirectoryPath(string username) - { - var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; - - // Legacy - if (!UsesIdForConfigurationPath) - { - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var safeFolderName = FileSystem.GetValidFilename(username); - - return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); - } - - return System.IO.Path.Combine(parentPath, Id.ToString("N")); - } - - public bool IsParentalScheduleAllowed() - { - return IsParentalScheduleAllowed(DateTime.UtcNow); - } - - public bool IsParentalScheduleAllowed(DateTime date) - { - var schedules = Policy.AccessSchedules; - - if (schedules.Length == 0) - { - return true; - } - - foreach (var i in schedules) - { - if (IsParentalScheduleAllowed(i, date)) - { - return true; - } - } - return false; - } - - private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) - { - if (date.Kind != DateTimeKind.Utc) - { - throw new ArgumentException("Utc date expected"); - } - - var localTime = date.ToLocalTime(); - - return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) && - IsWithinTime(schedule, localTime); - } - - private bool IsWithinTime(AccessSchedule schedule, DateTime localTime) - { - var hour = localTime.TimeOfDay.TotalHours; - - return hour >= schedule.StartHour && hour <= schedule.EndHour; - } - - public bool IsFolderGrouped(Guid id) - { - foreach (var i in Configuration.GroupedFolders) - { - if (new Guid(i) == id) - { - return true; - } - } - return false; - } - - [IgnoreDataMember] - public override bool SupportsPeople => false; - - public long InternalId { get; set; } - - - } -} diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index f7136bdf2..50ba9ef30 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -1,13 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class UserItemData + /// Class UserItemData. /// </summary> public class UserItemData { + public const double MinLikeValue = 6.5; + + /// <summary> + /// The _rating. + /// </summary> + private double? _rating; + /// <summary> /// Gets or sets the user id. /// </summary> @@ -21,11 +32,7 @@ namespace MediaBrowser.Controller.Entities public string Key { get; set; } /// <summary> - /// The _rating - /// </summary> - private double? _rating; - /// <summary> - /// Gets or sets the users 0-10 rating + /// Gets or sets the users 0-10 rating. /// </summary> /// <value>The rating.</value> /// <exception cref="ArgumentOutOfRangeException">Rating;A 0 to 10 rating is required for UserItemData.</exception> @@ -75,25 +82,25 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <value><c>true</c> if played; otherwise, <c>false</c>.</value> public bool Played { get; set; } + /// <summary> /// Gets or sets the index of the audio stream. /// </summary> /// <value>The index of the audio stream.</value> public int? AudioStreamIndex { get; set; } + /// <summary> /// Gets or sets the index of the subtitle stream. /// </summary> /// <value>The index of the subtitle stream.</value> public int? SubtitleStreamIndex { get; set; } - public const double MinLikeValue = 6.5; - /// <summary> - /// This is an interpreted property to indicate likes or dislikes + /// Gets or sets a value indicating whether the item is liked or not. /// This should never be serialized. /// </summary> /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool? Likes { get @@ -105,6 +112,7 @@ namespace MediaBrowser.Controller.Entities return null; } + set { if (value.HasValue) diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7fe8df8af..e547db523 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,12 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -16,29 +21,29 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class UserRootFolder : Folder { - private List<Guid> _childrenIds = null; private readonly object _childIdsLock = new object(); - protected override List<BaseItem> LoadChildren() - { - lock (_childIdsLock) - { - if (_childrenIds == null) - { - var list = base.LoadChildren(); - _childrenIds = list.Select(i => i.Id).ToList(); - return list; - } + private List<Guid> _childrenIds = null; - return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); - } + /// <summary> + /// Initializes a new instance of the <see cref="UserRootFolder"/> class. + /// </summary> + public UserRootFolder() + { + IsRoot = true; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; + [JsonIgnore] + protected override bool SupportsShortcutChildren => true; + + [JsonIgnore] + public override bool IsPreSorted => true; + private void ClearCache() { lock (_childIdsLock) @@ -47,6 +52,21 @@ namespace MediaBrowser.Controller.Entities } } + protected override List<BaseItem> LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null) + { + var list = base.LoadChildren(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { if (query.Recursive) @@ -60,14 +80,7 @@ namespace MediaBrowser.Controller.Entities PresetViews = query.PresetViews }); - var itemsArray = result; - var totalCount = itemsArray.Length; - - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = itemsArray //TODO Fix The co-variant conversion between Folder[] and BaseItem[], this can generate runtime issues. - }; + return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true); } public override int GetChildCount(User user) @@ -75,12 +88,6 @@ namespace MediaBrowser.Controller.Entities return GetChildren(user, true).Count; } - [IgnoreDataMember] - protected override bool SupportsShortcutChildren => true; - - [IgnoreDataMember] - public override bool IsPreSorted => true; - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList(); @@ -89,10 +96,10 @@ namespace MediaBrowser.Controller.Entities return list; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { ClearCache(); - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase)) { @@ -110,11 +117,11 @@ namespace MediaBrowser.Controller.Entities return base.GetNonCachedChildren(directoryService); } - protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { ClearCache(); - await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken) .ConfigureAwait(false); ClearCache(); diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 3e2191376..62f3c4b55 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -1,57 +1,94 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; -using MediaBrowser.Controller.Playlists; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class UserView : Folder, IHasCollectionType { + private static readonly string[] _viewTypesEligibleForGrouping = new string[] + { + Model.Entities.CollectionType.Movies, + Model.Entities.CollectionType.TvShows, + string.Empty + }; + + private static readonly string[] _originalFolderViewTypes = new string[] + { + Model.Entities.CollectionType.Books, + Model.Entities.CollectionType.MusicVideos, + Model.Entities.CollectionType.HomeVideos, + Model.Entities.CollectionType.Photos, + Model.Entities.CollectionType.Music, + Model.Entities.CollectionType.BoxSets + }; + + public static ITVSeriesManager TVSeriesManager { get; set; } + + /// <summary> + /// Gets or sets the view type. + /// </summary> public string ViewType { get; set; } + + /// <summary> + /// Gets or sets the display parent id. + /// </summary> public new Guid DisplayParentId { get; set; } + /// <summary> + /// Gets or sets the user id. + /// </summary> public Guid? UserId { get; set; } - public static ITVSeriesManager TVSeriesManager; - public static IPlaylistManager PlaylistManager; - - [IgnoreDataMember] + /// <inheritdoc /> + [JsonIgnore] public string CollectionType => ViewType; + /// <inheritdoc /> + [JsonIgnore] + public override bool SupportsInheritedParentImages => false; + + /// <inheritdoc /> + [JsonIgnore] + public override bool SupportsPlayedStatus => false; + + /// <inheritdoc /> + [JsonIgnore] + public override bool SupportsPeople => false; + + /// <inheritdoc /> public override IEnumerable<Guid> GetIdsForAncestorQuery() { - var list = new List<Guid>(); - if (!DisplayParentId.Equals(Guid.Empty)) { - list.Add(DisplayParentId); + yield return DisplayParentId; } else if (!ParentId.Equals(Guid.Empty)) { - list.Add(ParentId); + yield return ParentId; } else { - list.Add(Id); + yield return Id; } - return list; } - [IgnoreDataMember] - public override bool SupportsInheritedParentImages => false; - - [IgnoreDataMember] - public override bool SupportsPlayedStatus => false; - + /// <inheritdoc /> public override int GetChildCount(User user) { return GetChildren(user, true).Count; } + /// <inheritdoc /> protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) { var parent = this as Folder; @@ -65,16 +102,14 @@ namespace MediaBrowser.Controller.Entities parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } - return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager) + return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager) .GetUserItems(parent, this, CollectionType, query); } + /// <inheritdoc /> public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { - if (query == null) - { - query = new InternalItemsQuery(user); - } + query ??= new InternalItemsQuery(user); query.EnableTotalRecordCount = false; var result = GetItemList(query); @@ -82,16 +117,19 @@ namespace MediaBrowser.Controller.Entities return result.ToList(); } + /// <inheritdoc /> public override bool CanDelete() { return false; } + /// <inheritdoc /> public override bool IsSaveLocalMetadataEnabled() { return true; } + /// <inheritdoc /> public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { query.SetUser(user); @@ -102,32 +140,26 @@ namespace MediaBrowser.Controller.Entities return GetItemList(query); } + /// <inheritdoc /> protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return GetChildren(user, false); } - private static string[] UserSpecificViewTypes = new string[] - { - MediaBrowser.Model.Entities.CollectionType.Playlists - }; - public static bool IsUserSpecific(Folder folder) { - var collectionFolder = folder as ICollectionFolder; - - if (collectionFolder == null) + if (folder is not ICollectionFolder collectionFolder) { return false; } - var supportsUserSpecific = folder as ISupportsUserSpecificView; - if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView) + if (folder is ISupportsUserSpecificView supportsUserSpecific + && supportsUserSpecific.EnableUserSpecificView) { return true; } - return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase); } public static bool IsEligibleForGrouping(Folder folder) @@ -136,39 +168,19 @@ namespace MediaBrowser.Controller.Entities && IsEligibleForGrouping(collectionFolder.CollectionType); } - private static string[] ViewTypesEligibleForGrouping = new string[] - { - MediaBrowser.Model.Entities.CollectionType.Movies, - MediaBrowser.Model.Entities.CollectionType.TvShows, - string.Empty - }; - public static bool IsEligibleForGrouping(string viewType) { - return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); } - private static string[] OriginalFolderViewTypes = new string[] - { - MediaBrowser.Model.Entities.CollectionType.Books, - MediaBrowser.Model.Entities.CollectionType.MusicVideos, - MediaBrowser.Model.Entities.CollectionType.HomeVideos, - MediaBrowser.Model.Entities.CollectionType.Photos, - MediaBrowser.Model.Entities.CollectionType.Music, - MediaBrowser.Model.Entities.CollectionType.BoxSets - }; - public static bool EnableOriginalFolder(string viewType) { - return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken) { return Task.CompletedTask; } - - [IgnoreDataMember] - public override bool SupportsPeople => false; } } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 683218a9e..266fda767 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -1,15 +1,25 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; +using Season = MediaBrowser.Controller.Entities.TV.Season; +using Series = MediaBrowser.Controller.Entities.TV.Series; namespace MediaBrowser.Controller.Entities { @@ -17,13 +27,18 @@ namespace MediaBrowser.Controller.Entities { private readonly IUserViewManager _userViewManager; private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger<BaseItem> _logger; private readonly IUserDataManager _userDataManager; private readonly ITVSeriesManager _tvSeriesManager; private readonly IServerConfigurationManager _config; - private readonly IPlaylistManager _playlistManager; - public UserViewBuilder(IUserViewManager userViewManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager) + public UserViewBuilder( + IUserViewManager userViewManager, + ILibraryManager libraryManager, + ILogger<BaseItem> logger, + IUserDataManager userDataManager, + ITVSeriesManager tvSeriesManager, + IServerConfigurationManager config) { _userViewManager = userViewManager; _libraryManager = libraryManager; @@ -31,27 +46,26 @@ namespace MediaBrowser.Controller.Entities _userDataManager = userDataManager; _tvSeriesManager = tvSeriesManager; _config = config; - _playlistManager = playlistManager; } public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query) { var user = query.User; - //if (query.IncludeItemTypes != null && + // if (query.IncludeItemTypes != null && // query.IncludeItemTypes.Length == 1 && // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) - //{ + // { // if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) // { // return await FindPlaylists(queryParent, user, query).ConfigureAwait(false); // } - //} + // } switch (viewType) { case CollectionType.Folders: - return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query); + return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query); case CollectionType.TvShows: return GetTvView(queryParent, user, query); @@ -96,7 +110,7 @@ namespace MediaBrowser.Controller.Entities return GetMovieMovies(queryParent, user, query); case SpecialFolder.MovieCollections: - return GetMovieCollections(queryParent, user, query); + return GetMovieCollections(user, query); case SpecialFolder.TvFavoriteEpisodes: return GetFavoriteEpisodes(queryParent, user, query); @@ -108,8 +122,9 @@ namespace MediaBrowser.Controller.Entities { if (queryParent is UserView) { - return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query); + return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), query); } + return queryParent.GetItems(query); } } @@ -129,22 +144,23 @@ namespace MediaBrowser.Controller.Entities if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; } return parent.QueryRecursive(query); } - var list = new List<BaseItem>(); - - list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent)); - list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent)); - list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent)); - list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent)); - list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent)); - list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)); + var list = new List<BaseItem> + { + GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent), + GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent), + GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent), + GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent), + GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent), + GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent) + }; - return GetResult(list, parent, query); + return GetResult(list, query); } private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query) @@ -153,7 +169,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return _libraryManager.GetItemsResult(query); } @@ -164,7 +180,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; return _libraryManager.GetItemsResult(query); } @@ -175,7 +191,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); query.IsFavorite = true; - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; return _libraryManager.GetItemsResult(query); } @@ -186,15 +202,15 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return _libraryManager.GetItemsResult(query); } - private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query) + private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query) { query.Parent = null; - query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; + query.IncludeItemTypes = new[] { nameof(BoxSet) }; query.SetUser(user); query.Recursive = true; @@ -203,26 +219,25 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); - + query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return ConvertToResult(_libraryManager.GetItemList(query)); } private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.IsResumable = true; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -241,10 +256,9 @@ namespace MediaBrowser.Controller.Entities { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Movie).Name }, + IncludeItemTypes = new[] { nameof(Movie) }, Recursive = true, EnableTotalRecordCount = false - }).Items .SelectMany(i => i.Genres) .DistinctNames() @@ -259,12 +273,11 @@ namespace MediaBrowser.Controller.Entities _logger.LogError(ex, "Error getting genre"); return null; } - }) .Where(i => i != null) - .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent)); - return GetResult(genres, parent, query); + return GetResult(genres, query); } private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) @@ -274,7 +287,7 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { displayParent.Id }; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Movie).Name }; + query.IncludeItemTypes = new[] { nameof(Movie) }; return _libraryManager.GetItemsResult(query); } @@ -288,34 +301,39 @@ namespace MediaBrowser.Controller.Entities if (query.IncludeItemTypes.Length == 0) { - query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name }; + query.IncludeItemTypes = new[] + { + nameof(Series), + nameof(Season), + nameof(Episode) + }; } return parent.QueryRecursive(query); } - var list = new List<BaseItem>(); - - list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent)); - list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent)); - list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent)); - list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent)); - list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent)); - list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent)); - list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)); + var list = new List<BaseItem> + { + GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent), + GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent), + GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent), + GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent), + GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), + GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent) + }; - return GetResult(list, parent, query); + return GetResult(list, query); } private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); - + query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; query.IsVirtualItem = false; return ConvertToResult(_libraryManager.GetItemList(query)); @@ -325,26 +343,28 @@ namespace MediaBrowser.Controller.Entities { var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); - var result = _tvSeriesManager.GetNextUp(new NextUpQuery - { - Limit = query.Limit, - StartIndex = query.StartIndex, - UserId = query.User.Id - - }, parentFolders, query.DtoOptions); + var result = _tvSeriesManager.GetNextUp( + new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + }, + parentFolders, + query.DtoOptions); return result; } private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.IsResumable = true; query.Recursive = true; query.Parent = parent; query.SetUser(user); query.Limit = GetSpecialItemsLimit(); - query.IncludeItemTypes = new[] { typeof(Episode).Name }; + query.IncludeItemTypes = new[] { nameof(Episode) }; return ConvertToResult(_libraryManager.GetItemList(query)); } @@ -355,7 +375,7 @@ namespace MediaBrowser.Controller.Entities query.Parent = parent; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; return _libraryManager.GetItemsResult(query); } @@ -364,10 +384,9 @@ namespace MediaBrowser.Controller.Entities { var genres = parent.QueryRecursive(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Series).Name }, + IncludeItemTypes = new[] { nameof(Series) }, Recursive = true, EnableTotalRecordCount = false - }).Items .SelectMany(i => i.Genres) .DistinctNames() @@ -382,12 +401,11 @@ namespace MediaBrowser.Controller.Entities _logger.LogError(ex, "Error getting genre"); return null; } - }) .Where(i => i != null) - .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent)); - return GetResult(genres, parent, query); + return GetResult(genres, query); } private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) @@ -397,7 +415,7 @@ namespace MediaBrowser.Controller.Entities query.GenreIds = new[] { displayParent.Id }; query.SetUser(user); - query.IncludeItemTypes = new[] { typeof(Series).Name }; + query.IncludeItemTypes = new[] { nameof(Series) }; return _libraryManager.GetItemsResult(query); } @@ -407,19 +425,19 @@ namespace MediaBrowser.Controller.Entities { return new QueryResult<BaseItem> { - Items = result.Items, //TODO Fix The co-variant conversion between T[] and BaseItem[], this can generate runtime issues if T is not BaseItem. + Items = result.Items, // TODO Fix The co-variant conversion between T[] and BaseItem[], this can generate runtime issues if T is not BaseItem. TotalRecordCount = result.TotalRecordCount }; } - private QueryResult<BaseItem> GetResult<T>(IEnumerable<T> items, - BaseItem queryParent, + private QueryResult<BaseItem> GetResult<T>( + IEnumerable<T> items, InternalItemsQuery query) where T : BaseItem { items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager)); - return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config); + return PostFilterAndSort(items, null, query, _libraryManager); } public static bool FilterItem(BaseItem item, InternalItemsQuery query) @@ -427,12 +445,11 @@ namespace MediaBrowser.Controller.Entities return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager); } - public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, - BaseItem queryParent, + public static QueryResult<BaseItem> PostFilterAndSort( + IEnumerable<BaseItem> items, int? totalRecordLimit, InternalItemsQuery query, - ILibraryManager libraryManager, - IServerConfigurationManager configurationManager) + ILibraryManager libraryManager) { var user = query.User; @@ -445,14 +462,16 @@ namespace MediaBrowser.Controller.Entities return SortAndPage(items, totalRecordLimit, query, libraryManager, true); } - public static QueryResult<BaseItem> SortAndPage(IEnumerable<BaseItem> items, + public static QueryResult<BaseItem> SortAndPage( + IEnumerable<BaseItem> items, int? totalRecordLimit, InternalItemsQuery query, - ILibraryManager libraryManager, bool enableSorting) + ILibraryManager libraryManager, + bool enableSorting) { if (enableSorting) { - if (query.OrderBy.Length > 0) + if (query.OrderBy.Count > 0) { items = libraryManager.Sort(items, query.User, query.OrderBy); } @@ -604,7 +623,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasImdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Imdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Imdb)); if (hasValue != filterValue) { @@ -616,7 +635,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasTmdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Tmdb)); if (hasValue != filterValue) { @@ -628,7 +647,7 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasTvdbId.Value; - var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb)); + var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProvider.Tvdb)); if (hasValue != filterValue) { @@ -654,9 +673,7 @@ namespace MediaBrowser.Controller.Entities var isPlaceHolder = false; - var hasPlaceHolder = item as ISupportsPlaceHolders; - - if (hasPlaceHolder != null) + if (item is ISupportsPlaceHolders hasPlaceHolder) { isPlaceHolder = hasPlaceHolder.IsPlaceHolder; } @@ -671,13 +688,11 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasSpecialFeature.Value; - var movie = item as IHasSpecialFeatures; - - if (movie != null) + if (item is IHasSpecialFeatures movie) { var ok = filterValue - ? movie.SpecialFeatureIds.Length > 0 - : movie.SpecialFeatureIds.Length == 0; + ? movie.SpecialFeatureIds.Count > 0 + : movie.SpecialFeatureIds.Count == 0; if (!ok) { @@ -775,7 +790,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) + if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -806,7 +821,7 @@ namespace MediaBrowser.Controller.Entities } // Apply genre filter - if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => + if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id => { var genreItem = libraryManager.GetItemById(id); return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); @@ -944,6 +959,7 @@ namespace MediaBrowser.Controller.Entities .OfType<Folder>() .Where(UserView.IsEligibleForGrouping); } + return _libraryManager.GetUserRootFolder() .GetChildren(user, true) .OfType<Folder>() @@ -962,6 +978,7 @@ namespace MediaBrowser.Controller.Entities return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); }).ToArray(); } + return GetMediaFolders(user) .Where(i => { @@ -981,9 +998,9 @@ namespace MediaBrowser.Controller.Entities return new BaseItem[] { parent }; } - private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent) + private UserView GetUserViewWithName(string type, string sortName, BaseItem parent) { - return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N"), type, sortName); + return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName); } private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 31cd42975..7dd95b85c 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -1,47 +1,60 @@ +#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 MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Video + /// Class Video. /// </summary> public class Video : BaseItem, IHasAspectRatio, ISupportsPlaceHolders, IHasMediaSources { - [IgnoreDataMember] + public Video() + { + AdditionalParts = Array.Empty<string>(); + LocalAlternateVersions = Array.Empty<string>(); + SubtitleFiles = Array.Empty<string>(); + LinkedAlternateVersions = Array.Empty<LinkedChild>(); + } + + [JsonIgnore] public string PrimaryVersionId { get; set; } public string[] AdditionalParts { get; set; } + public string[] LocalAlternateVersions { get; set; } + public LinkedChild[] LinkedAlternateVersions { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume { get @@ -53,44 +66,23 @@ namespace MediaBrowser.Controller.Entities { return false; } + if (extraType.Value == Model.Entities.ExtraType.ThemeVideo) { return false; } + if (extraType.Value == Model.Entities.ExtraType.Trailer) { return false; } } - return true; - } - } - public void SetPrimaryVersionId(string id) - { - if (string.IsNullOrEmpty(id)) - { - PrimaryVersionId = null; - } - else - { - PrimaryVersionId = id; - } - - PresentationUniqueKey = CreatePresentationUniqueKey(); - } - - public override string CreatePresentationUniqueKey() - { - if (!string.IsNullOrEmpty(PrimaryVersionId)) - { - return PrimaryVersionId; + return true; } - - return base.CreatePresentationUniqueKey(); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsThemeMedia => true; /// <summary> @@ -137,53 +129,16 @@ namespace MediaBrowser.Controller.Entities /// <value>The video3 D format.</value> public Video3DFormat? Video3DFormat { get; set; } - public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder) - { - var videoType = VideoType; - - if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay) - { - videoType = VideoType.BluRay; - } - else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd) - { - videoType = VideoType.Dvd; - } - else - { - return Array.Empty<string>(); - } - return mediaEncoder.GetPlayableStreamFileNames(Path, videoType); - } - /// <summary> /// Gets or sets the aspect ratio. /// </summary> /// <value>The aspect ratio.</value> public string AspectRatio { get; set; } - public Video() - { - AdditionalParts = Array.Empty<string>(); - LocalAlternateVersions = Array.Empty<string>(); - SubtitleFiles = Array.Empty<string>(); - LinkedAlternateVersions = EmptyLinkedChildArray; - } - - public override bool CanDownload() - { - if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay) - { - return false; - } - - return IsFileProtocol; - } - - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public int MediaSourceCount { get @@ -196,29 +151,20 @@ namespace MediaBrowser.Controller.Entities return video.MediaSourceCount; } } + return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1; } } - [IgnoreDataMember] + [JsonIgnore] public bool IsStacked => AdditionalParts.Length > 0; - [IgnoreDataMember] + [JsonIgnore] public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0; - public IEnumerable<Guid> GetAdditionalPartIds() - { - return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); - } - - public IEnumerable<Guid> GetLocalAlternateVersionIds() - { - return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); - } - public static ILiveTvManager LiveTvManager { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType { get @@ -232,37 +178,77 @@ namespace MediaBrowser.Controller.Entities } } - protected override bool IsActiveRecording() + [JsonIgnore] + public bool IsCompleteMedia { - return LiveTvManager.GetActiveRecordingInfo(Path) != null; + get + { + if (SourceType == SourceType.Channel) + { + return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + } + + return !IsActiveRecording(); + } } - public override bool CanDelete() + [JsonIgnore] + protected virtual bool EnableDefaultVideoUserDataKeys => true; + + [JsonIgnore] + public override string ContainingFolderPath { - if (IsActiveRecording()) + get { - return false; - } + if (IsStacked) + { + return System.IO.Path.GetDirectoryName(Path); + } - return base.CanDelete(); + if (!IsPlaceHolder) + { + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return Path; + } + } + + return base.ContainingFolderPath; + } } - [IgnoreDataMember] - public bool IsCompleteMedia + [JsonIgnore] + public override string FileNameWithoutExtension { get { - if (SourceType == SourceType.Channel) + if (IsFileProtocol) { - return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase); + if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) + { + return System.IO.Path.GetFileName(Path); + } + + return System.IO.Path.GetFileNameWithoutExtension(Path); } - return !IsActiveRecording(); + return null; } } - [IgnoreDataMember] - protected virtual bool EnableDefaultVideoUserDataKeys => true; + /// <summary> + /// Gets a value indicating whether [is3 D]. + /// </summary> + /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> + [JsonIgnore] + public bool Is3D => Video3DFormat.HasValue; + + /// <summary> + /// Gets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + [JsonIgnore] + public override string MediaType => Model.Entities.MediaType.Video; public override List<string> GetUserDataKeys() { @@ -272,13 +258,13 @@ namespace MediaBrowser.Controller.Entities { if (ExtraType.HasValue) { - var key = this.GetProviderId(MetadataProviders.Tmdb); + var key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, GetUserDataKey(key)); } - key = this.GetProviderId(MetadataProviders.Imdb); + key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, GetUserDataKey(key)); @@ -286,13 +272,13 @@ namespace MediaBrowser.Controller.Entities } else { - var key = this.GetProviderId(MetadataProviders.Imdb); + var key = this.GetProviderId(MetadataProvider.Imdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProviders.Tmdb); + key = this.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(key)) { list.Insert(0, key); @@ -303,6 +289,65 @@ namespace MediaBrowser.Controller.Entities return list; } + public void SetPrimaryVersionId(string id) + { + if (string.IsNullOrEmpty(id)) + { + PrimaryVersionId = null; + } + else + { + PrimaryVersionId = id; + } + + PresentationUniqueKey = CreatePresentationUniqueKey(); + } + + public override string CreatePresentationUniqueKey() + { + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + return PrimaryVersionId; + } + + return base.CreatePresentationUniqueKey(); + } + + public override bool CanDownload() + { + if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay) + { + return false; + } + + return IsFileProtocol; + } + + protected override bool IsActiveRecording() + { + return LiveTvManager.GetActiveRecordingInfo(Path) != null; + } + + public override bool CanDelete() + { + if (IsActiveRecording()) + { + return false; + } + + return base.CanDelete(); + } + + public IEnumerable<Guid> GetAdditionalPartIds() + { + return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + + public IEnumerable<Guid> GetLocalAlternateVersionIds() + { + return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video))); + } + private string GetUserDataKey(string providerId) { var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant(); @@ -338,47 +383,6 @@ namespace MediaBrowser.Controller.Entities .OrderBy(i => i.SortName); } - [IgnoreDataMember] - public override string ContainingFolderPath - { - get - { - if (IsStacked) - { - return System.IO.Path.GetDirectoryName(Path); - } - - if (!IsPlaceHolder) - { - if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) - { - return Path; - } - } - - return base.ContainingFolderPath; - } - } - - [IgnoreDataMember] - public override string FileNameWithoutExtension - { - get - { - if (IsFileProtocol) - { - if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd) - { - return System.IO.Path.GetFileName(Path); - } - - return System.IO.Path.GetFileNameWithoutExtension(Path); - } - - return null; - } - } - internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem) { var updateType = base.UpdateFromResolvedItem(newItem); @@ -390,11 +394,13 @@ namespace MediaBrowser.Controller.Entities AdditionalParts = newVideo.AdditionalParts; updateType |= ItemUpdateType.MetadataImport; } + if (!LocalAlternateVersions.SequenceEqual(newVideo.LocalAlternateVersions, StringComparer.Ordinal)) { LocalAlternateVersions = newVideo.LocalAlternateVersions; updateType |= ItemUpdateType.MetadataImport; } + if (VideoType != newVideo.VideoType) { VideoType = newVideo.VideoType; @@ -405,43 +411,6 @@ namespace MediaBrowser.Controller.Entities return updateType; } - public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType) - { - if (videoType == VideoType.Dvd) - { - return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - if (videoType == VideoType.BluRay) - { - return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - return Array.Empty<string>(); - } - - /// <summary> - /// Gets a value indicating whether [is3 D]. - /// </summary> - /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] - public bool Is3D => Video3DFormat.HasValue; - - /// <summary> - /// Gets the type of the media. - /// </summary> - /// <value>The type of the media.</value> - [IgnoreDataMember] - public override string MediaType => Model.Entities.MediaType.Video; - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); @@ -485,9 +454,10 @@ namespace MediaBrowser.Controller.Entities } } - public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + /// <inheritdoc /> + public override async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) { - base.UpdateToRepository(updateReason, cancellationToken); + await base.UpdateToRepositoryAsync(updateReason, cancellationToken).ConfigureAwait(false); var localAlternates = GetLocalAlternateVersionIds() .Select(i => LibraryManager.GetItemById(i)) @@ -504,7 +474,7 @@ namespace MediaBrowser.Controller.Entities item.Genres = Genres; item.ProviderIds = ProviderIds; - item.UpdateToRepository(ItemUpdateType.MetadataDownload, cancellationToken); + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false); } } @@ -512,7 +482,8 @@ namespace MediaBrowser.Controller.Entities { if (!IsInMixedFolder) { - return new[] { + return new[] + { new FileSystemMetadata { FullName = ContainingFolderPath, @@ -535,7 +506,6 @@ namespace MediaBrowser.Controller.Entities { ItemId = Id, Index = DefaultVideoStreamIndex.Value - }).FirstOrDefault(); } diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 13e82fada..afdaf448b 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -1,16 +1,39 @@ +#nullable disable + +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Class Year + /// Class Year. /// </summary> public class Year : BaseItem, IItemByName { + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + public override bool CanDelete() + { + return false; + } + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -19,14 +42,6 @@ namespace MediaBrowser.Controller.Entities return list; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself - /// </summary> - /// <value>The containing folder path.</value> - [IgnoreDataMember] - public override string ContainingFolderPath => Path; - public override double GetDefaultPrimaryImageAspectRatio() { double value = 2; @@ -35,14 +50,6 @@ namespace MediaBrowser.Controller.Entities return value; } - [IgnoreDataMember] - public override bool SupportsAncestors => false; - - public override bool CanDelete() - { - return false; - } - public override bool IsSaveLocalMetadataEnabled() { return true; @@ -50,9 +57,7 @@ namespace MediaBrowser.Controller.Entities public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) { - var usCulture = new CultureInfo("en-US"); - - if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out var year)) + if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { return new List<BaseItem>(); } @@ -72,9 +77,6 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -103,15 +105,18 @@ namespace MediaBrowser.Controller.Entities Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); return true; } + return base.RequiresRefresh(); } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) |
