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