Support Jellyfin 10.7
This commit is contained in:
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The simkl endpoints.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = "DefaultAuthorization")]
|
||||||
|
[Route("Simkl")]
|
||||||
|
public class Endpoints : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly SimklApi _simklApi;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Endpoints"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="simklApi">Instance of the <see cref="SimklApi"/>.</param>
|
||||||
|
public Endpoints(SimklApi simklApi)
|
||||||
|
{
|
||||||
|
_simklApi = simklApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the oauth pin.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The oauth pin.</returns>
|
||||||
|
[HttpGet("oauth/pin")]
|
||||||
|
public async Task<ActionResult<CodeResponse?>> GetPin()
|
||||||
|
{
|
||||||
|
return await _simklApi.GetCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the status for the code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userCode">The user auth code.</param>
|
||||||
|
/// <returns>The code status response.</returns>
|
||||||
|
[HttpGet("oauth/pin/{userCode}")]
|
||||||
|
public async Task<ActionResult<CodeStatusResponse?>> GetPinStatus([FromRoute] string userCode)
|
||||||
|
{
|
||||||
|
return await _simklApi.GetCodeStatus(userCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the settings for the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user id.</param>
|
||||||
|
/// <returns>The user settings.</returns>
|
||||||
|
[HttpGet("users/settings/{userId}")]
|
||||||
|
public async Task<ActionResult<UserSettings?>> GetUserSettings([FromRoute] Guid userId)
|
||||||
|
{
|
||||||
|
var userConfiguration = SimklPlugin.Instance?.Configuration.GetByGuid(userId);
|
||||||
|
if (userConfiguration == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _simklApi.GetUserSettings(userConfiguration.UserToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ namespace Jellyfin.Plugin.Simkl.API.Exceptions
|
|||||||
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
|
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="msg">The message.</param>
|
/// <param name="msg">The message.</param>
|
||||||
public InvalidTokenException(string msg) : base(msg)
|
public InvalidTokenException(string msg)
|
||||||
|
: base(msg)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ namespace Jellyfin.Plugin.Simkl.API.Exceptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="msg">The message.</param>
|
/// <param name="msg">The message.</param>
|
||||||
/// <param name="inner">The inner exception.</param>
|
/// <param name="inner">The inner exception.</param>
|
||||||
public InvalidTokenException(string msg, Exception inner) : base(msg, inner)
|
public InvalidTokenException(string msg, Exception inner)
|
||||||
|
: base(msg, inner)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Jellyfin.Plugin.Simkl.API.Responses;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
#pragma warning disable SA1300
|
|
||||||
|
|
||||||
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 user_code { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Season.
|
/// Season.
|
||||||
@@ -8,11 +12,13 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the season number.
|
/// Gets or sets the season number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? number { get; set; }
|
[JsonPropertyName("number")]
|
||||||
|
public int? Number { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the episodes.
|
/// Gets or sets the episodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ShowEpisode[] episodes { get; set; }
|
[JsonPropertyName("episodes")]
|
||||||
|
public IReadOnlyList<ShowEpisode> Episodes { get; set; } = Array.Empty<ShowEpisode>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show episode.
|
/// Show episode.
|
||||||
@@ -8,7 +10,8 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets episode number.
|
/// Gets or sets episode number.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? number { get; set; }
|
[JsonPropertyName("number")]
|
||||||
|
public int? Number { get; set; }
|
||||||
// TODO: watched_at
|
// TODO: watched_at
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
#pragma warning disable SA1300
|
#pragma warning disable SA1300
|
||||||
|
|
||||||
@@ -12,31 +13,30 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// Gets or sets watched at.
|
/// Gets or sets watched at.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("watched_at")]
|
[JsonPropertyName("watched_at")]
|
||||||
public string watched_at { get; set; }
|
public DateTime? WatchedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets ids.
|
|
||||||
/// </summary>
|
|
||||||
public override SimklIds ids { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the title.
|
/// Gets or sets the title.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string title { get; set; }
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the season.
|
/// Gets or sets the season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int season { get; set; }
|
[JsonPropertyName("season")]
|
||||||
|
public int? Season { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the episode.
|
/// Gets or sets the episode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int episode { get; set; }
|
[JsonPropertyName("episode")]
|
||||||
|
public int? Episode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets multipart.
|
/// Gets or sets multipart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? multipart { get; set; }
|
[JsonPropertyName("multipart")]
|
||||||
|
public bool? Multipart { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,16 +10,19 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the file.
|
/// Gets or sets the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string file { get; set; }
|
[JsonPropertyName("file")]
|
||||||
|
public string? File { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the part.
|
/// Gets or sets the part.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? part { get; set; }
|
[JsonPropertyName("part")]
|
||||||
|
public int? Part { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the hash.
|
/// Gets or sets the hash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string hash { get; set; }
|
[JsonPropertyName("hash")]
|
||||||
|
public string? Hash { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma warning disable CA2227
|
#pragma warning disable CA2227
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
@@ -14,24 +15,27 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklHistory()
|
public SimklHistory()
|
||||||
{
|
{
|
||||||
movies = new List<SimklMovie>();
|
Movies = new List<SimklMovie>();
|
||||||
shows = new List<SimklShow>();
|
Shows = new List<SimklShow>();
|
||||||
episodes = new List<SimklEpisode>();
|
Episodes = new List<SimklEpisode>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets list of movies.
|
/// Gets or sets list of movies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SimklMovie> movies { get; set; }
|
[JsonPropertyName("movies")]
|
||||||
|
public List<SimklMovie> Movies { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of shows.
|
/// Gets or sets the list of shows.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SimklShow> shows { get; set; }
|
[JsonPropertyName("shows")]
|
||||||
|
public List<SimklShow> Shows { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of episodes.
|
/// Gets or sets the list of episodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SimklEpisode> episodes { get; set; }
|
[JsonPropertyName("episodes")]
|
||||||
|
public List<SimklEpisode> Episodes { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
@@ -17,52 +18,57 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
{
|
{
|
||||||
foreach (var (key, value) in providerIds)
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets simkl.
|
/// Gets or sets the simkl id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? simkl { get; set; }
|
[JsonPropertyName("simkl")]
|
||||||
|
public int? Simkl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the imdb id.
|
/// Gets or sets the imdb id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string imdb { get; set; }
|
[JsonPropertyName("imdb")]
|
||||||
|
public string? Imdb { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the slug.
|
/// Gets or sets the slug.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string slug { get; set; }
|
[JsonPropertyName("slug")]
|
||||||
|
public string? Slug { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the netflix id.
|
/// Gets or sets the netflix id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string netflix { get; set; }
|
[JsonPropertyName("netflix")]
|
||||||
|
public string? Netflix { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the TMDb id.
|
/// Gets or sets the TMDb id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string tmdb { get; set; }
|
[JsonPropertyName("tmdb")]
|
||||||
|
public string? Tmdb { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Simkl media object.
|
/// Simkl media object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SimklMediaObject
|
public class SimklMediaObject
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets ids.
|
/// Gets or sets ids.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract SimklIds ids { get; set; }
|
[JsonPropertyName("ids")]
|
||||||
|
public SimklIds? Ids { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Text.Json.Serialization;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
@@ -15,28 +15,28 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <param name="item">The base item dto.</param>
|
/// <param name="item">The base item dto.</param>
|
||||||
public SimklMovie(BaseItemDto item)
|
public SimklMovie(BaseItemDto item)
|
||||||
{
|
{
|
||||||
title = item.OriginalTitle;
|
Title = item.OriginalTitle;
|
||||||
year = item.ProductionYear;
|
Year = item.ProductionYear;
|
||||||
ids = new SimklMovieIds(item.ProviderIds);
|
Ids = new SimklMovieIds(item.ProviderIds);
|
||||||
watched_at = DateTime.UtcNow.ToString("yyyy-MM-dd HH\\:mm\\:ss", CultureInfo.InvariantCulture);
|
WatchedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the movie title.
|
/// Gets or sets the movie title.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string title { get; set; }
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the year.
|
/// Gets or sets the year.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? year { get; set; }
|
[JsonPropertyName("year")]
|
||||||
|
public int? Year { get; set; }
|
||||||
/// <inheritdoc />
|
|
||||||
public override SimklIds ids { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets watched at.
|
/// Gets or sets watched at.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string watched_at { get; }
|
[JsonPropertyName("watched_at")]
|
||||||
|
public DateTime? WatchedAt { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
@@ -19,31 +20,37 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the tvdb id.
|
/// Gets or sets the tvdb id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? tvdb { get; set; }
|
[JsonPropertyName("tvdb")]
|
||||||
|
public int? Tvdb { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the mal id.
|
/// Gets or sets the mal id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? mal { get; set; }
|
[JsonPropertyName("mal")]
|
||||||
|
public int? Mal { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the anidb id.
|
/// Gets or sets the anidb id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? anidb { get; set; }
|
[JsonPropertyName("anidb")]
|
||||||
|
public int? Anidb { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the hulu id.
|
/// Gets or sets the hulu id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? hulu { get; set; }
|
[JsonPropertyName("hulu")]
|
||||||
|
public int? Hulu { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the crunchyroll id.
|
/// Gets or sets the crunchyroll id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? crunchyroll { get; set; }
|
[JsonPropertyName("crunchyroll")]
|
||||||
|
public int? Crunchyroll { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the movie db id.
|
/// Gets or sets the movie db id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string moviedb { get; set; }
|
[JsonPropertyName("moviedb")]
|
||||||
|
public string? Moviedb { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
@@ -13,19 +15,19 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <param name="mediaInfo">The media info.</param>
|
/// <param name="mediaInfo">The media info.</param>
|
||||||
public SimklShow(BaseItemDto mediaInfo)
|
public SimklShow(BaseItemDto mediaInfo)
|
||||||
{
|
{
|
||||||
title = mediaInfo.SeriesName;
|
Title = mediaInfo.SeriesName;
|
||||||
ids = new SimklShowIds(mediaInfo.ProviderIds);
|
Ids = new SimklShowIds(mediaInfo.ProviderIds);
|
||||||
year = mediaInfo.ProductionYear;
|
Year = mediaInfo.ProductionYear;
|
||||||
seasons = new[]
|
Seasons = new[]
|
||||||
{
|
{
|
||||||
new Season
|
new Season
|
||||||
{
|
{
|
||||||
number = mediaInfo.ParentIndexNumber,
|
Number = mediaInfo.ParentIndexNumber,
|
||||||
episodes = new[]
|
Episodes = new[]
|
||||||
{
|
{
|
||||||
new ShowEpisode
|
new ShowEpisode
|
||||||
{
|
{
|
||||||
number = mediaInfo.IndexNumber
|
Number = mediaInfo.IndexNumber
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,21 +37,19 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets title.
|
/// Gets or sets title.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string title { get; set; }
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets year.
|
/// Gets or sets year.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? year { get; set; }
|
[JsonPropertyName("year")]
|
||||||
|
public int? Year { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets seasons.
|
/// Gets or sets seasons.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Season[] seasons { get; set; }
|
[JsonPropertyName("seasons")]
|
||||||
|
public IReadOnlyList<Season> Seasons { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets ids.
|
|
||||||
/// </summary>
|
|
||||||
public override SimklIds ids { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
@@ -19,31 +20,37 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets tvdb.
|
/// Gets or sets tvdb.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? tvdb { get; set; }
|
[JsonPropertyName("tvdb")]
|
||||||
|
public int? Tvdb { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets mal.
|
/// Gets or sets mal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? mal { get; set; }
|
[JsonPropertyName("mal")]
|
||||||
|
public int? Mal { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets anidb.
|
/// Gets or sets anidb.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? anidb { get; set; }
|
[JsonPropertyName("anidb")]
|
||||||
|
public int? Anidb { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets hulu.
|
/// Gets or sets hulu.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? hulu { get; set; }
|
[JsonPropertyName("hulu")]
|
||||||
|
public int? Hulu { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets crunchyroll.
|
/// Gets or sets crunchyroll.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? crunchyroll { get; set; }
|
[JsonPropertyName("crunchyroll")]
|
||||||
|
public int? Crunchyroll { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets zap2it.
|
/// Gets or sets zap2it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string zap2It { get; set; }
|
[JsonPropertyName("zap2It")]
|
||||||
|
public string? Zap2It { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma warning disable SA1300
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
#pragma warning disable SA1300
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
@@ -10,6 +12,7 @@ namespace Jellyfin.Plugin.Simkl.API.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets name.
|
/// Gets or sets name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string name { get; set; }
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Jellyfin.Plugin.Simkl.API.Objects
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Simkl.API.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User settings.
|
/// User settings.
|
||||||
@@ -8,11 +10,13 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets user.
|
/// Gets or sets user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public User user { get; set; }
|
[JsonPropertyName("user")]
|
||||||
|
public User? User { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets error.
|
/// Gets or sets error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string error { get; set; }
|
[JsonPropertyName("error")]
|
||||||
|
public string? Error { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,35 +11,35 @@ namespace Jellyfin.Plugin.Simkl.API.Responses
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets result.
|
/// Gets or sets result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Result { get; set; }
|
public string? Result { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets device code.
|
/// Gets or sets device code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("device_code")]
|
[JsonPropertyName("device_code")]
|
||||||
public string device_code { get; set; }
|
public string? DeviceCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets user code.
|
/// Gets or sets user code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("user_code")]
|
[JsonPropertyName("user_code")]
|
||||||
public string user_code { get; set; }
|
public string? UserCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets verification url.
|
/// Gets or sets verification url.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("verification_url")]
|
[JsonPropertyName("verification_url")]
|
||||||
public string verification_url { get; set; }
|
public string? VerificationUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets expires in.
|
/// Gets or sets expires in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("expires_in")]
|
[JsonPropertyName("expires_in")]
|
||||||
public int expires_in { get; set; }
|
public int? ExpiresIn { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets interval.
|
/// Gets or sets interval.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Interval { get; set; }
|
public int? Interval { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,17 +11,17 @@ namespace Jellyfin.Plugin.Simkl.API.Responses
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets result.
|
/// Gets or sets result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Result { get; set; }
|
public string? Result { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets message.
|
/// Gets or sets message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Message { get; set; }
|
public string? Message { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets access token.
|
/// Gets or sets access token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("access_token")]
|
[JsonPropertyName("access_token")]
|
||||||
public string access_token { get; set; }
|
public string? AccessToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,21 +10,21 @@ namespace Jellyfin.Plugin.Simkl.API.Responses
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets type.
|
/// Gets or sets type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Type { get; set; }
|
public string? Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets episode.
|
/// Gets or sets episode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklEpisode Episode { get; set; }
|
public SimklEpisode? Episode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets movie.
|
/// Gets or sets movie.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklMovie Movie { get; set; }
|
public SimklMovie? Movie { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets show.
|
/// Gets or sets show.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklShow Show { get; set; }
|
public SimklShow? Show { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Jellyfin.Plugin.Simkl.API.Objects;
|
using System;
|
||||||
|
using Jellyfin.Plugin.Simkl.API.Objects;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API.Responses
|
namespace Jellyfin.Plugin.Simkl.API.Responses
|
||||||
{
|
{
|
||||||
@@ -10,16 +11,16 @@ namespace Jellyfin.Plugin.Simkl.API.Responses
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets movies.
|
/// Gets or sets movies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklMovie[] Movies { get; set; }
|
public SimklMovie[] Movies { get; set; } = Array.Empty<SimklMovie>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets shows.
|
/// Gets or sets shows.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklShow[] Shows { get; set; }
|
public SimklShow[] Shows { get; set; } = Array.Empty<SimklShow>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets episodes.
|
/// Gets or sets episodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SimklEpisode[] Episodes { get; set; }
|
public SimklEpisode[] Episodes { get; set; } = Array.Empty<SimklEpisode>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,12 @@ namespace Jellyfin.Plugin.Simkl.API.Responses
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets added.
|
/// Gets or sets added.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SyncHistoryResponseCount Added { get; set; }
|
public SyncHistoryResponseCount Added { get; set; } = new SyncHistoryResponseCount();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets not found.
|
/// Gets or sets not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("not_found")]
|
[JsonPropertyName("not_found")]
|
||||||
public SyncHistoryNotFound not_found { get; set; }
|
public SyncHistoryNotFound? NotFound { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Server endpoints.
|
|
||||||
/// </summary>
|
|
||||||
public class ServerEndpoint : IService, IHasResultFactory
|
|
||||||
{
|
|
||||||
private readonly SimklApi _api;
|
|
||||||
private readonly ILogger<ServerEndpoint> _logger;
|
|
||||||
private readonly IJsonSerializer _json;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ServerEndpoint"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
|
||||||
/// <param name="json">Instance of the <see cref="IJsonSerializer"/> interface.</param>
|
|
||||||
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
|
||||||
public ServerEndpoint(ILoggerFactory loggerFactory, IJsonSerializer json, IHttpClient httpClient)
|
|
||||||
{
|
|
||||||
_logger = loggerFactory.CreateLogger<ServerEndpoint>();
|
|
||||||
_json = json;
|
|
||||||
_api = new SimklApi(json, loggerFactory.CreateLogger<SimklApi>(), httpClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.user_code).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).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 System.Threading.Tasks;
|
||||||
using Jellyfin.Plugin.Simkl.API.Exceptions;
|
using Jellyfin.Plugin.Simkl.API.Exceptions;
|
||||||
using Jellyfin.Plugin.Simkl.API.Objects;
|
using Jellyfin.Plugin.Simkl.API.Objects;
|
||||||
using Jellyfin.Plugin.Simkl.API.Responses;
|
using Jellyfin.Plugin.Simkl.API.Responses;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Serialization;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging; // using System.Threading;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.Simkl.API
|
namespace Jellyfin.Plugin.Simkl.API
|
||||||
{
|
{
|
||||||
@@ -16,9 +25,9 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
public class SimklApi
|
public class SimklApi
|
||||||
{
|
{
|
||||||
/* INTERFACES */
|
/* INTERFACES */
|
||||||
private readonly IJsonSerializer _json;
|
|
||||||
private readonly ILogger<SimklApi> _logger;
|
private readonly ILogger<SimklApi> _logger;
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||||
|
|
||||||
/* BASIC API THINGS */
|
/* BASIC API THINGS */
|
||||||
|
|
||||||
@@ -26,44 +35,43 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// Base url.
|
/// Base url.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Baseurl = @"https://api.simkl.com";
|
public const string Baseurl = @"https://api.simkl.com";
|
||||||
// public const string BASE_URL = @"http://private-9c39b-simkl.apiary-proxy.com";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Redirect uri.
|
/// Redirect uri.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api key.
|
/// Api key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Apikey = @"27dd5d6adc24aa1ad9f95ef913244cbaf6df5696036af577ed41670473dc97d0";
|
public const string Apikey = @"c721b22482097722a84a20ccc579cf9d232be85b9befe7b7805484d0ddbc6781";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Secret.
|
/// Secret.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Secret = @"d7b9feb9d48bbaa69dbabaca21ba4671acaa89198637e9e136a4d69ec97ab68b";
|
public const string Secret = @"87893fc73cdbd2e51a7c63975c6f941ac1c6155c0e20ffa76b83202dd10a507e";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SimklApi"/> class.
|
/// Initializes a new instance of the <see cref="SimklApi"/> class.
|
||||||
/// </summary>
|
/// </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="logger">Instance of the <see cref="ILogger{SimklApi}"/> interface.</param>
|
||||||
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||||
public SimklApi(IJsonSerializer json, ILogger<SimklApi> logger, IHttpClient httpClient)
|
public SimklApi(ILogger<SimklApi> logger, IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_json = json;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClient = httpClient;
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_jsonSerializerOptions = JsonDefaults.GetOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get code.
|
/// Get code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Code response.</returns>
|
/// <returns>Code response.</returns>
|
||||||
public async Task<CodeResponse> GetCode()
|
public async Task<CodeResponse?> GetCode()
|
||||||
{
|
{
|
||||||
var uri = $"/oauth/pin?client_id={Apikey}&redirect={RedirectUri}";
|
var uri = $"/oauth/pin?client_id={Apikey}&redirect={RedirectUri}";
|
||||||
return _json.DeserializeFromStream<CodeResponse>(await Get(uri).ConfigureAwait(false));
|
return await Get<CodeResponse>(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -71,10 +79,10 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userCode">User code.</param>
|
/// <param name="userCode">User code.</param>
|
||||||
/// <returns>Code status.</returns>
|
/// <returns>Code status.</returns>
|
||||||
public async Task<CodeStatusResponse> GetCodeStatus(string userCode)
|
public async Task<CodeStatusResponse?> GetCodeStatus(string userCode)
|
||||||
{
|
{
|
||||||
var uri = $"/oauth/pin/{userCode}?client_id={Apikey}";
|
var uri = $"/oauth/pin/{userCode}?client_id={Apikey}";
|
||||||
return _json.DeserializeFromStream<CodeStatusResponse>(await Get(uri).ConfigureAwait(false));
|
return await Get<CodeStatusResponse>(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -82,18 +90,18 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userToken">User token.</param>
|
/// <param name="userToken">User token.</param>
|
||||||
/// <returns>User settings.</returns>
|
/// <returns>User settings.</returns>
|
||||||
public async Task<UserSettings> GetUserSettings(string userToken)
|
public async Task<UserSettings?> GetUserSettings(string userToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _json.DeserializeFromStream<UserSettings>(await Post("/users/settings/", userToken).ConfigureAwait(false));
|
return await Post<UserSettings, object>("/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
|
// Wontfix: Custom status codes
|
||||||
// "You don't get to pick your response code" - Luke (System Architect of Emby)
|
// "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/
|
// 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
|
|||||||
/// <param name="item">Item.</param>
|
/// <param name="item">Item.</param>
|
||||||
/// <param name="userToken">User token.</param>
|
/// <param name="userToken">User token.</param>
|
||||||
/// <returns>Status.</returns>
|
/// <returns>Status.</returns>
|
||||||
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 history = CreateHistoryFromItem(item);
|
||||||
var r = await SyncHistoryAsync(history, userToken).ConfigureAwait(false);
|
var r = await SyncHistoryAsync(history, userToken);
|
||||||
_logger.LogDebug("Response: " + _json.SerializeToString(r));
|
_logger.LogDebug("Response: {@Response}", r);
|
||||||
if (history.movies.Count == r.Added.Movies && history.shows.Count == r.Added.Shows)
|
if (r != null && history.Movies.Count == r.Added.Movies && history.Shows.Count == r.Added.Shows)
|
||||||
{
|
{
|
||||||
return (true, item);
|
return (true, item);
|
||||||
}
|
}
|
||||||
@@ -117,19 +125,19 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
// let's try scrobbling from full path
|
// let's try scrobbling from full path
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(history, item) = await GetHistoryFromFileName(item).ConfigureAwait(false);
|
(history, item) = await GetHistoryFromFileName(item);
|
||||||
}
|
}
|
||||||
catch (InvalidDataException)
|
catch (InvalidDataException)
|
||||||
{
|
{
|
||||||
// Let's try again but this time using only the FILE name
|
// Let's try again but this time using only the FILE name
|
||||||
_logger.LogDebug("Couldn't scrobble using full path, trying using only filename");
|
_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);
|
r = await SyncHistoryAsync(history, userToken);
|
||||||
_logger.LogDebug("Response: " + _json.SerializeToString(r));
|
return r == null
|
||||||
|
? (false, item)
|
||||||
return (history.movies.Count == r.Added.Movies && history.shows.Count == r.Added.Shows, item);
|
: (history.Movies.Count == r.Added.Movies && history.Shows.Count == r.Added.Shows, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -137,14 +145,11 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filename">Filename.</param>
|
/// <param name="filename">Filename.</param>
|
||||||
/// <returns>Search file response.</returns>
|
/// <returns>Search file response.</returns>
|
||||||
private async Task<SearchFileResponse> GetFromFile(string filename)
|
private async Task<SearchFileResponse?> GetFromFile(string filename)
|
||||||
{
|
{
|
||||||
var f = new SimklFile { file = filename };
|
var f = new SimklFile { File = filename };
|
||||||
_logger.LogInformation("Posting: " + _json.SerializeToString(f));
|
_logger.LogInformation("Posting: {@File}", f);
|
||||||
using var r = new StreamReader(await Post("/search/file/", null, f).ConfigureAwait(false));
|
return await Post<SearchFileResponse, SimklFile>("/search/file/", null, f);
|
||||||
var t = await r.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
_logger.LogDebug("Response: " + t);
|
|
||||||
return _json.DeserializeFromString<SearchFileResponse>(t);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -156,68 +161,69 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
private async Task<(SimklHistory history, BaseItemDto item)> GetHistoryFromFileName(BaseItemDto item, bool fullpath = true)
|
private async Task<(SimklHistory history, BaseItemDto item)> GetHistoryFromFileName(BaseItemDto item, bool fullpath = true)
|
||||||
{
|
{
|
||||||
var fname = fullpath ? item.Path : Path.GetFileName(item.Path);
|
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();
|
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 + ")");
|
throw new InvalidDataException("type != movie (" + mo.Type + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Name = mo.Movie.title;
|
item.Name = mo.Movie.Title;
|
||||||
item.ProductionYear = mo.Movie.year;
|
item.ProductionYear = mo.Movie.Year;
|
||||||
history.movies.Add(mo.Movie);
|
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 + ")");
|
throw new InvalidDataException("type != episode (" + mo.Type + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Name = mo.Episode.title;
|
item.Name = mo.Episode.Title;
|
||||||
item.SeriesName = mo.Show.title;
|
item.SeriesName = mo.Show.Title;
|
||||||
item.IndexNumber = mo.Episode.episode;
|
item.IndexNumber = mo.Episode.Episode;
|
||||||
item.ParentIndexNumber = mo.Episode.season;
|
item.ParentIndexNumber = mo.Episode.Season;
|
||||||
item.ProductionYear = mo.Show.year;
|
item.ProductionYear = mo.Show.Year;
|
||||||
history.episodes.Add(mo.Episode);
|
history.Episodes.Add(mo.Episode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (history, item);
|
return (history, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpRequestOptions GetOptions(string userToken = null)
|
private static HttpRequestMessage GetOptions(string? userToken = null)
|
||||||
{
|
{
|
||||||
var options = new HttpRequestOptions
|
var requestMessage = new HttpRequestMessage();
|
||||||
{
|
requestMessage.Headers.TryAddWithoutValidation("simkl-api-key", Apikey);
|
||||||
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))
|
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)
|
private static SimklHistory CreateHistoryFromItem(BaseItemDto item)
|
||||||
{
|
{
|
||||||
var history = new SimklHistory();
|
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)
|
// TODO: TV Shows scrobbling (WIP)
|
||||||
history.shows.Add(new SimklShow(item));
|
history.Shows.Add(new SimklShow(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
return history;
|
return history;
|
||||||
@@ -229,17 +235,17 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// <param name="history">History object.</param>
|
/// <param name="history">History object.</param>
|
||||||
/// <param name="userToken">User token.</param>
|
/// <param name="userToken">User token.</param>
|
||||||
/// <returns>The sync history response.</returns>
|
/// <returns>The sync history response.</returns>
|
||||||
private async Task<SyncHistoryResponse> SyncHistoryAsync(SimklHistory history, string userToken)
|
private async Task<SyncHistoryResponse?> SyncHistoryAsync(SimklHistory history, string userToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Syncing History: " + _json.SerializeToString(history));
|
_logger.LogInformation("Syncing History");
|
||||||
return _json.DeserializeFromStream<SyncHistoryResponse>(await Post("/sync/history", userToken, history).ConfigureAwait(false));
|
return await Post<SyncHistoryResponse, SimklHistory>("/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");
|
_logger.LogError(e, "Invalid user token {UserToken}, deleting", userToken);
|
||||||
SimklPlugin.Instance.Configuration.DeleteUserToken(userToken);
|
SimklPlugin.Instance?.Configuration.DeleteUserToken(userToken);
|
||||||
throw new InvalidTokenException("Invalid user token " + userToken);
|
throw new InvalidTokenException("Invalid user token " + userToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,13 +256,15 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// <param name="url">Relative url.</param>
|
/// <param name="url">Relative url.</param>
|
||||||
/// <param name="userToken">Authentication token.</param>
|
/// <param name="userToken">Authentication token.</param>
|
||||||
/// <returns>HTTP(s) Stream to be used.</returns>
|
/// <returns>HTTP(s) Stream to be used.</returns>
|
||||||
private async Task<Stream> Get(string url, string userToken = null)
|
private async Task<T?> Get<T>(string url, string? userToken = null)
|
||||||
{
|
{
|
||||||
// Todo: If string is not null neither empty
|
// Todo: If string is not null neither empty
|
||||||
var options = GetOptions(userToken);
|
using var options = GetOptions(userToken);
|
||||||
options.Url = Baseurl + url;
|
options.RequestUri = new Uri(Baseurl + url);
|
||||||
|
options.Method = HttpMethod.Get;
|
||||||
return await _httpClient.Get(options).ConfigureAwait(false);
|
var responseMessage = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
|
.SendAsync(options);
|
||||||
|
return await responseMessage.Content.ReadFromJsonAsync<T>(_jsonSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -265,16 +273,23 @@ namespace Jellyfin.Plugin.Simkl.API
|
|||||||
/// <param name="url">Relative post url.</param>
|
/// <param name="url">Relative post url.</param>
|
||||||
/// <param name="userToken">Authentication token.</param>
|
/// <param name="userToken">Authentication token.</param>
|
||||||
/// <param name="data">Object to serialize.</param>
|
/// <param name="data">Object to serialize.</param>
|
||||||
private async Task<Stream> Post(string url, string userToken = null, object data = null)
|
private async Task<T1?> Post<T1, T2>(string url, string? userToken = null, T2? data = null)
|
||||||
|
where T2 : class
|
||||||
{
|
{
|
||||||
var options = GetOptions(userToken);
|
using var options = GetOptions(userToken);
|
||||||
options.Url = Baseurl + url;
|
options.RequestUri = new Uri(Baseurl + url);
|
||||||
|
options.Method = HttpMethod.Post;
|
||||||
if (data != null)
|
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<T1>(_jsonSerializerOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ namespace Jellyfin.Plugin.Simkl.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The user id.</param>
|
/// <param name="id">The user id.</param>
|
||||||
/// <returns>Stored user config.</returns>
|
/// <returns>Stored user config.</returns>
|
||||||
public UserConfig GetByGuid(Guid id)
|
public UserConfig? GetByGuid(Guid id)
|
||||||
{
|
{
|
||||||
return UserConfigs.FirstOrDefault(c => c.Id == id);
|
return UserConfigs.FirstOrDefault(c => c.Id == id);
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ namespace Jellyfin.Plugin.Simkl.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SimklPlugin.Instance.SaveConfiguration();
|
SimklPlugin.Instance?.SaveConfiguration();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,52 +4,61 @@
|
|||||||
<title>Simkl's TV Tracker</title>
|
<title>Simkl's TV Tracker</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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="page" class="page type-interior pluginConfigurationPage" id="SimklConfigurationPage"
|
||||||
|
data-require="emby-button,emby-checkbox,emby-input,emby-select">
|
||||||
<div data-role="content">
|
<div data-role="content">
|
||||||
<div class="content-primary">
|
<div class="content-primary">
|
||||||
<h1>Simkl's TV Tracker</h1>
|
<h1>Simkl's TV Tracker</h1>
|
||||||
<form id="SimklConfigurationForm">
|
<form id="SimklConfigurationForm">
|
||||||
<div id="selectContainer">
|
<div id="selectContainer">
|
||||||
<select onchange="SimklConfig.onSelectorChange();" is="emby-select" id="user-selector" label="Showing plugin settings for...">
|
<select onchange="SimklConfig.onSelectorChange();" is="emby-select" id="user-selector"
|
||||||
|
label="Showing plugin settings for...">
|
||||||
<!-- This will be populated by SimklConfig.populateUsers -->
|
<!-- This will be populated by SimklConfig.populateUsers -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="loginButtonContainer" hidden>
|
<div id="loginButtonContainer" hidden>
|
||||||
<h3>It seems you are not logged in, do you wish to log in?</h3>
|
<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="SimklConfig.startLoginProcess();" is="emby-button" type="button"
|
||||||
<button onclick="location.href='https://simkl.com/';" is="emby-button" type="button" class="raised block"><span>Create an account</span></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>
|
||||||
<div id="loggingIn" hidden>
|
<div id="loggingIn" hidden>
|
||||||
<h2>Logging In</h2>
|
<h2>Logging In</h2>
|
||||||
<div id="loginText"></div>
|
<div id="loginText"></div>
|
||||||
<h3 id="loginPin"></h3>
|
<h3 id="loginPin"></h3>
|
||||||
<span id="loginSecondsRemaining">900</span> seconds remaining
|
<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>
|
<button onclick="SimklConfig.stopLoginProcess();" is="emby-button" type="button"
|
||||||
|
class="raised button-cancel block"><span>Cancel</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="configOptionsContainer" hidden>
|
<div id="configOptionsContainer" hidden>
|
||||||
<h3>Hello again <span id="simklName">USERNAME</span>!</h3>
|
<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>
|
<button onclick="SimklConfig.logOut();" is="emby-button" type="button" class="raised button block">
|
||||||
|
<span>Log Out</span></button>
|
||||||
<h2>Scrobbling options:</h2>
|
<h2>Scrobbling options:</h2>
|
||||||
<div class="checkboxcontainer">
|
<div class="checkboxcontainer">
|
||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" id="ScrobbleMovies" />
|
<input is="emby-checkbox" type="checkbox" id="ScrobbleMovies"/>
|
||||||
<span>Autoscrobbling Movies</span>
|
<span>Autoscrobbling Movies</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkboxcontainer">
|
<div class="checkboxcontainer">
|
||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" id="ScrobbleShows" />
|
<input is="emby-checkbox" type="checkbox" id="ScrobbleShows"/>
|
||||||
<span>Autoscrobbling TV Shows</span>
|
<span>Autoscrobbling TV Shows</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<input is="emby-input" id="ScrobblePercentage" type="number" min="0" max="100" pattern="[0-9]*" label="Scrobbling percentage:" />
|
<input is="emby-input" id="ScrobblePercentage" type="number" min="0" max="100" pattern="[0-9]*"
|
||||||
|
label="Scrobbling percentage:"/>
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
Percentage watched needed to scrobble
|
Percentage watched needed to scrobble
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block"><span>${ButtonSave}</span></button>
|
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span>
|
||||||
<button is="emby-button" type="button" class="raised block" onclick="history.back();"><span>${Cancel}</span></button>
|
</button>
|
||||||
|
<button is="emby-button" type="button" class="raised block" onclick="history.back();"><span>${Cancel}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,42 +66,57 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var SimklConfig = {
|
var SimklConfig = {
|
||||||
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB",
|
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB",
|
||||||
onLoginProccess: false,
|
onLoginProcess: false,
|
||||||
configCache: [],
|
configCache: [],
|
||||||
loginTimer: null,
|
loginTimer: null,
|
||||||
remainingTimer: null,
|
remainingTimer: null,
|
||||||
finish: null,
|
finish: null,
|
||||||
populateUsers : async function (users) {
|
|
||||||
users.forEach(function(user) {
|
userSelector: document.querySelector('#user-selector'),
|
||||||
$("#user-selector").append(new Option(user.Name, user.Id));
|
loginButtonContainer: document.querySelector('#loginButtonContainer'),
|
||||||
|
configOptionsContainer: document.querySelector('#configOptionsContainer'),
|
||||||
|
simklName: document.querySelector('#simklName'),
|
||||||
|
loginSecondsRemaining: document.querySelector('#loginSecondsRemaining'),
|
||||||
|
loginText: document.querySelector('#loginText'),
|
||||||
|
loginPin: document.querySelector('#loginPin'),
|
||||||
|
loggingIn: document.querySelector('#loggingIn'),
|
||||||
|
|
||||||
|
populateUsers: async function (users) {
|
||||||
|
users.forEach(function (user) {
|
||||||
|
SimklConfig.userSelector.append(new Option(user.Name, user.Id));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadConfig : async function(user, config) {
|
loadConfig: async function (user, config) {
|
||||||
if (config != null) this.configCache = config;
|
if (config != null) {
|
||||||
else config = this.configCache;
|
this.configCache = config;
|
||||||
|
} else {
|
||||||
|
config = this.configCache;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Simkl: Loading config for user " + user);
|
console.log("Simkl: Loading config for user " + user);
|
||||||
console.log(config);
|
console.log(config);
|
||||||
|
|
||||||
$("#loginButtonContainer").hide();
|
SimklConfig.loginButtonContainer.setAttribute('hidden', '');
|
||||||
$("#configOptionsContainer").hide();
|
SimklConfig.configOptionsContainer.setAttribute('hidden', '')
|
||||||
|
|
||||||
if (config.UserConfigs.some(e => e.Id === user && e.UserToken != null && e.UserToken !== "")) {
|
if (config.UserConfigs.some(e => e.Id === user && e.UserToken != null && e.UserToken !== "")) {
|
||||||
$("#configOptionsContainer").show();
|
SimklConfig.configOptionsContainer.removeAttribute('hidden');
|
||||||
this.populateOptionsContainer(config.UserConfigs.filter(e => e.Id === user)[0]);
|
await this.populateOptionsContainer(config.UserConfigs.filter(e => e.Id === user)[0]);
|
||||||
} else {
|
} else {
|
||||||
$("#loginButtonContainer").show();
|
SimklConfig.loginButtonContainer.removeAttribute('hidden');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveConfig : async function(guid) {
|
saveConfig: async function (guid) {
|
||||||
var uconfig = this.configCache.UserConfigs.filter(e => e.Id === guid)[0];
|
const uconfig = this.configCache.UserConfigs.filter(e => e.Id === guid)[0];
|
||||||
|
|
||||||
for (var key in uconfig) {
|
for (const key in uconfig) {
|
||||||
var element = $("#configOptionsContainer #"+key);
|
const element = document.querySelector("#configOptionsContainer #" + key);
|
||||||
if (element.is(":checkbox")) {
|
if (element.type === 'checkbox') {
|
||||||
uconfig[key] = element.is(':checked');
|
uconfig[key] = element.checked;
|
||||||
} else {
|
} else {
|
||||||
if (element.val() != null) uconfig[key] = element.val();
|
if (element.value != null) {
|
||||||
|
uconfig[key] = element.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,51 +124,59 @@
|
|||||||
console.log(this.configCache);
|
console.log(this.configCache);
|
||||||
ApiClient.updatePluginConfiguration(this.guid, this.configCache).then(Dashboard.processPluginConfigurationUpdateResult);
|
ApiClient.updatePluginConfiguration(this.guid, this.configCache).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||||
},
|
},
|
||||||
populateOptionsContainer : async function(userConfig) {
|
populateOptionsContainer: async function (userConfig) {
|
||||||
$("#simklName").html(SimklAPI.getUserSettings(userConfig.Id).user.name);
|
SimklConfig.simklName.innerText = (await SimklAPI.getUserSettings(userConfig.Id)).user.name;
|
||||||
|
|
||||||
for (var key in userConfig) {
|
for (const key in userConfig) {
|
||||||
console.log(key, userConfig[key]);
|
console.log(key, userConfig[key]);
|
||||||
$("#configOptionsContainer input[type=checkbox]#"+key).attr("checked", userConfig[key]);
|
const chk = document.querySelector("#configOptionsContainer input[type=checkbox]#" + key);
|
||||||
$("#configOptionsContainer input[type=number]#"+key).val(userConfig[key]);
|
if (chk) {
|
||||||
|
chk.checked = userConfig[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = document.querySelector("#configOptionsContainer input[type=number]#" + key);
|
||||||
|
if (input) {
|
||||||
|
input.value = userConfig[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startLoginProcess : async function () {
|
startLoginProcess: async function () {
|
||||||
this.onLoginProcess = true;
|
this.onLoginProcess = true;
|
||||||
|
|
||||||
var code = SimklAPI.getCode();
|
const code = await SimklAPI.getCode();
|
||||||
|
console.log(code);
|
||||||
this.finish = new Date();
|
this.finish = new Date();
|
||||||
this.finish.setSeconds(this.finish.getSeconds() + code.expires_in);
|
this.finish.setSeconds(this.finish.getSeconds() + code.expires_in);
|
||||||
this.nextInterval = new Date();
|
this.nextInterval = new Date();
|
||||||
|
|
||||||
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this,code), code.Interval*1000);
|
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this, code), code.Interval * 1000);
|
||||||
this.remainingTimer = window.setInterval(function() {
|
this.remainingTimer = window.setInterval(function () {
|
||||||
$("#loginSecondsRemaining").html(Math.round((SimklConfig.finish.getTime() - (new Date().getTime()))/1000));
|
SimklConfig.loginSecondsRemaining.innerText = Math.round((SimklConfig.finish.getTime() - (new Date().getTime())) / 1000);
|
||||||
} ,1000);
|
}, 1000);
|
||||||
|
|
||||||
$("#loginText").html("Please visit <a href='" + code.verification_url + "/" + code.user_code +
|
SimklConfig.loginText.innerHTML = 'Please visit <a href="' + code.verification_url + '/' + code.user_code + '" target="_blank">' + code.verification_url + '</a> on your phone or compouter and enter the following code:';
|
||||||
"' target='_blank'>" + code.verification_url + "</a> on your phone or computer and enter the following code:");
|
|
||||||
$("#loginPin").html(code.user_code);
|
|
||||||
|
|
||||||
$("#loginButtonContainer").hide();
|
SimklConfig.loginPin.innerText = code.user_code;
|
||||||
await $("#loggingIn").show();
|
|
||||||
|
SimklConfig.loginButtonContainer.setAttribute('hidden', '');
|
||||||
|
SimklConfig.loggingIn.removeAttribute('hidden');
|
||||||
},
|
},
|
||||||
checkLoginProcess : function (code) {
|
checkLoginProcess: async function (code) {
|
||||||
var response = SimklAPI.checkCode(code.user_code);
|
const response = await SimklAPI.checkCode(code.user_code);
|
||||||
console.log("Response:");
|
console.log("Response:");
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
|
||||||
if (new Date() > this.finish) {
|
if (new Date() > this.finish) {
|
||||||
Dashboard.alert("Timed out!");
|
Dashboard.alert("Timed out!");
|
||||||
this.stopLoginProcess();
|
await this.stopLoginProcess();
|
||||||
} else if (response.Result === "KO") {
|
} else if (response.Result === "KO") {
|
||||||
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this,code), code.Interval*1000);
|
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this, code), code.Interval * 1000);
|
||||||
} else if (response.Result === "OK") {
|
} else if (response.Result === "OK") {
|
||||||
this.stopLoginProcess();
|
await this.stopLoginProcess();
|
||||||
|
|
||||||
// Save key on settings
|
// Save key on settings
|
||||||
var uguid = $("#user-selector").val();
|
const uguid = SimklConfig.userSelector.value;
|
||||||
var filter = this.configCache.UserConfigs.filter(function(c) {
|
const filter = this.configCache.UserConfigs.filter(function (c) {
|
||||||
return c.Id === uguid;
|
return c.Id === uguid;
|
||||||
});
|
});
|
||||||
if (filter.length > 0) {
|
if (filter.length > 0) {
|
||||||
@@ -159,26 +191,29 @@
|
|||||||
console.log(this.configCache);
|
console.log(this.configCache);
|
||||||
|
|
||||||
ApiClient.updatePluginConfiguration(this.guid, this.configCache);
|
ApiClient.updatePluginConfiguration(this.guid, this.configCache);
|
||||||
this.loadConfig(uguid);
|
await this.loadConfig(uguid);
|
||||||
} else {
|
} else {
|
||||||
Dashboard.alert("Error logging in");
|
Dashboard.alert("Error logging in");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stopLoginProcess : async function () {
|
stopLoginProcess: async function () {
|
||||||
this.onLoginProcess = false;
|
this.onLoginProcess = false;
|
||||||
window.clearTimeout(this.loginTimer);
|
window.clearTimeout(this.loginTimer);
|
||||||
window.clearInterval(this.remainingTimer);
|
window.clearInterval(this.remainingTimer);
|
||||||
$("#loginButtonContainer").show();
|
SimklConfig.loginButtonContainer.removeAttribute('hidden');
|
||||||
$("#loggingIn").hide();
|
SimklConfig.loggingIn.setAttribute('hidden', '');
|
||||||
},
|
},
|
||||||
onSelectorChange : async function () {
|
onSelectorChange: async function () {
|
||||||
if (this.onLoginProcess) this.stopLoginProcess();
|
if (this.onLoginProcess) {
|
||||||
this.loadConfig($("#user-selector").val(), null);
|
await this.stopLoginProcess();
|
||||||
|
}
|
||||||
|
await this.loadConfig(SimklConfig.userSelector.value, null);
|
||||||
},
|
},
|
||||||
logOut : function(uguid) {
|
logOut: function (uguid) {
|
||||||
if (uguid == null) uguid = $("#user-selector").val();
|
if (uguid == null) {
|
||||||
|
uguid = SimklConfig.userSelector.value;
|
||||||
|
}
|
||||||
|
|
||||||
// e1f34a57cb3c4767ab6f29cc5c7c0566
|
|
||||||
var filter = this.configCache.UserConfigs.filter(function (c) {
|
var filter = this.configCache.UserConfigs.filter(function (c) {
|
||||||
return c.Id === uguid;
|
return c.Id === uguid;
|
||||||
});
|
});
|
||||||
@@ -191,69 +226,86 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(this.configCache);
|
console.log(this.configCache);
|
||||||
ApiClient.updatePluginConfiguration(this.guid, this.configCache);
|
window.ApiClient.updatePluginConfiguration(this.guid, this.configCache);
|
||||||
this.loadConfig(uguid);
|
this.loadConfig(uguid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var SimklAPI = {
|
var SimklAPI = {
|
||||||
getCode: function () {
|
getCode: function () {
|
||||||
var uri = "/Simkl/oauth/pin";
|
const request = {
|
||||||
var request = new XMLHttpRequest();
|
url: window.ApiClient.getUrl('Simkl/oauth/pin'),
|
||||||
request.open("GET", uri, false);
|
type: 'GET',
|
||||||
request.send();
|
headers: {
|
||||||
|
accept: 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.status === 200) {
|
return window.ApiClient.fetch(request)
|
||||||
return $.parseJSON(request.response);
|
.then(function (result) {
|
||||||
} else {
|
return result;
|
||||||
console.log(request);
|
})
|
||||||
|
.catch(function (result) {
|
||||||
|
console.log(result);
|
||||||
Dashboard.alert("Some error occurred, see browser log for more details");
|
Dashboard.alert("Some error occurred, see browser log for more details");
|
||||||
SimklConfig.stopLoginProcess();
|
SimklConfig.stopLoginProcess();
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
checkCode : function (user_code) {
|
checkCode: function (user_code) {
|
||||||
var uri = "/Simkl/oauth/pin/" + user_code;
|
const request = {
|
||||||
var request = new XMLHttpRequest();
|
url: window.ApiClient.getUrl('Simkl/oauth/pin/' + user_code),
|
||||||
request.open("GET", uri, false);
|
type: 'GET',
|
||||||
request.send();
|
headers: {
|
||||||
|
accept: 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.status === 200) {
|
return window.ApiClient.fetch(request)
|
||||||
return $.parseJSON(request.response);
|
.then(function (result) {
|
||||||
} else {
|
return result;
|
||||||
console.log(request);
|
})
|
||||||
|
.catch(function (result) {
|
||||||
|
console.log(result);
|
||||||
Dashboard.alert("Some error occurred, see browser log for more details");
|
Dashboard.alert("Some error occurred, see browser log for more details");
|
||||||
SimklConfig.stopLoginProcess();
|
SimklConfig.stopLoginProcess();
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
getUserSettings: function (secret) {
|
getUserSettings: function (secret) {
|
||||||
var uri = "/Simkl/users/settings/" + secret;
|
const request = {
|
||||||
var request = new XMLHttpRequest();
|
url: window.ApiClient.getUrl('Simkl/users/settings/' + secret),
|
||||||
request.open("GET", uri, false);
|
type: 'GET',
|
||||||
request.send();
|
headers: {
|
||||||
|
accept: 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.status === 200) {
|
return window.ApiClient.fetch(request)
|
||||||
return $.parseJSON(request.response);
|
.then(function (result) {
|
||||||
} else {
|
return result;
|
||||||
console.log(request);
|
})
|
||||||
|
.catch(function (result) {
|
||||||
|
console.log(result);
|
||||||
Dashboard.alert("Something went wrong, see logs for more details");
|
Dashboard.alert("Something went wrong, see logs for more details");
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#SimklConfigurationPage").on("pageshow", async function(e) {
|
document.querySelector('#SimklConfigurationPage')
|
||||||
|
.addEventListener('pageshow', async function () {
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ApiClient.getUsers().then(SimklConfig.populateUsers),
|
window.ApiClient.getUsers().then(SimklConfig.populateUsers),
|
||||||
ApiClient.getPluginConfiguration(SimklConfig.guid).then(SimklConfig.loadConfig.bind(SimklConfig, ApiClient.getCurrentUserId()))]);
|
window.ApiClient.getPluginConfiguration(SimklConfig.guid).then(SimklConfig.loadConfig.bind(SimklConfig, ApiClient.getCurrentUserId()))]);
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#SimklConfigurationForm").on("submit", function(e) {
|
document.querySelector('#SimklConfigurationForm')
|
||||||
Dashboard.showLoadingMsg();
|
.addEventListener('submit', function (e) {
|
||||||
SimklConfig.saveConfig($("#user-selector").val());
|
e.preventDefault();
|
||||||
Dashboard.hideLoadingMsg();
|
|
||||||
|
|
||||||
return false;
|
Dashboard.showLoadingMsg();
|
||||||
|
SimklConfig.saveConfig(SimklConfig.userSelector.value);
|
||||||
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<NoWarn>CA1707;CA1819;SA1300</NoWarn>
|
<nullable>enable</nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.6-*" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.7-*" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
@@ -29,4 +30,8 @@
|
|||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Jellyfin.Plugin.Simkl.API;
|
||||||
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Simkl
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class PluginServiceRegistrator : IPluginServiceRegistrator
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void RegisterServices(IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
serviceCollection.AddScoped<SimklApi>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Playback progress scrobbler.
|
||||||
|
/// </summary>
|
||||||
|
public class PlaybackScrobbler : IServerEntryPoint
|
||||||
|
{
|
||||||
|
private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions
|
||||||
|
private readonly ILogger<PlaybackScrobbler> _logger;
|
||||||
|
private readonly Dictionary<string, Guid> _lastScrobbled; // Library ID of last scrobbled item
|
||||||
|
private readonly SimklApi _simklApi;
|
||||||
|
private DateTime _nextTry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PlaybackScrobbler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{Scrobbler}"/> interface.</param>
|
||||||
|
/// <param name="simklApi">Instance of the <see cref="SimklApi"/>.</param>
|
||||||
|
public PlaybackScrobbler(
|
||||||
|
ISessionManager sessionManager,
|
||||||
|
ILogger<PlaybackScrobbler> logger,
|
||||||
|
SimklApi simklApi)
|
||||||
|
{
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
_logger = logger;
|
||||||
|
_simklApi = simklApi;
|
||||||
|
_lastScrobbled = new Dictionary<string, Guid>();
|
||||||
|
_nextTry = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task RunAsync()
|
||||||
|
{
|
||||||
|
_sessionManager.PlaybackProgress += OnPlaybackProgress;
|
||||||
|
_sessionManager.PlaybackStopped += OnPlaybackStopped;
|
||||||
|
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;
|
||||||
|
_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
{
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,13 +27,13 @@ namespace Jellyfin.Plugin.Simkl
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current instance of the plugin.
|
/// Gets the current instance of the plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static SimklPlugin Instance { get; private set; }
|
public static SimklPlugin? Instance { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Guid Id => new Guid("07CAEF58-A94B-4211-A62C-F9774E04EBDB");
|
public override Guid Id => new Guid("07CAEF58-A94B-4211-A62C-F9774E04EBDB");
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Name => "Simkl TV Tracker";
|
public override string Name => "Simkl";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Description => "Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!";
|
public override string Description => "Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!";
|
||||||
|
|||||||
@@ -5,25 +5,11 @@
|
|||||||
Repository Url:
|
Repository Url:
|
||||||
https://repo.codyrobibero.dev/manifest.json
|
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
|
## Current features
|
||||||
- Multi-user support
|
- Multi-user support
|
||||||
- Auto scrobble Movies and TV Shows at given percentage to Simkl
|
- 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
|
- 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/
|
## Future features
|
||||||
|
- Sync all watch status with Simkl
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
name: "Simkl"
|
name: "Simkl"
|
||||||
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB"
|
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB"
|
||||||
version: "1.0.0.0"
|
version: "1.0.0.0"
|
||||||
targetAbi: "10.6.0.0"
|
targetAbi: "10.7.0.0"
|
||||||
owner: "crobibero"
|
owner: "crobibero"
|
||||||
overview: "Scrobble to Simkl"
|
overview: "Scrobble to Simkl"
|
||||||
description: >
|
description: >
|
||||||
|
|||||||
Reference in New Issue
Block a user