diff options
Diffstat (limited to 'Emby.Server.Implementations/Library')
14 files changed, 235 insertions, 178 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 665d70a41..b01fd93a7 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent) + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) { // Don't ignore application folders if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture)) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index cf6fc1845..a2301c8ae 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library } }; - private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); + private static readonly Glob[] _globs = Array.ConvertAll(_patterns, p => Glob.Parse(p, _globOptions)); /// <summary> /// Returns true if the supplied path should be ignored. diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a2abafd2a..cbded1ec6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,6 +1,5 @@ -#nullable disable - #pragma warning disable CS1591 +#pragma warning disable CA5394 using System; using System.Collections.Concurrent; @@ -18,6 +17,7 @@ using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks.Tasks; +using Emby.Server.Implementations.Sorting; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -89,8 +89,8 @@ namespace Emby.Server.Implementations.Library /// <summary> /// The _root folder. /// </summary> - private volatile AggregateFolder _rootFolder; - private volatile UserRootFolder _userRootFolder; + private volatile AggregateFolder? _rootFolder; + private volatile UserRootFolder? _userRootFolder; private bool _wizardCompleted; @@ -155,17 +155,17 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Occurs when [item added]. /// </summary> - public event EventHandler<ItemChangeEventArgs> ItemAdded; + public event EventHandler<ItemChangeEventArgs>? ItemAdded; /// <summary> /// Occurs when [item updated]. /// </summary> - public event EventHandler<ItemChangeEventArgs> ItemUpdated; + public event EventHandler<ItemChangeEventArgs>? ItemUpdated; /// <summary> /// Occurs when [item removed]. /// </summary> - public event EventHandler<ItemChangeEventArgs> ItemRemoved; + public event EventHandler<ItemChangeEventArgs>? ItemRemoved; /// <summary> /// Gets the root folder. @@ -264,7 +264,7 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> - private void ConfigurationUpdated(object sender, EventArgs e) + private void ConfigurationUpdated(object? sender, EventArgs e) { var config = _configurationManager.Configuration; @@ -338,7 +338,7 @@ namespace Emby.Server.Implementations.Library if (item is LiveTvProgram) { _logger.LogDebug( - "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -347,7 +347,7 @@ namespace Emby.Server.Implementations.Library else { _logger.LogInformation( - "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -366,7 +366,7 @@ namespace Emby.Server.Implementations.Library } _logger.LogDebug( - "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", metadataPath, @@ -395,7 +395,7 @@ namespace Emby.Server.Implementations.Library try { _logger.LogInformation( - "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", item.GetType().Name, item.Name ?? "Unknown name", fileSystemInfo.FullName, @@ -410,6 +410,24 @@ namespace Emby.Server.Implementations.Library File.Delete(fileSystemInfo.FullName); } } + catch (DirectoryNotFoundException) + { + _logger.LogInformation( + "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (FileNotFoundException) + { + _logger.LogInformation( + "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } catch (IOException) { if (isRequiredForDelete) @@ -443,7 +461,7 @@ namespace Emby.Server.Implementations.Library ReportItemRemoved(item, parent); } - private static IEnumerable<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children) + private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children) { var list = new List<string> { @@ -461,7 +479,7 @@ namespace Emby.Server.Implementations.Library /// <param name="args">The args.</param> /// <param name="resolvers">The resolvers.</param> /// <returns>BaseItem.</returns> - private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers) + private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers) { var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r)) .FirstOrDefault(i => i is not null); @@ -474,7 +492,7 @@ namespace Emby.Server.Implementations.Library return item; } - private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver) + private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver) { try { @@ -516,16 +534,16 @@ namespace Emby.Server.Implementations.Library return key.GetMD5(); } - public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null) + public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null) => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent); - private BaseItem ResolvePath( + private BaseItem? ResolvePath( FileSystemMetadata fileInfo, IDirectoryService directoryService, - IItemResolver[] resolvers, - Folder parent = null, + IItemResolver[]? resolvers, + Folder? parent = null, CollectionType? collectionType = null, - LibraryOptions libraryOptions = null) + LibraryOptions? libraryOptions = null) { ArgumentNullException.ThrowIfNull(fileInfo); @@ -598,7 +616,7 @@ namespace Emby.Server.Implementations.Library return ResolveItem(args, resolvers); } - public bool IgnoreFile(FileSystemMetadata file, BaseItem parent) + public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent) => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)); public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths) @@ -673,16 +691,16 @@ namespace Emby.Server.Implementations.Library private IEnumerable<BaseItem> ResolveFileList( IReadOnlyList<FileSystemMetadata> fileList, IDirectoryService directoryService, - Folder parent, + Folder? parent, CollectionType? collectionType, - IItemResolver[] resolvers, + IItemResolver[]? resolvers, LibraryOptions libraryOptions) { // Given that fileList is a list we can save enumerator allocations by indexing for (var i = 0; i < fileList.Count; i++) { var file = fileList[i]; - BaseItem result = null; + BaseItem? result = null; try { result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions); @@ -711,7 +729,7 @@ namespace Emby.Server.Implementations.Library Directory.CreateDirectory(rootFolderPath); var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? - ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))) + (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong")) .DeepCopy<Folder, AggregateFolder>(); // In case program data folder was moved @@ -777,7 +795,7 @@ namespace Emby.Server.Implementations.Library Directory.CreateDirectory(userRootPath); var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); - UserRootFolder tmpItem = null; + UserRootFolder? tmpItem = null; try { tmpItem = GetItemById(newItemId) as UserRootFolder; @@ -790,7 +808,8 @@ namespace Emby.Server.Implementations.Library if (tmpItem is null) { _logger.LogDebug("Creating new userRootFolder with DeepCopy"); - tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>(); + tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new InvalidOperationException("Failed to get user root path")) + .DeepCopy<Folder, UserRootFolder>(); } // In case program data folder was moved @@ -809,7 +828,8 @@ namespace Emby.Server.Implementations.Library return _userRootFolder; } - public BaseItem FindByPath(string path, bool? isFolder) + /// <inheritdoc /> + public BaseItem? FindByPath(string path, bool? isFolder) { // If this returns multiple items it could be tricky figuring out which one is correct. // In most cases, the newest one will be and the others obsolete but not yet cleaned up @@ -828,12 +848,8 @@ namespace Emby.Server.Implementations.Library .FirstOrDefault(); } - /// <summary> - /// Gets the person. - /// </summary> - /// <param name="name">The name.</param> - /// <returns>Task{Person}.</returns> - public Person GetPerson(string name) + /// <inheritdoc /> + public Person? GetPerson(string name) { var path = Person.GetPath(name); var id = GetItemByNameId<Person>(path); @@ -1015,7 +1031,7 @@ namespace Emby.Server.Implementations.Library } } - private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken) + public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -1024,7 +1040,8 @@ namespace Emby.Server.Implementations.Library new Progress<double>(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, - cancellationToken).ConfigureAwait(false); + allowRemoveRoot: removeRoot, + cancellationToken: cancellationToken).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -1032,7 +1049,8 @@ namespace Emby.Server.Implementations.Library new Progress<double>(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, - cancellationToken).ConfigureAwait(false); + allowRemoveRoot: removeRoot, + cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType<Folder>()) @@ -1050,7 +1068,7 @@ namespace Emby.Server.Implementations.Library var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96)); // Validate the entire media library - await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false); + await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken: cancellationToken).ConfigureAwait(false); progress.Report(96); @@ -1140,7 +1158,7 @@ namespace Emby.Server.Implementations.Library .ToList(); } - private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid> refreshQueue) + private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? refreshQueue) { var info = new VirtualFolderInfo { @@ -1204,20 +1222,15 @@ namespace Emby.Server.Implementations.Library return null; } - /// <summary> - /// Gets the item by id. - /// </summary> - /// <param name="id">The id.</param> - /// <returns>BaseItem.</returns> - /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> - public BaseItem GetItemById(Guid id) + /// <inheritdoc /> + public BaseItem? GetItemById(Guid id) { if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_cache.TryGetValue(id, out BaseItem item)) + if (_cache.TryGetValue(id, out BaseItem? item)) { return item; } @@ -1233,7 +1246,7 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public T GetItemById<T>(Guid id) + public T? GetItemById<T>(Guid id) where T : BaseItem { var item = GetItemById(id); @@ -1245,6 +1258,22 @@ namespace Emby.Server.Implementations.Library return null; } + /// <inheritdoc /> + public T? GetItemById<T>(Guid id, Guid userId) + where T : BaseItem + { + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); + return GetItemById<T>(id, user); + } + + /// <inheritdoc /> + public T? GetItemById<T>(Guid id, User? user) + where T : BaseItem + { + var item = GetItemById<T>(id); + return ItemIsVisible(item, user) ? item : null; + } + public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { if (query.Recursive && !query.ParentId.IsEmpty()) @@ -1405,7 +1434,7 @@ namespace Emby.Server.Implementations.Library var parents = new BaseItem[len]; for (int i = 0; i < len; i++) { - parents[i] = GetItemById(ancestorIds[i]); + parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id: {ancestorIds[i]}"); if (parents[i] is not (ICollectionFolder or UserView)) { return; @@ -1419,7 +1448,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } @@ -1516,7 +1545,7 @@ namespace Emby.Server.Implementations.Library } } - private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User user) + private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user) { if (item is UserView view) { @@ -1585,16 +1614,20 @@ namespace Emby.Server.Implementations.Library /// <returns>IEnumerable{System.String}.</returns> public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user) { + if (IntroProviders.Length == 0) + { + return []; + } + var tasks = IntroProviders - .Take(1) .Select(i => GetIntros(i, item, user)); var items = await Task.WhenAll(tasks).ConfigureAwait(false); return items - .SelectMany(i => i.ToArray()) + .SelectMany(i => i) .Select(ResolveIntro) - .Where(i => i is not null); + .Where(i => i is not null)!; // null values got filtered out } /// <summary> @@ -1623,9 +1656,9 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="info">The info.</param> /// <returns>Video.</returns> - private Video ResolveIntro(IntroInfo info) + private Video? ResolveIntro(IntroInfo info) { - Video video = null; + Video? video = null; if (info.ItemId.HasValue) { @@ -1676,42 +1709,42 @@ namespace Emby.Server.Implementations.Library return video; } - /// <summary> - /// Sorts the specified sort by. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="user">The user.</param> - /// <param name="sortBy">The sort by.</param> - /// <param name="sortOrder">The sort order.</param> - /// <returns>IEnumerable{BaseItem}.</returns> - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder) + /// <inheritdoc /> + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder) { - var isFirst = true; - - IOrderedEnumerable<BaseItem> orderedItems = null; + IOrderedEnumerable<BaseItem>? orderedItems = null; foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null)) { - if (isFirst) + if (orderBy is RandomComparer) { - orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy); + var randomItems = items.ToArray(); + Random.Shared.Shuffle(randomItems); + items = randomItems; + // Items are no longer ordered at this point, so set orderedItems back to null + orderedItems = null; + } + else if (orderedItems is null) + { + orderedItems = sortOrder == SortOrder.Descending + ? items.OrderByDescending(i => i, orderBy) + : items.OrderBy(i => i, orderBy); } else { - orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy); + orderedItems = sortOrder == SortOrder.Descending + ? orderedItems!.ThenByDescending(i => i, orderBy) + : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration } - - isFirst = false; } return orderedItems ?? items; } - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy) + /// <inheritdoc /> + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy) { - var isFirst = true; - - IOrderedEnumerable<BaseItem> orderedItems = null; + IOrderedEnumerable<BaseItem>? orderedItems = null; foreach (var (name, sortOrder) in orderBy) { @@ -1721,16 +1754,26 @@ namespace Emby.Server.Implementations.Library continue; } - if (isFirst) + if (comparer is RandomComparer) { - orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer); + var randomItems = items.ToArray(); + Random.Shared.Shuffle(randomItems); + items = randomItems; + // Items are no longer ordered at this point, so set orderedItems back to null + orderedItems = null; + } + else if (orderedItems is null) + { + orderedItems = sortOrder == SortOrder.Descending + ? items.OrderByDescending(i => i, comparer) + : items.OrderBy(i => i, comparer); } else { - orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, comparer) : orderedItems.ThenBy(i => i, comparer); + orderedItems = sortOrder == SortOrder.Descending + ? orderedItems!.ThenByDescending(i => i, comparer) + : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration } - - isFirst = false; } return orderedItems ?? items; @@ -1742,14 +1785,14 @@ namespace Emby.Server.Implementations.Library /// <param name="name">The name.</param> /// <param name="user">The user.</param> /// <returns>IBaseItemComparer.</returns> - private IBaseItemComparer GetComparer(ItemSortBy name, User user) + private IBaseItemComparer? GetComparer(ItemSortBy name, User? user) { var comparer = Comparers.FirstOrDefault(c => name == c.Type); // If it requires a user, create a new one, and assign the user if (comparer is IUserBaseItemComparer) { - var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType()); + var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances userComparer.User = user; userComparer.UserManager = _userManager; @@ -1761,23 +1804,14 @@ namespace Emby.Server.Implementations.Library return comparer; } - /// <summary> - /// Creates the item. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="parent">The parent item.</param> - public void CreateItem(BaseItem item, BaseItem parent) + /// <inheritdoc /> + public void CreateItem(BaseItem item, BaseItem? parent) { CreateItems(new[] { item }, parent, CancellationToken.None); } - /// <summary> - /// Creates the items. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="parent">The parent item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken) + /// <inheritdoc /> + public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken) { _itemRepository.SaveItems(items, cancellationToken); @@ -1860,7 +1894,7 @@ namespace Emby.Server.Implementations.Library try { var index = item.GetImageIndex(img); - image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false); + image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false); } catch (ArgumentException) { @@ -2059,16 +2093,16 @@ namespace Emby.Server.Implementations.Library public LibraryOptions GetLibraryOptions(BaseItem item) { - if (item is not CollectionFolder collectionFolder) + if (item is CollectionFolder collectionFolder) { - // List.Find is more performant than FirstOrDefault due to enumerator allocation - collectionFolder = GetCollectionFolders(item) - .Find(folder => folder is CollectionFolder) as CollectionFolder; + return collectionFolder.GetLibraryOptions(); } - return collectionFolder is null - ? new LibraryOptions() - : collectionFolder.GetLibraryOptions(); + // List.Find is more performant than FirstOrDefault due to enumerator allocation + return GetCollectionFolders(item) + .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2 + ? collectionFolder2.GetLibraryOptions() + : new LibraryOptions(); } public CollectionType? GetContentType(BaseItem item) @@ -2422,7 +2456,7 @@ namespace Emby.Server.Implementations.Library { if (parentId.HasValue) { - return GetItemById(parentId.Value); + return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"); } if (!userId.IsNullOrEmpty()) @@ -2459,7 +2493,7 @@ namespace Emby.Server.Implementations.Library var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; // TODO nullable - what are we trying to do there with empty episodeInfo? - EpisodeInfo episodeInfo = null; + EpisodeInfo? episodeInfo = null; if (episode.IsFileProtocol) { episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming); @@ -2662,7 +2696,7 @@ namespace Emby.Server.Implementations.Library } } - BaseItem GetExtra(FileSystemMetadata file, ExtraType extraType) + BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType) { var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType)); if (extra is not Video && extra is not Audio) @@ -2677,16 +2711,21 @@ namespace Emby.Server.Implementations.Library extra = itemById; } - extra.ExtraType = extraType; + // Only update extra type if it is more specific then the currently known extra type + if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown) + { + extra.ExtraType = extraType; + } + extra.ParentId = Guid.Empty; extra.OwnerId = owner.Id; return extra; } } - public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) + public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem) { - string newPath; + string? newPath; if (ownerItem is not null) { var libraryOptions = GetLibraryOptions(ownerItem); @@ -2760,8 +2799,8 @@ namespace Emby.Server.Implementations.Library } }) .Where(i => i is not null) - .Where(i => query.User is null || i.IsVisible(query.User)) - .ToList(); + .Where(i => query.User is null || i!.IsVisible(query.User)) + .ToList()!; // null values are filtered out } public List<string> GetPeopleNames(InternalPeopleQuery query) @@ -2783,8 +2822,10 @@ namespace Emby.Server.Implementations.Library } _itemRepository.UpdatePeople(item.Id, people); - - await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); + if (people is not null) + { + await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); + } } public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure) @@ -2863,7 +2904,7 @@ namespace Emby.Server.Implementations.Library if (collectionType is not null) { - var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); + var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values? await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false); } @@ -2897,7 +2938,7 @@ namespace Emby.Server.Implementations.Library private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) { - List<BaseItem> personsToSave = null; + List<BaseItem>? personsToSave = null; foreach (var person in people) { @@ -3010,9 +3051,7 @@ namespace Emby.Server.Implementations.Library { var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); - var list = libraryOptions.PathInfos.ToList(); - list.Add(pathInfo); - libraryOptions.PathInfos = list.ToArray(); + libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo]; SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); @@ -3031,8 +3070,7 @@ namespace Emby.Server.Implementations.Library SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); - var list = libraryOptions.PathInfos.ToList(); - foreach (var originalPathInfo in list) + foreach (var originalPathInfo in libraryOptions.PathInfos) { if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal)) { @@ -3041,8 +3079,6 @@ namespace Emby.Server.Implementations.Library } } - libraryOptions.PathInfos = list.ToArray(); - CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); } @@ -3095,7 +3131,7 @@ namespace Emby.Server.Implementations.Library if (refreshLibrary) { - await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false); + await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false); StartScanInBackground(); } @@ -3115,7 +3151,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(path)); } - List<NameValuePair> removeList = null; + List<NameValuePair>? removeList = null; foreach (var contentType in _configurationManager.Configuration.ContentTypes) { @@ -3168,5 +3204,20 @@ namespace Emby.Server.Implementations.Library CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); } + + private static bool ItemIsVisible(BaseItem? item, User? user) + { + if (item is null) + { + return false; + } + + if (user is null) + { + return true; + } + + return item is UserRootFolder || item.IsVisibleStandalone(user); + } } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 18ada6aeb..bb22ca82f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -113,6 +113,11 @@ namespace Emby.Server.Implementations.Library return true; } + if (stream.IsPgsSubtitleStream) + { + return true; + } + return false; } @@ -191,7 +196,7 @@ namespace Emby.Server.Implementations.Library if (user is not null) { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + SetDefaultAudioAndSubtitleStreamIndices(item, source, user); if (item.MediaType == MediaType.Audio) { @@ -274,7 +279,7 @@ namespace Emby.Server.Implementations.Library var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); - return results.SelectMany(i => i.ToList()); + return results.SelectMany(i => i); } private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken) @@ -296,7 +301,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Error getting media sources"); - return Enumerable.Empty<MediaSourceInfo>(); + return []; } } @@ -339,7 +344,7 @@ namespace Emby.Server.Implementations.Library { foreach (var source in sources) { - SetDefaultAudioAndSubtitleStreamIndexes(item, source, user); + SetDefaultAudioAndSubtitleStreamIndices(item, source, user); if (item.MediaType == MediaType.Audio) { @@ -360,7 +365,7 @@ namespace Emby.Server.Implementations.Library { if (string.IsNullOrEmpty(language)) { - return Array.Empty<string>(); + return []; } var culture = _localizationManager.FindLanguageInfo(language); @@ -369,14 +374,15 @@ namespace Emby.Server.Implementations.Library return culture.ThreeLetterISOLanguageNames; } - return new string[] { language }; + return [language]; } private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { if (userData.SubtitleStreamIndex.HasValue && user.RememberSubtitleSelections - && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) + && user.SubtitleMode != SubtitlePlaybackMode.None + && allowRememberingSelection) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid @@ -390,7 +396,7 @@ namespace Emby.Server.Implementations.Library var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; - var audioLangage = defaultAudioIndex is null + var audioLanguage = defaultAudioIndex is null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); @@ -398,9 +404,9 @@ namespace Emby.Server.Implementations.Library source.MediaStreams, preferredSubs, user.SubtitleMode, - audioLangage); + audioLanguage); - MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage); + MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage); } private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) @@ -421,7 +427,7 @@ namespace Emby.Server.Implementations.Library source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } - public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user) + public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) { // Item would only be null if the app didn't supply ItemId as part of the live stream open request var mediaType = item?.MediaType ?? MediaType.Video; @@ -526,7 +532,7 @@ namespace Emby.Server.Implementations.Library var item = request.ItemId.IsEmpty() ? null : _libraryManager.GetItemById(request.ItemId); - SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); + SetDefaultAudioAndSubtitleStreamIndices(item, clone, user); } return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 6aef87c52..ea223e3ec 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -124,16 +124,16 @@ namespace Emby.Server.Implementations.Library } else if (mode == SubtitlePlaybackMode.Always) { - // always load the most suitable full subtitles + // Always load the most suitable full subtitles filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // always load the most suitable full subtitles + // Always load the most suitable full subtitles filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); } - // load forced subs if we have found no suitable full subtitles + // Load forced subs if we have found no suitable full subtitles var iterStreams = filteredStreams is null || filteredStreams.Count == 0 ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) : filteredStreams; diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index c4b6b3756..21e7079d8 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Library var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase); - // Must be at least 3 characters after the attribute =, ], any character. - var maxIndex = str.Length - attribute.Length - 3; + // Must be at least 3 characters after the attribute =, ], any character, + // then we offset it by 1, because we want the index and not length. + var maxIndex = str.Length - attribute.Length - 2; while (attributeIndex > -1 && attributeIndex < maxIndex) { var attributeEnd = attributeIndex + attribute.Length; diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 7a61e2607..c9e3a4daf 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -35,11 +35,11 @@ namespace Emby.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); - item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 || + item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) || item.GetParents().Any(i => i.IsLocked); // Make sure DateCreated and DateModified have values - var fileInfo = directoryService.GetFile(item.Path); + var fileInfo = directoryService.GetFileSystemEntry(item.Path); if (fileInfo is null) { return false; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 0bfb7fbe6..9405f2102 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 1bdae7f62..f7270bec1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Emby.Naming.Audio; using Emby.Naming.Common; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.Audio; @@ -85,6 +86,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService); + var albumParser = new AlbumParser(_namingOptions); var directories = args.FileSystemChildren.Where(i => i.IsDirectory); @@ -100,6 +102,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio } } + // If the folder is a multi-disc folder, then it is not an artist folder + if (albumParser.IsMultiPart(fileSystemInfo.FullName)) + { + return; + } + // If we contain a music album assume we are an artist folder if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index 6cc04ea81..955055313 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml")) + if (filename.Contains("[boxset]", StringComparison.OrdinalIgnoreCase) || args.ContainsFileSystemEntryByName("collection.xml")) { return new BoxSet { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index a50435ae6..a03c1214d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.IO; using System.Linq; @@ -11,7 +9,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Resolvers; using MediaBrowser.LocalMetadata.Savers; -using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers { @@ -20,11 +17,11 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> public class PlaylistResolver : GenericFolderResolver<Playlist> { - private CollectionType?[] _musicPlaylistCollectionTypes = - { + private readonly CollectionType?[] _musicPlaylistCollectionTypes = + [ null, CollectionType.music - }; + ]; /// <inheritdoc/> protected override Playlist Resolve(ItemResolveArgs args) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 858c5b281..abf2d0115 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { IndexNumber = seasonParserResult.SeasonNumber, SeriesId = series.Id, - SeriesName = series.Name + SeriesName = series.Name, + Path = seasonParserResult.IsSeasonFolder ? path : null }; if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder) @@ -78,27 +79,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV } } - if (season.IndexNumber.HasValue) + if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name)) { var seasonNumber = season.IndexNumber.Value; - if (string.IsNullOrEmpty(season.Name)) - { - var seasonNames = series.SeasonNames; - if (seasonNames.TryGetValue(seasonNumber, out var seasonName)) - { - season.Name = seasonName; - } - else - { - season.Name = seasonNumber == 0 ? - args.LibraryOptions.SeasonZeroDisplayName : - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameSeasonNumber"), - seasonNumber, - args.LibraryOptions.PreferredMetadataLanguage); - } - } + season.Name = seasonNumber == 0 ? + args.LibraryOptions.SeasonZeroDisplayName : + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("NameSeasonNumber"), + seasonNumber, + args.LibraryOptions.PreferredMetadataLanguage); } return season; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 83a66c8e4..d9a559014 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -303,8 +303,8 @@ namespace Emby.Server.Implementations.Library { // Handle situations with the grouping setting, e.g. movies showing up in tv, etc. // Thanks to mixed content libraries included in the UserView - var hasCollectionType = parents.OfType<UserView>().ToArray(); - if (hasCollectionType.Length > 0) + var hasCollectionType = parents.OfType<UserView>().ToList(); + if (hasCollectionType.Count > 0) { if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies)) { diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 601aab5b9..725b8f76c 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -64,6 +64,11 @@ namespace Emby.Server.Implementations.Library.Validators try { var item = _libraryManager.GetPerson(person); + if (item is null) + { + _logger.LogWarning("Failed to get person: {Name}", person); + continue; + } var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -92,7 +97,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { BaseItemKind.Person }, + IncludeItemTypes = [BaseItemKind.Person], IsDeadPerson = true, IsLocked = false }); |
