Enable analyzers, fix all warnings

This commit is contained in:
Shadowghost 2022-04-16 19:29:37 +02:00
parent 0985b9455c
commit 9cc1a6f746
77 changed files with 4197 additions and 3128 deletions

View File

@ -1,30 +1,34 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktEpisode
namespace Trakt.Api.DataContracts.BaseModel
{
/// <summary>
/// Gets or sets the season number.
/// The trakt.tv episode class.
/// </summary>
[JsonPropertyName("season")]
public int? Season { get; set; }
public class TraktEpisode
{
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("season")]
public int? Season { get; set; }
/// <summary>
/// Gets or sets the episode number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
/// <summary>
/// Gets or sets the episode number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
/// <summary>
/// Gets or sets the episode title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// Gets or sets the episode title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// Gets or sets the episode ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktEpisodeId Ids { get; set; }
/// <summary>
/// Gets or sets the episode ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktEpisodeId Ids { get; set; }
}
}

View File

@ -1,5 +1,9 @@
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktEpisodeId : TraktTVId
namespace Trakt.Api.DataContracts.BaseModel
{
/// <summary>
/// The trakt.tv episode id class.
/// </summary>
public class TraktEpisodeId : TraktTVId
{
}
}

View File

@ -1,12 +1,22 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktIMDBandTMDBId : TraktId
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("imdb")]
public string Imdb { get; set; }
/// <summary>
/// The trakt.tv IDMb and TMDb id class.
/// </summary>
public class TraktIMDBandTMDBId : TraktId
{
/// <summary>
/// Gets or sets the IMDb id.
/// </summary>
[JsonPropertyName("imdb")]
public string Imdb { get; set; }
[JsonPropertyName("tmdb")]
public int? Tmdb { get; set; }
/// <summary>
/// Gets or sets the TMDb id.
/// </summary>
[JsonPropertyName("tmdb")]
public int? Tmdb { get; set; }
}
}

View File

@ -1,18 +1,22 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktId
namespace Trakt.Api.DataContracts.BaseModel
{
/// <summary>
/// Gets or sets the Trakt item id.
/// The trakt.tv id class.
/// </summary>
[JsonPropertyName("trakt")]
public int? Trakt { get; set; }
public class TraktId
{
/// <summary>
/// Gets or sets the trakt.tv item id.
/// </summary>
[JsonPropertyName("trakt")]
public int? Trakt { get; set; }
/// <summary>
/// Gets or sets the item slug.
/// </summary>
[JsonPropertyName("slug")]
public string Slug { get; set; }
/// <summary>
/// Gets or sets the item slug.
/// </summary>
[JsonPropertyName("slug")]
public string Slug { get; set; }
}
}

View File

@ -1,15 +1,28 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktMovie
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// The trakt.tv movie class.
/// </summary>
public class TraktMovie
{
/// <summary>
/// Gets or sets the movie title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public int? Year { get; set; }
/// <summary>
/// Gets or sets the movie year.
/// </summary>
[JsonPropertyName("year")]
public int? Year { get; set; }
[JsonPropertyName("ids")]
public TraktMovieId Ids { get; set; }
/// <summary>
/// Gets or sets the movie ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktMovieId Ids { get; set; }
}
}

View File

@ -1,5 +1,9 @@
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktMovieId : TraktIMDBandTMDBId
namespace Trakt.Api.DataContracts.BaseModel
{
/// <summary>
/// The trakt.tv movie id class.
/// </summary>
public class TraktMovieId : TraktIMDBandTMDBId
{
}
}

View File

@ -1,12 +1,22 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktPerson
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("name")]
public string Name { get; set; }
/// <summary>
/// The trakt.tv person class.
/// </summary>
public class TraktPerson
{
/// <summary>
/// Gets or sets the person name.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("ids")]
public TraktPersonId Ids { get; set; }
/// <summary>
/// Gets or sets the person ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktPersonId Ids { get; set; }
}
}

View File

@ -1,9 +1,16 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktPersonId : TraktIMDBandTMDBId
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
/// <summary>
/// The trakt.tv person id class.
/// </summary>
public class TraktPersonId : TraktIMDBandTMDBId
{
/// <summary>
/// Gets or sets the TVRage person id.
/// </summary>
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
}
}

View File

@ -1,12 +1,22 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public abstract class TraktRated
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("rating")]
public int? Rating { get; set; }
/// <summary>
/// The trakt.tv rated class.
/// </summary>
public abstract class TraktRated
{
/// <summary>
/// Gets or sets the rating.
/// </summary>
[JsonPropertyName("rating")]
public int? Rating { get; set; }
[JsonPropertyName("rated_at")]
public string RatedAt { get; set; }
/// <summary>
/// Gets or sets the rating date.
/// </summary>
[JsonPropertyName("rated_at")]
public string RatedAt { get; set; }
}
}

View File

@ -1,12 +1,22 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktSeason
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("number")]
public int? Number { get; set; }
/// <summary>
/// The trakt.tv season class.
/// </summary>
public class TraktSeason
{
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
[JsonPropertyName("ids")]
public TraktSeasonId Ids { get; set; }
/// <summary>
/// Gets or sets the season ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktSeasonId Ids { get; set; }
}
}

View File

@ -1,15 +1,28 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktSeasonId : TraktId
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("tmdb")]
public int? Tmdb { get; set; }
/// <summary>
/// The trakt.tv season id class.
/// </summary>
public class TraktSeasonId : TraktId
{
/// <summary>
/// Gets or sets the season TMDb id.
/// </summary>
[JsonPropertyName("tmdb")]
public int? Tmdb { get; set; }
[JsonPropertyName("tvdb")]
public int? Tvdb { get; set; }
/// <summary>
/// Gets or sets the season TVDb id.
/// </summary>
[JsonPropertyName("tvdb")]
public int? Tvdb { get; set; }
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
/// <summary>
/// Gets or sets the season TVRage id.
/// </summary>
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
}
}

View File

@ -1,15 +1,28 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktShow
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// The trakt.tv show class.
/// </summary>
public class TraktShow
{
/// <summary>
/// Gets or sets the show title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public int? Year { get; set; }
/// <summary>
/// Gets or sets the show year.
/// </summary>
[JsonPropertyName("year")]
public int? Year { get; set; }
[JsonPropertyName("ids")]
public TraktShowId Ids { get; set; }
/// <summary>
/// Gets or sets the show ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktShowId Ids { get; set; }
}
}

View File

@ -1,5 +1,9 @@
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktShowId : TraktTVId
namespace Trakt.Api.DataContracts.BaseModel
{
/// <summary>
/// The trakt.tv show id class.
/// </summary>
public class TraktShowId : TraktTVId
{
}
}

View File

@ -1,12 +1,22 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktTVId : TraktIMDBandTMDBId
namespace Trakt.Api.DataContracts.BaseModel
{
[JsonPropertyName("tvdb")]
public int? Tvdb { get; set; }
/// <summary>
/// The trakt.tv tv id class.
/// </summary>
public class TraktTVId : TraktIMDBandTMDBId
{
/// <summary>
/// Gets or sets the TVDb id.
/// </summary>
[JsonPropertyName("tvdb")]
public int? Tvdb { get; set; }
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
/// <summary>
/// Gets or sets the TVRage id.
/// </summary>
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
}
}

View File

@ -1,15 +1,28 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Scrobble;
public class SocialMedia
namespace Trakt.Api.DataContracts.Scrobble
{
[JsonPropertyName("facebook")]
public bool Facebook { get; set; }
/// <summary>
/// The trakt.tv social media class.
/// </summary>
public class SocialMedia
{
/// <summary>
/// Gets or sets a value indicating whether facebook posting should be enabled.
/// </summary>
[JsonPropertyName("facebook")]
public bool Facebook { get; set; }
[JsonPropertyName("twitter")]
public bool Twitter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether twittwe posting should be enabled.
/// </summary>
[JsonPropertyName("twitter")]
public bool Twitter { get; set; }
[JsonPropertyName("tumblr")]
public bool Tumblr { get; set; }
/// <summary>
/// Gets or sets a value indicating whether tumblr posting should be enabled.
/// </summary>
[JsonPropertyName("tumblr")]
public bool Tumblr { get; set; }
}
}

View File

@ -1,22 +1,41 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Scrobble;
public class TraktScrobbleEpisode
namespace Trakt.Api.DataContracts.Scrobble
{
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
/// <summary>
/// The trakt.tv episode scrobble class.
/// </summary>
public class TraktScrobbleEpisode
{
/// <summary>
/// Gets or sets the show.
/// </summary>
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
/// <summary>
/// Gets or sets the episode.
/// </summary>
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
[JsonPropertyName("progress")]
public float Progress { get; set; }
/// <summary>
/// Gets or sets the progress.
/// </summary>
[JsonPropertyName("progress")]
public float Progress { get; set; }
[JsonPropertyName("app_version")]
public string AppVersion { get; set; }
/// <summary>
/// Gets or sets the app version.
/// </summary>
[JsonPropertyName("app_version")]
public string AppVersion { get; set; }
[JsonPropertyName("app_date")]
public string AppDate { get; set; }
/// <summary>
/// Gets or sets the app date.
/// </summary>
[JsonPropertyName("app_date")]
public string AppDate { get; set; }
}
}

View File

@ -1,19 +1,35 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Scrobble;
public class TraktScrobbleMovie
namespace Trakt.Api.DataContracts.Scrobble
{
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
/// <summary>
/// The trakt.tv movie scrobble class.
/// </summary>
public class TraktScrobbleMovie
{
/// <summary>
/// Gets or sets the movie.
/// </summary>
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
[JsonPropertyName("progress")]
public float Progress { get; set; }
/// <summary>
/// Gets or sets the progress.
/// </summary>
[JsonPropertyName("progress")]
public float Progress { get; set; }
[JsonPropertyName("app_version")]
public string AppVersion { get; set; }
/// <summary>
/// Gets or sets the app versin.
/// </summary>
[JsonPropertyName("app_version")]
public string AppVersion { get; set; }
[JsonPropertyName("app_date")]
public string AppDate { get; set; }
/// <summary>
/// Gets or sets the app date.
/// </summary>
[JsonPropertyName("app_date")]
public string AppDate { get; set; }
}
}

View File

@ -1,25 +1,53 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Scrobble;
public class TraktScrobbleResponse
namespace Trakt.Api.DataContracts.Scrobble
{
[JsonPropertyName("action")]
public string Action { get; set; }
/// <summary>
/// The trakt.tv scrobble response class.
/// </summary>
public class TraktScrobbleResponse
{
/// <summary>
/// Gets or sets the id.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("progress")]
public float Progress { get; set; }
/// <summary>
/// Gets or sets the action.
/// </summary>
[JsonPropertyName("action")]
public string Action { get; set; }
[JsonPropertyName("sharing")]
public SocialMedia Sharing { get; set; }
/// <summary>
/// Gets or sets the progress.
/// </summary>
[JsonPropertyName("progress")]
public float Progress { get; set; }
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
/// <summary>
/// Gets or sets the sharing options.
/// </summary>
[JsonPropertyName("sharing")]
public SocialMedia Sharing { get; set; }
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
/// <summary>
/// Gets or sets the movie.
/// </summary>
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
/// <summary>
/// Gets or sets the episode.
/// </summary>
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
/// <summary>
/// Gets or sets the show.
/// </summary>
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
}
}

View File

@ -1,24 +1,43 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Collection;
public class TraktEpisodeCollected : TraktEpisode
namespace Trakt.Api.DataContracts.Sync.Collection
{
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
/// <summary>
/// The trakt.tv sync episodes collected class.
/// </summary>
public class TraktEpisodeCollected : TraktEpisode
{
/// <summary>
/// Gets or sets the colletion date.
/// </summary>
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
[JsonPropertyName("media_type")]
public string MediaType { get; set; }
/// <summary>
/// Gets or sets the media type.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; set; }
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
/// <summary>
/// Gets or sets the resolution.
/// </summary>
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
[JsonPropertyName("audio")]
public string Audio { get; set; }
/// <summary>
/// Gets or sets the audio.
/// </summary>
[JsonPropertyName("audio")]
public string Audio { get; set; }
[JsonPropertyName("audio_channels")]
public string AudioChannels { get; set; }
/// <summary>
/// Gets or sets the amount of audio channels.
/// </summary>
[JsonPropertyName("audio_channels")]
public string AudioChannels { get; set; }
// public bool 3d { get; set; }
// public bool 3d { get; set; }
}
}

View File

@ -1,24 +1,43 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Collection;
public class TraktMovieCollected : TraktMovie
namespace Trakt.Api.DataContracts.Sync.Collection
{
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
/// <summary>
/// The trakt.tv sync movies collected class.
/// </summary>
public class TraktMovieCollected : TraktMovie
{
/// <summary>
/// Gets or sets the collection date.
/// </summary>
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
[JsonPropertyName("media_type")]
public string MediaType { get; set; }
/// <summary>
/// Gets or sets the media type.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; set; }
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
/// <summary>
/// Gets or sets the resolution.
/// </summary>
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
[JsonPropertyName("audio")]
public string Audio { get; set; }
/// <summary>
/// Gets or sets the audio.
/// </summary>
[JsonPropertyName("audio")]
public string Audio { get; set; }
[JsonPropertyName("audio_channels")]
public string AudioChannels { get; set; }
/// <summary>
/// Gets or sets the amount of audio channels.
/// </summary>
[JsonPropertyName("audio_channels")]
public string AudioChannels { get; set; }
// public bool 3d { get; set; }
// public bool 3d { get; set; }
}
}

View File

@ -1,16 +1,26 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Sync.Collection;
public class TraktSeasonCollected
namespace Trakt.Api.DataContracts.Sync.Collection
{
[JsonPropertyName("number")]
public int Number { get; set; }
/// <summary>
/// The trakt.tv sync seasons collected class.
/// </summary>
public class TraktSeasonCollected
{
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisodeCollected> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<TraktEpisodeCollected> Episodes { get; set; }
}
}

View File

@ -1,14 +1,21 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Collection;
public class TraktShowCollected : TraktShow
namespace Trakt.Api.DataContracts.Sync.Collection
{
[JsonPropertyName("seasons")]
public List<TraktSeasonCollected> Seasons { get; set; }
/// <summary>
/// The trakt.tv sync show collected class.
/// </summary>
public class TraktShowCollected : TraktShow
{
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public List<TraktSeasonCollected> Seasons { get; set; }
}
}

View File

@ -1,21 +1,40 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Sync;
public class Items
namespace Trakt.Api.DataContracts.Sync
{
[JsonPropertyName("movies")]
public int Movies { get; set; }
/// <summary>
/// The trakt.tv sync items class.
/// </summary>
public class Items
{
/// <summary>
/// Gets or sets the movies.
/// </summary>
[JsonPropertyName("movies")]
public int Movies { get; set; }
[JsonPropertyName("shows")]
public int Shows { get; set; }
/// <summary>
/// Gets or sets the shows.
/// </summary>
[JsonPropertyName("shows")]
public int Shows { get; set; }
[JsonPropertyName("seasons")]
public int Seasons { get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public int Seasons { get; set; }
[JsonPropertyName("episodes")]
public int Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public int Episodes { get; set; }
[JsonPropertyName("people")]
public int People { get; set; }
/// <summary>
/// Gets or sets the people.
/// </summary>
[JsonPropertyName("people")]
public int People { get; set; }
}
}

View File

@ -1,26 +1,45 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync;
public class NotFoundObjects
namespace Trakt.Api.DataContracts.Sync
{
[JsonPropertyName("movies")]
public List<TraktMovie> Movies { get; set; }
/// <summary>
/// The trakt.tv sync not found objects class.
/// </summary>
public class NotFoundObjects
{
/// <summary>
/// Gets or sets the movies.
/// </summary>
[JsonPropertyName("movies")]
public List<TraktMovie> Movies { get; set; }
[JsonPropertyName("shows")]
public List<TraktShow> Shows { get; set; }
/// <summary>
/// Gets or sets the shows.
/// </summary>
[JsonPropertyName("shows")]
public List<TraktShow> Shows { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisode> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<TraktEpisode> Episodes { get; set; }
[JsonPropertyName("seasons")]
public List<TraktSeason> Seasons { get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public List<TraktSeason> Seasons { get; set; }
[JsonPropertyName("people")]
public List<TraktPerson> People { get; set; }
/// <summary>
/// Gets or sets the people.
/// </summary>
[JsonPropertyName("people")]
public List<TraktPerson> People { get; set; }
}
}

View File

@ -1,13 +1,23 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Ratings;
public class TraktEpisodeRated : TraktRated
namespace Trakt.Api.DataContracts.Sync.Ratings
{
[JsonPropertyName("number")]
public int? Number { get; set; }
/// <summary>
/// The trakt.tv sync episode rated class.
/// </summary>
public class TraktEpisodeRated : TraktRated
{
/// <summary>
/// Gets or sets the episode number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
[JsonPropertyName("ids")]
public TraktEpisodeId Ids { get; set; }
/// <summary>
/// Gets or sets the ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktEpisodeId Ids { get; set; }
}
}

View File

@ -1,16 +1,29 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Ratings;
public class TraktMovieRated : TraktRated
namespace Trakt.Api.DataContracts.Sync.Ratings
{
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// The trakt.tv sync movie rated class.
/// </summary>
public class TraktMovieRated : TraktRated
{
/// <summary>
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public int? Year { get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
[JsonPropertyName("year")]
public int? Year { get; set; }
[JsonPropertyName("ids")]
public TraktMovieId Ids { get; set; }
/// <summary>
/// Gets or sets the ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktMovieId Ids { get; set; }
}
}

View File

@ -1,17 +1,27 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Ratings;
public class TraktSeasonRated : TraktRated
namespace Trakt.Api.DataContracts.Sync.Ratings
{
[JsonPropertyName("number")]
public int? Number { get; set; }
/// <summary>
/// The trakt.tv sync season rated class.
/// </summary>
public class TraktSeasonRated : TraktRated
{
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisodeRated> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<TraktEpisodeRated> Episodes { get; set; }
}
}

View File

@ -1,23 +1,39 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Ratings;
public class TraktShowRated : TraktRated
namespace Trakt.Api.DataContracts.Sync.Ratings
{
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// The trakt.tv sync show rated class.
/// </summary>
public class TraktShowRated : TraktRated
{
/// <summary>
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public int? Year { get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
[JsonPropertyName("year")]
public int? Year { get; set; }
[JsonPropertyName("ids")]
public TraktShowId Ids { get; set; }
/// <summary>
/// Gets or sets the ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktShowId Ids { get; set; }
[JsonPropertyName("seasons")]
public List<TraktSeasonRated> Seasons { get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public List<TraktSeasonRated> Seasons { get; set; }
}
}

View File

@ -1,19 +1,35 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSync<TMovie, TShow, TEpisode>
namespace Trakt.Api.DataContracts.Sync
{
[JsonPropertyName("movies")]
public List<TMovie> Movies { get; set; }
/// <summary>
/// The trakt.tv sync class.
/// </summary>
/// <typeparam name="TMovie">The type of the movie.</typeparam>
/// <typeparam name="TShow">The type of the show.</typeparam>
/// <typeparam name="TEpisode">The type of the episode.</typeparam>
public class TraktSync<TMovie, TShow, TEpisode>
{
/// <summary>
/// Gets or sets the movies.
/// </summary>
[JsonPropertyName("movies")]
public List<TMovie> Movies { get; set; }
[JsonPropertyName("shows")]
public List<TShow> Shows { get; set; }
/// <summary>
/// Gets or sets the shows.
/// </summary>
[JsonPropertyName("shows")]
public List<TShow> Shows { get; set; }
[JsonPropertyName("episodes")]
public List<TEpisode> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<TEpisode> Episodes { get; set; }
}
}

View File

@ -1,7 +1,11 @@
using Trakt.Api.DataContracts.Sync.Collection;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncCollected : TraktSync<TraktMovieCollected, TraktShowCollected, TraktEpisodeCollected>
namespace Trakt.Api.DataContracts.Sync
{
/// <summary>
/// The trakt.tv sync collected class.
/// </summary>
public class TraktSyncCollected : TraktSync<TraktMovieCollected, TraktShowCollected, TraktEpisodeCollected>
{
}
}

View File

@ -1,7 +1,11 @@
using Trakt.Api.DataContracts.Sync.Ratings;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncRated : TraktSync<TraktMovieRated, TraktShowRated, TraktEpisodeRated>
namespace Trakt.Api.DataContracts.Sync
{
/// <summary>
/// The trakt.tv sync rated class.
/// </summary>
public class TraktSyncRated : TraktSync<TraktMovieRated, TraktShowRated, TraktEpisodeRated>
{
}
}

View File

@ -1,18 +1,34 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncResponse
namespace Trakt.Api.DataContracts.Sync
{
[JsonPropertyName("added")]
public Items Added { get; set; }
/// <summary>
/// The trakt.tv sync response class.
/// </summary>
public class TraktSyncResponse
{
/// <summary>
/// Gets or sets the added items.
/// </summary>
[JsonPropertyName("added")]
public Items Added { get; set; }
[JsonPropertyName("deleted")]
public Items Deleted { get; set; }
/// <summary>
/// Gets or sets the deleted items.
/// </summary>
[JsonPropertyName("deleted")]
public Items Deleted { get; set; }
[JsonPropertyName("existing")]
public Items Existing { get; set; }
/// <summary>
/// Gets or sets the existing items.
/// </summary>
[JsonPropertyName("existing")]
public Items Existing { get; set; }
[JsonPropertyName("not_found")]
public NotFoundObjects NotFound { get; set; }
/// <summary>
/// Gets or sets the not found items.
/// </summary>
[JsonPropertyName("not_found")]
public NotFoundObjects NotFound { get; set; }
}
}

View File

@ -1,7 +1,11 @@
using Trakt.Api.DataContracts.Sync.Watched;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncWatched : TraktSync<TraktMovieWatched, TraktShowWatched, TraktEpisodeWatched>
namespace Trakt.Api.DataContracts.Sync
{
/// <summary>
/// The trakt.tv sync watched class.
/// </summary>
public class TraktSyncWatched : TraktSync<TraktMovieWatched, TraktShowWatched, TraktEpisodeWatched>
{
}
}

View File

@ -1,10 +1,17 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Watched;
public class TraktEpisodeWatched : TraktEpisode
namespace Trakt.Api.DataContracts.Sync.Watched
{
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
/// <summary>
/// The trakt.tv sync episode watched class.
/// </summary>
public class TraktEpisodeWatched : TraktEpisode
{
/// <summary>
/// Gets or sets the watched date.
/// </summary>
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
}
}

View File

@ -1,10 +1,17 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Watched;
public class TraktMovieWatched : TraktMovie
namespace Trakt.Api.DataContracts.Sync.Watched
{
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
/// <summary>
/// The trakt.tv sync movie watched class.
/// </summary>
public class TraktMovieWatched : TraktMovie
{
/// <summary>
/// Gets or sets the watched date.
/// </summary>
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
}
}

View File

@ -1,17 +1,27 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Watched;
public class TraktSeasonWatched : TraktSeason
namespace Trakt.Api.DataContracts.Sync.Watched
{
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
/// <summary>
/// The trakt.tv sync season watched class.
/// </summary>
public class TraktSeasonWatched : TraktSeason
{
/// <summary>
/// Gets or sets the watched date.
/// </summary>
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisodeWatched> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<TraktEpisodeWatched> Episodes { get; set; }
}
}

View File

@ -1,17 +1,27 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Sync.Watched;
public class TraktShowWatched : TraktShow
namespace Trakt.Api.DataContracts.Sync.Watched
{
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
/// <summary>
/// The trakt.tv sync show watched class.
/// </summary>
public class TraktShowWatched : TraktShow
{
/// <summary>
/// Gets or sets the watched date.
/// </summary>
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
[JsonPropertyName("seasons")]
public List<TraktSeasonWatched> Seasons { get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public List<TraktSeasonWatched> Seasons { get; set; }
}
}

View File

@ -1,21 +1,40 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts;
public class TraktDeviceCode
namespace Trakt.Api.DataContracts
{
[JsonPropertyName("device_code")]
public string DeviceCode { get; set; }
/// <summary>
/// The trakt.tv device code class.
/// </summary>
public class TraktDeviceCode
{
/// <summary>
/// Gets or sets the device code.
/// </summary>
[JsonPropertyName("device_code")]
public string DeviceCode { get; set; }
[JsonPropertyName("user_code")]
public string UserCode { get; set; }
/// <summary>
/// Gets or sets the user code.
/// </summary>
[JsonPropertyName("user_code")]
public string UserCode { get; set; }
[JsonPropertyName("verification_url")]
public string VerificationUrl { get; set; }
/// <summary>
/// Gets or sets the verification URL.
/// </summary>
[JsonPropertyName("verification_url")]
public string VerificationUrl { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
/// <summary>
/// Gets or sets the expiration.
/// </summary>
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("interval")]
public int Interval { get; set; }
/// <summary>
/// Gets or sets the interval.
/// </summary>
[JsonPropertyName("interval")]
public int Interval { get; set; }
}
}

View File

@ -1,29 +1,54 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts;
public class TraktUserAccessToken
namespace Trakt.Api.DataContracts
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// The trakt.tv user access token class.
/// </summary>
public class TraktUserAccessToken
{
/// <summary>
/// Gets or sets the access token.
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
/// <summary>
/// Gets or sets the token type.
/// </summary>
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
/// <summary>
/// Gets or sets the expiration.
/// </summary>
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// Gets or sets the refresh token.
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
/// <summary>
/// Gets or sets the scope.
/// </summary>
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("created_at")]
public int CreatedAt { get; set; }
/// <summary>
/// Gets or sets the creation date.
/// </summary>
[JsonPropertyName("created_at")]
public int CreatedAt { get; set; }
// Expiration can be a bit of a problem with Trakt. It's usually 90 days, but it's unclear when the
// refresh_token expires, so leave a little buffer for expiration...
[JsonPropertyName("expirationWithBuffer")]
public int ExpirationWithBuffer => ExpiresIn * 3 / 4;
/// <summary>
/// Gets the expiration time.
/// </summary>
// Expiration can be a bit of a problem with Trakt. It's usually 90 days, but it's unclear when the
// refresh_token expires, so leave a little buffer for expiration...
[JsonPropertyName("expirationWithBuffer")]
public int ExpirationWithBuffer => ExpiresIn * 3 / 4;
}
}

View File

@ -1,21 +1,40 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts;
public class TraktUserRefreshTokenRequest
namespace Trakt.Api.DataContracts
{
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// The trakt.tv user refresh token class.
/// </summary>
public class TraktUserRefreshTokenRequest
{
/// <summary>
/// Gets or sets the refresh token.
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
/// <summary>
/// Gets or sets the client id.
/// </summary>
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
[JsonPropertyName("client_secret")]
public string ClientSecret { get; set; }
/// <summary>
/// Gets or sets the client secret.
/// </summary>
[JsonPropertyName("client_secret")]
public string ClientSecret { get; set; }
[JsonPropertyName("redirect_uri")]
public string RedirectUri { get; set; }
/// <summary>
/// Gets or sets the redirect URI.
/// </summary>
[JsonPropertyName("redirect_uri")]
public string RedirectUri { get; set; }
[JsonPropertyName("grant_type")]
public string GrantType { get; set; }
/// <summary>
/// Gets or sets the grant type.
/// </summary>
[JsonPropertyName("grant_type")]
public string GrantType { get; set; }
}
}

View File

@ -1,15 +1,28 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Users.Collection;
public class TraktEpisodeCollected
namespace Trakt.Api.DataContracts.Users.Collection
{
[JsonPropertyName("number")]
public int Number { get; set; }
/// <summary>
/// The trakt.tv users episode collected class.
/// </summary>
public class TraktEpisodeCollected
{
/// <summary>
/// Gets or sets the episode number.
/// </summary>
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
/// <summary>
/// Gets or sets the collection date.
/// </summary>
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
[JsonPropertyName("metadata")]
public TraktMetadata Metadata { get; set; }
/// <summary>
/// Gets or sets the metadata.
/// </summary>
[JsonPropertyName("metadata")]
public TraktMetadata Metadata { get; set; }
}
}

View File

@ -1,20 +1,36 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Users.Collection;
public class TraktMetadata
namespace Trakt.Api.DataContracts.Users.Collection
{
[JsonPropertyName("media_type")]
public string MediaType { get; set; }
/// <summary>
/// The trakt.tv users metadata class.
/// </summary>
public class TraktMetadata
{
/// <summary>
/// Gets or sets the media type.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; set; }
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
/// <summary>
/// Gets or sets the resolution.
/// </summary>
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
[JsonPropertyName("audio")]
public string Audio { get; set; }
/// <summary>
/// Gets or sets the audio.
/// </summary>
[JsonPropertyName("audio")]
public string Audio { get; set; }
[JsonPropertyName("audio_channels")]
public string AudioChannels { get; set; }
/// <summary>
/// Gets or sets the amount of audio channels.
/// </summary>
[JsonPropertyName("audio_channels")]
public string AudioChannels { get; set; }
// public bool 3d { get; set; }
// public bool 3d { get; set; }
}
}

View File

@ -1,16 +1,29 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Collection;
public class TraktMovieCollected
namespace Trakt.Api.DataContracts.Users.Collection
{
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
/// <summary>
/// The trakt.tv users movie collected class.
/// </summary>
public class TraktMovieCollected
{
/// <summary>
/// Gets or sets the last collection date.
/// </summary>
[JsonPropertyName("collected_at")]
public string CollectedAt { get; set; }
[JsonPropertyName("metadata")]
public TraktMetadata Metadata { get; set; }
/// <summary>
/// Gets or sets the metadata.
/// </summary>
[JsonPropertyName("metadata")]
public TraktMetadata Metadata { get; set; }
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
/// <summary>
/// Gets or sets the movie.
/// </summary>
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}
}

View File

@ -1,16 +1,26 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Users.Collection;
public class TraktSeasonCollected
namespace Trakt.Api.DataContracts.Users.Collection
{
[JsonPropertyName("number")]
public int Number { get; set; }
/// <summary>
/// The trakt.tv users season collected class.
/// </summary>
public class TraktSeasonCollected
{
/// <summary>
/// Gets or sets the season unumber.
/// </summary>
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisodeCollected> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<TraktEpisodeCollected> Episodes { get; set; }
}
}

View File

@ -1,20 +1,33 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Collection;
public class TraktShowCollected
namespace Trakt.Api.DataContracts.Users.Collection
{
[JsonPropertyName("last_collected_at")]
public string LastCollectedAt { get; set; }
/// <summary>
/// The trakt.tv users show collected class.
/// </summary>
public class TraktShowCollected
{
/// <summary>
/// Gets or sets the last collected date.
/// </summary>
[JsonPropertyName("last_collected_at")]
public string LastCollectedAt { get; set; }
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
/// <summary>
/// Gets or sets the show.
/// </summary>
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
[JsonPropertyName("seasons")]
public List<TraktSeasonCollected> Seasons { get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public List<TraktSeasonCollected> Seasons { get; set; }
}
}

View File

@ -1,10 +1,17 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Ratings;
public class TraktEpisodeRated : TraktRated
namespace Trakt.Api.DataContracts.Users.Ratings
{
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
/// <summary>
/// The trakt.tv users rating class.
/// </summary>
public class TraktEpisodeRated : TraktRated
{
/// <summary>
/// Gets or sets the episode.
/// </summary>
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
}
}

View File

@ -1,10 +1,17 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Ratings;
public class TraktMovieRated : TraktRated
namespace Trakt.Api.DataContracts.Users.Ratings
{
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
/// <summary>
/// The trakt.tv users movie rated class.
/// </summary>
public class TraktMovieRated : TraktRated
{
/// <summary>
/// Gets or sets the movie.
/// </summary>
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}
}

View File

@ -1,10 +1,17 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Ratings;
public class TraktSeasonRated : TraktRated
namespace Trakt.Api.DataContracts.Users.Ratings
{
[JsonPropertyName("season")]
public TraktSeason Season { get; set; }
/// <summary>
/// The trakt.tv users season rated class.
/// </summary>
public class TraktSeasonRated : TraktRated
{
/// <summary>
/// Gets or sets the season.
/// </summary>
[JsonPropertyName("season")]
public TraktSeason Season { get; set; }
}
}

View File

@ -1,10 +1,17 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Ratings;
public class TraktShowRated : TraktRated
namespace Trakt.Api.DataContracts.Users.Ratings
{
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
/// <summary>
/// The trakt.tv users show rated class.
/// </summary>
public class TraktShowRated : TraktRated
{
/// <summary>
/// Gets or sets the show.
/// </summary>
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
}
}

View File

@ -1,15 +1,28 @@
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Users.Watched;
public class Episode
namespace Trakt.Api.DataContracts.Users.Watched
{
[JsonPropertyName("last_watched_at")]
public string LastWatchedAt { get; set; }
/// <summary>
/// The trakt.tv users episode watched class.
/// </summary>
public class Episode
{
/// <summary>
/// Gets or sets the last watched date.
/// </summary>
[JsonPropertyName("last_watched_at")]
public string LastWatchedAt { get; set; }
[JsonPropertyName("number")]
public int Number { get; set; }
/// <summary>
/// Gets or sets the episode number.
/// </summary>
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("plays")]
public int Plays { get; set; }
/// <summary>
/// Gets or sets the amount of plays.
/// </summary>
[JsonPropertyName("plays")]
public int Plays { get; set; }
}
}

View File

@ -1,16 +1,26 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Trakt.Api.DataContracts.Users.Watched;
public class Season
namespace Trakt.Api.DataContracts.Users.Watched
{
[JsonPropertyName("number")]
public int Number { get; set; }
/// <summary>
/// The trakt.tv users season watched class.
/// </summary>
public class Season
{
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("episodes")]
public List<Episode> Episodes { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<Episode> Episodes { get; set; }
}
}

View File

@ -1,16 +1,29 @@
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Watched;
public class TraktMovieWatched
namespace Trakt.Api.DataContracts.Users.Watched
{
[JsonPropertyName("plays")]
public int Plays { get; set; }
/// <summary>
/// The trakt.tv users movie watched class.
/// </summary>
public class TraktMovieWatched
{
/// <summary>
/// Gets or sets the amount of plays.
/// </summary>
[JsonPropertyName("plays")]
public int Plays { get; set; }
[JsonPropertyName("last_watched_at")]
public string LastWatchedAt { get; set; }
/// <summary>
/// Gets or sets the last watched date.
/// </summary>
[JsonPropertyName("last_watched_at")]
public string LastWatchedAt { get; set; }
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
/// <summary>
/// Gets or sets the movie.
/// </summary>
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}
}

View File

@ -1,26 +1,45 @@
#pragma warning disable CA2227
#pragma warning disable CA1002
#pragma warning disable CA1002
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Trakt.Api.DataContracts.BaseModel;
namespace Trakt.Api.DataContracts.Users.Watched;
public class TraktShowWatched
namespace Trakt.Api.DataContracts.Users.Watched
{
[JsonPropertyName("plays")]
public int Plays { get; set; }
/// <summary>
/// The trakt.tv users show watched class.
/// </summary>
public class TraktShowWatched
{
/// <summary>
/// Gets or sets the amount of plays.
/// </summary>
[JsonPropertyName("plays")]
public int Plays { get; set; }
[JsonPropertyName("reset_at")]
public string ResetAt { get; set; }
/// <summary>
/// Gets or sets the reset date.
/// </summary>
[JsonPropertyName("reset_at")]
public string ResetAt { get; set; }
[JsonPropertyName("last_watched_at")]
public string LastWatchedAt { get; set; }
/// <summary>
/// Gets or sets the last watched date.
/// </summary>
[JsonPropertyName("last_watched_at")]
public string LastWatchedAt { get; set; }
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
/// <summary>
/// Gets or sets the show.
/// </summary>
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
[JsonPropertyName("seasons")]
public List<Season> Seasons { get; set; }
/// <summary>
/// Gets or sets the seasons.
/// </summary>
[JsonPropertyName("seasons")]
public List<Season> Seasons { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,145 +13,146 @@ using Trakt.Api.DataContracts.BaseModel;
using Trakt.Api.DataContracts.Sync;
using Trakt.Helpers;
namespace Trakt.Api;
/// <summary>
/// The Trakt.tv controller.
/// </summary>
[ApiController]
[Route("[controller]")]
[Produces(MediaTypeNames.Application.Json)]
public class TraktController : ControllerBase
namespace Trakt.Api
{
private readonly TraktApi _traktApi;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<TraktController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TraktController"/> class.
/// The trakt.tv controller class.
/// </summary>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
public TraktController(
IUserDataManager userDataManager,
ILoggerFactory loggerFactory,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
[ApiController]
[Route("[controller]")]
[Produces(MediaTypeNames.Application.Json)]
public class TraktController : ControllerBase
{
_logger = loggerFactory.CreateLogger<TraktController>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
_libraryManager = libraryManager;
}
private readonly TraktApi _traktApi;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<TraktController> _logger;
/// <summary>
/// Authorize this server with trakt.
/// </summary>
/// <param name="userId">The user id of the user connecting to trakt.</param>
/// <response code="200">Authorization code requested successfully.</response>
/// <returns>The trakt authorization code.</returns>
[HttpPost("Users/{userId}/Authorize")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> TraktDeviceAuthorization([FromRoute] string userId)
{
_logger.LogInformation("TraktDeviceAuthorization request received");
// Create a user if we don't have one yet - TODO there should be an endpoint for this that creates a default user
var traktUser = UserHelper.GetTraktUser(userId);
if (traktUser == null)
/// <summary>
/// Initializes a new instance of the <see cref="TraktController"/> class.
/// </summary>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
public TraktController(
IUserDataManager userDataManager,
ILoggerFactory loggerFactory,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
Plugin.Instance.PluginConfiguration.AddUser(userId);
traktUser = UserHelper.GetTraktUser(userId);
Plugin.Instance.SaveConfiguration();
_logger = loggerFactory.CreateLogger<TraktController>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
_libraryManager = libraryManager;
}
string userCode = await _traktApi.AuthorizeDevice(traktUser).ConfigureAwait(false);
return new
/// <summary>
/// Authorize this server with trakt.tv.
/// </summary>
/// <param name="userId">The user id of the user connecting to trakt.tv.</param>
/// <response code="200">Authorization code requested successfully.</response>
/// <returns>The trakt.tv authorization code.</returns>
[HttpPost("Users/{userId}/Authorize")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> TraktDeviceAuthorization([FromRoute] string userId)
{
userCode
};
}
_logger.LogInformation("TraktDeviceAuthorization request received");
/// <summary>
/// Poll the trakt device authorization status
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Polling successful.</response>
/// <returns>A value indicating whether the authorization code was connected to a trakt account.</returns>
[HttpGet("Users/{userId}/PollAuthorizationStatus")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<object> TraktPollAuthorizationStatus([FromRoute] string userId)
{
_logger.LogInformation("TraktPollAuthorizationStatus request received");
var traktUser = UserHelper.GetTraktUser(userId);
bool isAuthorized = traktUser.AccessToken != null && traktUser.RefreshToken != null;
// Create a user if we don't have one yet - TODO there should be an endpoint for this that creates a default user
var traktUser = UserHelper.GetTraktUser(userId);
if (traktUser == null)
{
Plugin.Instance.PluginConfiguration.AddUser(userId);
traktUser = UserHelper.GetTraktUser(userId);
Plugin.Instance.SaveConfiguration();
}
if (Plugin.Instance.PollingTasks.TryGetValue(userId, out var task))
{
isAuthorized = task.Result;
Plugin.Instance.PollingTasks.Remove(userId);
string userCode = await _traktApi.AuthorizeDevice(traktUser).ConfigureAwait(false);
return new
{
userCode
};
}
return new
/// <summary>
/// Poll the trakt.tv device authorization status.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Polling successful.</response>
/// <returns>A value indicating whether the authorization code was connected to a trakt.tv account.</returns>
[HttpGet("Users/{userId}/PollAuthorizationStatus")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<object> TraktPollAuthorizationStatus([FromRoute] string userId)
{
isAuthorized
};
}
_logger.LogInformation("TraktPollAuthorizationStatus request received");
var traktUser = UserHelper.GetTraktUser(userId);
bool isAuthorized = traktUser.AccessToken != null && traktUser.RefreshToken != null;
/// <summary>
/// Rate an item.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="rating">Rating between 1 - 10 (0 = unrate).</param>
/// <response code="200">Item rated successfully.</response>
/// <returns>A <see cref="TraktSyncResponse"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/Rate")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TraktSyncResponse>> TraktRateItem([FromRoute] string userId, [FromRoute] Guid itemId, [FromQuery] int rating)
{
_logger.LogInformation("RateItem request received");
if (Plugin.Instance.PollingTasks.TryGetValue(userId, out var task))
{
isAuthorized = task.Result;
Plugin.Instance.PollingTasks.Remove(userId);
}
var currentItem = _libraryManager.GetItemById(itemId);
if (currentItem == null)
{
_logger.LogInformation("currentItem is null");
return null;
return new
{
isAuthorized
};
}
return await _traktApi.SendItemRating(currentItem, rating, UserHelper.GetTraktUser(userId)).ConfigureAwait(false);
}
/// <summary>
/// Rate an item.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="rating">Rating between 1 - 10 (0 = unrate).</param>
/// <response code="200">Item rated successfully.</response>
/// <returns>A <see cref="TraktSyncResponse"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/Rate")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TraktSyncResponse>> TraktRateItem([FromRoute] string userId, [FromRoute] Guid itemId, [FromQuery] int rating)
{
_logger.LogInformation("RateItem request received");
/// <summary>
/// Get recommended trakt movies.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Recommended movies returned.</response>
/// <returns>A <see cref="List{TraktMovie}"/> with recommended movies.</returns>
[HttpPost("Users/{userId}/RecommendedMovies")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<TraktMovie>>> RecommendedTraktMovies([FromRoute] string userId)
{
return await _traktApi.SendMovieRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false);
}
var currentItem = _libraryManager.GetItemById(itemId);
/// <summary>
/// Get recommended trakt shows.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Recommended shows returned.</response>
/// <returns>A <see cref="List{TraktShow}"/> with recommended movies.</returns>
[HttpPost("Users/{userId}/RecommendedShows")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<TraktShow>>> RecommendedTraktShows([FromRoute] string userId)
{
return await _traktApi.SendShowRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false);
if (currentItem == null)
{
_logger.LogInformation("currentItem is null");
return null;
}
return await _traktApi.SendItemRating(currentItem, rating, UserHelper.GetTraktUser(userId)).ConfigureAwait(false);
}
/// <summary>
/// Get recommended trakt.tv movies.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Recommended movies returned.</response>
/// <returns>A <see cref="List{TraktMovie}"/> with recommended movies.</returns>
[HttpPost("Users/{userId}/RecommendedMovies")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<TraktMovie>>> RecommendedTraktMovies([FromRoute] string userId)
{
return await _traktApi.SendMovieRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false);
}
/// <summary>
/// Get recommended trakt.tv shows.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Recommended shows returned.</response>
/// <returns>A <see cref="List{TraktShow}"/> with recommended movies.</returns>
[HttpPost("Users/{userId}/RecommendedShows")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<TraktShow>>> RecommendedTraktShows([FromRoute] string userId)
{
return await _traktApi.SendShowRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false);
}
}
}

View File

@ -1,35 +1,118 @@
namespace Trakt.Api;
public static class TraktUris
namespace Trakt.Api
{
public const string BaseUrl = "https://api.trakt.tv";
public const string ClientId = "58f2251f1c9e7275e94fef723a8604e6848bbf86a0d97dda82382a6c3231608c";
public const string ClientSecret = "bf9fce37cf45c1de91da009e7ac6fca905a35d7a718bf65a52f92199073a2503";
/// <summary>
/// The trakt.tv URI class.
/// </summary>
public static class TraktUris
{
/// <summary>
/// The base URL.
/// </summary>
public const string BaseUrl = "https://api.trakt.tv";
public const string DeviceCode = BaseUrl + "/oauth/device/code";
public const string DeviceToken = BaseUrl + "/oauth/device/token";
public const string AccessToken = BaseUrl + "/oauth/token";
/// <summary>
/// The client id.
/// </summary>
public const string ClientId = "58f2251f1c9e7275e94fef723a8604e6848bbf86a0d97dda82382a6c3231608c";
public const string SyncCollectionAdd = BaseUrl + "/sync/collection";
public const string SyncCollectionRemove = BaseUrl + "/sync/collection/remove";
public const string SyncWatchedHistoryAdd = BaseUrl + "/sync/history";
public const string SyncWatchedHistoryRemove = BaseUrl + "/sync/history/remove";
public const string SyncRatingsAdd = BaseUrl + "/sync/ratings";
/// <summary>
/// The client secret.
/// </summary>
public const string ClientSecret = "bf9fce37cf45c1de91da009e7ac6fca905a35d7a718bf65a52f92199073a2503";
public const string ScrobbleStart = BaseUrl + "/scrobble/start";
public const string ScrobblePause = BaseUrl + "/scrobble/pause";
public const string ScrobbleStop = BaseUrl + "/scrobble/stop";
/// <summary>
/// The device code URI.
/// </summary>
public const string DeviceCode = BaseUrl + "/oauth/device/code";
public const string WatchedMovies = BaseUrl + "/sync/watched/movies";
public const string WatchedShows = BaseUrl + "/sync/watched/shows";
public const string CollectedMovies = BaseUrl + "/sync/collection/movies?extended=metadata";
public const string CollectedShows = BaseUrl + "/sync/collection/shows?extended=metadata";
/// <summary>
/// The device token URI.
/// </summary>
public const string DeviceToken = BaseUrl + "/oauth/device/token";
// Recommendations
public const string RecommendationsMovies = BaseUrl + "/recommendations/movies";
public const string RecommendationsShows = BaseUrl + "/recommendations/shows";
/// <summary>
/// The access token URI.
/// </summary>
public const string AccessToken = BaseUrl + "/oauth/token";
// Recommendations
public const string RecommendationsMoviesDismiss = BaseUrl + "/recommendations/movies/{0}";
public const string RecommendationsShowsDismiss = BaseUrl + "/recommendations/shows/{0}";
/// <summary>
/// The collection sync add URI.
/// </summary>
public const string SyncCollectionAdd = BaseUrl + "/sync/collection";
/// <summary>
/// The collection sync remove URI.
/// </summary>
public const string SyncCollectionRemove = BaseUrl + "/sync/collection/remove";
/// <summary>
/// The watched history add URI.
/// </summary>
public const string SyncWatchedHistoryAdd = BaseUrl + "/sync/history";
/// <summary>
/// The watched history remove URI.
/// </summary>
public const string SyncWatchedHistoryRemove = BaseUrl + "/sync/history/remove";
/// <summary>
/// The ratings add URI.
/// </summary>
public const string SyncRatingsAdd = BaseUrl + "/sync/ratings";
/// <summary>
/// The scrobble start URI.
/// </summary>
public const string ScrobbleStart = BaseUrl + "/scrobble/start";
/// <summary>
/// The scrobble pause URI.
/// </summary>
public const string ScrobblePause = BaseUrl + "/scrobble/pause";
/// <summary>
/// The scrobble stop URI.
/// </summary>
public const string ScrobbleStop = BaseUrl + "/scrobble/stop";
/// <summary>
/// The watched movies URI.
/// </summary>
public const string WatchedMovies = BaseUrl + "/sync/watched/movies";
/// <summary>
/// The watched shows URI.
/// </summary>
public const string WatchedShows = BaseUrl + "/sync/watched/shows";
/// <summary>
/// The collected movies URI.
/// </summary>
public const string CollectedMovies = BaseUrl + "/sync/collection/movies?extended=metadata";
/// <summary>
/// The collected series URI.
/// </summary>
public const string CollectedShows = BaseUrl + "/sync/collection/shows?extended=metadata";
/// <summary>
/// The movies recommendations URI.
/// </summary>
public const string RecommendationsMovies = BaseUrl + "/recommendations/movies";
/// <summary>
/// The shows recommendations URI.
/// </summary>
public const string RecommendationsShows = BaseUrl + "/recommendations/shows";
/// <summary>
/// The movies recommendations dismiss URI.
/// </summary>
public const string RecommendationsMoviesDismiss = BaseUrl + "/recommendations/movies/{0}";
/// <summary>
/// The shows recommendations dismiss URI.
/// </summary>
public const string RecommendationsShowsDismiss = BaseUrl + "/recommendations/shows/{0}";
}
}

View File

@ -5,25 +5,39 @@ using System.Linq;
using MediaBrowser.Model.Plugins;
using Trakt.Model;
namespace Trakt.Configuration;
public class PluginConfiguration : BasePluginConfiguration
namespace Trakt.Configuration
{
public PluginConfiguration()
/// <summary>
/// Plugin configuration class for trackt.tv plugin.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
TraktUsers = Array.Empty<TraktUser>();
}
public TraktUser[] TraktUsers { get; set; }
public void AddUser(string userId)
{
var traktUsers = TraktUsers.ToList();
var traktUser = new TraktUser
/// <summary>
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
/// </summary>
public PluginConfiguration()
{
LinkedMbUserId = userId
};
traktUsers.Add(traktUser);
TraktUsers = traktUsers.ToArray();
TraktUsers = Array.Empty<TraktUser>();
}
/// <summary>
/// Gets or sets the trakt users.
/// </summary>
public TraktUser[] TraktUsers { get; set; }
/// <summary>
/// Adds a user to the trakt user.
/// </summary>
/// <param name="userId">The user id.</param>
public void AddUser(string userId)
{
var traktUsers = TraktUsers.ToList();
var traktUser = new TraktUser
{
LinkedMbUserId = userId
};
traktUsers.Add(traktUser);
TraktUsers = traktUsers.ToArray();
}
}
}

View File

@ -7,193 +7,239 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.Entities;
using Trakt.Api.DataContracts.Users.Collection;
using Trakt.Helpers;
using Trakt.Model;
namespace Trakt;
public static class Extensions
namespace Trakt
{
public static int? ConvertToInt(this string input)
/// <summary>
/// Class for trakt.tv plugin extension functions.
/// </summary>
public static class Extensions
{
if (int.TryParse(input, out int result))
/// <summary>
/// Convert string to int.
/// </summary>
/// <param name="input">String to convert to int.</param>
/// <returns>int?.</returns>
public static int? ConvertToInt(this string input)
{
return result;
if (int.TryParse(input, out int result))
{
return result;
}
return null;
}
return null;
}
/// <summary>
/// Checks if <see cref="TraktMetadata"/> is empty.
/// </summary>
/// <param name="metadata">String to convert to int.</param>
/// <returns><see cref="bool"/> indicating if the provided <see cref="TraktMetadata"/> is empty.</returns>
public static bool IsEmpty(this TraktMetadata metadata)
=> string.IsNullOrEmpty(metadata.MediaType)
&& string.IsNullOrEmpty(metadata.Resolution)
&& string.IsNullOrEmpty(metadata.Audio)
&& string.IsNullOrEmpty(metadata.AudioChannels);
public static bool IsEmpty(this TraktMetadata metadata)
=> string.IsNullOrEmpty(metadata.MediaType)
&& string.IsNullOrEmpty(metadata.Resolution)
&& string.IsNullOrEmpty(metadata.Audio)
&& string.IsNullOrEmpty(metadata.AudioChannels);
public static string GetCodecRepresetation(this MediaStream audioStream)
{
var audio = audioStream != null && !string.IsNullOrEmpty(audioStream.Codec)
? audioStream.Codec.ToLowerInvariant().Replace(' ', '_')
: null;
switch (audio)
/// <summary>
/// Gets the trakt.tv codec representation of a <see cref="MediaStream"/>.
/// </summary>
/// <param name="audioStream">The <see cref="MediaStream"/>.</param>
/// <returns>string.</returns>
public static string GetCodecRepresetation(this MediaStream audioStream)
{
case "truehd":
return TraktAudio.dolby_truehd.ToString();
case "dts":
case "dca":
return TraktAudio.dts.ToString();
case "dtshd":
return TraktAudio.dts_ma.ToString();
case "ac3":
return TraktAudio.dolby_digital.ToString();
case "aac":
return TraktAudio.aac.ToString();
case "mp2":
return TraktAudio.mp3.ToString();
case "pcm":
return TraktAudio.lpcm.ToString();
case "ogg":
return TraktAudio.ogg.ToString();
case "wma":
return TraktAudio.wma.ToString();
case "flac":
return TraktAudio.flac.ToString();
default:
var audio = audioStream != null && !string.IsNullOrEmpty(audioStream.Codec)
? audioStream.Codec.ToLowerInvariant().Replace(' ', '_')
: null;
switch (audio)
{
case "truehd":
return TraktAudio.DolbyDigital.ToString();
case "dts":
case "dca":
return TraktAudio.Dts.ToString();
case "dtshd":
return TraktAudio.DtsMa.ToString();
case "ac3":
return TraktAudio.DolbyDigital.ToString();
case "aac":
return TraktAudio.Aac.ToString();
case "mp2":
return TraktAudio.Mp3.ToString();
case "pcm":
return TraktAudio.Lpcm.ToString();
case "ogg":
return TraktAudio.Ogg.ToString();
case "wma":
return TraktAudio.Wma.ToString();
case "flac":
return TraktAudio.Flac.ToString();
default:
return null;
}
}
/// <summary>
/// Checks if metadata of new collected movie is different from the already collected.
/// </summary>
/// <param name="collectedMovie">The <see cref="TraktMovieCollected"/>.</param>
/// <param name="movie">The <see cref="Movie"/>.</param>
/// <returns><see cref="bool"/> indicating if the new movie has different metadata to the already collected.</returns>
public static bool MetadataIsDifferent(this TraktMovieCollected collectedMovie, Movie movie)
{
var audioStream = movie.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio);
var resolution = movie.GetDefaultVideoStream().GetResolution();
var audio = GetCodecRepresetation(audioStream);
var audioChannels = audioStream.GetAudioChannels();
if (collectedMovie.Metadata == null || collectedMovie.Metadata.IsEmpty())
{
return !string.IsNullOrEmpty(resolution)
|| !string.IsNullOrEmpty(audio)
|| !string.IsNullOrEmpty(audioChannels);
}
return collectedMovie.Metadata.Audio != audio
|| collectedMovie.Metadata.AudioChannels != audioChannels
|| collectedMovie.Metadata.Resolution != resolution;
}
/// <summary>
/// Gets the resolution of a <see cref="MediaStream"/>.
/// </summary>
/// <param name="videoStream">The <see cref="MediaStream"/>.</param>
/// <returns>string.</returns>
public static string GetResolution(this MediaStream videoStream)
{
if (videoStream == null)
{
return null;
}
}
}
public static bool MetadataIsDifferent(this TraktMovieCollected collectedMovie, Movie movie)
{
var audioStream = movie.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio);
if (!videoStream.Width.HasValue)
{
return null;
}
var resolution = movie.GetDefaultVideoStream().GetResolution();
var audio = GetCodecRepresetation(audioStream);
var audioChannels = audioStream.GetAudioChannels();
if (videoStream.Width.Value >= 3800)
{
return "uhd_4k";
}
if (collectedMovie.Metadata == null || collectedMovie.Metadata.IsEmpty())
{
return !string.IsNullOrEmpty(resolution)
|| !string.IsNullOrEmpty(audio)
|| !string.IsNullOrEmpty(audioChannels);
}
if (videoStream.Width.Value >= 1900)
{
return "hd_1080p";
}
return collectedMovie.Metadata.Audio != audio
|| collectedMovie.Metadata.AudioChannels != audioChannels
|| collectedMovie.Metadata.Resolution != resolution;
}
if (videoStream.Width.Value >= 1270)
{
return "hd_720p";
}
if (videoStream.Width.Value >= 700)
{
return "sd_480p";
}
public static string GetResolution(this MediaStream videoStream)
{
if (videoStream == null)
{
return null;
}
if (!videoStream.Width.HasValue)
/// <summary>
/// Gets the ISO-8620 representation of a <see cref="DateTime"/>.
/// </summary>
/// <param name="dt">The <see cref="DateTime"/>.</param>
/// <returns>string.</returns>
public static string ToISO8601(this DateTime dt)
=> dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
/// <summary>
/// Gets the season number of an <see cref="Episode"/>.
/// </summary>
/// <param name="episode">The <see cref="Episode"/>.</param>
/// <returns>int.</returns>
public static int GetSeasonNumber(this Episode episode)
=> (episode.ParentIndexNumber != 0 ? episode.ParentIndexNumber ?? 1 : episode.ParentIndexNumber).Value;
/// <summary>
/// Gets the number of audio channels of a <see cref="MediaStream"/>.
/// </summary>
/// <param name="audioStream">The <see cref="MediaStream"/>.</param>
/// <returns>string.</returns>
public static string GetAudioChannels(this MediaStream audioStream)
{
return null;
if (audioStream == null || string.IsNullOrEmpty(audioStream.ChannelLayout))
{
return null;
}
var channels = audioStream.ChannelLayout.Split('(')[0];
switch (channels)
{
case "7":
return "6.1";
case "6":
return "5.1";
case "5":
return "5.0";
case "4":
return "4.0";
case "3":
return "2.1";
case "stereo":
return "2.0";
case "mono":
return "1.0";
default:
return channels;
}
}
if (videoStream.Width.Value >= 3800)
/// <summary>
/// Transforms an enumerable into a list with a speciifc amount of chunks.
/// </summary>
/// <param name="enumerable">The IEnumberable{T}.</param>
/// <param name="chunkSize">Size of the Chunks.</param>
/// <returns>IList{IEnumerable{T}}.</returns>
/// <typeparam name="T">The type of IEnumerable.</typeparam>
public static IList<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
return "uhd_4k";
var itemsReturned = 0;
var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
var count = list.Count;
var chunks = new List<IEnumerable<T>>();
while (itemsReturned < count)
{
chunks.Add(list.Take(chunkSize).ToList());
list = list.Skip(chunkSize).ToList();
itemsReturned += chunkSize;
}
return chunks;
}
if (videoStream.Width.Value >= 1900)
/// <summary>
/// Splits a progress into multiple parts.
/// </summary>
/// <param name="parent">The progress.</param>
/// <param name="parts">The number of parts to split into.</param>
/// <returns>ISplittableProgress{double}.</returns>
public static ISplittableProgress<double> Split(this IProgress<double> parent, int parts)
{
return "hd_1080p";
var current = parent.ToSplittableProgress();
return current.Split(parts);
}
if (videoStream.Width.Value >= 1270)
/// <summary>
/// Converts a progress into a splittable progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <returns>ISplittableProgress{double}.</returns>
public static ISplittableProgress<double> ToSplittableProgress(this IProgress<double> progress)
{
return "hd_720p";
}
if (videoStream.Width.Value >= 700)
{
return "sd_480p";
}
return null;
}
public static string ToISO8601(this DateTime dt)
=> dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
public static int GetSeasonNumber(this Episode episode)
=> (episode.ParentIndexNumber != 0 ? episode.ParentIndexNumber ?? 1 : episode.ParentIndexNumber).Value;
public static string GetAudioChannels(this MediaStream audioStream)
{
if (audioStream == null || string.IsNullOrEmpty(audioStream.ChannelLayout))
{
return null;
}
var channels = audioStream.ChannelLayout.Split('(')[0];
switch (channels)
{
case "7":
return "6.1";
case "6":
return "5.1";
case "5":
return "5.0";
case "4":
return "4.0";
case "3":
return "2.1";
case "stereo":
return "2.0";
case "mono":
return "1.0";
default:
return channels;
var splittable = new SplittableProgress(progress.Report);
return splittable;
}
}
public static IList<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
var itemsReturned = 0;
var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
var count = list.Count;
var chunks = new List<IEnumerable<T>>();
while (itemsReturned < count)
{
chunks.Add(list.Take(chunkSize).ToList());
list = list.Skip(chunkSize).ToList();
itemsReturned += chunkSize;
}
return chunks;
}
public static ISplittableProgress<double> Split(this IProgress<double> parent, int parts)
{
var current = parent.ToSplittableProgress();
return current.Split(parts);
}
public static ISplittableProgress<double> ToSplittableProgress(this IProgress<double> progress)
{
var splittable = new SplittableProgress(progress.Report);
return splittable;
}
#pragma warning disable
public enum TraktAudio
{
lpcm,
mp3,
aac,
dts,
dts_ma,
flac,
ogg,
wma,
dolby_prologic,
dolby_digital,
dolby_digital_plus,
dolby_truehd
}
#pragma warning restore CA1707
}

View File

@ -1,8 +1,23 @@
namespace Trakt.Helpers;
public enum EventType
namespace Trakt.Helpers
{
Add,
Remove,
Update
/// <summary>
/// Enum EventType.
/// </summary>
public enum EventType
{
/// <summary>
/// The addevent.
/// </summary>
Add,
/// <summary>
/// The remove event.
/// </summary>
Remove,
/// <summary>
/// The update event.
/// </summary>
Update
}
}

View File

@ -1,12 +1,18 @@
namespace Trakt.Helpers;
using System;
using System;
/// <summary>
/// Similar to <see cref="IProgress{T}"/>, but it contains a split method and Report is relative, not absolute.
/// </summary>
/// <typeparam name="T">The type of progress update value</typeparam>
public interface ISplittableProgress<T> : IProgress<T>
namespace Trakt.Helpers
{
ISplittableProgress<T> Split(int parts);
/// <summary>
/// Similar to <see cref="IProgress{T}"/>, but it contains a split method and Report is relative, not absolute.
/// </summary>
/// <typeparam name="T">The type of progress update value.</typeparam>
public interface ISplittableProgress<T> : IProgress<T>
{
/// <summary>
/// Splits the progress into parts.
/// </summary>
/// <param name="parts">The amount of parts to split into.</param>
/// <returns>ISplittableProgress{T}.</returns>
ISplittableProgress<T> Split(int parts);
}
}

View File

@ -1,13 +1,14 @@
using MediaBrowser.Controller.Entities;
using Trakt.Model;
namespace Trakt.Helpers;
internal class LibraryEvent
namespace Trakt.Helpers
{
public BaseItem Item { get; set; }
internal class LibraryEvent
{
public BaseItem Item { get; set; }
public TraktUser TraktUser { get; set; }
public TraktUser TraktUser { get; set; }
public EventType EventType { get; set; }
public EventType EventType { get; set; }
}
}

View File

@ -11,283 +11,286 @@ using Microsoft.Extensions.Logging;
using Trakt.Api;
using Trakt.Model;
namespace Trakt.Helpers;
internal class LibraryManagerEventsHelper : IDisposable
namespace Trakt.Helpers
{
private readonly List<LibraryEvent> _queuedEvents;
private readonly ILogger<LibraryManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private Timer _queueTimer;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public LibraryManagerEventsHelper(ILogger<LibraryManagerEventsHelper> logger, TraktApi traktApi)
internal class LibraryManagerEventsHelper : IDisposable
{
_queuedEvents = new List<LibraryEvent>();
_logger = logger;
_traktApi = traktApi;
}
private readonly List<LibraryEvent> _queuedEvents;
private readonly ILogger<LibraryManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private Timer _queueTimer;
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="eventType"></param>
public void QueueItem(BaseItem item, EventType eventType)
{
if (item == null)
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManagerEventsHelper"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="traktApi">The <see cref="TraktApi"/>.</param>
public LibraryManagerEventsHelper(ILogger<LibraryManagerEventsHelper> logger, TraktApi traktApi)
{
throw new ArgumentNullException(nameof(item));
_queuedEvents = new List<LibraryEvent>();
_logger = logger;
_traktApi = traktApi;
}
if (_queueTimer == null)
/// <summary>
/// Queues an item to be added to trakt.
/// </summary>
/// <param name="item"> The <see cref="BaseItem"/>.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
public void QueueItem(BaseItem item, EventType eventType)
{
_queueTimer = new Timer(
OnQueueTimerCallback,
null,
TimeSpan.FromMilliseconds(20000),
Timeout.InfiniteTimeSpan);
}
else
{
_queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
}
var users = Plugin.Instance.PluginConfiguration.TraktUsers;
if (users == null || users.Length == 0)
{
return;
}
// we need to process the video for each user
foreach (var user in users.Where(x => _traktApi.CanSync(item, x)))
{
// we have a match, this user is watching the folder the video is in. Add to queue and they
// will be processed when the next timer elapsed event fires.
var libraryEvent = new LibraryEvent { Item = item, TraktUser = user, EventType = eventType };
_queuedEvents.Add(libraryEvent);
}
}
/// <summary>
///
/// </summary>
private async void OnQueueTimerCallback(object state)
{
try
{
await OnQueueTimerCallbackInternal().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in OnQueueTimerCallbackInternal");
}
}
private async Task OnQueueTimerCallbackInternal()
{
_logger.LogInformation("Timer elapsed - Processing queued items");
if (!_queuedEvents.Any())
{
_logger.LogInformation("No events... Stopping queue timer");
// This may need to go
return;
}
var queue = _queuedEvents.ToList();
_queuedEvents.Clear();
foreach (var traktUser in Plugin.Instance.PluginConfiguration.TraktUsers)
{
var queuedMovieDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Movie &&
ev.EventType == EventType.Remove).ToList();
if (queuedMovieDeletes.Any())
if (item == null)
{
_logger.LogInformation("{Count} Movie Deletes to Process", queuedMovieDeletes.Count);
await ProcessQueuedMovieEvents(queuedMovieDeletes, traktUser, EventType.Remove).ConfigureAwait(false);
throw new ArgumentNullException(nameof(item));
}
if (_queueTimer == null)
{
_queueTimer = new Timer(
OnQueueTimerCallback,
null,
TimeSpan.FromMilliseconds(20000),
Timeout.InfiniteTimeSpan);
}
else
{
_logger.LogInformation("No Movie Deletes to Process");
_queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan);
}
var queuedMovieAdds = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Movie &&
ev.EventType == EventType.Add).ToList();
var users = Plugin.Instance.PluginConfiguration.TraktUsers;
if (queuedMovieAdds.Any())
if (users == null || users.Length == 0)
{
_logger.LogInformation("{Count} Movie Adds to Process", queuedMovieAdds.Count);
await ProcessQueuedMovieEvents(queuedMovieAdds, traktUser, EventType.Add).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No Movie Adds to Process");
return;
}
var queuedEpisodeDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Episode &&
ev.EventType == EventType.Remove).ToList();
if (queuedEpisodeDeletes.Any())
// Check if item can be synced for all users.
foreach (var user in users.Where(user => _traktApi.CanSync(item, user)))
{
_logger.LogInformation("{Count} Episode Deletes to Process", queuedEpisodeDeletes.Count);
await ProcessQueuedEpisodeEvents(queuedEpisodeDeletes, traktUser, EventType.Remove).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No Episode Deletes to Process");
}
var queuedEpisodeAdds = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Episode &&
ev.EventType == EventType.Add).ToList();
if (queuedEpisodeAdds.Any())
{
_logger.LogInformation("{Count} Episode Adds to Process", queuedEpisodeAdds.Count);
await ProcessQueuedEpisodeEvents(queuedEpisodeAdds, traktUser, EventType.Add).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No Episode Adds to Process");
}
var queuedShowDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Series &&
ev.EventType == EventType.Remove).ToList();
if (queuedShowDeletes.Any())
{
_logger.LogInformation("{Count} Series Deletes to Process", queuedMovieDeletes.Count);
await ProcessQueuedShowEvents(queuedShowDeletes, traktUser, EventType.Remove).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No Series Deletes to Process");
// Add to queue.
// Sync will be processed when the next timer elapsed event fires.
_queuedEvents.Add(new LibraryEvent { Item = item, TraktUser = user, EventType = eventType });
}
}
// Everything is processed. Reset the event list.
_queuedEvents.Clear();
}
private async Task ProcessQueuedShowEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var shows = events.Select(lev => (Series)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Tvdb)))
.ToList();
try
{
// Should probably not be awaiting this, but it's unlikely a user will be deleting more than one or two shows at a time
foreach (var show in shows)
{
await _traktApi.SendLibraryUpdateAsync(show, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued series events");
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedMovieEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var movies = events.Select(lev => (Movie)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Imdb)))
.ToList();
try
{
await _traktApi.SendLibraryUpdateAsync(movies, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued movie events");
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedEpisodeEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var episodes = events.Select(lev => (Episode)lev.Item)
.Where(lev => lev.Series != null && !string.IsNullOrEmpty(lev.Series.Name) && !string.IsNullOrEmpty(lev.Series.GetProviderId(MetadataProvider.Tvdb)))
.OrderBy(i => i.Series.Id)
.ToList();
// Can't progress further without episodes
if (!episodes.Any())
{
_logger.LogInformation("episodes count is 0");
return;
}
var payload = new List<Episode>();
var currentSeriesId = episodes[0].Series.Id;
foreach (var ep in episodes)
{
if (!currentSeriesId.Equals(ep.Series.Id))
{
// We're starting a new series. Time to send the current one to trakt.tv
await _traktApi.SendLibraryUpdateAsync(payload, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
currentSeriesId = ep.Series.Id;
payload.Clear();
}
payload.Add(ep);
}
if (payload.Any())
/// <summary>
/// Wait for timer callback to be completed.
/// </summary>
private async void OnQueueTimerCallback(object state)
{
try
{
await _traktApi.SendLibraryUpdateAsync(payload, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
await OnQueueTimerCallbackInternal().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued episode events");
_logger.LogError(ex, "Error in OnQueueTimerCallbackInternal");
}
}
/// <summary>
/// Wait for timer to be completed.
/// </summary>
private async Task OnQueueTimerCallbackInternal()
{
_logger.LogInformation("Timer elapsed - processing queued items");
if (!_queuedEvents.Any())
{
_logger.LogInformation("No events... stopping queue timer");
// This may need to go
return;
}
var queue = _queuedEvents.ToList();
_queuedEvents.Clear();
foreach (var traktUser in Plugin.Instance.PluginConfiguration.TraktUsers)
{
var queuedMovieDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Movie &&
ev.EventType == EventType.Remove).ToList();
if (queuedMovieDeletes.Any())
{
_logger.LogInformation("{Count} movie deletions to process", queuedMovieDeletes.Count);
await ProcessQueuedMovieEvents(queuedMovieDeletes, traktUser, EventType.Remove).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No movie deletions to process");
}
var queuedMovieAdds = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Movie &&
ev.EventType == EventType.Add).ToList();
if (queuedMovieAdds.Any())
{
_logger.LogInformation("{Count} movie additions to process", queuedMovieAdds.Count);
await ProcessQueuedMovieEvents(queuedMovieAdds, traktUser, EventType.Add).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No movie additions to process");
}
var queuedEpisodeDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Episode &&
ev.EventType == EventType.Remove).ToList();
if (queuedEpisodeDeletes.Any())
{
_logger.LogInformation("{Count} episode deletions to process", queuedEpisodeDeletes.Count);
await ProcessQueuedEpisodeEvents(queuedEpisodeDeletes, traktUser, EventType.Remove).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No episode deletions to process");
}
var queuedEpisodeAdds = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Episode &&
ev.EventType == EventType.Add).ToList();
if (queuedEpisodeAdds.Any())
{
_logger.LogInformation("{Count} episode additions to process", queuedEpisodeAdds.Count);
await ProcessQueuedEpisodeEvents(queuedEpisodeAdds, traktUser, EventType.Add).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No episode additions to process");
}
var queuedShowDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Series &&
ev.EventType == EventType.Remove).ToList();
if (queuedShowDeletes.Any())
{
_logger.LogInformation("{Count} series deletions to process", queuedMovieDeletes.Count);
await ProcessQueuedShowEvents(queuedShowDeletes, traktUser, EventType.Remove).ConfigureAwait(false);
}
else
{
_logger.LogInformation("No series deletions to process");
}
}
// Everything is processed. Reset the event list.
_queuedEvents.Clear();
}
private async Task ProcessQueuedShowEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var shows = events.Select(lev => (Series)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Tvdb)))
.ToList();
try
{
// Should probably not be awaiting this, but it's unlikely a user will be deleting more than one or two shows at a time
foreach (var show in shows)
{
await _traktApi.SendLibraryUpdateAsync(show, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued series events");
}
}
/// <summary>
/// Processes queued movie events.
/// </summary>
/// <param name="events">The <see cref="LibraryEvent"/> enumerable.</param>
/// <param name="traktUser">The <see cref="TraktUser"/>.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <returns>Task.</returns>
private async Task ProcessQueuedMovieEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var movies = events.Select(lev => (Movie)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Imdb)))
.ToList();
try
{
await _traktApi.SendLibraryUpdateAsync(movies, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued movie events");
}
}
/// <summary>
/// Processes queued episode events.
/// </summary>
/// <param name="events">The <see cref="LibraryEvent"/> enumerable.</param>
/// <param name="traktUser">The <see cref="TraktUser"/>.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <returns>Task.</returns>
private async Task ProcessQueuedEpisodeEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var episodes = events.Select(lev => (Episode)lev.Item)
.Where(lev => lev.Series != null && !string.IsNullOrEmpty(lev.Series.Name) && !string.IsNullOrEmpty(lev.Series.GetProviderId(MetadataProvider.Tvdb)))
.OrderBy(i => i.Series.Id)
.ToList();
// Can't progress further without episodes
if (!episodes.Any())
{
_logger.LogDebug("Episodes count is 0");
return;
}
var payload = new List<Episode>();
var currentSeriesId = episodes[0].Series.Id;
foreach (var ep in episodes)
{
if (!currentSeriesId.Equals(ep.Series.Id))
{
// We're starting a new series. Time to send the current one to trakt.tv
await _traktApi.SendLibraryUpdateAsync(payload, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
currentSeriesId = ep.Series.Id;
payload.Clear();
}
payload.Add(ep);
}
if (payload.Any())
{
try
{
await _traktApi.SendLibraryUpdateAsync(payload, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued episode events");
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_queueTimer?.Dispose();
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_queueTimer?.Dispose();
}
}
}

View File

@ -9,10 +9,12 @@
/// The watching state.
/// </summary>
Watching,
/// <summary>
/// The paused state.
/// </summary>
Paused,
/// <summary>
/// The stopped state.
/// </summary>

View File

@ -1,28 +1,34 @@
using System;
namespace Trakt.Helpers;
/// <summary>
/// Similar to <see cref="Progress"/>, but report is relative, not absolute.
/// </summary>
/// Can't be generic, because it's impossible to do arithmetics on generics
public class SplittableProgress : Progress<double>, ISplittableProgress<double>
namespace Trakt.Helpers
{
public SplittableProgress(Action<double> handler)
: base(handler)
/// <summary>
/// Similar to <see cref="Progress"/>, but report is relative, not absolute.
/// </summary>
/// Can't be generic, because it's impossible to do arithmetics on generics.
public class SplittableProgress : Progress<double>, ISplittableProgress<double>
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SplittableProgress"/> class.
/// </summary>
/// <param name="handler">Instance of the <see cref="Action"/> interface.</param>
public SplittableProgress(Action<double> handler)
: base(handler)
{
}
private double Progress { get; set; }
private double Progress { get; set; }
public ISplittableProgress<double> Split(int parts)
{
var child = new SplittableProgress(
d =>
{
Progress += d / parts;
OnReport(Progress);
});
return child;
/// <inheritdoc />
public ISplittableProgress<double> Split(int parts)
{
var child = new SplittableProgress(
d =>
{
Progress += d / parts;
OnReport(Progress);
});
return child;
}
}
}

View File

@ -9,207 +9,205 @@ using Microsoft.Extensions.Logging;
using Trakt.Api;
using Trakt.Model;
namespace Trakt.Helpers;
/// <summary>
/// Helper class used to update the watched status of movies/episodes. Attempts to organise
/// requests to lower trakt.tv api calls.
/// </summary>
internal class UserDataManagerEventsHelper : IDisposable
namespace Trakt.Helpers
{
private readonly ILogger<UserDataManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private readonly List<UserDataPackage> _userDataPackages;
private Timer _timer;
/// <summary>
///
/// Helper class used to update the watched status of movies/episodes.
/// Attempts to organise requests to lower API calls.
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public UserDataManagerEventsHelper(ILogger<UserDataManagerEventsHelper> logger, TraktApi traktApi)
internal class UserDataManagerEventsHelper : IDisposable
{
_userDataPackages = new List<UserDataPackage>();
_logger = logger;
_traktApi = traktApi;
}
private readonly ILogger<UserDataManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private readonly List<UserDataPackage> _userDataPackages;
private Timer _timer;
/// <summary>
///
/// </summary>
/// <param name="userDataSaveEventArgs"></param>
/// <param name="traktUser"></param>
public void ProcessUserDataSaveEventArgs(UserDataSaveEventArgs userDataSaveEventArgs, TraktUser traktUser)
{
var userPackage = _userDataPackages.FirstOrDefault(e => e.TraktUser.Equals(traktUser));
if (userPackage == null)
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManagerEventsHelper"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{UserDataManagerEventsHelper}"/>.</param>
/// <param name="traktApi">The <see cref="TraktApi"/>.</param>
public UserDataManagerEventsHelper(ILogger<UserDataManagerEventsHelper> logger, TraktApi traktApi)
{
userPackage = new UserDataPackage { TraktUser = traktUser };
_userDataPackages.Add(userPackage);
_userDataPackages = new List<UserDataPackage>();
_logger = logger;
_traktApi = traktApi;
}
if (_timer == null)
/// <summary>
/// Process user data save event for trakt.tv users.
/// </summary>
/// <param name="userDataSaveEventArgs">The <see cref="UserDataSaveEventArgs"/>.</param>
/// <param name="traktUser">The <see cref="TraktUser"/>.</param>
public void ProcessUserDataSaveEventArgs(UserDataSaveEventArgs userDataSaveEventArgs, TraktUser traktUser)
{
_timer = new Timer(
OnTimerCallback,
null,
TimeSpan.FromMilliseconds(5000),
Timeout.InfiniteTimeSpan);
}
else
{
_timer.Change(TimeSpan.FromMilliseconds(5000), Timeout.InfiniteTimeSpan);
}
var userPackage = _userDataPackages.FirstOrDefault(user => user.TraktUser.Equals(traktUser));
if (userPackage == null)
{
_userDataPackages.Add(new UserDataPackage { TraktUser = traktUser });
}
if (_timer == null)
{
_timer = new Timer(
OnTimerCallback,
null,
TimeSpan.FromMilliseconds(3000),
Timeout.InfiniteTimeSpan);
}
else
{
_timer.Change(TimeSpan.FromMilliseconds(5000), Timeout.InfiniteTimeSpan);
}
if (userDataSaveEventArgs.Item is Movie movie)
{
if (userDataSaveEventArgs.UserData.Played)
{
if (traktUser.PostSetWatched)
{
userPackage.SeenMovies.Add(movie);
}
// Force update trakt.tv if we have more than 100 seen movies in the queue due to API
if (userPackage.SeenMovies.Count >= 100)
{
_traktApi.SendMoviePlaystateUpdates(
userPackage.SeenMovies,
userPackage.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
userPackage.SeenMovies.Clear();
}
}
else
{
if (traktUser.PostSetUnwatched)
{
userPackage.UnSeenMovies.Add(movie);
}
// Force update trakt.tv if we have more than 100 unseen movies in the queue due to API
if (userPackage.UnSeenMovies.Count >= 100)
{
_traktApi.SendMoviePlaystateUpdates(
userPackage.UnSeenMovies,
userPackage.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
userPackage.UnSeenMovies.Clear();
}
}
return;
}
if (!(userDataSaveEventArgs.Item is Episode episode))
{
return;
}
// If it's not the series we're currently storing, upload our episodes and reset the arrays
if (!userPackage.CurrentSeriesId.Equals(episode.Series.Id))
{
if (userPackage.SeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
userPackage.SeenEpisodes,
userPackage.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
userPackage.SeenEpisodes.Clear();
}
if (userPackage.UnSeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
userPackage.UnSeenEpisodes,
userPackage.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
userPackage.UnSeenEpisodes.Clear();
}
userPackage.CurrentSeriesId = episode.Series.Id;
}
if (userDataSaveEventArgs.Item is Movie movie)
{
if (userDataSaveEventArgs.UserData.Played)
{
if (traktUser.PostSetWatched)
{
userPackage.SeenMovies.Add(movie);
}
if (userPackage.SeenMovies.Count >= 100)
{
_traktApi.SendMoviePlaystateUpdates(
userPackage.SeenMovies,
userPackage.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
userPackage.SeenMovies = new List<Movie>();
userPackage.SeenEpisodes.Add(episode);
}
}
else
{
if (traktUser.PostSetUnwatched)
{
userPackage.UnSeenMovies.Add(movie);
userPackage.UnSeenEpisodes.Add(episode);
}
}
}
if (userPackage.UnSeenMovies.Count >= 100)
private void OnTimerCallback(object state)
{
foreach (var package in _userDataPackages)
{
if (package.UnSeenMovies.Any())
{
_traktApi.SendMoviePlaystateUpdates(
userPackage.UnSeenMovies,
userPackage.TraktUser,
package.UnSeenMovies.ToList(),
package.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
userPackage.UnSeenMovies = new List<Movie>();
package.UnSeenMovies.Clear();
}
if (package.SeenMovies.Any())
{
_traktApi.SendMoviePlaystateUpdates(
package.SeenMovies.ToList(),
package.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
package.SeenMovies.Clear();
}
if (package.UnSeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
package.UnSeenEpisodes.ToList(),
package.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
package.UnSeenEpisodes.Clear();
}
if (package.SeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
package.SeenEpisodes.ToList(),
package.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
package.SeenEpisodes.Clear();
}
}
return;
}
if (!(userDataSaveEventArgs.Item is Episode episode))
public void Dispose()
{
return;
Dispose(true);
GC.SuppressFinalize(this);
}
// If it's not the series we're currently storing, upload our episodes and reset the arrays
if (!userPackage.CurrentSeriesId.Equals(episode.Series.Id))
protected virtual void Dispose(bool disposing)
{
if (userPackage.SeenEpisodes.Any())
if (disposing)
{
_traktApi.SendEpisodePlaystateUpdates(
userPackage.SeenEpisodes,
userPackage.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
userPackage.SeenEpisodes = new List<Episode>();
_timer.Dispose();
}
if (userPackage.UnSeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
userPackage.UnSeenEpisodes,
userPackage.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
userPackage.UnSeenEpisodes = new List<Episode>();
}
userPackage.CurrentSeriesId = episode.Series.Id;
}
if (userDataSaveEventArgs.UserData.Played)
{
if (traktUser.PostSetWatched)
{
userPackage.SeenEpisodes.Add(episode);
}
}
else
{
if (traktUser.PostSetUnwatched)
{
userPackage.UnSeenEpisodes.Add(episode);
}
}
}
private void OnTimerCallback(object state)
{
foreach (var package in _userDataPackages)
{
if (package.UnSeenMovies.Any())
{
var movies = package.UnSeenMovies.ToList();
package.UnSeenMovies.Clear();
_traktApi.SendMoviePlaystateUpdates(
movies,
package.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
}
if (package.SeenMovies.Any())
{
var movies = package.SeenMovies.ToList();
package.SeenMovies.Clear();
_traktApi.SendMoviePlaystateUpdates(
movies,
package.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
}
if (package.UnSeenEpisodes.Any())
{
var episodes = package.UnSeenEpisodes.ToList();
package.UnSeenEpisodes.Clear();
_traktApi.SendEpisodePlaystateUpdates(
episodes,
package.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
}
if (package.SeenEpisodes.Any())
{
var episodes = package.SeenEpisodes.ToList();
package.SeenEpisodes.Clear();
_traktApi.SendEpisodePlaystateUpdates(
episodes,
package.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_timer.Dispose();
}
}
}

View File

@ -4,30 +4,31 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using Trakt.Model;
namespace Trakt.Helpers;
/// <summary>
/// Class that contains all the items to be reported to trakt.tv and supporting properties.
/// </summary>
internal class UserDataPackage
namespace Trakt.Helpers
{
public UserDataPackage()
/// <summary>
/// Class that contains all the items to be reported to trakt.tv and supporting properties.
/// </summary>
internal class UserDataPackage
{
SeenMovies = new List<Movie>();
UnSeenMovies = new List<Movie>();
SeenEpisodes = new List<Episode>();
UnSeenEpisodes = new List<Episode>();
public UserDataPackage()
{
SeenMovies = new List<Movie>();
UnSeenMovies = new List<Movie>();
SeenEpisodes = new List<Episode>();
UnSeenEpisodes = new List<Episode>();
}
public TraktUser TraktUser { get; set; }
public Guid CurrentSeriesId { get; set; }
public List<Movie> SeenMovies { get; set; }
public List<Movie> UnSeenMovies { get; set; }
public List<Episode> SeenEpisodes { get; set; }
public List<Episode> UnSeenEpisodes { get; set; }
}
public TraktUser TraktUser { get; set; }
public Guid CurrentSeriesId { get; set; }
public List<Movie> SeenMovies { get; set; }
public List<Movie> UnSeenMovies { get; set; }
public List<Episode> SeenEpisodes { get; set; }
public List<Episode> UnSeenEpisodes { get; set; }
}

View File

@ -1,44 +1,44 @@
using System;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using Trakt.Model;
namespace Trakt.Helpers;
internal static class UserHelper
namespace Trakt.Helpers
{
public static TraktUser GetTraktUser(User user)
internal static class UserHelper
{
return GetTraktUser(user.Id);
}
public static TraktUser GetTraktUser(string userId)
{
return GetTraktUser(new Guid(userId));
}
public static TraktUser GetTraktUser(Guid userGuid)
{
if (Plugin.Instance.PluginConfiguration.TraktUsers == null)
public static TraktUser GetTraktUser(User user)
{
return null;
return GetTraktUser(user.Id);
}
return Plugin.Instance.PluginConfiguration.TraktUsers.FirstOrDefault(tUser =>
public static TraktUser GetTraktUser(string userId)
{
if (string.IsNullOrWhiteSpace(tUser.LinkedMbUserId))
return GetTraktUser(new Guid(userId));
}
public static TraktUser GetTraktUser(Guid userGuid)
{
if (Plugin.Instance.PluginConfiguration.TraktUsers == null)
{
return null;
}
return Plugin.Instance.PluginConfiguration.TraktUsers.FirstOrDefault(user =>
{
if (string.IsNullOrWhiteSpace(user.LinkedMbUserId))
{
return false;
}
if (Guid.TryParse(user.LinkedMbUserId, out Guid traktUserGuid)
&& traktUserGuid.Equals(userGuid))
{
return true;
}
return false;
}
if (Guid.TryParse(tUser.LinkedMbUserId, out Guid traktUserGuid)
&& traktUserGuid.Equals(userGuid))
{
return true;
}
return false;
});
});
}
}
}

82
Trakt/Model/TraktAudio.cs Normal file
View File

@ -0,0 +1,82 @@
using System.ComponentModel;
namespace Trakt.Model
{
/// <summary>
/// Enum TraktAudio.
/// </summary>
public enum TraktAudio
{
/// <summary>
/// LPCM audio.
/// </summary>
[Description("lpcm")]
Lpcm,
/// <summary>
/// MP3 audio.
/// </summary>
[Description("mp3")]
Mp3,
/// <summary>
/// AAC audio.
/// </summary>
[Description("aac")]
Aac,
/// <summary>
/// DTS audio.
/// </summary>
[Description("dts")]
Dts,
/// <summary>
/// DTS-HD Master Audio audio.
/// </summary>
[Description("dts_ma")]
DtsMa,
/// <summary>
/// FLAC audio.
/// </summary>
[Description("flac")]
Flac,
/// <summary>
/// OGG audio.
/// </summary>
[Description("ogg")]
Ogg,
/// <summary>
/// WMA audio.
/// </summary>
[Description("wma")]
Wma,
/// <summary>
/// Dolby ProLogic audio.
/// </summary>
[Description("dolby_prologic")]
DolbyProLogic,
/// <summary>
/// Dolby Digital audio.
/// </summary>
[Description("dolby_digital")]
DolbyDigital,
/// <summary>
/// Dolby Digital Plus audio.
/// </summary>
[Description("dolby_digital_plus")]
DolbyDigitalPlus,
/// <summary>
/// Dolby TrueHD audio.
/// </summary>
[Description("dolby_truehd")]
DolbyTrueHd
}
}

View File

@ -2,53 +2,108 @@
using System;
namespace Trakt.Model;
public class TraktUser
namespace Trakt.Model
{
public TraktUser()
/// <summary>
/// Trakt.tv user class.
/// </summary>
public class TraktUser
{
SkipUnwatchedImportFromTrakt = true;
SkipWatchedImportFromTrakt = false;
PostWatchedHistory = true;
PostUnwatchedHistory = true;
PostSetWatched = true;
PostSetUnwatched = true;
ExtraLogging = false;
ExportMediaInfo = false;
SynchronizeCollections = true;
Scrobble = true;
/// <summary>
/// Initializes a new instance of the <see cref="TraktUser"/> class.
/// </summary>
public TraktUser()
{
SkipUnwatchedImportFromTrakt = true;
SkipWatchedImportFromTrakt = false;
PostWatchedHistory = true;
PostUnwatchedHistory = true;
PostSetWatched = true;
PostSetUnwatched = true;
ExtraLogging = false;
ExportMediaInfo = false;
SynchronizeCollections = true;
Scrobble = true;
}
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// Gets or sets the refresh token.
/// </summary>
public string RefreshToken { get; set; }
/// <summary>
/// Gets or sets the linked Mb user id.
/// </summary>
public string LinkedMbUserId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the advanced rating option is enabled or not.
/// </summary>
public bool UsesAdvancedRating { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the skip unwatched import option is enabled or not.
/// </summary>
public bool SkipUnwatchedImportFromTrakt { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the skip watched import option is enabled or not.
/// </summary>
public bool SkipWatchedImportFromTrakt { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the watch history should be posted or not.
/// </summary>
public bool PostWatchedHistory { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the unwatch history should be posted or not.
/// </summary>
public bool PostUnwatchedHistory { get; set; }
/// <summary>
/// Gets or sets a value indicating whether setting an item to watched should be posted or not.
/// </summary>
public bool PostSetWatched { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the setting an item to unwatched should be posted or not.
/// </summary>
public bool PostSetUnwatched { get; set; }
/// <summary>
/// Gets or sets a value indicating whether extra logging is enabled or not.
/// </summary>
public bool ExtraLogging { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the media info should be exported or not.
/// </summary>
public bool ExportMediaInfo { get; set; }
/// <summary>
/// Gets or sets a value indicating whether collections should be synchronized or not.
/// </summary>
public bool SynchronizeCollections { get; set; }
/// <summary>
/// Gets or sets a value indicating whether scrobbling should take place or not.
/// </summary>
public bool Scrobble { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string[] LocationsExcluded { get; set; }
/// <summary>
/// Gets or sets the access token expiration.
/// </summary>
public DateTime AccessTokenExpiration { get; set; }
}
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public string LinkedMbUserId { get; set; }
public bool UsesAdvancedRating { get; set; }
public bool SkipUnwatchedImportFromTrakt { get; set; }
public bool SkipWatchedImportFromTrakt { get; set; }
public bool PostWatchedHistory { get; set; }
public bool PostUnwatchedHistory { get; set; }
public bool PostSetWatched { get; set; }
public bool PostSetUnwatched { get; set; }
public bool ExtraLogging { get; set; }
public bool ExportMediaInfo { get; set; }
public bool SynchronizeCollections { get; set; }
public bool Scrobble { get; set; }
public string[] LocationsExcluded { get; set; }
public DateTime AccessTokenExpiration { get; set; }
}

View File

@ -7,48 +7,68 @@ using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Trakt.Configuration;
namespace Trakt;
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
namespace Trakt
{
public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer)
: base(appPaths, xmlSerializer)
/// <summary>
/// Plugin class for the track.tv syncing.
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
Instance = this;
PollingTasks = new Dictionary<string, Task<bool>>();
}
/// <inheritdoc />
public override string Name => "Trakt";
/// <inheritdoc />
public override Guid Id => new Guid("4fe3201e-d6ae-4f2e-8917-e12bda571281");
/// <inheritdoc />
public override string Description
=> "Watch, rate, and discover media using Trakt. The HTPC just got more social.";
public static Plugin Instance { get; private set; }
public PluginConfiguration PluginConfiguration => Configuration;
public Dictionary<string, Task<bool>> PollingTasks { get; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
new PluginPageInfo
Instance = this;
PollingTasks = new Dictionary<string, Task<bool>>();
}
/// <inheritdoc />
public override string Name => "Trakt";
/// <inheritdoc />
public override Guid Id => new Guid("4fe3201e-d6ae-4f2e-8917-e12bda571281");
/// <inheritdoc />
public override string Description => "Sync your library to trakt.tv and scrobble your watch status.";
/// <summary>
/// Gets the instance of trakt.tv plugin.
/// </summary>
public static Plugin Instance { get; private set; }
/// <summary>
/// Gets the plugin configuration.
/// </summary>
public PluginConfiguration PluginConfiguration => Configuration;
/// <summary>
/// Gets the polling tasks.
/// </summary>
public Dictionary<string, Task<bool>> PollingTasks { get; }
/// <summary>
/// Return the plugin configuration page.
/// </summary>
/// <returns>PluginPageInfo.</returns>
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
Name = "trakt",
EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.html",
},
new PluginPageInfo
{
Name = "traktjs",
EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.js"
}
};
new PluginPageInfo
{
Name = "trakt",
EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.html",
},
new PluginPageInfo
{
Name = "traktjs",
EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.js"
}
};
}
}
}

View File

@ -24,277 +24,175 @@ using Trakt.Api.DataContracts.Users.Watched;
using Trakt.Helpers;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
namespace Trakt.ScheduledTasks;
/// <summary>
/// Task that will Sync each users trakt.tv profile with their local library. This task will only include
/// watched states.
/// </summary>
public class SyncFromTraktTask : IScheduledTask
namespace Trakt.ScheduledTasks
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<SyncFromTraktTask> _logger;
private readonly TraktApi _traktApi;
/// <summary>
///
/// Task that will Sync each users trakt.tv profile with their local library. This task will only include
/// watched states.
/// </summary>
/// <param name="loggerFactory"></param>
/// <param name="userManager"></param>
/// <param name="userDataManager"> </param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
public SyncFromTraktTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
public class SyncFromTraktTask : IScheduledTask
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncFromTraktTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
}
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<SyncFromTraktTask> _logger;
private readonly TraktApi _traktApi;
public string Key => "TraktSyncFromTraktTask";
public string Name => "Import playstates from Trakt.tv";
public string Description => "Sync Watched/Unwatched status from Trakt.tv for each Jellyfin user that has a configured Trakt account";
public string Category => "Trakt";
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
/// <summary>
/// Gather users and call <see cref="SyncTraktDataForUser"/>
/// </summary>
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList();
// No point going further if we don't have users.
if (users.Count == 0)
/// <summary>
/// Initializes a new instance of the <see cref="SyncFromTraktTask"/> class.
/// </summary>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public SyncFromTraktTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
_logger.LogInformation("No Users returned");
return;
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncFromTraktTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
}
// purely for progress reporting
var percentPerUser = 100 / users.Count;
double currentProgress = 0;
var numComplete = 0;
/// <inheritdoc />
public string Key => "TraktSyncFromTraktTask";
foreach (var user in users)
/// <inheritdoc />
public string Name => "Import playstates from Trakt.tv";
/// <inheritdoc />
public string Description => "Sync Watched/Unwatched status from Trakt.tv for each Jellyfin user that has a configured trakt.tv account";
/// <inheritdoc />
public string Category => "Trakt";
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList();
// No point going further if we don't have users.
if (users.Count == 0)
{
_logger.LogInformation("No Users returned");
return;
}
// Purely for progress reporting
var percentPerUser = 100 / users.Count;
double currentProgress = 0;
var numComplete = 0;
foreach (var user in users)
{
try
{
await SyncTraktDataForUser(user, currentProgress, progress, percentPerUser, cancellationToken).ConfigureAwait(false);
numComplete++;
currentProgress = percentPerUser * numComplete;
progress.Report(currentProgress);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error syncing trakt.tv data for user {UserName}", user.Username);
}
}
}
private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double currentProgress, IProgress<double> progress, double percentPerUser, CancellationToken cancellationToken)
{
var traktUser = UserHelper.GetTraktUser(user);
List<TraktMovieWatched> traktWatchedMovies;
List<TraktShowWatched> traktWatchedShows;
try
{
await SyncTraktDataForUser(user, currentProgress, progress, percentPerUser, cancellationToken).ConfigureAwait(false);
numComplete++;
currentProgress = percentPerUser * numComplete;
progress.Report(currentProgress);
/*
* In order to be as accurate as possible. We need to download the users show collection & the users watched shows.
* It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched
* like they do for movies.
*/
traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error syncing trakt data for user {UserName}", user.Username);
}
}
}
private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double currentProgress, IProgress<double> progress, double percentPerUser, CancellationToken cancellationToken)
{
var traktUser = UserHelper.GetTraktUser(user);
List<TraktMovieWatched> traktWatchedMovies;
List<TraktShowWatched> traktWatchedShows;
try
{
/*
* In order to be as accurate as possible. We need to download the users show collection & the users watched shows.
* It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched
* like they do for movies.
*/
traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled");
throw;
}
_logger.LogInformation("Trakt.tv watched Movies count = {Count}", traktWatchedMovies.Count);
_logger.LogInformation("Trakt.tv watched Shows count = {Count}", traktWatchedShows.Count);
var mediaItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user) { IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Episode }, IsVirtualItem = false, OrderBy = new[] { (ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) } })
.Where(i => _traktApi.CanSync(i, traktUser)).ToList();
// purely for progress reporting
var percentPerItem = percentPerUser / mediaItems.Count;
foreach (var movie in mediaItems.OfType<Movie>())
{
cancellationToken.ThrowIfCancellationRequested();
var matchedMovie = FindMatch(movie, traktWatchedMovies);
if (matchedMovie != null)
{
_logger.LogDebug("Movie is in Watched list {Name}", movie.Name);
var userData = _userDataManager.GetUserData(user.Id, movie);
bool changed = false;
DateTime? tLastPlayed = null;
if (DateTime.TryParse(matchedMovie.LastWatchedAt, out var value))
{
tLastPlayed = value;
}
// set movie as watched
if (!userData.Played)
{
userData.Played = true;
userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
changed = true;
}
// keep the highest play count
if (userData.PlayCount < matchedMovie.Plays)
{
userData.PlayCount = matchedMovie.Plays;
changed = true;
}
// Update last played if remote time is more recent
if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
{
userData.LastPlayedDate = tLastPlayed;
changed = true;
}
// Only process if there's a change
if (changed)
{
_userDataManager.SaveUserData(
user.Id,
movie,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
else
{
// _logger.LogInformation("Failed to match " + movie.Name);
_logger.LogError(ex, "Exception handled");
throw;
}
// purely for progress reporting
currentProgress += percentPerItem;
progress.Report(currentProgress);
}
_logger.LogInformation("Trakt.tv watched Movies count = {Count}", traktWatchedMovies.Count);
_logger.LogInformation("Trakt.tv watched Shows count = {Count}", traktWatchedShows.Count);
foreach (var episode in mediaItems.OfType<Episode>())
{
cancellationToken.ThrowIfCancellationRequested();
var matchedShow = FindMatch(episode.Series, traktWatchedShows);
var mediaItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user) { IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Episode }, IsVirtualItem = false, OrderBy = new[] { (ItemSortBy.SeriesSortName, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) } })
.Where(i => _traktApi.CanSync(i, traktUser)).ToList();
if (matchedShow != null)
// Purely for progress reporting
var percentPerItem = percentPerUser / mediaItems.Count;
foreach (var movie in mediaItems.OfType<Movie>())
{
var matchedSeason =
matchedShow.Seasons.FirstOrDefault(
tSeason =>
tSeason.Number
== (episode.ParentIndexNumber == 0
? 0
: episode.ParentIndexNumber ?? 1));
cancellationToken.ThrowIfCancellationRequested();
var matchedMovie = FindMatch(movie, traktWatchedMovies);
// keep track of the shows rewatch cycles
DateTime? tLastReset = null;
if (DateTime.TryParse(matchedShow.ResetAt, out var resetValue))
if (matchedMovie != null)
{
tLastReset = resetValue;
}
_logger.LogDebug("Movie is in Watched list {Name}", movie.Name);
// if it's not a match then it means trakt doesn't know about the season, leave the watched state alone and move on
if (matchedSeason != null)
{
// episode is in users libary. Now we need to determine if it's watched
var userData = _userDataManager.GetUserData(user.Id, episode);
var userData = _userDataManager.GetUserData(user.Id, movie);
bool changed = false;
var matchedEpisode =
matchedSeason.Episodes.FirstOrDefault(x => x.Number == (episode.IndexNumber ?? -1));
// 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 (matchedEpisode != null && tLastReset != null)
DateTime? tLastPlayed = null;
if (DateTime.TryParse(matchedMovie.LastWatchedAt, out var value))
{
if (DateTime.TryParse(matchedEpisode.LastWatchedAt, out var value) && value < tLastReset)
{
matchedEpisode = null;
}
tLastPlayed = value;
}
if (matchedEpisode != null)
// Set movie as watched
if (!userData.Played)
{
_logger.LogDebug("Episode is in Watched list {Data}", GetVerboseEpisodeData(episode));
if (!traktUser.SkipWatchedImportFromTrakt)
{
DateTime? tLastPlayed = null;
if (DateTime.TryParse(matchedEpisode.LastWatchedAt, out var value))
{
tLastPlayed = value;
}
// Set episode as watched
if (!userData.Played)
{
userData.Played = true;
userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
changed = true;
}
// keep the highest play count
if (userData.PlayCount < matchedEpisode.Plays)
{
userData.PlayCount = matchedEpisode.Plays;
changed = true;
}
// Update last played if remote time is more recent
if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
{
userData.LastPlayedDate = tLastPlayed;
changed = true;
}
}
}
else if (!traktUser.SkipUnwatchedImportFromTrakt)
{
userData.Played = false;
userData.PlayCount = 0;
userData.LastPlayedDate = null;
userData.Played = true;
userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
changed = true;
}
// only process if changed
// Keep the highest play count
if (userData.PlayCount < matchedMovie.Plays)
{
userData.PlayCount = matchedMovie.Plays;
changed = true;
}
// Update last played if remote time is more recent
if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
{
userData.LastPlayedDate = tLastPlayed;
changed = true;
}
// Only process if there's a change
if (changed)
{
_userDataManager.SaveUserData(
user.Id,
episode,
movie,
userData,
UserDataSaveReason.Import,
cancellationToken);
@ -302,102 +200,243 @@ public class SyncFromTraktTask : IScheduledTask
}
else
{
_logger.LogDebug("No Season match in Watched shows list {Episode}", GetVerboseEpisodeData(episode));
_logger.LogDebug("Failed to match {Movie}", movie.Name);
}
// Purely for progress reporting
currentProgress += percentPerItem;
progress.Report(currentProgress);
}
else
foreach (var episode in mediaItems.OfType<Episode>())
{
_logger.LogDebug("No Show match in Watched shows list {Episode}", GetVerboseEpisodeData(episode));
cancellationToken.ThrowIfCancellationRequested();
var matchedShow = FindMatch(episode.Series, traktWatchedShows);
if (matchedShow != null)
{
var matchedSeason =
matchedShow.Seasons.FirstOrDefault(
tSeason =>
tSeason.Number
== (episode.ParentIndexNumber == 0
? 0
: episode.ParentIndexNumber ?? 1));
// Keep track of the shows rewatch cycles
DateTime? tLastReset = null;
if (DateTime.TryParse(matchedShow.ResetAt, out var resetValue))
{
tLastReset = resetValue;
}
// 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 (matchedSeason != null)
{
// Episode is in users libary. Now we need to determine if it's watched
var userData = _userDataManager.GetUserData(user.Id, episode);
bool changed = false;
var matchedEpisode =
matchedSeason.Episodes.FirstOrDefault(x => x.Number == (episode.IndexNumber ?? -1));
// 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 (matchedEpisode != null && tLastReset != null)
{
if (DateTime.TryParse(matchedEpisode.LastWatchedAt, out var value) && value < tLastReset)
{
matchedEpisode = null;
}
}
if (matchedEpisode != null)
{
_logger.LogDebug("Episode is in Watched list {Data}", GetVerboseEpisodeData(episode));
if (!traktUser.SkipWatchedImportFromTrakt)
{
DateTime? tLastPlayed = null;
if (DateTime.TryParse(matchedEpisode.LastWatchedAt, out var value))
{
tLastPlayed = value;
}
// Set episode as watched
if (!userData.Played)
{
userData.Played = true;
userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
changed = true;
}
// Keep the highest play count
if (userData.PlayCount < matchedEpisode.Plays)
{
userData.PlayCount = matchedEpisode.Plays;
changed = true;
}
// Update last played if remote time is more recent
if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
{
userData.LastPlayedDate = tLastPlayed;
changed = true;
}
}
}
else if (!traktUser.SkipUnwatchedImportFromTrakt)
{
userData.Played = false;
userData.PlayCount = 0;
userData.LastPlayedDate = null;
changed = true;
}
// Only process if changed
if (changed)
{
_userDataManager.SaveUserData(
user.Id,
episode,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
else
{
_logger.LogDebug("No Season match in Watched shows list {Episode}", GetVerboseEpisodeData(episode));
}
}
else
{
_logger.LogDebug("No Show match in Watched shows list {Episode}", GetVerboseEpisodeData(episode));
}
// Purely for progress reporting
currentProgress += percentPerItem;
progress.Report(currentProgress);
}
}
private static string GetVerboseEpisodeData(Episode episode)
{
var episodeString = new StringBuilder()
.Append("Episode: ")
.Append(episode.ParentIndexNumber != null ? episode.ParentIndexNumber.ToString() : "null")
.Append('x')
.Append(episode.IndexNumber != null ? episode.IndexNumber.ToString() : "null")
.Append(" '").Append(episode.Name).Append("' ")
.Append("Series: '")
.Append(episode.Series != null
? !string.IsNullOrWhiteSpace(episode.Series.Name)
? episode.Series.Name
: "null property"
: "null class")
.Append('\'');
return episodeString.ToString();
}
/// <summary>
/// Gets a watched match for a series.
/// </summary>
/// <param name="item">The <see cref="Series"/>.</param>
/// <param name="results">IEnumerale of <see cref="TraktShowWatched"/>.</param>
/// <returns>TraktShowWatched.</returns>
public static TraktShowWatched FindMatch(Series item, IEnumerable<TraktShowWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
/// <summary>
/// Gets a collected matches for a series.
/// </summary>
/// <param name="item">The <see cref="Series"/>.</param>
/// <param name="results">IEnumerale of <see cref="TraktShowCollected"/>.</param>
/// <returns>TraktShowCollected.</returns>
public static TraktShowCollected FindMatch(Series item, IEnumerable<TraktShowCollected> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
/// <summary>
/// Gets a watched matches for a movie.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="results">IEnumerale of <see cref="TraktMovieWatched"/>.</param>
/// <returns>TraktMovieWatched.</returns>
public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable<TraktMovieWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Movie));
}
/// <summary>
/// Gets a collected matches for a movie.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="results">IEnumerale of <see cref="TraktMovieCollected"/>.</param>
/// <returns>TraktMovieCollected.</returns>
public static IEnumerable<TraktMovieCollected> FindMatches(BaseItem item, IEnumerable<TraktMovieCollected> results)
{
return results.Where(i => IsMatch(item, i.Movie)).ToList();
}
/// <summary>
/// Checks if a <see cref="BaseItem"/> matches a <see cref="TraktMovie"/>.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="movie">The <see cref="TraktMovie"/>.</param>
/// <returns><see cref="bool"/> indicating if the <see cref="BaseItem"/> matches a <see cref="TraktMovie"/>.</returns>
public static bool IsMatch(BaseItem item, TraktMovie movie)
{
var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrWhiteSpace(imdb) &&
string.Equals(imdb, movie.Ids.Imdb, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// purely for progress reporting
currentProgress += percentPerItem;
progress.Report(currentProgress);
var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
if (movie.Ids.Tmdb.HasValue && string.Equals(tmdb, movie.Ids.Tmdb.Value.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (item.Name == movie.Title && item.ProductionYear == movie.Year)
{
return true;
}
return false;
}
// _logger.LogInformation(syncItemFailures + " items not parsed");
}
private static string GetVerboseEpisodeData(Episode episode)
{
var episodeString = new StringBuilder()
.Append("Episode: ")
.Append(episode.ParentIndexNumber != null ? episode.ParentIndexNumber.ToString() : "null")
.Append('x')
.Append(episode.IndexNumber != null ? episode.IndexNumber.ToString() : "null")
.Append(" '").Append(episode.Name).Append("' ")
.Append("Series: '")
.Append(episode.Series != null
? !string.IsNullOrWhiteSpace(episode.Series.Name)
? episode.Series.Name
: "null property"
: "null class")
.Append('\'');
return episodeString.ToString();
}
public static TraktShowWatched FindMatch(Series item, IEnumerable<TraktShowWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
public static TraktShowCollected FindMatch(Series item, IEnumerable<TraktShowCollected> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable<TraktMovieWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Movie));
}
public static IEnumerable<TraktMovieCollected> FindMatches(BaseItem item, IEnumerable<TraktMovieCollected> results)
{
return results.Where(i => IsMatch(item, i.Movie)).ToList();
}
public static bool IsMatch(BaseItem item, TraktMovie movie)
{
var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrWhiteSpace(imdb) &&
string.Equals(imdb, movie.Ids.Imdb, StringComparison.OrdinalIgnoreCase))
/// <summary>
/// Checks if a <see cref="Series"/> matches a <see cref="TraktShow"/>.
/// </summary>
/// <param name="item">The <see cref="Series"/>.</param>
/// <param name="show">The <see cref="TraktShow"/>.</param>
/// <returns><see cref="bool"/> indicating if the <see cref="Series"/> matches a <see cref="TraktShow"/>.</returns>
public static bool IsMatch(Series item, TraktShow show)
{
return true;
var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrWhiteSpace(tvdb) &&
string.Equals(tvdb, show.Ids.Tvdb.ToString(), StringComparison.OrdinalIgnoreCase))
{
return true;
}
var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrWhiteSpace(imdb) &&
string.Equals(imdb, show.Ids.Imdb, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
if (movie.Ids.Tmdb.HasValue && string.Equals(tmdb, movie.Ids.Tmdb.Value.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (item.Name == movie.Title && item.ProductionYear == movie.Year)
{
return true;
}
return false;
}
public static bool IsMatch(Series item, TraktShow show)
{
var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrWhiteSpace(tvdb) &&
string.Equals(tvdb, show.Ids.Tvdb.ToString(), StringComparison.OrdinalIgnoreCase))
{
return true;
}
var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrWhiteSpace(imdb) &&
string.Equals(imdb, show.Ids.Imdb, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
}

View File

@ -20,160 +20,335 @@ using Trakt.Api.DataContracts.Sync;
using Trakt.Helpers;
using Trakt.Model;
namespace Trakt.ScheduledTasks;
/// <summary>
/// Task that will Sync each users local library with their respective trakt.tv profiles. This task will only include
/// titles, watched states will be synced in other tasks.
/// </summary>
public class SyncLibraryTask : IScheduledTask
namespace Trakt.ScheduledTasks
{
// private readonly IHttpClient _httpClient;
private readonly IUserManager _userManager;
private readonly ILogger<SyncLibraryTask> _logger;
private readonly TraktApi _traktApi;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
public SyncLibraryTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncLibraryTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
}
public string Key => "TraktSyncLibraryTask";
public string Name => "Sync library to trakt.tv";
public string Category => "Trakt";
public string Description
=> "Adds any media that is in each users trakt monitored locations to their trakt.tv profile";
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
/// <summary>
/// Gather users and call <see cref="SyncUserLibrary"/>
/// Task that will Sync each users local library with their respective trakt.tv profiles. This task will only include
/// titles, watched states will be synced in other tasks.
/// </summary>
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
public class SyncLibraryTask : IScheduledTask
{
var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList();
private readonly IUserManager _userManager;
// No point going further if we don't have users.
if (users.Count == 0)
private readonly ILogger<SyncLibraryTask> _logger;
private readonly TraktApi _traktApi;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="SyncLibraryTask"/> class.
/// </summary>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public SyncLibraryTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
_logger.LogInformation("No Users returned");
return;
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncLibraryTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
}
foreach (var user in users)
{
var traktUser = UserHelper.GetTraktUser(user);
/// <inheritdoc />
public string Key => "TraktSyncLibraryTask";
// I'll leave this in here for now, but in reality this continue should never be reached.
if (string.IsNullOrEmpty(traktUser?.LinkedMbUserId))
/// <inheritdoc />
public string Name => "Sync library to trakt.tv";
/// <inheritdoc />
public string Category => "Trakt";
/// <inheritdoc />
public string Description
=> "Adds any media that is in each users trakt.tv monitored locations to their trakt.tv profile";
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList();
// No point going further if we don't have users.
if (users.Count == 0)
{
_logger.LogError("traktUser is either null or has no linked MB account");
continue;
_logger.LogInformation("No Users returned");
return;
}
await
SyncUserLibrary(user, traktUser, progress.Split(users.Count), cancellationToken)
.ConfigureAwait(false);
foreach (var user in users)
{
var traktUser = UserHelper.GetTraktUser(user);
// I'll leave this in here for now, but in reality this continue should never be reached.
if (string.IsNullOrEmpty(traktUser?.LinkedMbUserId))
{
_logger.LogError("traktUser is either null or has no linked MB account");
continue;
}
await
SyncUserLibrary(user, traktUser, progress.Split(users.Count), cancellationToken)
.ConfigureAwait(false);
}
}
}
/// <summary>
/// Count media items and call <see cref="SyncMovies"/> and <see cref="SyncShows"/>
/// </summary>
/// <returns></returns>
private async Task SyncUserLibrary(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
await SyncMovies(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
await SyncShows(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
private async Task SyncMovies(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
/*
* In order to sync watched status to trakt.tv we need to know what's been watched on Trakt already. This
* will stop us from endlessly incrementing the watched values on the site.
*/
var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser).ConfigureAwait(false);
var libraryMovies =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[]
{
(ItemSortBy.SortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedMovies = new List<Movie>();
var playedMovies = new List<Movie>();
var unplayedMovies = new List<Movie>();
var decisionProgress = progress.Split(4).Split(libraryMovies.Count);
foreach (var child in libraryMovies)
/// <summary>
/// Count media items and call <see cref="SyncMovies"/> and <see cref="SyncShows"/>.
/// </summary>
/// <param name="user">The <see cref="Jellyfin.Data.Entities.User"/>.</param>
/// <param name="traktUser">The <see cref="TraktUser"/>.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>Task.</returns>
private async Task SyncUserLibrary(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var libraryMovie = child as Movie;
var userData = _userDataManager.GetUserData(user.Id, child);
await SyncMovies(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
await SyncShows(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
}
private async Task SyncMovies(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
/*
* In order to sync watched status to trakt.tv we need to know what's been watched on trakt.tv already. This
* will stop us from endlessly incrementing the watched values on the site.
*/
var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser).ConfigureAwait(false);
var libraryMovies =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[]
{
(ItemSortBy.SortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedMovies = new List<Movie>();
var playedMovies = new List<Movie>();
var unplayedMovies = new List<Movie>();
var decisionProgress = progress.Split(4).Split(libraryMovies.Count);
foreach (var child in libraryMovies)
{
cancellationToken.ThrowIfCancellationRequested();
var libraryMovie = child as Movie;
var userData = _userDataManager.GetUserData(user.Id, child);
if (traktUser.SynchronizeCollections)
{
// If movie is not collected, or (export media info setting is enabled and every collected matching movie has different metadata), collect it
var collectedMathingMovies = SyncFromTraktTask.FindMatches(libraryMovie, traktCollectedMovies).ToList();
if (!collectedMathingMovies.Any()
|| (traktUser.ExportMediaInfo
&& collectedMathingMovies.All(
collectedMovie => collectedMovie.MetadataIsDifferent(libraryMovie))))
{
collectedMovies.Add(libraryMovie);
}
}
var movieWatched = SyncFromTraktTask.FindMatch(libraryMovie, traktWatchedMovies);
// If the movie has been played locally and is unplayed on trakt.tv then add it to the list
if (userData.Played)
{
if (movieWatched == null)
{
if (traktUser.PostWatchedHistory)
{
playedMovies.Add(libraryMovie);
}
else if (!traktUser.SkipUnwatchedImportFromTrakt)
{
if (userData.Played)
{
userData.Played = false;
_userDataManager.SaveUserData(
user.Id,
libraryMovie,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
}
}
else
{
// If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
if (movieWatched != null && traktUser.PostUnwatchedHistory)
{
unplayedMovies.Add(libraryMovie);
}
}
decisionProgress.Report(100);
}
// Send movies to mark collected
if (traktUser.SynchronizeCollections)
{
// if movie is not collected, or (export media info setting is enabled and every collected matching movie has different metadata), collect it
var collectedMathingMovies = SyncFromTraktTask.FindMatches(libraryMovie, traktCollectedMovies).ToList();
if (!collectedMathingMovies.Any()
|| (traktUser.ExportMediaInfo
&& collectedMathingMovies.All(
collectedMovie => collectedMovie.MetadataIsDifferent(libraryMovie))))
{
collectedMovies.Add(libraryMovie);
}
await SendMovieCollectionUpdates(true, traktUser, collectedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
var movieWatched = SyncFromTraktTask.FindMatch(libraryMovie, traktWatchedMovies);
// Send movies to mark watched
await SendMoviePlaystateUpdates(true, traktUser, playedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
// if the movie has been played locally and is unplayed on trakt.tv then add it to the list
if (userData.Played)
// Send movies to mark unwatched
await SendMoviePlaystateUpdates(false, traktUser, unplayedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
private async Task SendMovieCollectionUpdates(
bool collected,
TraktUser traktUser,
List<Movie> movies,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Movies to {State} Collection {Count}", collected ? "add to" : "remove from", movies.Count);
if (movies.Count > 0)
{
if (movieWatched == null)
try
{
var dataContracts =
await _traktApi.SendLibraryUpdateAsync(
movies,
traktUser,
collected ? EventType.Add : EventType.Remove,
cancellationToken)
.ConfigureAwait(false);
if (dataContracts != null)
{
foreach (var traktSyncResponse in dataContracts)
{
LogTraktResponseDataContract(traktSyncResponse);
}
}
}
catch (ArgumentNullException argNullEx)
{
_logger.LogError(argNullEx, "ArgumentNullException handled sending movies to trakt.tv");
}
catch (Exception e)
{
_logger.LogError(e, "Exception handled sending movies to trakt.tv");
}
progress.Report(100);
}
}
private async Task SendMoviePlaystateUpdates(
bool seen,
TraktUser traktUser,
List<Movie> playedMovies,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Movies to set {State}watched: {Count}", seen ? string.Empty : "un", playedMovies.Count);
if (playedMovies.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendMoviePlaystateUpdates(playedMovies, traktUser, seen, cancellationToken).ConfigureAwait(false);
if (dataContracts != null)
{
foreach (var traktSyncResponse in dataContracts)
{
LogTraktResponseDataContract(traktSyncResponse);
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Error updating movie play states");
}
progress.Report(100);
}
}
private async Task SyncShows(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
var traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
var traktCollectedShows = await _traktApi.SendGetCollectedShowsRequest(traktUser).ConfigureAwait(false);
var episodeItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { BaseItemKind.Episode },
IsVirtualItem = false,
OrderBy = new[]
{
(ItemSortBy.SeriesSortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedEpisodes = new List<Episode>();
var playedEpisodes = new List<Episode>();
var unplayedEpisodes = new List<Episode>();
var decisionProgress = progress.Split(4).Split(episodeItems.Count);
foreach (var child in episodeItems)
{
cancellationToken.ThrowIfCancellationRequested();
var episode = child as Episode;
var userData = _userDataManager.GetUserData(user.Id, episode);
var isPlayedTraktTv = false;
var traktWatchedShow = SyncFromTraktTask.FindMatch(episode.Series, traktWatchedShows);
if (traktWatchedShow?.Seasons != null && traktWatchedShow.Seasons.Count > 0)
{
isPlayedTraktTv =
traktWatchedShow.Seasons.Any(
season =>
season.Number == episode.GetSeasonNumber() && season.Episodes != null
&& season.Episodes.Any(te => te.Number == episode.IndexNumber && te.Plays > 0));
}
// if the show has been played locally and is unplayed on trakt.tv then add it to the list
if (userData != null && userData.Played && !isPlayedTraktTv)
{
if (traktUser.PostWatchedHistory)
{
playedMovies.Add(libraryMovie);
playedEpisodes.Add(episode);
}
else if (!traktUser.SkipUnwatchedImportFromTrakt)
{
@ -183,305 +358,140 @@ public class SyncLibraryTask : IScheduledTask
_userDataManager.SaveUserData(
user.Id,
libraryMovie,
episode,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
}
}
else
{
// If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
if (movieWatched != null && traktUser.PostUnwatchedHistory)
else if (userData != null && !userData.Played && isPlayedTraktTv && traktUser.PostUnwatchedHistory)
{
unplayedMovies.Add(libraryMovie);
// If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
unplayedEpisodes.Add(episode);
}
}
decisionProgress.Report(100);
}
// send movies to mark collected
if (traktUser.SynchronizeCollections)
{
await SendMovieCollectionUpdates(true, traktUser, collectedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
// send movies to mark watched
await SendMoviePlaystateUpdates(true, traktUser, playedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
// send movies to mark unwatched
await SendMoviePlaystateUpdates(false, traktUser, unplayedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
private async Task SendMovieCollectionUpdates(
bool collected,
TraktUser traktUser,
List<Movie> movies,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Movies to {State} Collection {Count}", collected ? "add to" : "remove from", movies.Count);
if (movies.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendLibraryUpdateAsync(
movies,
traktUser,
collected ? EventType.Add : EventType.Remove,
cancellationToken)
.ConfigureAwait(false);
if (dataContracts != null)
if (traktUser.SynchronizeCollections)
{
foreach (var traktSyncResponse in dataContracts)
var traktCollectedShow = SyncFromTraktTask.FindMatch(episode.Series, traktCollectedShows);
if (traktCollectedShow?.Seasons == null
|| traktCollectedShow.Seasons.All(x => x.Number != episode.ParentIndexNumber)
|| traktCollectedShow.Seasons.First(x => x.Number == episode.ParentIndexNumber)
.Episodes.All(e => e.Number != episode.IndexNumber))
{
LogTraktResponseDataContract(traktSyncResponse);
collectedEpisodes.Add(episode);
}
}
}
catch (ArgumentNullException argNullEx)
{
_logger.LogError(argNullEx, "ArgumentNullException handled sending movies to trakt.tv");
}
catch (Exception e)
{
_logger.LogError(e, "Exception handled sending movies to trakt.tv");
}
progress.Report(100);
}
}
private async Task SendMoviePlaystateUpdates(
bool seen,
TraktUser traktUser,
List<Movie> playedMovies,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Movies to set {State}watched: {Count}", seen ? string.Empty : "un", playedMovies.Count);
if (playedMovies.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendMoviePlaystateUpdates(playedMovies, traktUser, seen, cancellationToken).ConfigureAwait(false);
if (dataContracts != null)
{
foreach (var traktSyncResponse in dataContracts)
{
LogTraktResponseDataContract(traktSyncResponse);
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Error updating movie play states");
}
progress.Report(100);
}
}
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
private async Task SyncShows(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
var traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
var traktCollectedShows = await _traktApi.SendGetCollectedShowsRequest(traktUser).ConfigureAwait(false);
var episodeItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { BaseItemKind.Episode },
IsVirtualItem = false,
OrderBy = new[]
{
(ItemSortBy.SeriesSortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedEpisodes = new List<Episode>();
var playedEpisodes = new List<Episode>();
var unplayedEpisodes = new List<Episode>();
var decisionProgress = progress.Split(4).Split(episodeItems.Count);
foreach (var child in episodeItems)
{
cancellationToken.ThrowIfCancellationRequested();
var episode = child as Episode;
var userData = _userDataManager.GetUserData(user.Id, episode);
var isPlayedTraktTv = false;
var traktWatchedShow = SyncFromTraktTask.FindMatch(episode.Series, traktWatchedShows);
if (traktWatchedShow?.Seasons != null && traktWatchedShow.Seasons.Count > 0)
{
isPlayedTraktTv =
traktWatchedShow.Seasons.Any(
season =>
season.Number == episode.GetSeasonNumber() && season.Episodes != null
&& season.Episodes.Any(te => te.Number == episode.IndexNumber && te.Plays > 0));
}
// if the show has been played locally and is unplayed on trakt.tv then add it to the list
if (userData != null && userData.Played && !isPlayedTraktTv)
{
if (traktUser.PostWatchedHistory)
{
playedEpisodes.Add(episode);
}
else if (!traktUser.SkipUnwatchedImportFromTrakt)
{
if (userData.Played)
{
userData.Played = false;
_userDataManager.SaveUserData(
user.Id,
episode,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
}
else if (userData != null && !userData.Played && isPlayedTraktTv && traktUser.PostUnwatchedHistory)
{
// If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
unplayedEpisodes.Add(episode);
decisionProgress.Report(100);
}
if (traktUser.SynchronizeCollections)
{
var traktCollectedShow = SyncFromTraktTask.FindMatch(episode.Series, traktCollectedShows);
if (traktCollectedShow?.Seasons == null
|| traktCollectedShow.Seasons.All(x => x.Number != episode.ParentIndexNumber)
|| traktCollectedShow.Seasons.First(x => x.Number == episode.ParentIndexNumber)
.Episodes.All(e => e.Number != episode.IndexNumber))
{
collectedEpisodes.Add(episode);
}
await SendEpisodeCollectionUpdates(true, traktUser, collectedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
decisionProgress.Report(100);
await SendEpisodePlaystateUpdates(true, traktUser, playedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false);
await SendEpisodePlaystateUpdates(false, traktUser, unplayedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
if (traktUser.SynchronizeCollections)
private async Task SendEpisodePlaystateUpdates(
bool seen,
TraktUser traktUser,
List<Episode> playedEpisodes,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
await SendEpisodeCollectionUpdates(true, traktUser, collectedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
await SendEpisodePlaystateUpdates(true, traktUser, playedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false);
await SendEpisodePlaystateUpdates(false, traktUser, unplayedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false);
}
private async Task SendEpisodePlaystateUpdates(
bool seen,
TraktUser traktUser,
List<Episode> playedEpisodes,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Episodes to set {State}watched: {Count}", seen ? string.Empty : "un", playedEpisodes.Count);
if (playedEpisodes.Count > 0)
{
try
_logger.LogInformation("Episodes to set {State}watched: {Count}", seen ? string.Empty : "un", playedEpisodes.Count);
if (playedEpisodes.Count > 0)
{
var dataContracts =
await _traktApi.SendEpisodePlaystateUpdates(playedEpisodes, traktUser, seen, cancellationToken).ConfigureAwait(false);
if (dataContracts != null)
try
{
foreach (var con in dataContracts)
var dataContracts =
await _traktApi.SendEpisodePlaystateUpdates(playedEpisodes, traktUser, seen, cancellationToken).ConfigureAwait(false);
if (dataContracts != null)
{
LogTraktResponseDataContract(con);
foreach (var con in dataContracts)
{
LogTraktResponseDataContract(con);
}
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Error updating episode play states");
}
progress.Report(100);
}
}
private async Task SendEpisodeCollectionUpdates(
bool collected,
TraktUser traktUser,
List<Episode> collectedEpisodes,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Episodes to add to Collection: {Count}", collectedEpisodes.Count);
if (collectedEpisodes.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendLibraryUpdateAsync(
collectedEpisodes,
traktUser,
collected ? EventType.Add : EventType.Remove,
cancellationToken)
.ConfigureAwait(false);
if (dataContracts != null)
catch (Exception e)
{
foreach (var traktSyncResponse in dataContracts)
_logger.LogError(e, "Error updating episode play states");
}
progress.Report(100);
}
}
private async Task SendEpisodeCollectionUpdates(
bool collected,
TraktUser traktUser,
List<Episode> collectedEpisodes,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Episodes to add to Collection: {Count}", collectedEpisodes.Count);
if (collectedEpisodes.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendLibraryUpdateAsync(
collectedEpisodes,
traktUser,
collected ? EventType.Add : EventType.Remove,
cancellationToken)
.ConfigureAwait(false);
if (dataContracts != null)
{
LogTraktResponseDataContract(traktSyncResponse);
foreach (var traktSyncResponse in dataContracts)
{
LogTraktResponseDataContract(traktSyncResponse);
}
}
}
catch (ArgumentNullException argNullEx)
{
_logger.LogError(argNullEx, "ArgumentNullException handled sending episodes to trakt.tv");
}
catch (Exception e)
{
_logger.LogError(e, "Exception handled sending episodes to trakt.tv");
}
progress.Report(100);
}
catch (ArgumentNullException argNullEx)
}
private void LogTraktResponseDataContract(TraktSyncResponse dataContract)
{
_logger.LogDebug("TraktResponse Added Movies: {Count}", dataContract.Added.Movies);
_logger.LogDebug("TraktResponse Added Shows: {Count}", dataContract.Added.Shows);
_logger.LogDebug("TraktResponse Added Seasons: {Count}", dataContract.Added.Seasons);
_logger.LogDebug("TraktResponse Added Episodes: {Count}", dataContract.Added.Episodes);
foreach (var traktMovie in dataContract.NotFound.Movies)
{
_logger.LogError(argNullEx, "ArgumentNullException handled sending episodes to trakt.tv");
_logger.LogError("TraktResponse not Found: {@TraktMovie}", traktMovie);
}
catch (Exception e)
foreach (var traktShow in dataContract.NotFound.Shows)
{
_logger.LogError(e, "Exception handled sending episodes to trakt.tv");
_logger.LogError("TraktResponse not Found: {@TraktShow}", traktShow);
}
progress.Report(100);
}
}
foreach (var traktSeason in dataContract.NotFound.Seasons)
{
_logger.LogError("TraktResponse not Found: {@TraktSeason}", traktSeason);
}
private void LogTraktResponseDataContract(TraktSyncResponse dataContract)
{
_logger.LogDebug("TraktResponse Added Movies: {Count}", dataContract.Added.Movies);
_logger.LogDebug("TraktResponse Added Shows: {Count}", dataContract.Added.Shows);
_logger.LogDebug("TraktResponse Added Seasons: {Count}", dataContract.Added.Seasons);
_logger.LogDebug("TraktResponse Added Episodes: {Count}", dataContract.Added.Episodes);
foreach (var traktMovie in dataContract.NotFound.Movies)
{
_logger.LogError("TraktResponse not Found: {@TraktMovie}", traktMovie);
}
foreach (var traktShow in dataContract.NotFound.Shows)
{
_logger.LogError("TraktResponse not Found: {@TraktShow}", traktShow);
}
foreach (var traktSeason in dataContract.NotFound.Seasons)
{
_logger.LogError("TraktResponse not Found: {@TraktSeason}", traktSeason);
}
foreach (var traktEpisode in dataContract.NotFound.Episodes)
{
_logger.LogError("TraktResponse not Found: {@TraktEpisode}", traktEpisode);
foreach (var traktEpisode in dataContract.NotFound.Episodes)
{
_logger.LogError("TraktResponse not Found: {@TraktEpisode}", traktEpisode);
}
}
}
}

View File

@ -34,7 +34,7 @@ namespace Trakt
private Dictionary<string, bool> _playbackPause;
/// <summary>
/// Processes server events.
/// Initializes a new instance of the <see cref="ServerMediator"/> class.
/// </summary>
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
@ -102,6 +102,7 @@ namespace Trakt
/// <summary>
/// Run observer tasks for observed events.
/// </summary>
/// <returns>Task.</returns>
public Task RunAsync()
{
_userDataManager.UserDataSaved += OnUserDataSaved;
@ -191,7 +192,7 @@ namespace Trakt
if (!_traktApi.CanSync(playbackProgressEventArgs.Item, traktUser))
{
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackProgressEventArgs.Item.Name, user.Username);
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackProgressEventArgs.Item.Path, user.Username);
continue;
}
@ -199,7 +200,7 @@ namespace Trakt
var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ?
(float)(playbackProgressEventArgs.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
_logger.LogDebug("User {User} started watching item {Item}.", user.Username, playbackProgressEventArgs.Item.Name);
_logger.LogDebug("User {User} started watching item {Item}.", user.Username, playbackProgressEventArgs.Item.Path);
try
{
@ -266,7 +267,7 @@ namespace Trakt
if (!_traktApi.CanSync(playbackProgressEventArgs.Item, traktUser))
{
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackProgressEventArgs.Item.Name, user.Username);
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackProgressEventArgs.Item.Path, user.Username);
continue;
}
@ -274,7 +275,7 @@ namespace Trakt
var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ?
(float)(playbackProgressEventArgs.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
_logger.LogDebug("User {User} progressed watching item {Item}.", user.Username, playbackProgressEventArgs.Item.Name);
_logger.LogDebug("User {User} progressed watching item {Item}.", user.Username, playbackProgressEventArgs.Item.Path);
try
{
@ -344,8 +345,8 @@ namespace Trakt
/// Media playback has stopped.
/// Depending on playback progress, let trakt.tv know the user has completed watching the item.
/// </summary>
/// <param name="sender"></param>
/// <param name="playbackStoppedEventArgs"></param>
/// <param name="sender">The sending entity.</param>
/// <param name="playbackStoppedEventArgs">The <see cref="PlaybackStopEventArgs"/>.</param>
private async void KernelPlaybackStopped(object sender, PlaybackStopEventArgs playbackStoppedEventArgs)
{
if (playbackStoppedEventArgs.Users == null || !playbackStoppedEventArgs.Users.Any() || playbackStoppedEventArgs.Item == null)
@ -441,7 +442,7 @@ namespace Trakt
/// <summary>
/// Removes event subscriptions on dispose.
/// </summary>
/// <param name="disposing"><see cref="bool"/> indicating if object is currently disposed</param>
/// <param name="disposing"><see cref="bool"/> indicating if object is currently disposed.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)

View File

@ -9,8 +9,6 @@
<Nullable>disable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<!-- Suppress all xmldoc warnings -->
<NoWarn>CS1591,CS1572,CS1573,CS1574,SA1629,SA1606,SA1611,SA1614,SA1615,SA1616,SA1642</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -25,7 +23,7 @@
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

View File

@ -1,4 +1,5 @@
<div id="traktConfigurationPage" data-role="page" class="page type-interior pluginConfigurationPage traktConfigurationPage" data-controller="__plugin/traktjs">
<div id="traktConfigurationPage" data-role="page"
class="page type-interior pluginConfigurationPage traktConfigurationPage" data-controller="__plugin/traktjs">
<div data-role="content">
<div class="content-primary">
@ -12,15 +13,18 @@
</div>
<div class="selectContainer">
<select is="emby-select" id="selectUser" name="selectUser" label="Configure Trakt for:"></select>
<select is="emby-select" id="selectUser" name="selectUser" label="Configure trakt.tv for:"></select>
</div>
<div>
<div class="fieldDescription hide" id="authorizedDescription">
This user is authorized. You can force a re-authorization by clicking the button below.
</div>
<button is="emby-button" type="button" id="authorizeDevice" class="raised block">Authorize device</button>
<button is="emby-button" type="button" id="authorizeDevice" class="raised block">Authorize
device</button>
<div id="activateWithCode" class="hide">
Please visit <a href="https://trakt.tv/activate" class="button-link emby-button" target="_blank">https://trakt.tv/activate</a> and authorize Jellyfin to access your account.<br />
Please visit <a href="https://trakt.tv/activate" class="button-link emby-button"
target="_blank">https://trakt.tv/activate</a> and authorize Jellyfin to access your
account.<br />
Your device code is <span id="userCode"></span>.
</div>
</div>
@ -32,56 +36,62 @@
<br />
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkSkipUnwatchedImportFromTrakt" name="chkSkipUnwatchedImportFromTrakt" />
<input is="emby-checkbox" type="checkbox" id="chkSkipUnwatchedImportFromTrakt"
name="chkSkipUnwatchedImportFromTrakt" />
<span>Skip unwatched import from Trakt</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
The Import from Trakt scheduled task will set local items unwatched if item is marked as unwached on Trakt. If checked, do not import unwatched status.
The Import from trakt.tv scheduled task will set local items unwatched if item is marked as
unwached on Trakt. If checked, do not import unwatched status.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkSkipWatchedImportFromTrakt" name="chkSkipWatchedImportFromTrakt" />
<input is="emby-checkbox" type="checkbox" id="chkSkipWatchedImportFromTrakt"
name="chkSkipWatchedImportFromTrakt" />
<span>Skip watched import from Trakt</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
The Import from Trakt scheduled task will set local items as watched if item is marked as watched on Trakt. If checked, do not import watched status.
The Import from trakt.tv scheduled task will set local items as watched if item is marked as
watched on Trakt. If checked, do not import watched status.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkPostWatchedHistory" name="chkPostWatchedHistory" />
<span>During Scheduled Task, set Trakt items to watched if local item is watched</span>
<input is="emby-checkbox" type="checkbox" id="chkPostWatchedHistory"
name="chkPostWatchedHistory" />
<span>During Scheduled Task, set trakt.tv items to watched if local item is watched</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Controls what is synced to Trakt when when scheduled task is run.
Controls what is synced to trakt.tv when when scheduled task is run.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkPostUnwatchedHistory" name="chkPostUnwatchedHistory" />
<span>During Scheduled Task, set Trakt items to unwatched if local item is unwatched</span>
<input is="emby-checkbox" type="checkbox" id="chkPostUnwatchedHistory"
name="chkPostUnwatchedHistory" />
<span>During Scheduled Task, set trakt.tv items to unwatched if local item is unwatched</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Controls what is synced to Trakt when when scheduled task is run.
Controls what is synced to trakt.tv when when scheduled task is run.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkPostSetWatched" name="chkPostSetWatched" />
<span>Set Trakt item to Watched when local item is changed to Watched.</span>
<span>Set trakt.tv item to Watched when local item is changed to Watched.</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Controls what is synced to Trakt when item statuses are changed during normal use.
Controls what is synced to trakt.tv when item statuses are changed during normal use.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkPostSetUnwatched" name="chkPostSetUnwatched" />
<span>Set Trakt item to Unwatched when local item is changed to Unwatched.</span>
<span>Set trakt.tv item to Unwatched when local item is changed to Unwatched.</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Controls what is synced to Trakt when item statuses are changed during normal use.
Controls what is synced to trakt.tv when item statuses are changed during normal use.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
@ -90,7 +100,7 @@
<span>Enable debug logging</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
When enabled, all data sent to trakt is logged.
When enabled, all data sent to trakt.tv is logged.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
@ -129,4 +139,3 @@
</div>
</div>
</div>

View File

@ -6,7 +6,7 @@ const TraktConfigurationPage = {
return curr.LinkedMbUserId == userId;
//return true;
})[0];
// User doesn't have a config, so create a default one.
// User doesn't have a config, so create a default one.
if (!currentUserConfig) {
// You don't have to put every property in here, just the ones the UI is expecting (below)
currentUserConfig = {
@ -165,7 +165,7 @@ export default function (view) {
});
}
ApiClient.fetch(request).then(function (result) {
console.log('Trakt user code: ' + result.userCode);
console.log('trakt.tv user code: ' + result.userCode);
view.querySelector('#authorizedDescription').classList.add('hide');
view.querySelector('#authorizeDevice').classList.add('hide');
view.querySelector('#userCode').textContent = result.userCode;
@ -182,7 +182,7 @@ export default function (view) {
}).catch(handleError);
});
view.addEventListener('viewshow', function() {
view.addEventListener('viewshow', function () {
const page = this;
ApiClient.getUsers().then(function (users) {
TraktConfigurationPage.populateUsers(users);

View File

@ -1,6 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- error on SA1000: The keyword 'new' should be followed by a space -->
<Rule Id="SA1000" Action="Error" />
<!-- error on SA1001: Commas should not be preceded by whitespace -->
<Rule Id="SA1001" Action="Error" />
<!-- error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line -->
<Rule Id="SA1117" Action="Error" />
<!-- error on SA1142: Refer to tuple fields by name -->
<Rule Id="SA1142" Action="Error" />
<!-- error on SA1210: Using directives should be ordered alphabetically by the namespaces -->
<Rule Id="SA1210" Action="Error" />
<!-- error on SA1316: Tuple element names should use correct casing -->
<Rule Id="SA1316" Action="Error" />
<!-- error on SA1414: Tuple types in signatures should have element names -->
<Rule Id="SA1414" Action="Error" />
<!-- error on SA1518: File is required to end with a single newline character -->
<Rule Id="SA1518" Action="Error" />
<!-- error on SA1629: Documentation text should end with a period -->
<Rule Id="SA1629" Action="Error" />
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
@ -38,6 +57,10 @@
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
<!-- error on CA1001: Types that own disposable fields should be disposable -->
<Rule Id="CA1001" Action="Error" />
<!-- error on CA1012: Abstract types should not have public constructors -->
<Rule Id="CA1012" Action="Error" />
<!-- error on CA1063: Implement IDisposable correctly -->
<Rule Id="CA1063" Action="Error" />
<!-- error on CA1305: Specify IFormatProvider -->
@ -56,9 +79,13 @@
<Rule Id="CA1843" Action="Error" />
<!-- error on CA1845: Use span-based 'string.Concat' -->
<Rule Id="CA1845" Action="Error" />
<!-- error on CA1849: Call async methods when in an async method -->
<Rule Id="CA1849" Action="Error" />
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
<!-- error on CA2215: Dispose methods should call base class dispose -->
<Rule Id="CA2215" Action="Error" />
<!-- error on CA2254: Template should be a static expression -->
<Rule Id="CA2254" Action="Error" />
@ -115,4 +142,9 @@
<!-- disable warning CA2234: Pass System.Uri objects instead of strings -->
<Rule Id="CA2234" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.BannedApiAnalyzers" RuleNamespace="Microsoft.Design">
<!-- error on RS0030: Do not used banned APIs -->
<Rule Id="RS0030" Action="Error" />
</Rules>
</RuleSet>