aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs11
-rw-r--r--Jellyfin.Data/Entities/BaseItemEntity.cs2
-rw-r--r--Jellyfin.Data/Entities/UserData.cs21
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs22
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs2
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs1
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs11
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs70
8 files changed, 83 insertions, 57 deletions
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index c8c14c187..5e28333b2 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -16,6 +16,7 @@ using MediaBrowser.Model.Entities;
using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
+#pragma warning disable RS0030 // Do not use banned APIs
namespace Emby.Server.Implementations.Library
{
@@ -134,7 +135,9 @@ namespace Emby.Server.Implementations.Library
{
return new UserData()
{
- Key = dto.Key,
+ ItemId = Guid.Parse(dto.Key),
+ Item = null!,
+ User = null!,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
@@ -152,7 +155,7 @@ namespace Emby.Server.Implementations.Library
{
return new UserItemData()
{
- Key = dto.Key,
+ Key = dto.ItemId.ToString("D"),
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
@@ -182,12 +185,12 @@ namespace Emby.Server.Implementations.Library
{
using var context = _repository.CreateDbContext();
var key = keys.FirstOrDefault();
- if (key is null)
+ if (key is null || Guid.TryParse(key, out var itemId))
{
return null;
}
- var userData = context.UserData.AsNoTracking().FirstOrDefault(e => e.Key == key && e.UserId.Equals(userId));
+ var userData = context.UserData.AsNoTracking().FirstOrDefault(e => e.ItemId == itemId && e.UserId.Equals(userId));
if (userData is not null)
{
diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs
index a9f9b1793..8a6fb16a1 100644
--- a/Jellyfin.Data/Entities/BaseItemEntity.cs
+++ b/Jellyfin.Data/Entities/BaseItemEntity.cs
@@ -110,8 +110,6 @@ public class BaseItemEntity
public string? SeriesName { get; set; }
- public string? UserDataKey { get; set; }
-
public string? SeasonName { get; set; }
public string? ExternalSeriesId { get; set; }
diff --git a/Jellyfin.Data/Entities/UserData.cs b/Jellyfin.Data/Entities/UserData.cs
index 1204446d0..fe8c8c5ce 100644
--- a/Jellyfin.Data/Entities/UserData.cs
+++ b/Jellyfin.Data/Entities/UserData.cs
@@ -9,12 +9,6 @@ namespace Jellyfin.Data.Entities;
public class UserData
{
/// <summary>
- /// Gets or sets the key.
- /// </summary>
- /// <value>The key.</value>
- public required string Key { get; set; }
-
- /// <summary>
/// Gets or sets the users 0-10 rating.
/// </summary>
/// <value>The rating.</value>
@@ -70,12 +64,23 @@ public class UserData
public bool? Likes { get; set; }
/// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the BaseItem.
+ /// </summary>
+ public required BaseItemEntity? Item { get; set; }
+
+ /// <summary>
/// Gets or Sets the UserId.
/// </summary>
- public Guid UserId { get; set; }
+ public required Guid UserId { get; set; }
/// <summary>
/// Gets or Sets the User.
/// </summary>
- public User? User { get; set; }
+ public required User? User { get; set; }
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 36d976a43..a6cdfe61f 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -671,25 +671,25 @@ public sealed class BaseItemRepository(
if (filter.IsLiked.HasValue)
{
baseQuery = baseQuery
- .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue);
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLikeValue);
}
if (filter.IsFavoriteOrLiked.HasValue)
{
baseQuery = baseQuery
- .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavoriteOrLiked);
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavoriteOrLiked);
}
if (filter.IsFavorite.HasValue)
{
baseQuery = baseQuery
- .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavorite);
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorite);
}
if (filter.IsPlayed.HasValue)
{
baseQuery = baseQuery
- .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.Played == filter.IsPlayed.Value);
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Played == filter.IsPlayed.Value);
}
if (filter.IsResumable.HasValue)
@@ -697,12 +697,12 @@ public sealed class BaseItemRepository(
if (filter.IsResumable.Value)
{
baseQuery = baseQuery
- .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0);
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks > 0);
}
else
{
baseQuery = baseQuery
- .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0);
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks == 0);
}
}
@@ -2019,12 +2019,12 @@ public sealed class BaseItemRepository(
ItemSortBy.AirTime => e => e.SortName, // TODO
ItemSortBy.Runtime => e => e.RunTimeTicks,
ItemSortBy.Random => e => EF.Functions.Random(),
- ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.LastPlayedDate,
- ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.PlayCount,
- ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.IsFavorite,
+ ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.LastPlayedDate,
+ ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount,
+ ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite,
ItemSortBy.IsFolder => e => e.IsFolder,
- ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played,
- ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played,
+ ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
+ ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue),
ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue),
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index 5f5bf09af..048ad0ffa 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -121,7 +121,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
{
var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
query = query.Where(e => e.PersonType == personType)
- .Where(e => context.BaseItems.Where(d => context.UserData.Where(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id)).Any(f => f.Key == d.UserDataKey))
+ .Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id)))
.Select(f => f.Name).Contains(e.Name));
}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
index ab5403271..b8419a59f 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
@@ -35,7 +35,6 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
builder.HasIndex(e => e.ParentId);
builder.HasIndex(e => e.PresentationUniqueKey);
builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
- builder.HasIndex(e => new { e.UserDataKey, e.Type });
// covering index
builder.HasIndex(e => new { e.TopParentId, e.Id });
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs
index 1113adb7b..5ebdf8d59 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs
@@ -13,10 +13,11 @@ public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<UserData> builder)
{
- builder.HasKey(d => new { d.Key, d.UserId });
- builder.HasIndex(d => new { d.Key, d.UserId, d.Played });
- builder.HasIndex(d => new { d.Key, d.UserId, d.PlaybackPositionTicks });
- builder.HasIndex(d => new { d.Key, d.UserId, d.IsFavorite });
- builder.HasIndex(d => new { d.Key, d.UserId, d.LastPlayedDate });
+ builder.HasKey(d => new { d.ItemId, d.UserId });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.Played });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
+ builder.HasOne(e => e.Item);
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 824c72e55..56465f8c1 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data;
using System.Diagnostics;
@@ -71,43 +72,55 @@ public class MigrateLibraryDb : IMigrationRoutine
connection.Open();
using var dbContext = _provider.CreateDbContext();
+ var stepElapsed = stopwatch.Elapsed;
+ _logger.LogInformation("Saving UserData entries took {0}.", stepElapsed);
+
+ _logger.LogInformation("Start moving TypedBaseItem.");
+ var typedBaseItemsQuery = "SELECT type, data, StartDate, EndDate, ChannelId, IsMovie, IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, guid, Genres, ParentId, Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId FROM TypedBaseItems";
+ dbContext.BaseItems.ExecuteDelete();
+
+ var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
+ foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
+ {
+ var baseItem = GetItem(dto);
+ dbContext.BaseItems.Add(baseItem.BaseItem);
+ legacyBaseItemWithUserKeys[baseItem.LegacyUserDataKey] = baseItem.BaseItem;
+ }
+
+ _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count);
+ dbContext.SaveChanges();
+ stepElapsed = stopwatch.Elapsed - stepElapsed;
+ _logger.LogInformation("Saving BaseItems entries took {0}.", stepElapsed);
+
_logger.LogInformation("Start moving UserData.");
var queryResult = connection.Query("SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas");
dbContext.UserData.ExecuteDelete();
var users = dbContext.Users.AsNoTracking().ToImmutableArray();
+ var oldUserdata = new Dictionary<string, UserData>();
foreach (var entity in queryResult)
{
var userData = GetUserData(users, entity);
- if (userData is null)
+ if (userData.Data is null)
{
_logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0));
continue;
}
- dbContext.UserData.Add(userData);
- }
-
- _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
- dbContext.SaveChanges();
- var stepElapsed = stopwatch.Elapsed;
- _logger.LogInformation("Saving UserData entries took {0}.", stepElapsed);
-
- _logger.LogInformation("Start moving TypedBaseItem.");
- var typedBaseItemsQuery = "SELECT type, data, StartDate, EndDate, ChannelId, IsMovie, IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, guid, Genres, ParentId, Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId FROM TypedBaseItems";
- dbContext.BaseItems.ExecuteDelete();
+ if (!legacyBaseItemWithUserKeys.TryGetValue(userData.LegacyUserDataKey!, out var refItem))
+ {
+ _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
+ continue;
+ }
- foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
- {
- dbContext.BaseItems.Add(GetItem(dto));
+ userData.Data.ItemId = refItem.Id;
+ dbContext.UserData.Add(userData.Data);
}
- _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count);
+ _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
dbContext.SaveChanges();
- stepElapsed = stopwatch.Elapsed - stepElapsed;
- _logger.LogInformation("Saving BaseItems entries took {0}.", stepElapsed);
_logger.LogInformation("Start moving MediaStreamInfos.");
var mediaStreamQuery = "SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path, IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width, AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag, Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer, DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired FROM MediaStreams";
@@ -266,17 +279,19 @@ public class MigrateLibraryDb : IMigrationRoutine
}
}
- private static UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
+ private static (UserData? Data, string? LegacyUserDataKey) GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
{
var indexOfUser = dto.GetInt32(1);
if (users.Length < indexOfUser)
{
- return null;
+ return (null, null);
}
- return new UserData()
+ var oldKey = dto.GetString(0);
+
+ return (new UserData()
{
- Key = dto.GetString(0),
+ ItemId = Guid.NewGuid(),
UserId = users.ElementAt(indexOfUser).Id,
Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
Played = dto.GetBoolean(3),
@@ -288,7 +303,8 @@ public class MigrateLibraryDb : IMigrationRoutine
SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
Likes = null,
User = null!,
- };
+ Item = null!
+ }, oldKey);
}
private AncestorId GetAncestorId(SqliteDataReader reader)
@@ -604,7 +620,7 @@ public class MigrateLibraryDb : IMigrationRoutine
return item;
}
- private BaseItemEntity GetItem(SqliteDataReader reader)
+ private (BaseItemEntity BaseItem, string LegacyUserDataKey) GetItem(SqliteDataReader reader)
{
var entity = new BaseItemEntity()
{
@@ -870,6 +886,10 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.SeriesName = seriesName;
}
+ if (reader.TryGetString(index++, out var userDataKey))
+ {
+ }
+
if (reader.TryGetString(index++, out var seasonName))
{
entity.SeasonName = seasonName;
@@ -971,7 +991,7 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.OwnerId = ownerId.ToString("N");
}
- return entity;
+ return (entity, userDataKey);
}
private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)