Переглянути джерело

Remove all DB data on item removal, delete internal trickplay files (#13753)

Tim Eisele 2 місяців тому
батько
коміт
8db6a39e92

+ 53 - 53
Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

@@ -5,80 +5,80 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 
-namespace Emby.Server.Implementations.Data
+namespace Emby.Server.Implementations.Data;
+
+public class CleanDatabaseScheduledTask : ILibraryPostScanTask
 {
-    public class CleanDatabaseScheduledTask : ILibraryPostScanTask
+    private readonly ILibraryManager _libraryManager;
+    private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+    public CleanDatabaseScheduledTask(
+        ILibraryManager libraryManager,
+        ILogger<CleanDatabaseScheduledTask> logger,
+        IDbContextFactory<JellyfinDbContext> dbProvider)
     {
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<CleanDatabaseScheduledTask> _logger;
-        private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _dbProvider = dbProvider;
+    }
 
-        public CleanDatabaseScheduledTask(
-            ILibraryManager libraryManager,
-            ILogger<CleanDatabaseScheduledTask> logger,
-            IDbContextFactory<JellyfinDbContext> dbProvider)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _dbProvider = dbProvider;
-        }
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
+    }
 
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+    {
+        var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
         {
-            await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
-        }
+            HasDeadParentId = true
+        });
 
-        private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
-        {
-            var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
-            {
-                HasDeadParentId = true
-            });
+        var numComplete = 0;
+        var numItems = itemIds.Count + 1;
 
-            var numComplete = 0;
-            var numItems = itemIds.Count + 1;
+        _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
 
-            _logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
+        foreach (var itemId in itemIds)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
 
-            foreach (var itemId in itemIds)
+            var item = _libraryManager.GetItemById(itemId);
+            if (item is not null)
             {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                var item = _libraryManager.GetItemById(itemId);
+                _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
 
-                if (item is not null)
+                _libraryManager.DeleteItem(item, new DeleteOptions
                 {
-                    _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
-
-                    _libraryManager.DeleteItem(item, new DeleteOptions
-                    {
-                        DeleteFileLocation = false
-                    });
-                }
-
-                numComplete++;
-                double percent = numComplete;
-                percent /= numItems;
-                progress.Report(percent * 100);
+                    DeleteFileLocation = false
+                });
             }
 
-            var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
-            await using (context.ConfigureAwait(false))
+            numComplete++;
+            double percent = numComplete;
+            percent /= numItems;
+            progress.Report(percent * 100);
+        }
+
+        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+        await using (context.ConfigureAwait(false))
+        {
+            var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+            await using (transaction.ConfigureAwait(false))
             {
-                var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
-                await using (transaction.ConfigureAwait(false))
-                {
-                    await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
-                    await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
-                }
+                await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+                await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
             }
-
-            progress.Report(100);
         }
+
+        progress.Report(100);
     }
 }

+ 37 - 19
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -78,6 +78,7 @@ namespace Emby.Server.Implementations.Library
         private readonly NamingOptions _namingOptions;
         private readonly IPeopleRepository _peopleRepository;
         private readonly ExtraResolver _extraResolver;
+        private readonly IPathManager _pathManager;
 
         /// <summary>
         /// The _root folder sync lock.
@@ -113,7 +114,8 @@ namespace Emby.Server.Implementations.Library
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="directoryService">The directory service.</param>
-        /// <param name="peopleRepository">The People Repository.</param>
+        /// <param name="peopleRepository">The people repository.</param>
+        /// <param name="pathManager">The path manager.</param>
         public LibraryManager(
             IServerApplicationHost appHost,
             ILoggerFactory loggerFactory,
@@ -130,7 +132,8 @@ namespace Emby.Server.Implementations.Library
             IImageProcessor imageProcessor,
             NamingOptions namingOptions,
             IDirectoryService directoryService,
-            IPeopleRepository peopleRepository)
+            IPeopleRepository peopleRepository,
+            IPathManager pathManager)
         {
             _appHost = appHost;
             _logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -148,6 +151,7 @@ namespace Emby.Server.Implementations.Library
             _cache = new ConcurrentDictionary<Guid, BaseItem>();
             _namingOptions = namingOptions;
             _peopleRepository = peopleRepository;
+            _pathManager = pathManager;
             _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -200,33 +204,33 @@ namespace Emby.Server.Implementations.Library
         /// Gets or sets the postscan tasks.
         /// </summary>
         /// <value>The postscan tasks.</value>
-        private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
+        private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
 
         /// <summary>
         /// Gets or sets the intro providers.
         /// </summary>
         /// <value>The intro providers.</value>
-        private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
+        private IIntroProvider[] IntroProviders { get; set; } = [];
 
         /// <summary>
         /// Gets or sets the list of entity resolution ignore rules.
         /// </summary>
         /// <value>The entity resolution ignore rules.</value>
-        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
+        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 
         /// <summary>
         /// Gets or sets the list of currently registered entity resolvers.
         /// </summary>
         /// <value>The entity resolvers enumerable.</value>
-        private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
+        private IItemResolver[] EntityResolvers { get; set; } = [];
 
-        private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
+        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 
         /// <summary>
         /// Gets or sets the comparers.
         /// </summary>
         /// <value>The comparers.</value>
-        private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
+        private IBaseItemComparer[] Comparers { get; set; } = [];
 
         public bool IsScanRunning { get; private set; }
 
@@ -359,7 +363,7 @@ namespace Emby.Server.Implementations.Library
 
             var children = item.IsFolder
                 ? ((Folder)item).GetRecursiveChildren(false)
-                : Array.Empty<BaseItem>();
+                : [];
 
             foreach (var metadataPath in GetMetadataPaths(item, children))
             {
@@ -465,14 +469,28 @@ namespace Emby.Server.Implementations.Library
             ReportItemRemoved(item, parent);
         }
 
-        private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+        {
+            var list = GetInternalMetadataPaths(item);
+            foreach (var child in children)
+            {
+                list.AddRange(GetInternalMetadataPaths(child));
+            }
+
+            return list;
+        }
+
+        private List<string> GetInternalMetadataPaths(BaseItem item)
         {
             var list = new List<string>
             {
                 item.GetInternalMetadataPath()
             };
 
-            list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
+            if (item is Video video)
+            {
+                list.Add(_pathManager.GetTrickplayDirectory(video));
+            }
 
             return list;
         }
@@ -593,7 +611,7 @@ namespace Emby.Server.Implementations.Library
                     {
                         _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
 
-                        files = Array.Empty<FileSystemMetadata>();
+                        files = [];
                     }
                     else
                     {
@@ -1463,7 +1481,7 @@ namespace Emby.Server.Implementations.Library
 
             // Optimize by querying against top level views
             query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
-            query.AncestorIds = Array.Empty<Guid>();
+            query.AncestorIds = [];
 
             // Prevent searching in all libraries due to empty filter
             if (query.TopParentIds.Length == 0)
@@ -1583,7 +1601,7 @@ namespace Emby.Server.Implementations.Library
                         return GetTopParentIdsForQuery(displayParent, user);
                     }
 
-                    return Array.Empty<Guid>();
+                    return [];
                 }
 
                 if (!view.ParentId.IsEmpty())
@@ -1594,7 +1612,7 @@ namespace Emby.Server.Implementations.Library
                         return GetTopParentIdsForQuery(displayParent, user);
                     }
 
-                    return Array.Empty<Guid>();
+                    return [];
                 }
 
                 // Handle grouping
@@ -1609,7 +1627,7 @@ namespace Emby.Server.Implementations.Library
                         .SelectMany(i => GetTopParentIdsForQuery(i, user));
                 }
 
-                return Array.Empty<Guid>();
+                return [];
             }
 
             if (item is CollectionFolder collectionFolder)
@@ -1623,7 +1641,7 @@ namespace Emby.Server.Implementations.Library
                 return new[] { topParent.Id };
             }
 
-            return Array.Empty<Guid>();
+            return [];
         }
 
         /// <summary>
@@ -1667,7 +1685,7 @@ namespace Emby.Server.Implementations.Library
             {
                 _logger.LogError(ex, "Error getting intros");
 
-                return Enumerable.Empty<IntroInfo>();
+                return [];
             }
         }
 
@@ -2894,7 +2912,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
 
-                    await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
+                    await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
                 }
 
                 CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);

+ 36 - 0
Emby.Server.Implementations/Library/PathManager.cs

@@ -0,0 +1,36 @@
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IPathManager implementation.
+/// </summary>
+public class PathManager : IPathManager
+{
+    private readonly IServerConfigurationManager _config;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PathManager"/> class.
+    /// </summary>
+    /// <param name="config">The server configuration manager.</param>
+    public PathManager(
+        IServerConfigurationManager config)
+    {
+        _config = config;
+    }
+
+    /// <inheritdoc />
+    public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
+    {
+        var basePath = _config.ApplicationPaths.TrickplayPath;
+        var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+
+        return saveWithMedia
+            ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+            : Path.Combine(basePath, idString);
+    }
+}

+ 13 - 6
Jellyfin.Server.Implementations/Item/BaseItemRepository.cs

@@ -101,16 +101,23 @@ public sealed class BaseItemRepository
 
         using var context = _dbProvider.CreateDbContext();
         using var transaction = context.Database.BeginTransaction();
-        context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
-        context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
-        context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
-        context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
         context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
-        context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
-        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
+        context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
         context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
+        context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
         context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
+        context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
         context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
+        context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
+        context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
+        context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
+        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
+        context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
+        context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
+        context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
+        context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
+        context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
+        context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
         context.SaveChanges();
         transaction.Commit();
     }

+ 8 - 8
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs

@@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Trickplay;
@@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager
     private readonly IImageEncoder _imageEncoder;
     private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
     private readonly IApplicationPaths _appPaths;
+    private readonly IPathManager _pathManager;
 
     private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
-    private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+    private static readonly string[] _trickplayImgExtensions = [".jpg"];
 
     /// <summary>
     /// Initializes a new instance of the <see cref="TrickplayManager"/> class.
@@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager
     /// <param name="imageEncoder">The image encoder.</param>
     /// <param name="dbProvider">The database provider.</param>
     /// <param name="appPaths">The application paths.</param>
+    /// <param name="pathManager">The path manager.</param>
     public TrickplayManager(
         ILogger<TrickplayManager> logger,
         IMediaEncoder mediaEncoder,
@@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager
         IServerConfigurationManager config,
         IImageEncoder imageEncoder,
         IDbContextFactory<JellyfinDbContext> dbProvider,
-        IApplicationPaths appPaths)
+        IApplicationPaths appPaths,
+        IPathManager pathManager)
     {
         _logger = logger;
         _mediaEncoder = mediaEncoder;
@@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager
         _imageEncoder = imageEncoder;
         _dbProvider = dbProvider;
         _appPaths = appPaths;
+        _pathManager = pathManager;
     }
 
     /// <inheritdoc />
@@ -610,12 +615,7 @@ public class TrickplayManager : ITrickplayManager
     /// <inheritdoc />
     public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
     {
-        var basePath = _config.ApplicationPaths.TrickplayPath;
-        var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
-        var path = saveWithMedia
-            ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
-            : Path.Combine(basePath, idString);
-
+        var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia);
         var subdirectory = string.Format(
             CultureInfo.InvariantCulture,
             "{0} - {1}x{2}",

+ 17 - 0
MediaBrowser.Controller/IO/IPathManager.cs

@@ -0,0 +1,17 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Interface ITrickplayManager.
+/// </summary>
+public interface IPathManager
+{
+    /// <summary>
+    /// Gets the path to the trickplay image base folder.
+    /// </summary>
+    /// <param name="item">The item.</param>
+    /// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
+    /// <returns>The absolute path.</returns>
+    public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false);
+}