Migrated UserData from library sqlite db to jellyfin.db

pull/12798/head
JPVenson 7 months ago committed by GitHub
parent 57452d65ef
commit d0b4b2ddb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -492,7 +492,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
@ -580,7 +579,6 @@ namespace Emby.Server.Implementations
}
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);

@ -1,369 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
{
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
{
private readonly IUserManager _userManager;
public SqliteUserDataRepository(
ILogger<SqliteUserDataRepository> logger,
IServerConfigurationManager config,
IUserManager userManager)
: base(logger)
{
_userManager = userManager;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
public override void Initialize()
{
base.Initialize();
using (var connection = GetConnection())
{
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users;
using var transaction = connection.BeginTransaction();
connection.Execute(string.Join(
';',
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
if (!userDataTableExists)
{
transaction.Commit();
return;
}
var existingColumnNames = GetColumnNames(connection, "userdata");
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (userDatasTableExists)
{
return;
}
ImportUserIds(connection, users);
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
transaction.Commit();
}
}
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
{
foreach (var user in users)
{
if (!userIdsWithUserData.Contains(user.Id))
{
continue;
}
statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
statement.ExecuteNonQuery();
}
}
}
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
{
var list = new List<Guid>();
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
{
foreach (var row in statement.ExecuteQuery())
{
try
{
list.Add(row.GetGuid(0));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error while getting user");
}
}
}
return list;
}
/// <inheritdoc />
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
ArgumentException.ThrowIfNullOrEmpty(key);
PersistUserData(userId, key, userData, cancellationToken);
}
/// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
PersistAllUserData(userId, userData, cancellationToken);
}
/// <summary>
/// Persists the user data.
/// </summary>
/// <param name="internalUserId">The user id.</param>
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
SaveUserData(connection, internalUserId, key, userData);
transaction.Commit();
}
}
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
statement.TryBind("@userId", internalUserId);
statement.TryBind("@key", key);
if (userData.Rating.HasValue)
{
statement.TryBind("@rating", userData.Rating.Value);
}
else
{
statement.TryBindNull("@rating");
}
statement.TryBind("@played", userData.Played);
statement.TryBind("@playCount", userData.PlayCount);
statement.TryBind("@isFavorite", userData.IsFavorite);
statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
if (userData.LastPlayedDate.HasValue)
{
statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
}
else
{
statement.TryBindNull("@lastPlayedDate");
}
if (userData.AudioStreamIndex.HasValue)
{
statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
}
else
{
statement.TryBindNull("@AudioStreamIndex");
}
if (userData.SubtitleStreamIndex.HasValue)
{
statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
}
else
{
statement.TryBindNull("@SubtitleStreamIndex");
}
statement.ExecuteNonQuery();
}
}
/// <summary>
/// Persist all user data for the specified user.
/// </summary>
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
foreach (var userItemData in userDataList)
{
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
}
transaction.Commit();
}
}
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
/// key.
/// </exception>
public UserItemData GetUserData(long userId, string key)
{
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
{
return ReadRow(row);
}
}
return null;
}
}
public UserItemData GetUserData(long userId, List<string> keys)
{
ArgumentNullException.ThrowIfNull(keys);
if (keys.Count == 0)
{
return null;
}
return GetUserData(userId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
/// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long userId)
{
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
var list = new List<UserItemData>();
using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery())
{
list.Add(ReadRow(row));
}
}
}
return list;
}
/// <summary>
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
private UserItemData ReadRow(SqliteDataReader reader)
{
var userData = new UserItemData
{
Key = reader.GetString(0)
};
if (reader.TryGetDouble(2, out var rating))
{
userData.Rating = rating;
}
userData.Played = reader.GetBoolean(3);
userData.PlayCount = reader.GetInt32(4);
userData.IsFavorite = reader.GetBoolean(5);
userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
userData.LastPlayedDate = lastPlayedDate;
}
if (reader.TryGetInt32(8, out var audioStreamIndex))
{
userData.AudioStreamIndex = audioStreamIndex;
}
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
{
userData.SubtitleStreamIndex = subtitleStreamIndex;
}
return userData;
}
}
}

@ -3,15 +3,17 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@ -26,22 +28,18 @@ namespace Emby.Server.Implementations.Library
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository;
private readonly IDbContextFactory<JellyfinDbContext> _repository;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public UserDataManager(
IServerConfigurationManager config,
IUserManager userManager,
IUserDataRepository repository)
IDbContextFactory<JellyfinDbContext> repository)
{
_config = config;
_userManager = userManager;
_repository = repository;
}
@ -61,11 +59,16 @@ namespace Emby.Server.Implementations.Library
var userId = user.InternalId;
using var repository = _repository.CreateDbContext();
foreach (var key in keys)
{
_repository.SaveUserData(userId, key, userData, cancellationToken);
userData.Key = key;
repository.UserData.Add(Map(userData, user.Id));
}
repository.SaveChanges();
var cacheKey = GetCacheKey(userId, item.Id);
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
@ -87,7 +90,7 @@ namespace Emby.Server.Implementations.Library
ArgumentNullException.ThrowIfNull(reason);
ArgumentNullException.ThrowIfNull(userDataDto);
var userData = GetUserData(user, item);
var userData = GetUserData(user, item) ?? throw new InvalidOperationException("Did not expect UserData to be null.");
if (userDataDto.PlaybackPositionTicks.HasValue)
{
@ -127,22 +130,68 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
private UserData Map(UserItemData dto, Guid userId)
{
var userId = user.InternalId;
return new UserData()
{
Key = dto.Key,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
UserId = userId,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
}
private UserItemData Map(UserData dto)
{
return new UserItemData()
{
Key = dto.Key,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
}
var cacheKey = GetCacheKey(userId, itemId);
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
{
var cacheKey = GetCacheKey(user.InternalId, itemId);
var data = GetUserDataInternal(user.Id, keys);
return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
if (data is null)
{
return null;
}
return _userData.GetOrAdd(cacheKey, data);
}
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
private UserItemData? GetUserDataInternal(Guid userId, List<string> keys)
{
var userData = _repository.GetUserData(internalUserId, keys);
using var context = _repository.CreateDbContext();
var key = keys.FirstOrDefault();
if (key is null)
{
return null;
}
var userData = context.UserData.AsNoTracking().FirstOrDefault(e => e.Key == key && e.UserId.Equals(userId));
if (userData is not null)
{
return userData;
return Map(userData);
}
if (keys.Count > 0)
@ -166,7 +215,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public UserItemData GetUserData(User user, BaseItem item)
public UserItemData? GetUserData(User user, BaseItem item)
{
return GetUserData(user, item.Id, item.GetUserDataKeys());
}
@ -178,7 +227,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
var userData = GetUserData(user, item) ?? throw new InvalidOperationException("Did not expect UserData to be null.");
var dto = GetUserItemDataDto(userData);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);

@ -262,7 +262,7 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
if (userData.PlaybackPositionTicks > 0)
if (userData?.PlaybackPositionTicks > 0)
{
return null;
}
@ -275,6 +275,11 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
if (userData is null)
{
return (DateTime.MinValue, GetEpisode);
}
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
return (lastWatchedDate, GetEpisode);

@ -662,10 +662,13 @@ public class UserLibraryController : BaseJellyfinApiController
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
// Set favorite status
data.IsFavorite = isFavorite;
if (data is not null)
{
// Set favorite status
data.IsFavorite = isFavorite;
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
}
return _userDataRepository.GetUserDataDto(item, user);
}
@ -681,9 +684,12 @@ public class UserLibraryController : BaseJellyfinApiController
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
data.Likes = likes;
if (data is not null)
{
data.Likes = likes;
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
}
return _userDataRepository.GetUserDataDto(item, user);
}

@ -0,0 +1,73 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Jellyfin.Data.Entities;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
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>
public double? Rating { get; set; }
/// <summary>
/// Gets or sets the playback position ticks.
/// </summary>
/// <value>The playback position ticks.</value>
public long PlaybackPositionTicks { get; set; }
/// <summary>
/// Gets or sets the play count.
/// </summary>
/// <value>The play count.</value>
public int PlayCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is favorite.
/// </summary>
/// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
public bool IsFavorite { get; set; }
/// <summary>
/// Gets or sets the last played date.
/// </summary>
/// <value>The last played date.</value>
public DateTime? LastPlayedDate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="UserData" /> is played.
/// </summary>
/// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
public bool Played { get; set; }
/// <summary>
/// Gets or sets the index of the audio stream.
/// </summary>
/// <value>The index of the audio stream.</value>
public int? AudioStreamIndex { get; set; }
/// <summary>
/// Gets or sets the index of the subtitle stream.
/// </summary>
/// <value>The index of the subtitle stream.</value>
public int? SubtitleStreamIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the item is liked or not.
/// This should never be serialized.
/// </summary>
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
public bool? Likes { get; set; }
public Guid UserId { get; set; }
public User? User { get; set; }
}

@ -88,6 +88,11 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<UserData> UserData => Set<UserData>();
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();

@ -0,0 +1,775 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDbContext))]
[Migration("20240907123425_UserDataInJfLib")]
partial class UserDataInJfLib
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<long>("EndTicks")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("SegmentProviderId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long>("StartTicks")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("MediaSegments");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.Property<int>("Bandwidth")
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<int>("Interval")
.HasColumnType("INTEGER");
b.Property<int>("ThumbnailCount")
.HasColumnType("INTEGER");
b.Property<int>("TileHeight")
.HasColumnType("INTEGER");
b.Property<int>("TileWidth")
.HasColumnType("INTEGER");
b.HasKey("ItemId", "Width");
b.ToTable("TrickplayInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("CastReceiverId")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
{
b.Property<int?>("AudioStreamIndex")
.HasColumnType("INTEGER");
b.Property<bool>("IsFavorite")
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("LastPlayedDate")
.HasColumnType("TEXT");
b.Property<bool?>("Likes")
.HasColumnType("INTEGER");
b.Property<int>("PlayCount")
.HasColumnType("INTEGER");
b.Property<long>("PlaybackPositionTicks")
.HasColumnType("INTEGER");
b.Property<bool>("Played")
.HasColumnType("INTEGER");
b.Property<double?>("Rating")
.HasColumnType("REAL");
b.Property<int?>("SubtitleStreamIndex")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasIndex("UserId");
b.HasIndex("Key", "UserId")
.IsUnique();
b.HasIndex("Key", "UserId", "IsFavorite");
b.HasIndex("Key", "UserId", "LastPlayedDate");
b.HasIndex("Key", "UserId", "PlaybackPositionTicks");
b.HasIndex("Key", "UserId", "Played");
b.ToTable("UserData");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,79 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class UserDataInJfLib : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserData",
columns: table => new
{
Key = table.Column<string>(type: "TEXT", nullable: false),
Rating = table.Column<double>(type: "REAL", nullable: true),
PlaybackPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
PlayCount = table.Column<int>(type: "INTEGER", nullable: false),
IsFavorite = table.Column<bool>(type: "INTEGER", nullable: false),
LastPlayedDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Played = table.Column<bool>(type: "INTEGER", nullable: false),
AudioStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
SubtitleStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
Likes = table.Column<bool>(type: "INTEGER", nullable: true),
UserId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.ForeignKey(
name: "FK_UserData_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_UserData_Key_UserId",
table: "UserData",
columns: new[] { "Key", "UserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserData_Key_UserId_IsFavorite",
table: "UserData",
columns: new[] { "Key", "UserId", "IsFavorite" });
migrationBuilder.CreateIndex(
name: "IX_UserData_Key_UserId_LastPlayedDate",
table: "UserData",
columns: new[] { "Key", "UserId", "LastPlayedDate" });
migrationBuilder.CreateIndex(
name: "IX_UserData_Key_UserId_PlaybackPositionTicks",
table: "UserData",
columns: new[] { "Key", "UserId", "PlaybackPositionTicks" });
migrationBuilder.CreateIndex(
name: "IX_UserData_Key_UserId_Played",
table: "UserData",
columns: new[] { "Key", "UserId", "Played" });
migrationBuilder.CreateIndex(
name: "IX_UserData_UserId",
table: "UserData",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserData");
}
}
}

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -612,6 +612,58 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
{
b.Property<int?>("AudioStreamIndex")
.HasColumnType("INTEGER");
b.Property<bool>("IsFavorite")
.HasColumnType("INTEGER");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("LastPlayedDate")
.HasColumnType("TEXT");
b.Property<bool?>("Likes")
.HasColumnType("INTEGER");
b.Property<int>("PlayCount")
.HasColumnType("INTEGER");
b.Property<long>("PlaybackPositionTicks")
.HasColumnType("INTEGER");
b.Property<bool>("Played")
.HasColumnType("INTEGER");
b.Property<double?>("Rating")
.HasColumnType("REAL");
b.Property<int?>("SubtitleStreamIndex")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasIndex("UserId");
b.HasIndex("Key", "UserId")
.IsUnique();
b.HasIndex("Key", "UserId", "IsFavorite");
b.HasIndex("Key", "UserId", "LastPlayedDate");
b.HasIndex("Key", "UserId", "PlaybackPositionTicks");
b.HasIndex("Key", "UserId", "Played");
b.ToTable("UserData");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
@ -683,6 +735,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");

@ -0,0 +1,23 @@
using System;
using Jellyfin.Data.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Jellyfin.Server.Implementations.ModelConfiguration;
/// <summary>
/// FluentAPI configuration for the UserData entity.
/// </summary>
public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<UserData> builder)
{
builder.HasNoKey();
builder.HasIndex(d => new { d.Key, d.UserId }).IsUnique();
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 });
}
}

@ -0,0 +1,89 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// The migration routine for migrating the userdata database to EF Core.
/// </summary>
public class MigrateUserData : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateUserData"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="provider">The database provider.</param>
/// <param name="paths">The server application paths.</param>
public MigrateUserData(
ILogger<MigrateUserDb> logger,
IDbContextFactory<JellyfinDbContext> provider,
IServerApplicationPaths paths)
{
_logger = logger;
_provider = provider;
_paths = paths;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("5bcb4197-e7c0-45aa-9902-963bceab5798");
/// <inheritdoc/>
public string Name => "MigrateUserData";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
_logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin.");
var dataPath = _paths.DataPath;
using var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}");
connection.Open();
using var dbContext = _provider.CreateDbContext();
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();
foreach (SqliteDataReader dto in queryResult)
{
var entity = new UserData()
{
Key = dto.GetString(0),
UserId = users.ElementAt(dto.GetInt32(1)).Id,
Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
Played = dto.GetBoolean(3),
PlayCount = dto.GetInt32(4),
IsFavorite = dto.GetBoolean(5),
PlaybackPositionTicks = dto.GetInt64(6),
LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
};
dbContext.UserData.Add(entity);
}
dbContext.SaveChanges();
}
}

@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">User to use.</param>
/// <param name="item">Item to use.</param>
/// <returns>User data.</returns>
UserItemData GetUserData(User user, BaseItem item);
UserItemData? GetUserData(User user, BaseItem item);
/// <summary>
/// Gets the user data dto.

@ -1,55 +0,0 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Persistence
{
/// <summary>
/// Provides an interface to implement a UserData repository.
/// </summary>
public interface IUserDataRepository : IDisposable
{
/// <summary>
/// Saves the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken);
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>The user data.</returns>
UserItemData GetUserData(long userId, string key);
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="keys">The keys.</param>
/// <returns>The user data.</returns>
UserItemData GetUserData(long userId, List<string> keys);
/// <summary>
/// Return all user data associated with the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>The list of user item data.</returns>
List<UserItemData> GetAllUserData(long userId);
/// <summary>
/// Save all user data associated with the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="userData">The user item data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken);
}
}

@ -312,8 +312,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (user is not null)
{
userData = _userDataManager.GetUserData(user, item);
userData.Played = played;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
if (userData is not null)
{
userData.Played = played;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
}
}
@ -326,8 +329,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (user is not null)
{
userData = _userDataManager.GetUserData(user, item);
userData.PlayCount = count;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
if (userData is not null)
{
userData.PlayCount = count;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
}
}
@ -340,8 +346,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (user is not null)
{
userData = _userDataManager.GetUserData(user, item);
userData.LastPlayedDate = lastPlayed;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
if (userData is not null)
{
userData.LastPlayedDate = lastPlayed;
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
}
}

@ -869,43 +869,46 @@ namespace MediaBrowser.XbmcMetadata.Savers
var userdata = userDataRepo.GetUserData(user, item);
writer.WriteElementString(
"isuserfavorite",
userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
if (userdata.Rating.HasValue)
{
writer.WriteElementString(
"userrating",
userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
}
if (!item.IsFolder)
if (userdata is not null)
{
writer.WriteElementString(
"playcount",
userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(
"watched",
userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
"isuserfavorite",
userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
if (userdata.LastPlayedDate.HasValue)
if (userdata.Rating.HasValue)
{
writer.WriteElementString(
"lastplayed",
userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant());
"userrating",
userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
}
writer.WriteStartElement("resume");
if (!item.IsFolder)
{
writer.WriteElementString(
"playcount",
userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(
"watched",
userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
var runTimeTicks = item.RunTimeTicks ?? 0;
if (userdata.LastPlayedDate.HasValue)
{
writer.WriteElementString(
"lastplayed",
userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant());
}
writer.WriteElementString(
"position",
TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(
"total",
TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
writer.WriteStartElement("resume");
var runTimeTicks = item.RunTimeTicks ?? 0;
writer.WriteElementString(
"position",
TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(
"total",
TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
}
writer.WriteEndElement();

@ -149,7 +149,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(new DateTime(2019, 8, 6, 9, 1, 18), item.DateCreated);
// userData
var userData = _userDataManager.GetUserData(_testUser, item);
var userData = _userDataManager.GetUserData(_testUser, item)!;
Assert.Equal(2, userData.PlayCount);
Assert.True(userData.Played);
Assert.Equal(new DateTime(2021, 02, 11, 07, 47, 23), userData.LastPlayedDate);

Loading…
Cancel
Save