Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use history api for importing episodes from trakt #205

Merged
merged 17 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 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)).AsEnumerable();
}

/// <summary>
/// Checks if a <see cref="BaseItem"/> matches a <see cref="TraktMovie"/>.
/// </summary>
Expand Down Expand Up @@ -539,4 +550,39 @@ public static bool IsMatch(Episode item, TraktEpisode episode)

return false;
}

/// <summary>
/// Checks if a <see cref="Episode"/> matches a <see cref="TraktEpisodeWatchedHistory"/>.
/// </summary>
/// <param name="item">The <see cref="Episode"/>.</param>
/// <param name="episodeHistory">The <see cref="TraktEpisodeWatchedHistory"/>.</param>
/// <returns><see cref="bool"/> indicating if the <see cref="Episode"/> matches a <see cref="TraktEpisodeWatchedHistory"/>.</returns>
public static bool IsMatch(Episode item, TraktEpisodeWatchedHistory episodeHistory)
{
// Match by provider id's
if (IsMatch(item, episodeHistory.Episode))
{
return true;
}

// Match by show, season and episode number if there isn't any provider id in common
// If there was a common provider id between the item and the trakt episode (f.e. both have tvdb id), you shouldn't check anymore by season/number
if (HasAnyProviderTvIdInCommon(item, episodeHistory.Episode)
&& IsMatch(item.Series, episodeHistory.Show)
&& item.GetSeasonNumber() == episodeHistory.Episode.Season
&& item.ContainsEpisodeNumber(episodeHistory.Episode.Number))
{
return true;
}

return false;
}

private static bool HasAnyProviderTvIdInCommon(Episode item, TraktEpisode traktEpisode)
{
return (item.HasProviderId(MetadataProvider.Tvdb) && traktEpisode.Ids.Tvdb != null)
|| (item.HasProviderId(MetadataProvider.Imdb) && traktEpisode.Ids.Imdb != null)
|| (item.HasProviderId(MetadataProvider.Tmdb) && traktEpisode.Ids.Tmdb != null)
|| (item.HasProviderId(MetadataProvider.TvRage) && traktEpisode.Ids.Tvrage != null);
crobibero marked this conversation as resolved.
Show resolved Hide resolved
}
}
66 changes: 18 additions & 48 deletions Trakt/ScheduledTasks/SyncFromTraktTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
Expand Down Expand Up @@ -109,7 +110,7 @@ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken can
}
}

private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double currentProgress, IProgress<double> progress, double percentPerUser, CancellationToken cancellationToken)
private async Task SyncTraktDataForUser(User user, double currentProgress, IProgress<double> progress, double percentPerUser, CancellationToken cancellationToken)
{
var traktUser = UserHelper.GetTraktUser(user, true);

Expand Down Expand Up @@ -319,77 +320,45 @@ 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 matchedPausedEpisode = Extensions.FindMatch(episode, traktPausedEpisodes);
var userData = _userDataManager.GetUserData(user.Id, episode);
bool changed = false;
bool episodeWatched = false;

if (!traktUser.SkipWatchedImportFromTrakt && matchedWatchedShow != null)
{
var matchedWatchedSeason = matchedWatchedShow.Seasons.FirstOrDefault(tSeason => tSeason.Number == episode.GetSeasonNumber());

// Keep track of the shows rewatch cycles
DateTime? tLastReset = null;
if (DateTime.TryParse(matchedWatchedShow.ResetAt, out var resetValue))
{
tLastReset = resetValue;
}

// Fallback procedure to find match by using episode history
if (matchedWatchedSeason == null && matchedWatchedEpisodeHistory != null)
{
// 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);
}
var matchedWatchedEpisodeHistory = Extensions.FindAllMatches(episode, traktWatchedEpisodesHistory);

// 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 if match is found in history
if (matchedWatchedEpisodeHistory != null && matchedWatchedEpisodeHistory.Any())
h3llrais3r marked this conversation as resolved.
Show resolved Hide resolved
{
// Check for matching episodes including multi-episode entities
var matchedWatchedEpisode = matchedWatchedSeason.Episodes.FirstOrDefault(x => episode.ContainsEpisodeNumber(x.Number));

// Fallback procedure to find match by using episode history
if (matchedWatchedEpisode == null && matchedWatchedEpisodeHistory != null)
{
// 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);
}

// Fallback procedure to find match by using episode history, without checking the season (episode can belong to different season)
if (matchedWatchedEpisode == null && matchedWatchedEpisodeHistory != 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)
{
matchedWatchedEpisode = season.Episodes.FirstOrDefault(tEpisode => tEpisode.Number == matchedWatchedEpisodeHistory.Episode.Number);
if (matchedWatchedEpisode != null)
{
break; // stop when found in a season
}
}
}
// History is ordered with last watched first, so take the first one
var lastWatchedEpisodeHistory = matchedWatchedEpisodeHistory.FirstOrDefault();

// 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 (matchedWatchedEpisode != null
if (lastWatchedEpisodeHistory != null
&& tLastReset != null
&& DateTime.TryParse(matchedWatchedEpisode.LastWatchedAt, out var lastPlayedValue)
&& DateTime.TryParse(lastWatchedEpisodeHistory.WatchedAt, out var lastPlayedValue)
&& lastPlayedValue < tLastReset)
{
matchedWatchedEpisode = null;
lastWatchedEpisodeHistory = null;
}

if (matchedWatchedEpisode != null)
if (lastWatchedEpisodeHistory != null)
{
_logger.LogDebug("Episode is in watched list of user {User}: {Data}", user.Username, GetVerboseEpisodeData(episode));
_logger.LogDebug("Episode is in watched history list of user {User}: {Data}", user.Username, GetVerboseEpisodeData(episode));

episodeWatched = true;
DateTime? tLastPlayed = null;
if (DateTime.TryParse(matchedWatchedEpisode.LastWatchedAt, out var lastWatchedValue))
if (DateTime.TryParse(lastWatchedEpisodeHistory.WatchedAt, out var lastWatchedValue))
{
tLastPlayed = lastWatchedValue;
}
Expand Down Expand Up @@ -427,17 +396,18 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
}

// Keep the highest play count
if (userData.PlayCount < matchedWatchedEpisode.Plays)
var playCount = matchedWatchedEpisodeHistory.Count();
if (userData.PlayCount < playCount)
{
_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;
_logger.LogDebug("Adjusting episode's play count to match a higher remote value (remote: {Remote} | local: {Local}) for user {User} locally: {Data}", playCount, userData.PlayCount, user.Username, GetVerboseEpisodeData(episode));
userData.PlayCount = playCount;
changed = true;
}
}
}
else
{
_logger.LogDebug("No season data found for user {User} for {Data}", user.Username, GetVerboseEpisodeData(episode));
_logger.LogDebug("No episode history data found for user {User} for {Data}", user.Username, GetVerboseEpisodeData(episode));
}
}
else
Expand Down