< 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
31%
Covered lines: 34
Uncovered lines: 75
Coverable lines: 109
Total lines: 288
Line coverage: 31.1%
Branch coverage
3%
Covered branches: 2
Total branches: 52
Branch coverage: 3.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%2040%
SaveUserData(...)0%210140%
GetUserData(...)100%11100%
GetUserDataInternal(...)50%4.18477.77%
GetCacheKey(...)100%11100%
GetUserData(...)100%11100%
GetUserDataDto(...)100%210%
GetUserDataDto(...)100%11100%
GetUserItemDataDto(...)100%11100%
UpdatePlayState(...)0%930300%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Diagnostics;
 5using System.Globalization;
 6using System.Threading;
 7using Jellyfin.Data.Entities;
 8using MediaBrowser.Controller.Configuration;
 9using MediaBrowser.Controller.Dto;
 10using MediaBrowser.Controller.Entities;
 11using MediaBrowser.Controller.Library;
 12using MediaBrowser.Controller.Persistence;
 13using MediaBrowser.Model.Dto;
 14using MediaBrowser.Model.Entities;
 15using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
 16using Book = MediaBrowser.Controller.Entities.Book;
 17
 18namespace Emby.Server.Implementations.Library
 19{
 20    /// <summary>
 21    /// Class UserDataManager.
 22    /// </summary>
 23    public class UserDataManager : IUserDataManager
 24    {
 2225        private readonly ConcurrentDictionary<string, UserItemData> _userData =
 2226            new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
 27
 28        private readonly IServerConfigurationManager _config;
 29        private readonly IUserManager _userManager;
 30        private readonly IUserDataRepository _repository;
 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="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 37        /// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
 38        public UserDataManager(
 39            IServerConfigurationManager config,
 40            IUserManager userManager,
 41            IUserDataRepository repository)
 42        {
 2243            _config = config;
 2244            _userManager = userManager;
 2245            _repository = repository;
 2246        }
 47
 48        /// <inheritdoc />
 49        public event EventHandler<UserDataSaveEventArgs>? UserDataSaved;
 50
 51        /// <inheritdoc />
 52        public void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, Cancellatio
 53        {
 054            ArgumentNullException.ThrowIfNull(userData);
 55
 056            ArgumentNullException.ThrowIfNull(item);
 57
 058            cancellationToken.ThrowIfCancellationRequested();
 59
 060            var keys = item.GetUserDataKeys();
 61
 062            var userId = user.InternalId;
 63
 064            foreach (var key in keys)
 65            {
 066                _repository.SaveUserData(userId, key, userData, cancellationToken);
 67            }
 68
 069            var cacheKey = GetCacheKey(userId, item.Id);
 070            _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
 71
 072            UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
 073            {
 074                Keys = keys,
 075                UserData = userData,
 076                SaveReason = reason,
 077                UserId = user.Id,
 078                Item = item
 079            });
 080        }
 81
 82        /// <inheritdoc />
 83        public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason)
 84        {
 085            ArgumentNullException.ThrowIfNull(user);
 086            ArgumentNullException.ThrowIfNull(item);
 087            ArgumentNullException.ThrowIfNull(reason);
 088            ArgumentNullException.ThrowIfNull(userDataDto);
 89
 090            var userData = GetUserData(user, item);
 91
 092            if (userDataDto.PlaybackPositionTicks.HasValue)
 93            {
 094                userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value;
 95            }
 96
 097            if (userDataDto.PlayCount.HasValue)
 98            {
 099                userData.PlayCount = userDataDto.PlayCount.Value;
 100            }
 101
 0102            if (userDataDto.IsFavorite.HasValue)
 103            {
 0104                userData.IsFavorite = userDataDto.IsFavorite.Value;
 105            }
 106
 0107            if (userDataDto.Likes.HasValue)
 108            {
 0109                userData.Likes = userDataDto.Likes.Value;
 110            }
 111
 0112            if (userDataDto.Played.HasValue)
 113            {
 0114                userData.Played = userDataDto.Played.Value;
 115            }
 116
 0117            if (userDataDto.LastPlayedDate.HasValue)
 118            {
 0119                userData.LastPlayedDate = userDataDto.LastPlayedDate.Value;
 120            }
 121
 0122            if (userDataDto.Rating.HasValue)
 123            {
 0124                userData.Rating = userDataDto.Rating.Value;
 125            }
 126
 0127            SaveUserData(user, item, userData, reason, CancellationToken.None);
 0128        }
 129
 130        private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
 131        {
 14132            var userId = user.InternalId;
 133
 14134            var cacheKey = GetCacheKey(userId, itemId);
 135
 14136            return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
 137        }
 138
 139        private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
 140        {
 2141            var userData = _repository.GetUserData(internalUserId, keys);
 142
 2143            if (userData is not null)
 144            {
 0145                return userData;
 146            }
 147
 2148            if (keys.Count > 0)
 149            {
 2150                return new UserItemData
 2151                {
 2152                    Key = keys[0]
 2153                };
 154            }
 155
 0156            throw new UnreachableException();
 157        }
 158
 159        /// <summary>
 160        /// Gets the internal key.
 161        /// </summary>
 162        /// <returns>System.String.</returns>
 163        private static string GetCacheKey(long internalUserId, Guid itemId)
 164        {
 14165            return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N", CultureInfo.Invari
 166        }
 167
 168        /// <inheritdoc />
 169        public UserItemData GetUserData(User user, BaseItem item)
 170        {
 14171            return GetUserData(user, item.Id, item.GetUserDataKeys());
 172        }
 173
 174        /// <inheritdoc />
 175        public UserItemDataDto GetUserDataDto(BaseItem item, User user)
 0176            => GetUserDataDto(item, null, user, new DtoOptions());
 177
 178        /// <inheritdoc />
 179        public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
 180        {
 14181            var userData = GetUserData(user, item);
 14182            var dto = GetUserItemDataDto(userData);
 183
 14184            item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
 14185            return dto;
 186        }
 187
 188        /// <summary>
 189        /// Converts a UserItemData to a DTOUserItemData.
 190        /// </summary>
 191        /// <param name="data">The data.</param>
 192        /// <returns>DtoUserItemData.</returns>
 193        /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
 194        private UserItemDataDto GetUserItemDataDto(UserItemData data)
 195        {
 14196            ArgumentNullException.ThrowIfNull(data);
 197
 14198            return new UserItemDataDto
 14199            {
 14200                IsFavorite = data.IsFavorite,
 14201                Likes = data.Likes,
 14202                PlaybackPositionTicks = data.PlaybackPositionTicks,
 14203                PlayCount = data.PlayCount,
 14204                Rating = data.Rating,
 14205                Played = data.Played,
 14206                LastPlayedDate = data.LastPlayedDate,
 14207                Key = data.Key
 14208            };
 209        }
 210
 211        /// <inheritdoc />
 212        public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
 213        {
 0214            var playedToCompletion = false;
 215
 0216            var runtimeTicks = item.GetRunTimeTicksForPlayState();
 217
 0218            var positionTicks = reportedPositionTicks ?? runtimeTicks;
 0219            var hasRuntime = runtimeTicks > 0;
 220
 221            // If a position has been reported, and if we know the duration
 0222            if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
 223            {
 0224                var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 225
 0226                if (pctIn < _config.Configuration.MinResumePct)
 227                {
 228                    // ignore progress during the beginning
 0229                    positionTicks = 0;
 230                }
 0231                else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
 232                {
 233                    // mark as completed close to the end
 0234                    positionTicks = 0;
 0235                    data.Played = playedToCompletion = true;
 236                }
 237                else
 238                {
 239                    // Enforce MinResumeDuration
 0240                    var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
 0241                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
 242                    {
 0243                        positionTicks = 0;
 0244                        data.Played = playedToCompletion = true;
 245                    }
 246                }
 247            }
 0248            else if (positionTicks > 0 && hasRuntime && item is AudioBook)
 249            {
 0250                var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
 0251                var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
 252
 0253                if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
 254                {
 255                    // ignore progress during the beginning
 0256                    positionTicks = 0;
 257                }
 0258                else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTi
 259                {
 260                    // mark as completed close to the end
 0261                    positionTicks = 0;
 0262                    data.Played = playedToCompletion = true;
 263                }
 264            }
 0265            else if (!hasRuntime)
 266            {
 267                // If we don't know the runtime we'll just have to assume it was fully played
 0268                data.Played = playedToCompletion = true;
 0269                positionTicks = 0;
 270            }
 271
 0272            if (!item.SupportsPlayedStatus)
 273            {
 0274                positionTicks = 0;
 0275                data.Played = false;
 276            }
 277
 0278            if (!item.SupportsPositionTicksResume)
 279            {
 0280                positionTicks = 0;
 281            }
 282
 0283            data.PlaybackPositionTicks = positionTicks;
 284
 0285            return playedToCompletion;
 286        }
 287    }
 288}