From 84a1d0934e217321e9ba5544c28af0660c7723f4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 24 Feb 2021 10:29:09 -0700 Subject: [PATCH] Support Jellyfin 10.7 --- Jellyfin.Plugin.Simkl/API/Endpoints.cs | 67 +++++ .../API/Exceptions/InvalidTokenException.cs | 6 +- Jellyfin.Plugin.Simkl/API/GetPin.cs | 14 - Jellyfin.Plugin.Simkl/API/GetPinStatus.cs | 19 -- Jellyfin.Plugin.Simkl/API/GetUserSettings.cs | 22 -- Jellyfin.Plugin.Simkl/API/Objects/Season.cs | 12 +- .../API/Objects/ShowEpisode.cs | 7 +- .../API/Objects/SimklEpisode.cs | 20 +- .../API/Objects/SimklFile.cs | 11 +- .../API/Objects/SimklHistory.cs | 16 +- Jellyfin.Plugin.Simkl/API/Objects/SimklIds.cs | 38 +-- .../API/Objects/SimklMediaObject.cs | 9 +- .../API/Objects/SimklMovie.cs | 24 +- .../API/Objects/SimklMovieIds.cs | 19 +- .../API/Objects/SimklShow.cs | 32 +-- .../API/Objects/SimklShowIds.cs | 19 +- Jellyfin.Plugin.Simkl/API/Objects/User.cs | 7 +- .../API/Objects/UserSettings.cs | 10 +- .../API/Responses/CodeResponse.cs | 12 +- .../API/Responses/CodeStatusResponse.cs | 6 +- .../API/Responses/SearchFileResponse.cs | 8 +- .../API/Responses/SyncHistoryNotFound.cs | 9 +- .../API/Responses/SyncHistoryResponse.cs | 4 +- Jellyfin.Plugin.Simkl/API/ServerEndpoint.cs | 75 ----- Jellyfin.Plugin.Simkl/API/SimklApi.cs | 183 ++++++------ .../Configuration/PluginConfiguration.cs | 4 +- .../Configuration/configPage.html | 272 +++++++++++------- .../Jellyfin.Plugin.Simkl.csproj | 13 +- .../PluginServiceRegistrator.cs | 16 ++ .../Services/PlaybackScrobbler.cs | 178 ++++++++++++ Jellyfin.Plugin.Simkl/Services/Scrobbler.cs | 206 ------------- .../Services/SimklNotificationsFactory.cs | 86 ------ Jellyfin.Plugin.Simkl/SimklPlugin.cs | 4 +- README.md | 20 +- build.yaml | 2 +- 35 files changed, 699 insertions(+), 751 deletions(-) create mode 100644 Jellyfin.Plugin.Simkl/API/Endpoints.cs delete mode 100644 Jellyfin.Plugin.Simkl/API/GetPin.cs delete mode 100644 Jellyfin.Plugin.Simkl/API/GetPinStatus.cs delete mode 100644 Jellyfin.Plugin.Simkl/API/GetUserSettings.cs delete mode 100644 Jellyfin.Plugin.Simkl/API/ServerEndpoint.cs create mode 100644 Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs create mode 100644 Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs delete mode 100644 Jellyfin.Plugin.Simkl/Services/Scrobbler.cs delete mode 100644 Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs diff --git a/Jellyfin.Plugin.Simkl/API/Endpoints.cs b/Jellyfin.Plugin.Simkl/API/Endpoints.cs new file mode 100644 index 0000000..00a8582 --- /dev/null +++ b/Jellyfin.Plugin.Simkl/API/Endpoints.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Jellyfin.Plugin.Simkl.API.Objects; +using Jellyfin.Plugin.Simkl.API.Responses; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Plugin.Simkl.API +{ + /// + /// The simkl endpoints. + /// + [ApiController] + [Authorize(Policy = "DefaultAuthorization")] + [Route("Simkl")] + public class Endpoints : ControllerBase + { + private readonly SimklApi _simklApi; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the . + public Endpoints(SimklApi simklApi) + { + _simklApi = simklApi; + } + + /// + /// Gets the oauth pin. + /// + /// The oauth pin. + [HttpGet("oauth/pin")] + public async Task> GetPin() + { + return await _simklApi.GetCode(); + } + + /// + /// Gets the status for the code. + /// + /// The user auth code. + /// The code status response. + [HttpGet("oauth/pin/{userCode}")] + public async Task> GetPinStatus([FromRoute] string userCode) + { + return await _simklApi.GetCodeStatus(userCode); + } + + /// + /// Gets the settings for the user. + /// + /// The user id. + /// The user settings. + [HttpGet("users/settings/{userId}")] + public async Task> GetUserSettings([FromRoute] Guid userId) + { + var userConfiguration = SimklPlugin.Instance?.Configuration.GetByGuid(userId); + if (userConfiguration == null) + { + return NotFound(); + } + + return await _simklApi.GetUserSettings(userConfiguration.UserToken); + } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Exceptions/InvalidTokenException.cs b/Jellyfin.Plugin.Simkl/API/Exceptions/InvalidTokenException.cs index 82a7aa5..9ff88d8 100644 --- a/Jellyfin.Plugin.Simkl/API/Exceptions/InvalidTokenException.cs +++ b/Jellyfin.Plugin.Simkl/API/Exceptions/InvalidTokenException.cs @@ -16,7 +16,8 @@ namespace Jellyfin.Plugin.Simkl.API.Exceptions /// Initializes a new instance of the class. /// /// The message. - public InvalidTokenException(string msg) : base(msg) + public InvalidTokenException(string msg) + : base(msg) { } @@ -25,7 +26,8 @@ namespace Jellyfin.Plugin.Simkl.API.Exceptions /// /// The message. /// The inner exception. - public InvalidTokenException(string msg, Exception inner) : base(msg, inner) + public InvalidTokenException(string msg, Exception inner) + : base(msg, inner) { } } diff --git a/Jellyfin.Plugin.Simkl/API/GetPin.cs b/Jellyfin.Plugin.Simkl/API/GetPin.cs deleted file mode 100644 index dfba395..0000000 --- a/Jellyfin.Plugin.Simkl/API/GetPin.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Jellyfin.Plugin.Simkl.API.Responses; -using MediaBrowser.Model.Services; - -namespace Jellyfin.Plugin.Simkl.API -{ - /// - /// Get oauth pin. - /// - [Route("/Simkl/oauth/pin", "GET")] - public class GetPin : IReturn - { - // Doesn't receive anything - } -} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/GetPinStatus.cs b/Jellyfin.Plugin.Simkl/API/GetPinStatus.cs deleted file mode 100644 index bbeba31..0000000 --- a/Jellyfin.Plugin.Simkl/API/GetPinStatus.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Jellyfin.Plugin.Simkl.API.Responses; -using MediaBrowser.Model.Services; -#pragma warning disable SA1300 - -namespace Jellyfin.Plugin.Simkl.API -{ - /// - /// Get pin status. - /// - [Route("/Simkl/oauth/pin/{user_code}", "GET")] - public class GetPinStatus : IReturn - { - /// - /// Gets or sets user code. - /// - [ApiMember(Name = "user_code", Description = "pin to be introduced by the user", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string user_code { get; set; } - } -} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/GetUserSettings.cs b/Jellyfin.Plugin.Simkl/API/GetUserSettings.cs deleted file mode 100644 index 167d05d..0000000 --- a/Jellyfin.Plugin.Simkl/API/GetUserSettings.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Jellyfin.Plugin.Simkl.API.Objects; -using MediaBrowser.Model.Services; - -namespace Jellyfin.Plugin.Simkl.API -{ - /// - /// Get user settings. - /// - [Route("/Simkl/users/settings/{userId}", "GET")] - public class GetUserSettings : IReturn - { - /// - /// Gets or sets user id. - /// - /// - /// Note: In the future, when we'll have config for more than one user, we'll use a parameter. - /// - [ApiMember(Name = "id", Description = "user id", IsRequired = true, DataType = "Guid", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } - } -} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/Season.cs b/Jellyfin.Plugin.Simkl/API/Objects/Season.cs index 1fcbe84..2b74912 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/Season.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/Season.cs @@ -1,4 +1,8 @@ -namespace Jellyfin.Plugin.Simkl.API.Objects +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Simkl.API.Objects { /// /// Season. @@ -8,11 +12,13 @@ /// /// Gets or sets the season number. /// - public int? number { get; set; } + [JsonPropertyName("number")] + public int? Number { get; set; } /// /// Gets or sets the episodes. /// - public ShowEpisode[] episodes { get; set; } + [JsonPropertyName("episodes")] + public IReadOnlyList Episodes { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/ShowEpisode.cs b/Jellyfin.Plugin.Simkl/API/Objects/ShowEpisode.cs index ccb6811..b5f7f8b 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/ShowEpisode.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/ShowEpisode.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Plugin.Simkl.API.Objects +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Simkl.API.Objects { /// /// Show episode. @@ -8,7 +10,8 @@ /// /// Gets or sets episode number. /// - public int? number { get; set; } + [JsonPropertyName("number")] + public int? Number { get; set; } // TODO: watched_at } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklEpisode.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklEpisode.cs index 76d35a3..38d1ffd 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklEpisode.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklEpisode.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Json.Serialization; #pragma warning disable SA1300 @@ -12,31 +13,30 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// Gets or sets watched at. /// [JsonPropertyName("watched_at")] - public string watched_at { get; set; } - - /// - /// Gets or sets ids. - /// - public override SimklIds ids { get; set; } + public DateTime? WatchedAt { get; set; } /// /// Gets or sets the title. /// - public string title { get; set; } + [JsonPropertyName("title")] + public string? Title { get; set; } /// /// Gets or sets the season. /// - public int season { get; set; } + [JsonPropertyName("season")] + public int? Season { get; set; } /// /// Gets or sets the episode. /// - public int episode { get; set; } + [JsonPropertyName("episode")] + public int? Episode { get; set; } /// /// Gets or sets multipart. /// - public bool? multipart { get; set; } + [JsonPropertyName("multipart")] + public bool? Multipart { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklFile.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklFile.cs index f4ee5ef..b79685d 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklFile.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklFile.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Jellyfin.Plugin.Simkl.API.Objects { /// @@ -8,16 +10,19 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// /// Gets or sets the file. /// - public string file { get; set; } + [JsonPropertyName("file")] + public string? File { get; set; } /// /// Gets or sets the part. /// - public int? part { get; set; } + [JsonPropertyName("part")] + public int? Part { get; set; } /// /// Gets or sets the hash. /// - public string hash { get; set; } + [JsonPropertyName("hash")] + public string? Hash { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklHistory.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklHistory.cs index af87d9f..ff6a5c6 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklHistory.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklHistory.cs @@ -1,6 +1,7 @@ #pragma warning disable CA2227 using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Jellyfin.Plugin.Simkl.API.Objects { @@ -14,24 +15,27 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// public SimklHistory() { - movies = new List(); - shows = new List(); - episodes = new List(); + Movies = new List(); + Shows = new List(); + Episodes = new List(); } /// /// Gets or sets list of movies. /// - public List movies { get; set; } + [JsonPropertyName("movies")] + public List Movies { get; set; } /// /// Gets or sets the list of shows. /// - public List shows { get; set; } + [JsonPropertyName("shows")] + public List Shows { get; set; } /// /// Gets or sets the list of episodes. /// - public List episodes { get; set; } + [JsonPropertyName("episodes")] + public List Episodes { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklIds.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklIds.cs index 5190e87..fbfef7f 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklIds.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklIds.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Text.Json.Serialization; namespace Jellyfin.Plugin.Simkl.API.Objects { @@ -17,52 +18,57 @@ namespace Jellyfin.Plugin.Simkl.API.Objects { foreach (var (key, value) in providerIds) { - if (key.Equals(nameof(simkl), StringComparison.OrdinalIgnoreCase)) + if (key.Equals(nameof(Simkl), StringComparison.OrdinalIgnoreCase)) { - simkl = Convert.ToInt32(value, CultureInfo.InvariantCulture); + Simkl = Convert.ToInt32(value, CultureInfo.InvariantCulture); } - else if (key.Equals(nameof(imdb), StringComparison.OrdinalIgnoreCase)) + else if (key.Equals(nameof(Imdb), StringComparison.OrdinalIgnoreCase)) { - imdb = value; + Imdb = value; } - else if (key.Equals(nameof(slug), StringComparison.OrdinalIgnoreCase)) + else if (key.Equals(nameof(Slug), StringComparison.OrdinalIgnoreCase)) { - slug = value; + Slug = value; } - else if (key.Equals(nameof(netflix), StringComparison.OrdinalIgnoreCase)) + else if (key.Equals(nameof(Netflix), StringComparison.OrdinalIgnoreCase)) { - netflix = value; + Netflix = value; } - else if (key.Equals(nameof(tmdb), StringComparison.OrdinalIgnoreCase)) + else if (key.Equals(nameof(Tmdb), StringComparison.OrdinalIgnoreCase)) { - tmdb = value; + Tmdb = value; } } } /// - /// Gets or sets simkl. + /// Gets or sets the simkl id. /// - public int? simkl { get; set; } + [JsonPropertyName("simkl")] + public int? Simkl { get; set; } /// /// Gets or sets the imdb id. /// - public string imdb { get; set; } + [JsonPropertyName("imdb")] + public string? Imdb { get; set; } /// /// Gets or sets the slug. /// - public string slug { get; set; } + [JsonPropertyName("slug")] + public string? Slug { get; set; } /// /// Gets or sets the netflix id. /// - public string netflix { get; set; } + [JsonPropertyName("netflix")] + public string? Netflix { get; set; } /// /// Gets or sets the TMDb id. /// - public string tmdb { get; set; } + [JsonPropertyName("tmdb")] + public string? Tmdb { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklMediaObject.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklMediaObject.cs index 70a9d90..bc99412 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklMediaObject.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklMediaObject.cs @@ -1,13 +1,16 @@ -namespace Jellyfin.Plugin.Simkl.API.Objects +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Simkl.API.Objects { /// /// Simkl media object. /// - public abstract class SimklMediaObject + public class SimklMediaObject { /// /// Gets or sets ids. /// - public abstract SimklIds ids { get; set; } + [JsonPropertyName("ids")] + public SimklIds? Ids { get; set; } } } diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklMovie.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklMovie.cs index 36cc90a..4b41406 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklMovie.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklMovie.cs @@ -1,5 +1,5 @@ using System; -using System.Globalization; +using System.Text.Json.Serialization; using MediaBrowser.Model.Dto; namespace Jellyfin.Plugin.Simkl.API.Objects @@ -15,28 +15,28 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// The base item dto. public SimklMovie(BaseItemDto item) { - title = item.OriginalTitle; - year = item.ProductionYear; - ids = new SimklMovieIds(item.ProviderIds); - watched_at = DateTime.UtcNow.ToString("yyyy-MM-dd HH\\:mm\\:ss", CultureInfo.InvariantCulture); + Title = item.OriginalTitle; + Year = item.ProductionYear; + Ids = new SimklMovieIds(item.ProviderIds); + WatchedAt = DateTime.UtcNow; } /// /// Gets or sets the movie title. /// - public string title { get; set; } + [JsonPropertyName("title")] + public string? Title { get; set; } /// /// Gets or sets the year. /// - public int? year { get; set; } - - /// - public override SimklIds ids { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } /// - /// Gets watched at. + /// Gets or sets watched at. /// - public string watched_at { get; } + [JsonPropertyName("watched_at")] + public DateTime? WatchedAt { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklMovieIds.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklMovieIds.cs index 5c5fa9c..163bdc9 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklMovieIds.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklMovieIds.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Jellyfin.Plugin.Simkl.API.Objects { @@ -19,31 +20,37 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// /// Gets or sets the tvdb id. /// - public int? tvdb { get; set; } + [JsonPropertyName("tvdb")] + public int? Tvdb { get; set; } /// /// Gets or sets the mal id. /// - public int? mal { get; set; } + [JsonPropertyName("mal")] + public int? Mal { get; set; } /// /// Gets or sets the anidb id. /// - public int? anidb { get; set; } + [JsonPropertyName("anidb")] + public int? Anidb { get; set; } /// /// Gets or sets the hulu id. /// - public int? hulu { get; set; } + [JsonPropertyName("hulu")] + public int? Hulu { get; set; } /// /// Gets or sets the crunchyroll id. /// - public int? crunchyroll { get; set; } + [JsonPropertyName("crunchyroll")] + public int? Crunchyroll { get; set; } /// /// Gets or sets the movie db id. /// - public string moviedb { get; set; } + [JsonPropertyName("moviedb")] + public string? Moviedb { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklShow.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklShow.cs index 840f94d..32cf3d2 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklShow.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklShow.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Model.Dto; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using MediaBrowser.Model.Dto; namespace Jellyfin.Plugin.Simkl.API.Objects { @@ -13,19 +15,19 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// The media info. public SimklShow(BaseItemDto mediaInfo) { - title = mediaInfo.SeriesName; - ids = new SimklShowIds(mediaInfo.ProviderIds); - year = mediaInfo.ProductionYear; - seasons = new[] + Title = mediaInfo.SeriesName; + Ids = new SimklShowIds(mediaInfo.ProviderIds); + Year = mediaInfo.ProductionYear; + Seasons = new[] { new Season { - number = mediaInfo.ParentIndexNumber, - episodes = new[] + Number = mediaInfo.ParentIndexNumber, + Episodes = new[] { new ShowEpisode { - number = mediaInfo.IndexNumber + Number = mediaInfo.IndexNumber } } } @@ -35,21 +37,19 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// /// Gets or sets title. /// - public string title { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } /// /// Gets or sets year. /// - public int? year { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } /// /// Gets or sets seasons. /// - public Season[] seasons { get; set; } - - /// - /// Gets or sets ids. - /// - public override SimklIds ids { get; set; } + [JsonPropertyName("seasons")] + public IReadOnlyList Seasons { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/SimklShowIds.cs b/Jellyfin.Plugin.Simkl/API/Objects/SimklShowIds.cs index 6635a24..a5e3b57 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/SimklShowIds.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/SimklShowIds.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Jellyfin.Plugin.Simkl.API.Objects { @@ -19,31 +20,37 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// /// Gets or sets tvdb. /// - public int? tvdb { get; set; } + [JsonPropertyName("tvdb")] + public int? Tvdb { get; set; } /// /// Gets or sets mal. /// - public int? mal { get; set; } + [JsonPropertyName("mal")] + public int? Mal { get; set; } /// /// Gets or sets anidb. /// - public int? anidb { get; set; } + [JsonPropertyName("anidb")] + public int? Anidb { get; set; } /// /// Gets or sets hulu. /// - public int? hulu { get; set; } + [JsonPropertyName("hulu")] + public int? Hulu { get; set; } /// /// Gets or sets crunchyroll. /// - public int? crunchyroll { get; set; } + [JsonPropertyName("crunchyroll")] + public int? Crunchyroll { get; set; } /// /// Gets or sets zap2it. /// - public string zap2It { get; set; } + [JsonPropertyName("zap2It")] + public string? Zap2It { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/User.cs b/Jellyfin.Plugin.Simkl/API/Objects/User.cs index 06a5efb..34fc4a8 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/User.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/User.cs @@ -1,4 +1,6 @@ -#pragma warning disable SA1300 +using System.Text.Json.Serialization; + +#pragma warning disable SA1300 namespace Jellyfin.Plugin.Simkl.API.Objects { @@ -10,6 +12,7 @@ namespace Jellyfin.Plugin.Simkl.API.Objects /// /// Gets or sets name. /// - public string name { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Objects/UserSettings.cs b/Jellyfin.Plugin.Simkl/API/Objects/UserSettings.cs index ec29200..4ba17fa 100644 --- a/Jellyfin.Plugin.Simkl/API/Objects/UserSettings.cs +++ b/Jellyfin.Plugin.Simkl/API/Objects/UserSettings.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Plugin.Simkl.API.Objects +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Simkl.API.Objects { /// /// User settings. @@ -8,11 +10,13 @@ /// /// Gets or sets user. /// - public User user { get; set; } + [JsonPropertyName("user")] + public User? User { get; set; } /// /// Gets or sets error. /// - public string error { get; set; } + [JsonPropertyName("error")] + public string? Error { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Responses/CodeResponse.cs b/Jellyfin.Plugin.Simkl/API/Responses/CodeResponse.cs index ba5b4fc..cf94224 100644 --- a/Jellyfin.Plugin.Simkl/API/Responses/CodeResponse.cs +++ b/Jellyfin.Plugin.Simkl/API/Responses/CodeResponse.cs @@ -11,35 +11,35 @@ namespace Jellyfin.Plugin.Simkl.API.Responses /// /// Gets or sets result. /// - public string Result { get; set; } + public string? Result { get; set; } /// /// Gets or sets device code. /// [JsonPropertyName("device_code")] - public string device_code { get; set; } + public string? DeviceCode { get; set; } /// /// Gets or sets user code. /// [JsonPropertyName("user_code")] - public string user_code { get; set; } + public string? UserCode { get; set; } /// /// Gets or sets verification url. /// [JsonPropertyName("verification_url")] - public string verification_url { get; set; } + public string? VerificationUrl { get; set; } /// /// Gets or sets expires in. /// [JsonPropertyName("expires_in")] - public int expires_in { get; set; } + public int? ExpiresIn { get; set; } /// /// Gets or sets interval. /// - public int Interval { get; set; } + public int? Interval { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Responses/CodeStatusResponse.cs b/Jellyfin.Plugin.Simkl/API/Responses/CodeStatusResponse.cs index 97390b9..baa7e1b 100644 --- a/Jellyfin.Plugin.Simkl/API/Responses/CodeStatusResponse.cs +++ b/Jellyfin.Plugin.Simkl/API/Responses/CodeStatusResponse.cs @@ -11,17 +11,17 @@ namespace Jellyfin.Plugin.Simkl.API.Responses /// /// Gets or sets result. /// - public string Result { get; set; } + public string? Result { get; set; } /// /// Gets or sets message. /// - public string Message { get; set; } + public string? Message { get; set; } /// /// Gets or sets access token. /// [JsonPropertyName("access_token")] - public string access_token { get; set; } + public string? AccessToken { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Responses/SearchFileResponse.cs b/Jellyfin.Plugin.Simkl/API/Responses/SearchFileResponse.cs index 90b0e0a..7f5690c 100644 --- a/Jellyfin.Plugin.Simkl/API/Responses/SearchFileResponse.cs +++ b/Jellyfin.Plugin.Simkl/API/Responses/SearchFileResponse.cs @@ -10,21 +10,21 @@ namespace Jellyfin.Plugin.Simkl.API.Responses /// /// Gets or sets type. /// - public string Type { get; set; } + public string? Type { get; set; } /// /// Gets or sets episode. /// - public SimklEpisode Episode { get; set; } + public SimklEpisode? Episode { get; set; } /// /// Gets or sets movie. /// - public SimklMovie Movie { get; set; } + public SimklMovie? Movie { get; set; } /// /// Gets or sets show. /// - public SimklShow Show { get; set; } + public SimklShow? Show { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryNotFound.cs b/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryNotFound.cs index f2fa868..67a67e3 100644 --- a/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryNotFound.cs +++ b/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryNotFound.cs @@ -1,4 +1,5 @@ -using Jellyfin.Plugin.Simkl.API.Objects; +using System; +using Jellyfin.Plugin.Simkl.API.Objects; namespace Jellyfin.Plugin.Simkl.API.Responses { @@ -10,16 +11,16 @@ namespace Jellyfin.Plugin.Simkl.API.Responses /// /// Gets or sets movies. /// - public SimklMovie[] Movies { get; set; } + public SimklMovie[] Movies { get; set; } = Array.Empty(); /// /// Gets or sets shows. /// - public SimklShow[] Shows { get; set; } + public SimklShow[] Shows { get; set; } = Array.Empty(); /// /// Gets or sets episodes. /// - public SimklEpisode[] Episodes { get; set; } + public SimklEpisode[] Episodes { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryResponse.cs b/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryResponse.cs index a8758bc..107f053 100644 --- a/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryResponse.cs +++ b/Jellyfin.Plugin.Simkl/API/Responses/SyncHistoryResponse.cs @@ -11,12 +11,12 @@ namespace Jellyfin.Plugin.Simkl.API.Responses /// /// Gets or sets added. /// - public SyncHistoryResponseCount Added { get; set; } + public SyncHistoryResponseCount Added { get; set; } = new SyncHistoryResponseCount(); /// /// Gets or sets not found. /// [JsonPropertyName("not_found")] - public SyncHistoryNotFound not_found { get; set; } + public SyncHistoryNotFound? NotFound { get; set; } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/ServerEndpoint.cs b/Jellyfin.Plugin.Simkl/API/ServerEndpoint.cs deleted file mode 100644 index dfcbba7..0000000 --- a/Jellyfin.Plugin.Simkl/API/ServerEndpoint.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Jellyfin.Plugin.Simkl.API.Objects; -using Jellyfin.Plugin.Simkl.API.Responses; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -#pragma warning disable CA1801 - -namespace Jellyfin.Plugin.Simkl.API -{ - /// - /// Server endpoints. - /// - public class ServerEndpoint : IService, IHasResultFactory - { - private readonly SimklApi _api; - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ServerEndpoint(ILoggerFactory loggerFactory, IJsonSerializer json, IHttpClient httpClient) - { - _logger = loggerFactory.CreateLogger(); - _json = json; - _api = new SimklApi(json, loggerFactory.CreateLogger(), httpClient); - } - - /// - /// Gets or sets result factory. - /// - public IHttpResultFactory ResultFactory { get; set; } - - /// - /// Gets or sets request. - /// - public IRequest Request { get; set; } - - /// - /// Get pin request. - /// - /// The request. - /// The code response. - public CodeResponse Get(GetPin request) - { - return _api.GetCode().GetAwaiter().GetResult(); - } - - /// - /// Get pin status. - /// - /// The request. - /// Code status response. - public CodeStatusResponse Get(GetPinStatus request) - { - return _api.GetCodeStatus(request.user_code).GetAwaiter().GetResult(); - } - - /// - /// Get user settings. - /// - /// The request. - /// User settings. - public UserSettings Get(GetUserSettings request) - { - _logger.LogDebug(_json.SerializeToString(request)); - return _api.GetUserSettings(SimklPlugin.Instance.Configuration.GetByGuid(request.UserId).UserToken).GetAwaiter().GetResult(); - } - } -} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/API/SimklApi.cs b/Jellyfin.Plugin.Simkl/API/SimklApi.cs index 5b491d5..8102105 100644 --- a/Jellyfin.Plugin.Simkl/API/SimklApi.cs +++ b/Jellyfin.Plugin.Simkl/API/SimklApi.cs @@ -1,12 +1,21 @@ -using System.IO; +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Net.Mime; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Plugin.Simkl.API.Exceptions; using Jellyfin.Plugin.Simkl.API.Objects; using Jellyfin.Plugin.Simkl.API.Responses; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; // using System.Threading; +using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Simkl.API { @@ -16,9 +25,9 @@ namespace Jellyfin.Plugin.Simkl.API public class SimklApi { /* INTERFACES */ - private readonly IJsonSerializer _json; private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; + private readonly JsonSerializerOptions _jsonSerializerOptions; /* BASIC API THINGS */ @@ -26,44 +35,43 @@ namespace Jellyfin.Plugin.Simkl.API /// Base url. /// public const string Baseurl = @"https://api.simkl.com"; - // public const string BASE_URL = @"http://private-9c39b-simkl.apiary-proxy.com"; /// /// Redirect uri. /// - public const string RedirectUri = @"https://simkl.com/apps/emby/connected/"; + /// public const string RedirectUri = @"https://simkl.com/apps/jellyfin/connected/"; + public const string RedirectUri = @"https://jellyfin.org"; /// /// Api key. /// - public const string Apikey = @"27dd5d6adc24aa1ad9f95ef913244cbaf6df5696036af577ed41670473dc97d0"; + public const string Apikey = @"c721b22482097722a84a20ccc579cf9d232be85b9befe7b7805484d0ddbc6781"; /// /// Secret. /// - public const string Secret = @"d7b9feb9d48bbaa69dbabaca21ba4671acaa89198637e9e136a4d69ec97ab68b"; + public const string Secret = @"87893fc73cdbd2e51a7c63975c6f941ac1c6155c0e20ffa76b83202dd10a507e"; /// /// Initializes a new instance of the class. /// - /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - public SimklApi(IJsonSerializer json, ILogger logger, IHttpClient httpClient) + /// Instance of the interface. + public SimklApi(ILogger logger, IHttpClientFactory httpClientFactory) { - _json = json; _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; + _jsonSerializerOptions = JsonDefaults.GetOptions(); } /// /// Get code. /// /// Code response. - public async Task GetCode() + public async Task GetCode() { var uri = $"/oauth/pin?client_id={Apikey}&redirect={RedirectUri}"; - return _json.DeserializeFromStream(await Get(uri).ConfigureAwait(false)); + return await Get(uri); } /// @@ -71,10 +79,10 @@ namespace Jellyfin.Plugin.Simkl.API /// /// User code. /// Code status. - public async Task GetCodeStatus(string userCode) + public async Task GetCodeStatus(string userCode) { var uri = $"/oauth/pin/{userCode}?client_id={Apikey}"; - return _json.DeserializeFromStream(await Get(uri).ConfigureAwait(false)); + return await Get(uri); } /// @@ -82,18 +90,18 @@ namespace Jellyfin.Plugin.Simkl.API /// /// User token. /// User settings. - public async Task GetUserSettings(string userToken) + public async Task GetUserSettings(string userToken) { try { - return _json.DeserializeFromStream(await Post("/users/settings/", userToken).ConfigureAwait(false)); + return await Post("/users/settings/", userToken); } - catch (MediaBrowser.Model.Net.HttpException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) + catch (HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { // Wontfix: Custom status codes // "You don't get to pick your response code" - Luke (System Architect of Emby) // https://emby.media/community/index.php?/topic/61889-wiki-issue-resultfactorythrowerror/ - return new UserSettings { error = "user_token_failed" }; + return new UserSettings { Error = "user_token_failed" }; } } @@ -103,12 +111,12 @@ namespace Jellyfin.Plugin.Simkl.API /// Item. /// User token. /// Status. - public async Task<(bool success, BaseItemDto item)> MarkAsWatched(BaseItemDto item, string userToken) + public async Task<(bool Success, BaseItemDto Item)> MarkAsWatched(BaseItemDto item, string userToken) { var history = CreateHistoryFromItem(item); - var r = await SyncHistoryAsync(history, userToken).ConfigureAwait(false); - _logger.LogDebug("Response: " + _json.SerializeToString(r)); - if (history.movies.Count == r.Added.Movies && history.shows.Count == r.Added.Shows) + var r = await SyncHistoryAsync(history, userToken); + _logger.LogDebug("Response: {@Response}", r); + if (r != null && history.Movies.Count == r.Added.Movies && history.Shows.Count == r.Added.Shows) { return (true, item); } @@ -117,19 +125,19 @@ namespace Jellyfin.Plugin.Simkl.API // let's try scrobbling from full path try { - (history, item) = await GetHistoryFromFileName(item).ConfigureAwait(false); + (history, item) = await GetHistoryFromFileName(item); } catch (InvalidDataException) { // Let's try again but this time using only the FILE name _logger.LogDebug("Couldn't scrobble using full path, trying using only filename"); - (history, item) = await GetHistoryFromFileName(item, false).ConfigureAwait(false); + (history, item) = await GetHistoryFromFileName(item, false); } - r = await SyncHistoryAsync(history, userToken).ConfigureAwait(false); - _logger.LogDebug("Response: " + _json.SerializeToString(r)); - - return (history.movies.Count == r.Added.Movies && history.shows.Count == r.Added.Shows, item); + r = await SyncHistoryAsync(history, userToken); + return r == null + ? (false, item) + : (history.Movies.Count == r.Added.Movies && history.Shows.Count == r.Added.Shows, item); } /// @@ -137,14 +145,11 @@ namespace Jellyfin.Plugin.Simkl.API /// /// Filename. /// Search file response. - private async Task GetFromFile(string filename) + private async Task GetFromFile(string filename) { - var f = new SimklFile { file = filename }; - _logger.LogInformation("Posting: " + _json.SerializeToString(f)); - using var r = new StreamReader(await Post("/search/file/", null, f).ConfigureAwait(false)); - var t = await r.ReadToEndAsync().ConfigureAwait(false); - _logger.LogDebug("Response: " + t); - return _json.DeserializeFromString(t); + var f = new SimklFile { File = filename }; + _logger.LogInformation("Posting: {@File}", f); + return await Post("/search/file/", null, f); } /// @@ -156,68 +161,69 @@ namespace Jellyfin.Plugin.Simkl.API private async Task<(SimklHistory history, BaseItemDto item)> GetHistoryFromFileName(BaseItemDto item, bool fullpath = true) { var fname = fullpath ? item.Path : Path.GetFileName(item.Path); - var mo = await GetFromFile(fname).ConfigureAwait(false); + var mo = await GetFromFile(fname); + if (mo == null) + { + throw new InvalidDataException("Search file response is null"); + } var history = new SimklHistory(); - if (item.IsMovie == true || item.Type == "Movie") + if (mo.Movie != null && + (item.IsMovie == true || string.Equals(item.Type, nameof(Movie), StringComparison.OrdinalIgnoreCase))) { - if (mo.Type != "movie") + if (!string.Equals(mo.Type, "movie", StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("type != movie (" + mo.Type + ")"); } - item.Name = mo.Movie.title; - item.ProductionYear = mo.Movie.year; - history.movies.Add(mo.Movie); + item.Name = mo.Movie.Title; + item.ProductionYear = mo.Movie.Year; + history.Movies.Add(mo.Movie); } - else if (item.IsSeries == true || item.Type == "Episode") + else if (mo.Episode != null + && mo.Show != null + && (item.IsSeries == true || string.Equals(item.Type, nameof(Episode), StringComparison.OrdinalIgnoreCase))) { - if (mo.Type != "episode") + if (!string.Equals(mo.Type, "episode", StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("type != episode (" + mo.Type + ")"); } - item.Name = mo.Episode.title; - item.SeriesName = mo.Show.title; - item.IndexNumber = mo.Episode.episode; - item.ParentIndexNumber = mo.Episode.season; - item.ProductionYear = mo.Show.year; - history.episodes.Add(mo.Episode); + item.Name = mo.Episode.Title; + item.SeriesName = mo.Show.Title; + item.IndexNumber = mo.Episode.Episode; + item.ParentIndexNumber = mo.Episode.Season; + item.ProductionYear = mo.Show.Year; + history.Episodes.Add(mo.Episode); } return (history, item); } - private static HttpRequestOptions GetOptions(string userToken = null) + private static HttpRequestMessage GetOptions(string? userToken = null) { - var options = new HttpRequestOptions - { - RequestContentType = "application/json", - LogErrorResponseBody = true, - EnableDefaultUserAgent = true - }; - options.RequestHeaders.Add("simkl-api-key", Apikey); - // options.RequestHeaders.Add("Content-Type", "application/json"); + var requestMessage = new HttpRequestMessage(); + requestMessage.Headers.TryAddWithoutValidation("simkl-api-key", Apikey); if (!string.IsNullOrEmpty(userToken)) { - options.RequestHeaders.Add("Authorization", "Bearer " + userToken); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userToken); } - return options; + return requestMessage; } private static SimklHistory CreateHistoryFromItem(BaseItemDto item) { var history = new SimklHistory(); - if (item.IsMovie == true || item.Type == "Movie") + if (item.IsMovie == true || string.Equals(item.Type, nameof(Movie), StringComparison.OrdinalIgnoreCase)) { - history.movies.Add(new SimklMovie(item)); + history.Movies.Add(new SimklMovie(item)); } - else if (item.IsSeries == true || item.Type == "Episode") + else if (item.IsSeries == true || string.Equals(item.Type, nameof(Episode), StringComparison.OrdinalIgnoreCase)) { // TODO: TV Shows scrobbling (WIP) - history.shows.Add(new SimklShow(item)); + history.Shows.Add(new SimklShow(item)); } return history; @@ -229,17 +235,17 @@ namespace Jellyfin.Plugin.Simkl.API /// History object. /// User token. /// The sync history response. - private async Task SyncHistoryAsync(SimklHistory history, string userToken) + private async Task SyncHistoryAsync(SimklHistory history, string userToken) { try { - _logger.LogInformation("Syncing History: " + _json.SerializeToString(history)); - return _json.DeserializeFromStream(await Post("/sync/history", userToken, history).ConfigureAwait(false)); + _logger.LogInformation("Syncing History"); + return await Post("/sync/history", userToken, history); } - catch (MediaBrowser.Model.Net.HttpException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) + catch (HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - _logger.LogError("Invalid user token " + userToken + ", deleting"); - SimklPlugin.Instance.Configuration.DeleteUserToken(userToken); + _logger.LogError(e, "Invalid user token {UserToken}, deleting", userToken); + SimklPlugin.Instance?.Configuration.DeleteUserToken(userToken); throw new InvalidTokenException("Invalid user token " + userToken); } } @@ -250,13 +256,15 @@ namespace Jellyfin.Plugin.Simkl.API /// Relative url. /// Authentication token. /// HTTP(s) Stream to be used. - private async Task Get(string url, string userToken = null) + private async Task Get(string url, string? userToken = null) { // Todo: If string is not null neither empty - var options = GetOptions(userToken); - options.Url = Baseurl + url; - - return await _httpClient.Get(options).ConfigureAwait(false); + using var options = GetOptions(userToken); + options.RequestUri = new Uri(Baseurl + url); + options.Method = HttpMethod.Get; + var responseMessage = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(options); + return await responseMessage.Content.ReadFromJsonAsync(_jsonSerializerOptions); } /// @@ -265,16 +273,23 @@ namespace Jellyfin.Plugin.Simkl.API /// Relative post url. /// Authentication token. /// Object to serialize. - private async Task Post(string url, string userToken = null, object data = null) + private async Task Post(string url, string? userToken = null, T2? data = null) + where T2 : class { - var options = GetOptions(userToken); - options.Url = Baseurl + url; + using var options = GetOptions(userToken); + options.RequestUri = new Uri(Baseurl + url); + options.Method = HttpMethod.Post; if (data != null) { - options.RequestContent = _json.SerializeToString(data); + options.Content = new StringContent( + JsonSerializer.Serialize(data, _jsonSerializerOptions), + Encoding.UTF8, + MediaTypeNames.Application.Json); } - return (await _httpClient.Post(options).ConfigureAwait(false)).Content; + var responseMessage = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(options); + return await responseMessage.Content.ReadFromJsonAsync(_jsonSerializerOptions); } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Simkl/Configuration/PluginConfiguration.cs index 9a6d262..89e8b9b 100644 --- a/Jellyfin.Plugin.Simkl/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.Simkl/Configuration/PluginConfiguration.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Plugin.Simkl.Configuration /// /// The user id. /// Stored user config. - public UserConfig GetByGuid(Guid id) + public UserConfig? GetByGuid(Guid id) { return UserConfigs.FirstOrDefault(c => c.Id == id); } @@ -46,7 +46,7 @@ namespace Jellyfin.Plugin.Simkl.Configuration } } - SimklPlugin.Instance.SaveConfiguration(); + SimklPlugin.Instance?.SaveConfiguration(); } } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/Configuration/configPage.html b/Jellyfin.Plugin.Simkl/Configuration/configPage.html index 9880408..9350ce7 100644 --- a/Jellyfin.Plugin.Simkl/Configuration/configPage.html +++ b/Jellyfin.Plugin.Simkl/Configuration/configPage.html @@ -4,52 +4,61 @@ Simkl's TV Tracker -
+

Simkl's TV Tracker

-
@@ -57,42 +66,57 @@
diff --git a/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj b/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj index 580728e..a84f577 100644 --- a/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj +++ b/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj @@ -1,21 +1,22 @@ - netstandard2.1 + net5.0 1.0.0.0 1.0.0.0 true true - CA1707;CA1819;SA1300 + enable - + + - + @@ -28,5 +29,9 @@ ../jellyfin.ruleset + + + + diff --git a/Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs b/Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs new file mode 100644 index 0000000..a96e4d0 --- /dev/null +++ b/Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs @@ -0,0 +1,16 @@ +using Jellyfin.Plugin.Simkl.API; +using MediaBrowser.Common.Plugins; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Plugin.Simkl +{ + /// + public class PluginServiceRegistrator : IPluginServiceRegistrator + { + /// + public void RegisterServices(IServiceCollection serviceCollection) + { + serviceCollection.AddScoped(); + } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs b/Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs new file mode 100644 index 0000000..ca7209e --- /dev/null +++ b/Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Jellyfin.Plugin.Simkl.API; +using Jellyfin.Plugin.Simkl.API.Exceptions; +using Jellyfin.Plugin.Simkl.Configuration; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Simkl.Services +{ + /// + /// Playback progress scrobbler. + /// + public class PlaybackScrobbler : IServerEntryPoint + { + private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions + private readonly ILogger _logger; + private readonly Dictionary _lastScrobbled; // Library ID of last scrobbled item + private readonly SimklApi _simklApi; + 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, + SimklApi simklApi) + { + _sessionManager = sessionManager; + _logger = logger; + _simklApi = simklApi; + _lastScrobbled = new Dictionary(); + _nextTry = DateTime.UtcNow; + } + + /// + public Task RunAsync() + { + _sessionManager.PlaybackProgress += OnPlaybackProgress; + _sessionManager.PlaybackStopped += OnPlaybackStopped; + return Task.CompletedTask; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose. + /// + /// Dispose all resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _sessionManager.PlaybackProgress -= OnPlaybackProgress; + _sessionManager.PlaybackStopped -= OnPlaybackStopped; + } + } + + private static bool CanBeScrobbled(UserConfig config, SessionInfo session, BaseItemDto mediaInfo, bool playedToCompletion) + { + if (!playedToCompletion) + { + if (session.NowPlayingItem.RunTimeTicks != null) + { + var percentageWatched = session.PlayState.PositionTicks / (float)session.NowPlayingItem.RunTimeTicks * 100f; + + // If percentage watched is below minimum, can't scrobble + if (percentageWatched < config.ScrobblePercentage) + { + return false; + } + } + + // If it's below minimum length, can't scrobble + if (session.NowPlayingItem.RunTimeTicks < 60 * 10000 * config.MinLength) + { + return false; + } + } + + return mediaInfo.Type switch + { + nameof(Movie) => config.ScrobbleMovies, + nameof(Episode) => config.ScrobbleShows, + _ => false + }; + } + + private async void OnPlaybackProgress(object? sessions, PlaybackProgressEventArgs e) + { + if (DateTime.UtcNow < _nextTry) + { + return; + } + + _nextTry = DateTime.UtcNow.AddSeconds(30); + await ScrobbleSession(e, false); + } + + private async void OnPlaybackStopped(object? sessions, PlaybackStopEventArgs e) + { + await ScrobbleSession(e, e.PlayedToCompletion); + } + + private async Task ScrobbleSession(PlaybackProgressEventArgs eventArgs, bool playedToCompletion) + { + try + { + var userId = eventArgs.Session.UserId; + var userConfig = SimklPlugin.Instance?.Configuration.GetByGuid(userId); + if (userConfig == null || string.IsNullOrEmpty(userConfig.UserToken)) + { + _logger.LogError( + "Can't scrobble: User {UserName} not logged in ({UserConfigStatus})", + eventArgs.Session.UserName, + userConfig == null); + return; + } + + if (!CanBeScrobbled(userConfig, eventArgs.Session, eventArgs.MediaInfo, playedToCompletion)) + { + return; + } + + if (_lastScrobbled.ContainsKey(eventArgs.Session.Id) && _lastScrobbled[eventArgs.Session.Id] == eventArgs.MediaInfo.Id) + { + _logger.LogDebug("Already scrobbled {ItemName} for {UserName}", eventArgs.MediaInfo.Name, eventArgs.Session.UserName); + return; + } + + _logger.LogInformation( + "Trying to scrobble {Name} ({NowPlayingId}) for {UserName} ({UserId}) - {PlayingItemPath} on {SessionId}", + eventArgs.MediaInfo.Name, + eventArgs.MediaInfo.Id, + eventArgs.Session.UserName, + userId, + eventArgs.MediaInfo.Path, + eventArgs.Session.Id); + + var response = await _simklApi.MarkAsWatched(eventArgs.MediaInfo, userConfig.UserToken); + if (response.Success) + { + _logger.LogDebug("Scrobbled without errors"); + _lastScrobbled[eventArgs.Session.Id] = eventArgs.MediaInfo.Id; + } + } + catch (InvalidTokenException) + { + _logger.LogDebug("Deleted user token"); + } + catch (InvalidDataException ex) + { + _logger.LogError(ex, "Couldn't scrobble"); + _lastScrobbled[eventArgs.Session.Id] = eventArgs.MediaInfo.Id; + } + catch (Exception ex) + { + _logger.LogError(ex, "Caught unknown exception while trying to scrobble"); + } + } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/Services/Scrobbler.cs b/Jellyfin.Plugin.Simkl/Services/Scrobbler.cs deleted file mode 100644 index 32b3c10..0000000 --- a/Jellyfin.Plugin.Simkl/Services/Scrobbler.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Plugin.Simkl.API; -using Jellyfin.Plugin.Simkl.API.Exceptions; -using Jellyfin.Plugin.Simkl.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Plugin.Simkl.Services -{ - /// - public class Scrobbler : IServerEntryPoint - { - private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - private readonly INotificationManager _notifications; - private readonly Dictionary _lastScrobbled; // Library ID of last scrobbled item - private SimklApi _api; - private DateTime _nextTry; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public Scrobbler( - IJsonSerializer json, - ISessionManager sessionManager, - ILoggerFactory loggerFactory, - IHttpClient httpClient, - INotificationManager notifications) - { - _json = json; - _sessionManager = sessionManager; - _logger = loggerFactory.CreateLogger(); - _notifications = notifications; - _api = new SimklApi(json, loggerFactory.CreateLogger(), httpClient); - _lastScrobbled = new Dictionary(); - _nextTry = DateTime.UtcNow; - } - - /// - public Task RunAsync() - { - _sessionManager.PlaybackProgress += OnPlaybackProgress; - return Task.CompletedTask; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose. - /// - /// Dispose all resources. - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _sessionManager.PlaybackProgress -= OnPlaybackProgress; - _api = null; - } - } - - /// - /// Session can be scrobbled. - /// - /// The plugin configuration. - /// The session. - /// If session can be scrobbled. - private static bool CanBeScrobbled(UserConfig config, SessionInfo session) - { - if (session.NowPlayingItem.RunTimeTicks != null) - { - var percentageWatched = session.PlayState.PositionTicks / (float)session.NowPlayingItem.RunTimeTicks * 100f; - - // If percentage watched is below minimum, can't scrobble - if (percentageWatched < config.ScrobblePercentage) - { - return false; - } - } - - // If it's below minimum length, can't scrobble - if (session.NowPlayingItem.RunTimeTicks < 60 * 10000 * config.MinLength) - { - return false; - } - - var item = session.FullNowPlayingItem; - return item switch - { - Movie _ => config.ScrobbleMovies, - Episode _ => config.ScrobbleShows, - _ => false - }; - } - - private bool CanSendNotification(BaseItemDto item) - { - if (item.IsMovie == true || item.Type == "Movie") - { - return _notifications.GetNotificationTypes().Any(t => t.Type == SimklNotificationsFactory.NotificationMovieType && t.Enabled); - } - - if (item.IsSeries == true || item.Type == "Episode") - { - return _notifications.GetNotificationTypes().Any(t => t.Type == SimklNotificationsFactory.NotificationShowType && t.Enabled); - } - - return false; - } - - private async void OnPlaybackProgress(object sessions, PlaybackProgressEventArgs e) - { - var sid = e.PlaySessionId; - Guid uid = e.Session.UserId, npid = e.Session.NowPlayingItem.Id; - try - { - if (DateTime.UtcNow < _nextTry) - { - return; - } - - _nextTry = DateTime.UtcNow.AddSeconds(30); - - var userConfig = SimklPlugin.Instance.Configuration.GetByGuid(uid); - if (userConfig == null || string.IsNullOrEmpty(userConfig.UserToken)) - { - _logger.LogError("Can't scrobble: User " + e.Session.UserName + " not logged in (" + (userConfig == null) + ")"); - return; - } - - if (!CanBeScrobbled(userConfig, e.Session)) - { - return; - } - - if (_lastScrobbled.ContainsKey(sid) && _lastScrobbled[sid] == npid) - { - _logger.LogDebug("Already scrobbled {0} for {1}", e.Session.NowPlayingItem.Name, e.Session.UserName); - return; - } - - _logger.LogDebug(_json.SerializeToString(e.Session.NowPlayingItem)); - _logger.LogInformation( - "Trying to scrobble {0} ({1}) for {2} ({3}) - {4} on {5}", - e.Session.NowPlayingItem.Name, - npid, - e.Session.UserName, - uid, - e.Session.NowPlayingItem.Path, - sid); - - var response = await _api.MarkAsWatched(e.MediaInfo, userConfig.UserToken).ConfigureAwait(false); - if (response.success) - { - _logger.LogDebug("Scrobbled without errors"); - _lastScrobbled[sid] = npid; - - if (CanSendNotification(response.item)) - { - await _notifications.SendNotification( - SimklNotificationsFactory.GetNotificationRequest(response.item, e.Session.UserId), - e.Session.FullNowPlayingItem, - CancellationToken.None) - .ConfigureAwait(false); - } - } - } - catch (InvalidTokenException) - { - _logger.LogDebug("Deleted user token"); - } - catch (InvalidDataException ex) - { - _logger.LogError(ex, "Couldn't scrobble."); - _lastScrobbled[sid] = npid; - } - catch (Exception ex) - { - _logger.LogError(ex, "Caught unknown exception while trying to scrobble."); - } - } - } -} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs b/Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs deleted file mode 100644 index b7eae2d..0000000 --- a/Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Notifications; - -namespace Jellyfin.Plugin.Simkl.Services -{ - /// - public class SimklNotificationsFactory : INotificationTypeFactory - { - /// - /// Notification category. - /// - public const string NotificationCategory = "Simkl Scrobbling"; - - /// - /// Notification movie type. - /// - public const string NotificationMovieType = "SimklScrobblingMovie"; - - /// - /// Notification show type. - /// - public const string NotificationShowType = "SimklScrobblingShow"; - - /// - public IEnumerable GetNotificationTypes() - { - yield return new NotificationTypeInfo - { - Type = NotificationMovieType, - Name = "Scrobbling Movie", - Category = NotificationCategory, - Enabled = true, - IsBasedOnUserEvent = false - }; - - yield return new NotificationTypeInfo - { - Type = NotificationShowType, - Name = "Scrobbling TV Show", - Category = NotificationCategory, - Enabled = true, - IsBasedOnUserEvent = false - }; - } - - /// - /// Get notification request. - /// - /// Item. - /// USer id. - /// The notification request. - public static NotificationRequest GetNotificationRequest(BaseItemDto item, Guid userId) - { - var nr = new NotificationRequest - { - Date = DateTime.UtcNow, - UserIds = new[] { userId }, - SendToUserMode = SendToUserType.Custom - }; - - // TODO: Set url parameter to simkl's movie url - if (item.IsMovie == true || item.Type == "Movie") - { - nr.NotificationType = NotificationMovieType; - nr.Name = "Movie Scrobbled to Simkl"; - nr.Description = "The movie " + item.Name; - nr.Description += " has been scrobbled to your account"; - } - - if (item.IsSeries == true || item.Type == "Episode") - { - nr.NotificationType = NotificationShowType; - nr.Name = "Episode Scrobbled to Simkl"; - nr.Description = item.SeriesName; - nr.Description += " S" + item.ParentIndexNumber + ":E" + item.IndexNumber; - nr.Description += " - " + item.Name; - nr.Description += " has been scrobbled to your account"; - } - - return nr; - } - } -} \ No newline at end of file diff --git a/Jellyfin.Plugin.Simkl/SimklPlugin.cs b/Jellyfin.Plugin.Simkl/SimklPlugin.cs index 254e4db..4b3d0ad 100644 --- a/Jellyfin.Plugin.Simkl/SimklPlugin.cs +++ b/Jellyfin.Plugin.Simkl/SimklPlugin.cs @@ -27,13 +27,13 @@ namespace Jellyfin.Plugin.Simkl /// /// Gets the current instance of the plugin. /// - public static SimklPlugin Instance { get; private set; } + public static SimklPlugin? Instance { get; private set; } /// public override Guid Id => new Guid("07CAEF58-A94B-4211-A62C-F9774E04EBDB"); /// - public override string Name => "Simkl TV Tracker"; + public override string Name => "Simkl"; /// public override string Description => "Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!"; diff --git a/README.md b/README.md index 4865a74..15585f0 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,11 @@ Repository Url: https://repo.codyrobibero.dev/manifest.json -## How to enable notifications when something is marked as watched -1. On Jellyfin's dashboard, you'll have to go to the bottom and, on expert options, select "Notifications" -2. There, on section "Simkl Scrobbling", enable both notifications - -## How to enable debugging -To report a bug or an error, we'll need more info to know how to fix it. To send us the needed reports you'll need to -first enable debug logging. - -1. On Jellyfin's dashboard, scroll to the bottom and, on expert options, select "Logs" (right above "Notifications") -2. Click on "Enable debug logging" -3. Restart the server and reproduce the error - -Now you can contact us to try and fix the problem - ## Current features - Multi-user support - Auto scrobble Movies and TV Shows at given percentage to Simkl -- Easy login using pin (no more putting passwords with the TV remote) +- Easy login using pin - If scrobbling fails, search it using filename using Simkl's API and then scrobble it -- Send notifications about scrobbling -Modified for Jellyfin from https://github.com/SIMKL/Emby/ \ No newline at end of file +## Future features +- Sync all watch status with Simkl \ No newline at end of file diff --git a/build.yaml b/build.yaml index 516657d..e1698f9 100644 --- a/build.yaml +++ b/build.yaml @@ -2,7 +2,7 @@ name: "Simkl" guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB" version: "1.0.0.0" -targetAbi: "10.6.0.0" +targetAbi: "10.7.0.0" owner: "crobibero" overview: "Scrobble to Simkl" description: >