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

This commit is contained in:
Tim Eisele 2025-03-23 17:05:13 +01:00 committed by GitHub
parent 8b6aec7ce5
commit 8db6a39e92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 170 additions and 92 deletions

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -101,16 +101,23 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext(); using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction(); using var transaction = context.Database.BeginTransaction();
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).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.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); context.TrickplayInfos.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.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
context.SaveChanges(); context.SaveChanges();
transaction.Commit(); transaction.Commit();
} }

View File

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

View File

@ -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);
}