From 5a6d9180fed81a30cb91ef3fed30176cd4402116 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 25 Sep 2025 00:20:30 +0300 Subject: [PATCH] Add People Dedup and multiple progress fixes (#14848) --- .../Data/CleanDatabaseScheduledTask.cs | 7 +- Emby.Server.Implementations/Dto/DtoService.cs | 27 +-- .../Library/LibraryManager.cs | 166 ++++++++++++------ .../Library/Validators/PeopleValidator.cs | 22 ++- .../Tasks/PeopleValidationTask.cs | 66 ++++++- .../Item/BaseItemRepository.cs | 20 ++- .../Item/PeopleRepository.cs | 34 ++-- .../Migrations/Routines/MigrateLibraryDb.cs | 4 +- .../Library/ILibraryManager.cs | 9 + .../Persistence/IItemRepository.cs | 12 +- .../PragmaConnectionInterceptor.cs | 2 +- 11 files changed, 257 insertions(+), 112 deletions(-) diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 31ae82d6a3..676bb7f816 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -50,6 +50,8 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask _logger.LogDebug("Cleaning {Number} items with dead parents", numItems); + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + foreach (var itemId in itemIds) { cancellationToken.ThrowIfCancellationRequested(); @@ -95,9 +97,10 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask numComplete++; double percent = numComplete; percent /= numItems; - progress.Report(percent * 100); + subProgress.Report(percent * 100); } + subProgress = new Progress((val) => progress.Report((val / 2) + 50)); var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { @@ -105,7 +108,9 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask await using (transaction.ConfigureAwait(false)) { await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(50); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0db1606ea5..c5dc3b054c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1051,30 +1051,15 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = hasArtist.Artists - // .Except(foundArtists, new DistinctNameComparer()) + dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) + .Where(e => e.Value.Length > 0) .Select(i => { - // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrEmpty(i)) + return new NameGuidPair { - return null; - } - - var artist = _libraryManager.GetArtist(i, new DtoOptions(false) - { - EnableImages = false - }); - if (artist is not null) - { - return new NameGuidPair - { - Name = artist.Name, - Id = artist.Id - }; - } - - return null; + Name = i.Key, + Id = i.Value.First().Id + }; }).Where(i => i is not null).ToArray(); } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a66835dec0..102779729e 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 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) @@ -990,6 +1033,11 @@ namespace Emby.Server.Implementations.Library return GetArtist(name, new DtoOptions(true)); } + public IReadOnlyDictionary GetArtists(IReadOnlyList names) + { + return _itemRepository.FindArtists(names); + } + public MusicArtist GetArtist(string name, DtoOptions options) { return CreateItemByName(MusicArtist.GetPath, name, options); @@ -1115,18 +1163,24 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes + var toDelete = new List(); foreach (var child in rootFolder.Children!.OfType()) { // 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 progress, CancellationToken cancellationToken) 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 subProgress = new Progress((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((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); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 18162ad2fc..6e4e5c7808 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -1,10 +1,14 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; +using Microsoft.EntityFrameworkCore; namespace Emby.Server.Implementations.ScheduledTasks.Tasks; @@ -15,16 +19,19 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask { private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; + private readonly IDbContextFactory _dbContextFactory; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization) + /// Instance of the interface. + public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory dbContextFactory) { _libraryManager = libraryManager; _localization = localization; + _dbContextFactory = dbContextFactory; } /// @@ -62,8 +69,61 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask } /// - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - return _libraryManager.ValidatePeopleAsync(progress, cancellationToken); + IProgress subProgress = new Progress((val) => progress.Report(val / 2)); + await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false); + + subProgress = new Progress((val) => progress.Report((val / 2) + 50)); + var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var dupQuery = context.Peoples + .GroupBy(e => new { e.Name, e.PersonType }) + .Where(e => e.Count() > 1) + .Select(e => e.Select(f => f.Id).ToArray()); + + var total = dupQuery.Count(); + + const int PartitionSize = 100; + var iterator = 0; + int itemCounter; + var buffer = ArrayPool.Shared.Rent(PartitionSize)!; + try + { + do + { + itemCounter = 0; + await foreach (var item in dupQuery + .Take(PartitionSize) + .AsAsyncEnumerable() + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + buffer[itemCounter++] = item; + } + + for (int i = 0; i < itemCounter; i++) + { + var item = buffer[i]; + var reference = item[0]; + var dups = item[1..]; + await context.PeopleBaseItemMap.WhereOneOrMany(dups, e => e.PeopleId) + .ExecuteUpdateAsync(e => e.SetProperty(f => f.PeopleId, reference), cancellationToken) + .ConfigureAwait(false); + await context.Peoples.Where(e => dups.Contains(e.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100f / total * ((iterator * PartitionSize) + i)); + } + + iterator++; + } while (itemCounter == PartitionSize && !cancellationToken.IsCancellationRequested); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + subProgress.Report(100); + } } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index a34e95c4de..68260fbf0e 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -99,11 +99,11 @@ public sealed class BaseItemRepository } /// - public void DeleteItem(Guid id) + public void DeleteItem(params IReadOnlyList ids) { - if (id.IsEmpty() || id.Equals(PlaceholderId)) + if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId))) { - throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id)); + throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids)); } using var context = _dbProvider.CreateDbContext(); @@ -111,7 +111,7 @@ public sealed class BaseItemRepository var date = (DateTime?)DateTime.UtcNow; - var relatedItems = TraverseHirachyDown(id, context).ToArray(); + var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray(); // Remove any UserData entries for the placeholder item that would conflict with the UserData // being detached from the item being deleted. This is necessary because, during an update, @@ -2538,4 +2538,16 @@ public sealed class BaseItemRepository return folderList; } + + /// + public IReadOnlyDictionary FindArtists(IReadOnlyList artistNames) + { + using var dbContext = _dbProvider.CreateDbContext(); + + var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!) + .Where(e => artistNames.Contains(e.Name)) + .ToArray(); + + return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast().ToArray()); + } } diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 24afaea550..0f423cf5d3 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -74,20 +74,34 @@ public class PeopleRepository(IDbContextFactory dbProvider, I /// public void UpdatePeople(Guid itemId, IReadOnlyList people) { - // TODO: yes for __SOME__ reason there can be duplicates. - people = people.DistinctBy(e => e.Id).ToArray(); - var personids = people.Select(f => f.Id); + // multiple metadata providers can provide the _same_ person + people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray(); + var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray(); using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray(); - context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map)); + var existingPersons = context.Peoples.Select(e => new + { + item = e, + SelectionKey = e.Name + "-" + e.PersonType + }) + .Where(p => personKeys.Contains(p.SelectionKey)) + .Select(f => f.item) + .ToArray(); + + var toAdd = people + .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString())) + .Select(Map); + context.Peoples.AddRange(toAdd); context.SaveChanges(); - var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList(); + var personsEntities = toAdd.Concat(existingPersons).ToArray(); + + var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList(); foreach (var person in people) { - var existingMap = maps.FirstOrDefault(e => e.PeopleId == person.Id); + var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString()); + var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.Role == person.Role); if (existingMap is null) { var sortOrder = (person.SortOrder ?? context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).Max(e => e.SortOrder) ?? 0) + 1; @@ -96,7 +110,7 @@ public class PeopleRepository(IDbContextFactory dbProvider, I Item = null!, ItemId = itemId, People = null!, - PeopleId = person.Id, + PeopleId = entityPerson.Id, ListOrder = sortOrder, SortOrder = sortOrder, Role = person.Role @@ -105,11 +119,11 @@ public class PeopleRepository(IDbContextFactory dbProvider, I else { // person mapping already exists so remove from list - maps.Remove(existingMap); + existingMaps.Remove(existingMap); } } - context.PeopleBaseItemMap.RemoveRange(maps); + context.PeopleBaseItemMap.RemoveRange(existingMaps); context.SaveChanges(); transaction.Commit(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index ca8e1054e5..b8f416a766 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -337,9 +337,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine } var entity = GetPerson(reader); - if (!peopleCache.TryGetValue(entity.Name, out var personCache)) + if (!peopleCache.TryGetValue(entity.Name + "|" + entity.PersonType, out var personCache)) { - peopleCache[entity.Name] = personCache = (entity, []); + peopleCache[entity.Name + "|" + entity.PersonType] = personCache = (entity, []); } if (reader.TryGetString(2, out var role)) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index b72d1d0b4c..fcc5ed672a 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -336,6 +336,13 @@ namespace MediaBrowser.Controller.Library /// Options to use for deletion. void DeleteItem(BaseItem item, DeleteOptions options); + /// + /// Deletes items that are not having any children like Actors. + /// + /// Items to delete. + /// In comparison to this method skips a lot of steps assuming there are no children to recusively delete nor does it define the special handling for channels and alike. + public void DeleteItemsUnsafeFast(IEnumerable items); + /// /// Deletes the item. /// @@ -624,6 +631,8 @@ namespace MediaBrowser.Controller.Library QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query); + IReadOnlyDictionary GetArtists(IReadOnlyList names); + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query); QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query); diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index e17dc38f7f..0026ab2b5f 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -22,8 +23,8 @@ public interface IItemRepository /// /// Deletes the item. /// - /// The identifier. - void DeleteItem(Guid id); + /// The identifier to delete. + void DeleteItem(params IReadOnlyList ids); /// /// Saves the items. @@ -122,4 +123,11 @@ public interface IItemRepository /// Whever the check should be done recursive. Warning expensive operation. /// A value indicating whever all children has been played. bool GetIsPlayed(User user, Guid id, bool recursive); + + /// + /// Gets all artist matches from the db. + /// + /// The names of the artists. + /// A map of the artist name and the potential matches. + IReadOnlyDictionary FindArtists(IReadOnlyList artistNames); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs index 47e44d97b9..fd2b9bd05b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/PragmaConnectionInterceptor.cs @@ -43,7 +43,7 @@ public class PragmaConnectionInterceptor : DbConnectionInterceptor _customPragma = customPragma; InitialCommand = BuildCommandText(); - _logger.LogInformation("SQLITE connection pragma command set to: \r\n {PragmaCommand}", InitialCommand); + _logger.LogInformation("SQLITE connection pragma command set to: \r\n{PragmaCommand}", InitialCommand); } private string? InitialCommand { get; set; }