diff options
| author | pokreman06 <112423673+pokreman06@users.noreply.github.com> | 2025-10-02 11:07:05 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-02 11:07:05 -0600 |
| commit | 0b4854c5eff7c862d05f43048e08dd3a1a25efaa (patch) | |
| tree | a4c417af05deef7878ab9342c85c506ad22e1ced /Emby.Server.Implementations/Library | |
| parent | d6a1c8413c6a213f6e579246c1b85aad9b028b3a (diff) | |
| parent | 0f42aa892e0a7fe2ac4e680e7647515af0909e5e (diff) | |
Merge branch 'jellyfin:master' into master
Diffstat (limited to 'Emby.Server.Implementations/Library')
9 files changed, 170 insertions, 86 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f9538fbad6..ca0744a17d 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -37,6 +37,11 @@ namespace Emby.Server.Implementations.Library return false; } + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) + { + return true; + } + // Don't ignore top level folders if (fileInfo.IsDirectory && (parent is AggregateFolder || (parent?.IsTopParent ?? false))) @@ -44,11 +49,6 @@ namespace Emby.Server.Implementations.Library return false; } - if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) - { - return true; - } - if (parent is null) { return false; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 401ca73b80..bafe3ad436 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -50,6 +50,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule return false; } + // Fast path in case the ignore files isn't a symlink and is empty + if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0 + && dirIgnoreFile.Length == 0) + { + return true; + } + // ignore the directory only if the .ignore file is empty // evaluate individual files otherwise return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 25ddade829..fe3a1ce611 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -48,6 +48,8 @@ namespace Emby.Server.Implementations.Library "**/.wd_tv", "**/lost+found/**", "**/lost+found", + "**/subs/**", + "**/subs", // Trickplay files "**/*.trickplay", diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 58a971f62a..ef497726e2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -327,6 +327,45 @@ namespace Emby.Server.Implementations.Library DeleteItem(item, options, parent, notifyParentItem); } + public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items) + { + var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray(); + + foreach (var (item, internalPaths, pathsToDelete) in pathMaps) + { + foreach (var metadataPath in internalPaths) + { + if (!Directory.Exists(metadataPath)) + { + continue; + } + + _logger.LogDebug( + "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + metadataPath, + item.Id); + + try + { + Directory.Delete(metadataPath, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); + } + } + + foreach (var fileSystemInfo in pathsToDelete) + { + DeleteItemPath(item, false, fileSystemInfo); + } + } + + _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]); + } + public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { ArgumentNullException.ThrowIfNull(item); @@ -403,59 +442,7 @@ namespace Emby.Server.Implementations.Library foreach (var fileSystemInfo in item.GetDeletePaths()) { - if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) - { - try - { - _logger.LogInformation( - "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - - if (fileSystemInfo.IsDirectory) - { - Directory.Delete(fileSystemInfo.FullName, true); - } - else - { - File.Delete(fileSystemInfo.FullName); - } - } - catch (DirectoryNotFoundException) - { - _logger.LogInformation( - "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (FileNotFoundException) - { - _logger.LogInformation( - "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (IOException) - { - if (isRequiredForDelete) - { - throw; - } - } - catch (UnauthorizedAccessException) - { - if (isRequiredForDelete) - { - throw; - } - } - } + DeleteItemPath(item, isRequiredForDelete, fileSystemInfo); isRequiredForDelete = false; } @@ -463,17 +450,73 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - _itemRepository.DeleteItem(item.Id); + _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]); _cache.TryRemove(item.Id, out _); foreach (var child in children) { - _itemRepository.DeleteItem(child.Id); _cache.TryRemove(child.Id, out _); } ReportItemRemoved(item, parent); } + private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo) + { + if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) + { + try + { + _logger.LogInformation( + "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + + if (fileSystemInfo.IsDirectory) + { + Directory.Delete(fileSystemInfo.FullName, true); + } + else + { + File.Delete(fileSystemInfo.FullName); + } + } + catch (DirectoryNotFoundException) + { + _logger.LogInformation( + "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (FileNotFoundException) + { + _logger.LogInformation( + "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (IOException) + { + if (isRequiredForDelete) + { + throw; + } + } + catch (UnauthorizedAccessException) + { + if (isRequiredForDelete) + { + throw; + } + } + } + } + private bool IsInternalItem(BaseItem item) { if (!item.IsFileProtocol) @@ -485,7 +528,7 @@ namespace Emby.Server.Implementations.Library { Genre => _configurationManager.ApplicationPaths.GenrePath, MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, - MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath, Person => _configurationManager.ApplicationPaths.PeoplePath, Studio => _configurationManager.ApplicationPaths.StudioPath, Year => _configurationManager.ApplicationPaths.YearPath, @@ -826,6 +869,7 @@ namespace Emby.Server.Implementations.Library if (!folder.ParentId.Equals(rootFolder.Id)) { + rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); folder.ParentId = rootFolder.Id; folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); } @@ -989,6 +1033,11 @@ namespace Emby.Server.Implementations.Library return GetArtist(name, new DtoOptions(true)); } + public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names) + { + return _itemRepository.FindArtists(names); + } + public MusicArtist GetArtist(string name, DtoOptions options) { return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options); @@ -1090,6 +1139,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { + RootFolder.Children = null; await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further @@ -1100,9 +1150,12 @@ namespace Emby.Server.Implementations.Library allowRemoveRoot: removeRoot, cancellationToken: cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); + var rootFolder = GetUserRootFolder(); + rootFolder.Children = null; + + await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().ValidateChildren( + await rootFolder.ValidateChildren( new Progress<double>(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, @@ -1110,18 +1163,24 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var child in GetUserRootFolder().Children.OfType<Folder>()) + var toDelete = new List<Guid>(); + foreach (var child in rootFolder.Children!.OfType<Folder>()) { // If the user has somehow deleted the collection directory, remove the metadata from the database. if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) { - _itemRepository.DeleteItem(collectionFolder.Id); + toDelete.Add(collectionFolder.Id); } else { await child.RefreshMetadata(cancellationToken).ConfigureAwait(false); } } + + if (toDelete.Count > 0) + { + _itemRepository.DeleteItem(toDelete.ToArray()); + } } private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken) @@ -2027,6 +2086,12 @@ namespace Emby.Server.Implementations.Library } } + if (!File.Exists(image.Path)) + { + _logger.LogWarning("Image not found at {ImagePath}", image.Path); + continue; + } + ImageDimensions size; try { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 1e3b8ea760..750346169f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -657,7 +657,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); + _logger.LogDebug(ex, "Error parsing cached media info."); } finally { diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 28cf695007..e0c8ae371b 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item - .GetRecursiveChildren(user, new InternalItemsQuery(user) - { - IncludeItemTypes = [BaseItemKind.Audio], - DtoOptions = dtoOptions - }) + .GetRecursiveChildren( + user, + new InternalItemsQuery(user) + { + IncludeItemTypes = [BaseItemKind.Audio], + DtoOptions = dtoOptions + }, + out _) .Cast<Audio>() .SelectMany(i => i.Genres) .Concat(item.Genres) diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index b2ceee97d8..333c8c34bf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -405,6 +405,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (child.IsDirectory) { + if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)) + { + continue; + } + if (IsDvdDirectory(child.FullName, filename, directoryService)) { var movie = new T diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index be1d96bf0b..72c8d7a9d2 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -80,6 +80,7 @@ namespace Emby.Server.Implementations.Library var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); _cache.AddOrUpdate(cacheKey, userData); + item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata UserDataSaved?.Invoke(this, new UserDataSaveEventArgs { @@ -159,7 +160,7 @@ namespace Emby.Server.Implementations.Library }; } - private UserItemData Map(UserData dto) + private static UserItemData Map(UserData dto) { return new UserItemData() { @@ -237,7 +238,10 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public UserItemData? GetUserData(User user, BaseItem item) { - return GetUserData(user, item.Id, item.GetUserDataKeys()); + return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData() + { + Key = item.GetUserDataKeys()[0], + }; } /// <inheritdoc /> @@ -304,7 +308,7 @@ namespace Emby.Server.Implementations.Library // ignore progress during the beginning positionTicks = 0; } - else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks) + else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond)) { // mark as completed close to the end positionTicks = 0; diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index b7fd24fa5c..f9a6f0d19e 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -1,5 +1,5 @@ using System; -using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; @@ -55,6 +55,8 @@ public class PeopleValidator var numPeople = people.Count; + IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2)); + _logger.LogDebug("Will refresh {Amount} people", numPeople); foreach (var person in people) @@ -92,7 +94,7 @@ public class PeopleValidator double percent = numComplete; percent /= numPeople; - progress.Report(100 * percent); + subProgress.Report(100 * percent); } var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery @@ -102,17 +104,13 @@ public class PeopleValidator IsLocked = false }); - foreach (var item in deadEntities) - { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50)); - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + var i = 0; + foreach (var item in deadEntities.Chunk(500)) + { + _libraryManager.DeleteItemsUnsafeFast(item); + subProgress.Report(100f / deadEntities.Count * (i++ * 100)); } progress.Report(100); |
