mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-09-29 15:31:04 -04:00
Add People Dedup and multiple progress fixes (#14848)
This commit is contained in:
parent
897975fc57
commit
5a6d9180fe
@ -50,6 +50,8 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
|||||||
|
|
||||||
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
|
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
|
||||||
|
|
||||||
|
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||||
|
|
||||||
foreach (var itemId in itemIds)
|
foreach (var itemId in itemIds)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@ -95,9 +97,10 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
|||||||
numComplete++;
|
numComplete++;
|
||||||
double percent = numComplete;
|
double percent = numComplete;
|
||||||
percent /= numItems;
|
percent /= numItems;
|
||||||
progress.Report(percent * 100);
|
subProgress.Report(percent * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||||
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using (context.ConfigureAwait(false))
|
await using (context.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
@ -105,7 +108,9 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
|||||||
await using (transaction.ConfigureAwait(false))
|
await using (transaction.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
subProgress.Report(50);
|
||||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
subProgress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// 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();
|
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||||
dto.ArtistItems = hasArtist.Artists
|
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
|
||||||
// .Except(foundArtists, new DistinctNameComparer())
|
.Where(e => e.Value.Length > 0)
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
// This should not be necessary but we're seeing some cases of it
|
return new NameGuidPair
|
||||||
if (string.IsNullOrEmpty(i))
|
|
||||||
{
|
{
|
||||||
return null;
|
Name = i.Key,
|
||||||
}
|
Id = i.Value.First().Id
|
||||||
|
};
|
||||||
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;
|
|
||||||
}).Where(i => i is not null).ToArray();
|
}).Where(i => i is not null).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,6 +327,45 @@ namespace Emby.Server.Implementations.Library
|
|||||||
DeleteItem(item, options, parent, notifyParentItem);
|
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)
|
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(item);
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
@ -403,59 +442,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
foreach (var fileSystemInfo in item.GetDeletePaths())
|
foreach (var fileSystemInfo in item.GetDeletePaths())
|
||||||
{
|
{
|
||||||
if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
|
DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isRequiredForDelete = false;
|
isRequiredForDelete = false;
|
||||||
}
|
}
|
||||||
@ -463,17 +450,73 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
item.SetParent(null);
|
item.SetParent(null);
|
||||||
|
|
||||||
_itemRepository.DeleteItem(item.Id);
|
_itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
|
||||||
_cache.TryRemove(item.Id, out _);
|
_cache.TryRemove(item.Id, out _);
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
_itemRepository.DeleteItem(child.Id);
|
|
||||||
_cache.TryRemove(child.Id, out _);
|
_cache.TryRemove(child.Id, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportItemRemoved(item, parent);
|
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)
|
private bool IsInternalItem(BaseItem item)
|
||||||
{
|
{
|
||||||
if (!item.IsFileProtocol)
|
if (!item.IsFileProtocol)
|
||||||
@ -990,6 +1033,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return GetArtist(name, new DtoOptions(true));
|
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)
|
public MusicArtist GetArtist(string name, DtoOptions options)
|
||||||
{
|
{
|
||||||
return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
|
return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
|
||||||
@ -1115,18 +1163,24 @@ namespace Emby.Server.Implementations.Library
|
|||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Quickly scan CollectionFolders for changes
|
// Quickly scan CollectionFolders for changes
|
||||||
|
var toDelete = new List<Guid>();
|
||||||
foreach (var child in rootFolder.Children!.OfType<Folder>())
|
foreach (var child in rootFolder.Children!.OfType<Folder>())
|
||||||
{
|
{
|
||||||
// If the user has somehow deleted the collection directory, remove the metadata from the database.
|
// If the user has somehow deleted the collection directory, remove the metadata from the database.
|
||||||
if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
|
if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
|
||||||
{
|
{
|
||||||
_itemRepository.DeleteItem(collectionFolder.Id);
|
toDelete.Add(collectionFolder.Id);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toDelete.Count > 0)
|
||||||
|
{
|
||||||
|
_itemRepository.DeleteItem(toDelete.ToArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
|
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@ -55,6 +55,8 @@ public class PeopleValidator
|
|||||||
|
|
||||||
var numPeople = people.Count;
|
var numPeople = people.Count;
|
||||||
|
|
||||||
|
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||||
|
|
||||||
_logger.LogDebug("Will refresh {Amount} people", numPeople);
|
_logger.LogDebug("Will refresh {Amount} people", numPeople);
|
||||||
|
|
||||||
foreach (var person in people)
|
foreach (var person in people)
|
||||||
@ -92,7 +94,7 @@ public class PeopleValidator
|
|||||||
double percent = numComplete;
|
double percent = numComplete;
|
||||||
percent /= numPeople;
|
percent /= numPeople;
|
||||||
|
|
||||||
progress.Report(100 * percent);
|
subProgress.Report(100 * percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
@ -102,17 +104,13 @@ public class PeopleValidator
|
|||||||
IsLocked = false
|
IsLocked = false
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var item in deadEntities)
|
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||||
{
|
|
||||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
var i = 0;
|
||||||
item,
|
foreach (var item in deadEntities.Chunk(500))
|
||||||
new DeleteOptions
|
{
|
||||||
{
|
_libraryManager.DeleteItemsUnsafeFast(item);
|
||||||
DeleteFileLocation = false
|
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Database.Implementations;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||||
|
|
||||||
@ -15,16 +19,19 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
|||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
|
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
|
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||||
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization)
|
/// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
|
||||||
|
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -62,8 +69,61 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _libraryManager.ValidatePeopleAsync(progress, cancellationToken);
|
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||||
|
await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
subProgress = new Progress<double>((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<Guid[]>.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<Guid[]>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
subProgress.Report(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,11 +99,11 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void DeleteItem(Guid id)
|
public void DeleteItem(params IReadOnlyList<Guid> 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();
|
using var context = _dbProvider.CreateDbContext();
|
||||||
@ -111,7 +111,7 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
var date = (DateTime?)DateTime.UtcNow;
|
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
|
// 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,
|
// being detached from the item being deleted. This is necessary because, during an update,
|
||||||
@ -2538,4 +2538,16 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
return folderList;
|
return folderList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> 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<MusicArtist>().ToArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,20 +74,34 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
|
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
|
||||||
{
|
{
|
||||||
// TODO: yes for __SOME__ reason there can be duplicates.
|
// multiple metadata providers can provide the _same_ person
|
||||||
people = people.DistinctBy(e => e.Id).ToArray();
|
people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray();
|
||||||
var personids = people.Select(f => f.Id);
|
var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
|
||||||
|
|
||||||
using var context = _dbProvider.CreateDbContext();
|
using var context = _dbProvider.CreateDbContext();
|
||||||
using var transaction = context.Database.BeginTransaction();
|
using var transaction = context.Database.BeginTransaction();
|
||||||
var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray();
|
var existingPersons = context.Peoples.Select(e => new
|
||||||
context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map));
|
{
|
||||||
|
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();
|
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)
|
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)
|
if (existingMap is null)
|
||||||
{
|
{
|
||||||
var sortOrder = (person.SortOrder ?? context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).Max(e => e.SortOrder) ?? 0) + 1;
|
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<JellyfinDbContext> dbProvider, I
|
|||||||
Item = null!,
|
Item = null!,
|
||||||
ItemId = itemId,
|
ItemId = itemId,
|
||||||
People = null!,
|
People = null!,
|
||||||
PeopleId = person.Id,
|
PeopleId = entityPerson.Id,
|
||||||
ListOrder = sortOrder,
|
ListOrder = sortOrder,
|
||||||
SortOrder = sortOrder,
|
SortOrder = sortOrder,
|
||||||
Role = person.Role
|
Role = person.Role
|
||||||
@ -105,11 +119,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// person mapping already exists so remove from list
|
// person mapping already exists so remove from list
|
||||||
maps.Remove(existingMap);
|
existingMaps.Remove(existingMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.PeopleBaseItemMap.RemoveRange(maps);
|
context.PeopleBaseItemMap.RemoveRange(existingMaps);
|
||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
|
@ -337,9 +337,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entity = GetPerson(reader);
|
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))
|
if (reader.TryGetString(2, out var role))
|
||||||
|
@ -336,6 +336,13 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <param name="options">Options to use for deletion.</param>
|
/// <param name="options">Options to use for deletion.</param>
|
||||||
void DeleteItem(BaseItem item, DeleteOptions options);
|
void DeleteItem(BaseItem item, DeleteOptions options);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes items that are not having any children like Actors.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">Items to delete.</param>
|
||||||
|
/// <remarks>In comparison to <see cref="DeleteItem(BaseItem, DeleteOptions, BaseItem, bool)"/> 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.</remarks>
|
||||||
|
public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes the item.
|
/// Deletes the item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -624,6 +631,8 @@ namespace MediaBrowser.Controller.Library
|
|||||||
|
|
||||||
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query);
|
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query);
|
||||||
|
|
||||||
|
IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names);
|
||||||
|
|
||||||
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
|
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
|
||||||
|
|
||||||
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query);
|
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query);
|
||||||
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Database.Implementations.Entities;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
|
|
||||||
@ -22,8 +23,8 @@ public interface IItemRepository
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes the item.
|
/// Deletes the item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The identifier.</param>
|
/// <param name="ids">The identifier to delete.</param>
|
||||||
void DeleteItem(Guid id);
|
void DeleteItem(params IReadOnlyList<Guid> ids);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the items.
|
/// Saves the items.
|
||||||
@ -122,4 +123,11 @@ public interface IItemRepository
|
|||||||
/// <param name="recursive">Whever the check should be done recursive. Warning expensive operation.</param>
|
/// <param name="recursive">Whever the check should be done recursive. Warning expensive operation.</param>
|
||||||
/// <returns>A value indicating whever all children has been played.</returns>
|
/// <returns>A value indicating whever all children has been played.</returns>
|
||||||
bool GetIsPlayed(User user, Guid id, bool recursive);
|
bool GetIsPlayed(User user, Guid id, bool recursive);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all artist matches from the db.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="artistNames">The names of the artists.</param>
|
||||||
|
/// <returns>A map of the artist name and the potential matches.</returns>
|
||||||
|
IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ public class PragmaConnectionInterceptor : DbConnectionInterceptor
|
|||||||
_customPragma = customPragma;
|
_customPragma = customPragma;
|
||||||
|
|
||||||
InitialCommand = BuildCommandText();
|
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; }
|
private string? InitialCommand { get; set; }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user