Skip to content

Commit

Permalink
Use history api for importing episodes from trakt
Browse files Browse the repository at this point in the history
The history api includes more details (i.e. indexer id) which can be used to properly match episodes (i.e. when jellyfin is using other indexer than trakt).
Matching by indexer id is a lot more accurate than matching by season and number.
This is a rework of jellyfin#191 where we are now using the history by default for episode sync.
  • Loading branch information
h3llrais3r committed Mar 1, 2023
1 parent f3ca422 commit b90028b
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 68 deletions.
13 changes: 12 additions & 1 deletion Trakt/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,23 @@ public static TraktMovieWatchedHistory FindMatch(Movie item, IEnumerable<TraktMo
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="results">>The <see cref="IEnumerable{TraktEpisodeWatchedHistory}"/>.</param>
/// <returns>TraktMovieWatchedHistory.</returns>
/// <returns>TraktEpisodeWatchedHistory.</returns>
public static TraktEpisodeWatchedHistory FindMatch(Episode item, IEnumerable<TraktEpisodeWatchedHistory> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Episode));
}

/// <summary>
/// Gets all watched history matches for an episode.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="results">>The <see cref="IEnumerable{TraktEpisodeWatchedHistory}"/>.</param>
/// <returns>IEnumerable{TraktEpisodeWatchedHistory}.</returns>
public static IEnumerable<TraktEpisodeWatchedHistory> FindAllMatches(Episode item, IEnumerable<TraktEpisodeWatchedHistory> results)
{
return results.Where(i => IsMatch(item, i.Episode)).AsEnumerable();
}

/// <summary>
/// Checks if a <see cref="BaseItem"/> matches a <see cref="TraktMovie"/>.
/// </summary>
Expand Down
143 changes: 76 additions & 67 deletions Trakt/ScheduledTasks/SyncFromTraktTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
{
cancellationToken.ThrowIfCancellationRequested();
var matchedWatchedShow = Extensions.FindMatch(episode.Series, traktWatchedShows);
var matchedWatchedEpisodeHistory = Extensions.FindMatch(episode, traktWatchedEpisodesHistory);
var matchedWatchedEpisodeHistories = Extensions.FindAllMatches(episode, traktWatchedEpisodesHistory);
var matchedPausedEpisode = Extensions.FindMatch(episode, traktPausedEpisodes);
var userData = _userDataManager.GetUserData(user.Id, episode);
bool changed = false;
Expand All @@ -336,42 +336,41 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
tLastReset = resetValue;
}

// Fallback procedure to find match by using episode history
if (matchedWatchedSeason == null && matchedWatchedEpisodeHistory != null)
// Check is match is found via history, fallback to match by season and number
if (matchedWatchedEpisodeHistories != null && matchedWatchedEpisodeHistories.Any())
{
// Find watched season via history match
_logger.LogDebug("Using history to match season for user {User} for {Data}", user.Username, GetVerboseEpisodeData(episode));
matchedWatchedSeason = matchedWatchedShow.Seasons.FirstOrDefault(tSeason => tSeason.Number == matchedWatchedEpisodeHistory.Episode.Season);
}

// If it's not a match then it means trakt.tv doesn't know about the season, leave the watched state alone and move on
if (matchedWatchedSeason != null)
{
// Check for matching episodes including multi-episode entities
var matchedWatchedEpisode = matchedWatchedSeason.Episodes.FirstOrDefault(x => episode.ContainsEpisodeNumber(x.Number));
// History is ordered with last watched first, so take the first one
var lastWatchedEpisodeHistory = matchedWatchedEpisodeHistories.FirstOrDefault();

// Fallback procedure to find match by using episode history
if (matchedWatchedEpisode == null && matchedWatchedEpisodeHistory != null)
// Prepend a check if the matched episode is on a rewatch cycle and
// discard it if the last play date was before the reset date
if (lastWatchedEpisodeHistory != null
&& tLastReset != null
&& DateTime.TryParse(lastWatchedEpisodeHistory.WatchedAt, out var lastPlayedValue)
&& lastPlayedValue < tLastReset)
{
// Find watched season and episode via history match
_logger.LogDebug("Using history to match season and episode for user {User} for {Data}", user.Username, GetVerboseEpisodeData(episode));
matchedWatchedEpisode = matchedWatchedSeason.Episodes.FirstOrDefault(tEpisode => tEpisode.Number == matchedWatchedEpisodeHistory.Episode.Number);
lastWatchedEpisodeHistory = null;
}

// Fallback procedure to find match by using episode history, without checking the season (episode can belong to different season)
if (matchedWatchedEpisode == null && matchedWatchedEpisodeHistory != null)
if (lastWatchedEpisodeHistory != null)
{
// Find watched episode via history match
_logger.LogDebug("Using history to match episode for user {User} for {Data}", user.Username, GetVerboseEpisodeData(episode));
foreach (var season in matchedWatchedShow.Seasons)
_logger.LogDebug("Episode is in watched list of user {User}: {Data}", user.Username, GetVerboseEpisodeData(episode));

episodeWatched = true;
DateTime? tLastPlayed = null;
if (DateTime.TryParse(lastWatchedEpisodeHistory.WatchedAt, out var lastWatchedValue))
{
matchedWatchedEpisode = season.Episodes.FirstOrDefault(tEpisode => tEpisode.Number == matchedWatchedEpisodeHistory.Episode.Number);
if (matchedWatchedEpisode != null)
{
break; // stop when found in a season
}
tLastPlayed = lastWatchedValue;
}

// Update episode data
changed = UpdateEpisodeData(user, episode, userData, tLastPlayed, matchedWatchedEpisodeHistories.Count());
}
}
else if (matchedWatchedSeason != null)
{
// Check for matching episodes including multi-episode entities
var matchedWatchedEpisode = matchedWatchedSeason.Episodes.FirstOrDefault(x => episode.ContainsEpisodeNumber(x.Number));

// Prepend a check if the matched episode is on a rewatch cycle and
// discard it if the last play date was before the reset date
Expand All @@ -394,45 +393,8 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
tLastPlayed = lastWatchedValue;
}

// Set episode as watched
if (!userData.Played)
{
// Only change LastPlayedDate if not set or the local and remote are more than 10 minutes apart
_logger.LogDebug("Marking episode as watched for user {User} locally: {Data}", user.Username, GetVerboseEpisodeData(episode));
if (tLastPlayed == null && userData.LastPlayedDate == null)
{
_logger.LogDebug("Episode's local and remote last played date are missing, falling back to the current time for user {User} locally: {Data}", user.Username, GetVerboseEpisodeData(episode));
userData.LastPlayedDate = DateTime.Now;
}

if (tLastPlayed != null
&& userData.LastPlayedDate != null
&& (tLastPlayed.Value - userData.LastPlayedDate.Value).Duration() > TimeSpan.FromMinutes(10)
&& userData.LastPlayedDate < tLastPlayed)
{
_logger.LogDebug("Setting episode's last played date to remote which is more than 10 minutes more recent than local (remote: {Remote} | local: {Local}) for user {User} locally: {Data}", tLastPlayed, userData.LastPlayedDate, user.Username, GetVerboseEpisodeData(episode));
userData.LastPlayedDate = tLastPlayed;
}

userData.Played = true;
changed = true;
}

// Update last played if remote time is more recent
if (tLastPlayed != null && (userData.LastPlayedDate == null || userData.LastPlayedDate < tLastPlayed))
{
_logger.LogDebug("Adjusting episode's last played date to match a more recent remote last played date (remote: {Remote} | local: {Local}) for user {User} locally: {Name}", tLastPlayed, userData.LastPlayedDate, user.Username, episode.Name);
userData.LastPlayedDate = tLastPlayed;
changed = true;
}

// Keep the highest play count
if (userData.PlayCount < matchedWatchedEpisode.Plays)
{
_logger.LogDebug("Adjusting episode's play count to match a higher remote value (remote: {Remote} | local: {Local}) for user {User} locally: {Data}", matchedWatchedEpisode.Plays, userData.PlayCount, user.Username, GetVerboseEpisodeData(episode));
userData.PlayCount = matchedWatchedEpisode.Plays;
changed = true;
}
// Update episode data
changed = UpdateEpisodeData(user, episode, userData, tLastPlayed, matchedWatchedEpisode.Plays);
}
}
else
Expand Down Expand Up @@ -500,6 +462,53 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
while (previousCount != 0);
}

private bool UpdateEpisodeData(Jellyfin.Data.Entities.User user, Episode episode, UserItemData userData, DateTime? tLastPlayed, int plays)
{
bool changed = false;

// Set episode as watched
if (!userData.Played)
{
// Only change LastPlayedDate if not set or the local and remote are more than 10 minutes apart
_logger.LogDebug("Marking episode as watched for user {User} locally: {Data}", user.Username, GetVerboseEpisodeData(episode));
if (tLastPlayed == null && userData.LastPlayedDate == null)
{
_logger.LogDebug("Episode's local and remote last played date are missing, falling back to the current time for user {User} locally: {Data}", user.Username, GetVerboseEpisodeData(episode));
userData.LastPlayedDate = DateTime.Now;
}

if (tLastPlayed != null
&& userData.LastPlayedDate != null
&& (tLastPlayed.Value - userData.LastPlayedDate.Value).Duration() > TimeSpan.FromMinutes(10)
&& userData.LastPlayedDate < tLastPlayed)
{
_logger.LogDebug("Setting episode's last played date to remote which is more than 10 minutes more recent than local (remote: {Remote} | local: {Local}) for user {User} locally: {Data}", tLastPlayed, userData.LastPlayedDate, user.Username, GetVerboseEpisodeData(episode));
userData.LastPlayedDate = tLastPlayed;
}

userData.Played = true;
changed = true;
}

// Update last played if remote time is more recent
if (tLastPlayed != null && (userData.LastPlayedDate == null || userData.LastPlayedDate < tLastPlayed))
{
_logger.LogDebug("Adjusting episode's last played date to match a more recent remote last played date (remote: {Remote} | local: {Local}) for user {User} locally: {Name}", tLastPlayed, userData.LastPlayedDate, user.Username, episode.Name);
userData.LastPlayedDate = tLastPlayed;
changed = true;
}

// Keep the highest play count
if (userData.PlayCount < plays)
{
_logger.LogDebug("Adjusting episode's play count to match a higher remote value (remote: {Remote} | local: {Local}) for user {User} locally: {Data}", plays, userData.PlayCount, user.Username, GetVerboseEpisodeData(episode));
userData.PlayCount = plays;
changed = true;
}

return changed;
}

private static string GetVerboseEpisodeData(Episode episode)
{
var episodeString = new StringBuilder()
Expand Down

0 comments on commit b90028b

Please sign in to comment.