diff options
Diffstat (limited to 'Api')
-rw-r--r-- | Api/JCoverSharedController.cs | 9 | ||||
-rw-r--r-- | Api/JCoverStaticProvider.cs | 40 | ||||
-rw-r--r-- | Api/ScriptInjector.cs | 74 | ||||
-rw-r--r-- | Api/coverscript.js | 64 |
4 files changed, 187 insertions, 0 deletions
diff --git a/Api/JCoverSharedController.cs b/Api/JCoverSharedController.cs new file mode 100644 index 0000000..a5eecc1 --- /dev/null +++ b/Api/JCoverSharedController.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Plugin.JCoverXtremePro.Api; + +[ApiController] +[Route("JCoverXtreme")] +public class JCoverSharedController : ControllerBase +{ +}
\ No newline at end of file diff --git a/Api/JCoverStaticProvider.cs b/Api/JCoverStaticProvider.cs new file mode 100644 index 0000000..e4587ba --- /dev/null +++ b/Api/JCoverStaticProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.JCoverXtremePro.Api; + +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +/// <summary> +/// Static file server for the JavaScript snippet injected by <see cref="ScriptInjector"/> +/// </summary> +[ApiController] +[Route("JCoverXtremeProStatic")] +public class JCoverStaticProvider : ControllerBase +{ + private readonly Assembly assembly; + private readonly string scriptPath; + + public JCoverStaticProvider() + { + assembly = Assembly.GetExecutingAssembly(); + scriptPath = GetType().Namespace + ".coverscript.js"; + } + + [HttpGet("ClientScript")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Produces("application/javascript")] + public ActionResult GetClientScript() + { + Plugin.Logger.LogInformation($"Requesting ClientScript {scriptPath}"); + var scriptStream = assembly.GetManifestResourceStream(scriptPath); + if (scriptStream == null) + { + return NotFound(); + } + + return File(scriptStream, "application/javascript"); + } +}
\ No newline at end of file diff --git a/Api/ScriptInjector.cs b/Api/ScriptInjector.cs new file mode 100644 index 0000000..d6b1865 --- /dev/null +++ b/Api/ScriptInjector.cs @@ -0,0 +1,74 @@ +namespace Jellyfin.Plugin.JCoverXtremePro.Api; + +using System; +using System.IO; +using System.Text.RegularExpressions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Logging; + +/// <summary> +/// Utility for injecting a JavaScript script tag into the Jellyfin web frontend. +/// </summary> +public static class ScriptInjector +{ + public static void PerformInjection( + IApplicationPaths applicationPaths, + IServerConfigurationManager configurationManager + ) + { + var indexHtmlFilePath = Path.Combine(applicationPaths.WebPath, "index.html"); + if (!File.Exists(indexHtmlFilePath)) + { + Plugin.Logger.LogWarning("Could not find index html file"); + return; + } + + var html = File.ReadAllText(indexHtmlFilePath); + var snippet = GetInjectedSnippet(GetHTTPBasePath(configurationManager)); + if (html.Contains(snippet, StringComparison.InvariantCulture)) + { + Plugin.Logger.LogInformation("Not injecting existing HTML snippet."); + return; + } + + html = Regex.Replace(html, $"<script[^>]*guid=\"{Plugin.GUID}\"[^>]*></script>", string.Empty); + var bodyEnd = html.LastIndexOf("</body>", StringComparison.InvariantCulture); + if (bodyEnd < 0) + { + Plugin.Logger.LogError("Could not find end of body to inject script"); + return; + } + + html = html.Insert(bodyEnd, snippet); + try + { + File.WriteAllText(indexHtmlFilePath, html); + Plugin.Logger.LogInformation("Injected index.html"); + } + catch (Exception e) + { + Plugin.Logger.LogError(e, "Failed to write patched index.html"); + } + } + + public static string GetHTTPBasePath(IServerConfigurationManager configurationManager) + { + var networkConfig = configurationManager.GetConfiguration("network"); + var configType = networkConfig.GetType(); + var baseUrlField = configType.GetProperty("BaseUrl"); + var baseUrl = baseUrlField!.GetValue(networkConfig)!.ToString()!.Trim('/'); + return baseUrl; + } + + public static string GetScriptUrl(string basePath) + { + return basePath + "/JCoverXtremeProStatic/ClientScript"; + } + + public static string GetInjectedSnippet(string basePath) + { + return + $"<script guid=\"{Plugin.GUID}\" plugin=\"{Plugin.Instance!.Name}\" src=\"{GetScriptUrl(basePath)}\" defer></script>"; + } +}
\ No newline at end of file diff --git a/Api/coverscript.js b/Api/coverscript.js new file mode 100644 index 0000000..24f3c34 --- /dev/null +++ b/Api/coverscript.js @@ -0,0 +1,64 @@ +(function () { + /** + * + * @param {HTMLElement} element + * @param {string} selector + * @returns {HTMLElement | null} + */ + function findParent(element, selector) { + let p = element + while (p) { + if (p.matches(selector)) return p + p = p.parentNode + } + return null + } + + const injectionMarker = "JCoverXtremePro-injection-marker"; + + /** + * + * @param {HTMLElement} cloneFrom + * @return {HTMLElement} + */ + function createDownloadSeriesButton(cloneFrom) { + /*<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize paper-icon-button-light" raised"="" title="Download"><span class="material-icons cloud_download" aria-hidden="true"></span></button>*/ + //import LayersIcon from '@mui/icons-material/Layers'; + //import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; + const element = document.createElement("button") + element.classList.add(...cloneFrom.classList) + element.classList.add(injectionMarker) + element.title = "Download Series" + const icon = document.createElement("span") + icon.classList.add("material-icons", "burst_mode") + icon.setAttribute("aria-hidden", "true") + element.appendChild(icon) + element.addEventListener("click", ev => { + ev.preventDefault() + + alert("YOU HAVE JUST BEEN INTERDICTED BY THE JCOVERXTREMEPRO SERIES DOWNLOADIFICATOR") + }) + return element + } + + const observer = new MutationObserver(() => { + console.log("JCoverXtremePro observation was triggered!") + console.log("Listing all download buttons") + /** + * @type {NodeListOf<Element>} + */ + const buttons = document.querySelectorAll(".imageEditorCard .cardFooter .btnDownloadRemoteImage") + + buttons.forEach(element => { + const downloadRowContainer = findParent(element, ".cardText") + if (downloadRowContainer.querySelector(`.${injectionMarker}`)) return + // TODO: extract information about the series, and check if this is at all viable + downloadRowContainer.appendChild(createDownloadSeriesButton(element)) + }) + + }) + observer.observe(document.body, {// TODO: selectively observe the body if at all possible + subtree: true, + childList: true, + }); +})()
\ No newline at end of file |