mirror of
https://github.com/jellyfin/jellyfin-plugin-trakt.git
synced 2025-02-17 03:37:34 +00:00
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:
parent
3f7a6cca29
commit
560c17bdf4
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user