Remove all warnings in project

This commit is contained in:
Cody Robibero 2021-12-13 18:03:27 -07:00
parent d46aa80b50
commit 5f4829fac1
79 changed files with 3713 additions and 3339 deletions

View File

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

View File

@ -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; }
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("season")]
public int? Season { get; set; }
public int? number { get; set; }
/// <summary>
/// Gets or sets the episode number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
public string title { get; set; }
/// <summary>
/// Gets or sets the episode title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
public TraktEpisodeId ids { get; set; }
}
}
/// <summary>
/// Gets or sets the episode ids.
/// </summary>
[JsonPropertyName("ids")]
public TraktEpisodeId Ids { get; set; }
}

View File

@ -1,6 +1,5 @@
namespace Trakt.Api.DataContracts.BaseModel
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktEpisodeId : TraktTVId
{
public class TraktEpisodeId : TraktTVId
{
}
}
}

View File

@ -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; }
}
}
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktIMDBandTMDBId : TraktId
{
[JsonPropertyName("imdb")]
public string Imdb { get; set; }
[JsonPropertyName("tmdb")]
public int? Tmdb { get; set; }
}

View File

@ -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; }
}
}
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktId
{
/// <summary>
/// Gets or sets the Trakt item id.
/// </summary>
[JsonPropertyName("trakt")]
public int? Trakt { get; set; }
/// <summary>
/// Gets or sets the item slug.
/// </summary>
[JsonPropertyName("slug")]
public string Slug { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("ids")]
public TraktMovieId Ids { get; set; }
}

View File

@ -1,6 +1,5 @@
namespace Trakt.Api.DataContracts.BaseModel
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktMovieId : TraktIMDBandTMDBId
{
public class TraktMovieId : TraktIMDBandTMDBId
{
}
}
}

View File

@ -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; }
}
}
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktPerson
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("ids")]
public TraktPersonId Ids { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
}

View File

@ -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; }
}
}
namespace Trakt.Api.DataContracts.BaseModel;
public abstract class TraktRated
{
[JsonPropertyName("rating")]
public int? Rating { get; set; }
[JsonPropertyName("rated_at")]
public string RatedAt { get; set; }
}

View File

@ -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; }
}
}
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktSeason
{
[JsonPropertyName("number")]
public int? Number { get; set; }
[JsonPropertyName("ids")]
public TraktSeasonId Ids { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("ids")]
public TraktShowId Ids { get; set; }
}

View File

@ -1,7 +1,5 @@

namespace Trakt.Api.DataContracts.BaseModel
namespace Trakt.Api.DataContracts.BaseModel;
public class TraktShowId : TraktTVId
{
public class TraktShowId : TraktTVId
{
}
}
}

View File

@ -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; }
}
}
[JsonPropertyName("tvrage")]
public int? Tvrage { get; set; }
}

View File

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

View File

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

View File

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

View File

@ -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; }
}
}
[JsonPropertyName("app_date")]
public string AppDate { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("app_date")]
public string AppDate { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
}

View File

@ -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; }
}
}
// public bool 3d { get; set; }
}

View File

@ -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; }
}
}
// public bool 3d { get; set; }
}

View File

@ -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<TraktEpisodeCollected> Episodes { get; set; }
}

View File

@ -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<TraktSeasonCollected> seasons { get; set; }
public class TraktSeasonCollected
{
public int number { get; set; }
public List<TraktEpisodeCollected> episodes { get; set; }
}
}
}
[JsonPropertyName("seasons")]
public List<TraktSeasonCollected> Seasons { get; set; }
}

View File

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

View File

@ -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<TraktMovie> Movies { get; set; }
[JsonPropertyName("shows")]
public List<TraktShow> Shows { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisode> Episodes { get; set; }
[JsonPropertyName("seasons")]
public List<TraktSeason> Seasons { get; set; }
[JsonPropertyName("people")]
public List<TraktPerson> People { get; set; }
}

View File

@ -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; }
}
}
public class TraktEpisodeRated : TraktRated
{
[JsonPropertyName("number")]
public int? Number { get; set; }
[JsonPropertyName("ids")]
public TraktEpisodeId Ids { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("ids")]
public TraktMovieId Ids { get; set; }
}

View File

@ -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<TraktEpisodeRated> Episodes { get; set; }
}

View File

@ -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<TraktSeasonRated> seasons { get; set; }
public class TraktSeasonRated : TraktRated
{
public int? number { get; set; }
public List<TraktEpisodeRated> episodes { get; set; }
}
}
}
[JsonPropertyName("seasons")]
public List<TraktSeasonRated> Seasons { get; set; }
}

View File

@ -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<TMovie, TShow, TEpisode>
{
public class TraktSync<TMovie, TShow, TEpisode>
{
public List<TMovie> movies { get; set; }
[JsonPropertyName("movies")]
public List<TMovie> Movies { get; set; }
public List<TShow> shows { get; set; }
[JsonPropertyName("shows")]
public List<TShow> Shows { get; set; }
public List<TEpisode> episodes { get; set; }
}
public class TraktSyncRated : TraktSync<TraktMovieRated, TraktShowRated, TraktEpisodeRated>
{
}
public class TraktSyncWatched : TraktSync<TraktMovieWatched, TraktShowWatched, TraktEpisodeWatched>
{
}
public class TraktSyncCollected : TraktSync<TraktMovieCollected, TraktShowCollected, TraktEpisodeCollected>
{
}
}
[JsonPropertyName("episodes")]
public List<TEpisode> Episodes { get; set; }
}

View File

@ -0,0 +1,7 @@
using Trakt.Api.DataContracts.Sync.Collection;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncCollected : TraktSync<TraktMovieCollected, TraktShowCollected, TraktEpisodeCollected>
{
}

View File

@ -0,0 +1,7 @@
using Trakt.Api.DataContracts.Sync.Ratings;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncRated : TraktSync<TraktMovieRated, TraktShowRated, TraktEpisodeRated>
{
}

View File

@ -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<TraktMovie> movies { get; set; }
public List<TraktShow> shows { get; set; }
public List<TraktEpisode> episodes { get; set; }
public List<TraktSeason> seasons { get; set; }
public List<TraktPerson> people { get; set; }
}
}
}
[JsonPropertyName("not_found")]
public NotFoundObjects NotFound { get; set; }
}

View File

@ -0,0 +1,7 @@
using Trakt.Api.DataContracts.Sync.Watched;
namespace Trakt.Api.DataContracts.Sync;
public class TraktSyncWatched : TraktSync<TraktMovieWatched, TraktShowWatched, TraktEpisodeWatched>
{
}

View File

@ -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; }
}
}
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
}

View File

@ -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<TraktEpisodeWatched> episodes { get; set; }
}
public class TraktSeasonWatched : TraktSeason
{
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
[JsonPropertyName("episodes")]
public List<TraktEpisodeWatched> Episodes { get; set; }
}

View File

@ -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<TraktSeasonWatched> seasons { get; set; }
}
}
public class TraktShowWatched : TraktShow
{
[JsonPropertyName("watched_at")]
public string WatchedAt { get; set; }
[JsonPropertyName("seasons")]
public List<TraktSeasonWatched> Seasons { get; set; }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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; }
}
}
// public bool 3d { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}

View File

@ -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<TraktEpisodeCollected> Episodes { get; set; }
}

View File

@ -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<TraktSeasonCollected> seasons { get; set; }
public class TraktSeasonCollected
{
public int number { get; set; }
public List<TraktEpisodeCollected> episodes { get; set; }
public class TraktEpisodeCollected
{
public int number { get; set; }
public string collected_at { get; set; }
public TraktMetadata metadata { get; set; }
}
}
}
}
[JsonPropertyName("seasons")]
public List<TraktSeasonCollected> Seasons { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("episode")]
public TraktEpisode Episode { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("season")]
public TraktSeason Season { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("show")]
public TraktShow Show { get; set; }
}

View File

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

View File

@ -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<Episode> Episodes { get; set; }
}

View File

@ -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; }
}
}
[JsonPropertyName("movie")]
public TraktMovie Movie { get; set; }
}

View File

@ -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<Season> seasons { get; set; }
public class Season
{
public int number { get; set; }
public List<Episode> episodes { get; set; }
public class Episode
{
public string last_watched_at { get; set; }
public int number { get; set; }
public int plays { get; set; }
}
}
}
}
[JsonPropertyName("seasons")]
public List<Season> Seasons { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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<TraktUser>();
}
TraktUsers = Array.Empty<TraktUser>();
}
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();
}
}

View File

@ -1,9 +0,0 @@
namespace Trakt
{
public enum MediaStatus
{
Watching,
Paused,
Stop
}
}

View File

@ -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<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
var itemsReturned = 0;
var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
var count = list.Count;
var chunks = new List<IEnumerable<T>>();
while (itemsReturned < count)
{
chunks.Add(list.Take(chunkSize).ToList());
list = list.Skip(chunkSize).ToList();
itemsReturned += chunkSize;
}
return chunks;
}
public static ISplittableProgress<double> Split(this IProgress<double> parent, int parts)
{
var current = parent.ToSplittableProgress();
return current.Split(parts);
}
public static ISplittableProgress<double> ToSplittableProgress(this IProgress<double> progress)
{
var splittable = new SplittableProgress(progress.Report);
return splittable;
}
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<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
var itemsReturned = 0;
var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
var count = list.Count;
var chunks = new List<IEnumerable<T>>();
while (itemsReturned < count)
{
chunks.Add(list.Take(chunkSize).ToList());
list = list.Skip(chunkSize).ToList();
itemsReturned += chunkSize;
}
return chunks;
}
public static ISplittableProgress<double> Split(this IProgress<double> parent, int parts)
{
var current = parent.ToSplittableProgress();
return current.Split(parts);
}
public static ISplittableProgress<double> ToSplittableProgress(this IProgress<double> progress)
{
var splittable = new SplittableProgress(progress.Report);
return splittable;
}
#pragma warning disable
public enum TraktAudio
{
lpcm,
mp3,
aac,
dts,
dts_ma,
flac,
ogg,
wma,
dolby_prologic,
dolby_digital,
dolby_digital_plus,
dolby_truehd
}
#pragma warning restore CA1707
}

View File

@ -0,0 +1,8 @@
namespace Trakt.Helpers;
public enum EventType
{
Add,
Remove,
Update
}

View File

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

View File

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

View File

@ -11,284 +11,283 @@ using Microsoft.Extensions.Logging;
using Trakt.Api;
using Trakt.Model;
namespace Trakt.Helpers
{
internal class LibraryManagerEventsHelper
{
private readonly List<LibraryEvent> _queuedEvents;
private Timer _queueTimer;
private readonly ILogger<LibraryManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
namespace Trakt.Helpers;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public LibraryManagerEventsHelper(ILogger<LibraryManagerEventsHelper> logger, TraktApi traktApi)
internal class LibraryManagerEventsHelper : IDisposable
{
private readonly List<LibraryEvent> _queuedEvents;
private readonly ILogger<LibraryManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private Timer _queueTimer;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public LibraryManagerEventsHelper(ILogger<LibraryManagerEventsHelper> logger, TraktApi traktApi)
{
_queuedEvents = new List<LibraryEvent>();
_logger = logger;
_traktApi = traktApi;
}
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="eventType"></param>
public void QueueItem(BaseItem item, EventType eventType)
{
if (item == null)
{
_queuedEvents = new List<LibraryEvent>();
_logger = logger;
_traktApi = traktApi;
throw new ArgumentNullException(nameof(item));
}
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="eventType"></param>
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);
}
}
/// <summary>
///
/// </summary>
private async void OnQueueTimerCallback(object state)
{
try
{
await OnQueueTimerCallbackInternal().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in OnQueueTimerCallbackInternal");
}
}
private async Task OnQueueTimerCallbackInternal()
{
_logger.LogInformation("Timer elapsed - Processing queued items");
if (!_queuedEvents.Any())
{
_logger.LogInformation("No events... Stopping queue timer");
// This may need to go
return;
}
var queue = _queuedEvents.ToList();
_queuedEvents.Clear();
foreach (var traktUser in Plugin.Instance.PluginConfiguration.TraktUsers)
{
var queuedMovieDeletes = queue.Where(ev =>
new Guid(ev.TraktUser.LinkedMbUserId).Equals(new Guid(traktUser.LinkedMbUserId)) &&
ev.Item is Movie &&
ev.EventType == EventType.Remove).ToList();
if (queuedMovieDeletes.Any())
{
_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");
}
}
/// <summary>
///
/// </summary>
private async void OnQueueTimerCallback(object state)
// Everything is processed. Reset the event list.
_queuedEvents.Clear();
}
private async Task ProcessQueuedShowEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var shows = events.Select(lev => (Series)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Tvdb)))
.ToList();
try
{
// Should probably not be awaiting this, but it's unlikely a user will be deleting more than one or two shows at a time
foreach (var show in shows)
{
await _traktApi.SendLibraryUpdateAsync(show, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued series events");
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedMovieEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var movies = events.Select(lev => (Movie)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Imdb)))
.ToList();
try
{
await _traktApi.SendLibraryUpdateAsync(movies, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued movie events");
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedEpisodeEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var episodes = events.Select(lev => (Episode)lev.Item)
.Where(lev => lev.Series != null && !string.IsNullOrEmpty(lev.Series.Name) && !string.IsNullOrEmpty(lev.Series.GetProviderId(MetadataProvider.Tvdb)))
.OrderBy(i => i.Series.Id)
.ToList();
// Can't progress further without episodes
if (!episodes.Any())
{
_logger.LogInformation("episodes count is 0");
return;
}
var payload = new List<Episode>();
var currentSeriesId = episodes[0].Series.Id;
foreach (var ep in episodes)
{
if (!currentSeriesId.Equals(ep.Series.Id))
{
// We're starting a new series. Time to send the current one to trakt.tv
await _traktApi.SendLibraryUpdateAsync(payload, traktUser, eventType, CancellationToken.None).ConfigureAwait(false);
currentSeriesId = ep.Series.Id;
payload.Clear();
}
payload.Add(ep);
}
if (payload.Any())
{
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<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var shows = events.Select(lev => (Series)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Tvdb)))
.ToList();
try
{
// Should probably not be awaiting this, but it's unlikely a user will be deleting more than one or two shows at a time
foreach (var show in shows)
{
await _traktApi.SendLibraryUpdateAsync(show, traktUser, CancellationToken.None, eventType).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued series events");
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedMovieEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var movies = events.Select(lev => (Movie)lev.Item)
.Where(lev => !string.IsNullOrEmpty(lev.Name) && !string.IsNullOrEmpty(lev.GetProviderId(MetadataProvider.Imdb)))
.ToList();
try
{
await _traktApi.SendLibraryUpdateAsync(movies, traktUser, CancellationToken.None, eventType).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled processing queued movie events");
}
}
/// <summary>
///
/// </summary>
/// <param name="events"></param>
/// <param name="traktUser"></param>
/// <param name="eventType"></param>
/// <returns></returns>
private async Task ProcessQueuedEpisodeEvents(IEnumerable<LibraryEvent> events, TraktUser traktUser, EventType eventType)
{
var episodes = events.Select(lev => (Episode)lev.Item)
.Where(lev => lev.Series != null && (!string.IsNullOrEmpty(lev.Series.Name) && !string.IsNullOrEmpty(lev.Series.GetProviderId(MetadataProvider.Tvdb))))
.OrderBy(i => i.Series.Id)
.ToList();
// Can't progress further without episodes
if (!episodes.Any())
{
_logger.LogInformation("episodes count is 0");
return;
}
var payload = new List<Episode>();
var currentSeriesId = episodes[0].Series.Id;
foreach (var ep in episodes)
{
if (!currentSeriesId.Equals(ep.Series.Id))
{
// We're starting a new series. Time to send the current one to trakt.tv
await _traktApi.SendLibraryUpdateAsync(payload, traktUser, 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();
}
}
}

View File

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

View File

@ -9,220 +9,207 @@ using Microsoft.Extensions.Logging;
using Trakt.Api;
using Trakt.Model;
namespace Trakt.Helpers
{
/// <summary>
/// Helper class used to update the watched status of movies/episodes. Attempts to organise
/// requests to lower trakt.tv api calls.
/// </summary>
internal class UserDataManagerEventsHelper
{
private List<UserDataPackage> _userDataPackages;
private readonly ILogger<UserDataManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private Timer _timer;
namespace Trakt.Helpers;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public UserDataManagerEventsHelper(ILogger<UserDataManagerEventsHelper> logger, TraktApi traktApi)
/// <summary>
/// Helper class used to update the watched status of movies/episodes. Attempts to organise
/// requests to lower trakt.tv api calls.
/// </summary>
internal class UserDataManagerEventsHelper : IDisposable
{
private readonly ILogger<UserDataManagerEventsHelper> _logger;
private readonly TraktApi _traktApi;
private readonly List<UserDataPackage> _userDataPackages;
private Timer _timer;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="traktApi"></param>
public UserDataManagerEventsHelper(ILogger<UserDataManagerEventsHelper> logger, TraktApi traktApi)
{
_userDataPackages = new List<UserDataPackage>();
_logger = logger;
_traktApi = traktApi;
}
/// <summary>
///
/// </summary>
/// <param name="userDataSaveEventArgs"></param>
/// <param name="traktUser"></param>
public void ProcessUserDataSaveEventArgs(UserDataSaveEventArgs userDataSaveEventArgs, TraktUser traktUser)
{
var userPackage = _userDataPackages.FirstOrDefault(e => e.TraktUser.Equals(traktUser));
if (userPackage == null)
{
_userDataPackages = new List<UserDataPackage>();
_logger = logger;
_traktApi = traktApi;
userPackage = new UserDataPackage { TraktUser = traktUser };
_userDataPackages.Add(userPackage);
}
/// <summary>
///
/// </summary>
/// <param name="userDataSaveEventArgs"></param>
/// <param name="traktUser"></param>
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<Movie>();
}
}
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<Movie>();
}
}
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<Episode>();
}
if (userPackage.UnSeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
userPackage.UnSeenEpisodes,
userPackage.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
userPackage.UnSeenEpisodes = new List<Episode>();
}
userPackage.CurrentSeriesId = episode.Series.Id;
}
_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<Movie>();
}
}
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<Movie>();
}
}
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<Episode>();
}
if (userPackage.UnSeenEpisodes.Any())
{
_traktApi.SendEpisodePlaystateUpdates(
userPackage.UnSeenEpisodes,
userPackage.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
userPackage.UnSeenEpisodes = new List<Episode>();
}
userPackage.CurrentSeriesId = episode.Series.Id;
}
if (userDataSaveEventArgs.UserData.Played)
{
if (traktUser.PostSetWatched)
{
userPackage.SeenEpisodes.Add(episode);
}
}
else
{
if (traktUser.PostSetUnwatched)
{
userPackage.UnSeenEpisodes.Add(episode);
}
}
}
/// <summary>
/// Class that contains all the items to be reported to trakt.tv and supporting properties.
/// </summary>
internal class UserDataPackage
private void OnTimerCallback(object state)
{
public TraktUser TraktUser { get; set; }
public Guid CurrentSeriesId { get; set; }
public List<Movie> SeenMovies { get; set; }
public List<Movie> UnSeenMovies { get; set; }
public List<Episode> SeenEpisodes { get; set; }
public List<Episode> UnSeenEpisodes { get; set; }
public UserDataPackage()
foreach (var package in _userDataPackages)
{
SeenMovies = new List<Movie>();
UnSeenMovies = new List<Movie>();
SeenEpisodes = new List<Episode>();
UnSeenEpisodes = new List<Episode>();
if (package.UnSeenMovies.Any())
{
var movies = package.UnSeenMovies.ToList();
package.UnSeenMovies.Clear();
_traktApi.SendMoviePlaystateUpdates(
movies,
package.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
}
if (package.SeenMovies.Any())
{
var movies = package.SeenMovies.ToList();
package.SeenMovies.Clear();
_traktApi.SendMoviePlaystateUpdates(
movies,
package.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
}
if (package.UnSeenEpisodes.Any())
{
var episodes = package.UnSeenEpisodes.ToList();
package.UnSeenEpisodes.Clear();
_traktApi.SendEpisodePlaystateUpdates(
episodes,
package.TraktUser,
false,
CancellationToken.None).ConfigureAwait(false);
}
if (package.SeenEpisodes.Any())
{
var episodes = package.SeenEpisodes.ToList();
package.SeenEpisodes.Clear();
_traktApi.SendEpisodePlaystateUpdates(
episodes,
package.TraktUser,
true,
CancellationToken.None).ConfigureAwait(false);
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_timer.Dispose();
}
}
}

View File

@ -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;
/// <summary>
/// Class that contains all the items to be reported to trakt.tv and supporting properties.
/// </summary>
internal class UserDataPackage
{
public UserDataPackage()
{
SeenMovies = new List<Movie>();
UnSeenMovies = new List<Movie>();
SeenEpisodes = new List<Episode>();
UnSeenEpisodes = new List<Episode>();
}
public TraktUser TraktUser { get; set; }
public Guid CurrentSeriesId { get; set; }
public List<Movie> SeenMovies { get; set; }
public List<Movie> UnSeenMovies { get; set; }
public List<Episode> SeenEpisodes { get; set; }
public List<Episode> UnSeenEpisodes { get; set; }
}

View File

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

View File

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

8
Trakt/MediaStatus.cs Normal file
View File

@ -0,0 +1,8 @@
namespace Trakt;
public enum MediaStatus
{
Watching,
Paused,
Stop
}

View File

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

View File

@ -7,49 +7,48 @@ using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Trakt.Configuration;
namespace Trakt
namespace Trakt;
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer)
: base(appPaths, xmlSerializer)
{
public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer)
: base(appPaths, xmlSerializer)
Instance = this;
PollingTasks = new Dictionary<string, Task<bool>>();
}
/// <inheritdoc />
public override string Name => "Trakt";
/// <inheritdoc />
public override Guid Id => new Guid("4fe3201e-d6ae-4f2e-8917-e12bda571281");
/// <inheritdoc />
public override string Description
=> "Watch, rate, and discover media using Trakt. The HTPC just got more social.";
public static Plugin Instance { get; private set; }
public PluginConfiguration PluginConfiguration => Configuration;
public Dictionary<string, Task<bool>> PollingTasks { get; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
Instance = this;
PollingTasks = new Dictionary<string, Task<bool>>();
}
/// <inheritdoc />
public override string Name => "Trakt";
/// <inheritdoc />
public override Guid Id => new Guid("4fe3201e-d6ae-4f2e-8917-e12bda571281");
/// <inheritdoc />
public override string Description
=> "Watch, rate, and discover media using Trakt. The HTPC just got more social.";
public static Plugin Instance { get; private set; }
public PluginConfiguration PluginConfiguration => Configuration;
public Dictionary<string, Task<bool>> PollingTasks { get; set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> 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"
}
};
}
}

View File

@ -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;
/// <summary>
/// Task that will Sync each users trakt.tv profile with their local library. This task will only include
/// watched states.
/// </summary>
public class SyncFromTraktTask : IScheduledTask
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<SyncFromTraktTask> _logger;
private readonly TraktApi _traktApi;
/// <summary>
/// Task that will Sync each users trakt.tv profile with their local library. This task will only include
/// watched states.
///
/// </summary>
public class SyncFromTraktTask : IScheduledTask
/// <param name="loggerFactory"></param>
/// <param name="userManager"></param>
/// <param name="userDataManager"> </param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
public SyncFromTraktTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<SyncFromTraktTask> _logger;
private readonly TraktApi _traktApi;
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncFromTraktTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
}
/// <summary>
///
/// </summary>
/// <param name="loggerFactory"></param>
/// <param name="userManager"></param>
/// <param name="userDataManager"> </param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
public SyncFromTraktTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
public string Key => "TraktSyncFromTraktTask";
public string Name => "Import playstates from Trakt.tv";
public string Description => "Sync Watched/Unwatched status from Trakt.tv for each Jellyfin user that has a configured Trakt account";
public string Category => "Trakt";
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
/// <summary>
/// Gather users and call <see cref="SyncTraktDataForUser"/>
/// </summary>
public async Task Execute(CancellationToken cancellationToken, IProgress<double> 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<SyncFromTraktTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), 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<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
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";
/// <summary>
/// Gather users and call <see cref="SyncTraktDataForUser"/>
/// </summary>
public async Task Execute(CancellationToken cancellationToken, IProgress<double> 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<double> progress, double percentPerUser)
{
var traktUser = UserHelper.GetTraktUser(user);
List<TraktMovieWatched> traktWatchedMovies;
List<TraktShowWatched> traktWatchedShows;
try
{
/*
* In order to be as accurate as possible. We need to download the users show collection & the users watched shows.
* It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched
* like they do for movies.
*/
traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
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<double> progress, double percentPerUser, CancellationToken cancellationToken)
{
var traktUser = UserHelper.GetTraktUser(user);
List<TraktMovieWatched> traktWatchedMovies;
List<TraktShowWatched> traktWatchedShows;
try
{
/*
* In order to be as accurate as possible. We need to download the users show collection & the users watched shows.
* It's unfortunate that trakt.tv doesn't explicitly supply a bulk method to determine shows that have not been watched
* like they do for movies.
*/
traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled");
throw;
}
_logger.LogInformation("Trakt.tv watched Movies count = {Count}", traktWatchedMovies.Count);
_logger.LogInformation("Trakt.tv watched Shows count = {Count}", traktWatchedShows.Count);
var mediaItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user) { IncludeItemTypes = new[] { 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<Movie>())
{
cancellationToken.ThrowIfCancellationRequested();
var matchedMovie = FindMatch(movie, traktWatchedMovies);
if (matchedMovie != null)
{
_logger.LogDebug("Movie is in Watched list {Name}", movie.Name);
var userData = _userDataManager.GetUserData(user.Id, movie);
bool changed = false;
DateTime? tLastPlayed = null;
if (DateTime.TryParse(matchedMovie.LastWatchedAt, out var value))
{
tLastPlayed = value;
}
// set movie as watched
if (!userData.Played)
{
userData.Played = true;
userData.LastPlayedDate = tLastPlayed ?? DateTime.Now;
changed = true;
}
// keep the highest play count
if (userData.PlayCount < matchedMovie.Plays)
{
userData.PlayCount = matchedMovie.Plays;
changed = true;
}
// Update last played if remote time is more recent
if (tLastPlayed != null && userData.LastPlayedDate < tLastPlayed)
{
userData.LastPlayedDate = tLastPlayed;
changed = true;
}
// Only process if there's a change
if (changed)
{
_userDataManager.SaveUserData(
user.Id,
movie,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
else
{
// _logger.LogInformation("Failed to match " + movie.Name);
}
_logger.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<string, SortOrder>(ItemSortBy.SeriesSortName, SortOrder.Ascending),
new ValueTuple<string, SortOrder>(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<Movie>())
foreach (var episode in mediaItems.OfType<Episode>())
{
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<Episode>())
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<TraktShowWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
public static TraktShowCollected FindMatch(Series item, IEnumerable<TraktShowCollected> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Show));
}
public static TraktMovieWatched FindMatch(BaseItem item, IEnumerable<TraktMovieWatched> results)
{
return results.FirstOrDefault(i => IsMatch(item, i.Movie));
}
public static IEnumerable<TraktMovieCollected> FindMatches(BaseItem item, IEnumerable<TraktMovieCollected> results)
{
return results.Where(i => IsMatch(item, i.Movie)).ToList();
}
public static bool IsMatch(BaseItem item, TraktMovie movie)
{
var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrWhiteSpace(imdb) &&
string.Equals(imdb, movie.Ids.Imdb, StringComparison.OrdinalIgnoreCase))
{
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<TraktShowWatched> 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<TraktShowCollected> 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<TraktMovieWatched> 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<TraktMovieCollected> FindMatches(BaseItem item, IEnumerable<TraktMovieCollected> 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;
}
}

View File

@ -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;
/// <summary>
/// Task that will Sync each users local library with their respective trakt.tv profiles. This task will only include
/// titles, watched states will be synced in other tasks.
/// </summary>
public class SyncLibraryTask : IScheduledTask
{
/// <summary>
/// Task that will Sync each users local library with their respective trakt.tv profiles. This task will only include
/// titles, watched states will be synced in other tasks.
/// </summary>
public class SyncLibraryTask : IScheduledTask
// private readonly IHttpClient _httpClient;
private readonly IUserManager _userManager;
private readonly ILogger<SyncLibraryTask> _logger;
private readonly TraktApi _traktApi;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
public SyncLibraryTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
//private readonly IHttpClient _httpClient;
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncLibraryTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
}
private readonly IUserManager _userManager;
public string Key => "TraktSyncLibraryTask";
private readonly ILogger<SyncLibraryTask> _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<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
public SyncLibraryTask(
ILoggerFactory loggerFactory,
IUserManager userManager,
IUserDataManager userDataManager,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem,
ILibraryManager libraryManager)
/// <summary>
/// Gather users and call <see cref="SyncUserLibrary"/>
/// </summary>
public async Task Execute(CancellationToken cancellationToken, IProgress<double> 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<SyncLibraryTask>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
_logger.LogInformation("No Users returned");
return;
}
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
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";
/// <summary>
/// Gather users and call <see cref="SyncUserLibrary"/>
/// </summary>
public async Task Execute(CancellationToken cancellationToken, IProgress<double> 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);
}
}
/// <summary>
/// Count media items and call <see cref="SyncMovies"/> and <see cref="SyncShows"/>
/// </summary>
/// <returns></returns>
private async Task SyncUserLibrary(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
await SyncMovies(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
await SyncShows(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Count media items and call <see cref="SyncMovies"/> and <see cref="SyncShows"/>
/// </summary>
/// <returns></returns>
private async Task SyncUserLibrary(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
await SyncMovies(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
await SyncShows(user, traktUser, progress.Split(2), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
private async Task SyncMovies(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
/*
* In order to sync watched status to trakt.tv we need to know what's been watched on Trakt already. This
* will stop us from endlessly incrementing the watched values on the site.
*/
var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser).ConfigureAwait(false);
var libraryMovies =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(Movie).Name },
IsVirtualItem = false,
OrderBy = new []
{
new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedMovies = new List<Movie>();
var playedMovies = new List<Movie>();
var unplayedMovies = new List<Movie>();
var decisionProgress = progress.Split(4).Split(libraryMovies.Count);
foreach (var child in libraryMovies)
{
cancellationToken.ThrowIfCancellationRequested();
var libraryMovie = child as Movie;
var userData = _userDataManager.GetUserData(user.Id, child);
if (traktUser.SynchronizeCollections)
{
// if movie is not collected, or (export media info setting is enabled and every collected matching movie has different metadata), collect it
var collectedMathingMovies = SyncFromTraktTask.FindMatches(libraryMovie, traktCollectedMovies).ToList();
if (!collectedMathingMovies.Any()
|| (traktUser.ExportMediaInfo
&& collectedMathingMovies.All(
collectedMovie => collectedMovie.MetadataIsDifferent(libraryMovie))))
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
private async Task SyncMovies(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
/*
* In order to sync watched status to trakt.tv we need to know what's been watched on Trakt already. This
* will stop us from endlessly incrementing the watched values on the site.
*/
var traktWatchedMovies = await _traktApi.SendGetAllWatchedMoviesRequest(traktUser).ConfigureAwait(false);
var traktCollectedMovies = await _traktApi.SendGetAllCollectedMoviesRequest(traktUser).ConfigureAwait(false);
var libraryMovies =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
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<Movie>();
var playedMovies = new List<Movie>();
var unplayedMovies = new List<Movie>();
_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<Movie> movies,
ISplittableProgress<double> 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<Movie> playedMovies,
ISplittableProgress<double> 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);
}
}
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
private async Task SyncShows(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
var traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
var traktCollectedShows = await _traktApi.SendGetCollectedShowsRequest(traktUser).ConfigureAwait(false);
var episodeItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(Episode).Name },
IsVirtualItem = false,
OrderBy = new[]
{
new ValueTuple<string, SortOrder>(ItemSortBy.SeriesSortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedEpisodes = new List<Episode>();
var playedEpisodes = new List<Episode>();
var unplayedEpisodes = new List<Episode>();
var decisionProgress = progress.Split(4).Split(episodeItems.Count);
foreach (var child in episodeItems)
{
cancellationToken.ThrowIfCancellationRequested();
var episode = child as Episode;
var userData = _userDataManager.GetUserData(user.Id, episode);
var isPlayedTraktTv = false;
var traktWatchedShow = SyncFromTraktTask.FindMatch(episode.Series, traktWatchedShows);
if (traktWatchedShow?.seasons != null && traktWatchedShow.seasons.Count > 0)
{
isPlayedTraktTv =
traktWatchedShow.seasons.Any(
season =>
season.number == episode.GetSeasonNumber() && season.episodes != null
&& season.episodes.Any(te => te.number == episode.IndexNumber && te.plays > 0));
}
// if the show has been played locally and is unplayed on trakt.tv then add it to the list
if (userData != null && userData.Played && !isPlayedTraktTv)
if (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<Movie> movies,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Movies to {State} Collection {Count}", collected ? "add to" : "remove from", movies.Count);
if (movies.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendLibraryUpdateAsync(
movies,
traktUser,
collected ? EventType.Add : EventType.Remove,
cancellationToken)
.ConfigureAwait(false);
if (dataContracts != null)
{
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<Movie> playedMovies,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Movies to set {State}watched: {Count}", seen ? string.Empty : "un", playedMovies.Count);
if (playedMovies.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendMoviePlaystateUpdates(playedMovies, traktUser, seen, cancellationToken).ConfigureAwait(false);
if (dataContracts != null)
{
foreach (var traktSyncResponse in dataContracts)
{
LogTraktResponseDataContract(traktSyncResponse);
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Error updating movie play states");
}
progress.Report(100);
}
}
/// <summary>
/// Sync watched and collected status of <see cref="Movie"/>s with trakt.
/// </summary>
private async Task SyncShows(
Jellyfin.Data.Entities.User user,
TraktUser traktUser,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
var traktWatchedShows = await _traktApi.SendGetWatchedShowsRequest(traktUser).ConfigureAwait(false);
var traktCollectedShows = await _traktApi.SendGetCollectedShowsRequest(traktUser).ConfigureAwait(false);
var episodeItems =
_libraryManager.GetItemList(
new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { nameof(Episode) },
IsVirtualItem = false,
OrderBy = new[]
{
(ItemSortBy.SeriesSortName, SortOrder.Ascending)
}
})
.Where(x => _traktApi.CanSync(x, traktUser))
.ToList();
var collectedEpisodes = new List<Episode>();
var playedEpisodes = new List<Episode>();
var unplayedEpisodes = new List<Episode>();
var decisionProgress = progress.Split(4).Split(episodeItems.Count);
foreach (var child in episodeItems)
{
cancellationToken.ThrowIfCancellationRequested();
var episode = child as Episode;
var userData = _userDataManager.GetUserData(user.Id, episode);
var isPlayedTraktTv = false;
var traktWatchedShow = SyncFromTraktTask.FindMatch(episode.Series, traktWatchedShows);
if (traktWatchedShow?.Seasons != null && traktWatchedShow.Seasons.Count > 0)
{
isPlayedTraktTv =
traktWatchedShow.Seasons.Any(
season =>
season.Number == episode.GetSeasonNumber() && season.Episodes != null
&& season.Episodes.Any(te => te.Number == episode.IndexNumber && te.Plays > 0));
}
// if the show has been played locally and is unplayed on trakt.tv then add it to the list
if (userData != null && userData.Played && !isPlayedTraktTv)
{
if (traktUser.PostWatchedHistory)
{
playedEpisodes.Add(episode);
}
else if (!traktUser.SkipUnwatchedImportFromTrakt)
{
if (userData.Played)
{
userData.Played = false;
_userDataManager.SaveUserData(
user.Id,
episode,
userData,
UserDataSaveReason.Import,
cancellationToken);
}
}
}
else if (userData != null && !userData.Played && isPlayedTraktTv && traktUser.PostUnwatchedHistory)
{
// If the show has not been played locally but is played on trakt.tv then add it to the unplayed list
unplayedEpisodes.Add(episode);
}
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<Episode> playedEpisodes,
ISplittableProgress<double> 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<Episode> playedEpisodes,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Episodes to set {State}watched: {Count}", seen ? string.Empty : "un", playedEpisodes.Count);
if (playedEpisodes.Count > 0)
{
try
{
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<Episode> collectedEpisodes,
ISplittableProgress<double> 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<Episode> collectedEpisodes,
ISplittableProgress<double> progress,
CancellationToken cancellationToken)
{
_logger.LogInformation("Episodes to add to Collection: {Count}", collectedEpisodes.Count);
if (collectedEpisodes.Count > 0)
{
try
{
var dataContracts =
await _traktApi.SendLibraryUpdateAsync(
collectedEpisodes,
traktUser,
collected ? EventType.Add : EventType.Remove,
cancellationToken)
.ConfigureAwait(false);
if (dataContracts != null)
{
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);
}
}
}

View File

@ -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;
/// <summary>
/// All communication between the server and the plugins server instance should occur in this class.
/// </summary>
public class ServerMediator : IServerEntryPoint
{
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<ServerMediator> _logger;
private readonly UserDataManagerEventsHelper _userDataManagerEventsHelper;
private readonly IUserDataManager _userDataManager;
private TraktApi _traktApi;
private LibraryManagerEventsHelper _libraryManagerEventsHelper;
/// <summary>
/// All communication between the server and the plugins server instance should occur in this class.
///
/// </summary>
public class ServerMediator : IServerEntryPoint
/// <param name="sessionManager"> </param>
/// <param name="userDataManager"></param>
/// <param name="libraryManager"> </param>
/// <param name="logger"></param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
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<ServerMediator> _logger;
private TraktApi _traktApi;
private LibraryManagerEventsHelper _libraryManagerEventsHelper;
private readonly UserDataManagerEventsHelper _userDataManagerEventsHelper;
private IUserDataManager _userDataManager;
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_userDataManager = userDataManager;
/// <summary>
///
/// </summary>
/// <param name="sessionManager"> </param>
/// <param name="userDataManager"></param>
/// <param name="libraryManager"> </param>
/// <param name="logger"></param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
public ServerMediator(
ISessionManager sessionManager,
IUserDataManager userDataManager,
ILibraryManager libraryManager,
ILoggerFactory loggerFactory,
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
IFileSystem fileSystem)
_logger = loggerFactory.CreateLogger<ServerMediator>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
_libraryManagerEventsHelper = new LibraryManagerEventsHelper(loggerFactory.CreateLogger<LibraryManagerEventsHelper>(), _traktApi);
_userDataManagerEventsHelper = new UserDataManagerEventsHelper(loggerFactory.CreateLogger<UserDataManagerEventsHelper>(), _traktApi);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<ServerMediator>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
_libraryManagerEventsHelper = new LibraryManagerEventsHelper(loggerFactory.CreateLogger<LibraryManagerEventsHelper>(), _traktApi);
_userDataManagerEventsHelper = new UserDataManagerEventsHelper(loggerFactory.CreateLogger<UserDataManagerEventsHelper>(), _traktApi);
return;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
/// <inheritdoc />
public Task RunAsync()
{
_userDataManager.UserDataSaved += OnUserDataSaved;
_sessionManager.PlaybackStart += KernelPlaybackStart;
_sessionManager.PlaybackStopped += KernelPlaybackStopped;
_libraryManager.ItemAdded += LibraryManagerItemAdded;
_libraryManager.ItemRemoved += LibraryManagerItemRemoved;
return Task.CompletedTask;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
/// <summary>
/// Let Trakt.tv know the user has started to watch something
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
/// <inheritdoc />
public Task RunAsync()
{
_userDataManager.UserDataSaved += OnUserDataSaved;
_sessionManager.PlaybackStart += KernelPlaybackStart;
_sessionManager.PlaybackStopped += KernelPlaybackStopped;
_libraryManager.ItemAdded += LibraryManagerItemAdded;
_libraryManager.ItemRemoved += LibraryManagerItemRemoved;
return Task.CompletedTask;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
/// <summary>
/// Let Trakt.tv know the user has started to watch something
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
}
/// <summary>
/// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has
/// completed watching the item.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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");
}
}
/// <summary>
/// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has
/// completed watching the item.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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");
}
}
/// <inheritdoc />
public void Dispose()
/// <inheritdoc />
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();
}
}
}

View File

@ -4,6 +4,13 @@
<TargetFramework>net6.0</TargetFramework>
<AssemblyVersion>12.0.0.0</AssemblyVersion>
<FileVersion>12.0.0.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>disable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<!-- Suppress all xmldoc warnings -->
<NoWarn>CS1591,CS1572,CS1573,CS1574,SA1629,SA1606,SA1611,SA1614,SA1615,SA1616,SA1642</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -16,7 +23,10 @@
<ItemGroup>
<PackageReference Include="Jellyfin.Data" Version="10.*-*" />
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

118
jellyfin.ruleset Normal file
View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
<Rule Id="SA1011" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
<!-- disable warning SA1118: Parameter must not span multiple lines. -->
<Rule Id="SA1118" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
<Rule Id="SA1202" Action="None" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
<Rule Id="SA1515" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
<!-- disable warning SA1602: Enumeration items should be documented -->
<Rule Id="SA1602" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
<!-- error on CA1063: Implement IDisposable correctly -->
<Rule Id="CA1063" Action="Error" />
<!-- error on CA1305: Specify IFormatProvider -->
<Rule Id="CA1305" Action="Error" />
<!-- error on CA1307: Specify StringComparison for clarity -->
<Rule Id="CA1307" Action="Error" />
<!-- error on CA1309: Use ordinal StringComparison -->
<Rule Id="CA1309" Action="Error" />
<!-- error on CA1725: Parameter names should match base declaration -->
<Rule Id="CA1725" Action="Error" />
<!-- error on CA1725: Call async methods when in an async method -->
<Rule Id="CA1727" Action="Error" />
<!-- error on CA1813: Avoid unsealed attributes -->
<Rule Id="CA1813" Action="Error" />
<!-- error on CA1843: Do not use 'WaitAll' with a single task -->
<Rule Id="CA1843" Action="Error" />
<!-- error on CA1845: Use span-based 'string.Concat' -->
<Rule Id="CA1845" Action="Error" />
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
<!-- error on CA2254: Template should be a static expression -->
<Rule Id="CA2254" Action="Error" />
<!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
<Rule Id="CA1014" Action="Info" />
<!-- disable warning CA1024: Use properties where appropriate -->
<Rule Id="CA1024" Action="Info" />
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1040: Avoid empty interfaces -->
<Rule Id="CA1040" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<!-- TODO: enable when false positives are fixed -->
<!-- disable warning CA1508: Avoid dead conditional code -->
<Rule Id="CA1508" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1724: Type names should not match namespaces -->
<Rule Id="CA1724" Action="Info" />
<!-- disable warning CA1805: Do not initialize unnecessarily -->
<Rule Id="CA1805" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
If so, remove the code from the assembly.
If this class is intended to contain only static members, make it static -->
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
<!-- disable warning CA2253: Named placeholders should not be numeric values -->
<Rule Id="CA2253" Action="Info" />
<!-- disable warning CA5394: Do not use insecure randomness -->
<Rule Id="CA5394" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
<Rule Id="CA1055" Action="None" />
<!-- disable warning CA1056: URI properties should not be strings -->
<Rule Id="CA1056" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
<!-- disable warning CA1848: Use the LoggerMessage delegates -->
<Rule Id="CA1848" Action="None" />
<!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments -->
<Rule Id="CA2101" Action="None" />
<!-- disable warning CA2234: Pass System.Uri objects instead of strings -->
<Rule Id="CA2234" Action="None" />
</Rules>
</RuleSet>