Example #1
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e)
        {
            // ignore change events for any reason other than manually toggling played.
            if (e.SaveReason != UserDataSaveReason.TogglePlayed)
            {
                return;
            }

            var baseItem = e.Item as BaseItem;

            if (baseItem != null)
            {
                // determine if user has trakt credentials
                var traktUser = UserHelper.GetTraktUser(e.UserId.ToString());

                // Can't progress
                if (traktUser == null || !_traktApi.CanSync(baseItem, traktUser))
                {
                    return;
                }

                // We have a user and the item is in a trakt monitored location.
                _userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(e, traktUser);
            }
        }
Example #2
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnUserDataSaved(object sender, UserDataSaveEventArgs e)
        {
            // ignore change events for any reason other than manually toggling played.
            if (e.SaveReason != UserDataSaveReason.TogglePlayed)
            {
                return;
            }

            if (e.Item is BaseItem baseItem)
            {
                // determine if user has trakt credentials
                var traktUser = UserHelper.GetTraktUser(e.UserId);

                // Can't progress
                if (traktUser == null || !_traktApi.CanSync(baseItem, traktUser))
                {
                    return;
                }

                if (!traktUser.PostSetWatched && !traktUser.PostSetUnwatched)
                {
                    // User doesn't want to post any status changes at all.
                    return;
                }

                // We have a user who wants to post updates and the item is in a trakt monitored location.
                _userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(e, traktUser);
            }
        }
Example #3
0
        /// <summary>
        /// Count media items and call <see cref="SyncMovies"/> and <see cref="SyncShows"/>
        /// </summary>
        /// <returns></returns>
        private async Task SyncUserLibrary(
            User user,
            TraktUser traktUser,
            ISplittableProgress <double> progress,
            CancellationToken cancellationToken)
        {
            // purely for progress reporting
            var mediaItemsCount =
                _libraryManager.GetItemList(
                    new InternalItemsQuery
            {
                IncludeItemTypes     = new[] { typeof(Movie).Name, typeof(Episode).Name },
                ExcludeLocationTypes = new[] { LocationType.Virtual }
            })
                .Count(i => _traktApi.CanSync(i, traktUser));

            if (mediaItemsCount == 0)
            {
                _logger.Info("No media found for '" + user.Name + "'.");
                return;
            }

            _logger.Info(mediaItemsCount + " Items found for '" + user.Name + "'.");

            await SyncMovies(user, traktUser, progress.Split(2), cancellationToken);
            await SyncShows(user, traktUser, progress.Split(2), cancellationToken);
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="item"></param>
        /// <param name="eventType"></param>
        public void QueueItem(BaseItem item, EventType eventType)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            if (_queueTimer == null)
            {
                _queueTimer = _timerFactory.Create(OnQueueTimerCallback, null, TimeSpan.FromMilliseconds(20000),
                                                   Timeout.InfiniteTimeSpan);
            }
            else
            {
                _queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
            }

            var users = Plugin.Instance.PluginConfiguration.TraktUsers;

            if (users == null || users.Length == 0)
            {
                return;
            }

            // we need to process the video for each user
            foreach (var user in users.Where(x => _traktApi.CanSync(item, x)))
            {
                // we have a match, this user is watching the folder the video is in. Add to queue and they
                // will be processed when the next timer elapsed event fires.
                var libraryEvent = new LibraryEvent {
                    Item = item, TraktUser = user, EventType = eventType
                };
                _queuedEvents.Add(libraryEvent);
            }
        }
Example #5
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="item"></param>
        /// <param name="eventType"></param>
        public void QueueItem(BaseItem item, EventType eventType)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            if (_queueTimer == null)
            {
                _queueTimer          = new Timer(20000); // fire every 20 seconds
                _queueTimer.Elapsed += QueueTimerElapsed;
            }
            else if (_queueTimer.Enabled)
            {
                // If enabled then multiple LibraryManager events are firing. Restart the timer
                _queueTimer.Stop();
                _queueTimer.Start();
            }

            if (!_queueTimer.Enabled)
            {
                _queueTimer.Enabled = true;
            }


            var users = Plugin.Instance.PluginConfiguration.TraktUsers;

            if (users == null || users.Length == 0)
            {
                return;
            }

            // we need to process the video for each user
            foreach (var user in users.Where(x => _traktApi.CanSync(item, x)))
            {
                // we have a match, this user is watching the folder the video is in. Add to queue and they
                // will be processed when the next timer elapsed event fires.
                var libraryEvent = new LibraryEvent {
                    Item = item, TraktUser = user, EventType = eventType
                };
                _queuedEvents.Add(libraryEvent);
            }
        }
Example #6
0
        /// <summary>
        /// Sync watched and collected status of <see cref="Movie"/>s with trakt.
        /// </summary>
        private async Task SyncMovies(
            User user,
            TraktUser traktUser,
            ISplittableProgress <double> progress,
            CancellationToken cancellationToken)
        {
            /*
             * In order to sync watched status to trakt.tv we need to know what's been watched on Trakt already. This
             * will stop us from endlessly incrementing the watched values on the site.
             */
            var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser, cancellationToken).ConfigureAwait(false);

            var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser, cancellationToken).ConfigureAwait(false);

            var libraryMovies =
                _libraryManager.GetItemList(
                    new InternalItemsQuery(user)
            {
                IncludeItemTypes = new[] { typeof(Movie).Name },
                IsVirtualItem    = false,
                OrderBy          = new[]
                {
                    new ValueTuple <string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
                }
            })
                .Where(x => _traktApi.CanSync(x, traktUser))
                .ToList();
            var collectedMovies   = new List <Movie>();
            var uncollectedMovies = new List <TraktMovieCollected>();
            var playedMovies      = new List <Movie>();
            var unplayedMovies    = new List <Movie>();

            var decisionProgress = progress.Split(4).Split(libraryMovies.Count);

            foreach (var child in libraryMovies)
            {
                cancellationToken.ThrowIfCancellationRequested();
                var libraryMovie = child as Movie;
                var userData     = _userDataManager.GetUserData(user, child);

                // if movie is not collected, or (export media info setting is enabled and every collected matching movie has different metadata), collect it
                var collectedMathingMovies = Match.FindMatches(libraryMovie, traktCollectedMovies).ToList();
                if (!collectedMathingMovies.Any() ||
                    (traktUser.ExportMediaInfo &&
                     collectedMathingMovies.All(
                         collectedMovie => collectedMovie.MetadataIsDifferent(libraryMovie))))
                {
                    collectedMovies.Add(libraryMovie);
                }

                var movieWatched = Match.FindMatch(libraryMovie, traktWatchedMovies);

                // if the movie has been played locally and is unplayed on trakt.tv then add it to the list
                if (userData.Played)
                {
                    if (movieWatched == null)
                    {
                        if (traktUser.PostWatchedHistory)
                        {
                            playedMovies.Add(libraryMovie);
                        }
                        else if (!traktUser.SkipUnwatchedImportFromTrakt)
                        {
                            if (userData.Played)
                            {
                                userData.Played = false;

                                _userDataManager.SaveUserData(
                                    user.InternalId,
                                    libraryMovie,
                                    userData,
                                    UserDataSaveReason.Import,
                                    cancellationToken);
                            }
                        }
                    }
                }
                else
                {
                    // If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
                    if (movieWatched != null)
                    {
                        unplayedMovies.Add(libraryMovie);
                    }
                }

                decisionProgress.Report(100);
            }

            foreach (var traktCollectedMovie in traktCollectedMovies)
            {
                if (!Match.FindMatches(traktCollectedMovie, libraryMovies).Any())
                {
                    _logger.Debug("No matches for {0}, will be uncollected on Trakt", _jsonSerializer.SerializeToString(traktCollectedMovie.movie));
                    uncollectedMovies.Add(traktCollectedMovie);
                }
            }

            if (traktUser.SyncCollection)
            {
                // send movies to mark collected
                await SendMovieCollectionAdds(traktUser, collectedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);

                // send movies to mark uncollected
                await SendMovieCollectionRemoves(traktUser, uncollectedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
            }
            // send movies to mark watched
            await SendMoviePlaystateUpdates(true, traktUser, playedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);

            // send movies to mark unwatched
            await SendMoviePlaystateUpdates(false, traktUser, unplayedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
        }
        private async Task SyncUserLibrary(User user,
                                           TraktUser traktUser,
                                           double progPercent,
                                           double percentPerUser,
                                           IProgress <double> progress,
                                           CancellationToken cancellationToken)
        {
            var libraryRoot = user.RootFolder;
            // purely for progress reporting
            var mediaItemsCount = libraryRoot.GetRecursiveChildren(user).Count(i => _traktApi.CanSync(i, traktUser));

            if (mediaItemsCount == 0)
            {
                _logger.Info("No media found for '" + user.Name + "'.");
                return;
            }
            _logger.Info(mediaItemsCount + " Items found for '" + user.Name + "'.");

            var percentPerItem = (float)percentPerUser / mediaItemsCount / 2.0;

            /*
             * In order to sync watched status to trakt.tv we need to know what's been watched on Trakt already. This
             * will stop us from endlessly incrementing the watched values on the site.
             */
            var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);

            var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser).ConfigureAwait(false);

            var movieItems = libraryRoot.GetRecursiveChildren(user)
                             .Where(x => x is Movie)
                             .Where(x => _traktApi.CanSync(x, traktUser))
                             .OrderBy(x => x.Name)
                             .ToList();
            var movies         = new List <Movie>();
            var playedMovies   = new List <Movie>();
            var unPlayedMovies = new List <Movie>();

            foreach (var child in movieItems)
            {
                cancellationToken.ThrowIfCancellationRequested();
                var movie    = child as Movie;
                var userData = _userDataManager.GetUserData(user.Id, child.GetUserDataKey());

                var collectedMovies = SyncFromTraktTask.FindMatches(movie, traktCollectedMovies).ToList();
                if (!collectedMovies.Any() || collectedMovies.All(collectedMovie => collectedMovie.MetadataIsDifferent(movie)))
                {
                    movies.Add(movie);
                }

                var movieWatched = SyncFromTraktTask.FindMatch(movie, traktWatchedMovies);
                if (userData.Played)
                {
                    if (movieWatched == null)
                    {
                        playedMovies.Add(movie);
                    }
                }
                else
                {
                    if (movieWatched != null)
                    {
                        unPlayedMovies.Add(movie);
                    }
                }
                // purely for progress reporting
                progPercent += percentPerItem;
                progress.Report(progPercent);
            }

            _logger.Info("Movies to add to Collection: " + movies.Count);
            // send any remaining entries
            if (movies.Count > 0)
            {
                try
                {
                    var dataContracts =
                        await
                        _traktApi.SendLibraryUpdateAsync(movies, traktUser, cancellationToken, EventType.Add)
                        .ConfigureAwait(false);

                    if (dataContracts != null)
                    {
                        foreach (var traktSyncResponse in dataContracts)
                        {
                            LogTraktResponseDataContract(traktSyncResponse);
                        }
                    }
                }
                catch (ArgumentNullException argNullEx)
                {
                    _logger.ErrorException("ArgumentNullException handled sending movies to trakt.tv", argNullEx);
                }
                catch (Exception e)
                {
                    _logger.ErrorException("Exception handled sending movies to trakt.tv", e);
                }
                // purely for progress reporting
                progPercent += (percentPerItem * movies.Count);
                progress.Report(progPercent);
            }

            _logger.Info("Movies to set watched: " + playedMovies.Count);
            if (playedMovies.Count > 0)
            {
                try
                {
                    var dataContracts =
                        await _traktApi.SendMoviePlaystateUpdates(playedMovies, traktUser, true, cancellationToken);

                    if (dataContracts != null)
                    {
                        foreach (var traktSyncResponse in dataContracts)
                        {
                            LogTraktResponseDataContract(traktSyncResponse);
                        }
                    }
                }
                catch (Exception e)
                {
                    _logger.ErrorException("Error updating movie play states", e);
                }
                // purely for progress reporting
                progPercent += (percentPerItem * playedMovies.Count);
                progress.Report(progPercent);
            }

            _logger.Info("Movies to set unwatched: " + unPlayedMovies.Count);
            if (unPlayedMovies.Count > 0)
            {
                try
                {
                    var dataContracts =
                        await _traktApi.SendMoviePlaystateUpdates(unPlayedMovies, traktUser, false, cancellationToken);

                    if (dataContracts != null)
                    {
                        foreach (var traktSyncResponse in dataContracts)
                        {
                            LogTraktResponseDataContract(traktSyncResponse);
                        }
                    }
                }
                catch (Exception e)
                {
                    _logger.ErrorException("Error updating movie play states", e);
                }
                // purely for progress reporting
                progPercent += (percentPerItem * unPlayedMovies.Count);
                progress.Report(progPercent);
            }

            var traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);

            var traktCollectedShows = await _traktApi.SendGetCollectedShowsRequest(traktUser).ConfigureAwait(false);

            var episodeItems = libraryRoot.GetRecursiveChildren(user)
                               .Where(x => x is Episode)
                               .Where(x => _traktApi.CanSync(x, traktUser))
                               .OrderBy(x => x is Episode ? (x as Episode).SeriesName : null)
                               .ToList();

            var episodes         = new List <Episode>();
            var playedEpisodes   = new List <Episode>();
            var unPlayedEpisodes = new List <Episode>();

            foreach (var child in episodeItems)
            {
                cancellationToken.ThrowIfCancellationRequested();
                var episode          = child as Episode;
                var userData         = _userDataManager.GetUserData(user.Id, episode.GetUserDataKey());
                var isPlayedTraktTv  = false;
                var traktWatchedShow = SyncFromTraktTask.FindMatch(episode.Series, traktWatchedShows);

                if (traktWatchedShow != null && traktWatchedShow.Seasons != null && traktWatchedShow.Seasons.Count > 0)
                {
                    isPlayedTraktTv =
                        traktWatchedShow.Seasons.Any(
                            season =>
                            season.Number == episode.GetSeasonNumber() &&
                            season.Episodes != null &&
                            season.Episodes.Any(te => te.Number == episode.IndexNumber && te.Plays > 0));
                }

                // if the show has been played locally and is unplayed on trakt.tv then add it to the list
                if (userData != null && userData.Played && !isPlayedTraktTv)
                {
                    playedEpisodes.Add(episode);
                }
                // If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
                else if (userData != null && !userData.Played && isPlayedTraktTv)
                {
                    unPlayedEpisodes.Add(episode);
                }
                var traktCollectedShow = SyncFromTraktTask.FindMatch(episode.Series, traktCollectedShows);
                if (traktCollectedShow == null ||
                    traktCollectedShow.Seasons == null ||
                    traktCollectedShow.Seasons.All(x => x.Number != episode.ParentIndexNumber) ||
                    traktCollectedShow.Seasons.First(x => x.Number == episode.ParentIndexNumber)
                    .Episodes.All(e => e.Number != episode.IndexNumber))
                {
                    episodes.Add(episode);
                }

                // purely for progress reporting
                progPercent += percentPerItem;
                progress.Report(progPercent);
            }


            _logger.Info("Episodes to add to Collection: " + episodes.Count);
            if (episodes.Count > 0)
            {
                try
                {
                    var dataContracts =
                        await
                        _traktApi.SendLibraryUpdateAsync(episodes, traktUser, cancellationToken, EventType.Add)
                        .ConfigureAwait(false);

                    if (dataContracts != null)
                    {
                        foreach (var traktSyncResponse in dataContracts)
                        {
                            LogTraktResponseDataContract(traktSyncResponse);
                        }
                    }
                }
                catch (ArgumentNullException argNullEx)
                {
                    _logger.ErrorException("ArgumentNullException handled sending episodes to trakt.tv", argNullEx);
                }
                catch (Exception e)
                {
                    _logger.ErrorException("Exception handled sending episodes to trakt.tv", e);
                }
                // purely for progress reporting
                progPercent += (percentPerItem * episodes.Count);
                progress.Report(progPercent);
            }

            _logger.Info("Episodes to set watched: " + playedEpisodes.Count);
            if (playedEpisodes.Count > 0)
            {
                try
                {
                    var dataContracts =
                        await _traktApi.SendEpisodePlaystateUpdates(playedEpisodes, traktUser, true, cancellationToken);

                    if (dataContracts != null)
                    {
                        foreach (var traktSyncResponse in dataContracts)
                        {
                            LogTraktResponseDataContract(traktSyncResponse);
                        }
                    }
                }
                catch (Exception e)
                {
                    _logger.ErrorException("Error updating episode play states", e);
                }
                // purely for progress reporting
                progPercent += (percentPerItem * playedEpisodes.Count);
                progress.Report(progPercent);
            }
            _logger.Info("Episodes to set unwatched: " + unPlayedEpisodes.Count);
            if (unPlayedEpisodes.Count > 0)
            {
                try
                {
                    var dataContracts =
                        await
                        _traktApi.SendEpisodePlaystateUpdates(unPlayedEpisodes, traktUser, false, cancellationToken);

                    if (dataContracts != null)
                    {
                        foreach (var traktSyncResponse in dataContracts)
                        {
                            LogTraktResponseDataContract(traktSyncResponse);
                        }
                    }
                }
                catch (Exception e)
                {
                    _logger.ErrorException("Error updating episode play states", e);
                }
                // purely for progress reporting
                progPercent += (percentPerItem * unPlayedEpisodes.Count);
                progress.Report(progPercent);
            }
        }
Example #8
0
        private async Task SyncTraktDataForUser(User user, double currentProgress, CancellationToken cancellationToken, IProgress <double> progress, double percentPerUser)
        {
            var libraryRoot = user.RootFolder;
            var traktUser   = UserHelper.GetTraktUser(user);

            IEnumerable <TraktMovieWatched> traktWatchedMovies;
            IEnumerable <TraktShowWatched>  traktWatchedShows;

            try
            {
                /*
                 * In order to be as accurate as possible. We need to download the users show collection & the users watched shows.
                 * It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched
                 * like they do for movies.
                 */
                traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);

                traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _logger.ErrorException("Exception handled", ex);
                throw;
            }


            _logger.Info("Trakt.tv watched Movies count = " + traktWatchedMovies.Count());
            _logger.Info("Trakt.tv watched Shows count = " + traktWatchedShows.Count());


            var mediaItems = libraryRoot.GetRecursiveChildren(user)
                             .Where(i => _traktApi.CanSync(i, traktUser))
                             .OrderBy(i =>
            {
                var episode = i as Episode;

                return(episode != null ? episode.Series.Id : i.Id);
            })
                             .ToList();

            // purely for progress reporting
            var percentPerItem = percentPerUser / mediaItems.Count;

            foreach (var movie in mediaItems.OfType <Movie>())
            {
                cancellationToken.ThrowIfCancellationRequested();
                var matchedMovie = FindMatch(movie, traktWatchedMovies);

                if (matchedMovie != null)
                {
                    _logger.Debug("Movie is in Watched list " + movie.Name);

                    var  userData = _userDataManager.GetUserData(user.Id, movie.GetUserDataKey());
                    bool changed  = false;

                    // set movie as watched
                    if (!userData.Played)
                    {
                        userData.Played = true;
                        changed         = true;
                    }

                    // keep the highest play count
                    int playcount = Math.Max(matchedMovie.Plays, userData.PlayCount);
                    // set movie playcount
                    if (userData.PlayCount != playcount)
                    {
                        userData.PlayCount = playcount;
                        changed            = true;
                    }

                    // Set last played to whichever is most recent, remote or local time...
                    if (!string.IsNullOrEmpty(matchedMovie.LastWatchedAt))
                    {
                        var tLastPlayed  = DateTime.Parse(matchedMovie.LastWatchedAt);
                        var latestPlayed = tLastPlayed > userData.LastPlayedDate
                            ? tLastPlayed
                            : userData.LastPlayedDate;
                        if (userData.LastPlayedDate != latestPlayed)
                        {
                            userData.LastPlayedDate = latestPlayed;
                            changed = true;
                        }
                    }

                    // Only process if there's a change
                    if (changed)
                    {
                        await
                        _userDataManager.SaveUserData(user.Id, movie, userData, UserDataSaveReason.Import,
                                                      cancellationToken);
                    }
                }
                else
                {
                    _logger.Info("Failed to match " + movie.Name);
                }

                // purely for progress reporting
                currentProgress += percentPerItem;
                progress.Report(currentProgress);
            }

            foreach (var episode in mediaItems.OfType <Episode>())
            {
                cancellationToken.ThrowIfCancellationRequested();
                var matchedShow = FindMatch(episode.Series, traktWatchedShows);

                if (matchedShow != null)
                {
                    var matchedSeason = matchedShow.Seasons
                                        .FirstOrDefault(tSeason => tSeason.Number == (episode.ParentIndexNumber == 0? 0 : ((episode.ParentIndexNumber ?? 1) + (episode.Series.AnimeSeriesIndex ?? 1) - 1)));

                    // if it's not a match then it means trakt doesn't know about the season, leave the watched state alone and move on
                    if (matchedSeason != null)
                    {
                        // episode is in users libary. Now we need to determine if it's watched
                        var  userData = _userDataManager.GetUserData(user.Id, episode.GetUserDataKey());
                        bool changed  = false;

                        var matchedEpisode = matchedSeason.Episodes.FirstOrDefault(x => x.Number == (episode.IndexNumber ?? -1));

                        if (matchedEpisode != null)
                        {
                            _logger.Debug("Episode is in Watched list " + GetVerboseEpisodeData(episode));

                            // Set episode as watched
                            if (!userData.Played)
                            {
                                userData.Played = true;
                                changed         = true;
                            }

                            // keep the highest play count
                            int playcount = Math.Max(matchedEpisode.Plays, userData.PlayCount);
                            // set episode playcount
                            if (userData.PlayCount != playcount)
                            {
                                userData.PlayCount = playcount;
                                changed            = true;
                            }
                        }
                        else if (!traktUser.SkipUnwatchedImportFromTrakt)
                        {
                            userData.Played         = false;
                            userData.PlayCount      = 0;
                            userData.LastPlayedDate = null;
                            changed = true;
                        }

                        // only process if changed
                        if (changed)
                        {
                            await
                            _userDataManager.SaveUserData(user.Id, episode, userData, UserDataSaveReason.Import,
                                                          cancellationToken);
                        }
                    }
                    else
                    {
                        _logger.Debug("No Season match in Watched shows list " + GetVerboseEpisodeData(episode));
                    }
                }
                else
                {
                    _logger.Debug("No Show match in Watched shows list " + GetVerboseEpisodeData(episode));
                }

                // purely for progress reporting
                currentProgress += percentPerItem;
                progress.Report(currentProgress);
            }
            //_logger.Info(syncItemFailures + " items not parsed");
        }
Example #9
0
        private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double currentProgress, CancellationToken cancellationToken, IProgress <double> progress, double percentPerUser)
        {
            var traktUser = UserHelper.GetTraktUser(user);

            List <TraktMovieWatched> traktWatchedMovies;
            List <TraktShowWatched>  traktWatchedShows;

            try
            {
                /*
                 * In order to be as accurate as possible. We need to download the users show collection & the users watched shows.
                 * It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched
                 * like they do for movies.
                 */
                traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);

                traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception handled");
                throw;
            }

            _logger.LogInformation("Trakt.tv watched Movies count = " + traktWatchedMovies.Count);
            _logger.LogInformation("Trakt.tv watched Shows count = " + traktWatchedShows.Count);

            var mediaItems =
                _libraryManager.GetItemList(
                    new InternalItemsQuery(user)
            {
                IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Episode).Name },
                IsVirtualItem    = false,
                OrderBy          = new[]
                {
                    new ValueTuple <string, SortOrder>(ItemSortBy.SeriesSortName, SortOrder.Ascending),
                    new ValueTuple <string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
                }
            })
                .Where(i => _traktApi.CanSync(i, traktUser)).ToList();

            // purely for progress reporting
            var percentPerItem = percentPerUser / mediaItems.Count;

            foreach (var movie in mediaItems.OfType <Movie>())
            {
                cancellationToken.ThrowIfCancellationRequested();
                var matchedMovie = FindMatch(movie, traktWatchedMovies);

                if (matchedMovie != null)
                {
                    _logger.LogDebug("Movie is in Watched list " + movie.Name);

                    var  userData = _userDataManager.GetUserData(user.Id, movie);
                    bool changed  = false;

                    DateTime?tLastPlayed = null;
                    if (DateTime.TryParse(matchedMovie.last_watched_at, out var value))
                    {
                        tLastPlayed = value;
                    }

                    // set movie as watched
                    if (!userData.Played)
                    {
                        userData.Played         = true;
                        userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
                        changed = true;
                    }

                    // keep the highest play count
                    if (userData.PlayCount < matchedMovie.plays)
                    {
                        userData.PlayCount = matchedMovie.plays;
                        changed            = true;
                    }

                    // Update last played if remote time is more recent
                    if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
                    {
                        userData.LastPlayedDate = tLastPlayed;
                        changed = true;
                    }

                    // Only process if there's a change
                    if (changed)
                    {
                        _userDataManager.SaveUserData(
                            user.Id,
                            movie,
                            userData,
                            UserDataSaveReason.Import,
                            cancellationToken);
                    }
                }
                else
                {
                    //_logger.LogInformation("Failed to match " + movie.Name);
                }

                // purely for progress reporting
                currentProgress += percentPerItem;
                progress.Report(currentProgress);
            }

            foreach (var episode in mediaItems.OfType <Episode>())
            {
                cancellationToken.ThrowIfCancellationRequested();
                var matchedShow = FindMatch(episode.Series, traktWatchedShows);

                if (matchedShow != null)
                {
                    var matchedSeason =
                        matchedShow.seasons.FirstOrDefault(
                            tSeason =>
                            tSeason.number
                            == (episode.ParentIndexNumber == 0
                                        ? 0
                                        : (episode.ParentIndexNumber ?? 1)));

                    // if it's not a match then it means trakt doesn't know about the season, leave the watched state alone and move on
                    if (matchedSeason != null)
                    {
                        // episode is in users libary. Now we need to determine if it's watched
                        var  userData = _userDataManager.GetUserData(user.Id, episode);
                        bool changed  = false;

                        var matchedEpisode =
                            matchedSeason.episodes.FirstOrDefault(x => x.number == (episode.IndexNumber ?? -1));

                        if (matchedEpisode != null)
                        {
                            _logger.LogDebug("Episode is in Watched list " + GetVerboseEpisodeData(episode));

                            if (!traktUser.SkipWatchedImportFromTrakt)
                            {
                                DateTime?tLastPlayed = null;
                                if (DateTime.TryParse(matchedEpisode.last_watched_at, out var value))
                                {
                                    tLastPlayed = value;
                                }

                                // Set episode as watched
                                if (!userData.Played)
                                {
                                    userData.Played         = true;
                                    userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
                                    changed = true;
                                }

                                // keep the highest play count
                                if (userData.PlayCount < matchedEpisode.plays)
                                {
                                    userData.PlayCount = matchedEpisode.plays;
                                    changed            = true;
                                }

                                // Update last played if remote time is more recent
                                if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
                                {
                                    userData.LastPlayedDate = tLastPlayed;
                                    changed = true;
                                }
                            }
                        }
                        else if (!traktUser.SkipUnwatchedImportFromTrakt)
                        {
                            userData.Played         = false;
                            userData.PlayCount      = 0;
                            userData.LastPlayedDate = null;
                            changed = true;
                        }

                        // only process if changed
                        if (changed)
                        {
                            _userDataManager.SaveUserData(
                                user.Id,
                                episode,
                                userData,
                                UserDataSaveReason.Import,
                                cancellationToken);
                        }
                    }
                    else
                    {
                        _logger.LogDebug("No Season match in Watched shows list " + GetVerboseEpisodeData(episode));
                    }
                }
                else
                {
                    _logger.LogDebug("No Show match in Watched shows list " + GetVerboseEpisodeData(episode));
                }

                // purely for progress reporting
                currentProgress += percentPerItem;
                progress.Report(currentProgress);
            }

            // _logger.LogInformation(syncItemFailures + " items not parsed");
        }