diff options
Diffstat (limited to 'Emby.Server.Implementations/Library')
35 files changed, 739 insertions, 551 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 3380e29d4..29758a078 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,8 +1,9 @@ using System; using System.IO; +using Emby.Naming.Audio; +using Emby.Naming.Common; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.IO; @@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library /// </summary> public class CoreResolutionIgnoreRule : IResolverIgnoreRule { - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; private readonly IServerApplicationPaths _serverApplicationPaths; /// <summary> /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="serverApplicationPaths">The server application paths.</param> - public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths) + public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths) { - _libraryManager = libraryManager; + _namingOptions = namingOptions; _serverApplicationPaths = serverApplicationPaths; } @@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library if (parent != null) { // Ignore trailer folders but allow it at the collection level - if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) + if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase) && !(parent is AggregateFolder) && !(parent is UserRootFolder)) { @@ -77,8 +78,8 @@ namespace Emby.Server.Implementations.Library if (parent != null) { // Don't resolve these into audio files - if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal) - && _libraryManager.IsAudioFile(filename)) + if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) + && AudioFileParser.IsAudioFile(filename, _namingOptions)) { return true; } diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs index 236453e80..868071a99 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -1,7 +1,10 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Globalization; +using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; @@ -39,6 +42,11 @@ namespace Emby.Server.Implementations.Library return _closeFn(); } + public Stream GetStream() + { + throw new NotSupportedException(); + } + public Task Open(CancellationToken openCancellationToken) { return Task.CompletedTask; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index e30a67593..5384c04b3 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Linq; using DotNet.Globbing; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 6a9f4174d..778b6225e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -19,6 +21,7 @@ using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; @@ -48,6 +51,7 @@ using MediaBrowser.Providers.MediaInfo; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using Genre = MediaBrowser.Controller.Entities.Genre; using Person = MediaBrowser.Controller.Entities.Person; using VideoResolver = Emby.Naming.Video.VideoResolver; @@ -75,6 +79,7 @@ namespace Emby.Server.Implementations.Library private readonly IFileSystem _fileSystem; private readonly IItemRepository _itemRepository; private readonly IImageProcessor _imageProcessor; + private readonly NamingOptions _namingOptions; /// <summary> /// The _root folder sync lock. @@ -84,9 +89,6 @@ namespace Emby.Server.Implementations.Library private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); - private NamingOptions _namingOptions; - private string[] _videoFileExtensions; - /// <summary> /// The _root folder. /// </summary> @@ -112,6 +114,7 @@ namespace Emby.Server.Implementations.Library /// <param name="itemRepository">The item repository.</param> /// <param name="imageProcessor">The image processor.</param> /// <param name="memoryCache">The memory cache.</param> + /// <param name="namingOptions">The naming options.</param> public LibraryManager( IServerApplicationHost appHost, ILogger<LibraryManager> logger, @@ -126,7 +129,8 @@ namespace Emby.Server.Implementations.Library IMediaEncoder mediaEncoder, IItemRepository itemRepository, IImageProcessor imageProcessor, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + NamingOptions namingOptions) { _appHost = appHost; _logger = logger; @@ -142,6 +146,7 @@ namespace Emby.Server.Implementations.Library _itemRepository = itemRepository; _imageProcessor = imageProcessor; _memoryCache = memoryCache; + _namingOptions = namingOptions; _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -175,10 +180,7 @@ namespace Emby.Server.Implementations.Library { lock (_rootFolderSyncLock) { - if (_rootFolder == null) - { - _rootFolder = CreateRootFolder(); - } + _rootFolder ??= CreateRootFolder(); } } @@ -286,14 +288,14 @@ namespace Emby.Server.Implementations.Library if (item is IItemByName) { - if (!(item is MusicArtist)) + if (item is not MusicArtist) { return; } } else if (!item.IsFolder) { - if (!(item is Video) && !(item is LiveTvChannel)) + if (item is not Video && item is not LiveTvChannel) { return; } @@ -332,8 +334,7 @@ namespace Emby.Server.Implementations.Library { try { - var task = BaseItem.ChannelManager.DeleteItem(item); - Task.WaitAll(task); + BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult(); } catch (ArgumentException) { @@ -491,7 +492,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path); + _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path); return null; } } @@ -558,7 +559,6 @@ namespace Emby.Server.Implementations.Library var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) { Parent = parent, - Path = fullPath, FileInfo = fileInfo, CollectionType = collectionType, LibraryOptions = libraryOptions @@ -647,7 +647,7 @@ namespace Emby.Server.Implementations.Library /// Determines whether a path should be ignored based on its contents - called after the contents have been read. /// </summary> /// <param name="args">The args.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> private static bool ShouldResolvePathContents(ItemResolveArgs args) { // Ignore any folders containing a file called .ignore @@ -684,7 +684,7 @@ namespace Emby.Server.Implementations.Library foreach (var item in items) { - ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService); + ResolverHelper.SetInitialItemValues(item, parent, this, directoryService); } items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions)); @@ -697,25 +697,32 @@ namespace Emby.Server.Implementations.Library } private IEnumerable<BaseItem> ResolveFileList( - IEnumerable<FileSystemMetadata> fileList, + IReadOnlyList<FileSystemMetadata> fileList, IDirectoryService directoryService, Folder parent, string collectionType, IItemResolver[] resolvers, LibraryOptions libraryOptions) { - return fileList.Select(f => + // 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; try { - return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions); + result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions); } catch (Exception ex) { - _logger.LogError(ex, "Error resolving path {path}", f.FullName); - return null; + _logger.LogError(ex, "Error resolving path {Path}", file.FullName); } - }).Where(i => i != null); + + if (result != null) + { + yield return result; + } + } } /// <summary> @@ -792,7 +799,7 @@ namespace Emby.Server.Implementations.Library { var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; - _logger.LogDebug("Creating userRootPath at {path}", userRootPath); + _logger.LogDebug("Creating userRootPath at {Path}", userRootPath); Directory.CreateDirectory(userRootPath); var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); @@ -803,7 +810,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId); + _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId); } if (tmpItem == null) @@ -820,7 +827,7 @@ namespace Emby.Server.Implementations.Library } _userRootFolder = tmpItem; - _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder); + _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder); } } } @@ -859,7 +866,7 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(name); var id = GetItemByNameId<Person>(path); - if (!(GetItemById(id) is Person item)) + if (GetItemById(id) is not Person item) { item = new Person { @@ -1066,17 +1073,17 @@ namespace Emby.Server.Implementations.Library // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren( new SimpleProgress<double>(), - cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), - recursive: false).ConfigureAwait(false); + recursive: false, + cancellationToken).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); await GetUserRootFolder().ValidateChildren( new SimpleProgress<double>(), - cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), - recursive: false).ConfigureAwait(false); + recursive: false, + cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType<Folder>()) @@ -1096,7 +1103,7 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => progress.Report(pct * 0.96)); // Validate the entire media library - await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); + await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false); progress.Report(96); @@ -1163,7 +1170,7 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } - _itemRepository.UpdateInheritedValues(cancellationToken); + _itemRepository.UpdateInheritedValues(); progress.Report(100); } @@ -1206,7 +1213,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error resolving shortcut file {file}", i); + _logger.LogError(ex, "Error resolving shortcut file {File}", i); return null; } }) @@ -1243,10 +1250,8 @@ namespace Emby.Server.Implementations.Library private CollectionTypeOptions? GetCollectionType(string path) { var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false); - foreach (var file in files) + foreach (ReadOnlySpan<char> file in files) { - // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it - // https://github.com/dotnet/runtime/issues/20008 if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res)) { return res; @@ -1261,7 +1266,7 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="id">The id.</param> /// <returns>BaseItem.</returns> - /// <exception cref="ArgumentNullException">id</exception> + /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> public BaseItem GetItemById(Guid id) { if (id == Guid.Empty) @@ -1693,7 +1698,7 @@ namespace Emby.Server.Implementations.Library if (video == null) { - _logger.LogError("Intro resolver returned null for {path}.", info.Path); + _logger.LogError("Intro resolver returned null for {Path}.", info.Path); } else { @@ -1712,7 +1717,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error resolving path {path}.", info.Path); + _logger.LogError(ex, "Error resolving path {Path}.", info.Path); } } else @@ -1754,22 +1759,20 @@ namespace Emby.Server.Implementations.Library return orderedItems ?? items; } - public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList) + public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy) { var isFirst = true; IOrderedEnumerable<BaseItem> orderedItems = null; - foreach (var orderBy in orderByList) + foreach (var (name, sortOrder) in orderBy) { - var comparer = GetComparer(orderBy.Item1, user); + var comparer = GetComparer(name, user); if (comparer == null) { continue; } - var sortOrder = orderBy.Item2; - if (isFirst) { orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer); @@ -2077,7 +2080,7 @@ namespace Emby.Server.Implementations.Library return new List<Folder>(); } - return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList()); + return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>()); } public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren) @@ -2102,20 +2105,20 @@ namespace Emby.Server.Implementations.Library return GetCollectionFoldersInternal(item, allUserRootChildren); } - private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren) + private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren) { return allUserRootChildren - .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase)) + .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase)) .ToList(); } public LibraryOptions GetLibraryOptions(BaseItem item) { - if (!(item is CollectionFolder collectionFolder)) + if (item is not CollectionFolder collectionFolder) { + // List.Find is more performant than FirstOrDefault due to enumerator allocation collectionFolder = GetCollectionFolders(item) - .OfType<CollectionFolder>() - .FirstOrDefault(); + .Find(folder => folder is CollectionFolder) as CollectionFolder; } return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); @@ -2499,17 +2502,6 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public bool IsVideoFile(string path) - { - var resolver = new VideoResolver(GetNamingOptions()); - return resolver.IsVideoFile(path); - } - - /// <inheritdoc /> - public bool IsAudioFile(string path) - => AudioFileParser.IsAudioFile(path, GetNamingOptions()); - - /// <inheritdoc /> public int? GetSeasonNumberFromPath(string path) => SeasonPathParser.Parse(path, true, true).SeasonNumber; @@ -2517,21 +2509,36 @@ namespace Emby.Server.Implementations.Library public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { var series = episode.Series; - bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); + bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); if (!isAbsoluteNaming.Value) { // In other words, no filter applied isAbsoluteNaming = null; } - var resolver = new EpisodeResolver(GetNamingOptions()); + var resolver = new EpisodeResolver(_namingOptions); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; // TODO nullable - what are we trying to do there with empty episodeInfo? - var episodeInfo = episode.IsFileProtocol - ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path) - : new Naming.TV.EpisodeInfo(episode.Path); + EpisodeInfo episodeInfo = null; + if (episode.IsFileProtocol) + { + episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming); + // Resolve from parent folder if it's not the Season folder + var parent = episode.GetParent(); + if (episodeInfo == null && parent.GetType() == typeof(Folder)) + { + episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming); + if (episodeInfo != null) + { + // add the container + episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.'); + } + } + } + + episodeInfo ??= new EpisodeInfo(episode.Path); try { @@ -2666,42 +2673,28 @@ namespace Emby.Server.Implementations.Library return changed; } - public NamingOptions GetNamingOptions() - { - if (_namingOptions == null) - { - _namingOptions = new NamingOptions(); - _videoFileExtensions = _namingOptions.VideoFileExtensions; - } - - return _namingOptions; - } - public ItemLookupInfo ParseName(string name) { - var resolver = new VideoResolver(GetNamingOptions()); - - var result = resolver.CleanDateTime(name); + var namingOptions = _namingOptions; + var result = VideoResolver.CleanDateTime(name, namingOptions); return new ItemLookupInfo { - Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name, + Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name, Year = result.Year }; } public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var namingOptions = GetNamingOptions(); + var namingOptions = _namingOptions; var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) + .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false)) .ToList(); - var videoListResolver = new VideoListResolver(namingOptions); - - var videos = videoListResolver.Resolve(fileSystemChildren); + var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); @@ -2712,7 +2705,7 @@ namespace Emby.Server.Implementations.Library var resolvers = new IItemResolver[] { - new GenericVideoResolver<Trailer>(this) + new GenericVideoResolver<Trailer>(_namingOptions) }; return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers) @@ -2738,16 +2731,14 @@ namespace Emby.Server.Implementations.Library public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var namingOptions = GetNamingOptions(); + var namingOptions = _namingOptions; var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) + .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty)) + .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false)) .ToList(); - var videoListResolver = new VideoListResolver(namingOptions); - - var videos = videoListResolver.Resolve(fileSystemChildren); + var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); @@ -2828,7 +2819,7 @@ namespace Emby.Server.Implementations.Library private void SetExtraTypeFromFilename(Video item) { - var resolver = new ExtraResolver(GetNamingOptions()); + var resolver = new ExtraResolver(_namingOptions); var result = resolver.GetExtraInfo(item.Path); @@ -2881,12 +2872,20 @@ namespace Emby.Server.Implementations.Library public void UpdatePeople(BaseItem item, List<PersonInfo> people) { + UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// <inheritdoc /> + public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken) + { if (!item.SupportsPeople) { return; } _itemRepository.UpdatePeople(item.Id, people); + + await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); } public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) @@ -2990,6 +2989,58 @@ namespace Emby.Server.Implementations.Library } } + private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) + { + var personsToSave = new List<BaseItem>(); + + foreach (var person in people) + { + cancellationToken.ThrowIfCancellationRequested(); + + var itemUpdateType = ItemUpdateType.MetadataDownload; + var saveEntity = false; + var personEntity = GetPerson(person.Name); + + // if PresentationUniqueKey is empty it's likely a new item. + if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + { + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); + saveEntity = true; + } + + foreach (var id in person.ProviderIds) + { + if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)) + { + personEntity.SetProviderId(id.Key, id.Value); + saveEntity = true; + } + } + + if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary)) + { + personEntity.SetImage( + new ItemImageInfo + { + Path = person.ImageUrl, + Type = ImageType.Primary + }, + 0); + + saveEntity = true; + itemUpdateType = ItemUpdateType.ImageUpdate; + } + + if (saveEntity) + { + personsToSave.Add(personEntity); + await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + } + } + + CreateItems(personsToSave, null, CancellationToken.None); + } + private void StartScanInBackground() { Task.Run(() => @@ -2999,9 +3050,9 @@ namespace Emby.Server.Implementations.Library }); } - public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) + public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath) { - AddMediaPathInternal(virtualFolderName, pathInfo, true); + AddMediaPathInternal(virtualFolderName, mediaPath, true); } private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions) @@ -3054,11 +3105,11 @@ namespace Emby.Server.Implementations.Library } } - public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo) + public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath) { - if (pathInfo == null) + if (mediaPath == null) { - throw new ArgumentNullException(nameof(pathInfo)); + throw new ArgumentNullException(nameof(mediaPath)); } var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; @@ -3071,9 +3122,9 @@ namespace Emby.Server.Implementations.Library var list = libraryOptions.PathInfos.ToList(); foreach (var originalPathInfo in list) { - if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal)) + if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal)) { - originalPathInfo.NetworkPath = pathInfo.NetworkPath; + originalPathInfo.NetworkPath = mediaPath.NetworkPath; break; } } @@ -3096,10 +3147,7 @@ namespace Emby.Server.Implementations.Library { if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal))) { - list.Add(new MediaPathInfo - { - Path = location - }); + list.Add(new MediaPathInfo(location)); } } diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index c2951dd15..83acd8e9f 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -8,13 +10,14 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -47,7 +50,7 @@ namespace Emby.Server.Implementations.Library { try { - await using FileStream jsonStream = File.OpenRead(cacheFilePath); + await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); @@ -84,7 +87,7 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath != null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = File.OpenWrite(cacheFilePath); + await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Saved media info to {0}", cacheFilePath); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index d0b85f07d..972d4ebbb 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -11,9 +13,9 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.Library private readonly IMediaEncoder _mediaEncoder; private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; + private readonly IDirectoryService _directoryService; private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); @@ -59,7 +62,8 @@ namespace Emby.Server.Implementations.Library ILogger<MediaSourceManager> logger, IFileSystem fileSystem, IUserDataManager userDataManager, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + IDirectoryService directoryService) { _itemRepo = itemRepo; _userManager = userManager; @@ -70,6 +74,7 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _localizationManager = localizationManager; _appPaths = applicationPaths; + _directoryService = directoryService; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -104,16 +109,6 @@ namespace Emby.Server.Implementations.Library return false; } - public List<MediaStream> GetMediaStreams(string mediaSourceId) - { - var list = GetMediaStreams(new MediaStreamQuery - { - ItemId = new Guid(mediaSourceId) - }); - - return GetMediaStreamsForItem(list); - } - public List<MediaStream> GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery @@ -159,7 +154,7 @@ namespace Emby.Server.Implementations.Library if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { await item.RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + new MetadataRefreshOptions(_directoryService) { EnableRemoteContentProbe = true, MetadataRefreshMode = MetadataRefreshMode.FullRefresh @@ -210,6 +205,7 @@ namespace Emby.Server.Implementations.Library return SortMediaSources(list); } + /// <inheritdoc />> public MediaProtocol GetPathProtocol(string path) { if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) @@ -256,7 +252,7 @@ namespace Emby.Server.Implementations.Library { if (path != null) { - if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1) + if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -295,7 +291,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Error getting media sources"); - return new List<MediaSourceInfo>(); + return Enumerable.Empty<MediaSourceInfo>(); } } @@ -350,7 +346,7 @@ namespace Emby.Server.Implementations.Library private string[] NormalizeLanguage(string language) { - if (language == null) + if (string.IsNullOrEmpty(language)) { return Array.Empty<string>(); } @@ -379,8 +375,7 @@ namespace Emby.Server.Implementations.Library } } - var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) - ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); + var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null @@ -409,9 +404,7 @@ namespace Emby.Server.Implementations.Library } } - var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference) - ? Array.Empty<string>() - : NormalizeLanguage(user.AudioLanguagePreference); + var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } @@ -495,14 +488,11 @@ namespace Emby.Server.Implementations.Library _liveStreamSemaphore.Release(); } - // TODO: Don't hardcode this - const bool isAudio = false; - try { if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing) { - AddMediaInfo(mediaSource, isAudio); + AddMediaInfo(mediaSource); } else { @@ -510,19 +500,19 @@ namespace Emby.Server.Implementations.Library string cacheKey = request.OpenToken; await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) - .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) + .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken) .ConfigureAwait(false); } } catch (Exception ex) { _logger.LogError(ex, "Error probing live tv stream"); - AddMediaInfo(mediaSource, isAudio); + AddMediaInfo(mediaSource); } // TODO: @bond Fix var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions); - _logger.LogInformation("Live stream opened: " + json); + _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource); var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions); if (!request.UserId.Equals(Guid.Empty)) @@ -537,7 +527,7 @@ namespace Emby.Server.Implementations.Library return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); } - private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) + private static void AddMediaInfo(MediaSourceInfo mediaSource) { mediaSource.DefaultSubtitleStreamIndex = null; @@ -588,22 +578,6 @@ namespace Emby.Server.Implementations.Library mediaSource.InferTotalBitrate(); } - public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) - { - var info = _openStreams.Values.FirstOrDefault(i => - { - var liveStream = i as ILiveStream; - if (liveStream != null) - { - return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); - } - - return false; - }); - - return Task.FromResult(info as IDirectStreamProvider); - } - public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) { var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false); @@ -612,7 +586,8 @@ namespace Emby.Server.Implementations.Library public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken) { - var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); + // TODO probably shouldn't throw here but it is kept for "backwards compatibility" + var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); var mediaSource = liveStreamInfo.MediaSource; @@ -648,7 +623,7 @@ namespace Emby.Server.Implementations.Library { try { - await using FileStream jsonStream = File.OpenRead(cacheFilePath); + await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); @@ -781,18 +756,19 @@ namespace Emby.Server.Implementations.Library mediaSource.InferTotalBitrate(true); } - public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) + public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } - var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false); - return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider); + // TODO probably shouldn't throw here but it is kept for "backwards compatibility" + var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); + return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider)); } - private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) + public ILiveStream GetLiveStreamInfo(string id) { if (string.IsNullOrEmpty(id)) { @@ -801,12 +777,16 @@ namespace Emby.Server.Implementations.Library if (_openStreams.TryGetValue(id, out ILiveStream info)) { - return Task.FromResult(info); - } - else - { - return Task.FromException<ILiveStream>(new ResourceNotFoundException()); + return info; } + + return null; + } + + /// <inheritdoc /> + public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId) + { + return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase)); } public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken) @@ -866,9 +846,7 @@ namespace Emby.Server.Implementations.Library return (provider, keyId); } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { Dispose(true); diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 28fa06239..b3837fedb 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -36,14 +38,11 @@ namespace Emby.Server.Implementations.Library } public static int? GetDefaultSubtitleStreamIndex( - List<MediaStream> streams, + IEnumerable<MediaStream> streams, string[] preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) { - streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) - .ToList(); - MediaStream stream = null; if (mode == SubtitlePlaybackMode.None) @@ -51,52 +50,48 @@ namespace Emby.Server.Implementations.Library return null; } + var sortedStreams = streams + .Where(i => i.Type == MediaStreamType.Subtitle) + .OrderByDescending(x => x.IsExternal) + .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(x => x.IsForced) + .ThenByDescending(x => x.IsDefault) + .ToList(); + if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic - - stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ?? - streams.FirstOrDefault(s => s.IsForced) ?? - streams.FirstOrDefault(s => s.IsDefault); + stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault); // if the audio language is not understood by the user, load their preferred subs, if there are any if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) { - stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); + stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); } } else if (mode == SubtitlePlaybackMode.Smart) { - // Prefer smart logic over embedded metadata - // if the audio language is not understood by the user, load their preferred subs, if there are any if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase)) { - stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ?? + stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ?? streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)); } } else if (mode == SubtitlePlaybackMode.Always) { // always load the most suitable full subtitles - stream = streams.FirstOrDefault(s => !s.IsForced); + stream = sortedStreams.FirstOrDefault(s => !s.IsForced); } else if (mode == SubtitlePlaybackMode.OnlyForced) { // always load the most suitable full subtitles - stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ?? - streams.FirstOrDefault(s => s.IsForced); + stream = sortedStreams.FirstOrDefault(x => x.IsForced); } // load forced subs if we have found no suitable full subtitles - stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)); - - if (stream != null) - { - return stream.Index; - } - - return null; + stream ??= sortedStreams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)); + return stream?.Index; } private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences) diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 658c53f28..e2f1fb0ad 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -34,9 +36,10 @@ namespace Emby.Server.Implementations.Library return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList(); } - public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions) + /// <inheritdoc /> + public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions) { - return GetInstantMixFromGenres(item.Genres, user, dtoOptions); + return GetInstantMixFromGenres(artist.Genres, user, dtoOptions); } public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions) @@ -100,8 +103,7 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions) { - var genre = item as MusicGenre; - if (genre != null) + if (item is MusicGenre genre) { return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 770cf6bb0..d5b855cdf 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Diagnostics.CodeAnalysis; using MediaBrowser.Common.Providers; @@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.Library /// <param name="path">The original path.</param> /// <param name="subPath">The original sub path.</param> /// <param name="newSubPath">The new sub path.</param> - /// <param name="newPath">The result of the sub path replacement</param> + /// <param name="newPath">The result of the sub path replacement.</param> /// <returns>The path after replacing the sub path.</returns> /// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception> public static bool TryReplaceSubPath( @@ -96,8 +94,14 @@ namespace Emby.Server.Implementations.Library // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results // when the sub path matches a similar but in-complete subpath var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar; - if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase) - || (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar)) + if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (path.Length > subPath.Length + && !oldSubPathEndsWithSeparator + && path[subPath.Length] != newDirectorySeparatorChar) { return false; } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 4e4cac75b..ac75e5d3a 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -18,11 +18,10 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="item">The item.</param> /// <param name="parent">The parent.</param> - /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="directoryService">The directory service.</param> - /// <exception cref="ArgumentException">Item must have a path</exception> - public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService) + /// <exception cref="ArgumentException">Item must have a path.</exception> + public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService) { // This version of the below method has no ItemResolveArgs, so we have to require the path already being set if (string.IsNullOrEmpty(item.Path)) @@ -43,9 +42,14 @@ namespace Emby.Server.Implementations.Library // Make sure DateCreated and DateModified have values var fileInfo = directoryService.GetFile(item.Path); - SetDateCreated(item, fileSystem, fileInfo); + if (fileInfo == null) + { + throw new FileNotFoundException("Can't find item path.", item.Path); + } - EnsureName(item, item.Path, fileInfo); + SetDateCreated(item, fileInfo); + + EnsureName(item, fileInfo); } /// <summary> @@ -72,9 +76,9 @@ namespace Emby.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); // Make sure the item has a name - EnsureName(item, item.Path, args.FileInfo); + EnsureName(item, args.FileInfo); - 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 @@ -84,29 +88,16 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Ensures the name. /// </summary> - private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo) + private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo) { // If the subclass didn't supply a name, add it here - if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath)) + if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path)) { - var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name; - - item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory); + item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name); } } /// <summary> - /// Gets the display name. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="isDirectory">if set to <c>true</c> [is directory].</param> - /// <returns>System.String.</returns> - private static string GetDisplayName(string path, bool isDirectory) - { - return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); - } - - /// <summary> /// Ensures DateCreated and DateModified have values. /// </summary> /// <param name="fileSystem">The file system.</param> @@ -114,21 +105,6 @@ namespace Emby.Server.Implementations.Library /// <param name="args">The args.</param> private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args) { - if (fileSystem == null) - { - throw new ArgumentNullException(nameof(fileSystem)); - } - - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - // See if a different path came out of the resolver than what went in if (!fileSystem.AreEqual(args.Path, item.Path)) { @@ -136,7 +112,7 @@ namespace Emby.Server.Implementations.Library if (childData != null) { - SetDateCreated(item, fileSystem, childData); + SetDateCreated(item, childData); } else { @@ -144,17 +120,17 @@ namespace Emby.Server.Implementations.Library if (fileData.Exists) { - SetDateCreated(item, fileSystem, fileData); + SetDateCreated(item, fileData); } } } else { - SetDateCreated(item, fileSystem, args.FileInfo); + SetDateCreated(item, args.FileInfo); } } - private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info) + private static void SetDateCreated(BaseItem item, FileSystemMetadata? info) { var config = BaseItem.ConfigurationManager.GetMetadataConfiguration(); @@ -163,7 +139,7 @@ namespace Emby.Server.Implementations.Library // directoryService.getFile may return null if (info != null) { - var dateCreated = fileSystem.GetCreationTimeUtc(info); + var dateCreated = info.CreationTimeUtc; if (dateCreated.Equals(DateTime.MinValue)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 4ad84579d..7a6aea9c1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -1,10 +1,15 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Emby.Naming.Audio; using Emby.Naming.AudioBook; +using Emby.Naming.Common; +using Emby.Naming.Video; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -19,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver { - private readonly ILibraryManager LibraryManager; + private readonly NamingOptions _namingOptions; - public AudioResolver(ILibraryManager libraryManager) + public AudioResolver(NamingOptions namingOptions) { - LibraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -38,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio string collectionType, IDirectoryService directoryService) { - var result = ResolveMultipleInternal(parent, files, collectionType, directoryService); + var result = ResolveMultipleInternal(parent, files, collectionType); if (result != null) { @@ -54,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List<FileSystemMetadata> files, - string collectionType, - IDirectoryService directoryService) + string collectionType) { if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) { - return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true); + return ResolveMultipleAudio(parent, files, true); } return null; @@ -85,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - var files = args.FileSystemChildren - .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) - .ToList(); - - return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + return FindAudioBook(args, false); } - if (LibraryManager.IsAudioFile(args.Path)) + if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) { var extension = Path.GetExtension(args.Path); @@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var isMixedCollectionType = string.IsNullOrEmpty(collectionType); // For conflicting extensions, give priority to videos - if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path)) + if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions)) { return null; } @@ -139,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return null; } - private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) - where T : MediaBrowser.Controller.Entities.Audio.Audio, new() + private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName) { // TODO: Allow GetMultiDiscMovie in here - const bool supportsMultiVersion = false; - - var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? - new MultiItemResolverResult(); + var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName); - if (result.Items.Count == 1) + if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item) { - // If we were supporting this we'd be checking filesFromOtherItems - var item = (T)result.Items[0]; - item.IsInMixedFolder = false; - item.Name = Path.GetFileName(item.ContainingFolderPath); - return item; + return null; } - return null; + // If we were supporting this we'd be checking filesFromOtherItems + item.IsInMixedFolder = false; + item.Name = Path.GetFileName(item.ContainingFolderPath); + return item; } - private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName) - where T : MediaBrowser.Controller.Entities.Audio.Audio, new() + private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName) { var files = new List<FileSystemMetadata>(); var items = new List<BaseItem>(); @@ -174,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { leftOver.Add(child); } - else if (!IsIgnored(child.Name)) + else { files.Add(child); } } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var resolver = new AudioBookListResolver(namingOptions); + var resolver = new AudioBookListResolver(_namingOptions); var resolverResult = resolver.Resolve(files).ToList(); var result = new MultiItemResolverResult @@ -208,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var firstMedia = resolvedItem.Files[0]; - var libraryItem = new T + var libraryItem = new AudioBook { Path = firstMedia.Path, IsInMixedFolder = isInMixedFolder, @@ -228,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return result; } - private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file) + private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file) { return result.Any(i => ContainsFile(i, file)); } - private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file) + private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file) { return result.Files.Any(i => ContainsFile(i, file)) || result.AlternateVersions.Any(i => ContainsFile(i, file)) || @@ -244,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase); } - - private static bool IsIgnored(string filename) - { - return false; - } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index bf32381eb..a9819a364 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -1,9 +1,12 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Naming.Audio; +using Emby.Naming.Common; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -20,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public class MusicAlbumResolver : ItemResolver<MusicAlbum> { private readonly ILogger<MusicAlbumResolver> _logger; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class. /// </summary> /// <param name="logger">The logger.</param> - /// <param name="fileSystem">The file system.</param> - /// <param name="libraryManager">The library manager.</param> - public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager) + /// <param name="namingOptions">The naming options.</param> + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions) { _logger = logger; - _fileSystem = fileSystem; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -80,9 +80,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <summary> /// Determine if the supplied file data points to a music album. /// </summary> + /// <param name="path">The path to check.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns> public bool IsMusicAlbum(string path, IDirectoryService directoryService) { - return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager); + return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService); } /// <summary> @@ -96,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (args.IsDirectory) { // if (args.Parent is MusicArtist) return true; // saves us from testing children twice - if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager)) + if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService)) { return true; } @@ -111,13 +114,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio private bool ContainsMusic( IEnumerable<FileSystemMetadata> list, bool allowSubfolders, - IDirectoryService directoryService, - ILogger<MusicAlbumResolver> logger, - IFileSystem fileSystem, - ILibraryManager libraryManager) + IDirectoryService directoryService) { // check for audio files before digging down into directories - var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName)); + var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions)); if (foundAudioFile) { // at least one audio file exists @@ -132,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var discSubfolderCount = 0; - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - var parser = new AlbumParser(namingOptions); + var parser = new AlbumParser(_namingOptions); var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory); var result = Parallel.ForEach(directories, (fileSystemInfo, state) => { var path = fileSystemInfo.FullName; - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService); if (hasMusic) { if (parser.IsMultiPart(path)) { - logger.LogDebug("Found multi-disc folder: " + path); + _logger.LogDebug("Found multi-disc folder: {Path}", path); Interlocked.Increment(ref discSubfolderCount); } else diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 60f82806f..27e18be42 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -1,6 +1,9 @@ +#nullable disable + using System; using System.Linq; using System.Threading.Tasks; +using Emby.Naming.Common; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -17,27 +20,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio public class MusicArtistResolver : ItemResolver<MusicArtist> { private readonly ILogger<MusicAlbumResolver> _logger; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; + private NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param> - /// <param name="fileSystem">The file system.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="config">The configuration manager.</param> + /// <param name="namingOptions">The naming options.</param> public MusicArtistResolver( ILogger<MusicAlbumResolver> logger, - IFileSystem fileSystem, - ILibraryManager libraryManager, - IServerConfigurationManager config) + NamingOptions namingOptions) { _logger = logger; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _config = config; + _namingOptions = namingOptions; } /// <summary> @@ -87,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var directoryService = args.DirectoryService; - var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); + var albumResolver = new MusicAlbumResolver(_logger, _namingOptions); // If we contain an album assume we are an artist folder var directories = args.FileSystemChildren.Where(i => i.IsDirectory); diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 6e688693b..0ebf0e530 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -1,8 +1,12 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.IO; using System.Linq; +using DiscUtils.Udf; +using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -14,17 +18,17 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Resolves a Path into a Video or Video subclass. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type of item to resolve.</typeparam> public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> where T : Video, new() { - protected readonly ILibraryManager LibraryManager; - - protected BaseVideoResolver(ILibraryManager libraryManager) + protected BaseVideoResolver(NamingOptions namingOptions) { - LibraryManager = libraryManager; + NamingOptions = namingOptions; } + protected NamingOptions NamingOptions { get; } + /// <summary> /// Resolves the specified args. /// </summary> @@ -45,11 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); + var namingOptions = NamingOptions; // If the path is a file check for a matching extensions - var parser = new VideoResolver(namingOptions); - if (args.IsDirectory) { TVideoType video = null; @@ -64,7 +66,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -80,9 +82,9 @@ namespace Emby.Server.Implementations.Library.Resolvers break; } - if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) + if (IsBluRayDirectory(filename)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -100,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } else if (IsDvdFile(filename)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -130,14 +132,14 @@ namespace Emby.Server.Implementations.Library.Resolvers } else { - var videoInfo = parser.Resolve(args.Path, false, false); + var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false); if (videoInfo == null) { return null; } - if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub) + if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub) { var path = args.Path; @@ -165,13 +167,13 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void SetVideoType(Video video, VideoFileInfo videoInfo) { - var extension = Path.GetExtension(video.Path); - video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) || - string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ? - VideoType.Iso : - VideoType.VideoFile; + var extension = Path.GetExtension(video.Path.AsSpan()); + video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".img", StringComparison.OrdinalIgnoreCase) + ? VideoType.Iso + : VideoType.VideoFile; - video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); video.IsPlaceHolder = videoInfo.IsStub; if (videoInfo.IsStub) @@ -193,14 +195,30 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (video.VideoType == VideoType.Iso) { - if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1) + if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase)) { video.IsoType = IsoType.Dvd; } - else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1) + else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase)) { video.IsoType = IsoType.BluRay; } + else + { + // use disc-utils, both DVDs and BDs use UDF filesystem + using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) + { + UdfReader udfReader = new UdfReader(videoFileStream); + if (udfReader.DirectoryExists("VIDEO_TS")) + { + video.IsoType = IsoType.Dvd; + } + else if (udfReader.DirectoryExists("BDMV")) + { + video.IsoType = IsoType.BluRay; + } + } + } } } @@ -250,10 +268,7 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void Set3DFormat(Video video) { - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var resolver = new Format3DParser(namingOptions); - var result = resolver.Parse(video.Path); + var result = Format3DParser.Parse(video.Path, NamingOptions); Set3DFormat(video, result.Is3D, result.Format3D); } @@ -261,6 +276,10 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Determines whether [is DVD directory] [the specified directory name]. /// </summary> + /// <param name="fullPath">The full path of the directory.</param> + /// <param name="directoryName">The name of the directory.</param> + /// <param name="directoryService">The directory service.</param> + /// <returns><c>true</c> if the provided directory is a DVD directory, <c>false</c> otherwise.</returns> protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService) { if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase)) @@ -282,25 +301,13 @@ namespace Emby.Server.Implementations.Library.Resolvers } /// <summary> - /// Determines whether [is blu ray directory] [the specified directory name]. + /// Determines whether [is bluray directory] [the specified directory name]. /// </summary> - protected bool IsBluRayDirectory(string fullPath, string directoryName, IDirectoryService directoryService) + /// <param name="directoryName">The directory name.</param> + /// <returns>Whether the directory is a bluray directory.</returns> + protected bool IsBluRayDirectory(string directoryName) { - if (!string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return true; - // var blurayExtensions = new[] - //{ - // ".mts", - // ".m2ts", - // ".bdmv", - // ".mpls" - //}; - - // return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase)); + return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 0525c7e30..e685c87f1 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -47,13 +49,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { var bookFiles = args.FileSystemChildren.Where(f => { - var fileExtension = Path.GetExtension(f.FullName) ?? - string.Empty; + var fileExtension = Path.GetExtension(f.FullName) + ?? string.Empty; return _validExtensions.Contains( fileExtension, - StringComparer - .OrdinalIgnoreCase); + StringComparer.OrdinalIgnoreCase); }).ToList(); // Don't return a Book if there is more (or less) than one document in the directory diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs index 7dbce7a6e..db7703cd6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -7,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Class FolderResolver. /// </summary> - public class FolderResolver : FolderResolver<Folder> + public class FolderResolver : GenericFolderResolver<Folder> { /// <summary> /// Gets the priority. @@ -30,24 +32,4 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } } - - /// <summary> - /// Class FolderResolver. - /// </summary> - /// <typeparam name="TItemType">The type of the T item type.</typeparam> - public abstract class FolderResolver<TItemType> : ItemResolver<TItemType> - where TItemType : Folder, new() - { - /// <summary> - /// Sets the initial item values. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="args">The args.</param> - protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args) - { - base.SetInitialItemValues(item, args); - - item.IsRoot = args.Parent == null; - } - } } diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs new file mode 100644 index 000000000..f109a5e9a --- /dev/null +++ b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs @@ -0,0 +1,27 @@ +#nullable disable + +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; + +namespace Emby.Server.Implementations.Library.Resolvers +{ + /// <summary> + /// Class FolderResolver. + /// </summary> + /// <typeparam name="TItemType">The type of the T item type.</typeparam> + public abstract class GenericFolderResolver<TItemType> : ItemResolver<TItemType> + where TItemType : Folder, new() + { + /// <summary> + /// Sets the initial item values. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="args">The args.</param> + protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args) + { + base.SetInitialItemValues(item, args); + + item.IsRoot = args.Parent == null; + } + } +} diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index 62268fce9..72341d9db 100644 --- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -1,15 +1,17 @@ +#nullable disable + #pragma warning disable CS1591 +using Emby.Naming.Common; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; namespace Emby.Server.Implementations.Library.Resolvers { public class GenericVideoResolver<T> : BaseVideoResolver<T> where T : Video, new() { - public GenericVideoResolver(ILibraryManager libraryManager) - : base(libraryManager) + public GenericVideoResolver(NamingOptions namingOptions) + : base(namingOptions) { } } diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index 9ca76095b..3f29ab191 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -7,11 +9,17 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Class ItemResolver. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type of BaseItem.</typeparam> public abstract class ItemResolver<T> : IItemResolver where T : BaseItem, new() { /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public virtual ResolverPriority Priority => ResolverPriority.First; + + /// <summary> /// Resolves the specified args. /// </summary> /// <param name="args">The args.</param> @@ -22,12 +30,6 @@ namespace Emby.Server.Implementations.Library.Resolvers } /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public virtual ResolverPriority Priority => ResolverPriority.First; - - /// <summary> /// Sets initial values on the newly resolved item. /// </summary> /// <param name="item">The item.</param> diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index 295e9e120..e7abe1e6d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.IO; using MediaBrowser.Controller.Entities; @@ -10,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// <summary> /// Class BoxSetResolver. /// </summary> - public class BoxSetResolver : FolderResolver<BoxSet> + public class BoxSetResolver : GenericFolderResolver<BoxSet> { /// <summary> /// Resolves the specified args. diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 714bc3a84..732be0fe5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -1,9 +1,13 @@ +#nullable disable + using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Emby.Naming.Common; using Emby.Naming.Video; +using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -21,6 +25,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// </summary> public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver { + private readonly IImageProcessor _imageProcessor; + private readonly StackResolver _stackResolver; + private string[] _validCollectionTypes = new[] { CollectionType.Movies, @@ -30,17 +37,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies CollectionType.Photos }; - private readonly IImageProcessor _imageProcessor; - /// <summary> /// Initializes a new instance of the <see cref="MovieResolver"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> /// <param name="imageProcessor">The image processor.</param> - public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor) - : base(libraryManager) + /// <param name="namingOptions">The naming options.</param> + public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) + : base(namingOptions) { _imageProcessor = imageProcessor; + _stackResolver = new StackResolver(NamingOptions); } /// <summary> @@ -86,9 +92,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - var files = args.FileSystemChildren - .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) - .ToList(); + var files = args.GetActualFileSystemChildren().ToList(); if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { @@ -255,10 +259,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var resolver = new VideoListResolver(namingOptions); - var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList(); + var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList(); var result = new MultiItemResolverResult { @@ -376,7 +377,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { var multiDiscFolders = new List<FileSystemMetadata>(); - var libraryOptions = args.GetLibraryOptions(); + var libraryOptions = args.LibraryOptions; var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos; var photos = new List<FileSystemMetadata>(); @@ -398,7 +399,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return movie; } - if (IsBluRayDirectory(child.FullName, filename, directoryService)) + if (IsBluRayDirectory(filename)) { var movie = new T { @@ -436,7 +437,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (result.Items.Count == 1) { var videoPath = result.Items[0].Path; - var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name)); + var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); if (!hasPhotos) { @@ -479,7 +480,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return true; } - if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService))) + if (subfolders.Any(s => IsBluRayDirectory(s.Name))) { videoTypes.Add(VideoType.BluRay); return true; @@ -509,9 +510,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList(); + var result = _stackResolver.ResolveDirectories(folderPaths).ToList(); if (result.Count != 1) { @@ -535,7 +534,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return returnVideo; } - private bool IsInvalid(Folder parent, string collectionType) + private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType) { if (parent != null) { @@ -545,12 +544,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (string.IsNullOrEmpty(collectionType)) + if (collectionType.IsEmpty) { return false; } - return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); + return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 3ac837057..7dd0ab185 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -1,4 +1,7 @@ +#nullable disable + using System; +using Emby.Naming.Common; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -10,22 +13,25 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Class PhotoAlbumResolver. /// </summary> - public class PhotoAlbumResolver : FolderResolver<PhotoAlbum> + public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum> { private readonly IImageProcessor _imageProcessor; - private ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class. /// </summary> /// <param name="imageProcessor">The image processor.</param> - /// <param name="libraryManager">The library manager.</param> - public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) + /// <param name="namingOptions">The naming options.</param> + public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) { _imageProcessor = imageProcessor; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } + /// <inheritdoc /> + public override ResolverPriority Priority => ResolverPriority.Second; + /// <summary> /// Resolves the specified args. /// </summary> @@ -39,8 +45,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) || - (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) + if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) + || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) { if (HasPhotos(args)) { @@ -68,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers foreach (var siblingFile in files) { - if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename)) + if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename)) { ownedByMedia = true; break; @@ -84,8 +90,5 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - - /// <inheritdoc /> - public override ResolverPriority Priority => ResolverPriority.Second; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index bcfcee9c6..51d819303 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,9 +1,13 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Emby.Naming.Common; +using Emby.Naming.Video; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -14,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers public class PhotoResolver : ItemResolver<Photo> { private readonly IImageProcessor _imageProcessor; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; + private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "folder", @@ -28,10 +33,11 @@ namespace Emby.Server.Implementations.Library.Resolvers "default" }; - public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) + + public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) { _imageProcessor = imageProcessor; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -47,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var collectionType = args.CollectionType; if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) - || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) + || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { @@ -58,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers foreach (var file in files) { - if (IsOwnedByMedia(_libraryManager, file.FullName, filename)) + if (IsOwnedByMedia(_namingOptions, file.FullName, filename)) { return null; } @@ -75,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename) + internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename) { - if (libraryManager.IsVideoFile(file)) - { - return IsOwnedByResolvedMedia(libraryManager, file, imageFilename); - } - - return false; + return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename); } - internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename) + internal static bool IsOwnedByResolvedMedia(string file, string imageFilename) => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); internal static bool IsImageFile(string path, IImageProcessor imageProcessor) diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 5f051321f..8ce59717d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -14,9 +16,10 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items. /// </summary> - public class PlaylistResolver : FolderResolver<Playlist> + public class PlaylistResolver : GenericFolderResolver<Playlist> { - private string[] _musicPlaylistCollectionTypes = new string[] { + private string[] _musicPlaylistCollectionTypes = + { string.Empty, CollectionType.Music }; diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs index 99f304190..6bb999641 100644 --- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -11,7 +13,7 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Library.Resolvers { - public class SpecialFolderResolver : FolderResolver<Folder> + public class SpecialFolderResolver : GenericFolderResolver<Folder> { private readonly IFileSystem _fileSystem; private readonly IServerApplicationPaths _appPaths; @@ -65,7 +67,6 @@ namespace Emby.Server.Implementations.Library.Resolvers return args.FileSystemChildren .Where(i => { - try { return !i.IsDirectory && diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 9b4cd7a3d..f72da3617 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -1,5 +1,8 @@ +#nullable disable + using System; using System.Linq; +using Emby.Naming.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -15,9 +18,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> - public EpisodeResolver(ILibraryManager libraryManager) - : base(libraryManager) + /// <param name="namingOptions">The naming options.</param> + public EpisodeResolver(NamingOptions namingOptions) + : base(namingOptions) { } @@ -35,31 +38,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - var season = parent as Season; - // Just in case the user decided to nest episodes. // Not officially supported but in some cases we can handle it. - if (season == null) - { - season = parent.GetParents().OfType<Season>().FirstOrDefault(); - } + + var season = parent as Season ?? parent.GetParents().OfType<Season>().FirstOrDefault(); // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders if ((season != null || string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || args.HasParent<Series>()) - && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase))) + && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name))) { var episode = ResolveVideo<Episode>(args, false); if (episode != null) { - var series = parent as Series; - if (series == null) - { - series = parent.GetParents().OfType<Series>().FirstOrDefault(); - } + var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault(); if (series != null) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 3332e1806..ea4851458 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,4 +1,7 @@ +#nullable disable + using System.Globalization; +using Emby.Naming.Common; using Emby.Naming.TV; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -10,24 +13,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Class SeasonResolver. /// </summary> - public class SeasonResolver : FolderResolver<Season> + public class SeasonResolver : GenericFolderResolver<Season> { - private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly ILogger<SeasonResolver> _logger; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="localization">The localization.</param> /// <param name="logger">The logger.</param> public SeasonResolver( - ILibraryManager libraryManager, + NamingOptions namingOptions, ILocalizationManager localization, ILogger<SeasonResolver> logger) { - _libraryManager = libraryManager; + _namingOptions = namingOptions; _localization = localization; _logger = logger; } @@ -41,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (args.Parent is Series series && args.IsDirectory) { - var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); + var namingOptions = _namingOptions; var path = args.Path; @@ -63,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var episodeInfo = resolver.Resolve(testPath, true); - if (episodeInfo != null) + if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue) { - if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue) - { - _logger.LogDebug( - "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", - path, - episodeInfo.SeasonNumber.Value, - episodeInfo.EpisodeNumber.Value); - - return null; - } + _logger.LogDebug( + "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", + path, + episodeInfo.SeasonNumber.Value, + episodeInfo.EpisodeNumber.Value); + + return null; } } @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameSeasonNumber"), seasonNumber, - args.GetLibraryOptions().PreferredMetadataLanguage); + args.LibraryOptions.PreferredMetadataLanguage); } return season; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 4a9d2cf8c..4e15acd18 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -1,12 +1,18 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using Emby.Naming.Common; using Emby.Naming.TV; +using Emby.Naming.Video; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -17,23 +23,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Class SeriesResolver. /// </summary> - public class SeriesResolver : FolderResolver<Series> + public class SeriesResolver : GenericFolderResolver<Series> { - private readonly IFileSystem _fileSystem; private readonly ILogger<SeriesResolver> _logger; - private readonly ILibraryManager _libraryManager; + private readonly NamingOptions _namingOptions; /// <summary> /// Initializes a new instance of the <see cref="SeriesResolver"/> class. /// </summary> - /// <param name="fileSystem">The file system.</param> /// <param name="logger">The logger.</param> - /// <param name="libraryManager">The library manager.</param> - public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager) + /// <param name="namingOptions">The naming options.</param> + public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions) { - _fileSystem = fileSystem; _logger = logger; - _libraryManager = libraryManager; + _namingOptions = namingOptions; } /// <summary> @@ -56,25 +59,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } + var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); + var collectionType = args.GetCollectionType(); if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - // if (args.ContainsFileSystemEntryByName("tvshow.nfo")) - //{ - // return new Series - // { - // Path = args.Path, - // Name = Path.GetFileName(args.Path) - // }; - //} - - var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path); + // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType + var configuredContentType = args.GetConfiguredContentType(); if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { return new Series { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = seriesInfo.Name }; } } @@ -91,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return new Series { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = seriesInfo.Name }; } @@ -100,12 +97,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false)) + if (IsSeriesFolder(args.Path, args.FileSystemChildren, false)) { return new Series { Path = args.Path, - Name = Path.GetFileName(args.Path) + Name = seriesInfo.Name }; } } @@ -114,13 +111,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - public static bool IsSeriesFolder( + private bool IsSeriesFolder( string path, IEnumerable<FileSystemMetadata> fileSystemChildren, - IDirectoryService directoryService, - IFileSystem fileSystem, - ILogger<SeriesResolver> logger, - ILibraryManager libraryManager, bool isTvContentType) { foreach (var child in fileSystemChildren) @@ -129,21 +122,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (IsSeasonFolder(child.FullName, isTvContentType)) { - logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); + _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; } } else { string fullName = child.FullName; - if (libraryManager.IsVideoFile(fullName)) + if (VideoResolver.IsVideoFile(path, _namingOptions)) { if (isTvContentType) { return true; } - var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); + var namingOptions = _namingOptions; var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions); @@ -156,7 +149,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV } } - logger.LogDebug("{Path} is not a series folder.", path); + _logger.LogDebug("{Path} is not a series folder.", path); return false; } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index bcdf854ca..bebc25b77 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -1,14 +1,16 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Linq; +using Diacritics.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Search; @@ -71,7 +73,7 @@ namespace Emby.Server.Implementations.Library /// <param name="query">The query.</param> /// <param name="user">The user.</param> /// <returns>IEnumerable{SearchHintResult}.</returns> - /// <exception cref="ArgumentNullException">searchTerm</exception> + /// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception> private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user) { var searchTerm = query.SearchTerm; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 827e3c64b..bb3034142 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -23,8 +25,6 @@ namespace Emby.Server.Implementations.Library /// </summary> public class UserDataManager : IUserDataManager { - public event EventHandler<UserDataSaveEventArgs> UserDataSaved; - private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); @@ -42,6 +42,8 @@ namespace Emby.Server.Implementations.Library _repository = repository; } + public event EventHandler<UserDataSaveEventArgs> UserDataSaved; + public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) { var user = _userManager.GetUserById(userId); @@ -88,10 +90,9 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. /// </summary> - /// <param name="userId"></param> - /// <param name="userData"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <param name="userData">The user item data.</param> + /// <param name="cancellationToken">The cancellation token.</param> public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) { var user = _userManager.GetUserById(userId); @@ -102,8 +103,8 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Retrieve all user data for the given user. /// </summary> - /// <param name="userId"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <returns>A <see cref="List{UserItemData}"/> containing all of the user's item data.</returns> public List<UserItemData> GetAllUserData(Guid userId) { var user = _userManager.GetUserById(userId); @@ -175,6 +176,7 @@ namespace Emby.Server.Implementations.Library return dto; } + /// <inheritdoc /> public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options) { var userData = GetUserData(user, item); @@ -189,7 +191,7 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="data">The data.</param> /// <returns>DtoUserItemData.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception> private UserItemDataDto GetUserItemDataDto(UserItemData data) { if (data == null) @@ -210,6 +212,7 @@ namespace Emby.Server.Implementations.Library }; } + /// <inheritdoc /> public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks) { var playedToCompletion = false; @@ -220,7 +223,7 @@ namespace Emby.Server.Implementations.Library var hasRuntime = runtimeTicks > 0; // If a position has been reported, and if we know the duration - if (positionTicks > 0 && hasRuntime && !(item is AudioBook)) + if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book) { var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100; @@ -239,7 +242,7 @@ namespace Emby.Server.Implementations.Library { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds; - if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book)) + if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) { positionTicks = 0; data.Played = playedToCompletion = true; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index ac041bcf6..711647e7a 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -339,19 +341,26 @@ namespace Emby.Server.Implementations.Library mediaTypes = mediaTypes.Distinct().ToList(); } - var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[] - { - nameof(Person), - nameof(Studio), - nameof(Year), - nameof(MusicGenre), - nameof(Genre) - } : Array.Empty<string>(); + var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 + ? new[] + { + nameof(Person), + nameof(Studio), + nameof(Year), + nameof(MusicGenre), + nameof(Genre) + } + : Array.Empty<string>(); var query = new InternalItemsQuery(user) { IncludeItemTypes = includeItemTypes, - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = new[] + { + (ItemSortBy.DateCreated, SortOrder.Descending), + (ItemSortBy.SortName, SortOrder.Descending), + (ItemSortBy.ProductionYear, SortOrder.Descending) + }, IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null, ExcludeItemTypes = excludeItemTypes, IsVirtualItem = false, diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index f9a3e2c64..40436d9c5 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -95,10 +95,13 @@ namespace Emby.Server.Implementations.Library.Validators _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - }, false); + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } progress.Report(100); diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs new file mode 100644 index 000000000..945b559ad --- /dev/null +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -0,0 +1,156 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Querying; +using Jellyfin.Data.Enums; +using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Entities; + +namespace Emby.Server.Implementations.Library.Validators +{ + /// <summary> + /// Class CollectionPostScanTask. + /// </summary> + public class CollectionPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + private readonly ICollectionManager _collectionManager; + private readonly ILogger<CollectionPostScanTask> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="collectionManager">The collection manager.</param> + /// <param name="logger">The logger.</param> + public CollectionPostScanTask( + ILibraryManager libraryManager, + ICollectionManager collectionManager, + ILogger<CollectionPostScanTask> logger) + { + _libraryManager = libraryManager; + _collectionManager = collectionManager; + _logger = logger; + } + + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>(); + + foreach (var library in _libraryManager.RootFolder.Children) + { + if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) + { + continue; + } + + var startIndex = 0; + var pagesize = 1000; + + while (true) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new string[] { MediaType.Video }, + IncludeItemTypes = new[] { nameof(Movie) }, + IsVirtualItem = false, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, + Parent = library, + StartIndex = startIndex, + Limit = pagesize, + Recursive = true + }); + + foreach (var m in movies) + { + if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) + { + if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) + { + movieList.Add(movie.Id); + } + else + { + collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id }; + } + } + } + + if (movies.Count < pagesize) + { + break; + } + + startIndex += pagesize; + } + } + + var numComplete = 0; + var count = collectionNameMoviesMap.Count; + + if (count == 0) + { + progress.Report(100); + return; + } + + var boxSets = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { nameof(BoxSet) }, + CollapseBoxSetItems = false, + Recursive = true + }); + + foreach (var (collectionName, movieIds) in collectionNameMoviesMap) + { + try + { + var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; + if (boxSet == null) + { + // won't automatically create collection if only one movie in it + if (movieIds.Count >= 2) + { + boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions + { + Name = collectionName, + IsLocked = true + }); + + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); + } + } + else + { + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); + } + } + + progress.Report(100); + } + } +} diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 8739a9e1b..8a9a4b865 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Validators } catch (Exception ex) { - _logger.LogError(ex, "Error validating IBN entry {person}", person); + _logger.LogError(ex, "Error validating IBN entry {Person}", person); } // Update progress diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 9a8c5f39d..8577d722e 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -87,12 +87,15 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - }, false); + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } progress.Report(100); |
