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

Fix episode matching for sync from trakt task #191

Merged
merged 3 commits into from
Dec 4, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
40 changes: 40 additions & 0 deletions Trakt/Api/DataContracts/Sync/History/TraktEpisodeWatchedHistory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;

namespace Trakt.Api.DataContracts.Sync.History;

/// <summary>
/// The trakt.tv sync episode watched history class.
/// </summary>
public class TraktEpisodeWatchedHistory
{
/// <summary>
/// Gets or sets the watched date.
/// </summary>
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }

/// <summary>
/// Gets or sets the action.
/// </summary>
[JsonPropertyName("action")]
public string Action { get; set; }

/// <summary>
/// Gets or sets the type.
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }

/// <summary>
/// Gets or sets the episode.
/// </summary>
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }

/// <summary>
/// Gets or sets the episode.
/// </summary>
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
}
34 changes: 34 additions & 0 deletions Trakt/Api/DataContracts/Sync/History/TraktMovieWatchedHistory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;

namespace Trakt.Api.DataContracts.Sync.History;

/// <summary>
/// The trakt.tv sync movie watched history class.
/// </summary>
public class TraktMovieWatchedHistory
{
/// <summary>
/// Gets or sets the watched date.
/// </summary>
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }

/// <summary>
/// Gets or sets the action.
/// </summary>
[JsonPropertyName("action")]
public string Action { get; set; }

/// <summary>
/// Gets or sets the type.
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; }

/// <summary>
/// Gets or sets the movie.
/// </summary>
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}
74 changes: 74 additions & 0 deletions Trakt/Api/TraktApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,26 @@ public async Task<List<DataContracts.Users.Watched.TraktShowWatched>> SendGetWat
return await GetFromTrakt<List<DataContracts.Users.Watched.TraktShowWatched>>(TraktUris.WatchedShows, traktUser).ConfigureAwait(false);
}

/// <summary>
/// Get watched movies history.
/// </summary>
/// <param name="traktUser">The <see cref="TraktUser"/>.</param>
/// <returns>Task{List{DataContracts.Sync.History.TraktMovieWatchedHistory}}.</returns>
public async Task<List<DataContracts.Sync.History.TraktMovieWatchedHistory>> SendGetWatchedMoviesHistoryRequest(TraktUser traktUser)
{
return await GetFromTraktWithPaging<DataContracts.Sync.History.TraktMovieWatchedHistory>(TraktUris.SyncWatchedMoviesHistory, traktUser).ConfigureAwait(false);
}

/// <summary>
/// Get watched episodes history.
/// </summary>
/// <param name="traktUser">The <see cref="TraktUser"/>.</param>
/// <returns>Task{List{DataContracts.Sync.History.TraktEpisodeWatchedHistory}}.</returns>
public async Task<List<DataContracts.Sync.History.TraktEpisodeWatchedHistory>> SendGetWatchedEpisodesHistoryRequest(TraktUser traktUser)
{
return await GetFromTraktWithPaging<DataContracts.Sync.History.TraktEpisodeWatchedHistory>(TraktUris.SyncWatchedEpisodesHistory, traktUser).ConfigureAwait(false);
}

/// <summary>
/// Get all paused movies.
/// </summary>
Expand Down Expand Up @@ -1080,6 +1100,60 @@ private async Task<T> GetFromTrakt<T>(string url, TraktUser traktUser, Cancellat
}
}

private Task<List<T>> GetFromTraktWithPaging<T>(string url, TraktUser traktUser)
{
return GetFromTraktWithPaging<T>(url, traktUser, CancellationToken.None);
}

private async Task<List<T>> GetFromTraktWithPaging<T>(string url, TraktUser traktUser, CancellationToken cancellationToken)
{
var httpClient = GetHttpClient();
var page = 1;
var result = new List<T>();

if (traktUser != null)
{
await SetRequestHeaders(httpClient, traktUser).ConfigureAwait(false);
}

await _traktResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
while (true)
{
var urlWithPage = url.Replace("{page}", page.ToString(CultureInfo.InvariantCulture), StringComparison.InvariantCulture);
var response = await RetryHttpRequest(async () => await httpClient.GetAsync(urlWithPage, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return result;
}

response.EnsureSuccessStatusCode();
var tmpResult = await response.Content.ReadFromJsonAsync<List<T>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (tmpResult != null)
{
result.AddRange(tmpResult);
}

if (int.Parse(response.Headers.GetValues("X-Pagination-Page-Count").FirstOrDefault(page.ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture) != page)
{
page++;
}
else
{
break; // break loop when no more new pages are available
}
}

return result;
}
finally
{
_traktResourcePool.Release();
}
}

private async Task<HttpResponseMessage> PostToTrakt(string url, object data)
{
var httpClient = GetHttpClient();
Expand Down
10 changes: 10 additions & 0 deletions Trakt/Api/TraktURIs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public static class TraktUris
/// </summary>
public const string SyncCollectionRemove = BaseUrl + "/sync/collection/remove";

/// <summary>
/// The watched movies history URI.
/// </summary>
public const string SyncWatchedMoviesHistory = BaseUrl + "/sync/history/movies?page={page}&limit=1000";

/// <summary>
/// The watched episodes history URI.
/// </summary>
public const string SyncWatchedEpisodesHistory = BaseUrl + "/sync/history/episodes?page={page}&limit=1000";

/// <summary>
/// The watched history add URI.
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions Trakt/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.Entities;
using Trakt.Api.DataContracts.BaseModel;
using Trakt.Api.DataContracts.Sync.History;
using Trakt.Api.DataContracts.Users.Collection;
using Trakt.Api.DataContracts.Users.Playback;
using Trakt.Api.DataContracts.Users.Watched;
Expand Down Expand Up @@ -424,6 +425,28 @@ public static TraktMoviePaused FindMatch(BaseItem item, IEnumerable<TraktMoviePa
return results.FirstOrDefault(i => IsMatch(item, i.Movie));
}

/// <summary>
/// Gets a watched history match for a movie.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="results">>The <see cref="IEnumerable{TraktMovieWatchedHistory}"/>.</param>
/// <returns>TraktMovieWatchedHistory.</returns>
public static TraktMovieWatchedHistory FindMatch(Movie item, IEnumerable<TraktMovieWatchedHistory> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Movie));
}

/// <summary>
/// Gets a watched history match for an episode.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="results">>The <see cref="IEnumerable{TraktEpisodeWatchedHistory}"/>.</param>
/// <returns>TraktMovieWatchedHistory.</returns>
public static TraktEpisodeWatchedHistory FindMatch(Episode item, IEnumerable<TraktEpisodeWatchedHistory> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Episode));
}

/// <summary>
/// Checks if a <see cref="BaseItem"/> matches a <see cref="TraktMovie"/>.
/// </summary>
Expand Down
49 changes: 48 additions & 1 deletion Trakt/ScheduledTasks/SyncFromTraktTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Trakt.Api;
using Trakt.Api.DataContracts.Sync.History;
using Trakt.Api.DataContracts.Users.Playback;
using Trakt.Api.DataContracts.Users.Watched;
using Trakt.Helpers;
Expand Down Expand Up @@ -122,6 +123,8 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double

List<TraktMovieWatched> traktWatchedMovies = new List<TraktMovieWatched>();
List<TraktShowWatched> traktWatchedShows = new List<TraktShowWatched>();
List<TraktMovieWatchedHistory> traktWatchedMoviesHistory = new List<TraktMovieWatchedHistory>(); // not used for now, just for reference to get watched movies history count
List<TraktEpisodeWatchedHistory> traktWatchedEpisodesHistory = new List<TraktEpisodeWatchedHistory>(); // used for fall episode matching by ids
List<TraktMoviePaused> traktPausedMovies = new List<TraktMoviePaused>();
List<TraktEpisodePaused> traktPausedEpisodes = new List<TraktEpisodePaused>();

Expand All @@ -136,6 +139,8 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
{
traktWatchedMovies.AddRange(await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false));
traktWatchedShows.AddRange(await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false));
traktWatchedMoviesHistory.AddRange(await _traktApi.SendGetWatchedMoviesHistoryRequest(traktUser).ConfigureAwait(false));
traktWatchedEpisodesHistory.AddRange(await _traktApi.SendGetWatchedEpisodesHistoryRequest(traktUser).ConfigureAwait(false));
}

if (!traktUser.SkipPlaybackProgressImportFromTrakt)
Expand All @@ -151,8 +156,10 @@ private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double
}

_logger.LogInformation("Trakt.tv watched movies for user {User}: {Count}", user.Username, traktWatchedMovies.Count);
_logger.LogInformation("Trakt.tv watched movies history for user {User}: {Count}", user.Username, traktWatchedMoviesHistory.Count);
_logger.LogInformation("Trakt.tv paused movies for user {User}: {Count}", user.Username, traktPausedMovies.Count);
_logger.LogInformation("Trakt.tv watched shows for user {User}: {Count}", user.Username, traktWatchedShows.Count);
_logger.LogInformation("Trakt.tv watched episodes history for user {User}: {Count}", user.Username, traktWatchedEpisodesHistory.Count);
_logger.LogInformation("Trakt.tv paused episodes for user {User}: {Count}", user.Username, traktPausedEpisodes.Count);

var baseQuery = new InternalItemsQuery(user)
Expand Down Expand Up @@ -312,6 +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 matchedPausedEpisode = Extensions.FindMatch(episode, traktPausedEpisodes);
var userData = _userDataManager.GetUserData(user.Id, episode);
bool changed = false;
Expand All @@ -328,12 +336,43 @@ 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)
{
// 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));

// 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
}
}
}

// 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
Expand Down Expand Up @@ -467,7 +506,15 @@ private static string GetVerboseEpisodeData(Episode episode)
? episode.Series.Name
: "null property"
: "null class")
.Append('\'');
.Append("' ")
.Append("Tvdb id: ")
.Append(episode.GetProviderId(MetadataProvider.Tvdb) ?? "null").Append(' ')
.Append("Tmdb id: ")
.Append(episode.GetProviderId(MetadataProvider.Tmdb) ?? "null").Append(' ')
.Append("Imdb id: ")
.Append(episode.GetProviderId(MetadataProvider.Imdb) ?? "null").Append(' ')
.Append("TvRage id: ")
.Append(episode.GetProviderId(MetadataProvider.TvRage) ?? "null");

return episodeString.ToString();
}
Expand Down