aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
authorpokreman06 <112423673+pokreman06@users.noreply.github.com>2025-10-02 11:07:05 -0600
committerGitHub <noreply@github.com>2025-10-02 11:07:05 -0600
commit0b4854c5eff7c862d05f43048e08dd3a1a25efaa (patch)
treea4c417af05deef7878ab9342c85c506ad22e1ced /Jellyfin.Server.Implementations
parentd6a1c8413c6a213f6e579246c1b85aad9b028b3a (diff)
parent0f42aa892e0a7fe2ac4e680e7647515af0909e5e (diff)
Merge branch 'jellyfin:master' into master
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs261
-rw-r--r--Jellyfin.Server.Implementations/Item/KeyframeRepository.cs13
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs71
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs200
-rw-r--r--Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs165
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs4
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);
}