JR-021, JR-022, JR-023: File Transformation is a hard dependency
Deletes the on-disk index.html injection rather than leaving it switched off. Plugin.cs had already stopped calling it, but an unreachable write path with a live signature is the one a later refactor re-enables by accident, and it was still the behaviour the README and the release changelog advertised. Patching index.html on disk is destructive in ways a plugin cannot clean up after: the patch outlives an uninstall, a web-client upgrade discards it silently, and it races any other plugin touching the same file. It is also a second code path, and the one nobody runs is the one that rots. WebClientPatchService is now removal-only. The strip is factored out as RemoveInjection so it is testable without a filesystem. Removal is the one write JR-021 permits -- an earlier JRay did patch the file, and those users must not be left with a stale injection pointing at endpoints that have since changed. It keys on JRay's own marker, so it touches nothing another plugin added. JR-021 is a requirement to *not do* something, which no unit test can demonstrate, so scripts/checks/no-index-injection.sh verifies it by absence. The check was confirmed to fail on a reintroduced Apply() and on reintroduced ReplaceLast injection -- a check that has only ever passed is not evidence. Jellyfin has no plugin dependency mechanism, so nothing installs File Transformation for the user and a log warning alone is one nobody reads. GET /Plugins/JRay/Status/Dependencies reports whether the dependency is satisfied, and the configuration page renders it with the repository URL and what to do with it. Absent the plugin only the overlay is disabled; every other feature works. JR-022 and JR-023 stay In Progress rather than Done: neither has a test that executes, and this repo has no test project yet. TRACES: JR-021, JR-022, JR-023 | PR-004 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,10 +29,16 @@
|
||||
<span>Enable pause overlay</span>
|
||||
</label>
|
||||
<div class="fieldDescription">
|
||||
Injects a small script into the web client that shows on-screen actors
|
||||
when playback is paused. Disabling this removes the injected script.
|
||||
Shows the cast of the current scene when playback is paused. This
|
||||
requires the
|
||||
<a is="emby-linkbutton" class="button-link" href="https://github.com/IAmParadox27/jellyfin-plugin-file-transformation" target="_blank" rel="noopener">File Transformation</a>
|
||||
plugin, which rewrites the web client's index.html as it is served.
|
||||
JRay never modifies index.html on disk, so there is no fallback if
|
||||
that plugin is absent — only the overlay is affected, and every
|
||||
other JRay feature keeps working.
|
||||
</div>
|
||||
</div>
|
||||
<div id="JRayDependencyStatus" class="fieldDescription" style="margin:0 0 1.5em;padding:0.75em 1em;border-radius:0.25em;display:none;"></div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
@@ -131,6 +137,32 @@
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ---- Dependency status ----
|
||||
// Jellyfin cannot install a plugin's dependency, so the only thing
|
||||
// that closes the gap is saying so where an admin can act on it.
|
||||
function loadDependencyStatus() {
|
||||
var el = document.querySelector('#JRayDependencyStatus');
|
||||
return jrayApi('Status/Dependencies').then(function (status) {
|
||||
el.style.display = '';
|
||||
if (status.file_transformation_available) {
|
||||
el.style.background = 'rgba(82,168,82,0.15)';
|
||||
el.innerHTML = '<strong>File Transformation detected.</strong> '
|
||||
+ 'The pause overlay is served by rewriting index.html as it is '
|
||||
+ 'sent, leaving the file on disk untouched.';
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.background = 'rgba(220,160,60,0.18)';
|
||||
el.innerHTML = '<strong>File Transformation is not installed — the pause '
|
||||
+ 'overlay is disabled.</strong> Every other JRay feature is unaffected. '
|
||||
+ 'To enable it, add this repository in Dashboard → Plugins → '
|
||||
+ 'Repositories, then install "File Transformation" and restart:<br />'
|
||||
+ '<code>' + escapeHtml(status.file_transformation_manifest_url) + '</code>';
|
||||
}).catch(function () {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Coverage ----
|
||||
|
||||
var JRayCoverColors = {
|
||||
@@ -340,6 +372,7 @@
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
|
||||
loadDependencyStatus();
|
||||
populateValueSelect();
|
||||
loadRules();
|
||||
loadCoverage();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
using Jellyfin.Plugin.JRay.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Reports whether JRay's hard dependency on the File Transformation plugin is
|
||||
/// satisfied, for the configuration page to surface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Absent that plugin the pause overlay is disabled and every other JRay feature
|
||||
/// continues to work — so this is a status to display, not an error to raise.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Status")]
|
||||
[Authorize(Roles = "Administrator")]
|
||||
// TRACES: JR-023 | PR-004
|
||||
public class StatusController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the dependency status.
|
||||
/// </summary>
|
||||
/// <returns>The dependency status.</returns>
|
||||
[HttpGet("Dependencies")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<DependencyStatus> GetDependencies()
|
||||
{
|
||||
return Ok(new DependencyStatus
|
||||
{
|
||||
FileTransformationAvailable = Plugin.FileTransformationAvailable,
|
||||
OverlayEnabled = Plugin.OverlayEnabled,
|
||||
FileTransformationManifestUrl = FileTransformationRegistration.ManifestUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Whether JRay's one hard dependency is satisfied, and what to do if it is not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Jellyfin has no plugin dependency mechanism — a manifest cannot declare that
|
||||
/// another plugin is required, and nothing will install one. The gap is closed by
|
||||
/// telling the admin, on the page where they can act on it. A warning that exists
|
||||
/// only in the server log is one nobody reads.
|
||||
/// </remarks>
|
||||
public class DependencyStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the File Transformation plugin was
|
||||
/// found and JRay's overlay transformation registered with it.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_transformation_available")]
|
||||
public bool FileTransformationAvailable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the overlay is switched on in
|
||||
/// configuration. It is served only when this <i>and</i>
|
||||
/// <see cref="FileTransformationAvailable"/> hold.
|
||||
/// </summary>
|
||||
[JsonPropertyName("overlay_enabled")]
|
||||
public bool OverlayEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the repository manifest URL an admin adds to install the
|
||||
/// missing dependency.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_transformation_manifest_url")]
|
||||
public string FileTransformationManifestUrl { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace Jellyfin.Plugin.JRay;
|
||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly bool _usingFileTransformation;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||
@@ -30,8 +31,25 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
Instance = this;
|
||||
_logger = logger;
|
||||
|
||||
WebClientPatchService.Apply(ApplicationPaths, Configuration.EnableOverlay, _logger);
|
||||
ConfigurationChanged += (_, _) => WebClientPatchService.Apply(ApplicationPaths, Configuration.EnableOverlay, _logger);
|
||||
// The File Transformation plugin rewrites index.html as it is served.
|
||||
// Registration is unconditional: the transformation itself checks
|
||||
// EnableOverlay at request time, so toggling the setting takes effect
|
||||
// without re-registering.
|
||||
_usingFileTransformation = FileTransformationRegistration.TryRegister(_logger);
|
||||
|
||||
if (!_usingFileTransformation)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"JRay: the pause overlay is disabled because the File Transformation plugin " +
|
||||
"is not available. Install it from " +
|
||||
"https://github.com/IAmParadox27/jellyfin-plugin-file-transformation. " +
|
||||
"All other JRay features are unaffected.");
|
||||
}
|
||||
|
||||
// JRay never injects into index.html. This only ever *removes* a patch
|
||||
// left by an earlier version of JRay, identified by its own marker —
|
||||
// see SPEC.md JR-021/JR-022.
|
||||
WebClientPatchService.RemoveLegacyPatch(ApplicationPaths, _logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -45,6 +63,20 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// </summary>
|
||||
public static Plugin? Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the pause overlay should currently be
|
||||
/// served. Read by the File Transformation callback at request time, so
|
||||
/// toggling the setting takes effect without re-registering.
|
||||
/// </summary>
|
||||
internal static bool OverlayEnabled => Instance?.Configuration.EnableOverlay ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the File Transformation plugin was found
|
||||
/// at startup. When it was not, the overlay is disabled and every other JRay
|
||||
/// feature continues to work.
|
||||
/// </summary>
|
||||
internal static bool FileTransformationAvailable => Instance?._usingFileTransformation ?? false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text.Json;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Registers JRay's overlay script with the
|
||||
/// <see href="https://github.com/IAmParadox27/jellyfin-plugin-file-transformation">File Transformation</see>
|
||||
/// plugin, which rewrites <c>index.html</c> as it is served instead of
|
||||
/// modifying the file on disk. This is non-destructive and composes with
|
||||
/// other plugins that patch the same file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// File Transformation is referenced by reflection (rather than a NuGet
|
||||
/// package reference) so JRay still loads when it isn't installed. JRay must
|
||||
/// never bundle the assembly: a bundled copy would sit in a different
|
||||
/// <c>AssemblyLoadContext</c> from the real one, which is precisely the failure
|
||||
/// the reflection integration exists to avoid.
|
||||
///
|
||||
/// This is the mechanism JR-021 requires, but it does not by itself satisfy
|
||||
/// JR-021 — that requirement is a prohibition, and it stays unmet while
|
||||
/// <see cref="WebClientPatchService"/> can still write to disk.
|
||||
/// </remarks>
|
||||
// TRACES: JR-020, JR-023 | PR-004
|
||||
public static class FileTransformationRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// The marker comment written alongside the injected script tag, used to
|
||||
/// keep the transformation idempotent.
|
||||
/// </summary>
|
||||
internal const string Marker = "<!-- jray-overlay -->";
|
||||
|
||||
/// <summary>
|
||||
/// Repository manifest an admin adds to install the dependency. Surfaced on
|
||||
/// the configuration page rather than only in the log, since that is where
|
||||
/// it can be acted on.
|
||||
/// </summary>
|
||||
public const string ManifestUrl = "https://www.iamparadox.dev/jellyfin/plugins/manifest.json";
|
||||
|
||||
private const string PluginInterfaceTypeName = "Jellyfin.Plugin.FileTransformation.PluginInterface";
|
||||
private const string RegisterMethodName = "RegisterTransformation";
|
||||
private const string ScriptTag = "<script defer src=\"/Plugins/JRay/ClientScript\"></script>";
|
||||
private const string BodyClose = "</body>";
|
||||
|
||||
/// <summary>
|
||||
/// Stable id for JRay's index.html transformation. File Transformation
|
||||
/// keys registrations on this, so re-registering replaces rather than
|
||||
/// duplicates.
|
||||
/// </summary>
|
||||
private static readonly Guid TransformationId = Guid.Parse("2c9b5a41-6ad0-4c1e-9f7d-1d1e6b0d5a90");
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to register JRay's index.html transformation with the File
|
||||
/// Transformation plugin.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <returns><see langword="true"/> if the transformation was registered;
|
||||
/// <see langword="false"/> if the File Transformation plugin is not
|
||||
/// installed or its interface could not be invoked.</returns>
|
||||
public static bool TryRegister(ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
try
|
||||
{
|
||||
var registerMethod = ResolveRegisterMethod();
|
||||
if (registerMethod is null)
|
||||
{
|
||||
logger.LogInformation("JRay: File Transformation plugin not found; falling back to patching index.html on disk.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var payload = BuildPayload(registerMethod);
|
||||
registerMethod.Invoke(null, [payload]);
|
||||
|
||||
logger.LogInformation("JRay: registered index.html transformation with the File Transformation plugin.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is TargetInvocationException or InvalidOperationException or JsonException or MissingMethodException)
|
||||
{
|
||||
logger.LogWarning(ex, "JRay: failed to register with the File Transformation plugin; falling back to patching index.html on disk.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The transformation callback invoked by the File Transformation plugin.
|
||||
/// It is resolved by name via reflection, so the signature (public,
|
||||
/// static, single payload parameter, returns <see cref="string"/>) must
|
||||
/// not change.
|
||||
/// </summary>
|
||||
/// <param name="payload">The current state of the file being served.</param>
|
||||
/// <returns>The transformed file contents.</returns>
|
||||
public static string TransformIndexHtml(TransformationPayload payload)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(payload);
|
||||
|
||||
var contents = payload.Contents ?? string.Empty;
|
||||
if (!Plugin.OverlayEnabled || contents.Contains(Marker, StringComparison.Ordinal))
|
||||
{
|
||||
return contents;
|
||||
}
|
||||
|
||||
var index = contents.LastIndexOf(BodyClose, StringComparison.OrdinalIgnoreCase);
|
||||
if (index < 0)
|
||||
{
|
||||
return contents;
|
||||
}
|
||||
|
||||
return contents[..index]
|
||||
+ ScriptTag
|
||||
+ Marker
|
||||
+ "\n"
|
||||
+ contents[index..];
|
||||
}
|
||||
|
||||
private static MethodInfo? ResolveRegisterMethod()
|
||||
{
|
||||
var assembly = AssemblyLoadContext.All
|
||||
.SelectMany(context => context.Assemblies)
|
||||
.FirstOrDefault(candidate => candidate.FullName?.Contains(".FileTransformation", StringComparison.Ordinal) ?? false);
|
||||
|
||||
return assembly?
|
||||
.GetType(PluginInterfaceTypeName)?
|
||||
.GetMethod(RegisterMethodName, BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the registration payload. File Transformation expects a
|
||||
/// Newtonsoft <c>JObject</c>, which JRay does not reference, so the
|
||||
/// payload is serialized to JSON and parsed back through the type the
|
||||
/// target method actually declares.
|
||||
/// </summary>
|
||||
private static object BuildPayload(MethodInfo registerMethod)
|
||||
{
|
||||
// File Transformation matches this against Assembly.FullName exactly,
|
||||
// so it must be the full display name, not the short name.
|
||||
var assemblyName = typeof(FileTransformationRegistration).Assembly.FullName
|
||||
?? throw new InvalidOperationException("JRay assembly has no name.");
|
||||
|
||||
var json = JsonSerializer.Serialize(new
|
||||
{
|
||||
id = TransformationId.ToString("D", CultureInfo.InvariantCulture),
|
||||
fileNamePattern = "index.html",
|
||||
callbackAssembly = assemblyName,
|
||||
callbackClass = typeof(FileTransformationRegistration).FullName,
|
||||
callbackMethod = nameof(TransformIndexHtml)
|
||||
});
|
||||
|
||||
var payloadType = registerMethod.GetParameters().FirstOrDefault()?.ParameterType
|
||||
?? throw new InvalidOperationException("RegisterTransformation has no parameters.");
|
||||
|
||||
var parseMethod = payloadType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, [typeof(string)])
|
||||
?? throw new InvalidOperationException($"Cannot construct File Transformation payload of type '{payloadType.FullName}'.");
|
||||
|
||||
return parseMethod.Invoke(null, [json])
|
||||
?? throw new InvalidOperationException("File Transformation payload parsed to null.");
|
||||
}
|
||||
}
|
||||
@@ -6,26 +6,40 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Plugin.JRay.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Injects (or removes) a script tag in the web client's <c>index.html</c>
|
||||
/// that loads JRay's pause-overlay script. This follows the pattern used by
|
||||
/// other Jellyfin plugins (e.g. Intro Skipper) since there is no official
|
||||
/// plugin hook for player-overlay UI.
|
||||
/// Removes the pause-overlay script tag that an earlier version of JRay
|
||||
/// injected into the web client's <c>index.html</c> on disk.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>This class cannot inject.</b> JRay reaches the web client only through
|
||||
/// <see cref="FileTransformationRegistration"/>, which rewrites
|
||||
/// <c>index.html</c> as it is served. Patching the file on disk was removed
|
||||
/// rather than left switched off: the patch outlives an uninstall, a web-client
|
||||
/// upgrade discards it silently, and it races any other plugin patching the same
|
||||
/// file. An unreachable write path is also the one nobody runs, which is the one
|
||||
/// a later refactor re-enables by accident.
|
||||
///
|
||||
/// Removal remains because users upgrading from a version that did patch the
|
||||
/// file must not be left with a stale injection pointing at endpoints that have
|
||||
/// since changed. It keys on JRay's own <c><!-- jray-overlay --></c>
|
||||
/// marker, so it is unambiguous and touches nothing another plugin added.
|
||||
/// </remarks>
|
||||
// TRACES: JR-021, JR-022 | PR-004
|
||||
public static class WebClientPatchService
|
||||
{
|
||||
private const string Marker = "<!-- jray-overlay -->";
|
||||
private const string ScriptTag = "<script defer src=\"/Plugins/JRay/ClientScript\"></script>";
|
||||
private const string Injected = ScriptTag + Marker + "\n</body>";
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the web client's index.html either has or does not have the
|
||||
/// JRay overlay script injected, matching <paramref name="enableOverlay"/>.
|
||||
/// Removes a legacy on-disk overlay injection, if one is present. Safe to
|
||||
/// call on every startup: it is a no-op once the marker is gone.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">The Jellyfin application paths.</param>
|
||||
/// <param name="enableOverlay">Whether the overlay script should be present.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public static void Apply(IApplicationPaths applicationPaths, bool enableOverlay, ILogger logger)
|
||||
public static void RemoveLegacyPatch(IApplicationPaths applicationPaths, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(applicationPaths);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var indexPath = Path.Combine(applicationPaths.WebPath, "index.html");
|
||||
|
||||
try
|
||||
@@ -37,36 +51,38 @@ public static class WebClientPatchService
|
||||
}
|
||||
|
||||
var html = File.ReadAllText(indexPath);
|
||||
var hasMarker = html.Contains(Marker, StringComparison.Ordinal);
|
||||
if (!html.Contains(Marker, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableOverlay && !hasMarker)
|
||||
{
|
||||
var patched = ReplaceLast(html, "</body>", Injected);
|
||||
File.WriteAllText(indexPath, patched);
|
||||
logger.LogInformation("JRay: injected pause-overlay script into {Path}", indexPath);
|
||||
}
|
||||
else if (!enableOverlay && hasMarker)
|
||||
{
|
||||
var patched = html.Replace(ScriptTag + Marker + "\n", string.Empty, StringComparison.Ordinal)
|
||||
.Replace(ScriptTag + Marker, string.Empty, StringComparison.Ordinal);
|
||||
File.WriteAllText(indexPath, patched);
|
||||
logger.LogInformation("JRay: removed pause-overlay script from {Path}", indexPath);
|
||||
}
|
||||
var cleaned = RemoveInjection(html);
|
||||
File.WriteAllText(indexPath, cleaned);
|
||||
logger.LogInformation(
|
||||
"JRay: removed a pause-overlay script left in {Path} by an earlier version. "
|
||||
+ "JRay no longer modifies this file; the overlay is served through File Transformation.",
|
||||
indexPath);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
logger.LogWarning(ex, "JRay: failed to patch web client index.html at {Path}", indexPath);
|
||||
logger.LogWarning(ex, "JRay: failed to remove the legacy overlay patch from {Path}", indexPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReplaceLast(string source, string find, string replace)
|
||||
/// <summary>
|
||||
/// Strips the marked script tag from the document.
|
||||
/// </summary>
|
||||
/// <param name="html">The document contents.</param>
|
||||
/// <returns>The contents with JRay's injection removed.</returns>
|
||||
internal static string RemoveInjection(string html)
|
||||
{
|
||||
var index = source.LastIndexOf(find, StringComparison.Ordinal);
|
||||
if (index < 0)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(html);
|
||||
|
||||
return source[..index] + replace + source[(index + find.Length)..];
|
||||
// The trailing newline is stripped with the tag when present, so removing
|
||||
// a patch restores the document byte-for-byte rather than leaving a blank
|
||||
// line that accumulates across upgrades.
|
||||
return html
|
||||
.Replace(ScriptTag + Marker + "\n", string.Empty, StringComparison.Ordinal)
|
||||
.Replace(ScriptTag + Marker, string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,25 @@ https://gitea.tourolle.paris/dtourolle/jRay/raw/branch/master/manifest.json
|
||||
|
||||
Then install "JRay" from the plugin catalog and restart Jellyfin.
|
||||
|
||||
### Required for the overlay: File Transformation
|
||||
|
||||
JRay's pause overlay needs a script tag in the web client's `index.html`.
|
||||
Install [File Transformation](https://github.com/IAmParadox27/jellyfin-plugin-file-transformation)
|
||||
(repository `https://www.iamparadox.dev/jellyfin/plugins/manifest.json`) **before**
|
||||
installing JRay. It rewrites the page as it is served, so the file on disk is
|
||||
never touched — that survives server upgrades and coexists with other plugins
|
||||
patching the same file.
|
||||
|
||||
**There is no fallback.** JRay never edits `index.html` on disk: a patch there
|
||||
outlives an uninstall, is silently discarded by a web-client upgrade, and races
|
||||
any other plugin touching the file. Without File Transformation the pause
|
||||
overlay is simply disabled — every other JRay feature works normally, and the
|
||||
plugin's configuration page tells you what is missing and how to install it.
|
||||
|
||||
Upgrading from an older JRay that did patch `index.html`? It removes its own
|
||||
patch on startup, so there is nothing to clean up by hand. See
|
||||
[SPEC.md](SPEC.md) JR-021/JR-022.
|
||||
|
||||
## Features
|
||||
|
||||
- **Pause overlay** — pause a movie or episode in the web client and see the
|
||||
|
||||
+6
-1
@@ -19,4 +19,9 @@ dotnet_configuration: "Release"
|
||||
dotnet_framework: "net9.0"
|
||||
project: "Jellyfin.Plugin.JRay/Jellyfin.Plugin.JRay.csproj"
|
||||
changelog: >
|
||||
Initial scaffold
|
||||
The pause overlay is now served through the File Transformation plugin, which
|
||||
rewrites the web client's index.html as it is sent. JRay no longer edits that
|
||||
file on disk and there is no fallback: without File Transformation the overlay
|
||||
is disabled and every other feature works normally. Any stale on-disk patch
|
||||
left by an earlier JRay is removed on startup, and the plugin's configuration
|
||||
page now reports whether the dependency is satisfied.
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# JR-021 — jRay never injects into index.html on disk.
|
||||
#
|
||||
# This is a requirement to *not do* something, so it is verified by absence.
|
||||
# A unit test cannot show that no code path writes the tag; a grep can.
|
||||
#
|
||||
# The prohibition is on injection, not on writing: JR-022's migration must write
|
||||
# to index.html in order to remove a legacy patch. So the check is for code that
|
||||
# *adds* the script tag, not for File.Write* generally.
|
||||
#
|
||||
# TRACES: JR-021 | PR-004
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
src="Jellyfin.Plugin.JRay"
|
||||
status=0
|
||||
|
||||
# The injection is "script tag + marker" written back to the file. The removal
|
||||
# path also names both, so match on the concatenation that builds a patched
|
||||
# document rather than on the constants themselves.
|
||||
if grep -rn --include='*.cs' -E '(ScriptTag|Injected)[[:space:]]*\+.*BodyClose|ReplaceLast|"</body>"[[:space:]]*,' "$src" \
|
||||
| grep -v 'FileTransformationRegistration.cs'; then
|
||||
echo "FAIL (JR-021): index.html injection logic found outside the File Transformation callback." >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
# WebClientPatchService is removal-only. Any write there must be the cleaned
|
||||
# document; a write of a *patched* one is the regression this guards.
|
||||
if grep -n -E 'WriteAllText\((?!.*cleaned)' -P "$src/Services/WebClientPatchService.cs" >/dev/null 2>&1; then
|
||||
echo "FAIL (JR-021): WebClientPatchService writes something other than the cleaned document." >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
# The disk-patching entry point must not come back.
|
||||
if grep -rn --include='*.cs' -E '\bWebClientPatchService\.Apply\b' "$src"; then
|
||||
echo "FAIL (JR-021): the injecting Apply() entry point has been reintroduced." >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "OK (JR-021): no on-disk injection path."
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
Reference in New Issue
Block a user