From 25d84797fcd3e3d6d5edb657747fb665c142a4bc Mon Sep 17 00:00:00 2001 From: Alexander Rufenach Date: Fri, 4 Jun 2021 04:41:31 -0400 Subject: [PATCH] Added XML comments --- Jellyfin.Plugin.AnilistSync.sln | 3 - Jellyfin.Plugin.AnilistSync/API/AnilistApi.cs | 76 ++++++++++- Jellyfin.Plugin.AnilistSync/API/ApiModel.cs | 123 ++++++++++++++++++ Jellyfin.Plugin.AnilistSync/API/Endpoints.cs | 17 +++ .../Configuration/UserConfig.cs | 11 +- .../Jellyfin.Plugin.AnilistSync.csproj | 4 - Jellyfin.Plugin.AnilistSync/Plugin.cs | 28 +++- .../Services/PlaybackScrobbler.cs | 22 +++- 8 files changed, 260 insertions(+), 24 deletions(-) diff --git a/Jellyfin.Plugin.AnilistSync.sln b/Jellyfin.Plugin.AnilistSync.sln index cf16231..e20694d 100644 --- a/Jellyfin.Plugin.AnilistSync.sln +++ b/Jellyfin.Plugin.AnilistSync.sln @@ -6,9 +6,6 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.AnilistSync", "Jellyfin.Plugin.AnilistSync\Jellyfin.Plugin.AnilistSync.csproj", "{2C1895EC-A7E1-48B6-A2E0-3E847D1FB748}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{785517C0-5B12-4228-9D64-2495680C537A}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Jellyfin.Plugin.AnilistSync/API/AnilistApi.cs b/Jellyfin.Plugin.AnilistSync/API/AnilistApi.cs index 680d7b6..07bac00 100644 --- a/Jellyfin.Plugin.AnilistSync/API/AnilistApi.cs +++ b/Jellyfin.Plugin.AnilistSync/API/AnilistApi.cs @@ -12,15 +12,31 @@ using Jellyfin.Plugin.AnilistSync.API.Exceptions; namespace Jellyfin.Plugin.AnilistSync.API { + /// + /// Anilist API. + /// public class AnilistApi { + // Interfaces // private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly JsonSerializerOptions _jsonSerializerOptions; + // Base URLs // + + /// + /// Base url for OAUth uses. + /// public const string BaseOauthUrl = @"https://anilist.co/api/v2"; + + /// + /// Base url for GraphQL queries. + /// public const string GraphQLUrl = @"https://graphql.anilist.co"; + /// + /// Generic GraphQL query string used for list updates. + /// public const string QueryString = @" mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { SaveMediaListEntry(id: $id, mediaId: $mediaId, status: $status, progress: $progress ) { @@ -30,10 +46,21 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { progress } }"; - + /// + /// Anilist client ID. + /// public const int ClientId = 5659; + + /// + /// Secret. + /// public const string Secret = @"h7ym2GZ6OjrdJ9sygDP7kDnQWsBdTwp4U8s7pt4X"; + /// + /// Initializes a new instance of class. + /// + /// Instance of the interface. + /// Instance of the interface. public AnilistApi(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; @@ -41,7 +68,11 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { _jsonSerializerOptions = JsonDefaults.GetOptions(); } - + /// + /// Get token. + /// + /// code. + /// public async Task GetToken(string? code) { string uri = @"/oauth/token"; @@ -58,6 +89,11 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return await responseMessage.Content.ReadFromJsonAsync(_jsonSerializerOptions); } + /// + /// Gets total episodes of specified Anilist mediaID. + /// + /// Anilist ID. + /// containing total episodes parameter. public async Task GetEpisodes(int? anilistId) { GraphQLBody content = new GraphQLBody { @@ -73,6 +109,11 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return await Post(content); } + /// + /// Gets the name and ID of the currently authenticated user. + /// + /// User token. + /// containing object. public async Task GetUser(string? userToken) { GraphQLBody content = new GraphQLBody { @@ -82,6 +123,12 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return await PostWithAuth(userToken, content); } + /// + /// Gets user's list ID of specified Anilist mediaID + /// + /// User token. + /// Anilist mediaID + /// containing public async Task GetListEntry(string? userToken, int? anilistId) { GraphQLBody content = new GraphQLBody @@ -98,6 +145,13 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return await PostWithAuth(userToken, content); } + /// + /// Updates status of specified list item + /// + /// User token. + /// List ID + /// Status. + /// containing updated list item. public async Task PostListStatusUpdate(string? userToken, int? listId, MediaListStatus? status) { GraphQLBody content = new GraphQLBody @@ -114,6 +168,13 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return await PostWithAuth(userToken, content); } + /// + /// Updates progress of specified list item. + /// + /// User token. + /// List ID> + /// Progress. + /// containing updated list item. public async Task PostListProgressUpdate(string? userToken, int? listId, int? progress) { GraphQLBody content = new GraphQLBody @@ -130,6 +191,11 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return await PostWithAuth(userToken, content); } + /// + /// API private GraphQL Post WITHOUT authentication. + /// + /// containing query and variables + /// containing API response. public async Task Post(GraphQLBody graphQL) { var responseMessage = await _httpClientFactory.CreateClient(NamedClient.Default).PostAsJsonAsync(GraphQLUrl, graphQL, _jsonSerializerOptions); @@ -144,6 +210,12 @@ mutation ($id: Int, $mediaId: Int, $status: MediaListStatus, $progress: Int,) { return root; } + /// + /// API private GraphQL Post WITH authentication. + /// + /// User token. + /// containing query and variables + /// containing API response. public async Task PostWithAuth(string? userToken, GraphQLBody graphQL) { using var requestMessage = new HttpRequestMessage diff --git a/Jellyfin.Plugin.AnilistSync/API/ApiModel.cs b/Jellyfin.Plugin.AnilistSync/API/ApiModel.cs index ffb3d45..73f4ee9 100644 --- a/Jellyfin.Plugin.AnilistSync/API/ApiModel.cs +++ b/Jellyfin.Plugin.AnilistSync/API/ApiModel.cs @@ -2,126 +2,249 @@ namespace Jellyfin.Plugin.AnilistSync.API { + /// + /// Media list status enum, + /// public enum MediaListStatus { + /// Currently watching. CURRENT, + /// Planning to watch. PLANNING, + /// Completed. COMPLETED, + /// Dropped. DROPPED, + /// n hold. PAUSED, + /// Rewatching. REPEATING } + /// + /// Root JSON object. + /// public class RootObject { + /// + /// Gets or sets data. + /// [JsonPropertyName("data")] public Data? Data { get; set; } + /// + /// Gets or sets errors. + /// [JsonPropertyName("errors")] public AnilistError[]? Errors { get; set; } } + /// + /// Data JSON Object. + /// public class Data { + /// + /// Gets or sets user. + /// [JsonPropertyName("Viewer")] public User? User { get; set; } + /// + /// Gets or sets list entry. + /// [JsonPropertyName("SaveMediaListEntry")] public ListEntry? ListEntry { get; set; } + /// + /// Gets or sets media. + /// [JsonPropertyName("Media")] public Media? Media { get; set; } } + /// + /// Error object. + /// public class AnilistError { + /// + /// Gets or sets error message. + /// [JsonPropertyName("message")] public string? ErrorMessage { get; set; } + /// + /// Gets or sets error status. + /// [JsonPropertyName("status")] public int? ErrorStatus { get; set; } + /// + /// Gets or sets error locations. + /// [JsonPropertyName("locations")] public Location[]? Locations { get; set; } } + /// + /// Locations of error(s) object. + /// public class Location { + /// + /// Gets or sets line. + /// [JsonPropertyName("line")] public int? Line { get; set; } + /// + /// Gets or sets column. + /// [JsonPropertyName("column")] public int? Column { get; set; } } + /// + /// List Entry object. + /// public class ListEntry { + /// + /// Gets or sets mediaId. + /// [JsonPropertyName("mediaId")] public int? MediaId { get; set; } + /// + /// Gets or sets ID. + /// [JsonPropertyName("id")] public int? Id { get; set; } + /// + /// Gets or sets progress. + /// [JsonPropertyName("progress")] public int? Progress { get; set; } + /// + /// Gets or sets status. + /// [JsonPropertyName("status")] public MediaListStatus? Status { get; set; } } + /// + /// Media object. + /// public class Media { + /// + /// Gets or sets episodes. + /// [JsonPropertyName("episodes")] public int? Episodes { get; set; } } + /// + /// User object. + /// public class User { + /// + /// Gets or sets user ID. + /// [JsonPropertyName("id")] public int? Id { get; set; } + /// + /// Gets or sets user name. + /// [JsonPropertyName("name")] public string? Name { get; set; } } + /// + /// GraphQLBody object. + /// public class GraphQLBody { + /// + /// Gets or sets GraphQL query string. + /// [JsonPropertyName("query")] public string? Query { get; set; } + /// + /// Gets or sets GraphQL variables. + /// [JsonPropertyName("variables")] public ListEntry? Variables { get; set; } } + /// + /// OAuth response object. + /// public class OAuth { + /// + /// Gets or sets grant type. + /// [JsonPropertyName("grant_type")] public string? GrantType { get; set; } + /// + /// Gets or sets client ID. + /// [JsonPropertyName("client_id")] public int? ClientId { get; set; } + /// + /// Gets or sets client secret. + /// [JsonPropertyName("client_secret")] public string? ClientSecret { get; set; } + /// + /// Gets or sets redirect URI. + /// [JsonPropertyName("redirect_uri")] public string? RedirectUri { get; set; } + /// + /// Gets or sets code. + /// [JsonPropertyName("code")] public string? Code { get; set; } } + /// + /// Code response object. + /// public class CodeResponse { + /// + /// Gets or sets token type. + /// [JsonPropertyName("token_type")] public string? TokenType { get; set; } + /// + /// Gets or sets expires in. + /// [JsonPropertyName("expires_in")] public int? ExpiresIn { get; set; } + /// + /// Gets or sets access token. + /// [JsonPropertyName("access_token")] public string? AccessToken { get; set; } + /// + /// Gets or sets refresh token. + /// [JsonPropertyName("refresh_token")] public string? RefreshToken { get; set; } } diff --git a/Jellyfin.Plugin.AnilistSync/API/Endpoints.cs b/Jellyfin.Plugin.AnilistSync/API/Endpoints.cs index cedb085..89389e7 100644 --- a/Jellyfin.Plugin.AnilistSync/API/Endpoints.cs +++ b/Jellyfin.Plugin.AnilistSync/API/Endpoints.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Plugin.AnilistSync.API { + /// + /// Anilist endpoints. + /// [ApiController] [Authorize(Policy = "DefaultAuthorization")] [Route("AnilistSync")] @@ -12,17 +15,31 @@ namespace Jellyfin.Plugin.AnilistSync.API { private readonly AnilistApi _anilistApi; + /// + /// Initializes a new instacne fo the class. + /// + /// public Endpoints(AnilistApi anilistApi) { _anilistApi = anilistApi; } + /// + /// Gets the OAuth token. + /// + /// User code. + /// Code response containing token. [HttpGet("oauth/token/{userCode}")] public async Task> GetToken([FromRoute] string userCode) { return await _anilistApi.GetToken(userCode); } + /// + /// Gets the currently authenticated user + /// + /// The user ID. + /// Root object containing User object [HttpGet("users/settings/{userId}")] public async Task> GetUser([FromRoute] Guid userId) { diff --git a/Jellyfin.Plugin.AnilistSync/Configuration/UserConfig.cs b/Jellyfin.Plugin.AnilistSync/Configuration/UserConfig.cs index 50d265f..7b25416 100644 --- a/Jellyfin.Plugin.AnilistSync/Configuration/UserConfig.cs +++ b/Jellyfin.Plugin.AnilistSync/Configuration/UserConfig.cs @@ -18,20 +18,23 @@ namespace Jellyfin.Plugin.AnilistSync.Configuration ScrobblePercentage = 80; ScrobbleNowWatchingPercentage = 5; MinLength = 5; - UserToken = string.Empty; // Todo: check if token is still valid + UserToken = string.Empty; ScrobbleTimeout = 30; } /// - /// Gets or sets a value indicating whether scrobble movies. + /// Gets or sets a value indicating whether to scrobble movies. /// public bool ScrobbleMovies { get; set; } /// - /// Gets or sets a value indicating whether scrobble shows. + /// Gets or sets a value indicating whether to scrobble shows. /// public bool ScrobbleShows { get; set; } + /// + /// Gets or sets a value indicating whether to scrobble rewatches. + /// public bool ScrobbleRewatches { get; set; } /// @@ -55,7 +58,7 @@ namespace Jellyfin.Plugin.AnilistSync.Configuration /// /// Gets or sets user token. /// - public string UserToken { get; set; } // Is the user logged in + public string UserToken { get; set; } /// /// Gets or sets scrobble timeout. diff --git a/Jellyfin.Plugin.AnilistSync/Jellyfin.Plugin.AnilistSync.csproj b/Jellyfin.Plugin.AnilistSync/Jellyfin.Plugin.AnilistSync.csproj index 4bbbc21..14b56d1 100644 --- a/Jellyfin.Plugin.AnilistSync/Jellyfin.Plugin.AnilistSync.csproj +++ b/Jellyfin.Plugin.AnilistSync/Jellyfin.Plugin.AnilistSync.csproj @@ -37,9 +37,5 @@ - - - - diff --git a/Jellyfin.Plugin.AnilistSync/Plugin.cs b/Jellyfin.Plugin.AnilistSync/Plugin.cs index 4b0121b..31bc752 100644 --- a/Jellyfin.Plugin.AnilistSync/Plugin.cs +++ b/Jellyfin.Plugin.AnilistSync/Plugin.cs @@ -11,23 +11,37 @@ using MediaBrowser.Model.Serialization; namespace Jellyfin.Plugin.AnilistSync { - + /// + /// Anilist Scrobbler. + /// public class Plugin : BasePlugin, IHasWebPages { - public override string Name => "AnilistSync"; - public override Guid Id => Guid.Parse("18c2a8ea-afa0-4a0b-aa94-072b492ab80b"); - public override string Description => "Jellyfin plugin to scrobble to Anilist"; - public Version version = new Version("2.2.0.0"); - + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { Instance = this; } + /// + public override string Name => "AnilistSync"; + + /// + public override Guid Id => Guid.Parse("18c2a8ea-afa0-4a0b-aa94-072b492ab80b"); + + /// + public override string Description => "Jellyfin plugin to scrobble to Anilist"; + + /// + /// Gets the current instance of the plugin + /// public static Plugin? Instance { get; private set; } - + /// public IEnumerable GetPages() { return new[] diff --git a/Jellyfin.Plugin.AnilistSync/Services/PlaybackScrobbler.cs b/Jellyfin.Plugin.AnilistSync/Services/PlaybackScrobbler.cs index 4af6893..5f4f2fe 100644 --- a/Jellyfin.Plugin.AnilistSync/Services/PlaybackScrobbler.cs +++ b/Jellyfin.Plugin.AnilistSync/Services/PlaybackScrobbler.cs @@ -15,15 +15,23 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.AnilistSync.Services { - + /// + /// Playback progress scrobbler. + /// public class PlaybackScrobbler : IServerEntryPoint { - private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions + private readonly ISessionManager _sessionManager; // Needed to set up the startPlayBack and endPlayBack functions private readonly ILogger _logger; private readonly Dictionary _lastScrobbled; // Library ID of last scrobbled item private readonly AnilistApi _anilistApi; private DateTime _nextTry; + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the . public PlaybackScrobbler(ISessionManager sessionManager, ILogger logger, AnilistApi anilistApi) { _sessionManager = sessionManager; @@ -33,6 +41,7 @@ namespace Jellyfin.Plugin.AnilistSync.Services _nextTry = DateTime.UtcNow; } + /// public Task RunAsync() { _sessionManager.PlaybackProgress += OnPlaybackProgress; @@ -40,12 +49,17 @@ namespace Jellyfin.Plugin.AnilistSync.Services return Task.CompletedTask; } + /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// + /// Dispose. + /// + /// Dispoe all resources. protected virtual void Dispose(bool disposing) { if (disposing) @@ -238,11 +252,11 @@ namespace Jellyfin.Plugin.AnilistSync.Services if (currentIndex == totalEpisodes) { status = MediaListStatus.COMPLETED; - var statusResponse = await _anilistApi.PostListStatusUpdate(userConfig.UserToken, listEntry?.Id, status); + await _anilistApi.PostListStatusUpdate(userConfig.UserToken, listEntry?.Id, status); } else { - var response = await _anilistApi.PostListProgressUpdate(userConfig.UserToken, listEntry?.Id, currentIndex); + await _anilistApi.PostListProgressUpdate(userConfig.UserToken, listEntry?.Id, currentIndex); } _logger.LogInformation("Scrobbled episode: ({currentIndex} of {totalEpisodes}) for Anilist ID: ({anilistId})", currentIndex, totalEpisodes, anilistId); _logger.LogInformation("Watch status: {status}", status);