Initial commit

This commit is contained in:
Alexander Rufenach
2021-06-01 18:23:53 -04:00
parent a0414b53be
commit 6b42bc8d73
31 changed files with 6 additions and 1799 deletions
-16
View File
@@ -1,16 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Simkl", "Jellyfin.Plugin.Simkl\Jellyfin.Plugin.Simkl.csproj", "{2C1895EC-A7E1-48B6-A2E0-3E847D1FB748}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2C1895EC-A7E1-48B6-A2E0-3E847D1FB748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2C1895EC-A7E1-48B6-A2E0-3E847D1FB748}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C1895EC-A7E1-48B6-A2E0-3E847D1FB748}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C1895EC-A7E1-48B6-A2E0-3E847D1FB748}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
-67
View File
@@ -1,67 +0,0 @@
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);
}
}
}
@@ -1,34 +0,0 @@
using System;
namespace Jellyfin.Plugin.Simkl.API.Exceptions
{
/// <inheritdoc />
public class InvalidTokenException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
/// </summary>
public InvalidTokenException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
/// </summary>
/// <param name="msg">The message.</param>
public InvalidTokenException(string msg)
: base(msg)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidTokenException"/> class.
/// </summary>
/// <param name="msg">The message.</param>
/// <param name="inner">The inner exception.</param>
public InvalidTokenException(string msg, Exception inner)
: base(msg, inner)
{
}
}
}
@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Season.
/// </summary>
public class Season
{
/// <summary>
/// Gets or sets the season number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
/// <summary>
/// Gets or sets the episodes.
/// </summary>
[JsonPropertyName("episodes")]
public IReadOnlyList<ShowEpisode> Episodes { get; set; } = Array.Empty<ShowEpisode>();
}
}
@@ -1,17 +0,0 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Show episode.
/// </summary>
public class ShowEpisode
{
/// <summary>
/// Gets or sets episode number.
/// </summary>
[JsonPropertyName("number")]
public int? Number { get; set; }
// TODO: watched_at
}
}
@@ -1,42 +0,0 @@
using System;
using System.Text.Json.Serialization;
#pragma warning disable SA1300
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl episode container.
/// </summary>
public class SimklEpisode : SimklMediaObject
{
/// <summary>
/// Gets or sets watched at.
/// </summary>
[JsonPropertyName("watched_at")]
public DateTime? WatchedAt { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Gets or sets the season.
/// </summary>
[JsonPropertyName("season")]
public int? Season { get; set; }
/// <summary>
/// Gets or sets the episode.
/// </summary>
[JsonPropertyName("episode")]
public int? Episode { get; set; }
/// <summary>
/// Gets or sets multipart.
/// </summary>
[JsonPropertyName("multipart")]
public bool? Multipart { get; set; }
}
}
@@ -1,28 +0,0 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl File.
/// </summary>
public class SimklFile
{
/// <summary>
/// Gets or sets the file.
/// </summary>
[JsonPropertyName("file")]
public string? File { get; set; }
/// <summary>
/// Gets or sets the part.
/// </summary>
[JsonPropertyName("part")]
public int? Part { get; set; }
/// <summary>
/// Gets or sets the hash.
/// </summary>
[JsonPropertyName("hash")]
public string? Hash { get; set; }
}
}
@@ -1,41 +0,0 @@
#pragma warning disable CA2227
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl history container.
/// </summary>
public class SimklHistory
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklHistory"/> class.
/// </summary>
public SimklHistory()
{
Movies = new List<SimklMovie>();
Shows = new List<SimklShow>();
Episodes = new List<SimklEpisode>();
}
/// <summary>
/// Gets or sets list of movies.
/// </summary>
[JsonPropertyName("movies")]
public List<SimklMovie> Movies { get; set; }
/// <summary>
/// Gets or sets the list of shows.
/// </summary>
[JsonPropertyName("shows")]
public List<SimklShow> Shows { get; set; }
/// <summary>
/// Gets or sets the list of episodes.
/// </summary>
[JsonPropertyName("episodes")]
public List<SimklEpisode> Episodes { get; set; }
}
}
@@ -1,74 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl Ids.
/// </summary>
public class SimklIds
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklIds"/> class.
/// </summary>
/// <param name="providerIds">The provider ids.</param>
public SimklIds(Dictionary<string, string> providerIds)
{
foreach (var (key, value) in providerIds)
{
if (key.Equals(nameof(Simkl), StringComparison.OrdinalIgnoreCase))
{
Simkl = Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
else if (key.Equals(nameof(Imdb), StringComparison.OrdinalIgnoreCase))
{
Imdb = value;
}
else if (key.Equals(nameof(Slug), StringComparison.OrdinalIgnoreCase))
{
Slug = value;
}
else if (key.Equals(nameof(Netflix), StringComparison.OrdinalIgnoreCase))
{
Netflix = value;
}
else if (key.Equals(nameof(Tmdb), StringComparison.OrdinalIgnoreCase))
{
Tmdb = value;
}
}
}
/// <summary>
/// Gets or sets the simkl id.
/// </summary>
[JsonPropertyName("simkl")]
public int? Simkl { get; set; }
/// <summary>
/// Gets or sets the imdb id.
/// </summary>
[JsonPropertyName("imdb")]
public string? Imdb { get; set; }
/// <summary>
/// Gets or sets the slug.
/// </summary>
[JsonPropertyName("slug")]
public string? Slug { get; set; }
/// <summary>
/// Gets or sets the netflix id.
/// </summary>
[JsonPropertyName("netflix")]
public string? Netflix { get; set; }
/// <summary>
/// Gets or sets the TMDb id.
/// </summary>
[JsonPropertyName("tmdb")]
public string? Tmdb { get; set; }
}
}
@@ -1,16 +0,0 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl media object.
/// </summary>
public class SimklMediaObject
{
/// <summary>
/// Gets or sets ids.
/// </summary>
[JsonPropertyName("ids")]
public SimklIds? Ids { get; set; }
}
}
@@ -1,42 +0,0 @@
using System;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Dto;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl movie.
/// </summary>
public class SimklMovie : SimklMediaObject
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklMovie"/> class.
/// </summary>
/// <param name="item">The base item dto.</param>
public SimklMovie(BaseItemDto item)
{
Title = item.OriginalTitle;
Year = item.ProductionYear;
Ids = new SimklMovieIds(item.ProviderIds);
WatchedAt = DateTime.UtcNow;
}
/// <summary>
/// Gets or sets the movie title.
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
[JsonPropertyName("year")]
public int? Year { get; set; }
/// <summary>
/// Gets or sets watched at.
/// </summary>
[JsonPropertyName("watched_at")]
public DateTime? WatchedAt { get; set; }
}
}
@@ -1,56 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl movie ids.
/// </summary>
public class SimklMovieIds : SimklIds
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklMovieIds"/> class.
/// </summary>
/// <param name="providerMovieIds">the provider movie ids.</param>
public SimklMovieIds(Dictionary<string, string> providerMovieIds)
: base(providerMovieIds)
{
}
/// <summary>
/// Gets or sets the tvdb id.
/// </summary>
[JsonPropertyName("tvdb")]
public int? Tvdb { get; set; }
/// <summary>
/// Gets or sets the mal id.
/// </summary>
[JsonPropertyName("mal")]
public int? Mal { get; set; }
/// <summary>
/// Gets or sets the anidb id.
/// </summary>
[JsonPropertyName("anidb")]
public int? Anidb { get; set; }
/// <summary>
/// Gets or sets the hulu id.
/// </summary>
[JsonPropertyName("hulu")]
public int? Hulu { get; set; }
/// <summary>
/// Gets or sets the crunchyroll id.
/// </summary>
[JsonPropertyName("crunchyroll")]
public int? Crunchyroll { get; set; }
/// <summary>
/// Gets or sets the movie db id.
/// </summary>
[JsonPropertyName("moviedb")]
public string? Moviedb { get; set; }
}
}
@@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Dto;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl show.
/// </summary>
public class SimklShow : SimklMediaObject
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklShow"/> class.
/// </summary>
/// <param name="mediaInfo">The media info.</param>
public SimklShow(BaseItemDto mediaInfo)
{
Title = mediaInfo.SeriesName;
Ids = new SimklShowIds(mediaInfo.ProviderIds);
Year = mediaInfo.ProductionYear;
Seasons = new[]
{
new Season
{
Number = mediaInfo.ParentIndexNumber,
Episodes = new[]
{
new ShowEpisode
{
Number = mediaInfo.IndexNumber
}
}
}
};
}
/// <summary>
/// Gets or sets title.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; }
/// <summary>
/// Gets or sets year.
/// </summary>
[JsonPropertyName("year")]
public int? Year { get; set; }
/// <summary>
/// Gets or sets seasons.
/// </summary>
[JsonPropertyName("seasons")]
public IReadOnlyList<Season> Seasons { get; set; }
}
}
@@ -1,56 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// Simkl show ids.
/// </summary>
public class SimklShowIds : SimklIds
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklShowIds"/> class.
/// </summary>
/// <param name="providerMovieIds">The provider movie ids.</param>
public SimklShowIds(Dictionary<string, string> providerMovieIds)
: base(providerMovieIds)
{
}
/// <summary>
/// Gets or sets tvdb.
/// </summary>
[JsonPropertyName("tvdb")]
public int? Tvdb { get; set; }
/// <summary>
/// Gets or sets mal.
/// </summary>
[JsonPropertyName("mal")]
public int? Mal { get; set; }
/// <summary>
/// Gets or sets anidb.
/// </summary>
[JsonPropertyName("anidb")]
public int? Anidb { get; set; }
/// <summary>
/// Gets or sets hulu.
/// </summary>
[JsonPropertyName("hulu")]
public int? Hulu { get; set; }
/// <summary>
/// Gets or sets crunchyroll.
/// </summary>
[JsonPropertyName("crunchyroll")]
public int? Crunchyroll { get; set; }
/// <summary>
/// Gets or sets zap2it.
/// </summary>
[JsonPropertyName("zap2It")]
public string? Zap2It { get; set; }
}
}
-18
View File
@@ -1,18 +0,0 @@
using System.Text.Json.Serialization;
#pragma warning disable SA1300
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// User.
/// </summary>
public class User
{
/// <summary>
/// Gets or sets name.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
}
}
@@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Simkl.API.Objects
{
/// <summary>
/// User settings.
/// </summary>
public class UserSettings
{
/// <summary>
/// Gets or sets user.
/// </summary>
[JsonPropertyName("user")]
public User? User { get; set; }
/// <summary>
/// Gets or sets error.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; set; }
}
}
@@ -1,45 +0,0 @@
using System.Text.Json.Serialization;
#pragma warning disable SA1300
namespace Jellyfin.Plugin.Simkl.API.Responses
{
/// <summary>
/// Code response.
/// </summary>
public class CodeResponse
{
/// <summary>
/// Gets or sets result.
/// </summary>
public string? Result { get; set; }
/// <summary>
/// Gets or sets device code.
/// </summary>
[JsonPropertyName("device_code")]
public string? DeviceCode { get; set; }
/// <summary>
/// Gets or sets user code.
/// </summary>
[JsonPropertyName("user_code")]
public string? UserCode { get; set; }
/// <summary>
/// Gets or sets verification url.
/// </summary>
[JsonPropertyName("verification_url")]
public string? VerificationUrl { get; set; }
/// <summary>
/// Gets or sets expires in.
/// </summary>
[JsonPropertyName("expires_in")]
public int? ExpiresIn { get; set; }
/// <summary>
/// Gets or sets interval.
/// </summary>
public int? Interval { get; set; }
}
}
@@ -1,27 +0,0 @@
using System.Text.Json.Serialization;
#pragma warning disable SA1300
namespace Jellyfin.Plugin.Simkl.API.Responses
{
/// <summary>
/// Code status response.
/// </summary>
public class CodeStatusResponse
{
/// <summary>
/// Gets or sets result.
/// </summary>
public string? Result { get; set; }
/// <summary>
/// Gets or sets message.
/// </summary>
public string? Message { get; set; }
/// <summary>
/// Gets or sets access token.
/// </summary>
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
}
}
@@ -1,30 +0,0 @@
using Jellyfin.Plugin.Simkl.API.Objects;
namespace Jellyfin.Plugin.Simkl.API.Responses
{
/// <summary>
/// Search file response.
/// </summary>
public class SearchFileResponse
{
/// <summary>
/// Gets or sets type.
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Gets or sets episode.
/// </summary>
public SimklEpisode? Episode { get; set; }
/// <summary>
/// Gets or sets movie.
/// </summary>
public SimklMovie? Movie { get; set; }
/// <summary>
/// Gets or sets show.
/// </summary>
public SimklShow? Show { get; set; }
}
}
@@ -1,26 +0,0 @@
using System;
using Jellyfin.Plugin.Simkl.API.Objects;
namespace Jellyfin.Plugin.Simkl.API.Responses
{
/// <summary>
/// sync history not found.
/// </summary>
public class SyncHistoryNotFound
{
/// <summary>
/// Gets or sets movies.
/// </summary>
public SimklMovie[] Movies { get; set; } = Array.Empty<SimklMovie>();
/// <summary>
/// Gets or sets shows.
/// </summary>
public SimklShow[] Shows { get; set; } = Array.Empty<SimklShow>();
/// <summary>
/// Gets or sets episodes.
/// </summary>
public SimklEpisode[] Episodes { get; set; } = Array.Empty<SimklEpisode>();
}
}
@@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
#pragma warning disable SA1300
namespace Jellyfin.Plugin.Simkl.API.Responses
{
/// <summary>
/// Sync history response.
/// </summary>
public class SyncHistoryResponse
{
/// <summary>
/// Gets or sets added.
/// </summary>
public SyncHistoryResponseCount Added { get; set; } = new SyncHistoryResponseCount();
/// <summary>
/// Gets or sets not found.
/// </summary>
[JsonPropertyName("not_found")]
public SyncHistoryNotFound? NotFound { get; set; }
}
}
@@ -1,23 +0,0 @@
namespace Jellyfin.Plugin.Simkl.API.Responses
{
/// <summary>
/// Sync history response count.
/// </summary>
public class SyncHistoryResponseCount
{
/// <summary>
/// Gets or sets movies.
/// </summary>
public int Movies { get; set; }
/// <summary>
/// Gets or sets shows.
/// </summary>
public int Shows { get; set; }
/// <summary>
/// Gets or sets episodes.
/// </summary>
public int Episodes { get; set; }
}
}
-294
View File
@@ -1,294 +0,0 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Plugin.Simkl.API.Exceptions;
using Jellyfin.Plugin.Simkl.API.Objects;
using Jellyfin.Plugin.Simkl.API.Responses;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Simkl.API
{
/// <summary>
/// Simkl Api.
/// </summary>
public class SimklApi
{
/* INTERFACES */
private readonly ILogger<SimklApi> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly JsonSerializerOptions _jsonSerializerOptions;
/* BASIC API THINGS */
/// <summary>
/// Base url.
/// </summary>
public const string Baseurl = @"https://api.simkl.com";
/// <summary>
/// Redirect uri.
/// </summary>
public const string RedirectUri = @"https://simkl.com/apps/jellyfin/connected/";
/// <summary>
/// Api key.
/// </summary>
public const string Apikey = @"c721b22482097722a84a20ccc579cf9d232be85b9befe7b7805484d0ddbc6781";
/// <summary>
/// Secret.
/// </summary>
public const string Secret = @"87893fc73cdbd2e51a7c63975c6f941ac1c6155c0e20ffa76b83202dd10a507e";
/// <summary>
/// Initializes a new instance of the <see cref="SimklApi"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{SimklApi}"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
public SimklApi(ILogger<SimklApi> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_jsonSerializerOptions = JsonDefaults.GetOptions();
}
/// <summary>
/// Get code.
/// </summary>
/// <returns>Code response.</returns>
public async Task<CodeResponse?> GetCode()
{
var uri = $"/oauth/pin?client_id={Apikey}&redirect={RedirectUri}";
return await Get<CodeResponse>(uri);
}
/// <summary>
/// Get code status.
/// </summary>
/// <param name="userCode">User code.</param>
/// <returns>Code status.</returns>
public async Task<CodeStatusResponse?> GetCodeStatus(string userCode)
{
var uri = $"/oauth/pin/{userCode}?client_id={Apikey}";
return await Get<CodeStatusResponse>(uri);
}
/// <summary>
/// Get user settings.
/// </summary>
/// <param name="userToken">User token.</param>
/// <returns>User settings.</returns>
public async Task<UserSettings?> GetUserSettings(string userToken)
{
try
{
return await Post<UserSettings, object>("/users/settings/", userToken);
}
catch (HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
// Wontfix: Custom status codes
// "You don't get to pick your response code" - Luke (System Architect of Emby)
// https://emby.media/community/index.php?/topic/61889-wiki-issue-resultfactorythrowerror/
return new UserSettings { Error = "user_token_failed" };
}
}
/// <summary>
/// Mark as watched.
/// </summary>
/// <param name="item">Item.</param>
/// <param name="userToken">User token.</param>
/// <returns>Status.</returns>
public async Task<(bool Success, BaseItemDto Item)> MarkAsWatched(BaseItemDto item, string userToken)
{
var history = CreateHistoryFromItem(item);
var r = await SyncHistoryAsync(history, userToken);
_logger.LogDebug("Response: {@Response}", r);
if (r != null && history.Movies.Count == r.Added.Movies && history.Shows.Count == r.Added.Shows)
{
return (true, item);
}
// If we are here, is because the item has not been found
// let's try scrobbling from full path
try
{
(history, item) = await GetHistoryFromFileName(item);
}
catch (InvalidDataException)
{
// Let's try again but this time using only the FILE name
_logger.LogDebug("Couldn't scrobble using full path, trying using only filename");
(history, item) = await GetHistoryFromFileName(item, false);
}
r = await SyncHistoryAsync(history, userToken);
return r == null
? (false, item)
: (history.Movies.Count == r.Added.Movies && history.Shows.Count == r.Added.Shows, item);
}
/// <summary>
/// Get from file.
/// </summary>
/// <param name="filename">Filename.</param>
/// <returns>Search file response.</returns>
private async Task<SearchFileResponse?> GetFromFile(string filename)
{
var f = new SimklFile { File = filename };
_logger.LogInformation("Posting: {@File}", f);
return await Post<SearchFileResponse, SimklFile>("/search/file/", null, f);
}
/// <summary>
/// Get history from file name.
/// </summary>
/// <param name="item">Item.</param>
/// <param name="fullpath">Full path.</param>
/// <returns>Srobble history.</returns>
private async Task<(SimklHistory history, BaseItemDto item)> GetHistoryFromFileName(BaseItemDto item, bool fullpath = true)
{
var fname = fullpath ? item.Path : Path.GetFileName(item.Path);
var mo = await GetFromFile(fname);
if (mo == null)
{
throw new InvalidDataException("Search file response is null");
}
var history = new SimklHistory();
if (mo.Movie != null &&
(item.IsMovie == true || string.Equals(item.Type, nameof(Movie), StringComparison.OrdinalIgnoreCase)))
{
if (!string.Equals(mo.Type, "movie", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException("type != movie (" + mo.Type + ")");
}
item.Name = mo.Movie.Title;
item.ProductionYear = mo.Movie.Year;
history.Movies.Add(mo.Movie);
}
else if (mo.Episode != null
&& mo.Show != null
&& (item.IsSeries == true || string.Equals(item.Type, nameof(Episode), StringComparison.OrdinalIgnoreCase)))
{
if (!string.Equals(mo.Type, "episode", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException("type != episode (" + mo.Type + ")");
}
item.Name = mo.Episode.Title;
item.SeriesName = mo.Show.Title;
item.IndexNumber = mo.Episode.Episode;
item.ParentIndexNumber = mo.Episode.Season;
item.ProductionYear = mo.Show.Year;
history.Episodes.Add(mo.Episode);
}
return (history, item);
}
private static HttpRequestMessage GetOptions(string? userToken = null)
{
var requestMessage = new HttpRequestMessage();
requestMessage.Headers.TryAddWithoutValidation("simkl-api-key", Apikey);
if (!string.IsNullOrEmpty(userToken))
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userToken);
}
return requestMessage;
}
private static SimklHistory CreateHistoryFromItem(BaseItemDto item)
{
var history = new SimklHistory();
if (item.IsMovie == true || string.Equals(item.Type, nameof(Movie), StringComparison.OrdinalIgnoreCase))
{
history.Movies.Add(new SimklMovie(item));
}
else if (item.IsSeries == true || string.Equals(item.Type, nameof(Episode), StringComparison.OrdinalIgnoreCase))
{
// TODO: TV Shows scrobbling (WIP)
history.Shows.Add(new SimklShow(item));
}
return history;
}
/// <summary>
/// Implements /sync/history method from simkl.
/// </summary>
/// <param name="history">History object.</param>
/// <param name="userToken">User token.</param>
/// <returns>The sync history response.</returns>
private async Task<SyncHistoryResponse?> SyncHistoryAsync(SimklHistory history, string userToken)
{
try
{
_logger.LogInformation("Syncing History");
return await Post<SyncHistoryResponse, SimklHistory>("/sync/history", userToken, history);
}
catch (HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
_logger.LogError(e, "Invalid user token {UserToken}, deleting", userToken);
SimklPlugin.Instance?.Configuration.DeleteUserToken(userToken);
throw new InvalidTokenException("Invalid user token " + userToken);
}
}
/// <summary>
/// API's private get method, given RELATIVE url and headers.
/// </summary>
/// <param name="url">Relative url.</param>
/// <param name="userToken">Authentication token.</param>
/// <returns>HTTP(s) Stream to be used.</returns>
private async Task<T?> Get<T>(string url, string? userToken = null)
{
// Todo: If string is not null neither empty
using var options = GetOptions(userToken);
options.RequestUri = new Uri(Baseurl + url);
options.Method = HttpMethod.Get;
var responseMessage = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options);
return await responseMessage.Content.ReadFromJsonAsync<T>(_jsonSerializerOptions);
}
/// <summary>
/// API's private post method.
/// </summary>
/// <param name="url">Relative post url.</param>
/// <param name="userToken">Authentication token.</param>
/// <param name="data">Object to serialize.</param>
private async Task<T1?> Post<T1, T2>(string url, string? userToken = null, T2? data = null)
where T2 : class
{
using var options = GetOptions(userToken);
options.RequestUri = new Uri(Baseurl + url);
options.Method = HttpMethod.Post;
if (data != null)
{
options.Content = new StringContent(
JsonSerializer.Serialize(data, _jsonSerializerOptions),
Encoding.UTF8,
MediaTypeNames.Application.Json);
}
var responseMessage = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options);
return await responseMessage.Content.ReadFromJsonAsync<T1>(_jsonSerializerOptions);
}
}
}
@@ -1,52 +0,0 @@
using System;
using System.Linq;
using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.Simkl.Configuration
{
/// <summary>
/// Class needed to create a Plugin and configure it.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
/// </summary>
public PluginConfiguration()
{
UserConfigs = Array.Empty<UserConfig>();
}
/// <summary>
/// Gets or sets the list of user configs.
/// </summary>
public UserConfig[] UserConfigs { get; set; }
/// <summary>
/// Get config by id.
/// </summary>
/// <param name="id">The user id.</param>
/// <returns>Stored user config.</returns>
public UserConfig? GetByGuid(Guid id)
{
return UserConfigs.FirstOrDefault(c => c.Id == id);
}
/// <summary>
/// Delete user token.
/// </summary>
/// <param name="userToken">User token.</param>
public void DeleteUserToken(string userToken)
{
foreach (var config in UserConfigs)
{
if (config.UserToken == userToken)
{
config.UserToken = string.Empty;
}
}
SimklPlugin.Instance?.SaveConfiguration();
}
}
}
@@ -1,70 +0,0 @@
using System;
namespace Jellyfin.Plugin.Simkl.Configuration
{
/// <summary>
/// User config.
/// </summary>
public class UserConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="UserConfig"/> class.
/// </summary>
public UserConfig()
{
ScrobbleMovies = true;
ScrobbleShows = true;
ScrobblePercentage = 70;
ScrobbleNowWatchingPercentage = 5;
MinLength = 5;
UserToken = string.Empty; // Todo: check if token is still valid
ScrobbleTimeout = 30;
}
/// <summary>
/// Gets or sets a value indicating whether scrobble movies.
/// </summary>
public bool ScrobbleMovies { get; set; }
/// <summary>
/// Gets or sets a value indicating whether scrobble shows.
/// </summary>
public bool ScrobbleShows { get; set; }
/// <summary>
/// Gets or sets scrobble percentage.
/// </summary>
public int ScrobblePercentage { get; set; }
/// <summary>
/// Gets or sets scrobble now watching percentage.
/// </summary>
public int ScrobbleNowWatchingPercentage { get; set; }
/// <summary>
/// Gets or sets min length.
/// </summary>
/// <remarks>
/// Minimum length for scrobbling (in minutes).
/// </remarks>
public int MinLength { get; set; }
/// <summary>
/// Gets or sets user token.
/// </summary>
public string UserToken { get; set; } // Is the user logged in
/// <summary>
/// Gets or sets scrobble timeout.
/// </summary>
/// <remarks>
/// Time between scrobbling tries.
/// </remarks>
public int ScrobbleTimeout { get; set; }
/// <summary>
/// Gets or sets user id.
/// </summary>
public Guid Id { get; set; }
}
}
@@ -1,313 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Simkl's TV Tracker</title>
</head>
<body>
<div data-role="page" class="page type-interior pluginConfigurationPage" id="SimklConfigurationPage"
data-require="emby-button,emby-checkbox,emby-input,emby-select">
<div data-role="content">
<div class="content-primary">
<h1>Simkl's TV Tracker</h1>
<form id="SimklConfigurationForm">
<div id="selectContainer">
<select onchange="SimklConfig.onSelectorChange();" is="emby-select" id="user-selector"
label="Showing plugin settings for...">
<!-- This will be populated by SimklConfig.populateUsers -->
</select>
</div>
<div id="loginButtonContainer" hidden>
<h3>It seems you are not logged in, do you wish to log in?</h3>
<button onclick="SimklConfig.startLoginProcess();" is="emby-button" type="button"
class="raised button-submit block"><span>Log In</span></button>
<button onclick="location.href='https://simkl.com/';" is="emby-button" type="button"
class="raised block"><span>Create an account</span></button>
</div>
<div id="loggingIn" hidden>
<h2>Logging In</h2>
<div id="loginText"></div>
<h3 id="loginPin"></h3>
<span id="loginSecondsRemaining">900</span> seconds remaining
<button onclick="SimklConfig.stopLoginProcess();" is="emby-button" type="button"
class="raised button-cancel block"><span>Cancel</span></button>
</div>
<div id="configOptionsContainer" hidden>
<h3>Hello again <span id="simklName">USERNAME</span>!</h3>
<button onclick="SimklConfig.logOut();" is="emby-button" type="button" class="raised button block">
<span>Log Out</span></button>
<h2>Scrobbling options:</h2>
<div class="checkboxcontainer">
<label>
<input is="emby-checkbox" type="checkbox" id="ScrobbleMovies"/>
<span>Autoscrobbling Movies</span>
</label>
</div>
<div class="checkboxcontainer">
<label>
<input is="emby-checkbox" type="checkbox" id="ScrobbleShows"/>
<span>Autoscrobbling TV Shows</span>
</label>
</div>
<div class="inputContainer">
<input is="emby-input" id="ScrobblePercentage" type="number" min="0" max="100" pattern="[0-9]*"
label="Scrobbling percentage:"/>
<div class="fieldDescription">
Percentage watched needed to scrobble
</div>
</div>
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised block" onclick="history.back();"><span>${Cancel}</span>
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
var SimklConfig = {
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB",
onLoginProcess: false,
configCache: [],
loginTimer: null,
remainingTimer: null,
finish: null,
userSelector: document.querySelector('#user-selector'),
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) {
if (config != null) {
this.configCache = config;
} else {
config = this.configCache;
}
console.log("Simkl: Loading config for user " + user);
console.log(config);
SimklConfig.loginButtonContainer.setAttribute('hidden', '');
SimklConfig.configOptionsContainer.setAttribute('hidden', '')
if (config.UserConfigs.some(e => e.Id === user && e.UserToken != null && e.UserToken !== "")) {
SimklConfig.configOptionsContainer.removeAttribute('hidden');
await this.populateOptionsContainer(config.UserConfigs.filter(e => e.Id === user)[0]);
} else {
SimklConfig.loginButtonContainer.removeAttribute('hidden');
}
},
saveConfig: async function (guid) {
const uconfig = this.configCache.UserConfigs.filter(e => e.Id === guid)[0];
for (const key in uconfig) {
const element = document.querySelector("#configOptionsContainer #" + key);
if (element) {
if (element.type === 'checkbox') {
uconfig[key] = element.checked;
} else {
if (element.value != null) {
uconfig[key] = element.value;
}
}
}
}
console.log("Saving config:");
console.log(this.configCache);
ApiClient.updatePluginConfiguration(this.guid, this.configCache).then(Dashboard.processPluginConfigurationUpdateResult);
},
populateOptionsContainer: async function (userConfig) {
SimklConfig.simklName.innerText = (await SimklAPI.getUserSettings(userConfig.Id)).user.name;
for (const key in userConfig) {
const chk = document.querySelector("#configOptionsContainer input[type=checkbox]#" + 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 () {
this.onLoginProcess = true;
const code = await SimklAPI.getCode();
this.finish = new Date();
this.finish.setSeconds(this.finish.getSeconds() + code.expires_in);
this.nextInterval = new Date();
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this, code), code.Interval * 1000);
this.remainingTimer = window.setInterval(function () {
SimklConfig.loginSecondsRemaining.innerText = Math.round((SimklConfig.finish.getTime() - (new Date().getTime())) / 1000);
}, 1000);
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:';
SimklConfig.loginPin.innerText = code.user_code;
SimklConfig.loginButtonContainer.setAttribute('hidden', '');
SimklConfig.loggingIn.removeAttribute('hidden');
},
checkLoginProcess: async function (code) {
const response = await SimklAPI.checkCode(code.user_code);
console.log("Response:");
console.log(response);
if (new Date() > this.finish) {
Dashboard.alert("Timed out!");
await this.stopLoginProcess();
} else if (response.Result === "KO") {
this.loginTimer = window.setTimeout(this.checkLoginProcess.bind(this, code), code.Interval * 1000);
} else if (response.Result === "OK") {
await this.stopLoginProcess();
// Save key on settings
const uguid = SimklConfig.userSelector.value;
const filter = this.configCache.UserConfigs.filter(function (c) {
return c.Id === uguid;
});
if (filter.length > 0) {
filter[0].UserToken = response.access_token;
} else {
this.configCache.UserConfigs.push({
Id: uguid,
UserToken: response.access_token
});
}
console.log(this.configCache);
ApiClient.updatePluginConfiguration(this.guid, this.configCache);
await this.loadConfig(uguid);
} else {
Dashboard.alert("Error logging in");
}
},
stopLoginProcess: async function () {
this.onLoginProcess = false;
window.clearTimeout(this.loginTimer);
window.clearInterval(this.remainingTimer);
SimklConfig.loginButtonContainer.removeAttribute('hidden');
SimklConfig.loggingIn.setAttribute('hidden', '');
},
onSelectorChange: async function () {
if (this.onLoginProcess) {
await this.stopLoginProcess();
}
await this.loadConfig(SimklConfig.userSelector.value, null);
},
logOut: function (uguid) {
if (uguid == null) {
uguid = SimklConfig.userSelector.value;
}
var filter = this.configCache.UserConfigs.filter(function (c) {
return c.Id === uguid;
});
console.log(filter);
if (filter.length > 0) {
filter[0].UserToken = "";
} else {
console.log("User not found " + uguid);
}
console.log(this.configCache);
window.ApiClient.updatePluginConfiguration(this.guid, this.configCache);
this.loadConfig(uguid);
}
}
var SimklAPI = {
getCode: function () {
const request = {
url: window.ApiClient.getUrl('Simkl/oauth/pin'),
type: 'GET',
headers: {
accept: 'application/json'
}
}
return window.ApiClient.fetch(request)
.then(function (result) {
return result;
})
.catch(function (result) {
console.error(result);
Dashboard.alert("Some error occurred, see browser log for more details");
SimklConfig.stopLoginProcess();
});
},
checkCode: function (user_code) {
const request = {
url: window.ApiClient.getUrl('Simkl/oauth/pin/' + user_code),
type: 'GET',
headers: {
accept: 'application/json'
}
}
return window.ApiClient.fetch(request)
.then(function (result) {
return result;
})
.catch(function (result) {
console.error(result);
Dashboard.alert("Some error occurred, see browser log for more details");
SimklConfig.stopLoginProcess();
});
},
getUserSettings: function (secret) {
const request = {
url: window.ApiClient.getUrl('Simkl/users/settings/' + secret),
type: 'GET',
headers: {
accept: 'application/json'
}
}
return window.ApiClient.fetch(request)
.then(function (result) {
return result;
})
.catch(function (result) {
console.error(result);
Dashboard.alert("Something went wrong, see logs for more details");
});
}
}
document.querySelector('#SimklConfigurationPage')
.addEventListener('pageshow', async function () {
Dashboard.showLoadingMsg();
await Promise.all([
window.ApiClient.getUsers().then(SimklConfig.populateUsers),
window.ApiClient.getPluginConfiguration(SimklConfig.guid).then(SimklConfig.loadConfig.bind(SimklConfig, ApiClient.getCurrentUserId()))]);
Dashboard.hideLoadingMsg();
});
document.querySelector('#SimklConfigurationForm')
.addEventListener('submit', function (e) {
e.preventDefault();
Dashboard.showLoadingMsg();
SimklConfig.saveConfig(SimklConfig.userSelector.value);
Dashboard.hideLoadingMsg();
});
</script>
</div>
</body>
</html>
@@ -1,37 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<nullable>enable</nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.7-*" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.*" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
@@ -1,16 +0,0 @@
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>();
}
}
}
@@ -1,178 +0,0 @@
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, PlaybackProgressEventArgs playbackProgress)
{
var position = playbackProgress.PlaybackPositionTicks;
var runtime = playbackProgress.MediaInfo.RunTimeTicks;
if (runtime != null)
{
var percentageWatched = position / (float)runtime * 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 (runtime < 60 * 10000 * config.MinLength)
{
return false;
}
return playbackProgress.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);
}
private async void OnPlaybackStopped(object? sessions, PlaybackStopEventArgs e)
{
await ScrobbleSession(e);
}
private async Task ScrobbleSession(PlaybackProgressEventArgs eventArgs)
{
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))
{
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");
}
}
}
}
-51
View File
@@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
using Jellyfin.Plugin.Simkl.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.Simkl
{
/// <summary>
/// SIMKL tracker.
/// </summary>
public class SimklPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
/// <summary>
/// Initializes a new instance of the <see cref="SimklPlugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
public SimklPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
/// <summary>
/// Gets the current instance of the plugin.
/// </summary>
public static SimklPlugin? Instance { get; private set; }
/// <inheritdoc />
public override Guid Id => new Guid("07CAEF58-A94B-4211-A62C-F9774E04EBDB");
/// <inheritdoc />
public override string Name => "Simkl";
/// <inheritdoc />
public override string Description => "Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!";
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
{
Name = Name,
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
};
}
}
}
+6 -7
View File
@@ -1,15 +1,14 @@
---
name: "Simkl"
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB"
name: "Anilist Sync"
guid: "18c2a8ea-afa0-4a0b-aa94-072b492ab80b"
version: "1.0.0.0"
targetAbi: "10.7.0.0"
owner: "crobibero"
overview: "Scrobble to Simkl"
owner: "ARufenach"
overview: "Scrobble to Anilist"
description: >
Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!
In development
category: "General"
framework: "net5.0"
artifacts:
- "Jellyfin.Plugin.Simkl.dll"
- "Jellyfin.Plugin.AnilistSync.dll"
changelog: >
Initial release