summaryrefslogtreecommitdiff
path: root/Api
diff options
context:
space:
mode:
Diffstat (limited to 'Api')
-rw-r--r--Api/JCoverSharedController.cs9
-rw-r--r--Api/JCoverStaticProvider.cs40
-rw-r--r--Api/ScriptInjector.cs74
-rw-r--r--Api/coverscript.js64
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