Fix episode matching for sync from trakt task

- Trakt has switched from tvdb to tmdb as indexer
- Episodes are often not aligned between tvdb and tmdb
- Watched shows api does not contain episodes ids
- History api does contain episode ids
- If no matching is found by season and number (from shows api), fallback to history api to determine a match by episode ids
This commit is contained in:
h3llrais3r 2022-11-28 19:42:33 +01:00
parent 3f7a6cca29
commit 560c17bdf4
6 changed files with 214 additions and 1 deletions

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -647,6 +647,26 @@ public class TraktApi
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>
@ -1080,6 +1100,60 @@ public class TraktApi
}
}
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 (result.GetType().IsGenericType && result.GetType().GetGenericTypeDefinition() == typeof(List<>))
{
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();

View File

@ -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>

View File

@ -7,6 +7,7 @@ using MediaBrowser.Controller.Entities.Movies;
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;
@ -424,6 +425,28 @@ public static class Extensions
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>

View File

@ -16,6 +16,7 @@ using MediaBrowser.Model.Querying;
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;
@ -122,6 +123,8 @@ public class SyncFromTraktTask : IScheduledTask
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>();
@ -136,6 +139,8 @@ public class SyncFromTraktTask : IScheduledTask
{
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)
@ -151,8 +156,10 @@ public class SyncFromTraktTask : IScheduledTask
}
_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)
@ -312,6 +319,7 @@ public class SyncFromTraktTask : IScheduledTask
{
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;
@ -328,12 +336,28 @@ public class SyncFromTraktTask : IScheduledTask
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 via history match
_logger.LogDebug("Using history to match episode for user {User} for {Data}", user.Username, GetVerboseEpisodeData(episode));
matchedWatchedEpisode = matchedWatchedSeason.Episodes.FirstOrDefault(tEpisode => tEpisode.Number == matchedWatchedEpisodeHistory.Episode.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
if (matchedWatchedEpisode != null
@ -467,7 +491,15 @@ public class SyncFromTraktTask : IScheduledTask
? 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();
}