mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-03 23:36:38 +01:00
665 lines
28 KiB
C#
665 lines
28 KiB
C#
#pragma warning disable RS0030 // Do not use banned APIs
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Database.Implementations;
|
|
using Jellyfin.Database.Implementations.Entities;
|
|
using Jellyfin.Extensions;
|
|
using MediaBrowser.Controller;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Controller.Entities.Audio;
|
|
using MediaBrowser.Controller.Persistence;
|
|
using MediaBrowser.Controller.Playlists;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
|
|
using DbLinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType;
|
|
using LinkedChildType = MediaBrowser.Controller.Entities.LinkedChildType;
|
|
|
|
namespace Jellyfin.Server.Implementations.Item;
|
|
|
|
/// <summary>
|
|
/// Handles item persistence operations (save, delete, update).
|
|
/// </summary>
|
|
public class ItemPersistenceService : IItemPersistenceService
|
|
{
|
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
|
private readonly IServerApplicationHost _appHost;
|
|
private readonly ILogger<ItemPersistenceService> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ItemPersistenceService"/> class.
|
|
/// </summary>
|
|
/// <param name="dbProvider">The database context factory.</param>
|
|
/// <param name="appHost">The application host.</param>
|
|
/// <param name="logger">The logger.</param>
|
|
public ItemPersistenceService(
|
|
IDbContextFactory<JellyfinDbContext> dbProvider,
|
|
IServerApplicationHost appHost,
|
|
ILogger<ItemPersistenceService> logger)
|
|
{
|
|
_dbProvider = dbProvider;
|
|
_appHost = appHost;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void DeleteItem(params IReadOnlyList<Guid> ids)
|
|
{
|
|
if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(BaseItemRepository.PlaceholderId)))
|
|
{
|
|
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
|
|
}
|
|
|
|
using var context = _dbProvider.CreateDbContext();
|
|
using var transaction = context.Database.BeginTransaction();
|
|
|
|
var date = (DateTime?)DateTime.UtcNow;
|
|
|
|
var descendantIds = DescendantQueryHelper.GetOwnedDescendantIdsBatch(context, ids);
|
|
foreach (var id in ids)
|
|
{
|
|
descendantIds.Add(id);
|
|
}
|
|
|
|
var extraIds = context.BaseItems
|
|
.Where(e => e.OwnerId.HasValue && descendantIds.Contains(e.OwnerId.Value))
|
|
.Select(e => e.Id)
|
|
.ToArray();
|
|
|
|
foreach (var extraId in extraIds)
|
|
{
|
|
descendantIds.Add(extraId);
|
|
}
|
|
|
|
var relatedItems = descendantIds.ToArray();
|
|
|
|
// When batch-deleting, multiple items may have UserData for the same (UserId, CustomDataKey).
|
|
// Moving all of them to PlaceholderId would violate the UNIQUE constraint.
|
|
// Deduplicate by loading keys client-side, keeping the best row per group.
|
|
var batchUserData = context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId);
|
|
|
|
var allRows = batchUserData
|
|
.Select(ud => new { ud.ItemId, ud.UserId, ud.CustomDataKey, ud.LastPlayedDate, ud.PlayCount })
|
|
.ToList();
|
|
|
|
var duplicateRows = allRows
|
|
.GroupBy(ud => new { ud.UserId, ud.CustomDataKey })
|
|
.Where(g => g.Count() > 1)
|
|
.SelectMany(g => g
|
|
.OrderByDescending(ud => ud.LastPlayedDate)
|
|
.ThenByDescending(ud => ud.PlayCount)
|
|
.Skip(1))
|
|
.ToList();
|
|
|
|
foreach (var dup in duplicateRows)
|
|
{
|
|
context.UserData
|
|
.Where(ud => ud.ItemId == dup.ItemId && ud.UserId == dup.UserId && ud.CustomDataKey == dup.CustomDataKey)
|
|
.ExecuteDelete();
|
|
}
|
|
|
|
// Delete existing placeholder rows that would conflict with the incoming ones
|
|
context.UserData
|
|
.Join(
|
|
batchUserData,
|
|
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
|
|
userData => new { userData.UserId, userData.CustomDataKey },
|
|
(placeholder, userData) => placeholder)
|
|
.Where(e => e.ItemId == BaseItemRepository.PlaceholderId)
|
|
.ExecuteDelete();
|
|
|
|
batchUserData
|
|
.ExecuteUpdate(e => e
|
|
.SetProperty(f => f.RetentionDate, date)
|
|
.SetProperty(f => f.ItemId, BaseItemRepository.PlaceholderId));
|
|
|
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
|
|
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
|
|
context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.LinkedChildren.WhereOneOrMany(relatedItems, e => e.ParentId).ExecuteDelete();
|
|
context.LinkedChildren.WhereOneOrMany(relatedItems, e => e.ChildId).ExecuteDelete();
|
|
context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
|
|
context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray();
|
|
context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
|
|
context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.SaveChanges();
|
|
transaction.Commit();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void UpdateInheritedValues()
|
|
{
|
|
using var context = _dbProvider.CreateDbContext();
|
|
using var transaction = context.Database.BeginTransaction();
|
|
|
|
context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
|
|
context.SaveChanges();
|
|
|
|
transaction.Commit();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
|
|
{
|
|
UpdateOrInsertItems(items, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(item);
|
|
|
|
var images = item.ImageInfos.Select(e => BaseItemMapper.MapImageToEntity(item.Id, e)).ToArray();
|
|
|
|
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
await using (context.ConfigureAwait(false))
|
|
{
|
|
if (!await context.BaseItems
|
|
.AnyAsync(bi => bi.Id == item.Id, cancellationToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
|
|
return;
|
|
}
|
|
|
|
await context.BaseItemImageInfos
|
|
.Where(e => e.ItemId == item.Id)
|
|
.ExecuteDeleteAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
await context.BaseItemImageInfos
|
|
.AddRangeAsync(images, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(item);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
await using (dbContext.ConfigureAwait(false))
|
|
{
|
|
var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
|
await using (transaction.ConfigureAwait(false))
|
|
{
|
|
var userKeys = item.GetUserDataKeys().ToArray();
|
|
var retentionDate = (DateTime?)null;
|
|
|
|
await dbContext.UserData
|
|
.Where(e => e.ItemId == BaseItemRepository.PlaceholderId)
|
|
.Where(e => userKeys.Contains(e.CustomDataKey))
|
|
.ExecuteUpdateAsync(
|
|
e => e
|
|
.SetProperty(f => f.ItemId, item.Id)
|
|
.SetProperty(f => f.RetentionDate, retentionDate),
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
item.UserData = await dbContext.UserData
|
|
.AsNoTracking()
|
|
.Where(e => e.ItemId == item.Id)
|
|
.ToArrayAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(items);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
|
|
foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != BaseItemRepository.PlaceholderId))
|
|
{
|
|
var ancestorIds = item.SupportsAncestors ?
|
|
item.GetAncestorIds().Distinct().ToList() :
|
|
null;
|
|
|
|
var topParent = item.GetTopParent();
|
|
|
|
var userdataKey = item.GetUserDataKeys();
|
|
var inheritedTags = item.GetInheritedTags();
|
|
|
|
tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
|
|
}
|
|
|
|
using var context = _dbProvider.CreateDbContext();
|
|
using var transaction = context.Database.BeginTransaction();
|
|
|
|
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
|
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
|
|
|
foreach (var item in tuples)
|
|
{
|
|
var entity = BaseItemMapper.Map(item.Item, _appHost);
|
|
entity.TopParentId = item.TopParent?.Id;
|
|
|
|
if (!existingItems.Any(e => e == entity.Id))
|
|
{
|
|
context.BaseItems.Add(entity);
|
|
}
|
|
else
|
|
{
|
|
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
|
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
|
context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
|
|
|
if (entity.Images is { Count: > 0 })
|
|
{
|
|
context.BaseItemImageInfos.AddRange(entity.Images);
|
|
}
|
|
|
|
if (entity.LockedFields is { Count: > 0 })
|
|
{
|
|
context.BaseItemMetadataFields.AddRange(entity.LockedFields);
|
|
}
|
|
|
|
context.BaseItems.Attach(entity).State = EntityState.Modified;
|
|
}
|
|
}
|
|
|
|
var itemValueMaps = tuples
|
|
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
|
.ToArray();
|
|
var allListedItemValues = itemValueMaps
|
|
.SelectMany(f => f.Values)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
var types = allListedItemValues.Select(e => e.MagicNumber).Distinct().ToArray();
|
|
var values = allListedItemValues.Select(e => e.Value).Distinct().ToArray();
|
|
var allListedItemValuesSet = allListedItemValues.ToHashSet();
|
|
|
|
var existingValues = context.ItemValues
|
|
.Where(e => types.Contains(e.Type) && values.Contains(e.Value))
|
|
.AsEnumerable()
|
|
.Where(e => allListedItemValuesSet.Contains((e.Type, e.Value)))
|
|
.ToArray();
|
|
var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
|
|
{
|
|
CleanValue = f.Value.GetCleanValue(),
|
|
ItemValueId = Guid.NewGuid(),
|
|
Type = f.MagicNumber,
|
|
Value = f.Value
|
|
}).ToArray();
|
|
context.ItemValues.AddRange(missingItemValues);
|
|
|
|
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
|
|
var valueMap = itemValueMaps
|
|
.Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
|
|
.ToArray();
|
|
|
|
var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
|
|
|
|
foreach (var item in valueMap)
|
|
{
|
|
var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
|
|
foreach (var itemValue in item.Values)
|
|
{
|
|
var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
|
|
if (existingItem is null)
|
|
{
|
|
context.ItemValuesMap.Add(new ItemValueMap()
|
|
{
|
|
Item = null!,
|
|
ItemId = item.Item.Id,
|
|
ItemValue = null!,
|
|
ItemValueId = itemValue.ItemValueId
|
|
});
|
|
}
|
|
else
|
|
{
|
|
itemMappedValues.Remove(existingItem);
|
|
}
|
|
}
|
|
|
|
context.ItemValuesMap.RemoveRange(itemMappedValues);
|
|
}
|
|
|
|
var itemsWithAncestors = tuples
|
|
.Where(t => t.Item.SupportsAncestors && t.AncestorIds != null)
|
|
.Select(t => t.Item.Id)
|
|
.ToList();
|
|
|
|
var allExistingAncestorIds = itemsWithAncestors.Count > 0
|
|
? context.AncestorIds
|
|
.Where(e => itemsWithAncestors.Contains(e.ItemId))
|
|
.ToList()
|
|
.GroupBy(e => e.ItemId)
|
|
.ToDictionary(g => g.Key, g => g.ToList())
|
|
: new Dictionary<Guid, List<AncestorId>>();
|
|
|
|
var allRequestedAncestorIds = tuples
|
|
.Where(t => t.Item.SupportsAncestors && t.AncestorIds != null)
|
|
.SelectMany(t => t.AncestorIds!)
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
var validAncestorIdsSet = allRequestedAncestorIds.Count > 0
|
|
? context.BaseItems
|
|
.Where(e => allRequestedAncestorIds.Contains(e.Id))
|
|
.Select(f => f.Id)
|
|
.ToHashSet()
|
|
: new HashSet<Guid>();
|
|
|
|
foreach (var item in tuples)
|
|
{
|
|
if (item.Item.SupportsAncestors && item.AncestorIds != null)
|
|
{
|
|
var existingAncestorIds = allExistingAncestorIds.GetValueOrDefault(item.Item.Id) ?? new List<AncestorId>();
|
|
var validAncestorIds = item.AncestorIds.Where(id => validAncestorIdsSet.Contains(id)).ToArray();
|
|
foreach (var ancestorId in validAncestorIds)
|
|
{
|
|
var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
|
|
if (existingAncestorId is null)
|
|
{
|
|
context.AncestorIds.Add(new AncestorId()
|
|
{
|
|
ParentItemId = ancestorId,
|
|
ItemId = item.Item.Id,
|
|
Item = null!,
|
|
ParentItem = null!
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existingAncestorIds.Remove(existingAncestorId);
|
|
}
|
|
}
|
|
|
|
context.AncestorIds.RemoveRange(existingAncestorIds);
|
|
}
|
|
}
|
|
|
|
context.SaveChanges();
|
|
|
|
var folderIds = tuples
|
|
.Where(t => t.Item is Folder)
|
|
.Select(t => t.Item.Id)
|
|
.ToList();
|
|
|
|
var videoIds = tuples
|
|
.Where(t => t.Item is Video)
|
|
.Select(t => t.Item.Id)
|
|
.ToList();
|
|
|
|
var allLinkedChildrenByParent = new Dictionary<Guid, List<LinkedChildEntity>>();
|
|
if (folderIds.Count > 0 || videoIds.Count > 0)
|
|
{
|
|
var allParentIds = folderIds.Concat(videoIds).Distinct().ToList();
|
|
var allLinkedChildren = context.LinkedChildren
|
|
.Where(e => allParentIds.Contains(e.ParentId))
|
|
.ToList();
|
|
|
|
allLinkedChildrenByParent = allLinkedChildren
|
|
.GroupBy(e => e.ParentId)
|
|
.ToDictionary(g => g.Key, g => g.ToList());
|
|
}
|
|
|
|
foreach (var item in tuples)
|
|
{
|
|
if (item.Item is Folder folder)
|
|
{
|
|
var existingLinkedChildren = allLinkedChildrenByParent.GetValueOrDefault(item.Item.Id)?.ToList() ?? new List<LinkedChildEntity>();
|
|
if (folder.LinkedChildren.Length > 0)
|
|
{
|
|
#pragma warning disable CS0618 // Type or member is obsolete - legacy path resolution for old data
|
|
var pathsToResolve = folder.LinkedChildren
|
|
.Where(lc => (!lc.ItemId.HasValue || lc.ItemId.Value.IsEmpty()) && !string.IsNullOrEmpty(lc.Path))
|
|
.Select(lc => lc.Path)
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
var pathToIdMap = pathsToResolve.Count > 0
|
|
? context.BaseItems
|
|
.Where(e => e.Path != null && pathsToResolve.Contains(e.Path))
|
|
.Select(e => new { e.Path, e.Id })
|
|
.GroupBy(e => e.Path!)
|
|
.ToDictionary(g => g.Key, g => g.First().Id)
|
|
: [];
|
|
|
|
var resolvedChildren = new List<(LinkedChild Child, Guid ChildId)>();
|
|
foreach (var linkedChild in folder.LinkedChildren)
|
|
{
|
|
var childItemId = linkedChild.ItemId;
|
|
if (!childItemId.HasValue || childItemId.Value.IsEmpty())
|
|
{
|
|
if (!string.IsNullOrEmpty(linkedChild.Path) && pathToIdMap.TryGetValue(linkedChild.Path, out var resolvedId))
|
|
{
|
|
childItemId = resolvedId;
|
|
}
|
|
}
|
|
#pragma warning restore CS0618
|
|
|
|
if (childItemId.HasValue && !childItemId.Value.IsEmpty())
|
|
{
|
|
resolvedChildren.Add((linkedChild, childItemId.Value));
|
|
}
|
|
}
|
|
|
|
resolvedChildren = resolvedChildren
|
|
.GroupBy(c => c.ChildId)
|
|
.Select(g => g.Last())
|
|
.ToList();
|
|
|
|
var childIdsToCheck = resolvedChildren.Select(c => c.ChildId).ToList();
|
|
var existingChildIds = childIdsToCheck.Count > 0
|
|
? context.BaseItems
|
|
.Where(e => childIdsToCheck.Contains(e.Id))
|
|
.Select(e => e.Id)
|
|
.ToHashSet()
|
|
: [];
|
|
|
|
var isPlaylist = folder is Playlist;
|
|
var sortOrder = 0;
|
|
foreach (var (linkedChild, childId) in resolvedChildren)
|
|
{
|
|
if (!existingChildIds.Contains(childId))
|
|
{
|
|
_logger.LogWarning(
|
|
"Skipping LinkedChild for parent {ParentName} ({ParentId}): child item {ChildId} does not exist in database",
|
|
item.Item.Name,
|
|
item.Item.Id,
|
|
childId);
|
|
continue;
|
|
}
|
|
|
|
var existingLink = existingLinkedChildren.FirstOrDefault(e => e.ChildId == childId);
|
|
if (existingLink is null)
|
|
{
|
|
context.LinkedChildren.Add(new LinkedChildEntity()
|
|
{
|
|
ParentId = item.Item.Id,
|
|
ChildId = childId,
|
|
ChildType = (DbLinkedChildType)linkedChild.Type,
|
|
SortOrder = isPlaylist ? sortOrder : null
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existingLink.SortOrder = isPlaylist ? sortOrder : null;
|
|
existingLink.ChildType = (DbLinkedChildType)linkedChild.Type;
|
|
existingLinkedChildren.Remove(existingLink);
|
|
}
|
|
|
|
sortOrder++;
|
|
}
|
|
}
|
|
|
|
if (existingLinkedChildren.Count > 0)
|
|
{
|
|
context.LinkedChildren.RemoveRange(existingLinkedChildren);
|
|
}
|
|
}
|
|
|
|
if (item.Item is Video video)
|
|
{
|
|
var existingLinkedChildren = (allLinkedChildrenByParent.GetValueOrDefault(video.Id) ?? new List<LinkedChildEntity>())
|
|
.Where(e => (int)e.ChildType == 2 || (int)e.ChildType == 3)
|
|
.ToList();
|
|
|
|
var newLinkedChildren = new List<(Guid ChildId, LinkedChildType Type)>();
|
|
|
|
if (video.LocalAlternateVersions.Length > 0)
|
|
{
|
|
var pathsToResolve = video.LocalAlternateVersions.Where(p => !string.IsNullOrEmpty(p)).ToList();
|
|
if (pathsToResolve.Count > 0)
|
|
{
|
|
var pathToIdMap = context.BaseItems
|
|
.Where(e => e.Path != null && pathsToResolve.Contains(e.Path))
|
|
.Select(e => new { e.Path, e.Id })
|
|
.GroupBy(e => e.Path!)
|
|
.ToDictionary(g => g.Key, g => g.First().Id);
|
|
|
|
foreach (var path in pathsToResolve)
|
|
{
|
|
if (pathToIdMap.TryGetValue(path, out var childId))
|
|
{
|
|
newLinkedChildren.Add((childId, LinkedChildType.LocalAlternateVersion));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (video.LinkedAlternateVersions.Length > 0)
|
|
{
|
|
foreach (var linkedChild in video.LinkedAlternateVersions)
|
|
{
|
|
if (linkedChild.ItemId.HasValue && !linkedChild.ItemId.Value.IsEmpty())
|
|
{
|
|
newLinkedChildren.Add((linkedChild.ItemId.Value, LinkedChildType.LinkedAlternateVersion));
|
|
}
|
|
}
|
|
}
|
|
|
|
newLinkedChildren = newLinkedChildren
|
|
.GroupBy(c => c.ChildId)
|
|
.Select(g => g.Last())
|
|
.ToList();
|
|
|
|
var childIdsToCheck = newLinkedChildren.Select(c => c.ChildId).ToList();
|
|
var existingChildIds = childIdsToCheck.Count > 0
|
|
? context.BaseItems
|
|
.Where(e => childIdsToCheck.Contains(e.Id))
|
|
.Select(e => e.Id)
|
|
.ToHashSet()
|
|
: [];
|
|
|
|
int sortOrder = 0;
|
|
foreach (var (childId, childType) in newLinkedChildren)
|
|
{
|
|
if (!existingChildIds.Contains(childId))
|
|
{
|
|
_logger.LogWarning(
|
|
"Skipping alternate version for video {VideoName} ({VideoId}): child item {ChildId} does not exist in database",
|
|
video.Name,
|
|
video.Id,
|
|
childId);
|
|
continue;
|
|
}
|
|
|
|
var existingLink = existingLinkedChildren.FirstOrDefault(e => e.ChildId == childId);
|
|
if (existingLink is null)
|
|
{
|
|
context.LinkedChildren.Add(new LinkedChildEntity
|
|
{
|
|
ParentId = video.Id,
|
|
ChildId = childId,
|
|
ChildType = (DbLinkedChildType)childType,
|
|
SortOrder = sortOrder
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existingLink.ChildType = (DbLinkedChildType)childType;
|
|
existingLink.SortOrder = sortOrder;
|
|
existingLinkedChildren.Remove(existingLink);
|
|
}
|
|
|
|
sortOrder++;
|
|
}
|
|
|
|
if (existingLinkedChildren.Count > 0)
|
|
{
|
|
var orphanedLocalVersionIds = existingLinkedChildren
|
|
.Where(e => e.ChildType == DbLinkedChildType.LocalAlternateVersion)
|
|
.Select(e => e.ChildId)
|
|
.ToList();
|
|
|
|
context.LinkedChildren.RemoveRange(existingLinkedChildren);
|
|
|
|
if (orphanedLocalVersionIds.Count > 0)
|
|
{
|
|
var orphanedItems = context.BaseItems
|
|
.Where(e => orphanedLocalVersionIds.Contains(e.Id) && e.OwnerId == video.Id)
|
|
.ToList();
|
|
|
|
if (orphanedItems.Count > 0)
|
|
{
|
|
_logger.LogInformation(
|
|
"Deleting {Count} orphaned LocalAlternateVersion items for video {VideoName} ({VideoId})",
|
|
orphanedItems.Count,
|
|
video.Name,
|
|
video.Id);
|
|
context.BaseItems.RemoveRange(orphanedItems);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
context.SaveChanges();
|
|
transaction.Commit();
|
|
}
|
|
|
|
private static List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
|
|
{
|
|
var list = new List<(ItemValueType, string)>();
|
|
|
|
if (item is IHasArtist hasArtist)
|
|
{
|
|
list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
|
|
}
|
|
|
|
if (item is IHasAlbumArtist hasAlbumArtist)
|
|
{
|
|
list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
|
|
}
|
|
|
|
list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
|
|
list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
|
|
list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
|
|
|
|
list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
|
|
|
|
list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
|
|
|
|
return list;
|
|
}
|
|
}
|