mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-20 07:45:26 +03:00
Optimize internal querying of UserData, other fixes (#14795)
This commit is contained in:
@@ -1090,6 +1090,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||||
{
|
{
|
||||||
|
RootFolder.Children = null;
|
||||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Start by just validating the children of the root, but go no further
|
// Start by just validating the children of the root, but go no further
|
||||||
@@ -1100,9 +1101,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
allowRemoveRoot: removeRoot,
|
allowRemoveRoot: removeRoot,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
var rootFolder = GetUserRootFolder();
|
||||||
|
rootFolder.Children = null;
|
||||||
|
|
||||||
await GetUserRootFolder().ValidateChildren(
|
await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await rootFolder.ValidateChildren(
|
||||||
new Progress<double>(),
|
new Progress<double>(),
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||||
recursive: false,
|
recursive: false,
|
||||||
@@ -1110,7 +1114,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Quickly scan CollectionFolders for changes
|
// Quickly scan CollectionFolders for changes
|
||||||
foreach (var child in GetUserRootFolder().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))
|
||||||
|
|||||||
@@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
var genres = item
|
var genres = item
|
||||||
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
.GetRecursiveChildren(
|
||||||
|
user,
|
||||||
|
new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
IncludeItemTypes = [BaseItemKind.Audio],
|
IncludeItemTypes = [BaseItemKind.Audio],
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
})
|
},
|
||||||
|
out _)
|
||||||
.Cast<Audio>()
|
.Cast<Audio>()
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Concat(item.Genres)
|
.Concat(item.Genres)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var userId = user.InternalId;
|
var userId = user.InternalId;
|
||||||
var cacheKey = GetCacheKey(userId, item.Id);
|
var cacheKey = GetCacheKey(userId, item.Id);
|
||||||
_cache.AddOrUpdate(cacheKey, userData);
|
_cache.AddOrUpdate(cacheKey, userData);
|
||||||
|
item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata
|
||||||
|
|
||||||
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
||||||
{
|
{
|
||||||
@@ -159,7 +160,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserItemData Map(UserData dto)
|
private static UserItemData Map(UserData dto)
|
||||||
{
|
{
|
||||||
return new UserItemData()
|
return new UserItemData()
|
||||||
{
|
{
|
||||||
@@ -237,7 +238,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public UserItemData? GetUserData(User user, BaseItem item)
|
public UserItemData? GetUserData(User user, BaseItem item)
|
||||||
{
|
{
|
||||||
return GetUserData(user, item.Id, item.GetUserDataKeys());
|
return item.UserData.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
|
||||||
|
{
|
||||||
|
Key = item.GetUserDataKeys()[0],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ public class YearsController : BaseJellyfinApiController
|
|||||||
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
|
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
|
||||||
|
|
||||||
IReadOnlyList<BaseItem> items;
|
IReadOnlyList<BaseItem> items;
|
||||||
|
int totalCount = -1;
|
||||||
if (parentItem.IsFolder)
|
if (parentItem.IsFolder)
|
||||||
{
|
{
|
||||||
var folder = (Folder)parentItem;
|
var folder = (Folder)parentItem;
|
||||||
@@ -118,7 +119,7 @@ public class YearsController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
|
items = recursive ? folder.GetRecursiveChildren(user, query, out totalCount) : folder.GetChildren(user, true).Where(Filter).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -153,7 +154,7 @@ public class YearsController : BaseJellyfinApiController
|
|||||||
|
|
||||||
var result = new QueryResult<BaseItemDto>(
|
var result = new QueryResult<BaseItemDto>(
|
||||||
startIndex,
|
startIndex,
|
||||||
ibnItemsArray.Count,
|
totalCount == -1 ? ibnItemsArray.Count : totalCount,
|
||||||
dtos.Where(i => i is not null).ToArray());
|
dtos.Where(i => i is not null).ToArray());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,13 +111,15 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
var date = (DateTime?)DateTime.UtcNow;
|
var date = (DateTime?)DateTime.UtcNow;
|
||||||
|
|
||||||
|
var relatedItems = TraverseHirachyDown(id, 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,
|
||||||
// UserData may be reattached to a new entry, but some entries can be left behind.
|
// UserData may be reattached to a new entry, but some entries can be left behind.
|
||||||
// Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
|
// Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
|
||||||
context.UserData
|
context.UserData
|
||||||
.Join(
|
.Join(
|
||||||
context.UserData.Where(e => e.ItemId == id),
|
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
|
||||||
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
|
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
|
||||||
userData => new { userData.UserId, userData.CustomDataKey },
|
userData => new { userData.UserId, userData.CustomDataKey },
|
||||||
(placeholder, userData) => placeholder)
|
(placeholder, userData) => placeholder)
|
||||||
@@ -125,29 +127,31 @@ public sealed class BaseItemRepository
|
|||||||
.ExecuteDelete();
|
.ExecuteDelete();
|
||||||
|
|
||||||
// Detach all user watch data
|
// Detach all user watch data
|
||||||
context.UserData.Where(e => e.ItemId == id)
|
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
|
||||||
.ExecuteUpdate(e => e
|
.ExecuteUpdate(e => e
|
||||||
.SetProperty(f => f.RetentionDate, date)
|
.SetProperty(f => f.RetentionDate, date)
|
||||||
.SetProperty(f => f.ItemId, PlaceholderId));
|
.SetProperty(f => f.ItemId, PlaceholderId));
|
||||||
|
|
||||||
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
|
||||||
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
|
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
|
context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
|
context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
|
context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
|
context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
|
||||||
context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
|
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.ItemDisplayPreferences.Where(e => e.ItemId == id).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.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
|
||||||
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
|
context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
|
context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
|
context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
|
var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray();
|
||||||
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
|
context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
||||||
context.TrickplayInfos.Where(e => e.ItemId == id).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();
|
context.SaveChanges();
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
@@ -434,7 +438,8 @@ public sealed class BaseItemRepository
|
|||||||
dbQuery = dbQuery.AsSingleQuery()
|
dbQuery = dbQuery.AsSingleQuery()
|
||||||
.Include(e => e.TrailerTypes)
|
.Include(e => e.TrailerTypes)
|
||||||
.Include(e => e.Provider)
|
.Include(e => e.Provider)
|
||||||
.Include(e => e.LockedFields);
|
.Include(e => e.LockedFields)
|
||||||
|
.Include(e => e.UserData);
|
||||||
|
|
||||||
if (filter.DtoOptions.EnableImages)
|
if (filter.DtoOptions.EnableImages)
|
||||||
{
|
{
|
||||||
@@ -745,8 +750,9 @@ public sealed class BaseItemRepository
|
|||||||
/// <param name="entity">The entity.</param>
|
/// <param name="entity">The entity.</param>
|
||||||
/// <param name="dto">The dto base instance.</param>
|
/// <param name="dto">The dto base instance.</param>
|
||||||
/// <param name="appHost">The Application server Host.</param>
|
/// <param name="appHost">The Application server Host.</param>
|
||||||
|
/// <param name="logger">The applogger.</param>
|
||||||
/// <returns>The dto to map.</returns>
|
/// <returns>The dto to map.</returns>
|
||||||
public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
|
public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logger)
|
||||||
{
|
{
|
||||||
dto.Id = entity.Id;
|
dto.Id = entity.Id;
|
||||||
dto.ParentId = entity.ParentId.GetValueOrDefault();
|
dto.ParentId = entity.ParentId.GetValueOrDefault();
|
||||||
@@ -791,6 +797,8 @@ public sealed class BaseItemRepository
|
|||||||
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
|
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
|
||||||
dto.Width = entity.Width.GetValueOrDefault();
|
dto.Width = entity.Width.GetValueOrDefault();
|
||||||
dto.Height = entity.Height.GetValueOrDefault();
|
dto.Height = entity.Height.GetValueOrDefault();
|
||||||
|
dto.UserData = entity.UserData;
|
||||||
|
|
||||||
if (entity.Provider is not null)
|
if (entity.Provider is not null)
|
||||||
{
|
{
|
||||||
dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
|
dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
|
||||||
@@ -1144,7 +1152,7 @@ public sealed class BaseItemRepository
|
|||||||
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Map(baseItemEntity, dto, appHost);
|
return Map(baseItemEntity, dto, appHost, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
|
private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
|
||||||
@@ -2449,4 +2457,56 @@ public sealed class BaseItemRepository
|
|||||||
return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
|
return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool GetIsPlayed(User user, Guid id, bool recursive)
|
||||||
|
{
|
||||||
|
using var dbContext = _dbProvider.CreateDbContext();
|
||||||
|
|
||||||
|
if (recursive)
|
||||||
|
{
|
||||||
|
var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
|
||||||
|
|
||||||
|
return dbContext.BaseItems
|
||||||
|
.Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
|
||||||
|
.All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseItemEntity, bool>>? filter = null)
|
||||||
|
{
|
||||||
|
var folderStack = new HashSet<Guid>()
|
||||||
|
{
|
||||||
|
parentId
|
||||||
|
};
|
||||||
|
var folderList = new HashSet<Guid>()
|
||||||
|
{
|
||||||
|
parentId
|
||||||
|
};
|
||||||
|
|
||||||
|
while (folderStack.Count != 0)
|
||||||
|
{
|
||||||
|
var items = folderStack.ToArray();
|
||||||
|
folderStack.Clear();
|
||||||
|
var query = dbContext.BaseItems
|
||||||
|
.WhereOneOrMany(items, e => e.ParentId!.Value);
|
||||||
|
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
query = query.Where(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in query.Select(e => e.Id).ToArray())
|
||||||
|
{
|
||||||
|
if (folderList.Add(item))
|
||||||
|
{
|
||||||
|
folderStack.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,12 @@ 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)
|
||||||
{
|
{
|
||||||
using var context = _dbProvider.CreateDbContext();
|
|
||||||
|
|
||||||
// TODO: yes for __SOME__ reason there can be duplicates.
|
// TODO: yes for __SOME__ reason there can be duplicates.
|
||||||
people = people.DistinctBy(e => e.Id).ToArray();
|
people = people.DistinctBy(e => e.Id).ToArray();
|
||||||
var personids = people.Select(f => f.Id);
|
var personids = people.Select(f => f.Id);
|
||||||
|
|
||||||
|
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();
|
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));
|
context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map));
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
@@ -104,6 +105,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
|||||||
context.PeopleBaseItemMap.RemoveRange(maps);
|
context.PeopleBaseItemMap.RemoveRange(maps);
|
||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private PersonInfo Map(People people)
|
private PersonInfo Map(People people)
|
||||||
|
|||||||
@@ -107,8 +107,15 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
ProductionLocations = Array.Empty<string>();
|
ProductionLocations = Array.Empty<string>();
|
||||||
RemoteTrailers = Array.Empty<MediaUrl>();
|
RemoteTrailers = Array.Empty<MediaUrl>();
|
||||||
ExtraIds = Array.Empty<Guid>();
|
ExtraIds = Array.Empty<Guid>();
|
||||||
|
UserData = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets the user data collection as cached from the last Db query.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<UserData> UserData { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string PreferredMetadataCountryCode { get; set; }
|
public string PreferredMetadataCountryCode { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Folder : BaseItem
|
public class Folder : BaseItem
|
||||||
{
|
{
|
||||||
|
private IEnumerable<BaseItem> _children;
|
||||||
|
|
||||||
public Folder()
|
public Folder()
|
||||||
{
|
{
|
||||||
LinkedChildren = Array.Empty<LinkedChild>();
|
LinkedChildren = Array.Empty<LinkedChild>();
|
||||||
@@ -108,11 +110,15 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the actual children.
|
/// Gets or Sets the actual children.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The actual children.</value>
|
/// <value>The actual children.</value>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual IEnumerable<BaseItem> Children => LoadChildren();
|
public virtual IEnumerable<BaseItem> Children
|
||||||
|
{
|
||||||
|
get => _children ??= LoadChildren();
|
||||||
|
set => _children = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets thread-safe access to all recursive children of this folder - without regard to user.
|
/// Gets thread-safe access to all recursive children of this folder - without regard to user.
|
||||||
@@ -281,6 +287,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default)
|
public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
Children = null; // invalidate cached children.
|
||||||
return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
|
return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +295,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
var dictionary = new Dictionary<Guid, BaseItem>();
|
var dictionary = new Dictionary<Guid, BaseItem>();
|
||||||
|
|
||||||
|
Children = null; // invalidate cached children.
|
||||||
var childrenList = Children.ToList();
|
var childrenList = Children.ToList();
|
||||||
|
|
||||||
foreach (var child in childrenList)
|
foreach (var child in childrenList)
|
||||||
@@ -526,6 +534,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
if (validChildrenNeedGeneration)
|
if (validChildrenNeedGeneration)
|
||||||
{
|
{
|
||||||
|
Children = null; // invalidate cached children.
|
||||||
validChildren = Children.ToList();
|
validChildren = Children.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,6 +577,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
if (recursive && child is Folder folder)
|
if (recursive && child is Folder folder)
|
||||||
{
|
{
|
||||||
|
folder.Children = null; // invalidate cached children.
|
||||||
await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
|
await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -686,16 +696,22 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
IEnumerable<BaseItem> items;
|
IEnumerable<BaseItem> items;
|
||||||
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
||||||
|
|
||||||
|
var totalCount = 0;
|
||||||
if (query.User is null)
|
if (query.User is null)
|
||||||
{
|
{
|
||||||
items = GetRecursiveChildren(filter);
|
items = GetRecursiveChildren(filter);
|
||||||
|
totalCount = items.Count();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
items = GetRecursiveChildren(user, query);
|
items = GetRecursiveChildren(user, query, out totalCount);
|
||||||
|
query.Limit = null;
|
||||||
|
query.StartIndex = null; // override these here as they have already been applied
|
||||||
}
|
}
|
||||||
|
|
||||||
return PostFilterAndSort(items, query);
|
var result = PostFilterAndSort(items, query);
|
||||||
|
result.TotalRecordCount = totalCount;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this is not UserRootFolder
|
if (this is not UserRootFolder
|
||||||
@@ -944,22 +960,31 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
IEnumerable<BaseItem> items;
|
IEnumerable<BaseItem> items;
|
||||||
|
|
||||||
|
int totalItemCount = 0;
|
||||||
if (query.User is null)
|
if (query.User is null)
|
||||||
{
|
{
|
||||||
items = Children.Where(filter);
|
items = Children.Where(filter);
|
||||||
|
totalItemCount = items.Count();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// need to pass this param to the children.
|
// need to pass this param to the children.
|
||||||
var childQuery = new InternalItemsQuery
|
var childQuery = new InternalItemsQuery
|
||||||
{
|
{
|
||||||
DisplayAlbumFolders = query.DisplayAlbumFolders
|
DisplayAlbumFolders = query.DisplayAlbumFolders,
|
||||||
|
Limit = query.Limit,
|
||||||
|
StartIndex = query.StartIndex
|
||||||
};
|
};
|
||||||
|
|
||||||
items = GetChildren(user, true, childQuery).Where(filter);
|
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
|
||||||
|
|
||||||
|
query.Limit = null;
|
||||||
|
query.StartIndex = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return PostFilterAndSort(items, query);
|
var result = PostFilterAndSort(items, query);
|
||||||
|
result.TotalRecordCount = totalItemCount;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
||||||
@@ -1242,30 +1267,30 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren)
|
public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
|
||||||
|
|
||||||
return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
query ??= new InternalItemsQuery();
|
||||||
|
query.User = user;
|
||||||
|
|
||||||
// the true root should return our users root folder children
|
// the true root should return our users root folder children
|
||||||
if (IsPhysicalRoot)
|
if (IsPhysicalRoot)
|
||||||
{
|
{
|
||||||
return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
|
return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new Dictionary<Guid, BaseItem>();
|
var result = new Dictionary<Guid, BaseItem>();
|
||||||
|
|
||||||
AddChildren(user, includeLinkedChildren, result, false, query);
|
totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
|
||||||
|
|
||||||
return result.Values.ToArray();
|
return result.Values.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query = null)
|
||||||
|
{
|
||||||
|
return GetChildren(user, includeLinkedChildren, out _, query);
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
||||||
{
|
{
|
||||||
return Children;
|
return Children;
|
||||||
@@ -1274,13 +1299,13 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the children to list.
|
/// Adds the children to list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
|
private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
|
||||||
{
|
{
|
||||||
// Prevent infinite recursion of nested folders
|
// Prevent infinite recursion of nested folders
|
||||||
visitedFolders ??= new HashSet<Folder>();
|
visitedFolders ??= new HashSet<Folder>();
|
||||||
if (!visitedFolders.Add(this))
|
if (!visitedFolders.Add(this))
|
||||||
{
|
{
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
|
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
|
||||||
@@ -1297,44 +1322,58 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
children = GetEligibleChildrenForRecursiveChildren(user);
|
children = GetEligibleChildrenForRecursiveChildren(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
|
|
||||||
|
|
||||||
if (includeLinkedChildren)
|
if (includeLinkedChildren)
|
||||||
{
|
{
|
||||||
AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders);
|
children = children.Concat(GetLinkedChildren(user)).ToArray();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
|
return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
|
||||||
{
|
|
||||||
foreach (var child in children)
|
|
||||||
{
|
|
||||||
if (!child.IsVisible(user))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query is null || UserViewBuilder.FilterItem(child, query))
|
private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
|
||||||
|
{
|
||||||
|
query ??= new InternalItemsQuery();
|
||||||
|
var limit = query.Limit;
|
||||||
|
query.Limit = 100; // this is a bit of a dirty hack thats in favor of specifically the webUI as it does not show more then +99 elements in its badges so there is no point in reading more then that.
|
||||||
|
|
||||||
|
var visibileChildren = children
|
||||||
|
.Where(e => e.IsVisible(user))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var realChildren = visibileChildren
|
||||||
|
.Where(e => query is null || UserViewBuilder.FilterItem(e, query))
|
||||||
|
.ToArray();
|
||||||
|
var childCount = realChildren.Count();
|
||||||
|
if (result.Count < query.Limit)
|
||||||
|
{
|
||||||
|
foreach (var child in realChildren
|
||||||
|
.Skip(query.StartIndex ?? 0)
|
||||||
|
.TakeWhile(e => query.Limit >= result.Count))
|
||||||
{
|
{
|
||||||
result[child.Id] = child;
|
result[child.Id] = child;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (recursive && child.IsFolder)
|
if (recursive)
|
||||||
{
|
{
|
||||||
var folder = (Folder)child;
|
foreach (var child in visibileChildren
|
||||||
|
.Where(e => e.IsFolder)
|
||||||
folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
|
.OfType<Folder>())
|
||||||
}
|
{
|
||||||
|
childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
|
return childCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
var result = new Dictionary<Guid, BaseItem>();
|
var result = new Dictionary<Guid, BaseItem>();
|
||||||
|
|
||||||
AddChildren(user, true, result, true, query);
|
totalCount = AddChildren(user, true, result, true, query);
|
||||||
|
|
||||||
return result.Values.ToArray();
|
return result.Values.ToArray();
|
||||||
}
|
}
|
||||||
@@ -1668,16 +1707,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public override bool IsPlayed(User user, UserItemData userItemData)
|
public override bool IsPlayed(User user, UserItemData userItemData)
|
||||||
{
|
{
|
||||||
var itemsResult = GetItemList(new InternalItemsQuery(user)
|
return ItemRepository.GetIsPlayed(user, Id, true);
|
||||||
{
|
|
||||||
Recursive = true,
|
|
||||||
IsFolder = false,
|
|
||||||
IsVirtualItem = false,
|
|
||||||
EnableTotalRecordCount = false
|
|
||||||
});
|
|
||||||
|
|
||||||
return itemsResult
|
|
||||||
.All(i => i.IsPlayed(user, userItemData: null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsUnplayed(User user, UserItemData userItemData)
|
public override bool IsUnplayed(User user, UserItemData userItemData)
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||||||
return Sort(children, user).ToArray();
|
return Sort(children, user).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
|
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||||
{
|
{
|
||||||
var children = base.GetRecursiveChildren(user, query);
|
var children = base.GetRecursiveChildren(user, query, out totalCount);
|
||||||
return Sort(children, user).ToArray();
|
return Sort(children, user).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
public override int GetChildCount(User user)
|
public override int GetChildCount(User user)
|
||||||
{
|
{
|
||||||
var result = GetChildren(user, true).Count;
|
var result = GetChildren(user, true, null).Count;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Children = null; // invalidate cached children.
|
||||||
// Refresh bottom up, seasons and episodes first, then the series
|
// Refresh bottom up, seasons and episodes first, then the series
|
||||||
var items = GetRecursiveChildren();
|
var items = GetRecursiveChildren();
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override int GetChildCount(User user)
|
public override int GetChildCount(User user)
|
||||||
{
|
{
|
||||||
return GetChildren(user, true).Count;
|
return GetChildren(user, true, null).Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -134,20 +134,22 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
|
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||||
{
|
{
|
||||||
query.SetUser(user);
|
query.SetUser(user);
|
||||||
query.Recursive = true;
|
query.Recursive = true;
|
||||||
query.EnableTotalRecordCount = false;
|
query.EnableTotalRecordCount = false;
|
||||||
query.ForceDirect = true;
|
query.ForceDirect = true;
|
||||||
|
var data = GetItemList(query);
|
||||||
|
totalCount = data.Count;
|
||||||
|
|
||||||
return GetItemList(query);
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
||||||
{
|
{
|
||||||
return GetChildren(user, false);
|
return GetChildren(user, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsUserSpecific(Folder folder)
|
public static bool IsUserSpecific(Folder folder)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
@@ -112,4 +113,13 @@ public interface IItemRepository
|
|||||||
/// <param name="id">The id to check.</param>
|
/// <param name="id">The id to check.</param>
|
||||||
/// <returns>True if the item exists, otherwise false.</returns>
|
/// <returns>True if the item exists, otherwise false.</returns>
|
||||||
Task<bool> ItemExistsAsync(Guid id);
|
Task<bool> ItemExistsAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating wherever all children of the requested Id has been played.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The userdata to check against.</param>
|
||||||
|
/// <param name="id">The Top id to check.</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>
|
||||||
|
bool GetIsPlayed(User user, Guid id, bool recursive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
|
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||||
{
|
{
|
||||||
return GetPlayableItems(user, query);
|
var items = GetPlayableItems(user, query);
|
||||||
|
totalCount = items.Count;
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems()
|
public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems()
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ public class BaseItemEntity
|
|||||||
|
|
||||||
public Guid? ParentId { get; set; }
|
public Guid? ParentId { get; set; }
|
||||||
|
|
||||||
|
public BaseItemEntity? DirectParent { get; set; }
|
||||||
|
|
||||||
public Guid? TopParentId { get; set; }
|
public Guid? TopParentId { get; set; }
|
||||||
|
|
||||||
public Guid? SeasonId { get; set; }
|
public Guid? SeasonId { get; set; }
|
||||||
@@ -168,6 +170,8 @@ public class BaseItemEntity
|
|||||||
|
|
||||||
public ICollection<AncestorId>? Children { get; set; }
|
public ICollection<AncestorId>? Children { get; set; }
|
||||||
|
|
||||||
|
public ICollection<BaseItemEntity>? DirectChildren { get; set; }
|
||||||
|
|
||||||
public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
|
public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
|
||||||
|
|
||||||
public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
|
public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
|
|||||||
builder.HasMany(e => e.Provider);
|
builder.HasMany(e => e.Provider);
|
||||||
builder.HasMany(e => e.Parents);
|
builder.HasMany(e => e.Parents);
|
||||||
builder.HasMany(e => e.Children);
|
builder.HasMany(e => e.Children);
|
||||||
|
builder.HasMany(e => e.DirectChildren).WithOne(e => e.DirectParent).HasForeignKey(e => e.ParentId).OnDelete(DeleteBehavior.Cascade);
|
||||||
builder.HasMany(e => e.LockedFields);
|
builder.HasMany(e => e.LockedFields);
|
||||||
builder.HasMany(e => e.TrailerTypes);
|
builder.HasMany(e => e.TrailerTypes);
|
||||||
builder.HasMany(e => e.Images);
|
builder.HasMany(e => e.Images);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddProperParentChildRelationBaseItemWithCascade : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_BaseItems_BaseItems_ParentId",
|
||||||
|
table: "BaseItems",
|
||||||
|
column: "ParentId",
|
||||||
|
principalTable: "BaseItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_BaseItems_BaseItems_ParentId",
|
||||||
|
table: "BaseItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
|
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
|
||||||
{
|
{
|
||||||
@@ -1450,6 +1450,16 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.Navigation("Item");
|
b.Navigation("Item");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
|
||||||
|
.WithMany("DirectChildren")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("DirectParent");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
|
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
|
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
|
||||||
@@ -1652,6 +1662,8 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.Navigation("Children");
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("DirectChildren");
|
||||||
|
|
||||||
b.Navigation("Images");
|
b.Navigation("Images");
|
||||||
|
|
||||||
b.Navigation("ItemValues");
|
b.Navigation("ItemValues");
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
|||||||
using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions);
|
using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions);
|
||||||
Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode);
|
||||||
|
|
||||||
|
await Task.Delay(2000).ConfigureAwait(true);
|
||||||
|
|
||||||
using var response = await client.GetAsync("Library/VirtualFolders");
|
using var response = await client.GetAsync("Library/VirtualFolders");
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user