Skip to content

Commit

Permalink
Merge pull request #138 from vosmiic/frontend-fixes
Browse files Browse the repository at this point in the history
Frontend fixes
  • Loading branch information
vosmiic committed Jun 10, 2024
2 parents d7f0b0e + 50dc8e4 commit 2ff3c0c
Show file tree
Hide file tree
Showing 9 changed files with 409 additions and 216 deletions.
102 changes: 69 additions & 33 deletions Api/AniSyncController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@
using jellyfin_ani_sync.Helpers;
using jellyfin_ani_sync.Interfaces;
using jellyfin_ani_sync.Models;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;

namespace jellyfin_ani_sync.Api {
[ApiController]
[Authorize(Policy = Policies.RequiresElevation)]
[Route("[controller]")]
public class AniSyncController : ControllerBase {
private readonly IHttpClientFactory _httpClientFactory;
Expand Down Expand Up @@ -77,6 +80,9 @@ public string BuildAuthorizeRequestUrl(ApiName provider, string clientId, string
[HttpGet]
[Route("testAnimeListSaveLocation")]
public async Task<IActionResult> TestAnimeSaveLocation(string saveLocation) {
if (String.IsNullOrEmpty(saveLocation))
return BadRequest("Save location is empty");

try {
await using (System.IO.File.Create(
Path.Combine(
Expand All @@ -96,30 +102,35 @@ public async Task<IActionResult> TestAnimeSaveLocation(string saveLocation) {
[HttpGet]
[Route("passwordGrant")]
public async Task<IActionResult> PasswordGrantAuthentication(ApiName provider, string userId, string username, string password) {
if (new ApiAuthentication(provider, _httpClientFactory, _serverApplicationHost, _httpContextAccessor, _loggerFactory, new ProviderApiAuth { ClientId = username, ClientSecret = password }).GetToken(Guid.Parse(userId)) != null) {
if (provider == ApiName.Kitsu) {
var userConfig = Plugin.Instance.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId));
try {
new ApiAuthentication(provider, _httpClientFactory, _serverApplicationHost, _httpContextAccessor, _loggerFactory, new ProviderApiAuth { ClientId = username, ClientSecret = password }).GetToken(Guid.Parse(userId));
} catch (Exception e) {
return StatusCode(500, $"Could not authenticate; {e.Message}");
}

if (provider == ApiName.Kitsu) {
var userConfig = Plugin.Instance.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId));

if (userConfig != null) {
KitsuApiCalls kitsuApiCalls = new KitsuApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, userConfig);
var kitsuUserConfig = await kitsuApiCalls.GetUserInformation();
if (kitsuUserConfig == null)
return StatusCode(500, "Could not authenticate");
var existingKeyPair = userConfig.KeyPairs.FirstOrDefault(item => item.Key == "KitsuUserId");
if (existingKeyPair != null) {
existingKeyPair.Value = kitsuUserConfig.Id.ToString();
} else {
userConfig.KeyPairs.Add(new KeyPairs { Key = "KitsuUserId", Value = kitsuUserConfig.Id.ToString() });
}

Plugin.Instance.SaveConfiguration();
}
Plugin.Instance.SaveConfiguration();
}

return Ok();
}

return BadRequest();
return Ok();
}

[AllowAnonymous]
[HttpGet]
[Route("authCallback")]
public IActionResult MalCallback(string code) {
Expand All @@ -140,7 +151,7 @@ public IActionResult MalCallback(string code) {
}
}

return Ok();
return new ObjectResult("Success! Received access token, please contact the Jellyfin administrator to test the authentication.") { StatusCode = 200 };
} else {
_logger.LogError("Authenticated user ID could not be found in the configuration. Please regenerate the authentication URL and try again");
return StatusCode(500);
Expand All @@ -153,32 +164,41 @@ public IActionResult MalCallback(string code) {
public async Task<ActionResult> GetUser(ApiName apiName, string userId) {
UserConfig? userConfig = Plugin.Instance?.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId));
if (userConfig == null) {
_logger.LogError("User not found");
return new StatusCodeResult(500);
_logger.LogError("User not found in config");
return StatusCode(500, "User not found in config");
}

switch (apiName) {
case ApiName.Mal:
MalApiCalls malApiCalls = new MalApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, Plugin.Instance.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId)));

return new OkObjectResult(await malApiCalls.GetUserInformation());
MalApiCalls.User? malUser = await malApiCalls.GetUserInformation();
return malUser != null ? new OkObjectResult(malUser) : StatusCode(500, "Authentication failed");
case ApiName.AniList:
AniListApiCalls aniListApiCalls = new AniListApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, Plugin.Instance.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId)));

AniListViewer.Viewer? user = await aniListApiCalls.GetCurrentUser();
if (user == null) {
return StatusCode(500, "Authentication failed");
}

return new OkObjectResult(new MalApiCalls.User {
Name = user?.Name
Name = user.Name
});
case ApiName.Kitsu:
KitsuApiCalls kitsuApiCalls;
try {
kitsuApiCalls = new KitsuApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, Plugin.Instance.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId)));
} catch (ArgumentNullException) {
_logger.LogError("User could not be retrieved from API");
return new StatusCodeResult(500);
return StatusCode(500, "User could not be retrieved from API");
}

var apiCall = await kitsuApiCalls.GetUserInformation();
if (apiCall == null) {
return StatusCode(500, "Authentication failed");
}

return new OkObjectResult(new MalApiCalls.User {
Name = apiCall.Name
});
Expand All @@ -190,17 +210,21 @@ public async Task<ActionResult> GetUser(ApiName apiName, string userId) {
annictApiCalls = new AnnictApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, Plugin.Instance.PluginConfiguration.UserConfig.FirstOrDefault(item => item.UserId == Guid.Parse(userId)));
} catch (ArgumentNullException) {
_logger.LogError("User could not be retrieved from API");
return new StatusCodeResult(500);
return StatusCode(500, "User could not be retrieved from API");
}

var annictApiCall = await annictApiCalls.GetCurrentUser();
if (annictApiCall == null) {
return StatusCode(500, "Authentication failed");
}

return new OkObjectResult(new MalApiCalls.User {
Name = annictApiCall.AnnictSearchData.Viewer.username
});
case ApiName.Shikimori:
string? shikimoriAppName = ConfigHelper.GetShikimoriAppName(_logger);
if (string.IsNullOrEmpty(shikimoriAppName)) {
return new StatusCodeResult(500);
return StatusCode(500, "No App Name");
}

ShikimoriApiCalls shikimoriApiCalls = new ShikimoriApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, new Dictionary<string, string> { { "User-Agent", shikimoriAppName } }, userConfig);
Expand All @@ -212,12 +236,12 @@ public async Task<ActionResult> GetUser(ApiName apiName, string userId) {
});
} else {
_logger.LogError("User could not be retrieved from API");
return new StatusCodeResult(500);
return StatusCode(500, "User could not be retrieved from API");
}
case ApiName.Simkl:
string? simklClientId = ConfigHelper.GetSimklClientId(_logger);
if (string.IsNullOrEmpty(simklClientId)) {
return new StatusCodeResult(500);
return StatusCode(500, "No Client ID");
}

var simklApiCalls = new SimklApiCalls(_httpClientFactory, _loggerFactory, _serverApplicationHost, _httpContextAccessor, _memoryCache, _delayer, new Dictionary<string, string> { { "simkl-api-key", simklClientId } }, userConfig);
Expand All @@ -227,7 +251,7 @@ public async Task<ActionResult> GetUser(ApiName apiName, string userId) {
Name = null
});
} else {
return new StatusCodeResult(500);
return StatusCode(500, "Not authenticated");
}
}

Expand All @@ -236,26 +260,38 @@ public async Task<ActionResult> GetUser(ApiName apiName, string userId) {

[HttpGet]
[Route("parameters")]
public object GetFrontendParameters() {
public object GetFrontendParameters(ParameterInclude[]? includes) {
Parameters toReturn = new Parameters();
toReturn.providerList = new List<ExpandoObject>();
foreach (ApiName apiName in Enum.GetValues<ApiName>()) {
dynamic provider = new ExpandoObject();
provider.Name = apiName.GetType()
.GetMember(apiName.ToString())
.First()
.GetCustomAttribute<DisplayAttribute>()
?.GetName();
provider.Key = apiName;
toReturn.providerList.Add(provider);
if (includes == null || includes.Contains(ParameterInclude.ProviderList)) {
toReturn.providerList = new List<ExpandoObject>();
foreach (ApiName apiName in Enum.GetValues<ApiName>()) {
dynamic provider = new ExpandoObject();
provider.Name = apiName.GetType()
.GetMember(apiName.ToString())
.First()
.GetCustomAttribute<DisplayAttribute>()
?.GetName();
provider.Key = apiName;
toReturn.providerList.Add(provider);
}
}

toReturn.localIpAddress = Request.HttpContext.Connection.LocalIpAddress != null ? Request.HttpContext.Connection.LocalIpAddress.ToString() : "localhost";
toReturn.localPort = _serverApplicationHost.ListenWithHttps ? _serverApplicationHost.HttpsPort : _serverApplicationHost.HttpPort;
toReturn.https = _serverApplicationHost.ListenWithHttps;
if (includes == null || includes.Contains(ParameterInclude.LocalIpAddress))
toReturn.localIpAddress = Request.HttpContext.Connection.LocalIpAddress != null ? Request.HttpContext.Connection.LocalIpAddress.ToString() : "localhost";
if (includes == null || includes.Contains(ParameterInclude.LocalPort))
toReturn.localPort = _serverApplicationHost.ListenWithHttps ? _serverApplicationHost.HttpsPort : _serverApplicationHost.HttpPort;
if (includes == null || includes.Contains(ParameterInclude.Https))
toReturn.https = _serverApplicationHost.ListenWithHttps;
return toReturn;
}

public enum ParameterInclude {
ProviderList = 0,
LocalIpAddress = 1,
LocalPort = 2,
Https = 3
}

private class Parameters {
public string localIpAddress { get; set; }
public int localPort { get; set; }
Expand Down
11 changes: 4 additions & 7 deletions Api/AuthApiCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,8 @@ public AuthApiCall(IHttpClientFactory httpClientFactory,
public async Task<HttpResponseMessage?> AuthenticatedApiCall(ApiName provider, CallType callType, string url, FormUrlEncodedContent formUrlEncodedContent = null, StringContent stringContent = null, Dictionary<string, string>? requestHeaders = null) {
int attempts = 0;
int timeoutSeconds = 4;
UserApiAuth auth;
try {
auth = UserConfig.UserApiAuth.FirstOrDefault(item => item.Name == provider);
if (auth == null || auth.AccessToken == null) throw new NullReferenceException();
} catch (NullReferenceException) {
UserApiAuth? auth = UserConfig.UserApiAuth?.FirstOrDefault(item => item.Name == provider);
if (auth == null) {
_logger.LogError("Could not find authentication details, please authenticate the plugin first");
return null;
}
Expand Down Expand Up @@ -119,8 +116,8 @@ public AuthApiCall(IHttpClientFactory httpClientFactory,
UserApiAuth newAuth;
try {
newAuth = new ApiAuthentication(provider, _httpClientFactory, _serverApplicationHost, _httpContextAccessor, _loggerFactory).GetToken(UserConfig.UserId, refreshToken: auth.RefreshToken);
} catch (Exception) {
_logger.LogError("Could not re-authenticate. Please manually re-authenticate the user via the AniSync configuration page");
} catch (Exception e) {
_logger.LogError($"Could not re-authenticate: {e.Message}, please manually re-authenticate the user via the AniSync configuration page");
return null;
}

Expand Down
60 changes: 60 additions & 0 deletions Configuration/CommonJs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// below tab functionality is heavily influenced by the tabs used in jellyfin-plugin-media-cleaner: https://github.com/shemanaev/jellyfin-plugin-media-cleaner

export function getTabs() {
const tabs = [
{
href: configurationPageUrl('Ani-Sync'),
name: 'General'
},
{
href: configurationPageUrl('AniSync_ManualSync'),
name: 'Manual Sync'
}
];
return tabs;
}

export function setTabs(selectedIndex, itemsFn) {
const $tabs = document.querySelector('.pluginConfigurationPage:not(.hide) #navigationTabs');
$tabs.innerHTML = '';

let i = 0;
for (const tab of itemsFn()) {
const elem = document.createElement("a");
elem.innerHTML = tab.name;
elem.addEventListener('click', (e) => Dashboard.navigate('/' + tab.href, false));
elem.className = 'emby-button' + (i === selectedIndex ? ' ui-btn-active' : '');
elem.dataset.role = 'button';

i++;
$tabs.appendChild(elem);
}
}

export const TabGeneral = 0;
export const TabManualSync = 1;

const configurationPageUrl = (name) => 'configurationpage?name=' + encodeURIComponent(name);

export function setProviderSelection(page, providerList, providerListSelectElement) {
var html = '';
for (var x = 0; x < providerList.length; x++) {
html += '<option value="' + providerList[x].Key + '">' + providerList[x].Name + '</option>';
}
page.querySelector(providerListSelectElement).innerHTML = html;
}

export function populateUserList(page, users, userListSelectElement) {
var html = '';
for (var x = 0; x < users.length; x++) {
html += '<option value="' + users[x].Id + '">' + users[x].Name + '</option>';
}
page.querySelector(userListSelectElement).innerHTML = html;
}

export const parameterInclude = {
ProviderList: 0,
LocalIpAddress: 1,
LocalPort: 2,
Https: 3
}
Loading

0 comments on commit 2ff3c0c

Please sign in to comment.