/// <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); } }
/// <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); } }
/// <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); } }
/// <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); } }
/// <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); } }
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"); }
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"); }