init
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Exceptions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class InvalidTokenException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
|
||||
/// </summary>
|
||||
public InvalidTokenException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message.</param>
|
||||
public InvalidTokenException(string msg) : base(msg)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message.</param>
|
||||
/// <param name="inner">The inner exception.</param>
|
||||
public InvalidTokenException(string msg, Exception inner) : base(msg, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Jellyfin.Plugin.Simkl.API.Responses;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Get oauth pin.
|
||||
/// </summary>
|
||||
[Route("/Simkl/oauth/pin", "GET")]
|
||||
public class GetPin : IReturn<CodeResponse>
|
||||
{
|
||||
// Doesn't receive anything
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Jellyfin.Plugin.Simkl.API.Responses;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Get pin status.
|
||||
/// </summary>
|
||||
[Route("/Simkl/oauth/pin/{user_code}", "GET")]
|
||||
public class GetPinStatus : IReturn<CodeStatusResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets user code.
|
||||
/// </summary>
|
||||
[ApiMember(Name = "user_code", Description = "pin to be introduced by the user", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UserCode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Jellyfin.Plugin.Simkl.API.Objects;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Get user settings.
|
||||
/// </summary>
|
||||
[Route("/Simkl/users/settings/{userId}", "GET")]
|
||||
public class GetUserSettings : IReturn<UserSettings>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets user id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note: In the future, when we'll have config for more than one user, we'll use a parameter.
|
||||
/// </remarks>
|
||||
[ApiMember(Name = "id", Description = "user id", IsRequired = true, DataType = "Guid", ParameterType = "path", Verb = "GET")]
|
||||
public Guid UserId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Account.
|
||||
/// </summary>
|
||||
public class Account
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets account id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets timezone.
|
||||
/// </summary>
|
||||
public string Timezone { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Connections.
|
||||
/// </summary>
|
||||
public class Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether facebook.
|
||||
/// </summary>
|
||||
public bool Facebook { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Season.
|
||||
/// </summary>
|
||||
public class Season
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the season number.
|
||||
/// </summary>
|
||||
public int? Number { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episodes.
|
||||
/// </summary>
|
||||
public ShowEpisode[] Episodes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Show episode.
|
||||
/// </summary>
|
||||
public class ShowEpisode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets episode number.
|
||||
/// </summary>
|
||||
public int? Number { get; set; }
|
||||
// TODO: watched_at
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl episode container.
|
||||
/// </summary>
|
||||
public class SimklEpisode : SimklMediaObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets watched at.
|
||||
/// </summary>
|
||||
[JsonPropertyName("watched_at")]
|
||||
public string WatchedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets ids.
|
||||
/// </summary>
|
||||
public override SimklIds Ids { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the season.
|
||||
/// </summary>
|
||||
public int Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode.
|
||||
/// </summary>
|
||||
public int Episode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets multipart.
|
||||
/// </summary>
|
||||
public bool? Multipart { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl File.
|
||||
/// </summary>
|
||||
public class SimklFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the file.
|
||||
/// </summary>
|
||||
public string File { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the part.
|
||||
/// </summary>
|
||||
public int? Part { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hash.
|
||||
/// </summary>
|
||||
public string Hash { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma warning disable CA2227
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl history container.
|
||||
/// </summary>
|
||||
public class SimklHistory
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklHistory"/> class.
|
||||
/// </summary>
|
||||
public SimklHistory()
|
||||
{
|
||||
Movies = new List<SimklMovie>();
|
||||
Shows = new List<SimklShow>();
|
||||
Episodes = new List<SimklEpisode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of movies.
|
||||
/// </summary>
|
||||
public List<SimklMovie> Movies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of shows.
|
||||
/// </summary>
|
||||
public List<SimklShow> Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of episodes.
|
||||
/// </summary>
|
||||
public List<SimklEpisode> Episodes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl Ids.
|
||||
/// </summary>
|
||||
public class SimklIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklIds"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerIds">The provider ids.</param>
|
||||
public SimklIds(Dictionary<string, string> providerIds)
|
||||
{
|
||||
foreach (var (key, value) in providerIds)
|
||||
{
|
||||
var prop = GetType().GetProperty(key, BindingFlags.IgnoreCase);
|
||||
if (prop.PropertyType == typeof(int?))
|
||||
{
|
||||
prop.SetValue(this, int.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(string))
|
||||
{
|
||||
prop.SetValue(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets simkl.
|
||||
/// </summary>
|
||||
public int? Simkl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the imdb id.
|
||||
/// </summary>
|
||||
public string Imdb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the slug.
|
||||
/// </summary>
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the netflix id.
|
||||
/// </summary>
|
||||
public string Netflix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TMDb id.
|
||||
/// </summary>
|
||||
public string Tmdb { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl media object.
|
||||
/// </summary>
|
||||
public abstract class SimklMediaObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets ids.
|
||||
/// </summary>
|
||||
public abstract SimklIds Ids { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl movie.
|
||||
/// </summary>
|
||||
public class SimklMovie : SimklMediaObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklMovie"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The base item dto.</param>
|
||||
public SimklMovie(BaseItemDto item)
|
||||
{
|
||||
Title = item.OriginalTitle;
|
||||
Year = item.ProductionYear;
|
||||
Ids = new SimklMovieIds(item.ProviderIds);
|
||||
WatchedAt = DateTime.UtcNow.ToString("yyyy-MM-dd HH\\:mm\\:ss", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the movie title.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override SimklIds Ids { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets watched at.
|
||||
/// </summary>
|
||||
public string WatchedAt { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl movie ids.
|
||||
/// </summary>
|
||||
public class SimklMovieIds : SimklIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklMovieIds"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerMovieIds">the provider movie ids.</param>
|
||||
public SimklMovieIds(Dictionary<string, string> providerMovieIds)
|
||||
: base(providerMovieIds)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tvdb id.
|
||||
/// </summary>
|
||||
public int? Tvdb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mal id.
|
||||
/// </summary>
|
||||
public int? Mal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the anidb id.
|
||||
/// </summary>
|
||||
public int? Anidb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hulu id.
|
||||
/// </summary>
|
||||
public int? Hulu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the crunchyroll id.
|
||||
/// </summary>
|
||||
public int? Crunchyroll { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the movie db id.
|
||||
/// </summary>
|
||||
public string Moviedb { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl show.
|
||||
/// </summary>
|
||||
public class SimklShow : SimklMediaObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklShow"/> class.
|
||||
/// </summary>
|
||||
/// <param name="mediaInfo">The media info.</param>
|
||||
public SimklShow(BaseItemDto mediaInfo)
|
||||
{
|
||||
Title = mediaInfo.SeriesName;
|
||||
Ids = new SimklShowIds(mediaInfo.ProviderIds);
|
||||
Year = mediaInfo.ProductionYear;
|
||||
Seasons = new Season[]
|
||||
{
|
||||
new Season
|
||||
{
|
||||
Number = mediaInfo.ParentIndexNumber,
|
||||
Episodes = new[]
|
||||
{
|
||||
new ShowEpisode
|
||||
{
|
||||
Number = mediaInfo.IndexNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets title.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets year.
|
||||
/// </summary>
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets seasons.
|
||||
/// </summary>
|
||||
public Season[] Seasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets ids.
|
||||
/// </summary>
|
||||
public override SimklIds Ids { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl show ids.
|
||||
/// </summary>
|
||||
public class SimklShowIds : SimklIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklShowIds"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerMovieIds">The provider movie ids.</param>
|
||||
public SimklShowIds(Dictionary<string, string> providerMovieIds)
|
||||
: base(providerMovieIds)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets tvdb.
|
||||
/// </summary>
|
||||
public int? Tvdb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets mal.
|
||||
/// </summary>
|
||||
public int? Mal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets anidb.
|
||||
/// </summary>
|
||||
public int? Anidb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets hulu.
|
||||
/// </summary>
|
||||
public int? Hulu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets crunchyroll.
|
||||
/// </summary>
|
||||
public int? Crunchyroll { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets zap2it.
|
||||
/// </summary>
|
||||
public string Zap2It { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// User.
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets joined at.
|
||||
/// </summary>
|
||||
[JsonPropertyName("joined_at")]
|
||||
public DateTime JoinedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets gender.
|
||||
/// </summary>
|
||||
public string Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets avatar.
|
||||
/// </summary>
|
||||
public string Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets bio.
|
||||
/// </summary>
|
||||
public string Bio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets loc.
|
||||
/// </summary>
|
||||
public string Loc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets age.
|
||||
/// </summary>
|
||||
public string Age { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// User settings.
|
||||
/// </summary>
|
||||
public class UserSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets user.
|
||||
/// </summary>
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets account.
|
||||
/// </summary>
|
||||
public Account Account { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets error.
|
||||
/// </summary>
|
||||
public string Error { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Code response.
|
||||
/// </summary>
|
||||
public class CodeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets result.
|
||||
/// </summary>
|
||||
public string Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets device code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("device_code")]
|
||||
public string DeviceCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets user code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("user_code")]
|
||||
public string UserCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets verification url.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verification_url")]
|
||||
public string VerificationUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets expires in.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets interval.
|
||||
/// </summary>
|
||||
public int Interval { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Code status response.
|
||||
/// </summary>
|
||||
public class CodeStatusResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets result.
|
||||
/// </summary>
|
||||
public string Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets message.
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets access token.
|
||||
/// </summary>
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Jellyfin.Plugin.Simkl.API.Objects;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Search file response.
|
||||
/// </summary>
|
||||
public class SearchFileResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets type.
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets episode.
|
||||
/// </summary>
|
||||
public SimklEpisode Episode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets movie.
|
||||
/// </summary>
|
||||
public SimklMovie Movie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets show.
|
||||
/// </summary>
|
||||
public SimklShow Show { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Jellyfin.Plugin.Simkl.API.Objects;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// sync history not found.
|
||||
/// </summary>
|
||||
public class SyncHistoryNotFound
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets movies.
|
||||
/// </summary>
|
||||
public SimklMovie[] Movies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets shows.
|
||||
/// </summary>
|
||||
public SimklShow[] Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets episodes.
|
||||
/// </summary>
|
||||
public SimklEpisode[] Episodes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Sync history response.
|
||||
/// </summary>
|
||||
public class SyncHistoryResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets added.
|
||||
/// </summary>
|
||||
public SyncHistoryResponseCount Added { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets not found.
|
||||
/// </summary>
|
||||
[JsonPropertyName("not_found")]
|
||||
public SyncHistoryNotFound NotFound { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Sync history response count.
|
||||
/// </summary>
|
||||
public class SyncHistoryResponseCount
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets movies.
|
||||
/// </summary>
|
||||
public int Movies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets shows.
|
||||
/// </summary>
|
||||
public int Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets episodes.
|
||||
/// </summary>
|
||||
public int Episodes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Jellyfin.Plugin.Simkl.API.Objects;
|
||||
using Jellyfin.Plugin.Simkl.API.Responses;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server endpoints.
|
||||
/// </summary>
|
||||
public class ServerEndpoint : IService, IHasResultFactory
|
||||
{
|
||||
private readonly SimklApi _api;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IJsonSerializer _json;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerEndpoint"/> class.
|
||||
/// </summary>
|
||||
/// <param name="api">The simkl api.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{ServerEndpoint}"/> interface.</param>
|
||||
/// <param name="json">Instance of the <see cref="IJsonSerializer"/> interface.</param>
|
||||
public ServerEndpoint(SimklApi api, ILogger logger, IJsonSerializer json)
|
||||
{
|
||||
_api = api;
|
||||
_logger = logger;
|
||||
_json = json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets result factory.
|
||||
/// </summary>
|
||||
public IHttpResultFactory ResultFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets request.
|
||||
/// </summary>
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get pin request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>The code response.</returns>
|
||||
public CodeResponse Get(GetPin request)
|
||||
{
|
||||
return _api.GetCode().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get pin status.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Code status response.</returns>
|
||||
public CodeStatusResponse Get(GetPinStatus request)
|
||||
{
|
||||
return _api.GetCodeStatus(request.UserCode).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user settings.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>User settings.</returns>
|
||||
public UserSettings Get(GetUserSettings request)
|
||||
{
|
||||
_logger.LogDebug(_json.SerializeToString(request));
|
||||
return _api.GetUserSettings(SimklPlugin.Instance.Configuration.GetByGuid(request.UserId).UserToken).Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using System.IO;
|
||||
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.Net;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging; // using System.Threading;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Simkl Api.
|
||||
/// </summary>
|
||||
public class SimklApi
|
||||
{
|
||||
/* INTERFACES */
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly ILogger<SimklApi> _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
/* BASIC API THINGS */
|
||||
|
||||
/// <summary>
|
||||
/// Base url.
|
||||
/// </summary>
|
||||
public const string Baseurl = @"https://api.simkl.com";
|
||||
// public const string BASE_URL = @"http://private-9c39b-simkl.apiary-proxy.com";
|
||||
|
||||
/// <summary>
|
||||
/// Redirect uri.
|
||||
/// </summary>
|
||||
public const string RedirectUri = @"https://simkl.com/apps/emby/connected/";
|
||||
|
||||
/// <summary>
|
||||
/// Api key.
|
||||
/// </summary>
|
||||
public const string Apikey = @"27dd5d6adc24aa1ad9f95ef913244cbaf6df5696036af577ed41670473dc97d0";
|
||||
|
||||
/// <summary>
|
||||
/// Secret.
|
||||
/// </summary>
|
||||
public const string Secret = @"d7b9feb9d48bbaa69dbabaca21ba4671acaa89198637e9e136a4d69ec97ab68b";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="json">Instance of the <see cref="IJsonSerializer"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{SimklApi}"/> interface.</param>
|
||||
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
||||
public SimklApi(IJsonSerializer json, ILogger<SimklApi> logger, IHttpClient httpClient)
|
||||
{
|
||||
_json = json;
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get code.
|
||||
/// </summary>
|
||||
/// <returns>Code response.</returns>
|
||||
public async Task<CodeResponse> GetCode()
|
||||
{
|
||||
var uri = $"/oauth/pin?client_id={Apikey}&redirect={RedirectUri}";
|
||||
return _json.DeserializeFromStream<CodeResponse>(await Get(uri).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get code status.
|
||||
/// </summary>
|
||||
/// <param name="userCode">User code.</param>
|
||||
/// <returns>Code status.</returns>
|
||||
public async Task<CodeStatusResponse> GetCodeStatus(string userCode)
|
||||
{
|
||||
var uri = $"/oauth/pin/{userCode}?client_id={Apikey}";
|
||||
return _json.DeserializeFromStream<CodeStatusResponse>(await Get(uri).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user settings.
|
||||
/// </summary>
|
||||
/// <param name="userToken">User token.</param>
|
||||
/// <returns>User settings.</returns>
|
||||
public async Task<UserSettings> GetUserSettings(string userToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromStream<UserSettings>(await Post("/users/settings/", userToken).ConfigureAwait(false));
|
||||
}
|
||||
catch (MediaBrowser.Model.Net.HttpException 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" };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark as watched.
|
||||
/// </summary>
|
||||
/// <param name="item">Item.</param>
|
||||
/// <param name="userToken">User token.</param>
|
||||
/// <returns>Status.</returns>
|
||||
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)
|
||||
{
|
||||
return (true, item);
|
||||
}
|
||||
|
||||
// If we are here, is because the item has not been found
|
||||
// let's try scrobbling from full path
|
||||
try
|
||||
{
|
||||
(history, item) = await GetHistoryFromFileName(item).ConfigureAwait(false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get from file.
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename.</param>
|
||||
/// <returns>Search file response.</returns>
|
||||
private async Task<SearchFileResponse> 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 = r.ReadToEnd();
|
||||
_logger.LogDebug("Response: " + t);
|
||||
return _json.DeserializeFromString<SearchFileResponse>(t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get history from file name.
|
||||
/// </summary>
|
||||
/// <param name="item">Item.</param>
|
||||
/// <param name="fullpath">Full path.</param>
|
||||
/// <returns>Srobble history.</returns>
|
||||
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 history = new SimklHistory();
|
||||
if (item.IsMovie == true || item.Type == "Movie")
|
||||
{
|
||||
if (mo.Type != "movie")
|
||||
{
|
||||
throw new InvalidDataException("type != movie (" + mo.Type + ")");
|
||||
}
|
||||
|
||||
item.Name = mo.Movie.Title;
|
||||
item.ProductionYear = mo.Movie.Year;
|
||||
history.Movies.Add(mo.Movie);
|
||||
}
|
||||
else if (item.IsSeries == true || item.Type == "Episode")
|
||||
{
|
||||
if (mo.Type != "episode")
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
return (history, item);
|
||||
}
|
||||
|
||||
private static HttpRequestOptions 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");
|
||||
if (!string.IsNullOrEmpty(userToken))
|
||||
{
|
||||
options.RequestHeaders.Add("Authorization", "Bearer " + userToken);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static SimklHistory CreateHistoryFromItem(BaseItemDto item)
|
||||
{
|
||||
var history = new SimklHistory();
|
||||
|
||||
if (item.IsMovie == true || item.Type == "Movie")
|
||||
{
|
||||
history.Movies.Add(new SimklMovie(item));
|
||||
}
|
||||
else if (item.IsSeries == true || item.Type == "Episode")
|
||||
{
|
||||
// TODO: TV Shows scrobbling (WIP)
|
||||
history.Shows.Add(new SimklShow(item));
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements /sync/history method from simkl.
|
||||
/// </summary>
|
||||
/// <param name="history">History object.</param>
|
||||
/// <param name="userToken">User token.</param>
|
||||
/// <returns>The sync history response.</returns>
|
||||
private async Task<SyncHistoryResponse> SyncHistoryAsync(SimklHistory history, string userToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Syncing History: " + _json.SerializeToString(history));
|
||||
return _json.DeserializeFromStream<SyncHistoryResponse>(await Post("/sync/history", userToken, history).ConfigureAwait(false));
|
||||
}
|
||||
catch (MediaBrowser.Model.Net.HttpException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.LogError("Invalid user token " + userToken + ", deleting");
|
||||
SimklPlugin.Instance.Configuration.DeleteUserToken(userToken);
|
||||
throw new InvalidTokenException("Invalid user token " + userToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API's private get method, given RELATIVE url and headers.
|
||||
/// </summary>
|
||||
/// <param name="url">Relative url.</param>
|
||||
/// <param name="userToken">Authentication token.</param>
|
||||
/// <returns>HTTP(s) Stream to be used.</returns>
|
||||
private async Task<Stream> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API's private post method.
|
||||
/// </summary>
|
||||
/// <param name="url">Relative post url.</param>
|
||||
/// <param name="userToken">Authentication token.</param>
|
||||
/// <param name="data">Object to serialize.</param>
|
||||
private async Task<Stream> Post(string url, string userToken = null, object data = null)
|
||||
{
|
||||
var options = GetOptions(userToken);
|
||||
options.Url = Baseurl + url;
|
||||
if (data != null)
|
||||
{
|
||||
options.RequestContent = _json.SerializeToString(data);
|
||||
}
|
||||
|
||||
return (await _httpClient.Post(options).ConfigureAwait(false)).Content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Class needed to create a Plugin and configure it.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
UserConfigs = Array.Empty<UserConfig>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of user configs.
|
||||
/// </summary>
|
||||
public UserConfig[] UserConfigs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get config by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The user id.</param>
|
||||
/// <returns>Stored user config.</returns>
|
||||
public UserConfig GetByGuid(Guid id)
|
||||
{
|
||||
return UserConfigs.FirstOrDefault(c => c.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete user token.
|
||||
/// </summary>
|
||||
/// <param name="userToken">User token.</param>
|
||||
public void DeleteUserToken(string userToken)
|
||||
{
|
||||
foreach (var config in UserConfigs)
|
||||
{
|
||||
if (config.UserToken == userToken)
|
||||
{
|
||||
config.UserToken = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
SimklPlugin.Instance.SaveConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// User config.
|
||||
/// </summary>
|
||||
public class UserConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserConfig"/> class.
|
||||
/// </summary>
|
||||
public UserConfig()
|
||||
{
|
||||
ScrobbleMovies = true;
|
||||
ScrobbleShows = true;
|
||||
ScrobblePercentage = 70;
|
||||
ScrobbleNowWatchingPercentage = 5;
|
||||
MinLength = 5;
|
||||
UserToken = string.Empty; // Todo: check if token is still valid
|
||||
ScrobbleTimeout = 30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether scrobble movies.
|
||||
/// </summary>
|
||||
public bool ScrobbleMovies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether scrobble shows.
|
||||
/// </summary>
|
||||
public bool ScrobbleShows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets scrobble percentage.
|
||||
/// </summary>
|
||||
public int ScrobblePercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets scrobble now watching percentage.
|
||||
/// </summary>
|
||||
public int ScrobbleNowWatchingPercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets min length.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Minimum length for scrobbling (in minutes).
|
||||
/// </remarks>
|
||||
public int MinLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets user token.
|
||||
/// </summary>
|
||||
public string UserToken { get; set; } // Is the user logged in
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets scrobble timeout.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Time between scrobbling tries.
|
||||
/// </remarks>
|
||||
public int ScrobbleTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets user id.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Simkl's TV Tracker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-role="page" class="page type-interior pluginConfigurationPage" id="SimklConfigurationPage" data-require="emby-button,emby-checkbox,emby-input,emby-select">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<h1>Simkl's TV Tracker</h1>
|
||||
<form id="SimklConfigurationForm">
|
||||
<div id="selectContainer">
|
||||
<select onchange="SimklConfig.onSelectorChange();" is="emby-select" id="user-selector" label="Showing plugin settings for...">
|
||||
<!-- This will be populated by SimklConfig.populateUsers -->
|
||||
</select>
|
||||
</div>
|
||||
<div id="loginButtonContainer" hidden>
|
||||
<h3>It seems you are not logged in, do you wish to log in?</h3>
|
||||
<button onclick="SimklConfig.startLoginProcess();" is="emby-button" type="button" class="raised button-submit block"><span>Log In</span></button>
|
||||
<button onclick="location.href='https://simkl.com/';" is="emby-button" type="button" class="raised block"><span>Create an account</span></button>
|
||||
</div>
|
||||
<div id="loggingIn" hidden>
|
||||
<h2>Logging In</h2>
|
||||
<div id="loginText"></div>
|
||||
<h3 id="loginPin"></h3>
|
||||
<span id="loginSecondsRemaining">900</span> seconds remaining
|
||||
<button onclick="SimklConfig.stopLoginProcess();" is="emby-button" type="button" class="raised button-cancel block"><span>Cancel</span></button>
|
||||
</div>
|
||||
<div id="configOptionsContainer" hidden>
|
||||
<h3>Hello again <span id="simklName">USERNAME</span>!</h3>
|
||||
<button onclick="SimklConfig.logOut();" is="emby-button" type="button" class="raised button block"><span>Log Out</span></button>
|
||||
<h2>Scrobbling options:</h2>
|
||||
<div class="checkboxcontainer">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="scrobbleMovies" />
|
||||
<span>Autoscrobbling Movies</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxcontainer">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="scrobbleShows" />
|
||||
<span>Autoscrobbling TV Shows</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="scr_pct" type="number" min="0" max="100" pattern="[0-9]*" label="Scrobbling percentage:" />
|
||||
<div class="fieldDescription">
|
||||
Percentage watched needed to scrobble
|
||||
</div>
|
||||
</div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block"><span>${ButtonSave}</span></button>
|
||||
<button is="emby-button" type="button" class="raised block" onclick="history.back();"><span>${Cancel}</span></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var SimklConfig = {
|
||||
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB",
|
||||
onLoginProccess: false,
|
||||
configCache: [],
|
||||
loginTimer: null,
|
||||
remainingTimer: null,
|
||||
finish: null,
|
||||
populateUsers : async function (users) {
|
||||
users.forEach(function(user) {
|
||||
$("#user-selector").append(new Option(user.Name, user.Id));
|
||||
});
|
||||
},
|
||||
loadConfig : async function(user, config) {
|
||||
if (config != null) this.configCache = config;
|
||||
else config = this.configCache;
|
||||
|
||||
console.log("Simkl: Loading config for user " + user);
|
||||
console.log(config);
|
||||
|
||||
$("#loginButtonContainer").hide();
|
||||
$("#configOptionsContainer").hide();
|
||||
|
||||
if (config.userConfigs.some(e => e.guid === user && e.userToken != null && e.userToken !== "")) {
|
||||
$("#configOptionsContainer").show();
|
||||
this.populateOptionsContainer(config.userConfigs.filter(e => e.guid === user)[0]);
|
||||
} else {
|
||||
$("#loginButtonContainer").show();
|
||||
}
|
||||
},
|
||||
saveConfig : async function(guid) {
|
||||
var uconfig = this.configCache.userConfigs.filter(e => e.guid === guid)[0];
|
||||
|
||||
for (var key in uconfig) {
|
||||
var element = $("#configOptionsContainer #"+key);
|
||||
if (element.is(":checkbox")) {
|
||||
uconfig[key] = element.attr("checked");
|
||||
} else {
|
||||
if (element.val() != null) uconfig[key] = element.val();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Saving config:");
|
||||
console.log(this.configCache);
|
||||
ApiClient.updatePluginConfiguration(this.guid, this.configCache).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
},
|
||||
populateOptionsContainer : async function(userConfig) {
|
||||
$("#simklName").html(SimklAPI.getUserSettings(userConfig.guid).user.name);
|
||||
|
||||
for (var key in userConfig) {
|
||||
$("#configOptionsContainer input[type=checkbox]#"+key).attr("checked", userConfig[key]);
|
||||
$("#configOptionsContainer input[type=number]#"+key).val(userConfig[key]);
|
||||
}
|
||||
},
|
||||
startLoginProcess : async function () {
|
||||
this.onLoginProcess = true;
|
||||
|
||||
var code = SimklAPI.getCode();
|
||||
this.finish = new Date();
|
||||
this.finish.setSeconds(this.finish.getSeconds() + code.expires_in);
|
||||
this.nextInterval = new Date();
|
||||
|
||||
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this,code), code.interval*1000);
|
||||
this.remainingTimer = window.setInterval(function() {
|
||||
$("#loginSecondsRemaining").html(Math.round((SimklConfig.finish.getTime() - (new Date().getTime()))/1000));
|
||||
} ,1000);
|
||||
|
||||
$("#loginText").html("Please visit <a href='" + code.verification_url + "/" + code.user_code +
|
||||
"' target='_blank'>" + code.verification_url + "</a> on your phone or computer and enter the following code:");
|
||||
$("#loginPin").html(code.user_code);
|
||||
|
||||
$("#loginButtonContainer").hide();
|
||||
await $("#loggingIn").show();
|
||||
},
|
||||
checkLoginProcess : function (code) {
|
||||
var response = SimklAPI.checkCode(code.user_code);
|
||||
console.log("Response:");
|
||||
console.log(response);
|
||||
|
||||
if (new Date() > this.finish) {
|
||||
Dashboard.alert("Timed out!");
|
||||
this.stopLoginProcess();
|
||||
} else if (response.result === "KO") {
|
||||
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this,code), code.interval*1000);
|
||||
} else if (response.result === "OK") {
|
||||
this.stopLoginProcess();
|
||||
|
||||
// Save key on settings
|
||||
var uguid = $("#user-selector").val();
|
||||
var filter = this.configCache.userConfigs.filter(function(c) {
|
||||
return c.guid === uguid;
|
||||
});
|
||||
if (filter.length > 0) {
|
||||
filter[0].userToken = response.access_token;
|
||||
} else {
|
||||
this.configCache.userConfigs.push({
|
||||
guid: uguid,
|
||||
userToken: response.access_token
|
||||
});
|
||||
}
|
||||
|
||||
console.log(this.configCache);
|
||||
|
||||
ApiClient.updatePluginConfiguration(this.guid, this.configCache);
|
||||
this.loadConfig(uguid);
|
||||
} else {
|
||||
Dashboard.alert("Error logging in");
|
||||
}
|
||||
},
|
||||
stopLoginProcess : async function () {
|
||||
this.onLoginProcess = false;
|
||||
window.clearTimeout(this.loginTimer);
|
||||
window.clearInterval(this.remainingTimer);
|
||||
$("#loginButtonContainer").show();
|
||||
$("#loggingIn").hide();
|
||||
},
|
||||
onSelectorChange : async function () {
|
||||
if (this.onLoginProcess) this.stopLoginProcess();
|
||||
this.loadConfig($("#user-selector").val(), null);
|
||||
},
|
||||
logOut : function(uguid) {
|
||||
if (uguid == null) uguid = $("#user-selector").val();
|
||||
|
||||
// e1f34a57cb3c4767ab6f29cc5c7c0566
|
||||
var filter = this.configCache.userConfigs.filter(function (c) {
|
||||
return c.guid === uguid;
|
||||
});
|
||||
console.log(filter);
|
||||
|
||||
if (filter.length > 0) {
|
||||
filter[0].userToken = "";
|
||||
} else {
|
||||
console.log("User not found " + uguid);
|
||||
}
|
||||
|
||||
console.log(this.configCache);
|
||||
ApiClient.updatePluginConfiguration(this.guid, this.configCache);
|
||||
this.loadConfig(uguid);
|
||||
}
|
||||
}
|
||||
|
||||
var SimklAPI = {
|
||||
getCode: function () {
|
||||
var uri = "/Simkl/oauth/pin";
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", uri, false);
|
||||
request.send();
|
||||
|
||||
if (request.status === 200) {
|
||||
return $.parseJSON(request.response);
|
||||
} else {
|
||||
console.log(request);
|
||||
Dashboard.alert("Some error occurred, see browser log for more details");
|
||||
SimklConfig.stopLoginProcess();
|
||||
}
|
||||
},
|
||||
checkCode : function (user_code) {
|
||||
var uri = "/Simkl/oauth/pin/" + user_code;
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", uri, false);
|
||||
request.send();
|
||||
|
||||
if (request.status === 200) {
|
||||
return $.parseJSON(request.response);
|
||||
} else {
|
||||
console.log(request);
|
||||
Dashboard.alert("Some error occurred, see browser log for more details");
|
||||
SimklConfig.stopLoginProcess();
|
||||
}
|
||||
},
|
||||
getUserSettings: function (secret) {
|
||||
var uri = "/Simkl/users/settings/" + secret;
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", uri, false);
|
||||
request.send();
|
||||
|
||||
if (request.status === 200) {
|
||||
return $.parseJSON(request.response);
|
||||
} else {
|
||||
console.log(request);
|
||||
Dashboard.alert("Something went wrong, see logs for more details");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#SimklConfigurationPage").on("pageshow", async function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
await Promise.all([
|
||||
ApiClient.getUsers().then(SimklConfig.populateUsers),
|
||||
ApiClient.getPluginConfiguration(SimklConfig.guid).then(SimklConfig.loadConfig.bind(SimklConfig, ApiClient.getCurrentUserId()))]);
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
|
||||
$("#SimklConfigurationForm").on("submit", function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
SimklConfig.saveConfig($("#user-selector").val());
|
||||
Dashboard.hideLoadingMsg();
|
||||
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<NoWarn>CA1707;CA1819</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.6-*" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Configuration\configPage.html" />
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,206 @@
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class Scrobbler : IServerEntryPoint
|
||||
{
|
||||
private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions
|
||||
private readonly ILogger<Scrobbler> _logger;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly INotificationManager _notifications;
|
||||
private readonly Dictionary<string, Guid> _lastScrobbled; // Library ID of last scrobbled item
|
||||
private SimklApi _api;
|
||||
private DateTime _nextTry;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Scrobbler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="json">Instance of the <see cref="IJsonSerializer"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILogger{Scrobbler}"/> interface.</param>
|
||||
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
||||
/// <param name="notifications">Instance of the <see cref="INotificationManager"/> interface.</param>
|
||||
public Scrobbler(
|
||||
IJsonSerializer json,
|
||||
ISessionManager sessionManager,
|
||||
ILoggerFactory loggerFactory,
|
||||
IHttpClient httpClient,
|
||||
INotificationManager notifications)
|
||||
{
|
||||
_json = json;
|
||||
_sessionManager = sessionManager;
|
||||
_logger = loggerFactory.CreateLogger<Scrobbler>();
|
||||
_notifications = notifications;
|
||||
_api = new SimklApi(json, loggerFactory.CreateLogger<SimklApi>(), httpClient);
|
||||
_lastScrobbled = new Dictionary<string, Guid>();
|
||||
_nextTry = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_sessionManager.PlaybackProgress += OnPlaybackProgress;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Dispose all resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
|
||||
_api = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Session can be scrobbled.
|
||||
/// </summary>
|
||||
/// <param name="config">The plugin configuration.</param>
|
||||
/// <param name="session">The session.</param>
|
||||
/// <returns>If session can be scrobbled.</returns>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Notifications;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl.Services
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SimklNotificationsFactory : INotificationTypeFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Notification category.
|
||||
/// </summary>
|
||||
public const string NotificationCategory = "Simkl Scrobbling";
|
||||
|
||||
/// <summary>
|
||||
/// Notification movie type.
|
||||
/// </summary>
|
||||
public const string NotificationMovieType = "SimklScrobblingMovie";
|
||||
|
||||
/// <summary>
|
||||
/// Notification show type.
|
||||
/// </summary>
|
||||
public const string NotificationShowType = "SimklScrobblingShow";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<NotificationTypeInfo> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get notification request.
|
||||
/// </summary>
|
||||
/// <param name="item">Item.</param>
|
||||
/// <param name="userId">USer id.</param>
|
||||
/// <returns>The notification request.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.Simkl.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Simkl
|
||||
{
|
||||
/// <summary>
|
||||
/// SIMKL tracker.
|
||||
/// </summary>
|
||||
public class SimklPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimklPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||
public SimklPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current instance of the plugin.
|
||||
/// </summary>
|
||||
public static SimklPlugin Instance { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Guid Id => new Guid("07CAEF58-A94B-4211-A62C-F9774E04EBDB");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => "Simkl TV Tracker";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Description => "Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
yield return new PluginPageInfo
|
||||
{
|
||||
Name = Name,
|
||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user