diff --git a/.editorconfig b/.editorconfig index 5a7b38e..b84e563 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf -max_line_length = 9999 +max_line_length = off # YAML indentation [*.{yml,yaml}] diff --git a/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs b/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs index e6206cc..d6f6382 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktEpisode.cs @@ -1,13 +1,30 @@ -namespace Trakt.Api.DataContracts.BaseModel +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktEpisode { - public class TraktEpisode - { - public int? season { get; set; } + /// + /// Gets or sets the season number. + /// + [JsonPropertyName("season")] + public int? Season { get; set; } - public int? number { get; set; } + /// + /// Gets or sets the episode number. + /// + [JsonPropertyName("number")] + public int? Number { get; set; } - public string title { get; set; } + /// + /// Gets or sets the episode title. + /// + [JsonPropertyName("title")] + public string Title { get; set; } - public TraktEpisodeId ids { get; set; } - } -} \ No newline at end of file + /// + /// Gets or sets the episode ids. + /// + [JsonPropertyName("ids")] + public TraktEpisodeId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs b/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs index 64a129f..5e18f74 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktEpisodeId.cs @@ -1,6 +1,5 @@ -namespace Trakt.Api.DataContracts.BaseModel +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktEpisodeId : TraktTVId { - public class TraktEpisodeId : TraktTVId - { - } -} \ No newline at end of file +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktIMDBandTMDBId.cs b/Trakt/Api/DataContracts/BaseModel/TraktIMDBandTMDBId.cs index 0651c94..8e1a66a 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktIMDBandTMDBId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktIMDBandTMDBId.cs @@ -1,9 +1,12 @@ -namespace Trakt.Api.DataContracts.BaseModel -{ - public class TraktIMDBandTMDBId : TraktId - { - public string imdb { get; set; } +using System.Text.Json.Serialization; - public int? tmdb { get; set; } - } -} \ No newline at end of file +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktIMDBandTMDBId : TraktId +{ + [JsonPropertyName("imdb")] + public string Imdb { get; set; } + + [JsonPropertyName("tmdb")] + public int? Tmdb { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktId.cs b/Trakt/Api/DataContracts/BaseModel/TraktId.cs index a1f77bc..26104d9 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktId.cs @@ -1,10 +1,18 @@ - -namespace Trakt.Api.DataContracts.BaseModel -{ - public class TraktId - { - public int? trakt { get; set; } +using System.Text.Json.Serialization; - public string slug { get; set; } - } -} \ No newline at end of file +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktId +{ + /// + /// Gets or sets the Trakt item id. + /// + [JsonPropertyName("trakt")] + public int? Trakt { get; set; } + + /// + /// Gets or sets the item slug. + /// + [JsonPropertyName("slug")] + public string Slug { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs b/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs index 67e946d..3c958eb 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktMovie.cs @@ -1,12 +1,15 @@ - -namespace Trakt.Api.DataContracts.BaseModel +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktMovie { - public class TraktMovie - { - public string title { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } - public int? year { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } - public TraktMovieId ids { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("ids")] + public TraktMovieId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs b/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs index 75d2616..ba277a6 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktMovieId.cs @@ -1,6 +1,5 @@ -namespace Trakt.Api.DataContracts.BaseModel +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktMovieId : TraktIMDBandTMDBId { - public class TraktMovieId : TraktIMDBandTMDBId - { - } -} \ No newline at end of file +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs b/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs index f6a7bf1..c2cd971 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktPerson.cs @@ -1,9 +1,12 @@ -namespace Trakt.Api.DataContracts.BaseModel -{ - public class TraktPerson - { - public string name { get; set; } +using System.Text.Json.Serialization; - public TraktPersonId ids { get; set; } - } -} \ No newline at end of file +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktPerson +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("ids")] + public TraktPersonId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs b/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs index 1b1b1fb..d47314d 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktPersonId.cs @@ -1,7 +1,9 @@ -namespace Trakt.Api.DataContracts.BaseModel +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktPersonId : TraktIMDBandTMDBId { - public class TraktPersonId : TraktIMDBandTMDBId - { - public int? tvrage { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("tvrage")] + public int? Tvrage { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktRated.cs b/Trakt/Api/DataContracts/BaseModel/TraktRated.cs index ec57a51..1fa93af 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktRated.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktRated.cs @@ -1,9 +1,12 @@ -namespace Trakt.Api.DataContracts.BaseModel -{ - public abstract class TraktRated - { - public int? rating { get; set; } +using System.Text.Json.Serialization; - public string rated_at { get; set; } - } -} \ No newline at end of file +namespace Trakt.Api.DataContracts.BaseModel; + +public abstract class TraktRated +{ + [JsonPropertyName("rating")] + public int? Rating { get; set; } + + [JsonPropertyName("rated_at")] + public string RatedAt { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs b/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs index b9022dd..07f4efd 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktSeason.cs @@ -1,9 +1,12 @@ -namespace Trakt.Api.DataContracts.BaseModel -{ - public class TraktSeason - { - public int? number { get; set; } +using System.Text.Json.Serialization; - public TraktSeasonId ids { get; set; } - } -} \ No newline at end of file +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktSeason +{ + [JsonPropertyName("number")] + public int? Number { get; set; } + + [JsonPropertyName("ids")] + public TraktSeasonId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs b/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs index c34442c..10e5c3a 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktSeasonId.cs @@ -1,11 +1,15 @@ -namespace Trakt.Api.DataContracts.BaseModel +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktSeasonId : TraktId { - public class TraktSeasonId : TraktId - { - public int? tmdb { get; set; } + [JsonPropertyName("tmdb")] + public int? Tmdb { get; set; } - public int? tvdb { get; set; } + [JsonPropertyName("tvdb")] + public int? Tvdb { get; set; } - public int? tvrage { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("tvrage")] + public int? Tvrage { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktShow.cs b/Trakt/Api/DataContracts/BaseModel/TraktShow.cs index 215b4f8..3e5664d 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktShow.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktShow.cs @@ -1,11 +1,15 @@ -namespace Trakt.Api.DataContracts.BaseModel +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktShow { - public class TraktShow - { - public string title { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } - public int? year { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } - public TraktShowId ids { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("ids")] + public TraktShowId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs b/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs index 885915d..cd66573 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktShowId.cs @@ -1,7 +1,5 @@ - -namespace Trakt.Api.DataContracts.BaseModel +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktShowId : TraktTVId { - public class TraktShowId : TraktTVId - { - } -} \ No newline at end of file +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktTVId.cs b/Trakt/Api/DataContracts/BaseModel/TraktTVId.cs index 91c9378..c706c3a 100644 --- a/Trakt/Api/DataContracts/BaseModel/TraktTVId.cs +++ b/Trakt/Api/DataContracts/BaseModel/TraktTVId.cs @@ -1,10 +1,12 @@ +using System.Text.Json.Serialization; -namespace Trakt.Api.DataContracts.BaseModel +namespace Trakt.Api.DataContracts.BaseModel; + +public class TraktTVId : TraktIMDBandTMDBId { - public class TraktTVId : TraktIMDBandTMDBId - { - public int? tvdb { get; set; } + [JsonPropertyName("tvdb")] + public int? Tvdb { get; set; } - public int? tvrage { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("tvrage")] + public int? Tvrage { get; set; } +} diff --git a/Trakt/Api/DataContracts/BaseModel/TraktUserSummary.cs b/Trakt/Api/DataContracts/BaseModel/TraktUserSummary.cs deleted file mode 100644 index 14a0b35..0000000 --- a/Trakt/Api/DataContracts/BaseModel/TraktUserSummary.cs +++ /dev/null @@ -1,14 +0,0 @@ - -namespace Trakt.Api.DataContracts.BaseModel -{ - public class TraktUserSummary - { - public string username { get; set; } - - public string name { get; set; } - - public bool vip { get; set; } - - public bool @private { get; set; } - } -} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Comments/TraktComment.cs b/Trakt/Api/DataContracts/Comments/TraktComment.cs deleted file mode 100644 index f4d2101..0000000 --- a/Trakt/Api/DataContracts/Comments/TraktComment.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Trakt.Api.DataContracts.BaseModel; - -namespace Trakt.Api.DataContracts.Comments -{ - public class TraktComment - { - public int id { get; set; } - - public int? parent_id { get; set; } - - public string created_at { get; set; } - - public string comment { get; set; } - - public bool spoiler { get; set; } - - public bool review { get; set; } - - public int replies { get; set; } - - public int likes { get; set; } - - public int? user_rating { get; set; } - - public TraktUserSummary user { get; set; } - } -} \ No newline at end of file diff --git a/Trakt/Api/DataContracts/Scrobble/SocialMedia.cs b/Trakt/Api/DataContracts/Scrobble/SocialMedia.cs new file mode 100644 index 0000000..61c65cd --- /dev/null +++ b/Trakt/Api/DataContracts/Scrobble/SocialMedia.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Scrobble; + +public class SocialMedia +{ + [JsonPropertyName("facebook")] + public bool Facebook { get; set; } + + [JsonPropertyName("twitter")] + public bool Twitter { get; set; } + + [JsonPropertyName("tumblr")] + public bool Tumblr { get; set; } +} diff --git a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs index 67d9ceb..799c50f 100644 --- a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs +++ b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleEpisode.cs @@ -1,17 +1,22 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Scrobble +namespace Trakt.Api.DataContracts.Scrobble; + +public class TraktScrobbleEpisode { - public class TraktScrobbleEpisode - { - public TraktShow show { get; set; } + [JsonPropertyName("show")] + public TraktShow Show { get; set; } - public TraktEpisode episode { get; set; } + [JsonPropertyName("episode")] + public TraktEpisode Episode { get; set; } - public float progress { get; set; } + [JsonPropertyName("progress")] + public float Progress { get; set; } - public string app_version { get; set; } + [JsonPropertyName("app_version")] + public string AppVersion { get; set; } - public string app_date { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("app_date")] + public string AppDate { get; set; } +} diff --git a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs index e374faa..80a81a7 100644 --- a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs +++ b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleMovie.cs @@ -1,15 +1,19 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Scrobble +namespace Trakt.Api.DataContracts.Scrobble; + +public class TraktScrobbleMovie { - public class TraktScrobbleMovie - { - public TraktMovie movie { get; set; } + [JsonPropertyName("movie")] + public TraktMovie Movie { get; set; } - public float progress { get; set; } + [JsonPropertyName("progress")] + public float Progress { get; set; } - public string app_version { get; set; } + [JsonPropertyName("app_version")] + public string AppVersion { get; set; } - public string app_date { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("app_date")] + public string AppDate { get; set; } +} diff --git a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs index 8a2e02b..48f1022 100644 --- a/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs +++ b/Trakt/Api/DataContracts/Scrobble/TraktScrobbleResponse.cs @@ -1,28 +1,25 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Scrobble +namespace Trakt.Api.DataContracts.Scrobble; + +public class TraktScrobbleResponse { - public class TraktScrobbleResponse - { - public string action { get; set; } + [JsonPropertyName("action")] + public string Action { get; set; } - public float progress { get; set; } + [JsonPropertyName("progress")] + public float Progress { get; set; } - public SocialMedia sharing { get; set; } + [JsonPropertyName("sharing")] + public SocialMedia Sharing { get; set; } - public class SocialMedia - { - public bool facebook { get; set; } + [JsonPropertyName("movie")] + public TraktMovie Movie { get; set; } - public bool twitter { get; set; } + [JsonPropertyName("episode")] + public TraktEpisode Episode { get; set; } - public bool tumblr { get; set; } - } - - public TraktMovie movie { get; set; } - - public TraktEpisode episode { get; set; } - - public TraktShow show { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("show")] + public TraktShow Show { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs index 6301b13..38a7138 100644 --- a/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktEpisodeCollected.cs @@ -1,19 +1,24 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Collection +namespace Trakt.Api.DataContracts.Sync.Collection; + +public class TraktEpisodeCollected : TraktEpisode { - public class TraktEpisodeCollected : TraktEpisode - { - public string collected_at { get; set; } + [JsonPropertyName("collected_at")] + public string CollectedAt { get; set; } - public string media_type { get; set; } + [JsonPropertyName("media_type")] + public string MediaType { get; set; } - public string resolution { get; set; } + [JsonPropertyName("resolution")] + public string Resolution { get; set; } - public string audio { get; set; } + [JsonPropertyName("audio")] + public string Audio { get; set; } - public string audio_channels { get; set; } + [JsonPropertyName("audio_channels")] + public string AudioChannels { get; set; } - //public bool 3d { get; set; } - } -} \ No newline at end of file + // public bool 3d { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs index b4ce621..93d8366 100644 --- a/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktMovieCollected.cs @@ -1,19 +1,24 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Collection +namespace Trakt.Api.DataContracts.Sync.Collection; + +public class TraktMovieCollected : TraktMovie { - public class TraktMovieCollected : TraktMovie - { - public string collected_at { get; set; } + [JsonPropertyName("collected_at")] + public string CollectedAt { get; set; } - public string media_type { get; set; } + [JsonPropertyName("media_type")] + public string MediaType { get; set; } - public string resolution { get; set; } + [JsonPropertyName("resolution")] + public string Resolution { get; set; } - public string audio { get; set; } + [JsonPropertyName("audio")] + public string Audio { get; set; } - public string audio_channels { get; set; } + [JsonPropertyName("audio_channels")] + public string AudioChannels { get; set; } - //public bool 3d { get; set; } - } -} \ No newline at end of file + // public bool 3d { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktSeasonCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktSeasonCollected.cs new file mode 100644 index 0000000..f377b0c --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktSeasonCollected.cs @@ -0,0 +1,16 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Sync.Collection; + +public class TraktSeasonCollected +{ + [JsonPropertyName("number")] + public int Number { get; set; } + + [JsonPropertyName("episodes")] + public List Episodes { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs b/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs index 7d75e7f..0d0476d 100644 --- a/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs +++ b/Trakt/Api/DataContracts/Sync/Collection/TraktShowCollected.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Collection +namespace Trakt.Api.DataContracts.Sync.Collection; + +public class TraktShowCollected : TraktShow { - public class TraktShowCollected : TraktShow - { - public List seasons { get; set; } - - public class TraktSeasonCollected - { - public int number { get; set; } - - public List episodes { get; set; } - } - } -} \ No newline at end of file + [JsonPropertyName("seasons")] + public List Seasons { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Items.cs b/Trakt/Api/DataContracts/Sync/Items.cs new file mode 100644 index 0000000..e58a5f8 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Items.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Sync; + +public class Items +{ + [JsonPropertyName("movies")] + public int Movies { get; set; } + + [JsonPropertyName("shows")] + public int Shows { get; set; } + + [JsonPropertyName("seasons")] + public int Seasons { get; set; } + + [JsonPropertyName("episodes")] + public int Episodes { get; set; } + + [JsonPropertyName("people")] + public int People { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/NotFoundObjects.cs b/Trakt/Api/DataContracts/Sync/NotFoundObjects.cs new file mode 100644 index 0000000..da53377 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/NotFoundObjects.cs @@ -0,0 +1,26 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync; + +public class NotFoundObjects +{ + [JsonPropertyName("movies")] + public List Movies { get; set; } + + [JsonPropertyName("shows")] + public List Shows { get; set; } + + [JsonPropertyName("episodes")] + public List Episodes { get; set; } + + [JsonPropertyName("seasons")] + public List Seasons { get; set; } + + [JsonPropertyName("people")] + public List People { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs index b3ec287..c01faaa 100644 --- a/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktEpisodeRated.cs @@ -1,11 +1,13 @@ +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Ratings -{ - public class TraktEpisodeRated : TraktRated - { - public int? number { get; set; } +namespace Trakt.Api.DataContracts.Sync.Ratings; - public TraktEpisodeId ids { get; set; } - } -} \ No newline at end of file +public class TraktEpisodeRated : TraktRated +{ + [JsonPropertyName("number")] + public int? Number { get; set; } + + [JsonPropertyName("ids")] + public TraktEpisodeId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs index 5b94371..94fc380 100644 --- a/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktMovieRated.cs @@ -1,13 +1,16 @@ +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Ratings +namespace Trakt.Api.DataContracts.Sync.Ratings; + +public class TraktMovieRated : TraktRated { - public class TraktMovieRated : TraktRated - { - public string title { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } - public int? year { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } - public TraktMovieId ids { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("ids")] + public TraktMovieId Ids { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktSeasonRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktSeasonRated.cs new file mode 100644 index 0000000..4c42cad --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktSeasonRated.cs @@ -0,0 +1,17 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; + +namespace Trakt.Api.DataContracts.Sync.Ratings; + +public class TraktSeasonRated : TraktRated +{ + [JsonPropertyName("number")] + public int? Number { get; set; } + + [JsonPropertyName("episodes")] + public List Episodes { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs b/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs index 61e1e5b..0e1c0d6 100644 --- a/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs +++ b/Trakt/Api/DataContracts/Sync/Ratings/TraktShowRated.cs @@ -1,23 +1,23 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + using System.Collections.Generic; +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Ratings +namespace Trakt.Api.DataContracts.Sync.Ratings; + +public class TraktShowRated : TraktRated { - public class TraktShowRated : TraktRated - { - public string title { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } - public int? year { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } - public TraktShowId ids { get; set; } + [JsonPropertyName("ids")] + public TraktShowId Ids { get; set; } - public List seasons { get; set; } - - public class TraktSeasonRated : TraktRated - { - public int? number { get; set; } - - public List episodes { get; set; } - } - } -} \ No newline at end of file + [JsonPropertyName("seasons")] + public List Seasons { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/TraktSync.cs b/Trakt/Api/DataContracts/Sync/TraktSync.cs index da8eda5..3b34d25 100644 --- a/Trakt/Api/DataContracts/Sync/TraktSync.cs +++ b/Trakt/Api/DataContracts/Sync/TraktSync.cs @@ -1,28 +1,19 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + using System.Collections.Generic; -using Trakt.Api.DataContracts.Sync.Collection; -using Trakt.Api.DataContracts.Sync.Ratings; -using Trakt.Api.DataContracts.Sync.Watched; +using System.Text.Json.Serialization; -namespace Trakt.Api.DataContracts.Sync +namespace Trakt.Api.DataContracts.Sync; + +public class TraktSync { - public class TraktSync - { - public List movies { get; set; } + [JsonPropertyName("movies")] + public List Movies { get; set; } - public List shows { get; set; } + [JsonPropertyName("shows")] + public List Shows { get; set; } - public List episodes { get; set; } - } - - public class TraktSyncRated : TraktSync - { - } - - public class TraktSyncWatched : TraktSync - { - } - - public class TraktSyncCollected : TraktSync - { - } -} \ No newline at end of file + [JsonPropertyName("episodes")] + public List Episodes { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/TraktSyncCollected.cs b/Trakt/Api/DataContracts/Sync/TraktSyncCollected.cs new file mode 100644 index 0000000..650026f --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/TraktSyncCollected.cs @@ -0,0 +1,7 @@ +using Trakt.Api.DataContracts.Sync.Collection; + +namespace Trakt.Api.DataContracts.Sync; + +public class TraktSyncCollected : TraktSync +{ +} diff --git a/Trakt/Api/DataContracts/Sync/TraktSyncRated.cs b/Trakt/Api/DataContracts/Sync/TraktSyncRated.cs new file mode 100644 index 0000000..5b70589 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/TraktSyncRated.cs @@ -0,0 +1,7 @@ +using Trakt.Api.DataContracts.Sync.Ratings; + +namespace Trakt.Api.DataContracts.Sync; + +public class TraktSyncRated : TraktSync +{ +} diff --git a/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs b/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs index 8c2bc6c..c541d40 100644 --- a/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs +++ b/Trakt/Api/DataContracts/Sync/TraktSyncResponse.cs @@ -1,42 +1,18 @@ -using System.Collections.Generic; -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; -namespace Trakt.Api.DataContracts.Sync +namespace Trakt.Api.DataContracts.Sync; + +public class TraktSyncResponse { - public class TraktSyncResponse - { - public Items added { get; set; } + [JsonPropertyName("added")] + public Items Added { get; set; } - public Items deleted { get; set; } + [JsonPropertyName("deleted")] + public Items Deleted { get; set; } - public Items existing { get; set; } + [JsonPropertyName("existing")] + public Items Existing { get; set; } - public class Items - { - public int movies { get; set; } - - public int shows { get; set; } - - public int seasons { get; set; } - - public int episodes { get; set; } - - public int people { get; set; } - } - - public NotFoundObjects not_found { get; set; } - - public class NotFoundObjects - { - public List movies { get; set; } - - public List shows { get; set; } - - public List episodes { get; set; } - - public List seasons { get; set; } - - public List people { get; set; } - } - } -} \ No newline at end of file + [JsonPropertyName("not_found")] + public NotFoundObjects NotFound { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/TraktSyncWatched.cs b/Trakt/Api/DataContracts/Sync/TraktSyncWatched.cs new file mode 100644 index 0000000..e433350 --- /dev/null +++ b/Trakt/Api/DataContracts/Sync/TraktSyncWatched.cs @@ -0,0 +1,7 @@ +using Trakt.Api.DataContracts.Sync.Watched; + +namespace Trakt.Api.DataContracts.Sync; + +public class TraktSyncWatched : TraktSync +{ +} diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs index df1c13f..998f828 100644 --- a/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktEpisodeWatched.cs @@ -1,9 +1,10 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Watched +namespace Trakt.Api.DataContracts.Sync.Watched; + +public class TraktEpisodeWatched : TraktEpisode { - public class TraktEpisodeWatched : TraktEpisode - { - public string watched_at { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("watched_at")] + public string WatchedAt { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs index 888f113..23c4d76 100644 --- a/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktMovieWatched.cs @@ -1,9 +1,10 @@ -using Trakt.Api.DataContracts.BaseModel; +using System.Text.Json.Serialization; +using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Watched +namespace Trakt.Api.DataContracts.Sync.Watched; + +public class TraktMovieWatched : TraktMovie { - public class TraktMovieWatched : TraktMovie - { - public string watched_at { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("watched_at")] + public string WatchedAt { get; set; } +} diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs index be1ee04..b0b1dc3 100644 --- a/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktSeasonWatched.cs @@ -1,12 +1,17 @@ -using System.Collections.Generic; +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Watched -{ - public class TraktSeasonWatched : TraktSeason - { - public string watched_at { get; set; } +namespace Trakt.Api.DataContracts.Sync.Watched; - public List episodes { get; set; } - } +public class TraktSeasonWatched : TraktSeason +{ + [JsonPropertyName("watched_at")] + public string WatchedAt { get; set; } + + [JsonPropertyName("episodes")] + public List Episodes { get; set; } } diff --git a/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs b/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs index 232fd5e..432e9ca 100644 --- a/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs +++ b/Trakt/Api/DataContracts/Sync/Watched/TraktShowWatched.cs @@ -1,12 +1,17 @@ -using System.Collections.Generic; +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Sync.Watched -{ - public class TraktShowWatched : TraktShow - { - public string watched_at { get; set; } +namespace Trakt.Api.DataContracts.Sync.Watched; - public List seasons { get; set; } - } -} \ No newline at end of file +public class TraktShowWatched : TraktShow +{ + [JsonPropertyName("watched_at")] + public string WatchedAt { get; set; } + + [JsonPropertyName("seasons")] + public List Seasons { get; set; } +} diff --git a/Trakt/Api/DataContracts/TraktDeviceCode.cs b/Trakt/Api/DataContracts/TraktDeviceCode.cs index a6dfc09..5eeafb6 100644 --- a/Trakt/Api/DataContracts/TraktDeviceCode.cs +++ b/Trakt/Api/DataContracts/TraktDeviceCode.cs @@ -1,11 +1,21 @@ -namespace Trakt.Api.DataContracts +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts; + +public class TraktDeviceCode { - public class TraktDeviceCode - { - public string device_code { get; set; } - public string user_code { get; set; } - public string verification_url { get; set; } - public int expires_in { get; set; } - public int interval { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("device_code")] + public string DeviceCode { get; set; } + + [JsonPropertyName("user_code")] + public string UserCode { get; set; } + + [JsonPropertyName("verification_url")] + public string VerificationUrl { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("interval")] + public int Interval { get; set; } +} diff --git a/Trakt/Api/DataContracts/TraktUserAccessToken.cs b/Trakt/Api/DataContracts/TraktUserAccessToken.cs index 07a2120..47bad36 100644 --- a/Trakt/Api/DataContracts/TraktUserAccessToken.cs +++ b/Trakt/Api/DataContracts/TraktUserAccessToken.cs @@ -1,16 +1,29 @@ -namespace Trakt.Api.DataContracts +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts; + +public class TraktUserAccessToken { - public class TraktUserAccessToken - { - public string access_token { get; set; } - public string token_type { get; set; } - public int expires_in { get; set; } - public string refresh_token { get; set; } - public string scope { get; set; } - public int created_at { 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... - public int expirationWithBuffer => expires_in * 3 / 4; - } -} \ No newline at end of file + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + + [JsonPropertyName("scope")] + public string Scope { get; set; } + + [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; +} diff --git a/Trakt/Api/DataContracts/TraktUserRefreshTokenRequest.cs b/Trakt/Api/DataContracts/TraktUserRefreshTokenRequest.cs index cf7e8a2..fdcf600 100644 --- a/Trakt/Api/DataContracts/TraktUserRefreshTokenRequest.cs +++ b/Trakt/Api/DataContracts/TraktUserRefreshTokenRequest.cs @@ -1,11 +1,21 @@ -namespace Trakt.Api.DataContracts +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts; + +public class TraktUserRefreshTokenRequest { - public class TraktUserRefreshTokenRequest - { - public string refresh_token { get; set; } - public string client_id { get; set; } - public string client_secret { get; set; } - public string redirect_uri { get; set; } - public string grant_type { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + + [JsonPropertyName("client_id")] + public string ClientId { get; set; } + + [JsonPropertyName("client_secret")] + public string ClientSecret { get; set; } + + [JsonPropertyName("redirect_uri")] + public string RedirectUri { get; set; } + + [JsonPropertyName("grant_type")] + public string GrantType { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktEpisodeCollected.cs b/Trakt/Api/DataContracts/Users/Collection/TraktEpisodeCollected.cs new file mode 100644 index 0000000..e35d9d4 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Collection/TraktEpisodeCollected.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Users.Collection; + +public class TraktEpisodeCollected +{ + [JsonPropertyName("number")] + public int Number { get; set; } + + [JsonPropertyName("collected_at")] + public string CollectedAt { get; set; } + + [JsonPropertyName("metadata")] + public TraktMetadata Metadata { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs b/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs index 3860739..db65d4b 100644 --- a/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs +++ b/Trakt/Api/DataContracts/Users/Collection/TraktMetadata.cs @@ -1,17 +1,20 @@ - +using System.Text.Json.Serialization; -namespace Trakt.Api.DataContracts.Users.Collection +namespace Trakt.Api.DataContracts.Users.Collection; + +public class TraktMetadata { - public class TraktMetadata - { - public string media_type { get; set; } + [JsonPropertyName("media_type")] + public string MediaType { get; set; } - public string resolution { get; set; } + [JsonPropertyName("resolution")] + public string Resolution { get; set; } - public string audio { get; set; } + [JsonPropertyName("audio")] + public string Audio { get; set; } - public string audio_channels { get; set; } + [JsonPropertyName("audio_channels")] + public string AudioChannels { get; set; } - //public bool 3d { get; set; } - } -} \ No newline at end of file + // public bool 3d { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs b/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs index b06cc2c..50e19c2 100644 --- a/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs +++ b/Trakt/Api/DataContracts/Users/Collection/TraktMovieCollected.cs @@ -1,14 +1,16 @@ - +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Collection +namespace Trakt.Api.DataContracts.Users.Collection; + +public class TraktMovieCollected { - public class TraktMovieCollected - { - public string collected_at { get; set; } + [JsonPropertyName("collected_at")] + public string CollectedAt { get; set; } - public TraktMetadata metadata { get; set; } + [JsonPropertyName("metadata")] + public TraktMetadata Metadata { get; set; } - public TraktMovie movie { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("movie")] + public TraktMovie Movie { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktSeasonCollected.cs b/Trakt/Api/DataContracts/Users/Collection/TraktSeasonCollected.cs new file mode 100644 index 0000000..b47a064 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Collection/TraktSeasonCollected.cs @@ -0,0 +1,16 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Users.Collection; + +public class TraktSeasonCollected +{ + [JsonPropertyName("number")] + public int Number { get; set; } + + [JsonPropertyName("episodes")] + public List Episodes { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs b/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs index 192e354..bb93f30 100644 --- a/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs +++ b/Trakt/Api/DataContracts/Users/Collection/TraktShowCollected.cs @@ -1,31 +1,20 @@ -using System.Collections.Generic; +#pragma warning disable CA2227 +#pragma warning disable CA1002 +using System.Collections.Generic; +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Collection +namespace Trakt.Api.DataContracts.Users.Collection; + +public class TraktShowCollected { - public class TraktShowCollected - { - public string last_collected_at { get; set; } + [JsonPropertyName("last_collected_at")] + public string LastCollectedAt { get; set; } - public TraktShow show { get; set; } + [JsonPropertyName("show")] + public TraktShow Show { get; set; } - public List seasons { get; set; } - - public class TraktSeasonCollected - { - public int number { get; set; } - - public List episodes { get; set; } - - public class TraktEpisodeCollected - { - public int number { get; set; } - - public string collected_at { get; set; } - - public TraktMetadata metadata { get; set; } - } - } - } -} \ No newline at end of file + [JsonPropertyName("seasons")] + public List Seasons { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs index 9a4e5c5..9fe8014 100644 --- a/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktEpisodeRated.cs @@ -1,9 +1,10 @@ +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Ratings +namespace Trakt.Api.DataContracts.Users.Ratings; + +public class TraktEpisodeRated : TraktRated { - public class TraktEpisodeRated : TraktRated - { - public TraktEpisode episode { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("episode")] + public TraktEpisode Episode { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs index 6dd37db..20433cc 100644 --- a/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktMovieRated.cs @@ -1,9 +1,10 @@ +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Ratings +namespace Trakt.Api.DataContracts.Users.Ratings; + +public class TraktMovieRated : TraktRated { - public class TraktMovieRated : TraktRated - { - public TraktMovie movie { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("movie")] + public TraktMovie Movie { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs index c15e37a..3af4f42 100644 --- a/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktSeasonRated.cs @@ -1,9 +1,10 @@ +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Ratings +namespace Trakt.Api.DataContracts.Users.Ratings; + +public class TraktSeasonRated : TraktRated { - public class TraktSeasonRated : TraktRated - { - public TraktSeason season { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("season")] + public TraktSeason Season { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs b/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs index 2c79767..3f6975e 100644 --- a/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs +++ b/Trakt/Api/DataContracts/Users/Ratings/TraktShowRated.cs @@ -1,9 +1,10 @@ +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Ratings +namespace Trakt.Api.DataContracts.Users.Ratings; + +public class TraktShowRated : TraktRated { - public class TraktShowRated : TraktRated - { - public TraktShow show { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("show")] + public TraktShow Show { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Watched/Episode.cs b/Trakt/Api/DataContracts/Users/Watched/Episode.cs new file mode 100644 index 0000000..43f56ef --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Watched/Episode.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Users.Watched; + +public class Episode +{ + [JsonPropertyName("last_watched_at")] + public string LastWatchedAt { get; set; } + + [JsonPropertyName("number")] + public int Number { get; set; } + + [JsonPropertyName("plays")] + public int Plays { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Watched/Season.cs b/Trakt/Api/DataContracts/Users/Watched/Season.cs new file mode 100644 index 0000000..ede3e22 --- /dev/null +++ b/Trakt/Api/DataContracts/Users/Watched/Season.cs @@ -0,0 +1,16 @@ +#pragma warning disable CA2227 +#pragma warning disable CA1002 + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Trakt.Api.DataContracts.Users.Watched; + +public class Season +{ + [JsonPropertyName("number")] + public int Number { get; set; } + + [JsonPropertyName("episodes")] + public List Episodes { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs b/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs index 90b2b64..cb6f05e 100644 --- a/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs +++ b/Trakt/Api/DataContracts/Users/Watched/TraktMovieWatched.cs @@ -1,14 +1,16 @@ - +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Watched +namespace Trakt.Api.DataContracts.Users.Watched; + +public class TraktMovieWatched { - public class TraktMovieWatched - { - public int plays { get; set; } + [JsonPropertyName("plays")] + public int Plays { get; set; } - public string last_watched_at { get; set; } + [JsonPropertyName("last_watched_at")] + public string LastWatchedAt { get; set; } - public TraktMovie movie { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("movie")] + public TraktMovie Movie { get; set; } +} diff --git a/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs b/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs index 2b6e20c..88fd5eb 100644 --- a/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs +++ b/Trakt/Api/DataContracts/Users/Watched/TraktShowWatched.cs @@ -1,35 +1,26 @@ -using System.Collections.Generic; +#pragma warning disable CA2227 +#pragma warning disable CA1002 +using System.Collections.Generic; +using System.Text.Json.Serialization; using Trakt.Api.DataContracts.BaseModel; -namespace Trakt.Api.DataContracts.Users.Watched +namespace Trakt.Api.DataContracts.Users.Watched; + +public class TraktShowWatched { - public class TraktShowWatched - { - public int plays { get; set; } + [JsonPropertyName("plays")] + public int Plays { get; set; } - public string last_watched_at { get; set; } + [JsonPropertyName("reset_at")] + public string ResetAt { get; set; } - public string reset_at { get; set; } + [JsonPropertyName("last_watched_at")] + public string LastWatchedAt { get; set; } - public TraktShow show { get; set; } + [JsonPropertyName("show")] + public TraktShow Show { get; set; } - public List seasons { get; set; } - - public class Season - { - public int number { get; set; } - - public List episodes { get; set; } - - public class Episode - { - public string last_watched_at { get; set; } - - public int number { get; set; } - - public int plays { get; set; } - } - } - } -} \ No newline at end of file + [JsonPropertyName("seasons")] + public List Seasons { get; set; } +} diff --git a/Trakt/Api/TraktApi.cs b/Trakt/Api/TraktApi.cs index d80ce38..35b0300 100644 --- a/Trakt/Api/TraktApi.cs +++ b/Trakt/Api/TraktApi.cs @@ -1,13 +1,17 @@ -using System; +#pragma warning disable CA1002 + +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using Microsoft.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; @@ -17,10 +21,12 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; using Trakt.Api.DataContracts; using Trakt.Api.DataContracts.BaseModel; using Trakt.Api.DataContracts.Scrobble; using Trakt.Api.DataContracts.Sync; +using Trakt.Api.DataContracts.Sync.Collection; using Trakt.Api.DataContracts.Sync.Ratings; using Trakt.Api.DataContracts.Sync.Watched; using Trakt.Helpers; @@ -28,146 +34,358 @@ using Trakt.Model; using TraktEpisodeCollected = Trakt.Api.DataContracts.Sync.Collection.TraktEpisodeCollected; using TraktMovieCollected = Trakt.Api.DataContracts.Sync.Collection.TraktMovieCollected; using TraktShowCollected = Trakt.Api.DataContracts.Sync.Collection.TraktShowCollected; -using System.Text.Json; -using System.Net.Mime; -using Jellyfin.Extensions.Json; -namespace Trakt.Api +namespace Trakt.Api; + +/// +/// +/// +public class TraktApi { - /// - /// - /// - public class TraktApi + private static readonly SemaphoreSlim _traktResourcePool = new SemaphoreSlim(1, 1); + + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IServerApplicationHost _appHost; + private readonly IUserDataManager _userDataManager; + private readonly IFileSystem _fileSystem; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + + public TraktApi( + ILogger logger, + IHttpClientFactory httpClientFactory, + IServerApplicationHost appHost, + IUserDataManager userDataManager, + IFileSystem fileSystem) { - private static readonly SemaphoreSlim _traktResourcePool = new SemaphoreSlim(1, 1); + _httpClientFactory = httpClientFactory; + _appHost = appHost; + _userDataManager = userDataManager; + _fileSystem = fileSystem; + _logger = logger; + } - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerApplicationHost _appHost; - private readonly IUserDataManager _userDataManager; - private readonly IFileSystem _fileSystem; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - - public TraktApi( - ILogger logger, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - IUserDataManager userDataManager, - IFileSystem fileSystem) + /// + /// Checks whether it's possible/allowed to sync a for a . + /// + /// + /// Item to check. + /// + /// + /// The trakt user to check for. + /// + /// + /// indicates if it's possible/allowed to sync this item. + /// + public bool CanSync(BaseItem item, TraktUser traktUser) + { + if (item.Path == null || item.LocationType == LocationType.Virtual) { - _httpClientFactory = httpClientFactory; - _appHost = appHost; - _userDataManager = userDataManager; - _fileSystem = fileSystem; - _logger = logger; - } - - /// - /// Checks whether it's possible/allowed to sync a for a . - /// - /// - /// Item to check. - /// - /// - /// The trakt user to check for. - /// - /// - /// indicates if it's possible/allowed to sync this item. - /// - public bool CanSync(BaseItem item, TraktUser traktUser) - { - if (item.Path == null || item.LocationType == LocationType.Virtual) - { - return false; - } - - if (traktUser.LocationsExcluded != null && traktUser.LocationsExcluded.Any(s => _fileSystem.ContainsSubPath(s, item.Path))) - { - return false; - } - - if (item is Movie movie) - { - return !string.IsNullOrEmpty(movie.GetProviderId(MetadataProvider.Imdb)) || - !string.IsNullOrEmpty(movie.GetProviderId(MetadataProvider.Tmdb)); - } - - if (item is Episode episode - && episode.Series != null - && !episode.IsMissingEpisode - && (episode.IndexNumber.HasValue - || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.Imdb)) - || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.Tmdb)) - || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.Tvdb)) - || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.TvRage)) - )) - { - var series = episode.Series; - - return !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.Imdb)) - || !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.Tmdb)) - || !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.TvRage)) - || !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.Tvdb)); - } - return false; } - /// - /// Report to trakt.tv that a movie is being watched, or has been watched. - /// - /// The movie being watched/scrobbled - /// MediaStatus enum dictating whether item is being watched or scrobbled - /// The user that watching the current movie - /// - /// A standard TraktResponse Data Contract - public async Task SendMovieStatusUpdateAsync(Movie movie, MediaStatus mediaStatus, TraktUser traktUser, float progressPercent) + if (traktUser.LocationsExcluded != null && traktUser.LocationsExcluded.Any(s => _fileSystem.ContainsSubPath(s, item.Path))) { - var movieData = new TraktScrobbleMovie + return false; + } + + if (item is Movie movie) + { + return !string.IsNullOrEmpty(movie.GetProviderId(MetadataProvider.Imdb)) || + !string.IsNullOrEmpty(movie.GetProviderId(MetadataProvider.Tmdb)); + } + + if (item is Episode episode + && episode.Series != null + && !episode.IsMissingEpisode + && (episode.IndexNumber.HasValue + || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.Imdb)) + || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.Tmdb)) + || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.Tvdb)) + || !string.IsNullOrEmpty(episode.GetProviderId(MetadataProvider.TvRage)) + )) + { + var series = episode.Series; + + return !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.Imdb)) + || !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.Tmdb)) + || !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.TvRage)) + || !string.IsNullOrEmpty(series.GetProviderId(MetadataProvider.Tvdb)); + } + + return false; + } + + /// + /// Report to trakt.tv that a movie is being watched, or has been watched. + /// + /// The movie being watched/scrobbled + /// MediaStatus enum dictating whether item is being watched or scrobbled + /// The user that watching the current movie + /// + /// A standard TraktResponse Data Contract + public async Task SendMovieStatusUpdateAsync(Movie movie, MediaStatus mediaStatus, TraktUser traktUser, float progressPercent) + { + var movieData = new TraktScrobbleMovie + { + AppDate = DateTimeOffset.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + AppVersion = _appHost.ApplicationVersionString, + Progress = progressPercent, + Movie = new TraktMovie { - app_date = DateTimeOffset.Now.Date.ToString("yyyy-MM-dd"), - app_version = _appHost.ApplicationVersionString, - progress = progressPercent, - movie = new TraktMovie + Title = movie.Name, + Year = movie.ProductionYear, + Ids = GetTraktIMDBTMDBIds(movie) + } + }; + + string url; + switch (mediaStatus) + { + case MediaStatus.Watching: + url = TraktUris.ScrobbleStart; + break; + case MediaStatus.Paused: + url = TraktUris.ScrobblePause; + break; + default: + url = TraktUris.ScrobbleStop; + break; + } + + using (var response = await PostToTrakt(url, movieData, traktUser, CancellationToken.None).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); + } + } + + /// + /// Reports to trakt.tv that an episode is being watched. Or that Episode(s) have been watched. + /// + /// The episode being watched + /// Enum indicating whether an episode is being watched or scrobbled + /// The user that's watching the episode + /// + /// A List of standard TraktResponse Data Contracts + public async Task> SendEpisodeStatusUpdateAsync(Episode episode, MediaStatus status, TraktUser traktUser, float progressPercent) + { + var episodeDatas = new List(); + + var indexNumber = 0; + var finalNumber = 0; + if (episode.IndexNumber.HasValue) + { + indexNumber = episode.IndexNumber.Value; + finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; + } + + var number = indexNumber; + var firstPass = true; + do + { + var scrobbleEpisode = new TraktScrobbleEpisode + { + AppDate = DateTimeOffset.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + AppVersion = _appHost.ApplicationVersionString, + Progress = progressPercent, + Episode = new TraktEpisode { - title = movie.Name, - year = movie.ProductionYear, - ids = GetTraktIMDBTMDBIds(movie) + Season = episode.GetSeasonNumber() + }, + Show = new TraktShow + { + Title = episode.Series.Name, + Year = episode.Series.ProductionYear, + Ids = GetTraktTvIds(episode.Series) } }; - - string url; - switch (mediaStatus) + if (episode.IndexNumber.HasValue) { - case MediaStatus.Watching: - url = TraktUris.ScrobbleStart; - break; - case MediaStatus.Paused: - url = TraktUris.ScrobblePause; - break; - default: - url = TraktUris.ScrobbleStop; - break; + scrobbleEpisode.Episode.Number = number; } - using (var response = await PostToTrakt(url, movieData, CancellationToken.None, traktUser).ConfigureAwait(false)) + // provider IDs in multi-episode file will be for the first episode only + if (firstPass) { - return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); + // output provider IDs for first episode + scrobbleEpisode.Episode.Ids = GetTraktTvIds(episode); + firstPass = false; + } + + episodeDatas.Add(scrobbleEpisode); + + number++; + } + while (episode.IndexNumber.HasValue && number <= finalNumber); + + string url; + switch (status) + { + case MediaStatus.Watching: + url = TraktUris.ScrobbleStart; + break; + case MediaStatus.Paused: + url = TraktUris.ScrobblePause; + break; + default: + url = TraktUris.ScrobbleStop; + break; + } + + var responses = new List(); + foreach (var traktScrobbleEpisode in episodeDatas) + { + using (var response = await PostToTrakt(url, traktScrobbleEpisode, traktUser, CancellationToken.None).ConfigureAwait(false)) + { + responses.Add(await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false)); } } + return responses; + } - /// - /// Reports to trakt.tv that an episode is being watched. Or that Episode(s) have been watched. - /// - /// The episode being watched - /// Enum indicating whether an episode is being watched or scrobbled - /// The user that's watching the episode - /// - /// A List of standard TraktResponse Data Contracts - public async Task> SendEpisodeStatusUpdateAsync(Episode episode, MediaStatus status, TraktUser traktUser, float progressPercent) + /// + /// Add or remove a list of movies to/from the users trakt.tv library + /// + /// The movies to add + /// The user who's library is being updated + /// + /// The cancellation token. + /// Task{TraktResponseDataContract}. + public async Task> SendLibraryUpdateAsync( + IList movies, + TraktUser traktUser, + EventType eventType, + CancellationToken cancellationToken) + { + if (movies == null) { - var episodeDatas = new List(); + throw new ArgumentNullException(nameof(movies)); + } + + if (traktUser == null) + { + throw new ArgumentNullException(nameof(traktUser)); + } + + if (eventType == EventType.Update) + { + return null; + } + + var moviesPayload = movies.Select(m => + { + var audioStream = m.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); + var traktMovieCollected = new TraktMovieCollected + { + CollectedAt = m.DateCreated.ToISO8601(), + Title = m.Name, + Year = m.ProductionYear, + Ids = GetTraktIMDBTMDBIds(m) + }; + if (traktUser.ExportMediaInfo) + { + traktMovieCollected.AudioChannels = audioStream.GetAudioChannels(); + traktMovieCollected.Audio = audioStream.GetCodecRepresetation(); + traktMovieCollected.Resolution = m.GetDefaultVideoStream().GetResolution(); + } + + return traktMovieCollected; + }).ToList(); + var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + + var responses = new List(); + var chunks = moviesPayload.ToChunks(100); + foreach (var chunk in chunks) + { + var data = new TraktSyncCollected + { + Movies = chunk.ToList() + }; + using (var response = await PostToTrakt(url, data, traktUser, cancellationToken).ConfigureAwait(false)) + { + responses.Add(await JsonSerializer.DeserializeAsync(response, _jsonOptions, cancellationToken).ConfigureAwait(false)); + } + } + + return responses; + } + + /// + /// Add or remove a list of Episodes to/from the users trakt.tv library + /// + /// The episodes to add + /// The user who's library is being updated + /// + /// The cancellation token. + /// Task{TraktResponseDataContract}. + public async Task> SendLibraryUpdateAsync( + IReadOnlyList episodes, + TraktUser traktUser, + EventType eventType, + CancellationToken cancellationToken) + { + if (episodes == null) + { + throw new ArgumentNullException(nameof(episodes)); + } + + if (traktUser == null) + { + throw new ArgumentNullException(nameof(traktUser)); + } + + if (eventType == EventType.Update) + { + return null; + } + + var responses = new List(); + var chunks = episodes.ToChunks(100); + foreach (var chunk in chunks) + { + responses.Add(await SendLibraryUpdateInternalAsync(chunk.ToList(), traktUser, eventType, cancellationToken).ConfigureAwait(false)); + } + + return responses; + } + + private async Task SendLibraryUpdateInternalAsync( + IEnumerable episodes, + TraktUser traktUser, + EventType eventType, + CancellationToken cancellationToken) + { + var episodesPayload = new List(); + var showPayload = new List(); + foreach (Episode episode in episodes) + { + var audioStream = episode.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); + + var syncShow = FindShow(showPayload, episode.Series); + if (syncShow == null) + { + syncShow = new TraktShowCollected + { + Ids = GetTraktTvIds(episode.Series), + Seasons = new List() + }; + + showPayload.Add(syncShow); + } + + var syncSeason = + syncShow.Seasons.FirstOrDefault(ss => ss.Number == episode.GetSeasonNumber()); + if (syncSeason == null) + { + syncSeason = new TraktSeasonCollected + { + Number = episode.GetSeasonNumber(), + Episodes = new List() + }; + + syncShow.Seasons.Add(syncSeason); + } var indexNumber = 0; var finalNumber = 0; @@ -181,925 +399,716 @@ namespace Trakt.Api var firstPass = true; do { - var scrobbleEpisode = new TraktScrobbleEpisode + var traktEpisodeCollected = new TraktEpisodeCollected { - app_date = DateTimeOffset.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), - app_version = _appHost.ApplicationVersionString, - progress = progressPercent, - episode = new TraktEpisode - { - season = episode.GetSeasonNumber() - }, - show = new TraktShow - { - title = episode.Series.Name, - year = episode.Series.ProductionYear, - ids = GetTraktTVIds(episode.Series) - } + CollectedAt = episode.DateCreated.ToISO8601() }; - if (episode.IndexNumber.HasValue) + + if (episode.IndexNumber.HasValue) { - scrobbleEpisode.episode.number = number; + traktEpisodeCollected.Number = number; } - //provider IDs in multi-episode file will be for the first episode only + if (firstPass) { - //output provider IDs for first episode - scrobbleEpisode.episode.ids = GetTraktTVIds(episode); + // Omit this from the rest because then we end up attaching the provider IDs of the first episode to the subsequent ones + traktEpisodeCollected.Ids = GetTraktTvIds(episode); firstPass = false; } - episodeDatas.Add(scrobbleEpisode); - number++; - } while (episode.IndexNumber.HasValue && number <= finalNumber); - - - string url; - switch (status) - { - case MediaStatus.Watching: - url = TraktUris.ScrobbleStart; - break; - case MediaStatus.Paused: - url = TraktUris.ScrobblePause; - break; - default: - url = TraktUris.ScrobbleStop; - break; - } - var responses = new List(); - foreach (var traktScrobbleEpisode in episodeDatas) - { - using (var response = await PostToTrakt(url, traktScrobbleEpisode, CancellationToken.None, traktUser).ConfigureAwait(false)) - { - responses.Add(await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false)); - } - } - return responses; - } - - /// - /// Add or remove a list of movies to/from the users trakt.tv library - /// - /// The movies to add - /// The user who's library is being updated - /// The cancellation token. - /// - /// Task{TraktResponseDataContract}. - public async Task> SendLibraryUpdateAsync( - IList movies, - TraktUser traktUser, - CancellationToken cancellationToken, - EventType eventType) - { - if (movies == null) - { - throw new ArgumentNullException(nameof(movies)); - } - - if (traktUser == null) - { - throw new ArgumentNullException(nameof(traktUser)); - } - - if (eventType == EventType.Update) - { - return null; - } - - var moviesPayload = movies.Select(m => - { - var audioStream = m.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); - var traktMovieCollected = new TraktMovieCollected - { - collected_at = m.DateCreated.ToISO8601(), - title = m.Name, - year = m.ProductionYear, - ids = GetTraktIMDBTMDBIds(m) - }; if (traktUser.ExportMediaInfo) { - traktMovieCollected.audio_channels = audioStream.GetAudioChannels(); - traktMovieCollected.audio = audioStream.GetCodecRepresetation(); - traktMovieCollected.resolution = m.GetDefaultVideoStream().GetResolution(); + // traktEpisodeCollected.Is3D = episode.Is3D; + traktEpisodeCollected.AudioChannels = audioStream.GetAudioChannels(); + traktEpisodeCollected.Audio = audioStream.GetCodecRepresetation(); + traktEpisodeCollected.Resolution = episode.GetDefaultVideoStream().GetResolution(); } - return traktMovieCollected; - }).ToList(); - var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + syncSeason.Episodes.Add(traktEpisodeCollected); - var responses = new List(); - var chunks = moviesPayload.ToChunks(100); - foreach (var chunk in chunks) - { - var data = new TraktSyncCollected - { - movies = chunk.ToList() - }; - using (var response = await PostToTrakt(url, data, cancellationToken, traktUser).ConfigureAwait(false)) - { - responses.Add(await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false)); - } + number++; } - - return responses; + while (episode.IndexNumber.HasValue && number <= finalNumber); } - /// - /// Add or remove a list of Episodes to/from the users trakt.tv library - /// - /// The episodes to add - /// The user who's library is being updated - /// The cancellation token. - /// - /// Task{TraktResponseDataContract}. - public async Task> SendLibraryUpdateAsync( - IReadOnlyList episodes, - TraktUser traktUser, - CancellationToken cancellationToken, - EventType eventType) + var data = new TraktSyncCollected { - if (episodes == null) - { - throw new ArgumentNullException(nameof(episodes)); - } + Episodes = episodesPayload.ToList(), + Shows = showPayload.ToList() + }; - if (traktUser == null) - { - throw new ArgumentNullException(nameof(traktUser)); - } + var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + using (var response = await PostToTrakt(url, data, traktUser, cancellationToken).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + } + } - if (eventType == EventType.Update) - { - return null; - } - - var responses = new List(); - var chunks = episodes.ToChunks(100); - foreach (var chunk in chunks) - { - responses.Add(await SendLibraryUpdateInternalAsync(chunk.ToList(), traktUser, cancellationToken, eventType).ConfigureAwait(false)); - } - return responses; + /// + /// Add or remove a Show(Series) to/from the users trakt.tv library + /// + /// The show to remove + /// The user who's library is being updated + /// + /// The cancellation token. + /// Task{TraktResponseDataContract}. + public async Task SendLibraryUpdateAsync( + Series show, + TraktUser traktUser, + EventType eventType, + CancellationToken cancellationToken) + { + if (show == null) + { + throw new ArgumentNullException(nameof(show)); } - private async Task SendLibraryUpdateInternalAsync( - IEnumerable episodes, - TraktUser traktUser, - CancellationToken cancellationToken, - EventType eventType) + if (traktUser == null) { - var episodesPayload = new List(); - var showPayload = new List(); - foreach (Episode episode in episodes) - { - var audioStream = episode.GetMediaStreams().FirstOrDefault(x => x.Type == MediaStreamType.Audio); - - var syncShow = findShow(showPayload, episode.Series); - if (syncShow == null) - { - syncShow = new TraktShowCollected - { - ids = GetTraktTVIds(episode.Series), - seasons = new List() - }; - - showPayload.Add(syncShow); - } - var syncSeason = - syncShow.seasons.FirstOrDefault(ss => ss.number == episode.GetSeasonNumber()); - if (syncSeason == null) - { - syncSeason = new TraktShowCollected.TraktSeasonCollected - { - number = episode.GetSeasonNumber(), - episodes = new List() - }; - - syncShow.seasons.Add(syncSeason); - } - - var indexNumber = 0; - var finalNumber = 0; - if (episode.IndexNumber.HasValue) - { - indexNumber = episode.IndexNumber.Value; - finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; - } - - var number = indexNumber; - var firstPass = true; - do - { - var traktEpisodeCollected = new TraktEpisodeCollected - { - collected_at = episode.DateCreated.ToISO8601() - }; - if (episode.IndexNumber.HasValue) - { - traktEpisodeCollected.number = number; - } - if (firstPass) - { - // Omit this from the rest because then we end up attaching the provider IDs of the first episode to the subsequent ones - traktEpisodeCollected.ids = GetTraktTVIds(episode); - firstPass = false; - } - if (traktUser.ExportMediaInfo) - { - //traktEpisodeCollected.Is3D = episode.Is3D; - traktEpisodeCollected.audio_channels = audioStream.GetAudioChannels(); - traktEpisodeCollected.audio = audioStream.GetCodecRepresetation(); - traktEpisodeCollected.resolution = episode.GetDefaultVideoStream().GetResolution(); - } - - syncSeason.episodes.Add(traktEpisodeCollected); - - number++; - } while (episode.IndexNumber.HasValue && number <= finalNumber); - - } - - var data = new TraktSyncCollected - { - episodes = episodesPayload.ToList(), - shows = showPayload.ToList() - }; - - var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; - using (var response = await PostToTrakt(url, data, cancellationToken, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); - } + throw new ArgumentNullException(nameof(traktUser)); } - /// - /// Add or remove a Show(Series) to/from the users trakt.tv library - /// - /// The show to remove - /// The user who's library is being updated - /// The cancellation token. - /// - /// Task{TraktResponseDataContract}. - public async Task SendLibraryUpdateAsync( - Series show, - TraktUser traktUser, - CancellationToken cancellationToken, - EventType eventType) + if (eventType == EventType.Update) { - if (show == null) - { - throw new ArgumentNullException(nameof(show)); - } + return null; + } - if (traktUser == null) + var showPayload = new List + { + new TraktShowCollected { - throw new ArgumentNullException(nameof(traktUser)); + Title = show.Name, + Year = show.ProductionYear, + Ids = GetTraktTvIds(show) } + }; - if (eventType == EventType.Update) - { - return null; - } + var data = new TraktSyncCollected + { + Shows = showPayload.ToList() + }; - var showPayload = new List + var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; + using (var response = await PostToTrakt(url, data, traktUser, cancellationToken).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Rate an item + /// + /// + /// + /// + /// + public async Task SendItemRating(BaseItem item, int rating, TraktUser traktUser) + { + object data = new { }; + if (item is Movie) + { + data = new { - new TraktShowCollected + movies = new[] { - title = show.Name, - year = show.ProductionYear, - ids = GetTraktTVIds(show) + new TraktMovieRated + { + Title = item.Name, + Year = item.ProductionYear, + Ids = GetTraktIMDBTMDBIds((Movie)item), + Rating = rating + } } }; - - var data = new TraktSyncCollected - { - shows = showPayload.ToList() - }; - - var url = eventType == EventType.Add ? TraktUris.SyncCollectionAdd : TraktUris.SyncCollectionRemove; - using (var response = await PostToTrakt(url, data, cancellationToken, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); - } } - - - - /// - /// Rate an item - /// - /// - /// - /// - /// - public async Task SendItemRating(BaseItem item, int rating, TraktUser traktUser) + else if (item is Episode episode) { - object data = new { }; - if (item is Movie) + var show = new TraktShowRated { - data = new + Ids = GetTraktTvIds(episode.Series), + Seasons = new List { - movies = new[] + new TraktSeasonRated { - new TraktMovieRated + Number = episode.GetSeasonNumber(), + Episodes = new List { - title = item.Name, - year = item.ProductionYear, - ids = GetTraktIMDBTMDBIds((Movie) item), - rating = rating - } - } - }; - - } - else if (item is Episode) - { - var episode = item as Episode; - - var show = new TraktShowRated - { - ids = GetTraktTVIds(episode.Series), - seasons = new List - { - new TraktShowRated.TraktSeasonRated + new TraktEpisodeRated { - number = episode.GetSeasonNumber(), - episodes = new List - { - new TraktEpisodeRated - { - number = episode.IndexNumber, - rating = rating, - ids = GetTraktTVIds(episode) - } - } + Number = episode.IndexNumber, + Rating = rating, + Ids = GetTraktTvIds(episode) } } - }; - data = new - { - shows = new[] - { - show - } - }; - } - else // It's a Series - { - data = new - { - shows = new[] - { - new TraktShowRated - { - rating = rating, - title = item.Name, - year = item.ProductionYear, - ids = GetTraktTVIds((Series) item) - } - } - }; - } - - using (var response = await PostToTrakt(TraktUris.SyncRatingsAdd, data, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// - /// - /// - /// - public async Task> SendMovieRecommendationsRequest(TraktUser traktUser) - { - using (var response = await GetFromTrakt(TraktUris.RecommendationsMovies, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// - /// - /// - /// - public async Task> SendShowRecommendationsRequest(TraktUser traktUser) - { - using (var response = await GetFromTrakt(TraktUris.RecommendationsShows, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// - /// - /// - /// - public async Task> SendGetAllWatchedMoviesRequest(TraktUser traktUser) - { - using (var response = await GetFromTrakt(TraktUris.WatchedMovies, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// - /// - /// - /// - public async Task> SendGetWatchedShowsRequest(TraktUser traktUser) - { - using (var response = await GetFromTrakt(TraktUris.WatchedShows, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// - /// - /// - /// - public async Task> SendGetAllCollectedMoviesRequest(TraktUser traktUser) - { - using (var response = await GetFromTrakt(TraktUris.CollectedMovies, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// - /// - /// - /// - public async Task> SendGetCollectedShowsRequest(TraktUser traktUser) - { - using (var response = await GetFromTrakt(TraktUris.CollectedShows, traktUser).ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); - } - } - - /// - /// Send a list of movies to trakt.tv that have been marked watched or unwatched. - /// - /// The list of movies to send - /// The trakt user profile that is being updated - /// True if movies are being marked seen, false otherwise - /// The Cancellation Token - /// - // TODO: netstandard2.1: use IAsyncEnumerable - public async Task> SendMoviePlaystateUpdates(List movies, TraktUser traktUser, bool seen, CancellationToken cancellationToken) - { - if (movies == null) - { - throw new ArgumentNullException(nameof(movies)); - } - - if (traktUser == null) - { - throw new ArgumentNullException(nameof(traktUser)); - } - - var moviesPayload = movies.Select(m => - { - var lastPlayedDate = seen - ? _userDataManager.GetUserData(new Guid(traktUser.LinkedMbUserId), m).LastPlayedDate - : null; - return new TraktMovieWatched - { - title = m.Name, - ids = GetTraktIMDBTMDBIds(m), - year = m.ProductionYear, - watched_at = lastPlayedDate?.ToISO8601() - }; - }).ToList(); - var chunks = moviesPayload.ToChunks(100).ToList(); - var traktResponses = new List(); - - foreach (var chunk in chunks) - { - var data = new TraktSyncWatched - { - movies = chunk.ToList() - }; - var url = seen ? TraktUris.SyncWatchedHistoryAdd : TraktUris.SyncWatchedHistoryRemove; - - using (var response = await PostToTrakt(url, data, cancellationToken, traktUser).ConfigureAwait(false)) - { - if (response != null) - { - traktResponses.Add(await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false)); } } - } - - return traktResponses; + }; + data = new + { + shows = new[] + { + show + } + }; + } + else // It's a Series + { + data = new + { + shows = new[] + { + new TraktShowRated + { + Rating = rating, + Title = item.Name, + Year = item.ProductionYear, + Ids = GetTraktTvIds((Series)item) + } + } + }; } - /// - /// Send a list of episodes to trakt.tv that have been marked watched or unwatched - /// - /// The list of episodes to send - /// The trakt user profile that is being updated - /// True if episodes are being marked seen, false otherwise - /// The Cancellation Token - /// - public async Task> SendEpisodePlaystateUpdates(List episodes, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + using (var response = await PostToTrakt(TraktUris.SyncRatingsAdd, data, traktUser).ConfigureAwait(false)) { - if (episodes == null) - { - throw new ArgumentNullException(nameof(episodes)); - } + return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); + } + } - if (traktUser == null) - { - throw new ArgumentNullException(nameof(traktUser)); - } + /// + /// + /// + /// + /// + public async Task> SendMovieRecommendationsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.RecommendationsMovies, traktUser).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); + } + } - var chunks = episodes.ToChunks(100).ToList(); - var traktResponses = new List(); + /// + /// + /// + /// + /// + public async Task> SendShowRecommendationsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.RecommendationsShows, traktUser).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); + } + } - foreach (var chunk in chunks) - { - var response = await SendEpisodePlaystateUpdatesInternalAsync(chunk, traktUser, seen, cancellationToken).ConfigureAwait(false); + /// + /// + /// + /// + /// + public async Task> SendGetAllWatchedMoviesRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.WatchedMovies, traktUser).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); + } + } - if (response != null) - { - traktResponses.Add(response); - } - } - return traktResponses; + /// + /// + /// + /// + /// + public async Task> SendGetWatchedShowsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.WatchedShows, traktUser).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); + } + } + + /// + /// + /// + /// + /// + public async Task> SendGetAllCollectedMoviesRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.CollectedMovies, traktUser).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); + } + } + + /// + /// + /// + /// + /// + public async Task> SendGetCollectedShowsRequest(TraktUser traktUser) + { + using (var response = await GetFromTrakt(TraktUris.CollectedShows, traktUser).ConfigureAwait(false)) + { + return await JsonSerializer.DeserializeAsync>(response, _jsonOptions).ConfigureAwait(false); + } + } + + /// + /// Send a list of movies to trakt.tv that have been marked watched or unwatched. + /// + /// The list of movies to send + /// The trakt user profile that is being updated + /// True if movies are being marked seen, false otherwise + /// The Cancellation Token + /// + // TODO: netstandard2.1: use IAsyncEnumerable + public async Task> SendMoviePlaystateUpdates(List movies, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + { + if (movies == null) + { + throw new ArgumentNullException(nameof(movies)); } - private async Task SendEpisodePlaystateUpdatesInternalAsync(IEnumerable episodeChunk, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + if (traktUser == null) { - var data = new TraktSyncWatched { episodes = new List(), shows = new List() }; - foreach (var episode in episodeChunk) + throw new ArgumentNullException(nameof(traktUser)); + } + + var moviesPayload = movies.Select(m => + { + var lastPlayedDate = seen + ? _userDataManager.GetUserData(new Guid(traktUser.LinkedMbUserId), m).LastPlayedDate + : null; + return new TraktMovieWatched { - var lastPlayedDate = seen - ? _userDataManager.GetUserData(new Guid(traktUser.LinkedMbUserId), episode) - .LastPlayedDate - : null; + Title = m.Name, + Ids = GetTraktIMDBTMDBIds(m), + Year = m.ProductionYear, + WatchedAt = lastPlayedDate?.ToISO8601() + }; + }).ToList(); + var chunks = moviesPayload.ToChunks(100).ToList(); + var traktResponses = new List(); - var syncShow = findShow(data.shows, episode.Series); - if (syncShow == null) - { - syncShow = new TraktShowWatched - { - ids = GetTraktTVIds(episode.Series), - seasons = new List() - }; - data.shows.Add(syncShow); - } - - var syncSeason = syncShow.seasons.FirstOrDefault(ss => ss.number == episode.GetSeasonNumber()); - if (syncSeason == null) - { - syncSeason = new TraktSeasonWatched - { - number = episode.GetSeasonNumber(), - episodes = new List() - }; - syncShow.seasons.Add(syncSeason); - } - - var indexNumber = 0; - var finalNumber = 0; - if (episode.IndexNumber.HasValue) - { - indexNumber = episode.IndexNumber.Value; - finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; - } - - var number = indexNumber; - var firstPass = true; - do - { - var watchedEpisode = new TraktEpisodeWatched - { - watched_at = lastPlayedDate.HasValue ? lastPlayedDate.Value.ToISO8601() : null - }; - if (episode.IndexNumber.HasValue) - { - watchedEpisode.number = number; - } - //provider IDs in multi-episode file will be for the first episode only - if (firstPass) - { - //output provider IDs for first episode - watchedEpisode.ids = GetTraktTVIds(episode); - firstPass = false; - } - syncSeason.episodes.Add(watchedEpisode); - - number++; - } while (episode.IndexNumber.HasValue && number <= finalNumber); - - } + foreach (var chunk in chunks) + { + var data = new TraktSyncWatched + { + Movies = chunk.ToList() + }; var url = seen ? TraktUris.SyncWatchedHistoryAdd : TraktUris.SyncWatchedHistoryRemove; - using (var response = await PostToTrakt(url, data, cancellationToken, traktUser).ConfigureAwait(false)) + using (var response = await PostToTrakt(url, data, traktUser, cancellationToken).ConfigureAwait(false)) { - return await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); - } - } - - public async Task AuthorizeDevice(TraktUser traktUser) - { - var deviceCodeRequest = new - { - client_id = TraktUris.ClientId - }; - - TraktDeviceCode deviceCode; - using (var response = await PostToTrakt(TraktUris.DeviceCode, deviceCodeRequest, null).ConfigureAwait(false)) - { - deviceCode = await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); - } - - // Start polling in the background - Plugin.Instance.PollingTasks[traktUser.LinkedMbUserId] = Task.Run(() => PollForAccessToken(deviceCode, traktUser)); - - return deviceCode.user_code; - } - - public async Task PollForAccessToken(TraktDeviceCode deviceCode, TraktUser traktUser) - { - var deviceAccessTokenRequest = new - { - code = deviceCode.device_code, - client_id = TraktUris.ClientId, - client_secret = TraktUris.ClientSecret - }; - - var pollingInterval = deviceCode.interval; - var expiresAt = DateTime.UtcNow.AddSeconds(deviceCode.expires_in); - _logger.LogInformation("Polling for access token every {PollingInterval}s. Expires at {ExpiresAt} UTC.", pollingInterval, expiresAt); - while (DateTime.UtcNow < expiresAt) - { - using (var response = await PostToTrakt(TraktUris.DeviceToken, deviceAccessTokenRequest).ConfigureAwait(false)) + if (response != null) { - switch (response.StatusCode) - { - case HttpStatusCode.BadRequest: - // Pending - waiting for the user to authorize your app - break; - case HttpStatusCode.NotFound: - _logger.LogError("Not Found - invalid device_code"); - break; - case HttpStatusCode.Conflict: - _logger.LogWarning("Already Used - user already approved this code"); - return false; - case HttpStatusCode.Gone: - _logger.LogError("Expired - the tokens have expired, restart the process"); - break; - case (HttpStatusCode)418: - _logger.LogInformation("Denied - user explicitly denied this code"); - return false; - case (HttpStatusCode)429: - _logger.LogWarning("Polling too quickly. Slowing down"); - pollingInterval += 1; - break; - case HttpStatusCode.OK: - _logger.LogInformation("Device successfully authorized"); + traktResponses.Add(await JsonSerializer.DeserializeAsync(response, _jsonOptions, cancellationToken).ConfigureAwait(false)); + } + } + } - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var userAccessToken = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false); - if (userAccessToken != null) - { - traktUser.AccessToken = userAccessToken.access_token; - traktUser.RefreshToken = userAccessToken.refresh_token; - traktUser.AccessTokenExpiration = DateTime.Now.AddSeconds(userAccessToken.expirationWithBuffer); - Plugin.Instance.SaveConfiguration(); - return true; - } - break; - } + return traktResponses; + } + + /// + /// Send a list of episodes to trakt.tv that have been marked watched or unwatched + /// + /// The list of episodes to send + /// The trakt user profile that is being updated + /// True if episodes are being marked seen, false otherwise + /// The Cancellation Token + /// + public async Task> SendEpisodePlaystateUpdates(List episodes, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + { + if (episodes == null) + { + throw new ArgumentNullException(nameof(episodes)); + } + + if (traktUser == null) + { + throw new ArgumentNullException(nameof(traktUser)); + } + + var chunks = episodes.ToChunks(100).ToList(); + var traktResponses = new List(); + + foreach (var chunk in chunks) + { + var response = await SendEpisodePlaystateUpdatesInternalAsync(chunk, traktUser, seen, cancellationToken).ConfigureAwait(false); + + if (response != null) + { + traktResponses.Add(response); + } + } + + return traktResponses; + } + + private async Task SendEpisodePlaystateUpdatesInternalAsync(IEnumerable episodeChunk, TraktUser traktUser, bool seen, CancellationToken cancellationToken) + { + var data = new TraktSyncWatched { Episodes = new List(), Shows = new List() }; + foreach (var episode in episodeChunk) + { + var lastPlayedDate = seen + ? _userDataManager.GetUserData(new Guid(traktUser.LinkedMbUserId), episode) + .LastPlayedDate + : null; + + var syncShow = FindShow(data.Shows, episode.Series); + if (syncShow == null) + { + syncShow = new TraktShowWatched + { + Ids = GetTraktTvIds(episode.Series), + Seasons = new List() + }; + data.Shows.Add(syncShow); + } + + var syncSeason = syncShow.Seasons.FirstOrDefault(ss => ss.Number == episode.GetSeasonNumber()); + if (syncSeason == null) + { + syncSeason = new TraktSeasonWatched + { + Number = episode.GetSeasonNumber(), + Episodes = new List() + }; + syncShow.Seasons.Add(syncSeason); + } + + var indexNumber = 0; + var finalNumber = 0; + if (episode.IndexNumber.HasValue) + { + indexNumber = episode.IndexNumber.Value; + finalNumber = (episode.IndexNumberEnd ?? episode.IndexNumber).Value; + } + + var number = indexNumber; + var firstPass = true; + do + { + var watchedEpisode = new TraktEpisodeWatched + { + WatchedAt = lastPlayedDate.HasValue ? lastPlayedDate.Value.ToISO8601() : null + }; + if (episode.IndexNumber.HasValue) + { + watchedEpisode.Number = number; } - await Task.Delay(pollingInterval * 1000).ConfigureAwait(false); - } - return false; - } - - public async Task RefreshUserAccessToken(TraktUser traktUser) - { - if (string.IsNullOrWhiteSpace(traktUser.RefreshToken)) - { - _logger.LogError("Tried to reauthenticate with Trakt, but no refreshToken was available"); - return; - } - - var data = new TraktUserRefreshTokenRequest - { - client_id = TraktUris.ClientId, - client_secret = TraktUris.ClientSecret, - redirect_uri = "urn:ietf:wg:oauth:2.0:oob", - refresh_token = traktUser.RefreshToken, - grant_type = "refresh_token" - }; - - TraktUserAccessToken userAccessToken; - try - { - using (var response = await PostToTrakt(TraktUris.AccessToken, data).ConfigureAwait(false)) + // provider IDs in multi-episode file will be for the first episode only + if (firstPass) { - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - userAccessToken = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false); + // output provider IDs for first episode + watchedEpisode.Ids = GetTraktTvIds(episode); + firstPass = false; } - } - catch (HttpRequestException ex) - { - _logger.LogError(ex, "An error occurred during token refresh"); - return; - } + syncSeason.Episodes.Add(watchedEpisode); - if (userAccessToken != null) - { - traktUser.AccessToken = userAccessToken.access_token; - traktUser.RefreshToken = userAccessToken.refresh_token; - traktUser.AccessTokenExpiration = DateTime.Now.AddSeconds(userAccessToken.expirationWithBuffer); - Plugin.Instance.SaveConfiguration(); - _logger.LogInformation("Successfully refreshed the access token for user {UserId}", traktUser.LinkedMbUserId); + number++; } + while (episode.IndexNumber.HasValue && number <= finalNumber); } - private Task GetFromTrakt(string url, TraktUser traktUser) + var url = seen ? TraktUris.SyncWatchedHistoryAdd : TraktUris.SyncWatchedHistoryRemove; + + using (var response = await PostToTrakt(url, data, traktUser, cancellationToken).ConfigureAwait(false)) { - return GetFromTrakt(url, CancellationToken.None, traktUser); + return await JsonSerializer.DeserializeAsync(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + } + } + + public async Task AuthorizeDevice(TraktUser traktUser) + { + var deviceCodeRequest = new + { + client_id = TraktUris.ClientId + }; + + TraktDeviceCode deviceCode; + using (var response = await PostToTrakt(TraktUris.DeviceCode, deviceCodeRequest, null).ConfigureAwait(false)) + { + deviceCode = await JsonSerializer.DeserializeAsync(response, _jsonOptions).ConfigureAwait(false); } - private async Task GetFromTrakt(string url, CancellationToken cancellationToken, TraktUser traktUser) + // Start polling in the background + Plugin.Instance.PollingTasks[traktUser.LinkedMbUserId] = Task.Run(() => PollForAccessToken(deviceCode, traktUser)); + + return deviceCode.UserCode; + } + + public async Task PollForAccessToken(TraktDeviceCode deviceCode, TraktUser traktUser) + { + var deviceAccessTokenRequest = new { - var httpClient = GetHttpClient(); + code = deviceCode.DeviceCode, + client_id = TraktUris.ClientId, + client_secret = TraktUris.ClientSecret + }; - if (traktUser != null) - { - await SetRequestHeaders(httpClient, traktUser).ConfigureAwait(false); - } - - await _traktResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - var response = await RetryHttpRequest(async () => await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); - return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - } - finally - { - _traktResourcePool.Release(); - } - } - - private async Task PostToTrakt(string url, object data) + var pollingInterval = deviceCode.Interval; + var expiresAt = DateTime.UtcNow.AddSeconds(deviceCode.ExpiresIn); + _logger.LogInformation("Polling for access token every {PollingInterval}s. Expires at {ExpiresAt} UTC.", pollingInterval, expiresAt); + while (DateTime.UtcNow < expiresAt) { - var httpClient = GetHttpClient(); - - var bytes = JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions); - var content = new ByteArrayContent(bytes); - content.Headers.Add(HeaderNames.ContentType, MediaTypeNames.Application.Json); - - await _traktResourcePool.WaitAsync().ConfigureAwait(false); - - try + using (var response = await PostToTrakt(TraktUris.DeviceToken, deviceAccessTokenRequest).ConfigureAwait(false)) { - return await httpClient.PostAsync(url, content).ConfigureAwait(false); - } - finally - { - _traktResourcePool.Release(); - } - } - - private Task PostToTrakt(string url, object data, TraktUser traktUser) - { - return PostToTrakt(url, data, CancellationToken.None, traktUser); - } - - /// - /// Posts data to url, authenticating with . - /// - /// If null, authentication headers not added. - private async Task PostToTrakt( - string url, - object data, - CancellationToken cancellationToken, - TraktUser traktUser) - { - if (traktUser != null && traktUser.ExtraLogging) - { - _logger.LogDebug("{@JsonData}", data); - } - - var httpClient = GetHttpClient(); - - if (traktUser != null) - { - await SetRequestHeaders(httpClient, traktUser).ConfigureAwait(false); - } - - var bytes = JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions); - var content = new ByteArrayContent(bytes); - content.Headers.Add(HeaderNames.ContentType, MediaTypeNames.Application.Json); - - await _traktResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - var response = await RetryHttpRequest(async () => await httpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); - return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - } - finally - { - _traktResourcePool.Release(); - } - } - - private async Task RetryHttpRequest(Func> function) - { - HttpResponseMessage response = null; - for (int i = 0; i < 3; i++) - { - try + switch (response.StatusCode) { - response = await function().ConfigureAwait(false); - if (response.StatusCode == (HttpStatusCode) 429) - { - var delay = response.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(1); - await Task.Delay(delay).ConfigureAwait(false); - } - else - { + case HttpStatusCode.BadRequest: + // Pending - waiting for the user to authorize your app break; - } - } catch {} - } - + case HttpStatusCode.NotFound: + _logger.LogError("Not Found - invalid device_code"); + break; + case HttpStatusCode.Conflict: + _logger.LogWarning("Already Used - user already approved this code"); + return false; + case HttpStatusCode.Gone: + _logger.LogError("Expired - the tokens have expired, restart the process"); + break; + case (HttpStatusCode)418: + _logger.LogInformation("Denied - user explicitly denied this code"); + return false; + case (HttpStatusCode)429: + _logger.LogWarning("Polling too quickly. Slowing down"); + pollingInterval += 1; + break; + case HttpStatusCode.OK: + _logger.LogInformation("Device successfully authorized"); - return response; - } + var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var userAccessToken = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false); + if (userAccessToken != null) + { + traktUser.AccessToken = userAccessToken.AccessToken; + traktUser.RefreshToken = userAccessToken.RefreshToken; + traktUser.AccessTokenExpiration = DateTime.Now.AddSeconds(userAccessToken.ExpirationWithBuffer); + Plugin.Instance.SaveConfiguration(); + return true; + } - private HttpClient GetHttpClient() - { - var client = _httpClientFactory.CreateClient(NamedClient.Default); - client.DefaultRequestHeaders.Add("trakt-api-version", "2"); - client.DefaultRequestHeaders.Add("trakt-api-key", TraktUris.ClientId); - return client; - } - - private async Task SetRequestHeaders(HttpClient httpClient, TraktUser traktUser) - { - if (DateTimeOffset.Now > traktUser.AccessTokenExpiration) - { - traktUser.AccessToken = string.Empty; - await RefreshUserAccessToken(traktUser).ConfigureAwait(false); + break; + } } - if (!string.IsNullOrEmpty(traktUser.AccessToken)) + await Task.Delay(pollingInterval * 1000).ConfigureAwait(false); + } + + return false; + } + + public async Task RefreshUserAccessToken(TraktUser traktUser) + { + if (string.IsNullOrWhiteSpace(traktUser.RefreshToken)) + { + _logger.LogError("Tried to reauthenticate with Trakt, but no refreshToken was available"); + return; + } + + var data = new TraktUserRefreshTokenRequest + { + ClientId = TraktUris.ClientId, + ClientSecret = TraktUris.ClientSecret, + RedirectUri = "urn:ietf:wg:oauth:2.0:oob", + RefreshToken = traktUser.RefreshToken, + GrantType = "refresh_token" + }; + + TraktUserAccessToken userAccessToken; + try + { + using (var response = await PostToTrakt(TraktUris.AccessToken, data).ConfigureAwait(false)) + { +#pragma warning disable CA2007 + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); +#pragma warning restore CA2007 + userAccessToken = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false); + } + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "An error occurred during token refresh"); + return; + } + + if (userAccessToken != null) + { + traktUser.AccessToken = userAccessToken.AccessToken; + traktUser.RefreshToken = userAccessToken.RefreshToken; + traktUser.AccessTokenExpiration = DateTime.Now.AddSeconds(userAccessToken.ExpirationWithBuffer); + Plugin.Instance.SaveConfiguration(); + _logger.LogInformation("Successfully refreshed the access token for user {UserId}", traktUser.LinkedMbUserId); + } + } + + private Task GetFromTrakt(string url, TraktUser traktUser) + { + return GetFromTrakt(url, traktUser, CancellationToken.None); + } + + private async Task GetFromTrakt(string url, TraktUser traktUser, CancellationToken cancellationToken) + { + var httpClient = GetHttpClient(); + + if (traktUser != null) + { + await SetRequestHeaders(httpClient, traktUser).ConfigureAwait(false); + } + + await _traktResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var response = await RetryHttpRequest(async () => await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _traktResourcePool.Release(); + } + } + + private async Task PostToTrakt(string url, object data) + { + var httpClient = GetHttpClient(); + + var bytes = JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions); + var content = new ByteArrayContent(bytes); + content.Headers.Add(HeaderNames.ContentType, MediaTypeNames.Application.Json); + + await _traktResourcePool.WaitAsync().ConfigureAwait(false); + + try + { + return await httpClient.PostAsync(url, content).ConfigureAwait(false); + } + finally + { + _traktResourcePool.Release(); + } + } + + private Task PostToTrakt(string url, object data, TraktUser traktUser) + { + return PostToTrakt(url, data, traktUser, CancellationToken.None); + } + + /// + /// Posts data to url, authenticating with . + /// + /// If null, authentication headers not added. + private async Task PostToTrakt( + string url, + object data, + TraktUser traktUser, + CancellationToken cancellationToken) + { + if (traktUser != null && traktUser.ExtraLogging) + { + _logger.LogDebug("{@JsonData}", data); + } + + var httpClient = GetHttpClient(); + + if (traktUser != null) + { + await SetRequestHeaders(httpClient, traktUser).ConfigureAwait(false); + } + + var bytes = JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions); + var content = new ByteArrayContent(bytes); + content.Headers.Add(HeaderNames.ContentType, MediaTypeNames.Application.Json); + + await _traktResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var response = await RetryHttpRequest(async () => await httpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _traktResourcePool.Release(); + } + } + + private async Task RetryHttpRequest(Func> function) + { + HttpResponseMessage response = null; + for (int i = 0; i < 3; i++) + { + try + { + response = await function().ConfigureAwait(false); + if (response.StatusCode == (HttpStatusCode)429) + { + var delay = response.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(1); + await Task.Delay(delay).ConfigureAwait(false); + } + else + { + break; + } + } + catch (Exception) { - httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + traktUser.AccessToken); } } - private static TReturn GetTraktIMDBTMDBIds(TInput mediaObject) - where TInput : IHasProviderIds where TReturn : TraktIMDBandTMDBId, new() + return response; + } + + private HttpClient GetHttpClient() + { + var client = _httpClientFactory.CreateClient(NamedClient.Default); + client.DefaultRequestHeaders.Add("trakt-api-version", "2"); + client.DefaultRequestHeaders.Add("trakt-api-key", TraktUris.ClientId); + return client; + } + + private async Task SetRequestHeaders(HttpClient httpClient, TraktUser traktUser) + { + if (DateTimeOffset.Now > traktUser.AccessTokenExpiration) { - return new TReturn - { - imdb = mediaObject.GetProviderId(MetadataProvider.Imdb), - tmdb = mediaObject.GetProviderId(MetadataProvider.Tmdb).ConvertToInt() - }; + traktUser.AccessToken = string.Empty; + await RefreshUserAccessToken(traktUser).ConfigureAwait(false); } - private static TReturn GetTraktTVIds(TInput mediaObject) - where TInput : IHasProviderIds where TReturn : TraktTVId, new() + if (!string.IsNullOrEmpty(traktUser.AccessToken)) { - TReturn retval = GetTraktIMDBTMDBIds(mediaObject); - retval.tvdb = mediaObject.GetProviderId(MetadataProvider.Tvdb).ConvertToInt(); - retval.tvrage = mediaObject.GetProviderId(MetadataProvider.TvRage).ConvertToInt(); - return retval; + httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + traktUser.AccessToken); } + } - private static TTraktShow findShow(List shows, Series series) - where TTraktShow : TraktShow + private static TReturn GetTraktIMDBTMDBIds(TInput mediaObject) + where TInput : IHasProviderIds + where TReturn : TraktIMDBandTMDBId, new() + { + return new TReturn { - return shows.FirstOrDefault( - sre => sre.ids != null && - ( - sre.ids.imdb == series.GetProviderId(MetadataProvider.Imdb) && - sre.ids.tmdb == series.GetProviderId(MetadataProvider.Tmdb).ConvertToInt() && - sre.ids.tvdb == series.GetProviderId(MetadataProvider.Tvdb).ConvertToInt() && - sre.ids.tvrage == series.GetProviderId(MetadataProvider.TvRage).ConvertToInt() - )); - } + Imdb = mediaObject.GetProviderId(MetadataProvider.Imdb), + Tmdb = mediaObject.GetProviderId(MetadataProvider.Tmdb).ConvertToInt() + }; + } + private static TReturn GetTraktTvIds(TInput mediaObject) + where TInput : IHasProviderIds + where TReturn : TraktTVId, new() + { + TReturn retval = GetTraktIMDBTMDBIds(mediaObject); + retval.Tvdb = mediaObject.GetProviderId(MetadataProvider.Tvdb).ConvertToInt(); + retval.Tvrage = mediaObject.GetProviderId(MetadataProvider.TvRage).ConvertToInt(); + return retval; + } + + private static TTraktShow FindShow(List shows, Series series) + where TTraktShow : TraktShow + { + return shows.FirstOrDefault( + sre => sre.Ids != null && sre.Ids.Imdb == series.GetProviderId(MetadataProvider.Imdb) && sre.Ids.Tmdb == series.GetProviderId(MetadataProvider.Tmdb).ConvertToInt() && sre.Ids.Tvdb == series.GetProviderId(MetadataProvider.Tvdb).ConvertToInt() && sre.Ids.Tvrage == series.GetProviderId(MetadataProvider.TvRage).ConvertToInt()); } } diff --git a/Trakt/Api/TraktController.cs b/Trakt/Api/TraktController.cs index fdf6d6f..180391d 100644 --- a/Trakt/Api/TraktController.cs +++ b/Trakt/Api/TraktController.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Net.Mime; using System.Net.Http; +using System.Net.Mime; using System.Threading.Tasks; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; @@ -14,145 +13,145 @@ using Trakt.Api.DataContracts.BaseModel; using Trakt.Api.DataContracts.Sync; using Trakt.Helpers; -namespace Trakt.Api +namespace Trakt.Api; + +/// +/// The Trakt.tv controller. +/// +[ApiController] +[Route("[controller]")] +[Produces(MediaTypeNames.Application.Json)] +public class TraktController : ControllerBase { + private readonly TraktApi _traktApi; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + /// - /// The Trakt.tv controller. + /// Initializes a new instance of the class. /// - [ApiController] - [Route("[controller]")] - [Produces(MediaTypeNames.Application.Json)] - public class TraktController : ControllerBase + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public TraktController( + IUserDataManager userDataManager, + ILoggerFactory loggerFactory, + IHttpClientFactory httpClientFactory, + IServerApplicationHost appHost, + IFileSystem fileSystem, + ILibraryManager libraryManager) { - private readonly TraktApi _traktApi; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + _logger = loggerFactory.CreateLogger(); + _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); + _libraryManager = libraryManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public TraktController( - IUserDataManager userDataManager, - ILoggerFactory loggerFactory, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - IFileSystem fileSystem, - ILibraryManager libraryManager) + /// + /// Authorize this server with trakt. + /// + /// The user id of the user connecting to trakt. + /// Authorization code requested successfully. + /// The trakt authorization code. + [HttpPost("Users/{userId}/Authorize")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> 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) { - _logger = loggerFactory.CreateLogger(); - _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); - _libraryManager = libraryManager; + Plugin.Instance.PluginConfiguration.AddUser(userId); + traktUser = UserHelper.GetTraktUser(userId); + Plugin.Instance.SaveConfiguration(); } - /// - /// Authorize this server with trakt. - /// - /// The user id of the user connecting to trakt. - /// Authorization code requested successfully. - /// The trakt authorization code. - [HttpPost("Users/{userId}/Authorize")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> TraktDeviceAuthorization([FromRoute] string userId) + string userCode = await _traktApi.AuthorizeDevice(traktUser).ConfigureAwait(false); + + return new { - _logger.LogInformation("TraktDeviceAuthorization request received"); + userCode + }; + } - // 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(); - } - string userCode = await _traktApi.AuthorizeDevice(traktUser).ConfigureAwait(false); + /// + /// Poll the trakt device authorization status + /// + /// The user id. + /// Polling successful. + /// A value indicating whether the authorization code was connected to a trakt account. + [HttpGet("Users/{userId}/PollAuthorizationStatus")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult TraktPollAuthorizationStatus([FromRoute] string userId) + { + _logger.LogInformation("TraktPollAuthorizationStatus request received"); + var traktUser = UserHelper.GetTraktUser(userId); + bool isAuthorized = traktUser.AccessToken != null && traktUser.RefreshToken != null; - return new - { - userCode - }; + if (Plugin.Instance.PollingTasks.TryGetValue(userId, out var task)) + { + isAuthorized = task.Result; + Plugin.Instance.PollingTasks.Remove(userId); } - /// - /// Poll the trakt device authorization status - /// - /// The user id. - /// Polling successful. - /// A value indicating whether the authorization code was connected to a trakt account. - [HttpGet("Users/{userId}/PollAuthorizationStatus")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult TraktPollAuthorizationStatus([FromRoute] string userId) + return new { - _logger.LogInformation("TraktPollAuthorizationStatus request received"); - var traktUser = UserHelper.GetTraktUser(userId); - bool isAuthorized = traktUser.AccessToken != null && traktUser.RefreshToken != null; + isAuthorized + }; + } - if (Plugin.Instance.PollingTasks.TryGetValue(userId, out var task)) - { - isAuthorized = task.Result; - Plugin.Instance.PollingTasks.Remove(userId); - } + /// + /// Rate an item. + /// + /// The user id. + /// The item id. + /// Rating between 1 - 10 (0 = unrate). + /// Item rated successfully. + /// A . + [HttpPost("Users/{userId}/Items/{itemId}/Rate")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> TraktRateItem([FromRoute] string userId, [FromRoute] Guid itemId, [FromQuery] int rating) + { + _logger.LogInformation("RateItem request received"); - return new - { - isAuthorized - }; + var currentItem = _libraryManager.GetItemById(itemId); + + if (currentItem == null) + { + _logger.LogInformation("currentItem is null"); + return null; } - /// - /// Rate an item. - /// - /// The user id. - /// The item id. - /// Rating between 1 - 10 (0 = unrate). - /// Item rated successfully. - /// A . - [HttpPost("Users/{userId}/Items/{itemId}/Rate")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> TraktRateItem([FromRoute] string userId, [FromRoute] Guid itemId, [FromQuery] int rating) - { - _logger.LogInformation("RateItem request received"); + return await _traktApi.SendItemRating(currentItem, rating, UserHelper.GetTraktUser(userId)).ConfigureAwait(false); + } - var currentItem = _libraryManager.GetItemById(itemId); + /// + /// Get recommended trakt movies. + /// + /// The user id. + /// Recommended movies returned. + /// A with recommended movies. + [HttpPost("Users/{userId}/RecommendedMovies")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> RecommendedTraktMovies([FromRoute] string userId) + { + return await _traktApi.SendMovieRecommendationsRequest(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); - } - - /// - /// Get recommended trakt movies. - /// - /// The user id. - /// Recommended movies returned. - /// A with recommended movies. - [HttpPost("Users/{userId}/RecommendedMovies")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> RecommendedTraktMovies([FromRoute] string userId) - { - return await _traktApi.SendMovieRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false); - } - - /// - /// Get recommended trakt shows. - /// - /// The user id. - /// Recommended shows returned. - /// A with recommended movies. - [HttpPost("Users/{userId}/RecommendedShows")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> RecommendedTraktShows([FromRoute] string userId) - { - return await _traktApi.SendShowRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false); - } + /// + /// Get recommended trakt shows. + /// + /// The user id. + /// Recommended shows returned. + /// A with recommended movies. + [HttpPost("Users/{userId}/RecommendedShows")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> RecommendedTraktShows([FromRoute] string userId) + { + return await _traktApi.SendShowRecommendationsRequest(UserHelper.GetTraktUser(userId)).ConfigureAwait(false); } } diff --git a/Trakt/Api/TraktURIs.cs b/Trakt/Api/TraktURIs.cs index c8b9b13..047f630 100644 --- a/Trakt/Api/TraktURIs.cs +++ b/Trakt/Api/TraktURIs.cs @@ -1,36 +1,35 @@ -namespace Trakt.Api +namespace Trakt.Api; + +public static class TraktUris { - public static class TraktUris - { - public const string BaseUrl = "https://api.trakt.tv"; - public const string ClientId = "58f2251f1c9e7275e94fef723a8604e6848bbf86a0d97dda82382a6c3231608c"; - public const string ClientSecret = "bf9fce37cf45c1de91da009e7ac6fca905a35d7a718bf65a52f92199073a2503"; + public const string BaseUrl = "https://api.trakt.tv"; + public const string ClientId = "58f2251f1c9e7275e94fef723a8604e6848bbf86a0d97dda82382a6c3231608c"; + public const string ClientSecret = "bf9fce37cf45c1de91da009e7ac6fca905a35d7a718bf65a52f92199073a2503"; - public const string DeviceCode = BaseUrl + "/oauth/device/code"; - public const string DeviceToken = BaseUrl + "/oauth/device/token"; - public const string AccessToken = BaseUrl + "/oauth/token"; + public const string DeviceCode = BaseUrl + "/oauth/device/code"; + public const string DeviceToken = BaseUrl + "/oauth/device/token"; + public const string AccessToken = BaseUrl + "/oauth/token"; - 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"; + 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"; - public const string ScrobbleStart = BaseUrl + "/scrobble/start"; - public const string ScrobblePause = BaseUrl + "/scrobble/pause"; - public const string ScrobbleStop = BaseUrl + "/scrobble/stop"; + public const string ScrobbleStart = BaseUrl + "/scrobble/start"; + public const string ScrobblePause = BaseUrl + "/scrobble/pause"; + public const string ScrobbleStop = BaseUrl + "/scrobble/stop"; - 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"; + 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"; - // Recommendations - public const string RecommendationsMovies = BaseUrl + "/recommendations/movies"; - public const string RecommendationsShows = BaseUrl + "/recommendations/shows"; + // Recommendations + public const string RecommendationsMovies = BaseUrl + "/recommendations/movies"; + public const string RecommendationsShows = BaseUrl + "/recommendations/shows"; - // Recommendations - public const string RecommendationsMoviesDismiss = BaseUrl + "/recommendations/movies/{0}"; - public const string RecommendationsShowsDismiss = BaseUrl + "/recommendations/shows/{0}"; - } + // Recommendations + public const string RecommendationsMoviesDismiss = BaseUrl + "/recommendations/movies/{0}"; + public const string RecommendationsShowsDismiss = BaseUrl + "/recommendations/shows/{0}"; } diff --git a/Trakt/Configuration/PluginConfiguration.cs b/Trakt/Configuration/PluginConfiguration.cs index 491776f..2fac498 100644 --- a/Trakt/Configuration/PluginConfiguration.cs +++ b/Trakt/Configuration/PluginConfiguration.cs @@ -1,28 +1,29 @@ -using System; +#pragma warning disable CA1819 + +using System; using System.Linq; using MediaBrowser.Model.Plugins; using Trakt.Model; -namespace Trakt.Configuration +namespace Trakt.Configuration; + +public class PluginConfiguration : BasePluginConfiguration { - public class PluginConfiguration : BasePluginConfiguration + public PluginConfiguration() { - public PluginConfiguration() - { - TraktUsers = Array.Empty(); - } + TraktUsers = Array.Empty(); + } - public TraktUser[] TraktUsers { get; set; } + public TraktUser[] TraktUsers { get; set; } - public void AddUser(string userId) + public void AddUser(string userId) + { + var traktUsers = TraktUsers.ToList(); + var traktUser = new TraktUser { - var traktUsers = TraktUsers.ToList(); - var traktUser = new TraktUser - { - LinkedMbUserId = userId - }; - traktUsers.Add(traktUser); - TraktUsers = traktUsers.ToArray(); - } + LinkedMbUserId = userId + }; + traktUsers.Add(traktUser); + TraktUsers = traktUsers.ToArray(); } } diff --git a/Trakt/Enums.cs b/Trakt/Enums.cs deleted file mode 100644 index fce00c9..0000000 --- a/Trakt/Enums.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Trakt -{ - public enum MediaStatus - { - Watching, - Paused, - Stop - } -} diff --git a/Trakt/Extensions.cs b/Trakt/Extensions.cs index 8401051..73c7de5 100644 --- a/Trakt/Extensions.cs +++ b/Trakt/Extensions.cs @@ -8,191 +8,192 @@ using MediaBrowser.Model.Entities; using Trakt.Api.DataContracts.Users.Collection; using Trakt.Helpers; -namespace Trakt +namespace Trakt; + +public static class Extensions { - public static class Extensions + public static int? ConvertToInt(this string input) { - public static int? ConvertToInt(this string input) + if (int.TryParse(input, out int result)) { - if (int.TryParse(input, out int result)) - { - return result; - } - - return null; + return result; } - public static bool IsEmpty(this TraktMetadata metadata) - => string.IsNullOrEmpty(metadata.media_type) - && string.IsNullOrEmpty(metadata.resolution) - && string.IsNullOrEmpty(metadata.audio) - && string.IsNullOrEmpty(metadata.audio_channels); + return null; + } - public static string GetCodecRepresetation(this MediaStream audioStream) + 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) { - var audio = audioStream != null && !string.IsNullOrEmpty(audioStream.Codec) - ? audioStream.Codec.ToLowerInvariant().Replace(" ", "_") - : null; - switch (audio) - { - 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: - return null; - } - } - - 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.audio_channels != audioChannels - || collectedMovie.metadata.resolution != resolution; - } - - public static string GetResolution(this MediaStream videoStream) - { - if (videoStream == null) - { + 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: return null; - } - - if (!videoStream.Width.HasValue) - { - return null; - } - - if (videoStream.Width.Value >= 3800) - { - return "uhd_4k"; - } - - if (videoStream.Width.Value >= 1900) - { - return "hd_1080p"; - } - - if (videoStream.Width.Value >= 1270) - { - 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; - } - } - - public static IList> ToChunks(this IEnumerable enumerable, int chunkSize) - { - var itemsReturned = 0; - var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable. - var count = list.Count; - var chunks = new List>(); - while (itemsReturned < count) - { - chunks.Add(list.Take(chunkSize).ToList()); - list = list.Skip(chunkSize).ToList(); - itemsReturned += chunkSize; - } - - return chunks; - } - - public static ISplittableProgress Split(this IProgress parent, int parts) - { - var current = parent.ToSplittableProgress(); - return current.Split(parts); - } - - public static ISplittableProgress ToSplittableProgress(this IProgress progress) - { - var splittable = new SplittableProgress(progress.Report); - return splittable; - } - - public enum TraktAudio - { - lpcm, - mp3, - aac, - dts, - dts_ma, - flac, - ogg, - wma, - dolby_prologic, - dolby_digital, - dolby_digital_plus, - dolby_truehd } } + + 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; + } + + public static string GetResolution(this MediaStream videoStream) + { + if (videoStream == null) + { + return null; + } + + if (!videoStream.Width.HasValue) + { + return null; + } + + if (videoStream.Width.Value >= 3800) + { + return "uhd_4k"; + } + + if (videoStream.Width.Value >= 1900) + { + return "hd_1080p"; + } + + if (videoStream.Width.Value >= 1270) + { + 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; + } + } + + public static IList> ToChunks(this IEnumerable enumerable, int chunkSize) + { + var itemsReturned = 0; + var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable. + var count = list.Count; + var chunks = new List>(); + while (itemsReturned < count) + { + chunks.Add(list.Take(chunkSize).ToList()); + list = list.Skip(chunkSize).ToList(); + itemsReturned += chunkSize; + } + + return chunks; + } + + public static ISplittableProgress Split(this IProgress parent, int parts) + { + var current = parent.ToSplittableProgress(); + return current.Split(parts); + } + + public static ISplittableProgress ToSplittableProgress(this IProgress 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 } diff --git a/Trakt/Helpers/EventType.cs b/Trakt/Helpers/EventType.cs new file mode 100644 index 0000000..c5c5a93 --- /dev/null +++ b/Trakt/Helpers/EventType.cs @@ -0,0 +1,8 @@ +namespace Trakt.Helpers; + +public enum EventType +{ + Add, + Remove, + Update +} diff --git a/Trakt/Helpers/ISplittableProgress.cs b/Trakt/Helpers/ISplittableProgress.cs index 3ec615c..1af808d 100644 --- a/Trakt/Helpers/ISplittableProgress.cs +++ b/Trakt/Helpers/ISplittableProgress.cs @@ -1,13 +1,12 @@ -namespace Trakt.Helpers -{ - using System; +namespace Trakt.Helpers; - /// - /// Similar to , but it contains a split method and Report is relative, not absolute. - /// - /// The type of progress update value - public interface ISplittableProgress : IProgress - { - ISplittableProgress Split(int parts); - } +using System; + +/// +/// Similar to , but it contains a split method and Report is relative, not absolute. +/// +/// The type of progress update value +public interface ISplittableProgress : IProgress +{ + ISplittableProgress Split(int parts); } diff --git a/Trakt/Helpers/LibraryEvent.cs b/Trakt/Helpers/LibraryEvent.cs new file mode 100644 index 0000000..dcd71eb --- /dev/null +++ b/Trakt/Helpers/LibraryEvent.cs @@ -0,0 +1,13 @@ +using MediaBrowser.Controller.Entities; +using Trakt.Model; + +namespace Trakt.Helpers; + +internal class LibraryEvent +{ + public BaseItem Item { get; set; } + + public TraktUser TraktUser { get; set; } + + public EventType EventType { get; set; } +} diff --git a/Trakt/Helpers/LibraryManagerEventsHelper.cs b/Trakt/Helpers/LibraryManagerEventsHelper.cs index 8891c1a..fe0ae62 100644 --- a/Trakt/Helpers/LibraryManagerEventsHelper.cs +++ b/Trakt/Helpers/LibraryManagerEventsHelper.cs @@ -11,284 +11,283 @@ using Microsoft.Extensions.Logging; using Trakt.Api; using Trakt.Model; -namespace Trakt.Helpers -{ - internal class LibraryManagerEventsHelper - { - private readonly List _queuedEvents; - private Timer _queueTimer; - private readonly ILogger _logger; - private readonly TraktApi _traktApi; +namespace Trakt.Helpers; - /// - /// - /// - /// - /// - public LibraryManagerEventsHelper(ILogger logger, TraktApi traktApi) +internal class LibraryManagerEventsHelper : IDisposable +{ + private readonly List _queuedEvents; + private readonly ILogger _logger; + private readonly TraktApi _traktApi; + private Timer _queueTimer; + + /// + /// + /// + /// + /// + public LibraryManagerEventsHelper(ILogger logger, TraktApi traktApi) + { + _queuedEvents = new List(); + _logger = logger; + _traktApi = traktApi; + } + + /// + /// + /// + /// + /// + public void QueueItem(BaseItem item, EventType eventType) + { + if (item == null) { - _queuedEvents = new List(); - _logger = logger; - _traktApi = traktApi; + throw new ArgumentNullException(nameof(item)); } - /// - /// - /// - /// - /// - public void QueueItem(BaseItem item, EventType eventType) + if (_queueTimer == null) { - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } + _queueTimer = new Timer( + OnQueueTimerCallback, + null, + TimeSpan.FromMilliseconds(20000), + Timeout.InfiniteTimeSpan); + } + else + { + _queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan); + } - if (_queueTimer == null) + 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); + } + } + + /// + /// + /// + 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()) { - _queueTimer = new Timer( - OnQueueTimerCallback, - null, - TimeSpan.FromMilliseconds(20000), - Timeout.InfiniteTimeSpan); + _logger.LogInformation("{Count} Movie Deletes to Process", queuedMovieDeletes.Count); + await ProcessQueuedMovieEvents(queuedMovieDeletes, traktUser, EventType.Remove).ConfigureAwait(false); } else { - _queueTimer.Change(TimeSpan.FromMilliseconds(20000), Timeout.InfiniteTimeSpan); + _logger.LogInformation("No Movie Deletes to Process"); } - var users = Plugin.Instance.PluginConfiguration.TraktUsers; + 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 (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))) + if (queuedMovieAdds.Any()) { - // 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); + _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"); + } + + 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 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"); } } - /// - /// - /// - private async void OnQueueTimerCallback(object state) + // Everything is processed. Reset the event list. + _queuedEvents.Clear(); + } + + private async Task ProcessQueuedShowEvents(IEnumerable 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"); + } + } + + /// + /// + /// + /// + /// + /// + /// + private async Task ProcessQueuedMovieEvents(IEnumerable 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"); + } + } + + /// + /// + /// + /// + /// + /// + /// + private async Task ProcessQueuedEpisodeEvents(IEnumerable 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(); + 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 OnQueueTimerCallbackInternal().ConfigureAwait(false); + await _traktApi.SendLibraryUpdateAsync(payload, traktUser, eventType, CancellationToken.None).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()) - { - _logger.LogInformation(queuedMovieDeletes.Count + " Movie Deletes to Process"); - await ProcessQueuedMovieEvents(queuedMovieDeletes, traktUser, EventType.Remove).ConfigureAwait(false); - } - else - { - _logger.LogInformation("No Movie Deletes 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(queuedMovieAdds.Count + " Movie Adds to Process"); - await ProcessQueuedMovieEvents(queuedMovieAdds, traktUser, EventType.Add).ConfigureAwait(false); - } - else - { - _logger.LogInformation("No Movie Adds 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(queuedEpisodeDeletes.Count + " Episode Deletes to Process"); - 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(queuedEpisodeAdds.Count + " Episode Adds to Process"); - 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(queuedMovieDeletes.Count + " Series Deletes to Process"); - await ProcessQueuedShowEvents(queuedShowDeletes, traktUser, EventType.Remove).ConfigureAwait(false); - } - else - { - _logger.LogInformation("No Series Deletes to Process"); - } - } - - // Everything is processed. Reset the event list. - _queuedEvents.Clear(); - } - - private async Task ProcessQueuedShowEvents(IEnumerable 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, CancellationToken.None, eventType).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception handled processing queued series events"); - } - } - - /// - /// - /// - /// - /// - /// - /// - private async Task ProcessQueuedMovieEvents(IEnumerable 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, CancellationToken.None, eventType).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception handled processing queued movie events"); - } - - } - - /// - /// - /// - /// - /// - /// - /// - private async Task ProcessQueuedEpisodeEvents(IEnumerable 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(); - 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, CancellationToken.None, eventType).ConfigureAwait(false); - - currentSeriesId = ep.Series.Id; - payload.Clear(); - } - - payload.Add(ep); - } - - if (payload.Any()) - { - try - { - await _traktApi.SendLibraryUpdateAsync(payload, traktUser, CancellationToken.None, eventType).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception handled processing queued episode events"); - } + _logger.LogError(ex, "Exception handled processing queued episode events"); } } } - internal class LibraryEvent + public void Dispose() { - public BaseItem Item { get; set; } - - public TraktUser TraktUser { get; set; } - - public EventType EventType { get; set; } + Dispose(true); + GC.SuppressFinalize(this); } - public enum EventType + protected virtual void Dispose(bool disposing) { - Add, - Remove, - Update + if (disposing) + { + _queueTimer?.Dispose(); + } } } diff --git a/Trakt/Helpers/SplittableProgress.cs b/Trakt/Helpers/SplittableProgress.cs index d2821bf..9f26c8d 100644 --- a/Trakt/Helpers/SplittableProgress.cs +++ b/Trakt/Helpers/SplittableProgress.cs @@ -1,29 +1,28 @@ using System; -namespace Trakt.Helpers +namespace Trakt.Helpers; + +/// +/// Similar to , but report is relative, not absolute. +/// +/// Can't be generic, because it's impossible to do arithmetics on generics +public class SplittableProgress : Progress, ISplittableProgress { - /// - /// Similar to , but report is relative, not absolute. - /// - /// Can't be generic, because it's impossible to do arithmetics on generics - public class SplittableProgress : Progress, ISplittableProgress + public SplittableProgress(Action handler) + : base(handler) { - public SplittableProgress(Action handler) - : base(handler) - { - } + } - private double Progress { get; set; } + private double Progress { get; set; } - ISplittableProgress ISplittableProgress.Split(int parts) - { - var child = new SplittableProgress( - d => - { - Progress += d / parts; - OnReport(Progress); - }); - return child; - } + public ISplittableProgress Split(int parts) + { + var child = new SplittableProgress( + d => + { + Progress += d / parts; + OnReport(Progress); + }); + return child; } } diff --git a/Trakt/Helpers/UserDataManagerEventsHelper.cs b/Trakt/Helpers/UserDataManagerEventsHelper.cs index 27d84f8..70eb91e 100644 --- a/Trakt/Helpers/UserDataManagerEventsHelper.cs +++ b/Trakt/Helpers/UserDataManagerEventsHelper.cs @@ -9,220 +9,207 @@ using Microsoft.Extensions.Logging; using Trakt.Api; using Trakt.Model; -namespace Trakt.Helpers -{ - /// - /// Helper class used to update the watched status of movies/episodes. Attempts to organise - /// requests to lower trakt.tv api calls. - /// - internal class UserDataManagerEventsHelper - { - private List _userDataPackages; - private readonly ILogger _logger; - private readonly TraktApi _traktApi; - private Timer _timer; +namespace Trakt.Helpers; - /// - /// - /// - /// - /// - public UserDataManagerEventsHelper(ILogger logger, TraktApi traktApi) +/// +/// Helper class used to update the watched status of movies/episodes. Attempts to organise +/// requests to lower trakt.tv api calls. +/// +internal class UserDataManagerEventsHelper : IDisposable +{ + private readonly ILogger _logger; + private readonly TraktApi _traktApi; + private readonly List _userDataPackages; + private Timer _timer; + + /// + /// + /// + /// + /// + public UserDataManagerEventsHelper(ILogger logger, TraktApi traktApi) + { + _userDataPackages = new List(); + _logger = logger; + _traktApi = traktApi; + } + + /// + /// + /// + /// + /// + public void ProcessUserDataSaveEventArgs(UserDataSaveEventArgs userDataSaveEventArgs, TraktUser traktUser) + { + var userPackage = _userDataPackages.FirstOrDefault(e => e.TraktUser.Equals(traktUser)); + + if (userPackage == null) { - _userDataPackages = new List(); - _logger = logger; - _traktApi = traktApi; + userPackage = new UserDataPackage { TraktUser = traktUser }; + _userDataPackages.Add(userPackage); } - /// - /// - /// - /// - /// - public void ProcessUserDataSaveEventArgs(UserDataSaveEventArgs userDataSaveEventArgs, TraktUser traktUser) + if (_timer == null) { - var userPackage = _userDataPackages.FirstOrDefault(e => e.TraktUser.Equals(traktUser)); - - if (userPackage == null) - { - userPackage = new UserDataPackage { TraktUser = traktUser }; - _userDataPackages.Add(userPackage); - } - - - if (_timer == null) - { - _timer = new Timer( - OnTimerCallback, - null, - TimeSpan.FromMilliseconds(5000), - 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); - } - - if (userPackage.SeenMovies.Count >= 100) - { - _traktApi.SendMoviePlaystateUpdates( - userPackage.SeenMovies, - userPackage.TraktUser, - true, - CancellationToken.None).ConfigureAwait(false); - userPackage.SeenMovies = new List(); - } - } - else - { - if(traktUser.PostSetUnwatched) - { - userPackage.UnSeenMovies.Add(movie); - } - - if (userPackage.UnSeenMovies.Count >= 100) - { - _traktApi.SendMoviePlaystateUpdates( - userPackage.UnSeenMovies, - userPackage.TraktUser, - false, - CancellationToken.None).ConfigureAwait(false); - userPackage.UnSeenMovies = new List(); - } - } - - 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 = new List(); - } - - if (userPackage.UnSeenEpisodes.Any()) - { - _traktApi.SendEpisodePlaystateUpdates( - userPackage.UnSeenEpisodes, - userPackage.TraktUser, - false, - CancellationToken.None).ConfigureAwait(false); - userPackage.UnSeenEpisodes = new List(); - } - - userPackage.CurrentSeriesId = episode.Series.Id; - } + _timer = new Timer( + OnTimerCallback, + null, + TimeSpan.FromMilliseconds(5000), + Timeout.InfiniteTimeSpan); + } + else + { + _timer.Change(TimeSpan.FromMilliseconds(5000), Timeout.InfiniteTimeSpan); + } + if (userDataSaveEventArgs.Item is Movie movie) + { if (userDataSaveEventArgs.UserData.Played) { if (traktUser.PostSetWatched) { - userPackage.SeenEpisodes.Add(episode); + userPackage.SeenMovies.Add(movie); + } + + if (userPackage.SeenMovies.Count >= 100) + { + _traktApi.SendMoviePlaystateUpdates( + userPackage.SeenMovies, + userPackage.TraktUser, + true, + CancellationToken.None).ConfigureAwait(false); + userPackage.SeenMovies = new List(); } } else { - if(traktUser.PostSetUnwatched) + if (traktUser.PostSetUnwatched) { - userPackage.UnSeenEpisodes.Add(episode); + userPackage.UnSeenMovies.Add(movie); + } + + if (userPackage.UnSeenMovies.Count >= 100) + { + _traktApi.SendMoviePlaystateUpdates( + userPackage.UnSeenMovies, + userPackage.TraktUser, + false, + CancellationToken.None).ConfigureAwait(false); + userPackage.UnSeenMovies = new List(); } } + + return; } - private void OnTimerCallback(object state) + if (!(userDataSaveEventArgs.Item is Episode episode)) { - 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); - } + return; + } - 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); - } + // 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 = new List(); + } + + if (userPackage.UnSeenEpisodes.Any()) + { + _traktApi.SendEpisodePlaystateUpdates( + userPackage.UnSeenEpisodes, + userPackage.TraktUser, + false, + CancellationToken.None).ConfigureAwait(false); + userPackage.UnSeenEpisodes = new List(); + } + + userPackage.CurrentSeriesId = episode.Series.Id; + } + + if (userDataSaveEventArgs.UserData.Played) + { + if (traktUser.PostSetWatched) + { + userPackage.SeenEpisodes.Add(episode); + } + } + else + { + if (traktUser.PostSetUnwatched) + { + userPackage.UnSeenEpisodes.Add(episode); } } } - /// - /// Class that contains all the items to be reported to trakt.tv and supporting properties. - /// - internal class UserDataPackage + private void OnTimerCallback(object state) { - public TraktUser TraktUser { get; set; } - - public Guid CurrentSeriesId { get; set; } - - public List SeenMovies { get; set; } - - public List UnSeenMovies { get; set; } - - public List SeenEpisodes { get; set; } - - public List UnSeenEpisodes { get; set; } - - public UserDataPackage() + foreach (var package in _userDataPackages) { - SeenMovies = new List(); - UnSeenMovies = new List(); - SeenEpisodes = new List(); - UnSeenEpisodes = new List(); + 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(); } } } diff --git a/Trakt/Helpers/UserDataPackage.cs b/Trakt/Helpers/UserDataPackage.cs new file mode 100644 index 0000000..6889ff1 --- /dev/null +++ b/Trakt/Helpers/UserDataPackage.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using Trakt.Model; + +namespace Trakt.Helpers; + +/// +/// Class that contains all the items to be reported to trakt.tv and supporting properties. +/// +internal class UserDataPackage +{ + public UserDataPackage() + { + SeenMovies = new List(); + UnSeenMovies = new List(); + SeenEpisodes = new List(); + UnSeenEpisodes = new List(); + } + + public TraktUser TraktUser { get; set; } + + public Guid CurrentSeriesId { get; set; } + + public List SeenMovies { get; set; } + + public List UnSeenMovies { get; set; } + + public List SeenEpisodes { get; set; } + + public List UnSeenEpisodes { get; set; } +} diff --git a/Trakt/Helpers/UserHelper.cs b/Trakt/Helpers/UserHelper.cs new file mode 100644 index 0000000..e632e6d --- /dev/null +++ b/Trakt/Helpers/UserHelper.cs @@ -0,0 +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 +{ + public static TraktUser GetTraktUser(User user) + { + 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) + { + return null; + } + + return Plugin.Instance.PluginConfiguration.TraktUsers.FirstOrDefault(tUser => + { + if (string.IsNullOrWhiteSpace(tUser.LinkedMbUserId)) + { + return false; + } + + if (Guid.TryParse(tUser.LinkedMbUserId, out Guid traktUserGuid) + && traktUserGuid.Equals(userGuid)) + { + return true; + } + + return false; + }); + } +} diff --git a/Trakt/Helpers/UserHelpers.cs b/Trakt/Helpers/UserHelpers.cs deleted file mode 100644 index af29c7a..0000000 --- a/Trakt/Helpers/UserHelpers.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Linq; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Entities; -using Trakt.Model; - -namespace Trakt.Helpers -{ - internal static class UserHelper - { - public static TraktUser GetTraktUser(User user) - { - 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) - { - return null; - } - - return Plugin.Instance.PluginConfiguration.TraktUsers.FirstOrDefault(tUser => - { - if (string.IsNullOrWhiteSpace(tUser.LinkedMbUserId)) - { - return false; - } - - if (Guid.TryParse(tUser.LinkedMbUserId, out Guid traktUserGuid) - && traktUserGuid.Equals(userGuid)) - { - return true; - } - - return false; - }); - } - } -} diff --git a/Trakt/MediaStatus.cs b/Trakt/MediaStatus.cs new file mode 100644 index 0000000..205b8d1 --- /dev/null +++ b/Trakt/MediaStatus.cs @@ -0,0 +1,8 @@ +namespace Trakt; + +public enum MediaStatus +{ + Watching, + Paused, + Stop +} diff --git a/Trakt/Model/TraktUser.cs b/Trakt/Model/TraktUser.cs index e6b6034..fff2e1c 100644 --- a/Trakt/Model/TraktUser.cs +++ b/Trakt/Model/TraktUser.cs @@ -1,53 +1,54 @@ -using System; +#pragma warning disable CA1819 -namespace Trakt.Model +using System; + +namespace Trakt.Model; + +public class TraktUser { - public class TraktUser + public TraktUser() { - 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; } - - public TraktUser() - { - SkipUnwatchedImportFromTrakt = true; - SkipWatchedImportFromTrakt = false; - PostWatchedHistory = true; - PostUnwatchedHistory = true; - PostSetWatched = true; - PostSetUnwatched = true; - ExtraLogging = false; - ExportMediaInfo = false; - SynchronizeCollections = true; - Scrobble = true; - } + SkipUnwatchedImportFromTrakt = true; + SkipWatchedImportFromTrakt = false; + PostWatchedHistory = true; + PostUnwatchedHistory = true; + PostSetWatched = true; + PostSetUnwatched = true; + ExtraLogging = false; + ExportMediaInfo = false; + SynchronizeCollections = true; + Scrobble = true; } + + 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; } } diff --git a/Trakt/Plugin.cs b/Trakt/Plugin.cs index e1cc96d..d55c833 100644 --- a/Trakt/Plugin.cs +++ b/Trakt/Plugin.cs @@ -7,49 +7,48 @@ using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using Trakt.Configuration; -namespace Trakt +namespace Trakt; + +public class Plugin : BasePlugin, IHasWebPages { - public class Plugin : BasePlugin, IHasWebPages + public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) + : base(appPaths, xmlSerializer) { - public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) - : base(appPaths, xmlSerializer) + Instance = this; + PollingTasks = new Dictionary>(); + } + + /// + public override string Name => "Trakt"; + + /// + public override Guid Id => new Guid("4fe3201e-d6ae-4f2e-8917-e12bda571281"); + + /// + 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> PollingTasks { get; } + + /// + public IEnumerable GetPages() + { + return new[] { - Instance = this; - PollingTasks = new Dictionary>(); - } - - /// - public override string Name => "Trakt"; - - /// - public override Guid Id => new Guid("4fe3201e-d6ae-4f2e-8917-e12bda571281"); - - /// - 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> PollingTasks { get; set; } - - /// - public IEnumerable GetPages() - { - return new[] + new PluginPageInfo { - new PluginPageInfo - { - Name = "trakt", - EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.html", - }, - new PluginPageInfo - { - Name = "traktjs", - EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.js" - } - }; - } + Name = "trakt", + EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.html", + }, + new PluginPageInfo + { + Name = "traktjs", + EmbeddedResourcePath = GetType().Namespace + ".Web.trakt.js" + } + }; } } diff --git a/Trakt/ScheduledTasks/SyncFromTraktTask.cs b/Trakt/ScheduledTasks/SyncFromTraktTask.cs index fdbc094..9fb919c 100644 --- a/Trakt/ScheduledTasks/SyncFromTraktTask.cs +++ b/Trakt/ScheduledTasks/SyncFromTraktTask.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Net.Http; +using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -15,398 +16,388 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Jellyfin.Data.Enums; using Microsoft.Extensions.Logging; using Trakt.Api; using Trakt.Api.DataContracts.BaseModel; using Trakt.Api.DataContracts.Users.Collection; using Trakt.Api.DataContracts.Users.Watched; using Trakt.Helpers; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; -namespace Trakt.ScheduledTasks +namespace Trakt.ScheduledTasks; + +/// +/// Task that will Sync each users trakt.tv profile with their local library. This task will only include +/// watched states. +/// +public class SyncFromTraktTask : IScheduledTask { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly TraktApi _traktApi; + /// - /// Task that will Sync each users trakt.tv profile with their local library. This task will only include - /// watched states. + /// /// - public class SyncFromTraktTask : IScheduledTask + /// + /// + /// + /// + /// + /// + public SyncFromTraktTask( + ILoggerFactory loggerFactory, + IUserManager userManager, + IUserDataManager userDataManager, + IHttpClientFactory httpClientFactory, + IServerApplicationHost appHost, + IFileSystem fileSystem, + ILibraryManager libraryManager) { - private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataManager; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly TraktApi _traktApi; + _userManager = userManager; + _userDataManager = userDataManager; + _libraryManager = libraryManager; + _logger = loggerFactory.CreateLogger(); + _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); + } - /// - /// - /// - /// - /// - /// - /// - /// - /// - public SyncFromTraktTask( - ILoggerFactory loggerFactory, - IUserManager userManager, - IUserDataManager userDataManager, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - IFileSystem fileSystem, - ILibraryManager libraryManager) + 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 GetDefaultTriggers() => Enumerable.Empty(); + + /// + /// Gather users and call + /// + public async Task Execute(CancellationToken cancellationToken, IProgress progress) + { + 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) { - _userManager = userManager; - _userDataManager = userDataManager; - _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(); - _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); + _logger.LogInformation("No Users returned"); + return; } - public string Key => "TraktSyncFromTraktTask"; + // purely for progress reporting + var percentPerUser = 100 / users.Count; + double currentProgress = 0; + var numComplete = 0; - public IEnumerable GetDefaultTriggers() => Enumerable.Empty(); - - 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"; - - /// - /// Gather users and call - /// - public async Task Execute(CancellationToken cancellationToken, IProgress progress) + foreach (var user in users) { - 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, cancellationToken, progress, percentPerUser).ConfigureAwait(false); - - numComplete++; - currentProgress = percentPerUser * numComplete; - progress.Report(currentProgress); - } - 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, CancellationToken cancellationToken, IProgress progress, double percentPerUser) - { - var traktUser = UserHelper.GetTraktUser(user); - - List traktWatchedMovies; - List 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); + await SyncTraktDataForUser(user, currentProgress, progress, percentPerUser, cancellationToken).ConfigureAwait(false); + + numComplete++; + currentProgress = percentPerUser * numComplete; + progress.Report(currentProgress); } catch (Exception ex) { - _logger.LogError(ex, "Exception handled"); - throw; + _logger.LogError(ex, "Error syncing trakt data for user {UserName}", user.Username); + } + } + } + + private async Task SyncTraktDataForUser(Jellyfin.Data.Entities.User user, double currentProgress, IProgress progress, double percentPerUser, CancellationToken cancellationToken) + { + var traktUser = UserHelper.GetTraktUser(user); + + List traktWatchedMovies; + List 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[] { nameof(Movie), nameof(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()) + { + 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.LogInformation("Trakt.tv watched Movies count = " + traktWatchedMovies.Count); - _logger.LogInformation("Trakt.tv watched Shows count = " + traktWatchedShows.Count); - - var mediaItems = - _libraryManager.GetItemList( - new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Episode).Name }, - IsVirtualItem = false, - OrderBy = new[] - { - new ValueTuple(ItemSortBy.SeriesSortName, SortOrder.Ascending), - new ValueTuple(ItemSortBy.SortName, SortOrder.Ascending) - } - }) - .Where(i => _traktApi.CanSync(i, traktUser)).ToList(); - // purely for progress reporting - var percentPerItem = percentPerUser / mediaItems.Count; + currentProgress += percentPerItem; + progress.Report(currentProgress); + } - foreach (var movie in mediaItems.OfType()) + foreach (var episode in mediaItems.OfType()) + { + cancellationToken.ThrowIfCancellationRequested(); + var matchedShow = FindMatch(episode.Series, traktWatchedShows); + + if (matchedShow != null) { - cancellationToken.ThrowIfCancellationRequested(); - var matchedMovie = FindMatch(movie, traktWatchedMovies); + var matchedSeason = + matchedShow.Seasons.FirstOrDefault( + tSeason => + tSeason.Number + == (episode.ParentIndexNumber == 0 + ? 0 + : episode.ParentIndexNumber ?? 1)); - if (matchedMovie != null) + // keep track of the shows rewatch cycles + DateTime? tLastReset = null; + if (DateTime.TryParse(matchedShow.ResetAt, out var resetValue)) { - _logger.LogDebug("Movie is in Watched list " + movie.Name); + tLastReset = resetValue; + } - var userData = _userDataManager.GetUserData(user.Id, movie); + // 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); bool changed = false; - DateTime? tLastPlayed = null; - if (DateTime.TryParse(matchedMovie.last_watched_at, out var value)) + 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) { - tLastPlayed = value; + if (DateTime.TryParse(matchedEpisode.LastWatchedAt, out var value) && value < tLastReset) + { + matchedEpisode = null; + } } - // set movie as watched - if (!userData.Played) + if (matchedEpisode != null) { - userData.Played = true; - userData.LastPlayedDate = tLastPlayed ?? DateTime.Now; + _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; } - // 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 + // only process if changed if (changed) { _userDataManager.SaveUserData( - user.Id, - movie, - userData, - UserDataSaveReason.Import, - cancellationToken); + user.Id, + episode, + userData, + UserDataSaveReason.Import, + cancellationToken); } } else { - //_logger.LogInformation("Failed to match " + movie.Name); + _logger.LogDebug("No Season match in Watched shows list {Episode}", GetVerboseEpisodeData(episode)); } - - // purely for progress reporting - currentProgress += percentPerItem; - progress.Report(currentProgress); } - - foreach (var episode in mediaItems.OfType()) + else { - 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.reset_at, out var resetValue)) - { - tLastReset = resetValue; - } - - // 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); - 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.last_watched_at, out var value) && value < tLastReset) - { - matchedEpisode = null; - } - } - - if (matchedEpisode != null) - { - _logger.LogDebug("Episode is in Watched list " + GetVerboseEpisodeData(episode)); - - if(!traktUser.SkipWatchedImportFromTrakt) - { - DateTime? tLastPlayed = null; - if (DateTime.TryParse(matchedEpisode.last_watched_at, 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 " + GetVerboseEpisodeData(episode)); - } - } - else - { - _logger.LogDebug("No Show match in Watched shows list " + GetVerboseEpisodeData(episode)); - } - - // purely for progress reporting - currentProgress += percentPerItem; - progress.Report(currentProgress); + _logger.LogDebug("No Show match in Watched shows list {Episode}", GetVerboseEpisodeData(episode)); } - // _logger.LogInformation(syncItemFailures + " items not parsed"); + // purely for progress reporting + currentProgress += percentPerItem; + progress.Report(currentProgress); } - private static string GetVerboseEpisodeData(Episode episode) + // _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 results) + { + return results.FirstOrDefault(i => IsMatch(item, i.Show)); + } + + public static TraktShowCollected FindMatch(Series item, IEnumerable results) + { + return results.FirstOrDefault(i => IsMatch(item, i.Show)); + } + + public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable results) + { + return results.FirstOrDefault(i => IsMatch(item, i.Movie)); + } + + public static IEnumerable FindMatches(BaseItem item, IEnumerable 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)) { - 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(); + return true; } - public static TraktShowWatched FindMatch(Series item, IEnumerable results) + var tmdb = item.GetProviderId(MetadataProvider.Tmdb); + + if (movie.Ids.Tmdb.HasValue && string.Equals(tmdb, movie.Ids.Tmdb.Value.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) { - return results.FirstOrDefault(i => IsMatch(item, i.show)); + return true; } - public static TraktShowCollected FindMatch(Series item, IEnumerable results) + if (item.Name == movie.Title && item.ProductionYear == movie.Year) { - return results.FirstOrDefault(i => IsMatch(item, i.show)); + return true; } - public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable results) + 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 results.FirstOrDefault(i => IsMatch(item, i.movie)); + return true; } - public static IEnumerable FindMatches(BaseItem item, IEnumerable results) + var imdb = item.GetProviderId(MetadataProvider.Imdb); + if (!string.IsNullOrWhiteSpace(imdb) && + string.Equals(imdb, show.Ids.Imdb, StringComparison.OrdinalIgnoreCase)) { - return results.Where(i => IsMatch(item, i.movie)).ToList(); + return true; } - 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; - } - - 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; - } + return false; } } diff --git a/Trakt/ScheduledTasks/SyncLibraryTask.cs b/Trakt/ScheduledTasks/SyncLibraryTask.cs index 66043d0..0160fb0 100644 --- a/Trakt/ScheduledTasks/SyncLibraryTask.cs +++ b/Trakt/ScheduledTasks/SyncLibraryTask.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using System.Net.Http; +using Jellyfin.Data.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -13,333 +14,166 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Jellyfin.Data.Enums; using Microsoft.Extensions.Logging; using Trakt.Api; using Trakt.Api.DataContracts.Sync; using Trakt.Helpers; using Trakt.Model; -namespace Trakt.ScheduledTasks +namespace Trakt.ScheduledTasks; + +/// +/// 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. +/// +public class SyncLibraryTask : IScheduledTask { - /// - /// 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. - /// - public class SyncLibraryTask : IScheduledTask + // private readonly IHttpClient _httpClient; + + private readonly IUserManager _userManager; + + private readonly ILogger _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) { - //private readonly IHttpClient _httpClient; + _userManager = userManager; + _userDataManager = userDataManager; + _libraryManager = libraryManager; + _logger = loggerFactory.CreateLogger(); + _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); + } - private readonly IUserManager _userManager; + public string Key => "TraktSyncLibraryTask"; - private readonly ILogger _logger; + public string Name => "Sync library to trakt.tv"; - private readonly TraktApi _traktApi; + public string Category => "Trakt"; - private readonly IUserDataManager _userDataManager; + public string Description + => "Adds any media that is in each users trakt monitored locations to their trakt.tv profile"; - private readonly ILibraryManager _libraryManager; + public IEnumerable GetDefaultTriggers() => Enumerable.Empty(); - public SyncLibraryTask( - ILoggerFactory loggerFactory, - IUserManager userManager, - IUserDataManager userDataManager, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - IFileSystem fileSystem, - ILibraryManager libraryManager) + /// + /// Gather users and call + /// + public async Task Execute(CancellationToken cancellationToken, IProgress progress) + { + 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) { - _userManager = userManager; - _userDataManager = userDataManager; - _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(); - _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); + _logger.LogInformation("No Users returned"); + return; } - public IEnumerable GetDefaultTriggers() => Enumerable.Empty(); - - 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"; - - /// - /// Gather users and call - /// - public async Task Execute(CancellationToken cancellationToken, IProgress progress) + foreach (var user in users) { - var users = _userManager.Users.Where(u => UserHelper.GetTraktUser(u) != null).ToList(); + var traktUser = UserHelper.GetTraktUser(user); - // No point going further if we don't have users. - if (users.Count == 0) + // I'll leave this in here for now, but in reality this continue should never be reached. + if (string.IsNullOrEmpty(traktUser?.LinkedMbUserId)) { - _logger.LogInformation("No Users returned"); - return; + _logger.LogError("traktUser is either null or has no linked MB account"); + continue; } - 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); - } + await + SyncUserLibrary(user, traktUser, progress.Split(users.Count), cancellationToken) + .ConfigureAwait(false); } + } - /// - /// Count media items and call and - /// - /// - private async Task SyncUserLibrary( - Jellyfin.Data.Entities.User user, - TraktUser traktUser, - ISplittableProgress progress, - CancellationToken cancellationToken) - { - await SyncMovies(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false); - await SyncShows(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false); - } + /// + /// Count media items and call and + /// + /// + private async Task SyncUserLibrary( + Jellyfin.Data.Entities.User user, + TraktUser traktUser, + ISplittableProgress progress, + CancellationToken cancellationToken) + { + await SyncMovies(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false); + await SyncShows(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false); + } - /// - /// Sync watched and collected status of s with trakt. - /// - private async Task SyncMovies( - Jellyfin.Data.Entities.User user, - TraktUser traktUser, - ISplittableProgress 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[] { typeof(Movie).Name }, - IsVirtualItem = false, - OrderBy = new [] - { - new ValueTuple(ItemSortBy.SortName, SortOrder.Ascending) - } - }) - .Where(x => _traktApi.CanSync(x, traktUser)) - .ToList(); - var collectedMovies = new List(); - var playedMovies = new List(); - var unplayedMovies = new List(); - - 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)))) + /// + /// Sync watched and collected status of s with trakt. + /// + private async Task SyncMovies( + Jellyfin.Data.Entities.User user, + TraktUser traktUser, + ISplittableProgress 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) { - 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) + IncludeItemTypes = new[] { nameof(Movie) }, + IsVirtualItem = false, + OrderBy = new[] { - playedMovies.Add(libraryMovie); + (ItemSortBy.SortName, SortOrder.Ascending) } - else if (!traktUser.SkipUnwatchedImportFromTrakt) - { - if (userData.Played) - { - userData.Played = false; + }) + .Where(x => _traktApi.CanSync(x, traktUser)) + .ToList(); + var collectedMovies = new List(); + var playedMovies = new List(); + var unplayedMovies = new List(); - _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); - } - } + 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); - decisionProgress.Report(100); - } - - // send movies to mark collected if (traktUser.SynchronizeCollections) { - await SendMovieCollectionUpdates(true, traktUser, collectedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false); + // 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); + } } - // send movies to mark watched - await SendMoviePlaystateUpdates(true, traktUser, playedMovies, progress.Split(4), cancellationToken).ConfigureAwait(false); + var movieWatched = SyncFromTraktTask.FindMatch(libraryMovie, traktWatchedMovies); - // 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 movies, - ISplittableProgress progress, - CancellationToken cancellationToken) - { - _logger.LogInformation("Movies to " + (collected ? "add to" : "remove from") + " Collection: " + movies.Count); - if (movies.Count > 0) + // if the movie has been played locally and is unplayed on trakt.tv then add it to the list + if (userData.Played) { - try - { - var dataContracts = - await - _traktApi.SendLibraryUpdateAsync( - movies, - traktUser, - cancellationToken, - collected ? EventType.Add : EventType.Remove).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 playedMovies, - ISplittableProgress progress, - CancellationToken cancellationToken) - { - _logger.LogInformation("Movies to set " + (seen ? string.Empty : "un") + "watched: " + 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); - } - } - - /// - /// Sync watched and collected status of s with trakt. - /// - private async Task SyncShows( - Jellyfin.Data.Entities.User user, - TraktUser traktUser, - ISplittableProgress 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[] { typeof(Episode).Name }, - IsVirtualItem = false, - OrderBy = new[] - { - new ValueTuple(ItemSortBy.SeriesSortName, SortOrder.Ascending) - } - }) - .Where(x => _traktApi.CanSync(x, traktUser)) - .ToList(); - - var collectedEpisodes = new List(); - var playedEpisodes = new List(); - var unplayedEpisodes = new List(); - - 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 (movieWatched == null) { if (traktUser.PostWatchedHistory) { - playedEpisodes.Add(episode); + playedMovies.Add(libraryMovie); } else if (!traktUser.SkipUnwatchedImportFromTrakt) { @@ -349,140 +183,305 @@ namespace Trakt.ScheduledTasks _userDataManager.SaveUserData( user.Id, - episode, + libraryMovie, userData, UserDataSaveReason.Import, cancellationToken); } } } - else if (userData != null && !userData.Played && isPlayedTraktTv && traktUser.PostUnwatchedHistory) + } + 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) { - // If the show has not been played locally but is played on trakt.tv then add it to the unplayed list - unplayedEpisodes.Add(episode); + unplayedMovies.Add(libraryMovie); } + } - if (traktUser.SynchronizeCollections) + 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 movies, + ISplittableProgress 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) { - 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)) + foreach (var traktSyncResponse in dataContracts) { - collectedEpisodes.Add(episode); + 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"); + } - decisionProgress.Report(100); + progress.Report(100); + } + } + + private async Task SendMoviePlaystateUpdates( + bool seen, + TraktUser traktUser, + List playedMovies, + ISplittableProgress 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); + } + } + + /// + /// Sync watched and collected status of s with trakt. + /// + private async Task SyncShows( + Jellyfin.Data.Entities.User user, + TraktUser traktUser, + ISplittableProgress 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[] { nameof(Episode) }, + IsVirtualItem = false, + OrderBy = new[] + { + (ItemSortBy.SeriesSortName, SortOrder.Ascending) + } + }) + .Where(x => _traktApi.CanSync(x, traktUser)) + .ToList(); + + var collectedEpisodes = new List(); + var playedEpisodes = new List(); + var unplayedEpisodes = new List(); + + 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); } if (traktUser.SynchronizeCollections) { - await SendEpisodeCollectionUpdates(true, traktUser, collectedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false); + 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 SendEpisodePlaystateUpdates(true, traktUser, playedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false); - - await SendEpisodePlaystateUpdates(false, traktUser, unplayedEpisodes, progress.Split(4), cancellationToken).ConfigureAwait(false); + decisionProgress.Report(100); } - private async Task SendEpisodePlaystateUpdates( - bool seen, - TraktUser traktUser, - List playedEpisodes, - ISplittableProgress progress, - CancellationToken cancellationToken) + if (traktUser.SynchronizeCollections) { - _logger.LogInformation("Episodes to set " + (seen ? string.Empty : "un") + "watched: " + playedEpisodes.Count); - if (playedEpisodes.Count > 0) + 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 playedEpisodes, + ISplittableProgress progress, + CancellationToken cancellationToken) + { + _logger.LogInformation("Episodes to set {State}watched: {Count}", seen ? string.Empty : "un", playedEpisodes.Count); + if (playedEpisodes.Count > 0) + { + try { - try + var dataContracts = + await _traktApi.SendEpisodePlaystateUpdates(playedEpisodes, traktUser, seen, cancellationToken).ConfigureAwait(false); + if (dataContracts != null) { - var dataContracts = - await _traktApi.SendEpisodePlaystateUpdates(playedEpisodes, traktUser, seen, cancellationToken).ConfigureAwait(false); - if (dataContracts != null) + foreach (var con in dataContracts) { - foreach (var con in dataContracts) - { - LogTraktResponseDataContract(con); - } + 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 collectedEpisodes, - ISplittableProgress progress, - CancellationToken cancellationToken) - { - _logger.LogInformation("Episodes to add to Collection: " + collectedEpisodes.Count); - if (collectedEpisodes.Count > 0) + catch (Exception e) { - try + _logger.LogError(e, "Error updating episode play states"); + } + + progress.Report(100); + } + } + + private async Task SendEpisodeCollectionUpdates( + bool collected, + TraktUser traktUser, + List collectedEpisodes, + ISplittableProgress 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) { - var dataContracts = - await - _traktApi.SendLibraryUpdateAsync( - collectedEpisodes, - traktUser, - cancellationToken, - collected ? EventType.Add : EventType.Remove).ConfigureAwait(false); - if (dataContracts != null) + foreach (var traktSyncResponse in dataContracts) { - foreach (var traktSyncResponse in dataContracts) - { - LogTraktResponseDataContract(traktSyncResponse); - } + 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) + { + _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); + } + } + + 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); } - private void LogTraktResponseDataContract(TraktSyncResponse dataContract) + foreach (var traktShow in dataContract.NotFound.Shows) { - _logger.LogDebug("TraktResponse Added Movies: " + dataContract.added.movies); - _logger.LogDebug("TraktResponse Added Shows: " + dataContract.added.shows); - _logger.LogDebug("TraktResponse Added Seasons: " + dataContract.added.seasons); - _logger.LogDebug("TraktResponse Added Episodes: " + dataContract.added.episodes); - foreach (var traktMovie in dataContract.not_found.movies) - { - _logger.LogError("TraktResponse not Found: {@TraktMovie}", traktMovie); - } + _logger.LogError("TraktResponse not Found: {@TraktShow}", traktShow); + } - foreach (var traktShow in dataContract.not_found.shows) - { - _logger.LogError("TraktResponse not Found: {@TraktShow}", traktShow); - } + foreach (var traktSeason in dataContract.NotFound.Seasons) + { + _logger.LogError("TraktResponse not Found: {@TraktSeason}", traktSeason); + } - foreach (var traktSeason in dataContract.not_found.seasons) - { - _logger.LogError("TraktResponse not Found: {@TraktSeason}", traktSeason); - } - - foreach (var traktEpisode in dataContract.not_found.episodes) - { - _logger.LogError("TraktResponse not Found: {@TraktEpisode}", traktEpisode); - } + foreach (var traktEpisode in dataContract.NotFound.Episodes) + { + _logger.LogError("TraktResponse not Found: {@TraktEpisode}", traktEpisode); } } } diff --git a/Trakt/ServerMediator.cs b/Trakt/ServerMediator.cs index aa88920..944ee57 100644 --- a/Trakt/ServerMediator.cs +++ b/Trakt/ServerMediator.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -using System.Threading.Tasks; using System.Net.Http; +using System.Threading.Tasks; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -15,206 +15,276 @@ using Microsoft.Extensions.Logging; using Trakt.Api; using Trakt.Helpers; -namespace Trakt +namespace Trakt; + +/// +/// All communication between the server and the plugins server instance should occur in this class. +/// +public class ServerMediator : IServerEntryPoint { + private readonly ISessionManager _sessionManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly UserDataManagerEventsHelper _userDataManagerEventsHelper; + private readonly IUserDataManager _userDataManager; + private TraktApi _traktApi; + private LibraryManagerEventsHelper _libraryManagerEventsHelper; + /// - /// All communication between the server and the plugins server instance should occur in this class. + /// /// - public class ServerMediator : IServerEntryPoint + /// + /// + /// + /// + /// + /// + /// + public ServerMediator( + ISessionManager sessionManager, + IUserDataManager userDataManager, + ILibraryManager libraryManager, + ILoggerFactory loggerFactory, + IHttpClientFactory httpClientFactory, + IServerApplicationHost appHost, + IFileSystem fileSystem) { - private readonly ISessionManager _sessionManager; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private TraktApi _traktApi; - private LibraryManagerEventsHelper _libraryManagerEventsHelper; - private readonly UserDataManagerEventsHelper _userDataManagerEventsHelper; - private IUserDataManager _userDataManager; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _userDataManager = userDataManager; - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public ServerMediator( - ISessionManager sessionManager, - IUserDataManager userDataManager, - ILibraryManager libraryManager, - ILoggerFactory loggerFactory, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - IFileSystem fileSystem) + _logger = loggerFactory.CreateLogger(); + + _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); + _libraryManagerEventsHelper = new LibraryManagerEventsHelper(loggerFactory.CreateLogger(), _traktApi); + _userDataManagerEventsHelper = new UserDataManagerEventsHelper(loggerFactory.CreateLogger(), _traktApi); + } + + /// + /// + /// + /// + /// + private void OnUserDataSaved(object sender, UserDataSaveEventArgs e) + { + // ignore change events for any reason other than manually toggling played. + if (e.SaveReason != UserDataSaveReason.TogglePlayed) { - _sessionManager = sessionManager; - _libraryManager = libraryManager; - _userDataManager = userDataManager; - - _logger = loggerFactory.CreateLogger(); - - _traktApi = new TraktApi(loggerFactory.CreateLogger(), httpClientFactory, appHost, userDataManager, fileSystem); - _libraryManagerEventsHelper = new LibraryManagerEventsHelper(loggerFactory.CreateLogger(), _traktApi); - _userDataManagerEventsHelper = new UserDataManagerEventsHelper(loggerFactory.CreateLogger(), _traktApi); - + return; } - /// - /// - /// - /// - /// - private void OnUserDataSaved(object sender, UserDataSaveEventArgs e) + if (e.Item != null) { - // ignore change events for any reason other than manually toggling played. - if (e.SaveReason != UserDataSaveReason.TogglePlayed) + // determine if user has trakt credentials + var traktUser = UserHelper.GetTraktUser(e.UserId); + + // Can't progress + if (traktUser == null || !_traktApi.CanSync(e.Item, traktUser)) { return; } - if (e.Item is BaseItem baseItem) + if (!traktUser.PostSetWatched && !traktUser.PostSetUnwatched) { - // determine if user has trakt credentials - var traktUser = UserHelper.GetTraktUser(e.UserId); + // User doesn't want to post any status changes at all. + return; + } - // Can't progress - if (traktUser == null || !_traktApi.CanSync(baseItem, traktUser)) + // We have a user who wants to post updates and the item is in a trakt monitored location. + _userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(e, traktUser); + } + } + + /// + public Task RunAsync() + { + _userDataManager.UserDataSaved += OnUserDataSaved; + _sessionManager.PlaybackStart += KernelPlaybackStart; + _sessionManager.PlaybackStopped += KernelPlaybackStopped; + _libraryManager.ItemAdded += LibraryManagerItemAdded; + _libraryManager.ItemRemoved += LibraryManagerItemRemoved; + return Task.CompletedTask; + } + + /// + /// + /// + /// + /// + private void LibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) + { + if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series)) + { + return; + } + + if (e.Item.LocationType == LocationType.Virtual) + { + return; + } + + _libraryManagerEventsHelper.QueueItem(e.Item, EventType.Remove); + } + + /// + /// + /// + /// + /// + private void LibraryManagerItemAdded(object sender, ItemChangeEventArgs e) + { + // Don't do anything if it's not a supported media type + if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series)) + { + return; + } + + if (e.Item.LocationType == LocationType.Virtual) + { + return; + } + + _libraryManagerEventsHelper.QueueItem(e.Item, EventType.Add); + } + + /// + /// Let Trakt.tv know the user has started to watch something + /// + /// + /// + private async void KernelPlaybackStart(object sender, PlaybackProgressEventArgs e) + { + try + { + _logger.LogInformation("Playback Started"); + + if (e.Users == null || !e.Users.Any() || e.Item == null) + { + _logger.LogError("Event details incomplete. Cannot process current media"); + return; + } + + foreach (var user in e.Users) + { + // Since Emby is user profile friendly, I'm going to need to do a user lookup every time something starts + var traktUser = UserHelper.GetTraktUser(user); + + if (traktUser == null) { - return; + _logger.LogInformation("Could not match user with any stored credentials"); + continue; } - if (!traktUser.PostSetWatched && !traktUser.PostSetUnwatched) + if (!traktUser.Scrobble) { - // User doesn't want to post any status changes at all. - return; + continue; } - // We have a user who wants to post updates and the item is in a trakt monitored location. - _userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(e, traktUser); - } - } - - /// - public Task RunAsync() - { - _userDataManager.UserDataSaved += OnUserDataSaved; - _sessionManager.PlaybackStart += KernelPlaybackStart; - _sessionManager.PlaybackStopped += KernelPlaybackStopped; - _libraryManager.ItemAdded += LibraryManagerItemAdded; - _libraryManager.ItemRemoved += LibraryManagerItemRemoved; - return Task.CompletedTask; - } - - - - /// - /// - /// - /// - /// - private void LibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) - { - if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series)) - { - return; - } - - if (e.Item.LocationType == LocationType.Virtual) - { - return; - } - - _libraryManagerEventsHelper.QueueItem(e.Item, EventType.Remove); - } - - /// - /// - /// - /// - /// - private void LibraryManagerItemAdded(object sender, ItemChangeEventArgs e) - { - // Don't do anything if it's not a supported media type - if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series)) - { - return; - } - - if (e.Item.LocationType == LocationType.Virtual) - { - return; - } - - _libraryManagerEventsHelper.QueueItem(e.Item, EventType.Add); - } - - - - /// - /// Let Trakt.tv know the user has started to watch something - /// - /// - /// - private async void KernelPlaybackStart(object sender, PlaybackProgressEventArgs e) - { - try - { - _logger.LogInformation("Playback Started"); - - if (e.Users == null || !e.Users.Any() || e.Item == null) + if (!_traktApi.CanSync(e.Item, traktUser)) { - _logger.LogError("Event details incomplete. Cannot process current media"); - return; + continue; } - foreach (var user in e.Users) + _logger.LogDebug("{UseId} appears to be monitoring {Path}", traktUser.LinkedMbUserId, e.Item.Path); + + var video = e.Item as Video; + var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ? + (float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f; + + try { - // Since Emby is user profile friendly, I'm going to need to do a user lookup every time something starts - var traktUser = UserHelper.GetTraktUser(user); - - if (traktUser == null) + if (video is Movie movie) { - _logger.LogInformation("Could not match user with any stored credentials"); - continue; + _logger.LogDebug("Send movie status update"); + await _traktApi.SendMovieStatusUpdateAsync( + movie, + MediaStatus.Watching, + traktUser, + progressPercent).ConfigureAwait(false); } - - if (!traktUser.Scrobble) + else if (video is Episode episode) { - continue; + _logger.LogDebug("Send episode status update"); + await _traktApi.SendEpisodeStatusUpdateAsync( + episode, + MediaStatus.Watching, + traktUser, + progressPercent).ConfigureAwait(false); } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception handled sending status update"); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending watching status update"); + } + } - if (!_traktApi.CanSync(e.Item, traktUser)) - { - continue; - } + /// + /// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has + /// completed watching the item. + /// + /// + /// + private async void KernelPlaybackStopped(object sender, PlaybackStopEventArgs e) + { + if (e.Users == null || !e.Users.Any() || e.Item == null) + { + _logger.LogError("Event details incomplete. Cannot process current media"); + return; + } - _logger.LogDebug(traktUser.LinkedMbUserId + " appears to be monitoring " + e.Item.Path); + try + { + _logger.LogInformation("Playback Stopped"); - var video = e.Item as Video; - var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ? - (float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f; + foreach (var user in e.Users) + { + var traktUser = UserHelper.GetTraktUser(user); + + if (traktUser == null) + { + _logger.LogError("Could not match trakt user"); + continue; + } + + if (!traktUser.Scrobble) + { + continue; + } + + if (!_traktApi.CanSync(e.Item, traktUser)) + { + continue; + } + + var video = e.Item as Video; + + if (e.PlayedToCompletion) + { + _logger.LogInformation("Item is played. Scrobble"); try { if (video is Movie movie) { - _logger.LogDebug("Send movie status update"); await _traktApi.SendMovieStatusUpdateAsync( movie, - MediaStatus.Watching, + MediaStatus.Stop, traktUser, - progressPercent).ConfigureAwait(false); + 100).ConfigureAwait(false); } else if (video is Episode episode) { - _logger.LogDebug("Send episode status update"); await _traktApi.SendEpisodeStatusUpdateAsync( episode, - MediaStatus.Watching, + MediaStatus.Stop, traktUser, - progressPercent).ConfigureAwait(false); + 100).ConfigureAwait(false); } } catch (Exception ex) @@ -222,106 +292,39 @@ namespace Trakt _logger.LogError(ex, "Exception handled sending status update"); } } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending watching status update"); - } - } - - /// - /// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has - /// completed watching the item. - /// - /// - /// - private async void KernelPlaybackStopped(object sender, PlaybackStopEventArgs e) - { - if (e.Users == null || !e.Users.Any() || e.Item == null) - { - _logger.LogError("Event details incomplete. Cannot process current media"); - return; - } - - try - { - _logger.LogInformation("Playback Stopped"); - - foreach (var user in e.Users) + else { - var traktUser = UserHelper.GetTraktUser(user); + var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ? + (float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f; + _logger.LogInformation("Item Not fully played. Tell trakt.tv we are no longer watching but don't scrobble"); - if (traktUser == null) + if (video is Movie movie) { - _logger.LogError("Could not match trakt user"); - continue; - } - - if (!traktUser.Scrobble) - { - continue; - } - - if (!_traktApi.CanSync(e.Item, traktUser)) - { - continue; - } - - var video = e.Item as Video; - - if (e.PlayedToCompletion) - { - _logger.LogInformation("Item is played. Scrobble"); - - try - { - if (video is Movie movie) - { - await _traktApi.SendMovieStatusUpdateAsync( - movie, - MediaStatus.Stop, - traktUser, - 100).ConfigureAwait(false); - } - else if (video is Episode episode) - { - await _traktApi.SendEpisodeStatusUpdateAsync( - episode, - MediaStatus.Stop, - traktUser, - 100).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception handled sending status update"); - } + await _traktApi.SendMovieStatusUpdateAsync(movie, MediaStatus.Stop, traktUser, progressPercent).ConfigureAwait(false); } else { - var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ? - (float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f; - _logger.LogInformation("Item Not fully played. Tell trakt.tv we are no longer watching but don't scrobble"); - - if (video is Movie movie) - { - await _traktApi.SendMovieStatusUpdateAsync(movie, MediaStatus.Stop, traktUser, progressPercent).ConfigureAwait(false); - } - else - { - await _traktApi.SendEpisodeStatusUpdateAsync(video as Episode, MediaStatus.Stop, traktUser, progressPercent).ConfigureAwait(false); - } + await _traktApi.SendEpisodeStatusUpdateAsync(video as Episode, MediaStatus.Stop, traktUser, progressPercent).ConfigureAwait(false); } } } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending scrobble"); - } } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending scrobble"); + } + } - /// - public void Dispose() + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) { _userDataManager.UserDataSaved -= OnUserDataSaved; _sessionManager.PlaybackStart -= KernelPlaybackStart; @@ -329,7 +332,9 @@ namespace Trakt _libraryManager.ItemAdded -= LibraryManagerItemAdded; _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; _traktApi = null; + _libraryManagerEventsHelper.Dispose(); _libraryManagerEventsHelper = null; + _userDataManagerEventsHelper.Dispose(); } } } diff --git a/Trakt/Trakt.csproj b/Trakt/Trakt.csproj index 03c0b99..cd6d5fe 100644 --- a/Trakt/Trakt.csproj +++ b/Trakt/Trakt.csproj @@ -4,6 +4,13 @@ net6.0 12.0.0.0 12.0.0.0 + true + true + disable + AllEnabledByDefault + ../jellyfin.ruleset + + CS1591,CS1572,CS1573,CS1574,SA1629,SA1606,SA1611,SA1614,SA1615,SA1616,SA1642 @@ -16,7 +23,10 @@ - + + + + diff --git a/jellyfin.ruleset b/jellyfin.ruleset new file mode 100644 index 0000000..7adc350 --- /dev/null +++ b/jellyfin.ruleset @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +