< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.UserDataManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/UserDataManager.cs
Line coverage
37%
Covered lines: 91
Uncovered lines: 153
Coverable lines: 244
Total lines: 551
Line coverage: 37.2%
Branch coverage
25%
Covered branches: 35
Total branches: 136
Branch coverage: 25.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/2/2026 - 12:12:50 AM Line coverage: 15.9% (26/163) Branch coverage: 5.8% (4/68) Total lines: 3685/4/2026 - 12:15:16 AM Line coverage: 32.9% (57/173) Branch coverage: 25.6% (21/82) Total lines: 3895/20/2026 - 12:15:44 AM Line coverage: 32.9% (57/173) Branch coverage: 24.3% (20/82) Total lines: 3897/6/2026 - 12:16:28 AM Line coverage: 28.5% (67/235) Branch coverage: 21% (27/128) Total lines: 5267/21/2026 - 12:16:33 AM Line coverage: 37% (90/243) Branch coverage: 25.7% (35/136) Total lines: 5507/26/2026 - 12:17:51 AM Line coverage: 37.2% (91/244) Branch coverage: 25.7% (35/136) Total lines: 551 5/2/2026 - 12:12:50 AM Line coverage: 15.9% (26/163) Branch coverage: 5.8% (4/68) Total lines: 3685/4/2026 - 12:15:16 AM Line coverage: 32.9% (57/173) Branch coverage: 25.6% (21/82) Total lines: 3895/20/2026 - 12:15:44 AM Line coverage: 32.9% (57/173) Branch coverage: 24.3% (20/82) Total lines: 3897/6/2026 - 12:16:28 AM Line coverage: 28.5% (67/235) Branch coverage: 21% (27/128) Total lines: 5267/21/2026 - 12:16:33 AM Line coverage: 37% (90/243) Branch coverage: 25.7% (35/136) Total lines: 5507/26/2026 - 12:17:51 AM Line coverage: 37.2% (91/244) Branch coverage: 25.7% (35/136) Total lines: 551

Coverage delta

Coverage delta 20 -20

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SaveUserData(...)0%4260%
SaveUserData(...)0%272160%
Map(...)100%210%
Map(...)100%11100%
GetUserDataBatch(...)72.72%222290.62%
GetResumeUserData(...)100%11100%
GetResumeUserDataBatch(...)15.78%7773820%
GetCacheKey(...)100%11100%
GetUserData(...)75%44100%
ResolveUserDataRow(...)80%1010100%
GetUserDataDto(...)100%210%
GetUserDataDto(...)50%4485.71%
GetUserItemDataDto(...)100%11100%
UpdatePlayState(...)0%930300%
ResetPlaybackStreamSelections(...)0%4260%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/UserDataManager.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.Linq;
 7using System.Threading;
 8using BitFaster.Caching.Lru;
 9using Jellyfin.Database.Implementations;
 10using Jellyfin.Database.Implementations.Entities;
 11using MediaBrowser.Controller.Configuration;
 12using MediaBrowser.Controller.Dto;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Library;
 15using MediaBrowser.Model.Dto;
 16using MediaBrowser.Model.Entities;
 17using Microsoft.EntityFrameworkCore;
 18using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
 19using Book = MediaBrowser.Controller.Entities.Book;
 20
 21namespace Emby.Server.Implementations.Library
 22{
 23    /// <summary>
 24    /// Class UserDataManager.
 25    /// </summary>
 26    public class UserDataManager : IUserDataManager
 27    {
 28        private readonly IServerConfigurationManager _config;
 29        private readonly IDbContextFactory<JellyfinDbContext> _repository;
 30        private readonly FastConcurrentLru<string, UserItemData> _cache;
 31
 32        /// <summary>
 33        /// Initializes a new instance of the <see cref="UserDataManager"/> class.
 34        /// </summary>
 35        /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
 36        /// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</para
 37        public UserDataManager(
 38            IServerConfigurationManager config,
 39            IDbContextFactory<JellyfinDbContext> repository)
 40        {
 2941            _config = config;
 2942            _repository = repository;
 2943            _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.Cache
 2944        }
 45
 46        /// <inheritdoc />
 47        public event EventHandler<UserDataSaveEventArgs>? UserDataSaved;
 48
 49        /// <inheritdoc />
 50        public void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, Cancellatio
 51        {
 052            ArgumentNullException.ThrowIfNull(userData);
 53
 054            ArgumentNullException.ThrowIfNull(item);
 55
 056            cancellationToken.ThrowIfCancellationRequested();
 57
 058            var keys = item.GetUserDataKeys();
 59
 060            using var dbContext = _repository.CreateDbContext();
 061            using var transaction = dbContext.Database.BeginTransaction();
 62
 063            foreach (var key in keys)
 64            {
 065                userData.Key = key;
 066                var userDataEntry = Map(userData, user.Id, item.Id);
 067                if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && 
 68                {
 069                    dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
 70                }
 71                else
 72                {
 073                    dbContext.UserData.Add(userDataEntry);
 74                }
 75            }
 76
 077            dbContext.SaveChanges();
 078            transaction.Commit();
 79
 080            var userId = user.InternalId;
 081            var cacheKey = GetCacheKey(userId, item.Id);
 082            _cache.AddOrUpdate(cacheKey, userData);
 083            item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate th
 84
 085            UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
 086            {
 087                Keys = keys,
 088                UserData = userData,
 089                SaveReason = reason,
 090                UserId = user.Id,
 091                Item = item
 092            });
 093        }
 94
 95        /// <inheritdoc />
 96        public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason)
 97        {
 098            ArgumentNullException.ThrowIfNull(user);
 099            ArgumentNullException.ThrowIfNull(item);
 0100            ArgumentNullException.ThrowIfNull(userDataDto);
 101
 0102            var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null."
 103
 0104            if (userDataDto.PlaybackPositionTicks.HasValue)
 105            {
 0106                userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value;
 107            }
 108
 0109            if (userDataDto.PlayCount.HasValue)
 110            {
 0111                userData.PlayCount = userDataDto.PlayCount.Value;
 112            }
 113
 0114            if (userDataDto.IsFavorite.HasValue)
 115            {
 0116                userData.IsFavorite = userDataDto.IsFavorite.Value;
 117            }
 118
 0119            if (userDataDto.Likes.HasValue)
 120            {
 0121                userData.Likes = userDataDto.Likes.Value;
 122            }
 123
 0124            if (userDataDto.Played.HasValue)
 125            {
 0126                userData.Played = userDataDto.Played.Value;
 127            }
 128
 0129            if (userDataDto.LastPlayedDate.HasValue)
 130            {
 0131                userData.LastPlayedDate = userDataDto.LastPlayedDate.Value;
 132            }
 133
 0134            if (userDataDto.Rating.HasValue)
 135            {
 0136                userData.Rating = userDataDto.Rating.Value;
 137            }
 138
 0139            SaveUserData(user, item, userData, reason, CancellationToken.None);
 0140        }
 141
 142        private UserData Map(UserItemData dto, Guid userId, Guid itemId)
 143        {
 0144            return new UserData()
 0145            {
 0146                ItemId = itemId,
 0147                CustomDataKey = dto.Key,
 0148                Item = null,
 0149                User = null,
 0150                AudioStreamIndex = dto.AudioStreamIndex,
 0151                IsFavorite = dto.IsFavorite,
 0152                LastPlayedDate = dto.LastPlayedDate,
 0153                Likes = dto.Likes,
 0154                PlaybackPositionTicks = dto.PlaybackPositionTicks,
 0155                PlayCount = dto.PlayCount,
 0156                Played = dto.Played,
 0157                Rating = dto.Rating,
 0158                UserId = userId,
 0159                SubtitleStreamIndex = dto.SubtitleStreamIndex,
 0160            };
 161        }
 162
 163        private static UserItemData Map(UserData dto)
 164        {
 6165            return new UserItemData()
 6166            {
 6167                Key = dto.CustomDataKey!,
 6168                AudioStreamIndex = dto.AudioStreamIndex,
 6169                IsFavorite = dto.IsFavorite,
 6170                LastPlayedDate = dto.LastPlayedDate,
 6171                Likes = dto.Likes,
 6172                PlaybackPositionTicks = dto.PlaybackPositionTicks,
 6173                PlayCount = dto.PlayCount,
 6174                Played = dto.Played,
 6175                Rating = dto.Rating,
 6176                SubtitleStreamIndex = dto.SubtitleStreamIndex,
 6177            };
 178        }
 179
 180        /// <inheritdoc />
 181        public Dictionary<Guid, UserItemData> GetUserDataBatch(IReadOnlyList<BaseItem> items, User user)
 182        {
 5183            var result = new Dictionary<Guid, UserItemData>(items.Count);
 5184            var itemsNeedingQuery = new List<(BaseItem Item, List<string> Keys)>();
 185
 20186            foreach (var item in items)
 187            {
 5188                var cacheKey = GetCacheKey(user.InternalId, item.Id);
 5189                if (_cache.TryGet(cacheKey, out var cachedData))
 190                {
 2191                    result[item.Id] = cachedData;
 192                }
 193                else
 194                {
 3195                    var userDataRow = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
 3196                    var userData = userDataRow is not null ? Map(userDataRow) : null;
 3197                    if (userData is not null)
 198                    {
 0199                        result[item.Id] = userData;
 0200                        _cache.AddOrUpdate(cacheKey, userData);
 201                    }
 202                    else
 203                    {
 3204                        var keys = item.GetUserDataKeys();
 3205                        itemsNeedingQuery.Add((item, keys));
 206                    }
 207                }
 208            }
 209
 5210            if (itemsNeedingQuery.Count == 0)
 211            {
 3212                return result;
 213            }
 214
 215            // Build a single query for all missing items. Fetch rows by item alone so rows kept
 216            // under keys from older metadata resolve the same way as the in-memory path.
 2217            var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
 2218            using var context = _repository.CreateDbContext();
 2219            var userDataArray = context.UserData
 2220                .AsNoTracking()
 2221                .Where(e => e.UserId.Equals(user.Id))
 2222                .WhereOneOrMany(allItemIds, e => e.ItemId)
 2223                .ToArray();
 224
 2225            var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
 10226            foreach (var (item, keys) in itemsNeedingQuery)
 227            {
 228                UserItemData userData;
 3229                if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
 230                {
 2231                    userData = Map(ResolveUserDataRow(item, itemUserData)!);
 232                }
 233                else
 234                {
 1235                    userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
 236                }
 237
 3238                result[item.Id] = userData;
 3239                var cacheKey = GetCacheKey(user.InternalId, item.Id);
 3240                _cache.AddOrUpdate(cacheKey, userData);
 241            }
 242
 2243            return result;
 2244        }
 245
 246        /// <inheritdoc />
 247        public VersionResumeData? GetResumeUserData(User user, BaseItem item)
 248        {
 6249            return GetResumeUserDataBatch([item], user).GetValueOrDefault(item.Id);
 250        }
 251
 252        /// <inheritdoc />
 253        public IReadOnlyDictionary<Guid, VersionResumeData> GetResumeUserDataBatch(IReadOnlyList<BaseItem> items, User u
 254        {
 10255            ArgumentNullException.ThrowIfNull(user);
 256
 10257            var result = new Dictionary<Guid, VersionResumeData>();
 258
 259            // Candidate primaries: a directly queried version (PrimaryVersionId set) keeps its own data.
 260            // Linked alternates are already known in memory; only the local-alternate existence check
 261            // would otherwise hit the database (one query per item via Video.HasLocalAlternateVersions),
 262            // so collect those ids and resolve them all in a single query below.
 10263            List<Video>? candidates = null;
 10264            List<Guid>? localProbeIds = null;
 38265            foreach (var item in items)
 266            {
 9267                if (item is not Video video || video.PrimaryVersionId.HasValue)
 268                {
 269                    continue;
 270                }
 271
 0272                (candidates ??= []).Add(video);
 273
 0274                if (video.LinkedAlternateVersions.Length == 0)
 275                {
 0276                    (localProbeIds ??= []).Add(video.Id);
 277                }
 278            }
 279
 10280            if (candidates is null)
 281            {
 10282                return result;
 283            }
 284
 0285            HashSet<Guid>? withLocalAlternates = null;
 0286            if (localProbeIds is not null)
 287            {
 0288                using var dbContext = _repository.CreateDbContext();
 0289                withLocalAlternates = dbContext.LinkedChildren
 0290                    .Where(lc => lc.ChildType == Jellyfin.Database.Implementations.Entities.LinkedChildType.LocalAlterna
 0291                    .WhereOneOrMany(localProbeIds, lc => lc.ParentId)
 0292                    .Select(lc => lc.ParentId)
 0293                    .Distinct()
 0294                    .ToHashSet();
 295            }
 296
 0297            List<(Guid PrimaryId, IReadOnlyList<Video> Versions)>? versionGroups = null;
 0298            List<BaseItem>? allVersions = null;
 299
 0300            foreach (var video in candidates)
 301            {
 302                // Only items that actually have alternate versions aggregate over them.
 0303                if (video.LinkedAlternateVersions.Length == 0
 0304                    && (withLocalAlternates is null || !withLocalAlternates.Contains(video.Id)))
 305                {
 306                    continue;
 307                }
 308
 0309                var versions = video.GetAllVersions();
 0310                if (versions.Count < 2)
 311                {
 312                    continue;
 313                }
 314
 0315                (versionGroups ??= []).Add((video.Id, versions));
 0316                (allVersions ??= []).AddRange(versions);
 317            }
 318
 0319            if (versionGroups is null)
 320            {
 0321                return result;
 322            }
 323
 0324            var userDataByVersion = GetUserDataBatch(allVersions!.DistinctBy(i => i.Id).ToList(), user);
 325
 0326            foreach (var (primaryId, versions) in versionGroups)
 327            {
 328                // Consider both in-progress and completed versions so a finished alternate still marks the primary as p
 0329                var resumeVersion = VersionPlaybackSelector.SelectMostRecentlyPlayed(
 0330                    versions,
 0331                    version => userDataByVersion.GetValueOrDefault(version.Id),
 0332                    data => data.PlaybackPositionTicks > 0 || data.Played);
 333
 0334                if (resumeVersion is not null)
 335                {
 0336                    result[primaryId] = new VersionResumeData(resumeVersion.Id, userDataByVersion[resumeVersion.Id]);
 337                }
 338            }
 339
 0340            return result;
 341        }
 342
 343        /// <summary>
 344        /// Gets the internal key.
 345        /// </summary>
 346        /// <returns>System.String.</returns>
 347        private static string GetCacheKey(long internalUserId, Guid itemId)
 348        {
 8349            return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N", CultureInfo.Invari
 350        }
 351
 352        /// <inheritdoc />
 353        public UserItemData? GetUserData(User user, BaseItem item)
 354        {
 12355            ArgumentNullException.ThrowIfNull(user);
 11356            var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
 11357            return row is not null ? Map(row) : new UserItemData()
 11358            {
 11359                Key = item.GetUserDataKeys()[0],
 11360            };
 361        }
 362
 363        /// <summary>
 364        /// Picks the row matching the item's current user data keys, in key order, so rows left behind
 365        /// under keys from older metadata don't take priority over the rows the write path updates.
 366        /// </summary>
 367        /// <param name="item">The item whose keys to match.</param>
 368        /// <param name="rows">The candidate user data rows for a single user.</param>
 369        /// <returns>The best matching row, or <c>null</c> when there are none.</returns>
 370        private static UserData? ResolveUserDataRow(BaseItem item, IEnumerable<UserData>? rows)
 371        {
 16372            var candidates = rows?.ToList();
 16373            if (candidates is null || candidates.Count == 0)
 374            {
 10375                return null;
 376            }
 377
 26378            foreach (var key in item.GetUserDataKeys())
 379            {
 9380                var match = candidates.Find(e => string.Equals(e.CustomDataKey, key, StringComparison.Ordinal));
 9381                if (match is not null)
 382                {
 4383                    return match;
 384                }
 385            }
 386
 2387            return candidates[0];
 4388        }
 389
 390        /// <inheritdoc />
 391        public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
 0392            => GetUserDataDto(item, null, user, new DtoOptions());
 393
 394        /// <inheritdoc />
 395        public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
 396        {
 6397            var userData = GetUserData(user, item);
 6398            if (userData is null)
 399            {
 0400                return null;
 401            }
 402
 6403            var dto = GetUserItemDataDto(userData, item.Id);
 404
 6405            item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
 406
 407            // For an item with alternate versions, surface the most recently played version's resume point.
 6408            GetResumeUserData(user, item)?.ApplyTo(dto);
 409
 6410            return dto;
 411        }
 412
 413        /// <summary>
 414        /// Converts a UserItemData to a DTOUserItemData.
 415        /// </summary>
 416        /// <param name="data">The data.</param>
 417        /// <param name="itemId">The reference key to an Item.</param>
 418        /// <returns>DtoUserItemData.</returns>
 419        /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
 420        private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
 421        {
 6422            ArgumentNullException.ThrowIfNull(data);
 423
 6424            return new UserItemDataDto
 6425            {
 6426                IsFavorite = data.IsFavorite,
 6427                Likes = data.Likes,
 6428                PlaybackPositionTicks = data.PlaybackPositionTicks,
 6429                PlayCount = data.PlayCount,
 6430                Rating = data.Rating,
 6431                Played = data.Played,
 6432                LastPlayedDate = data.LastPlayedDate,
 6433                ItemId = itemId,
 6434                Key = data.Key
 6435            };
 436        }
 437
 438        /// <inheritdoc />
 439        public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
 440        {
 0441            var playedToCompletion = false;
 442
 0443            var runtimeTicks = item.GetRunTimeTicksForPlayState();
 444
 0445            var positionTicks = reportedPositionTicks ?? runtimeTicks;
 0446            var hasRuntime = runtimeTicks > 0;
 447
 448            // If a position has been reported, and if we know the duration
 0449            if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
 450            {
 0451                var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 452
 0453                if (pctIn < _config.Configuration.MinResumePct)
 454                {
 455                    // ignore progress during the beginning
 0456                    positionTicks = 0;
 457                }
 0458                else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPe
 459                {
 460                    // mark as completed close to the end
 0461                    positionTicks = 0;
 0462                    data.Played = playedToCompletion = true;
 463                }
 464                else
 465                {
 466                    // Enforce MinResumeDuration
 0467                    var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
 0468                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
 469                    {
 0470                        positionTicks = 0;
 0471                        data.Played = playedToCompletion = true;
 472                    }
 473                }
 474            }
 0475            else if (positionTicks > 0 && hasRuntime && item is AudioBook)
 476            {
 0477                var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
 0478                var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
 479
 0480                if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
 481                {
 482                    // ignore progress during the beginning
 0483                    positionTicks = 0;
 484                }
 0485                else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTi
 486                {
 487                    // mark as completed close to the end
 0488                    positionTicks = 0;
 0489                    data.Played = playedToCompletion = true;
 490                }
 491            }
 0492            else if (!hasRuntime)
 493            {
 494                // If we don't know the runtime we'll just have to assume it was fully played
 0495                data.Played = playedToCompletion = true;
 0496                positionTicks = 0;
 497            }
 498
 0499            if (!item.SupportsPlayedStatus)
 500            {
 0501                positionTicks = 0;
 0502                data.Played = false;
 503            }
 504
 0505            if (!item.SupportsPositionTicksResume)
 506            {
 0507                positionTicks = 0;
 508            }
 509
 0510            data.PlaybackPositionTicks = positionTicks;
 511
 0512            return playedToCompletion;
 513        }
 514
 515        /// <inheritdoc />
 516        public void ResetPlaybackStreamSelections(User user, BaseItem item)
 517        {
 0518            ArgumentNullException.ThrowIfNull(user);
 0519            ArgumentNullException.ThrowIfNull(item);
 520
 0521            using var dbContext = _repository.CreateDbContext();
 0522            var rows = dbContext.UserData
 0523                .Where(e => e.ItemId == item.Id && e.UserId == user.Id
 0524                            && (e.AudioStreamIndex != null || e.SubtitleStreamIndex != null))
 0525                .ToList();
 526
 0527            if (rows.Count == 0)
 528            {
 0529                return;
 530            }
 531
 0532            foreach (var row in rows)
 533            {
 0534                row.AudioStreamIndex = null;
 0535                row.SubtitleStreamIndex = null;
 536            }
 537
 0538            dbContext.SaveChanges();
 539
 0540            var cacheKey = GetCacheKey(user.InternalId, item.Id);
 0541            if (_cache.TryGet(cacheKey, out var cached))
 542            {
 0543                cached.AudioStreamIndex = null;
 0544                cached.SubtitleStreamIndex = null;
 0545                _cache.AddOrUpdate(cacheKey, cached);
 546            }
 547
 0548            item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray();
 0549        }
 550    }
 551}

Methods/Properties

.ctor(MediaBrowser.Controller.Configuration.IServerConfigurationManager,Microsoft.EntityFrameworkCore.IDbContextFactory`1<Jellyfin.Database.Implementations.JellyfinDbContext>)
SaveUserData(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.UserItemData,MediaBrowser.Model.Entities.UserDataSaveReason,System.Threading.CancellationToken)
SaveUserData(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.UpdateUserItemDataDto,MediaBrowser.Model.Entities.UserDataSaveReason)
Map(MediaBrowser.Controller.Entities.UserItemData,System.Guid,System.Guid)
Map(Jellyfin.Database.Implementations.Entities.UserData)
GetUserDataBatch(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User)
GetResumeUserData(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
GetResumeUserDataBatch(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,Jellyfin.Database.Implementations.Entities.User)
GetCacheKey(System.Int64,System.Guid)
GetUserData(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
ResolveUserDataRow(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<Jellyfin.Database.Implementations.Entities.UserData>)
GetUserDataDto(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
GetUserDataDto(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Dto.BaseItemDto,Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Dto.DtoOptions)
GetUserItemDataDto(MediaBrowser.Controller.Entities.UserItemData,System.Guid)
UpdatePlayState(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.UserItemData,System.Nullable`1<System.Int64>)
ResetPlaybackStreamSelections(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)