aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library
diff options
context:
space:
mode:
authorpokreman06 <112423673+pokreman06@users.noreply.github.com>2025-10-02 11:07:05 -0600
committerGitHub <noreply@github.com>2025-10-02 11:07:05 -0600
commit0b4854c5eff7c862d05f43048e08dd3a1a25efaa (patch)
treea4c417af05deef7878ab9342c85c506ad22e1ced /Emby.Server.Implementations/Library
parentd6a1c8413c6a213f6e579246c1b85aad9b028b3a (diff)
parent0f42aa892e0a7fe2ac4e680e7647515af0909e5e (diff)
Merge branch 'jellyfin:master' into master
Diffstat (limited to 'Emby.Server.Implementations/Library')
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs10
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs7
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs185
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs13
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs10
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs22
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);