mirror of
https://github.com/jellyfin/jellyfin-plugin-trakt.git
synced 2024-11-26 23:30:25 +00:00
Remove all warnings in project
This commit is contained in:
parent
d46aa80b50
commit
5f4829fac1
@ -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}]
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace Trakt.Api.DataContracts.BaseModel
|
||||
namespace Trakt.Api.DataContracts.BaseModel;
|
||||
|
||||
public class TraktEpisodeId : TraktTVId
|
||||
{
|
||||
public class TraktEpisodeId : TraktTVId
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
namespace Trakt.Api.DataContracts.BaseModel
|
||||
namespace Trakt.Api.DataContracts.BaseModel;
|
||||
|
||||
public class TraktMovieId : TraktIMDBandTMDBId
|
||||
{
|
||||
public class TraktMovieId : TraktIMDBandTMDBId
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
|
||||
namespace Trakt.Api.DataContracts.BaseModel
|
||||
namespace Trakt.Api.DataContracts.BaseModel;
|
||||
|
||||
public class TraktShowId : TraktTVId
|
||||
{
|
||||
public class TraktShowId : TraktTVId
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
15
Trakt/Api/DataContracts/Scrobble/SocialMedia.cs
Normal file
15
Trakt/Api/DataContracts/Scrobble/SocialMedia.cs
Normal 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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
21
Trakt/Api/DataContracts/Sync/Items.cs
Normal file
21
Trakt/Api/DataContracts/Sync/Items.cs
Normal 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; }
|
||||
}
|
26
Trakt/Api/DataContracts/Sync/NotFoundObjects.cs
Normal file
26
Trakt/Api/DataContracts/Sync/NotFoundObjects.cs
Normal 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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
17
Trakt/Api/DataContracts/Sync/Ratings/TraktSeasonRated.cs
Normal file
17
Trakt/Api/DataContracts/Sync/Ratings/TraktSeasonRated.cs
Normal 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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
7
Trakt/Api/DataContracts/Sync/TraktSyncCollected.cs
Normal file
7
Trakt/Api/DataContracts/Sync/TraktSyncCollected.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Trakt.Api.DataContracts.Sync.Collection;
|
||||
|
||||
namespace Trakt.Api.DataContracts.Sync;
|
||||
|
||||
public class TraktSyncCollected : TraktSync<TraktMovieCollected, TraktShowCollected, TraktEpisodeCollected>
|
||||
{
|
||||
}
|
7
Trakt/Api/DataContracts/Sync/TraktSyncRated.cs
Normal file
7
Trakt/Api/DataContracts/Sync/TraktSyncRated.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Trakt.Api.DataContracts.Sync.Ratings;
|
||||
|
||||
namespace Trakt.Api.DataContracts.Sync;
|
||||
|
||||
public class TraktSyncRated : TraktSync<TraktMovieRated, TraktShowRated, TraktEpisodeRated>
|
||||
{
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
7
Trakt/Api/DataContracts/Sync/TraktSyncWatched.cs
Normal file
7
Trakt/Api/DataContracts/Sync/TraktSyncWatched.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Trakt.Api.DataContracts.Sync.Watched;
|
||||
|
||||
namespace Trakt.Api.DataContracts.Sync;
|
||||
|
||||
public class TraktSyncWatched : TraktSync<TraktMovieWatched, TraktShowWatched, TraktEpisodeWatched>
|
||||
{
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
15
Trakt/Api/DataContracts/Users/Watched/Episode.cs
Normal file
15
Trakt/Api/DataContracts/Users/Watched/Episode.cs
Normal 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; }
|
||||
}
|
16
Trakt/Api/DataContracts/Users/Watched/Season.cs
Normal file
16
Trakt/Api/DataContracts/Users/Watched/Season.cs
Normal 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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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}";
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
namespace Trakt
|
||||
{
|
||||
public enum MediaStatus
|
||||
{
|
||||
Watching,
|
||||
Paused,
|
||||
Stop
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
8
Trakt/Helpers/EventType.cs
Normal file
8
Trakt/Helpers/EventType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Trakt.Helpers;
|
||||
|
||||
public enum EventType
|
||||
{
|
||||
Add,
|
||||
Remove,
|
||||
Update
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
13
Trakt/Helpers/LibraryEvent.cs
Normal file
13
Trakt/Helpers/LibraryEvent.cs
Normal 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; }
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
Trakt/Helpers/UserDataPackage.cs
Normal file
33
Trakt/Helpers/UserDataPackage.cs
Normal 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; }
|
||||
}
|
44
Trakt/Helpers/UserHelper.cs
Normal file
44
Trakt/Helpers/UserHelper.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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
8
Trakt/MediaStatus.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Trakt;
|
||||
|
||||
public enum MediaStatus
|
||||
{
|
||||
Watching,
|
||||
Paused,
|
||||
Stop
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
118
jellyfin.ruleset
Normal 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>
|
Loading…
Reference in New Issue
Block a user