< 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
24%
Covered lines: 39
Uncovered lines: 120
Coverable lines: 159
Total lines: 364
Line coverage: 24.5%
Branch coverage
9%
Covered branches: 6
Total branches: 64
Branch coverage: 9.3%
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(...)75%5460%
GetUserDataInternal(...)33.33%8664.28%
GetCacheKey(...)100%11100%
GetUserData(...)100%11100%
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);
 83
 084            UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
 085            {
 086                Keys = keys,
 087                UserData = userData,
 088                SaveReason = reason,
 089                UserId = user.Id,
 090                Item = item
 091            });
 092        }
 93
 94        /// <inheritdoc />
 95        public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason)
 96        {
 097            ArgumentNullException.ThrowIfNull(user);
 098            ArgumentNullException.ThrowIfNull(item);
 099            ArgumentNullException.ThrowIfNull(userDataDto);
 100
 0101            var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null."
 102
 0103            if (userDataDto.PlaybackPositionTicks.HasValue)
 104            {
 0105                userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value;
 106            }
 107
 0108            if (userDataDto.PlayCount.HasValue)
 109            {
 0110                userData.PlayCount = userDataDto.PlayCount.Value;
 111            }
 112
 0113            if (userDataDto.IsFavorite.HasValue)
 114            {
 0115                userData.IsFavorite = userDataDto.IsFavorite.Value;
 116            }
 117
 0118            if (userDataDto.Likes.HasValue)
 119            {
 0120                userData.Likes = userDataDto.Likes.Value;
 121            }
 122
 0123            if (userDataDto.Played.HasValue)
 124            {
 0125                userData.Played = userDataDto.Played.Value;
 126            }
 127
 0128            if (userDataDto.LastPlayedDate.HasValue)
 129            {
 0130                userData.LastPlayedDate = userDataDto.LastPlayedDate.Value;
 131            }
 132
 0133            if (userDataDto.Rating.HasValue)
 134            {
 0135                userData.Rating = userDataDto.Rating.Value;
 136            }
 137
 0138            SaveUserData(user, item, userData, reason, CancellationToken.None);
 0139        }
 140
 141        private UserData Map(UserItemData dto, Guid userId, Guid itemId)
 142        {
 0143            return new UserData()
 0144            {
 0145                ItemId = itemId,
 0146                CustomDataKey = dto.Key,
 0147                Item = null,
 0148                User = null,
 0149                AudioStreamIndex = dto.AudioStreamIndex,
 0150                IsFavorite = dto.IsFavorite,
 0151                LastPlayedDate = dto.LastPlayedDate,
 0152                Likes = dto.Likes,
 0153                PlaybackPositionTicks = dto.PlaybackPositionTicks,
 0154                PlayCount = dto.PlayCount,
 0155                Played = dto.Played,
 0156                Rating = dto.Rating,
 0157                UserId = userId,
 0158                SubtitleStreamIndex = dto.SubtitleStreamIndex,
 0159            };
 160        }
 161
 162        private UserItemData Map(UserData dto)
 163        {
 0164            return new UserItemData()
 0165            {
 0166                Key = dto.CustomDataKey!,
 0167                AudioStreamIndex = dto.AudioStreamIndex,
 0168                IsFavorite = dto.IsFavorite,
 0169                LastPlayedDate = dto.LastPlayedDate,
 0170                Likes = dto.Likes,
 0171                PlaybackPositionTicks = dto.PlaybackPositionTicks,
 0172                PlayCount = dto.PlayCount,
 0173                Played = dto.Played,
 0174                Rating = dto.Rating,
 0175                SubtitleStreamIndex = dto.SubtitleStreamIndex,
 0176            };
 177        }
 178
 179        private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
 180        {
 9181            var cacheKey = GetCacheKey(user.InternalId, itemId);
 182
 9183            if (_cache.TryGet(cacheKey, out var data))
 184            {
 7185                return data;
 186            }
 187
 2188            data = GetUserDataInternal(user.Id, itemId, keys);
 189
 2190            if (data is null)
 191            {
 0192                return new UserItemData()
 0193                {
 0194                    Key = keys[0],
 0195                };
 196            }
 197
 2198            return _cache.GetOrAdd(cacheKey, _ => data);
 199        }
 200
 201        private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
 202        {
 2203            if (keys.Count == 0)
 204            {
 0205                return null;
 206            }
 207
 2208            using var context = _repository.CreateDbContext();
 2209            var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKe
 210
 2211            if (userData.Length > 0)
 212            {
 0213                var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
 0214                if (directDataReference is not null)
 215                {
 0216                    return Map(directDataReference);
 217                }
 218
 0219                return Map(userData.First());
 220            }
 221
 2222            return new UserItemData
 2223            {
 2224                Key = keys.Last()!
 2225            };
 2226        }
 227
 228        /// <summary>
 229        /// Gets the internal key.
 230        /// </summary>
 231        /// <returns>System.String.</returns>
 232        private static string GetCacheKey(long internalUserId, Guid itemId)
 233        {
 9234            return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N", CultureInfo.Invari
 235        }
 236
 237        /// <inheritdoc />
 238        public UserItemData? GetUserData(User user, BaseItem item)
 239        {
 9240            return GetUserData(user, item.Id, item.GetUserDataKeys());
 241        }
 242
 243        /// <inheritdoc />
 244        public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
 0245            => GetUserDataDto(item, null, user, new DtoOptions());
 246
 247        /// <inheritdoc />
 248        public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
 249        {
 9250            var userData = GetUserData(user, item);
 9251            if (userData is null)
 252            {
 0253                return null;
 254            }
 255
 9256            var dto = GetUserItemDataDto(userData, item.Id);
 257
 9258            item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
 9259            return dto;
 260        }
 261
 262        /// <summary>
 263        /// Converts a UserItemData to a DTOUserItemData.
 264        /// </summary>
 265        /// <param name="data">The data.</param>
 266        /// <param name="itemId">The reference key to an Item.</param>
 267        /// <returns>DtoUserItemData.</returns>
 268        /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
 269        private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
 270        {
 9271            ArgumentNullException.ThrowIfNull(data);
 272
 9273            return new UserItemDataDto
 9274            {
 9275                IsFavorite = data.IsFavorite,
 9276                Likes = data.Likes,
 9277                PlaybackPositionTicks = data.PlaybackPositionTicks,
 9278                PlayCount = data.PlayCount,
 9279                Rating = data.Rating,
 9280                Played = data.Played,
 9281                LastPlayedDate = data.LastPlayedDate,
 9282                ItemId = itemId,
 9283                Key = data.Key
 9284            };
 285        }
 286
 287        /// <inheritdoc />
 288        public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
 289        {
 0290            var playedToCompletion = false;
 291
 0292            var runtimeTicks = item.GetRunTimeTicksForPlayState();
 293
 0294            var positionTicks = reportedPositionTicks ?? runtimeTicks;
 0295            var hasRuntime = runtimeTicks > 0;
 296
 297            // If a position has been reported, and if we know the duration
 0298            if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
 299            {
 0300                var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 301
 0302                if (pctIn < _config.Configuration.MinResumePct)
 303                {
 304                    // ignore progress during the beginning
 0305                    positionTicks = 0;
 306                }
 0307                else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
 308                {
 309                    // mark as completed close to the end
 0310                    positionTicks = 0;
 0311                    data.Played = playedToCompletion = true;
 312                }
 313                else
 314                {
 315                    // Enforce MinResumeDuration
 0316                    var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
 0317                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
 318                    {
 0319                        positionTicks = 0;
 0320                        data.Played = playedToCompletion = true;
 321                    }
 322                }
 323            }
 0324            else if (positionTicks > 0 && hasRuntime && item is AudioBook)
 325            {
 0326                var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
 0327                var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
 328
 0329                if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
 330                {
 331                    // ignore progress during the beginning
 0332                    positionTicks = 0;
 333                }
 0334                else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTi
 335                {
 336                    // mark as completed close to the end
 0337                    positionTicks = 0;
 0338                    data.Played = playedToCompletion = true;
 339                }
 340            }
 0341            else if (!hasRuntime)
 342            {
 343                // If we don't know the runtime we'll just have to assume it was fully played
 0344                data.Played = playedToCompletion = true;
 0345                positionTicks = 0;
 346            }
 347
 0348            if (!item.SupportsPlayedStatus)
 349            {
 0350                positionTicks = 0;
 0351                data.Played = false;
 352            }
 353
 0354            if (!item.SupportsPositionTicksResume)
 355            {
 0356                positionTicks = 0;
 357            }
 358
 0359            data.PlaybackPositionTicks = positionTicks;
 360
 0361            return playedToCompletion;
 362        }
 363    }
 364}

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>)