using System; using System.Reflection; using System.IO; using System.Collections.Generic; using System.Linq; using System.Runtime.Loader; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using MediaBrowser.Common.Configuration; using Jellyfin.Plugin.MediaBarEnhanced.Helpers; namespace Jellyfin.Plugin.MediaBarEnhanced { /// /// Handles the injection of the MediaBarEnhanced script into the Jellyfin web interface. /// public class ScriptInjector { private readonly IApplicationPaths _appPaths; private readonly ILogger _logger; public const string ScriptTag = ""; public const string CssTag = ""; public const string ScriptMarker = ""; public const string CssMarker = ""; /// /// Initializes a new instance of the class. /// /// The application paths. /// The logger. public ScriptInjector(IApplicationPaths appPaths, ILogger logger) { _appPaths = appPaths; _logger = logger; } /// /// Injects the script tag into index.html if it's not already present. /// /// True if injection was successful or already present, false otherwise. public void Inject() { try { var webPath = GetWebPath(); if (string.IsNullOrEmpty(webPath)) { _logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback."); RegisterFileTransformation(); return; } var indexPath = Path.Combine(webPath, "index.html"); if (!File.Exists(indexPath)) { _logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath); RegisterFileTransformation(); return; } var content = File.ReadAllText(indexPath); var injectedJS = false; var injectedCSS = false; if (!content.Contains(ScriptTag)) { var index = content.IndexOf(ScriptMarker, StringComparison.OrdinalIgnoreCase); if (index != -1) { content = content.Insert(index, ScriptTag + Environment.NewLine); injectedJS = true; } } if (!content.Contains(CssTag)) { var index = content.IndexOf(CssMarker, StringComparison.OrdinalIgnoreCase); if (index != -1) { content = content.Insert(index, CssTag + Environment.NewLine); injectedCSS = true; } } if (injectedJS && injectedCSS) { File.WriteAllText(indexPath, content); _logger.LogInformation("MediaBarEnhanced script injected into index.html."); } else if (injectedJS) { File.WriteAllText(indexPath, content); _logger.LogInformation("MediaBarEnhanced JS script injected into index.html. But CSS was already present or could not be injected."); } else if (injectedCSS) { File.WriteAllText(indexPath, content); _logger.LogInformation("MediaBarEnhanced CSS injected into index.html. But JS script was already present or could not be injected."); } else { _logger.LogInformation("MediaBarEnhanced script and CSS already present in index.html. Or could not be injected."); } } catch (Exception ex) { _logger.LogError(ex, "Error injecting MediaBarEnhanced resources. Attempting fallback."); RegisterFileTransformation(); } } /// /// Removes the script tag from index.html. /// public void Remove() { UnregisterFileTransformation(); try { var webPath = GetWebPath(); if (string.IsNullOrEmpty(webPath)) { return; } var indexPath = Path.Combine(webPath, "index.html"); if (!File.Exists(indexPath)) { return; } var content = File.ReadAllText(indexPath); var modified = false; if (content.Contains(ScriptTag)) { content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, ""); modified = true; } if (content.Contains(CssTag)) { content = content.Replace(CssTag + Environment.NewLine, "").Replace(CssTag, ""); modified = true; } if (modified) { File.WriteAllText(indexPath, content); _logger.LogInformation("MediaBarEnhanced script removed from index.html."); } } catch (Exception ex) { _logger.LogError(ex, "Error removing MediaBarEnhanced script."); } } private string? GetWebPath() { var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public); return prop?.GetValue(_appPaths) as string; } private void RegisterFileTransformation() { _logger.LogInformation("MediaBarEnhanced Fallback. Registering file transformations."); List payloads = new List(); { JObject payload = new JObject(); // Random GUID for ID payload.Add("id", "0dfac9d7-d898-4944-900b-1c1837707279"); payload.Add("fileNamePattern", "index.html"); payload.Add("callbackAssembly", GetType().Assembly.FullName); payload.Add("callbackClass", typeof(TransformationPatches).FullName); payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml)); payloads.Add(payload); } Assembly? fileTransformationAssembly = AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false); if (fileTransformationAssembly != null) { Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface"); if (pluginInterfaceType != null) { foreach (JObject payload in payloads) { pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload }); } _logger.LogInformation("File transformations registered successfully."); } else { _logger.LogWarning("FileTransformation plugin found but PluginInterface type missing."); } } else { _logger.LogWarning("FileTransformation plugin assembly not found. Fallback failed."); } } private void UnregisterFileTransformation() { try { Assembly? fileTransformationAssembly = AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false); if (fileTransformationAssembly != null) { Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface"); if (pluginInterfaceType != null) { // The ID must match the one used in RegisterFileTransformation Guid id = Guid.Parse("0dfac9d7-d898-4944-900b-1c1837707279"); pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id }); _logger.LogInformation("File transformation unregistered successfully."); } } } catch (Exception ex) { // Log but don't throw, as we want to continue with normal removal _logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered."); } } } }