diff options
| author | pokreman06 <112423673+pokreman06@users.noreply.github.com> | 2025-10-02 11:07:05 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-02 11:07:05 -0600 |
| commit | 0b4854c5eff7c862d05f43048e08dd3a1a25efaa (patch) | |
| tree | a4c417af05deef7878ab9342c85c506ad22e1ced /Jellyfin.Server.Implementations | |
| parent | d6a1c8413c6a213f6e579246c1b85aad9b028b3a (diff) | |
| parent | 0f42aa892e0a7fe2ac4e680e7647515af0909e5e (diff) | |
Merge branch 'jellyfin:master' into master
Diffstat (limited to 'Jellyfin.Server.Implementations')
7 files changed, 441 insertions, 275 deletions
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 74d99455df..e5c3cef3d3 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -39,7 +39,7 @@ public class BackupService : IBackupService ReferenceHandler = ReferenceHandler.IgnoreCycles, }; - private readonly Version _backupEngineVersion = Version.Parse("0.2.0"); + private readonly Version _backupEngineVersion = new Version(0, 2, 0); /// <summary> /// Initializes a new instance of the <see cref="BackupService"/> class. diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index cf4f405ee5..4d17e37699 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -99,11 +99,11 @@ public sealed class BaseItemRepository } /// <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(); @@ -111,13 +111,15 @@ public sealed class BaseItemRepository var date = (DateTime?)DateTime.UtcNow; + var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray(); + // Remove any UserData entries for the placeholder item that would conflict with the UserData // being detached from the item being deleted. This is necessary because, during an update, // 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. context.UserData .Join( - context.UserData.Where(e => e.ItemId == id), + context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId), placeholder => new { placeholder.UserId, placeholder.CustomDataKey }, userData => new { userData.UserId, userData.CustomDataKey }, (placeholder, userData) => placeholder) @@ -125,29 +127,31 @@ public sealed class BaseItemRepository .ExecuteDelete(); // Detach all user watch data - context.UserData.Where(e => e.ItemId == id) + context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId) .ExecuteUpdate(e => e .SetProperty(f => f.RetentionDate, date) .SetProperty(f => f.ItemId, PlaceholderId)); - context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); - context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); - context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); - context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); - context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.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.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).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.Where(e => e.ItemId == id).ExecuteDelete(); - context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete(); - context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete(); - context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); - context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); - context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).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(); } @@ -267,7 +271,7 @@ public sealed class BaseItemRepository result.TotalRecordCount = dbQuery.Count(); } - dbQuery = ApplyGroupingFilter(dbQuery, filter); + dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); @@ -286,7 +290,7 @@ public sealed class BaseItemRepository dbQuery = TranslateQuery(dbQuery, context, filter); - dbQuery = ApplyGroupingFilter(dbQuery, filter); + dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); @@ -328,7 +332,7 @@ public sealed class BaseItemRepository var mainquery = PrepareItemQuery(context, filter); mainquery = TranslateQuery(mainquery, context, filter); mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated)); - mainquery = ApplyGroupingFilter(mainquery, filter); + mainquery = ApplyGroupingFilter(context, mainquery, filter); mainquery = ApplyQueryPaging(mainquery, filter); return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); @@ -365,37 +369,53 @@ public sealed class BaseItemRepository return query.ToArray(); } - private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) + private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) { // This whole block is needed to filter duplicate entries on request // for the time being it cannot be used because it would destroy the ordering // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own - // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); - // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) - // { - // dbQuery = ApplyOrder(dbQuery, filter); - // dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First()); - // } - // else if (enableGroupByPresentationUniqueKey) - // { - // dbQuery = ApplyOrder(dbQuery, filter); - // dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()); - // } - // else if (filter.GroupBySeriesPresentationUniqueKey) - // { - // dbQuery = ApplyOrder(dbQuery, filter); - // dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First()); - // } - // else - // { - // dbQuery = dbQuery.Distinct(); - // dbQuery = ApplyOrder(dbQuery, filter); - // } - dbQuery = dbQuery.Distinct(); + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); + if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) + { + var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.FirstOrDefault()).Select(e => e!.Id); + dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id)); + } + else if (enableGroupByPresentationUniqueKey) + { + var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id); + dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id)); + } + else if (filter.GroupBySeriesPresentationUniqueKey) + { + var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id); + dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id)); + } + else + { + dbQuery = dbQuery.Distinct(); + } + dbQuery = ApplyOrder(dbQuery, filter); + dbQuery = ApplyNavigations(dbQuery, filter); + + return dbQuery; + } + + private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) + { + dbQuery = dbQuery.Include(e => e.TrailerTypes) + .Include(e => e.Provider) + .Include(e => e.LockedFields) + .Include(e => e.UserData); + + if (filter.DtoOptions.EnableImages) + { + dbQuery = dbQuery.Include(e => e.Images); + } + return dbQuery; } @@ -422,8 +442,7 @@ public sealed class BaseItemRepository private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter) { dbQuery = TranslateQuery(dbQuery, context, filter); - dbQuery = ApplyOrder(dbQuery, filter); - dbQuery = ApplyGroupingFilter(dbQuery, filter); + dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); return dbQuery; } @@ -431,15 +450,7 @@ public sealed class BaseItemRepository private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter) { IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking(); - dbQuery = dbQuery.AsSingleQuery() - .Include(e => e.TrailerTypes) - .Include(e => e.Provider) - .Include(e => e.LockedFields); - - if (filter.DtoOptions.EnableImages) - { - dbQuery = dbQuery.Include(e => e.Images); - } + dbQuery = dbQuery.AsSingleQuery(); return dbQuery; } @@ -470,7 +481,7 @@ public sealed class BaseItemRepository var counts = dbQuery .GroupBy(x => x.Type) .Select(x => new { x.Key, Count = x.Count() }) - .AsEnumerable(); + .ToArray(); var lookup = _itemTypeLookup.BaseItemKindNames; var result = new ItemCounts(); @@ -724,13 +735,20 @@ public sealed class BaseItemRepository } using var context = _dbProvider.CreateDbContext(); - var item = PrepareItemQuery(context, new() + var dbQuery = PrepareItemQuery(context, new() { DtoOptions = new() { EnableImages = true } - }).FirstOrDefault(e => e.Id == id); + }); + dbQuery = dbQuery.Include(e => e.TrailerTypes) + .Include(e => e.Provider) + .Include(e => e.LockedFields) + .Include(e => e.UserData) + .Include(e => e.Images); + + var item = dbQuery.FirstOrDefault(e => e.Id == id); if (item is null) { return null; @@ -745,8 +763,9 @@ public sealed class BaseItemRepository /// <param name="entity">The entity.</param> /// <param name="dto">The dto base instance.</param> /// <param name="appHost">The Application server Host.</param> + /// <param name="logger">The applogger.</param> /// <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.ParentId = entity.ParentId.GetValueOrDefault(); @@ -791,6 +810,8 @@ public sealed class BaseItemRepository dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty); dto.Width = entity.Width.GetValueOrDefault(); dto.Height = entity.Height.GetValueOrDefault(); + dto.UserData = entity.UserData; + if (entity.Provider is not null) { dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); @@ -1144,7 +1165,7 @@ public sealed class BaseItemRepository 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) @@ -1302,7 +1323,13 @@ public sealed class BaseItemRepository result.Items = [ .. query - .Select(e => e.First()) + .Select(e => e.AsQueryable() + .Include(e => e.TrailerTypes) + .Include(e => e.Provider) + .Include(e => e.LockedFields) + .Include(e => e.Images) + .AsSingleQuery() + .First()) .AsEnumerable() .Where(e => e is not null) .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e => @@ -1884,7 +1911,7 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) { - baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.NameStartsWith)); + baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith)); } if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) @@ -2027,22 +2054,26 @@ public sealed class BaseItemRepository if (filter.MinParentalRating != null) { var min = filter.MinParentalRating; - minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null; - if (min.SubScore != null) - { - minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null); - } + var minScore = min.Score; + var minSubScore = min.SubScore ?? 0; + + minParentalRatingFilter = e => + e.InheritedParentalRatingValue == null || + e.InheritedParentalRatingValue > minScore || + (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore); } Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null; if (filter.MaxParentalRating != null) { var max = filter.MaxParentalRating; - maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null; - if (max.SubScore != null) - { - maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null); - } + var maxScore = max.Score; + var maxSubScore = max.SubScore ?? 0; + + maxParentalRatingFilter = e => + e.InheritedParentalRatingValue == null || + e.InheritedParentalRatingValue < maxScore || + (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore); } if (filter.HasParentalRating ?? false) @@ -2270,8 +2301,18 @@ public sealed class BaseItemRepository if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0) { - var include = filter.HasAnyProviderId.Select(e => $"{e.Key}:{e.Value}").ToArray(); - baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => include.Contains(f))); + // Allow setting a null or empty value to get all items that have the specified provider set. + var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArray(); + if (includeAny.Length > 0) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId))); + } + + var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Key}:{e.Value}").ToArray(); + if (includeSelected.Length > 0) + { + baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => includeSelected.Contains(f))); + } } if (filter.HasImdbId.HasValue) @@ -2449,4 +2490,68 @@ public sealed class BaseItemRepository 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; + } + + /// <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()); + } } diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs index 93c6f472e2..438458c6be 100644 --- a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs +++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs @@ -55,11 +55,14 @@ public class KeyframeRepository : IKeyframeRepository public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken) { using var context = _dbProvider.CreateDbContext(); - using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false); - await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } } /// <inheritdoc /> diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index be58e2a527..e03c136915 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -35,16 +35,22 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); - // dbQuery = dbQuery.OrderBy(e => e.ListOrder); - if (filter.Limit > 0) + // Include PeopleBaseItemMap + if (!filter.ItemId.IsEmpty()) { - dbQuery = dbQuery.Take(filter.Limit); + dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId)) + .OrderBy(e => e.BaseItems!.First(e => e.ItemId == filter.ItemId).ListOrder) + .ThenBy(e => e.PersonType) + .ThenBy(e => e.Name); + } + else + { + dbQuery = dbQuery.OrderBy(e => e.Name); } - // Include PeopleBaseItemMap - if (!filter.ItemId.IsEmpty()) + if (filter.Limit > 0) { - dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId)); + dbQuery = dbQuery.Take(filter.Limit); } return dbQuery.AsEnumerable().Select(Map).ToArray(); @@ -68,19 +74,42 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I /// <inheritdoc /> public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people) { - using var context = _dbProvider.CreateDbContext(); + foreach (var item in people.Where(e => e.Role is null)) + { + item.Role = string.Empty; + } - // TODO: yes for __SOME__ reason there can be duplicates. - people = people.DistinctBy(e => e.Id).ToArray(); - var personids = people.Select(f => f.Id); - 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)); + // multiple metadata providers can provide the _same_ person + people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray(); + var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray(); + + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + var existingPersons = context.Peoples.Select(e => new + { + item = e, + SelectionKey = e.Name + "-" + e.PersonType + }) + .Where(p => personKeys.Contains(p.SelectionKey)) + .Select(f => f.item) + .ToArray(); + + var toAdd = people + .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString())) + .Select(Map); + context.Peoples.AddRange(toAdd); context.SaveChanges(); - var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList(); + var personsEntities = toAdd.Concat(existingPersons).ToArray(); + + var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList(); + + var listOrder = 0; + 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.People.PersonType == person.Type.ToString() && e.Role == person.Role); if (existingMap is null) { context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() @@ -88,22 +117,28 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I Item = null!, ItemId = itemId, People = null!, - PeopleId = person.Id, - ListOrder = person.SortOrder, + PeopleId = entityPerson.Id, + ListOrder = listOrder, SortOrder = person.SortOrder, Role = person.Role }); } else { + // Update the order for existing mappings + existingMap.ListOrder = listOrder; + existingMap.SortOrder = person.SortOrder; // person mapping already exists so remove from list - maps.Remove(existingMap); + existingMaps.Remove(existingMap); } + + listOrder++; } - context.PeopleBaseItemMap.RemoveRange(maps); + context.PeopleBaseItemMap.RemoveRange(existingMaps); context.SaveChanges(); + transaction.Commit(); } private PersonInfo Map(People people) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 97c9d79f53..d00c87463c 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -68,87 +68,89 @@ public class MediaSegmentManager : IMediaSegmentManager return; } - using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - - _logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count); - - if (forceOverwrite) - { - // delete all existing media segments if forceOverwrite is set. - await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - - foreach (var provider in providers) + var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (db.ConfigureAwait(false)) { - if (!await provider.Supports(baseItem).ConfigureAwait(false)) - { - _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path); - continue; - } + _logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count); - IQueryable<MediaSegment> existingSegments; if (forceOverwrite) { - existingSegments = Array.Empty<MediaSegment>().AsQueryable(); - } - else - { - existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name)); + // delete all existing media segments if forceOverwrite is set. + await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); } - var requestItem = new MediaSegmentGenerationRequest() + foreach (var provider in providers) { - ItemId = baseItem.Id, - ExistingSegments = existingSegments.Select(e => Map(e)).ToArray() - }; + if (!await provider.Supports(baseItem).ConfigureAwait(false)) + { + _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path); + continue; + } - try - { - var segments = await provider.GetMediaSegments(requestItem, cancellationToken) - .ConfigureAwait(false); + IQueryable<MediaSegment> existingSegments; + if (forceOverwrite) + { + existingSegments = Array.Empty<MediaSegment>().AsQueryable(); + } + else + { + existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name)); + } + + var requestItem = new MediaSegmentGenerationRequest() + { + ItemId = baseItem.Id, + ExistingSegments = existingSegments.Select(e => Map(e)).ToArray() + }; - if (!forceOverwrite) + try { - var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items. - if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f => + var segments = await provider.GetMediaSegments(requestItem, cancellationToken) + .ConfigureAwait(false); + + if (!forceOverwrite) + { + var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items. + if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f => + { + return + e.StartTicks == f.StartTicks && + e.EndTicks == f.EndTicks && + e.Type == f.Type; + }))) + { + _logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path); + continue; + } + + // delete existing media segments that were re-generated. + await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } + + if (segments.Count == 0 && !requestItem.ExistingSegments.Any()) { - return - e.StartTicks == f.StartTicks && - e.EndTicks == f.EndTicks && - e.Type == f.Type; - }))) + _logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path); + continue; + } + else if (segments.Count == 0 && requestItem.ExistingSegments.Any()) { - _logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path); + _logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path); continue; } - // delete existing media segments that were re-generated. - await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - - if (segments.Count == 0 && !requestItem.ExistingSegments.Any()) - { - _logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path); - continue; - } - else if (segments.Count == 0 && requestItem.ExistingSegments.Any()) - { - _logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path); - continue; + _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); + var providerId = GetProviderId(provider.Name); + foreach (var segment in segments) + { + segment.ItemId = baseItem.Id; + await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + } } - - _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); - var providerId = GetProviderId(provider.Name); - foreach (var segment in segments) + catch (Exception ex) { - segment.ItemId = baseItem.Id; - await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); } } - catch (Exception ex) - { - _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); - } } } @@ -157,24 +159,34 @@ public class MediaSegmentManager : IMediaSegmentManager { ArgumentOutOfRangeException.ThrowIfLessThan(mediaSegment.EndTicks, mediaSegment.StartTicks); - using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - db.MediaSegments.Add(Map(mediaSegment, segmentProviderId)); - await db.SaveChangesAsync().ConfigureAwait(false); + var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + db.MediaSegments.Add(Map(mediaSegment, segmentProviderId)); + await db.SaveChangesAsync().ConfigureAwait(false); + } + return mediaSegment; } /// <inheritdoc /> public async Task DeleteSegmentAsync(Guid segmentId) { - using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false); + var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + await db.MediaSegments.Where(e => e.Id.Equals(segmentId)).ExecuteDeleteAsync().ConfigureAwait(false); + } } /// <inheritdoc /> public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken) { - using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } } /// <inheritdoc /> @@ -186,36 +198,38 @@ public class MediaSegmentManager : IMediaSegmentManager return []; } - using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - - var query = db.MediaSegments - .Where(e => e.ItemId.Equals(item.Id)); - - if (typeFilter is not null) + var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (db.ConfigureAwait(false)) { - query = query.Where(e => typeFilter.Contains(e.Type)); - } + var query = db.MediaSegments + .Where(e => e.ItemId.Equals(item.Id)); - if (filterByProvider) - { - var providerIds = _segmentProviders - .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) - .Select(f => GetProviderId(f.Name)) - .ToArray(); - if (providerIds.Length == 0) + if (typeFilter is not null) { - return []; + query = query.Where(e => typeFilter.Contains(e.Type)); } - query = query.Where(e => providerIds.Contains(e.SegmentProviderId)); - } + if (filterByProvider) + { + var providerIds = _segmentProviders + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .Select(f => GetProviderId(f.Name)) + .ToArray(); + if (providerIds.Length == 0) + { + return []; + } - return query - .OrderBy(e => e.StartTicks) - .AsNoTracking() - .AsEnumerable() - .Select(Map) - .ToArray(); + query = query.Where(e => providerIds.Contains(e.SegmentProviderId)); + } + + return query + .OrderBy(e => e.StartTicks) + .AsNoTracking() + .AsEnumerable() + .Select(Map) + .ToArray(); + } } private static MediaSegmentDto Map(MediaSegment segment) diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 0f21e11a35..0e126fe9a0 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -1,109 +1,116 @@ -#pragma warning disable CA1307 -#pragma warning disable CA1309 - using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; -namespace Jellyfin.Server.Implementations.Users +namespace Jellyfin.Server.Implementations.Users; + +/// <summary> +/// Manages the storage and retrieval of display preferences through Entity Framework. +/// </summary> +public sealed class DisplayPreferencesManager : IDisplayPreferencesManager { + private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory; + /// <summary> - /// Manages the storage and retrieval of display preferences through Entity Framework. + /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class. /// </summary> - public sealed class DisplayPreferencesManager : IDisplayPreferencesManager, IAsyncDisposable + /// <param name="dbContextFactory">The database context factory.</param> + public DisplayPreferencesManager(IDbContextFactory<JellyfinDbContext> dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + /// <inheritdoc /> + public DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client) { - private readonly JellyfinDbContext _dbContext; + using var dbContext = _dbContextFactory.CreateDbContext(); + var prefs = dbContext.DisplayPreferences + .Include(pref => pref.HomeSections) + .FirstOrDefault(pref => + pref.UserId.Equals(userId) && pref.Client == client && pref.ItemId.Equals(itemId)); - /// <summary> - /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class. - /// </summary> - /// <param name="dbContextFactory">The database context factory.</param> - public DisplayPreferencesManager(IDbContextFactory<JellyfinDbContext> dbContextFactory) + if (prefs is null) { - _dbContext = dbContextFactory.CreateDbContext(); + prefs = new DisplayPreferences(userId, itemId, client); + dbContext.DisplayPreferences.Add(prefs); + dbContext.SaveChanges(); } - /// <inheritdoc /> - public DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client) + return prefs; + } + + /// <inheritdoc /> + public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client) + { + using var dbContext = _dbContextFactory.CreateDbContext(); + var prefs = dbContext.ItemDisplayPreferences + .FirstOrDefault(pref => pref.UserId.Equals(userId) && pref.ItemId.Equals(itemId) && pref.Client == client); + + if (prefs is null) { - var prefs = _dbContext.DisplayPreferences - .Include(pref => pref.HomeSections) - .FirstOrDefault(pref => - pref.UserId.Equals(userId) && string.Equals(pref.Client, client) && pref.ItemId.Equals(itemId)); - - if (prefs is null) - { - prefs = new DisplayPreferences(userId, itemId, client); - _dbContext.DisplayPreferences.Add(prefs); - } - - return prefs; + prefs = new ItemDisplayPreferences(userId, Guid.Empty, client); + dbContext.ItemDisplayPreferences.Add(prefs); + dbContext.SaveChanges(); } - /// <inheritdoc /> - public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client) - { - var prefs = _dbContext.ItemDisplayPreferences - .FirstOrDefault(pref => pref.UserId.Equals(userId) && pref.ItemId.Equals(itemId) && string.Equals(pref.Client, client)); + return prefs; + } - if (prefs is null) - { - prefs = new ItemDisplayPreferences(userId, Guid.Empty, client); - _dbContext.ItemDisplayPreferences.Add(prefs); - } + /// <inheritdoc /> + public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client) + { + using var dbContext = _dbContextFactory.CreateDbContext(); + return dbContext.ItemDisplayPreferences + .Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && prefs.Client == client) + .ToList(); + } - return prefs; - } + /// <inheritdoc /> + public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client) + { + using var dbContext = _dbContextFactory.CreateDbContext(); + return dbContext.CustomItemDisplayPreferences + .Where(prefs => prefs.UserId.Equals(userId) + && prefs.ItemId.Equals(itemId) + && prefs.Client == client) + .ToDictionary(prefs => prefs.Key, prefs => prefs.Value); + } - /// <inheritdoc /> - public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client) - { - return _dbContext.ItemDisplayPreferences - .Where(prefs => prefs.UserId.Equals(userId) && !prefs.ItemId.Equals(default) && string.Equals(prefs.Client, client)) - .ToList(); - } + /// <inheritdoc /> + public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences) + { + using var dbContext = _dbContextFactory.CreateDbContext(); + dbContext.CustomItemDisplayPreferences.Where(prefs => prefs.UserId.Equals(userId) + && prefs.ItemId.Equals(itemId) + && prefs.Client == client) + .ExecuteDelete(); - /// <inheritdoc /> - public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client) + foreach (var (key, value) in customPreferences) { - return _dbContext.CustomItemDisplayPreferences - .Where(prefs => prefs.UserId.Equals(userId) - && prefs.ItemId.Equals(itemId) - && string.Equals(prefs.Client, client)) - .ToDictionary(prefs => prefs.Key, prefs => prefs.Value); + dbContext.CustomItemDisplayPreferences + .Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value)); } - /// <inheritdoc /> - public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences) - { - var existingPrefs = _dbContext.CustomItemDisplayPreferences - .Where(prefs => prefs.UserId.Equals(userId) - && prefs.ItemId.Equals(itemId) - && string.Equals(prefs.Client, client)); - _dbContext.CustomItemDisplayPreferences.RemoveRange(existingPrefs); - - foreach (var (key, value) in customPreferences) - { - _dbContext.CustomItemDisplayPreferences - .Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value)); - } - } + dbContext.SaveChanges(); + } - /// <inheritdoc /> - public void SaveChanges() - { - _dbContext.SaveChanges(); - } + /// <inheritdoc/> + public void UpdateDisplayPreferences(DisplayPreferences displayPreferences) + { + using var dbContext = _dbContextFactory.CreateDbContext(); + dbContext.DisplayPreferences.Attach(displayPreferences).State = EntityState.Modified; + dbContext.SaveChanges(); + } - /// <inheritdoc /> - public async ValueTask DisposeAsync() - { - await _dbContext.DisposeAsync().ConfigureAwait(false); - } + /// <inheritdoc/> + public void UpdateItemDisplayPreferences(ItemDisplayPreferences itemDisplayPreferences) + { + using var dbContext = _dbContextFactory.CreateDbContext(); + dbContext.ItemDisplayPreferences.Attach(itemDisplayPreferences).State = EntityState.Modified; + dbContext.SaveChanges(); } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 3dfb14d716..d0b41a7f6b 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -272,6 +272,7 @@ namespace Jellyfin.Server.Implementations.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { + dbContext.Users.Attach(user); dbContext.Users.Remove(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); } @@ -887,7 +888,8 @@ namespace Jellyfin.Server.Implementations.Users private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) { - dbContext.Users.Update(user); + dbContext.Users.Attach(user); + dbContext.Entry(user).State = EntityState.Modified; _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } |
