using System.IO; using System.Threading.Tasks; using Jellyfin.Plugin.Simkl.API.Exceptions; using Jellyfin.Plugin.Simkl.API.Objects; using Jellyfin.Plugin.Simkl.API.Responses; using MediaBrowser.Common.Net; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; // using System.Threading; namespace Jellyfin.Plugin.Simkl.API { /// /// Simkl Api. /// public class SimklApi { /* INTERFACES */ private readonly IJsonSerializer _json; private readonly ILogger _logger; private readonly IHttpClient _httpClient; /* BASIC API THINGS */ /// /// Base url. /// public const string Baseurl = @"https://api.simkl.com"; // public const string BASE_URL = @"http://private-9c39b-simkl.apiary-proxy.com"; /// /// Redirect uri. /// public const string RedirectUri = @"https://simkl.com/apps/emby/connected/"; /// /// Api key. /// public const string Apikey = @"27dd5d6adc24aa1ad9f95ef913244cbaf6df5696036af577ed41670473dc97d0"; /// /// Secret. /// public const string Secret = @"d7b9feb9d48bbaa69dbabaca21ba4671acaa89198637e9e136a4d69ec97ab68b"; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public SimklApi(IJsonSerializer json, ILogger logger, IHttpClient httpClient) { _json = json; _logger = logger; _httpClient = httpClient; } /// /// Get code. /// /// Code response. public async Task GetCode() { var uri = $"/oauth/pin?client_id={Apikey}&redirect={RedirectUri}"; return _json.DeserializeFromStream(await Get(uri).ConfigureAwait(false)); } /// /// Get code status. /// /// User code. /// Code status. public async Task GetCodeStatus(string userCode) { var uri = $"/oauth/pin/{userCode}?client_id={Apikey}"; return _json.DeserializeFromStream(await Get(uri).ConfigureAwait(false)); } /// /// Get user settings. /// /// User token. /// User settings. public async Task GetUserSettings(string userToken) { try { return _json.DeserializeFromStream(await Post("/users/settings/", userToken).ConfigureAwait(false)); } catch (MediaBrowser.Model.Net.HttpException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { // Wontfix: Custom status codes // "You don't get to pick your response code" - Luke (System Architect of Emby) // https://emby.media/community/index.php?/topic/61889-wiki-issue-resultfactorythrowerror/ return new UserSettings { error = "user_token_failed" }; } } /// /// Mark as watched. /// /// Item. /// User token. /// Status. public async Task<(bool success, BaseItemDto item)> MarkAsWatched(BaseItemDto item, string userToken) { var history = CreateHistoryFromItem(item); var r = await SyncHistoryAsync(history, userToken).ConfigureAwait(false); _logger.LogDebug("Response: " + _json.SerializeToString(r)); if (history.movies.Count == r.Added.Movies && history.shows.Count == r.Added.Shows) { return (true, item); } // If we are here, is because the item has not been found // let's try scrobbling from full path try { (history, item) = await GetHistoryFromFileName(item).ConfigureAwait(false); } catch (InvalidDataException) { // Let's try again but this time using only the FILE name _logger.LogDebug("Couldn't scrobble using full path, trying using only filename"); (history, item) = await GetHistoryFromFileName(item, false).ConfigureAwait(false); } r = await SyncHistoryAsync(history, userToken).ConfigureAwait(false); _logger.LogDebug("Response: " + _json.SerializeToString(r)); return (history.movies.Count == r.Added.Movies && history.shows.Count == r.Added.Shows, item); } /// /// Get from file. /// /// Filename. /// Search file response. private async Task GetFromFile(string filename) { var f = new SimklFile { file = filename }; _logger.LogInformation("Posting: " + _json.SerializeToString(f)); using var r = new StreamReader(await Post("/search/file/", null, f).ConfigureAwait(false)); var t = await r.ReadToEndAsync().ConfigureAwait(false); _logger.LogDebug("Response: " + t); return _json.DeserializeFromString(t); } /// /// Get history from file name. /// /// Item. /// Full path. /// Srobble history. private async Task<(SimklHistory history, BaseItemDto item)> GetHistoryFromFileName(BaseItemDto item, bool fullpath = true) { var fname = fullpath ? item.Path : Path.GetFileName(item.Path); var mo = await GetFromFile(fname).ConfigureAwait(false); var history = new SimklHistory(); if (item.IsMovie == true || item.Type == "Movie") { if (mo.Type != "movie") { throw new InvalidDataException("type != movie (" + mo.Type + ")"); } item.Name = mo.Movie.title; item.ProductionYear = mo.Movie.year; history.movies.Add(mo.Movie); } else if (item.IsSeries == true || item.Type == "Episode") { if (mo.Type != "episode") { throw new InvalidDataException("type != episode (" + mo.Type + ")"); } item.Name = mo.Episode.title; item.SeriesName = mo.Show.title; item.IndexNumber = mo.Episode.episode; item.ParentIndexNumber = mo.Episode.season; item.ProductionYear = mo.Show.year; history.episodes.Add(mo.Episode); } return (history, item); } private static HttpRequestOptions GetOptions(string userToken = null) { var options = new HttpRequestOptions { RequestContentType = "application/json", LogErrorResponseBody = true, EnableDefaultUserAgent = true }; options.RequestHeaders.Add("simkl-api-key", Apikey); // options.RequestHeaders.Add("Content-Type", "application/json"); if (!string.IsNullOrEmpty(userToken)) { options.RequestHeaders.Add("Authorization", "Bearer " + userToken); } return options; } private static SimklHistory CreateHistoryFromItem(BaseItemDto item) { var history = new SimklHistory(); if (item.IsMovie == true || item.Type == "Movie") { history.movies.Add(new SimklMovie(item)); } else if (item.IsSeries == true || item.Type == "Episode") { // TODO: TV Shows scrobbling (WIP) history.shows.Add(new SimklShow(item)); } return history; } /// /// Implements /sync/history method from simkl. /// /// History object. /// User token. /// The sync history response. private async Task SyncHistoryAsync(SimklHistory history, string userToken) { try { _logger.LogInformation("Syncing History: " + _json.SerializeToString(history)); return _json.DeserializeFromStream(await Post("/sync/history", userToken, history).ConfigureAwait(false)); } catch (MediaBrowser.Model.Net.HttpException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { _logger.LogError("Invalid user token " + userToken + ", deleting"); SimklPlugin.Instance.Configuration.DeleteUserToken(userToken); throw new InvalidTokenException("Invalid user token " + userToken); } } /// /// API's private get method, given RELATIVE url and headers. /// /// Relative url. /// Authentication token. /// HTTP(s) Stream to be used. private async Task Get(string url, string userToken = null) { // Todo: If string is not null neither empty var options = GetOptions(userToken); options.Url = Baseurl + url; return await _httpClient.Get(options).ConfigureAwait(false); } /// /// API's private post method. /// /// Relative post url. /// Authentication token. /// Object to serialize. private async Task Post(string url, string userToken = null, object data = null) { var options = GetOptions(userToken); options.Url = Baseurl + url; if (data != null) { options.RequestContent = _json.SerializeToString(data); } return (await _httpClient.Post(options).ConfigureAwait(false)).Content; } } }