< 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
15%
Covered lines: 26
Uncovered lines: 137
Coverable lines: 163
Total lines: 368
Line coverage: 15.9%
Branch coverage
5%
Covered branches: 4
Total branches: 68
Branch coverage: 5.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SaveUserData(...)0%4260%
SaveUserData(...)0%272160%
Map(...)100%210%
Map(...)100%210%
GetUserData(...)0%2040%
GetUserDataInternal(...)0%4260%
GetCacheKey(...)100%210%
GetUserData(...)75%44100%
GetUserDataDto(...)100%210%
GetUserDataDto(...)50%2283.33%
GetUserItemDataDto(...)100%11100%
UpdatePlayState(...)0%930300%

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        {
 2141            _config = config;
 2142            _repository = repository;
 2143            _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.Cache
 2144        }
 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        {
 0165            return new UserItemData()
 0166            {
 0167                Key = dto.CustomDataKey!,
 0168                AudioStreamIndex = dto.AudioStreamIndex,
 0169                IsFavorite = dto.IsFavorite,
 0170                LastPlayedDate = dto.LastPlayedDate,
 0171                Likes = dto.Likes,
 0172                PlaybackPositionTicks = dto.PlaybackPositionTicks,
 0173                PlayCount = dto.PlayCount,
 0174                Played = dto.Played,
 0175                Rating = dto.Rating,
 0176                SubtitleStreamIndex = dto.SubtitleStreamIndex,
 0177            };
 178        }
 179
 180        private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
 181        {
 0182            var cacheKey = GetCacheKey(user.InternalId, itemId);
 183
 0184            if (_cache.TryGet(cacheKey, out var data))
 185            {
 0186                return data;
 187            }
 188
 0189            data = GetUserDataInternal(user.Id, itemId, keys);
 190
 0191            if (data is null)
 192            {
 0193                return new UserItemData()
 0194                {
 0195                    Key = keys[0],
 0196                };
 197            }
 198
 0199            return _cache.GetOrAdd(cacheKey, _ => data);
 200        }
 201
 202        private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
 203        {
 0204            if (keys.Count == 0)
 205            {
 0206                return null;
 207            }
 208
 0209            using var context = _repository.CreateDbContext();
 0210            var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKe
 211
 0212            if (userData.Length > 0)
 213            {
 0214                var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
 0215                if (directDataReference is not null)
 216                {
 0217                    return Map(directDataReference);
 218                }
 219
 0220                return Map(userData.First());
 221            }
 222
 0223            return new UserItemData
 0224            {
 0225                Key = keys.Last()!
 0226            };
 0227        }
 228
 229        /// <summary>
 230        /// Gets the internal key.
 231        /// </summary>
 232        /// <returns>System.String.</returns>
 233        private static string GetCacheKey(long internalUserId, Guid itemId)
 234        {
 0235            return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N", CultureInfo.Invari
 236        }
 237
 238        /// <inheritdoc />
 239        public UserItemData? GetUserData(User user, BaseItem item)
 240        {
 9241            return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData(
 9242            {
 9243                Key = item.GetUserDataKeys()[0],
 9244            };
 245        }
 246
 247        /// <inheritdoc />
 248        public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
 0249            => GetUserDataDto(item, null, user, new DtoOptions());
 250
 251        /// <inheritdoc />
 252        public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
 253        {
 9254            var userData = GetUserData(user, item);
 9255            if (userData is null)
 256            {
 0257                return null;
 258            }
 259
 9260            var dto = GetUserItemDataDto(userData, item.Id);
 261
 9262            item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
 9263            return dto;
 264        }
 265
 266        /// <summary>
 267        /// Converts a UserItemData to a DTOUserItemData.
 268        /// </summary>
 269        /// <param name="data">The data.</param>
 270        /// <param name="itemId">The reference key to an Item.</param>
 271        /// <returns>DtoUserItemData.</returns>
 272        /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
 273        private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
 274        {
 9275            ArgumentNullException.ThrowIfNull(data);
 276
 9277            return new UserItemDataDto
 9278            {
 9279                IsFavorite = data.IsFavorite,
 9280                Likes = data.Likes,
 9281                PlaybackPositionTicks = data.PlaybackPositionTicks,
 9282                PlayCount = data.PlayCount,
 9283                Rating = data.Rating,
 9284                Played = data.Played,
 9285                LastPlayedDate = data.LastPlayedDate,
 9286                ItemId = itemId,
 9287                Key = data.Key
 9288            };
 289        }
 290
 291        /// <inheritdoc />
 292        public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
 293        {
 0294            var playedToCompletion = false;
 295
 0296            var runtimeTicks = item.GetRunTimeTicksForPlayState();
 297
 0298            var positionTicks = reportedPositionTicks ?? runtimeTicks;
 0299            var hasRuntime = runtimeTicks > 0;
 300
 301            // If a position has been reported, and if we know the duration
 0302            if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
 303            {
 0304                var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 305
 0306                if (pctIn < _config.Configuration.MinResumePct)
 307                {
 308                    // ignore progress during the beginning
 0309                    positionTicks = 0;
 310                }
 0311                else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPe
 312                {
 313                    // mark as completed close to the end
 0314                    positionTicks = 0;
 0315                    data.Played = playedToCompletion = true;
 316                }
 317                else
 318                {
 319                    // Enforce MinResumeDuration
 0320                    var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
 0321                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
 322                    {
 0323                        positionTicks = 0;
 0324                        data.Played = playedToCompletion = true;
 325                    }
 326                }
 327            }
 0328            else if (positionTicks > 0 && hasRuntime && item is AudioBook)
 329            {
 0330                var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
 0331                var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
 332
 0333                if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
 334                {
 335                    // ignore progress during the beginning
 0336                    positionTicks = 0;
 337                }
 0338                else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTi
 339                {
 340                    // mark as completed close to the end
 0341                    positionTicks = 0;
 0342                    data.Played = playedToCompletion = true;
 343                }
 344            }
 0345            else if (!hasRuntime)
 346            {
 347                // If we don't know the runtime we'll just have to assume it was fully played
 0348                data.Played = playedToCompletion = true;
 0349                positionTicks = 0;
 350            }
 351
 0352            if (!item.SupportsPlayedStatus)
 353            {
 0354                positionTicks = 0;
 0355                data.Played = false;
 356            }
 357
 0358            if (!item.SupportsPositionTicksResume)
 359            {
 0360                positionTicks = 0;
 361            }
 362
 0363            data.PlaybackPositionTicks = positionTicks;
 364
 0365            return playedToCompletion;
 366        }
 367    }
 368}

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)
GetUserData(Jellyfin.Database.Implementations.Entities.User,System.Guid,System.Collections.Generic.List`1<System.String>)
GetUserDataInternal(System.Guid,System.Guid,System.Collections.Generic.List`1<System.String>)
GetCacheKey(System.Int64,System.Guid)
GetUserData(Jellyfin.Database.Implementations.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
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>)