Inject skip button into web interface
Text for the skip button and auto skip message can now be customized Closes #104, 105
This commit is contained in:
parent
ce52a0b979
commit
f4e84d4d07
5
ACKNOWLEDGEMENTS.md
Normal file
5
ACKNOWLEDGEMENTS.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Intro Skipper is made possible by the following open source projects:
|
||||||
|
|
||||||
|
* License: MIT
|
||||||
|
* [acoustid-match](https://github.com/dnknth/acoustid-match)
|
||||||
|
* [JellyScrub](https://github.com/nicknsy/jellyscrub)
|
@ -163,18 +163,21 @@ public class AutoSkip : IServerEntryPoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify the user that an introduction is being skipped for them.
|
// Notify the user that an introduction is being skipped for them.
|
||||||
_sessionManager.SendMessageCommand(
|
var notificationText = Plugin.Instance!.Configuration.AutoSkipNotificationText;
|
||||||
|
if (!string.IsNullOrWhiteSpace(notificationText))
|
||||||
|
{
|
||||||
|
_sessionManager.SendMessageCommand(
|
||||||
session.Id,
|
session.Id,
|
||||||
session.Id,
|
session.Id,
|
||||||
new MessageCommand()
|
new MessageCommand()
|
||||||
{
|
{
|
||||||
Header = string.Empty, // some clients require header to be a string instead of null
|
Header = string.Empty, // some clients require header to be a string instead of null
|
||||||
Text = "Automatically skipped intro",
|
Text = notificationText,
|
||||||
TimeoutMs = 2000,
|
TimeoutMs = 2000,
|
||||||
},
|
},
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
// Send the seek command
|
|
||||||
_logger.LogDebug("Sending seek command to {Session}", deviceId);
|
_logger.LogDebug("Sending seek command to {Session}", deviceId);
|
||||||
|
|
||||||
var introEnd = (long)intro.IntroEnd - Plugin.Instance!.Configuration.SecondsOfIntroToPlay;
|
var introEnd = (long)intro.IntroEnd - Plugin.Instance!.Configuration.SecondsOfIntroToPlay;
|
||||||
|
@ -74,6 +74,11 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
|
|
||||||
// ===== Playback settings =====
|
// ===== Playback settings =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to show the skip intro button.
|
||||||
|
/// </summary>
|
||||||
|
public bool SkipButtonVisible { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether introductions should be automatically skipped.
|
/// Gets or sets a value indicating whether introductions should be automatically skipped.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -127,4 +132,16 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// Gets or sets the minimum duration of audio (in seconds) that is considered silent.
|
/// Gets or sets the minimum duration of audio (in seconds) that is considered silent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double SilenceDetectionMinimumDuration { get; set; } = 0.33;
|
public double SilenceDetectionMinimumDuration { get; set; } = 0.33;
|
||||||
|
|
||||||
|
// ===== Localization support =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text to display in the Skip Intro button.
|
||||||
|
/// </summary>
|
||||||
|
public string SkipButtonText { get; set; } = "Skip Intro";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the notification text sent after automatically skipping an introduction.
|
||||||
|
/// </summary>
|
||||||
|
public string AutoSkipNotificationText { get; set; } = "Automatically skipped intro";
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
namespace ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User interface configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class UserInterfaceConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UserInterfaceConfiguration"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="visible">Skip button visibility.</param>
|
||||||
|
/// <param name="text">Skip button text.</param>
|
||||||
|
public UserInterfaceConfiguration(bool visible, string text)
|
||||||
|
{
|
||||||
|
SkipButtonVisible = visible;
|
||||||
|
SkipButtonText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to show the skip intro button.
|
||||||
|
/// </summary>
|
||||||
|
public bool SkipButtonVisible { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text to display in the skip intro button.
|
||||||
|
/// </summary>
|
||||||
|
public string SkipButtonText { get; set; }
|
||||||
|
}
|
@ -194,6 +194,19 @@
|
|||||||
<fieldset class="verticalSection-extrabottompadding">
|
<fieldset class="verticalSection-extrabottompadding">
|
||||||
<legend>Playback</legend>
|
<legend>Playback</legend>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label class="emby-checkbox-label">
|
||||||
|
<input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
|
||||||
|
<span>Show skip intro button</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="fieldDescription">
|
||||||
|
If checked, a skip button will be displayed at the start of an episode's introduction.
|
||||||
|
<strong>This setting only applies to the web interface.</strong>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="AutoSkip" type="checkbox" is="emby-checkbox" />
|
<input id="AutoSkip" type="checkbox" is="emby-checkbox" />
|
||||||
@ -201,7 +214,8 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, intros will be automatically skipped. If you access Jellyfin through a reverse proxy, it must be configured to proxy web
|
If checked, intros will be automatically skipped. If you access Jellyfin through a
|
||||||
|
reverse proxy, it must be configured to proxy web
|
||||||
sockets.<br />
|
sockets.<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -246,6 +260,30 @@
|
|||||||
Seconds of introduction that should be played. Defaults to 2.
|
Seconds of introduction that should be played. Defaults to 2.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>User Interface Customization</summary>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel" for="SkipButtonText">
|
||||||
|
Skip intro button text
|
||||||
|
</label>
|
||||||
|
<input id="SkipButtonText" type="text" is="emby-input" />
|
||||||
|
<div class="fieldDescription">
|
||||||
|
Text to display in the skip intro button.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel" for="AutoSkipNotificationText">
|
||||||
|
Automatic skip notification message
|
||||||
|
</label>
|
||||||
|
<input id="AutoSkipNotificationText" type="text" is="emby-input" />
|
||||||
|
<div class="fieldDescription">
|
||||||
|
Message sent to a user after automatically skipping an introduction. Leave blank to skip sending a notification.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -395,13 +433,16 @@
|
|||||||
// internals
|
// internals
|
||||||
"SilenceDetectionMaximumNoise",
|
"SilenceDetectionMaximumNoise",
|
||||||
"SilenceDetectionMinimumDuration",
|
"SilenceDetectionMinimumDuration",
|
||||||
|
"SkipButtonText",
|
||||||
|
"AutoSkipNotificationText"
|
||||||
]
|
]
|
||||||
|
|
||||||
var booleanConfigurationFields = [
|
var booleanConfigurationFields = [
|
||||||
"AnalyzeSeasonZero",
|
"AnalyzeSeasonZero",
|
||||||
"RegenerateEdlFiles",
|
"RegenerateEdlFiles",
|
||||||
"AutoSkip",
|
"AutoSkip",
|
||||||
"SkipFirstEpisode"
|
"SkipFirstEpisode",
|
||||||
|
"SkipButtonVisible",
|
||||||
]
|
]
|
||||||
|
|
||||||
// visualizer elements
|
// visualizer elements
|
||||||
|
224
ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js
Normal file
224
ConfusedPolarBear.Plugin.IntroSkipper/Configuration/inject.js
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
let nowPlayingItemSkipSegments = {};
|
||||||
|
let videoPlayer = {};
|
||||||
|
|
||||||
|
function d(msg) {
|
||||||
|
console.debug("[intro skipper]", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setup event listeners */
|
||||||
|
function setup() {
|
||||||
|
document.addEventListener("viewshow", viewshow);
|
||||||
|
d("Registered hooks");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler that runs whenever the current view changes.
|
||||||
|
* Used to detect the start of video playback.
|
||||||
|
*/
|
||||||
|
function viewshow() {
|
||||||
|
const location = window.location.hash;
|
||||||
|
d("Location changed to " + location);
|
||||||
|
|
||||||
|
if (location !== "#!/video") {
|
||||||
|
d("Ignoring location change");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d("Adding button CSS and element");
|
||||||
|
injectSkipButtonCss();
|
||||||
|
injectSkipButtonElement();
|
||||||
|
|
||||||
|
d("Hooking video timeupdate");
|
||||||
|
videoPlayer = document.querySelector("video");
|
||||||
|
videoPlayer.addEventListener("timeupdate", videoPositionChanged);
|
||||||
|
|
||||||
|
d("Getting timestamps of introduction");
|
||||||
|
getIntroTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get skip button UI configuration */
|
||||||
|
async function getUserInterfaceConfiguration() {
|
||||||
|
const reqInit = {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/Intros/UserInterfaceConfiguration", reqInit);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the CSS used by the skip intro button.
|
||||||
|
* Calling this function is a no-op if the CSS has already been injected.
|
||||||
|
*/
|
||||||
|
function injectSkipButtonCss() {
|
||||||
|
if (testElement("style#introSkipperCss"))
|
||||||
|
{
|
||||||
|
d("CSS already added");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d("Adding CSS");
|
||||||
|
|
||||||
|
let styleElement = document.createElement("style");
|
||||||
|
styleElement.id = "introSkipperCss";
|
||||||
|
styleElement.innerText = `
|
||||||
|
@media (hover:hover) and (pointer:fine) {
|
||||||
|
#skipIntro .paper-icon-button-light:hover:not(:disabled) {
|
||||||
|
color: black !important;
|
||||||
|
background-color: rgba(47, 93, 98, 0) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#skipIntro.upNextContainer {
|
||||||
|
width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skipIntro {
|
||||||
|
padding: 0 1px;
|
||||||
|
position: absolute;
|
||||||
|
right: 10em;
|
||||||
|
bottom: 9em;
|
||||||
|
background-color: rgba(25, 25, 25, 0.66);
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 0px;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: inset 0 0 0 0 #f9f9f9;
|
||||||
|
-webkit-transition: ease-out 0.4s;
|
||||||
|
-moz-transition: ease-out 0.4s;
|
||||||
|
transition: ease-out 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
#skipIntro {
|
||||||
|
right: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#skipIntro:hover {
|
||||||
|
box-shadow: inset 400px 0 0 0 #f9f9f9;
|
||||||
|
-webkit-transition: ease-in 1s;
|
||||||
|
-moz-transition: ease-in 1s;
|
||||||
|
transition: ease-in 1s;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.querySelector("head").appendChild(styleElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject the skip intro button into the video player.
|
||||||
|
* Calling this function is a no-op if the CSS has already been injected.
|
||||||
|
*/
|
||||||
|
async function injectSkipButtonElement() {
|
||||||
|
if (testElement(".btnSkipIntro")) {
|
||||||
|
d("Button already added");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d("Adding button");
|
||||||
|
|
||||||
|
let config = await getUserInterfaceConfiguration();
|
||||||
|
if (!config.SkipButtonVisible) {
|
||||||
|
d("Not adding button: not visible");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the skip button div
|
||||||
|
const button = document.createElement("div");
|
||||||
|
button.id = "skipIntro"
|
||||||
|
button.classList.add("hide");
|
||||||
|
button.addEventListener("click", skipIntro);
|
||||||
|
button.innerHTML = `
|
||||||
|
<button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light">
|
||||||
|
<span id="btnSkipIntroText"></span>
|
||||||
|
<span class="material-icons skip_next"></span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Alternative workaround for #44. Jellyfin's video component registers a global click handler
|
||||||
|
* (located at src/controllers/playback/video/index.js:1492) that pauses video playback unless
|
||||||
|
* the clicked element has a parent with the class "videoOsdBottom" or "upNextContainer".
|
||||||
|
*/
|
||||||
|
button.classList.add("upNextContainer");
|
||||||
|
|
||||||
|
// Append the button to the video OSD
|
||||||
|
let controls = document.querySelector("div#videoOsdPage");
|
||||||
|
controls.appendChild(button);
|
||||||
|
|
||||||
|
document.querySelector("#btnSkipIntroText").textContent = config.SkipButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the introduction timestamps of the currently playing item. */
|
||||||
|
async function getIntroTimestamps() {
|
||||||
|
let id = await getNowPlayingItemId();
|
||||||
|
|
||||||
|
const address = ApiClient.serverAddress();
|
||||||
|
|
||||||
|
const url = `${address}/Episode/${id}/IntroTimestamps`;
|
||||||
|
const reqInit = {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `MediaBrowser Token=${ApiClient.accessToken()}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(url, reqInit).then(r => {
|
||||||
|
if (!r.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.json();
|
||||||
|
}).then(intro => {
|
||||||
|
nowPlayingItemSkipSegments = intro;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Playback position changed, check if the skip button needs to be displayed. */
|
||||||
|
function videoPositionChanged() {
|
||||||
|
// Ensure a skip segment was found.
|
||||||
|
if (!nowPlayingItemSkipSegments?.Valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipButton = document.querySelector("#skipIntro");
|
||||||
|
if (!skipButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = videoPlayer.currentTime;
|
||||||
|
if (position >= nowPlayingItemSkipSegments.ShowSkipPromptAt &&
|
||||||
|
position < nowPlayingItemSkipSegments.HideSkipPromptAt) {
|
||||||
|
skipButton.classList.remove("hide");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipButton.classList.add("hide");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seeks to the end of the intro. */
|
||||||
|
function skipIntro(e) {
|
||||||
|
d("Skipping intro");
|
||||||
|
d(nowPlayingItemSkipSegments);
|
||||||
|
videoPlayer.currentTime = nowPlayingItemSkipSegments.IntroEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Looks up the ID of the currently playing item. */
|
||||||
|
async function getNowPlayingItemId() {
|
||||||
|
d("Looking up ID of currently playing item");
|
||||||
|
|
||||||
|
let id = await ApiClient.getCurrentUserId();
|
||||||
|
|
||||||
|
let sessions = await ApiClient.getSessions();
|
||||||
|
let filtered = sessions.filter(x => (x.UserId === id) && x.NowPlayingItem);
|
||||||
|
|
||||||
|
d("Filtered " + sessions.length + " sessions down to " + filtered.length);
|
||||||
|
|
||||||
|
return filtered[0].NowPlayingItem.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests if an element with the provided selector exists. */
|
||||||
|
function testElement(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
|
setup();
|
@ -24,6 +24,7 @@
|
|||||||
<None Remove="Configuration\configPage.html" />
|
<None Remove="Configuration\configPage.html" />
|
||||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||||
<EmbeddedResource Include="Configuration\visualizer.js" />
|
<EmbeddedResource Include="Configuration\visualizer.js" />
|
||||||
|
<EmbeddedResource Include="Configuration\inject.js" />
|
||||||
<EmbeddedResource Include="Configuration\version.txt" />
|
<EmbeddedResource Include="Configuration\version.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -104,4 +105,16 @@ public class SkipIntroController : ControllerBase
|
|||||||
|
|
||||||
return intros;
|
return intros;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user interface configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">UserInterfaceConfiguration returned.</response>
|
||||||
|
/// <returns>UserInterfaceConfiguration.</returns>
|
||||||
|
[Route("Intros/UserInterfaceConfiguration")]
|
||||||
|
public ActionResult<UserInterfaceConfiguration> GetUserInterfaceConfiguration()
|
||||||
|
{
|
||||||
|
var config = Plugin.Instance!.Configuration;
|
||||||
|
return new UserInterfaceConfiguration(config.SkipButtonVisible, config.SkipButtonText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
@ -47,9 +48,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
FingerprintCachePath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "cache");
|
|
||||||
FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
|
FFmpegPath = serverConfiguration.GetEncodingOptions().EncoderAppPathDisplay;
|
||||||
_introPath = Path.Join(applicationPaths.PluginConfigurationsPath, "intros", "intros.xml");
|
|
||||||
|
var introsDirectory = Path.Join(applicationPaths.PluginConfigurationsPath, "intros");
|
||||||
|
FingerprintCachePath = Path.Join(introsDirectory, "cache");
|
||||||
|
_introPath = Path.Join(introsDirectory, "intros.xml");
|
||||||
|
|
||||||
// Create the base & cache directories (if needed).
|
// Create the base & cache directories (if needed).
|
||||||
if (!Directory.Exists(FingerprintCachePath))
|
if (!Directory.Exists(FingerprintCachePath))
|
||||||
@ -68,6 +71,35 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to load introduction timestamps: {Exception}", ex);
|
_logger.LogWarning("Unable to load introduction timestamps: {Exception}", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject the skip intro button code into the web interface.
|
||||||
|
var indexPath = Path.Join(applicationPaths.WebPath, "index.html");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InjectSkipButton(indexPath, Path.Join(introsDirectory, "index-pre-skip-button.html"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// WarningManager.SetFlag(PluginWarning.UnableToAddSkipButton);
|
||||||
|
|
||||||
|
if (ex is UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
var suggestion = OperatingSystem.IsLinux() ?
|
||||||
|
"running `sudo chown jellyfin PATH` (if this is a native installation)" :
|
||||||
|
"changing the permissions of PATH";
|
||||||
|
|
||||||
|
suggestion = suggestion.Replace("PATH", indexPath, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
_logger.LogError(
|
||||||
|
"Failed to add skip button to web interface. Try {Suggestion} and restarting the server. Error: {Error}",
|
||||||
|
suggestion,
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("Unknown error encountered while adding skip button: {Error}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -148,6 +180,29 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<PluginPageInfo> GetPages()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = this.Name,
|
||||||
|
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
|
||||||
|
},
|
||||||
|
new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = "visualizer.js",
|
||||||
|
EmbeddedResourcePath = GetType().Namespace + ".Configuration.visualizer.js"
|
||||||
|
},
|
||||||
|
new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = "skip-intro-button.js",
|
||||||
|
EmbeddedResourcePath = GetType().Namespace + ".Configuration.inject.js"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
internal BaseItem GetItem(Guid id)
|
internal BaseItem GetItem(Guid id)
|
||||||
{
|
{
|
||||||
return _libraryManager.GetItemById(id);
|
return _libraryManager.GetItemById(id);
|
||||||
@ -176,26 +231,50 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<PluginPageInfo> GetPages()
|
|
||||||
{
|
|
||||||
return new[]
|
|
||||||
{
|
|
||||||
new PluginPageInfo
|
|
||||||
{
|
|
||||||
Name = this.Name,
|
|
||||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html"
|
|
||||||
},
|
|
||||||
new PluginPageInfo
|
|
||||||
{
|
|
||||||
Name = "visualizer.js",
|
|
||||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.visualizer.js"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnConfigurationChanged(object? sender, BasePluginConfiguration e)
|
private void OnConfigurationChanged(object? sender, BasePluginConfiguration e)
|
||||||
{
|
{
|
||||||
AutoSkipChanged?.Invoke(this, EventArgs.Empty);
|
AutoSkipChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inject the skip button script into the web interface.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="indexPath">Full path to index.html.</param>
|
||||||
|
/// <param name="backupPath">Full path to create a backup of index.html at.</param>
|
||||||
|
private void InjectSkipButton(string indexPath, string backupPath)
|
||||||
|
{
|
||||||
|
// Parts of this code are based off of JellyScrub's script injection code.
|
||||||
|
// https://github.com/nicknsy/jellyscrub/blob/4ce806f602988a662cfe3cdbaac35ee8046b7ec4/Nick.Plugin.Jellyscrub/JellyscrubPlugin.cs
|
||||||
|
|
||||||
|
_logger.LogInformation("Adding skip button to {Path}", indexPath);
|
||||||
|
|
||||||
|
_logger.LogDebug("Reading index.html from {Path}", indexPath);
|
||||||
|
var contents = File.ReadAllText(indexPath);
|
||||||
|
_logger.LogDebug("Successfully read index.html");
|
||||||
|
|
||||||
|
var scriptTag = "<script src=\"configurationpage?name=skip-intro-button.js\"></script>";
|
||||||
|
|
||||||
|
// Only inject the script tag once
|
||||||
|
if (contents.Contains(scriptTag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Skip button already added");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup the original version of the web interface
|
||||||
|
_logger.LogInformation("Backing up index.html to {Backup}", backupPath);
|
||||||
|
File.WriteAllText(backupPath, contents);
|
||||||
|
|
||||||
|
// Inject a link to the script at the end of the <head> section.
|
||||||
|
// A regex is used here to ensure the replacement is only done once.
|
||||||
|
_logger.LogDebug("Injecting script tag");
|
||||||
|
var headEnd = new Regex("</head>", RegexOptions.IgnoreCase);
|
||||||
|
contents = headEnd.Replace(contents, scriptTag + "</head>", 1);
|
||||||
|
|
||||||
|
// Write the modified file contents
|
||||||
|
_logger.LogDebug("Saving modified file");
|
||||||
|
File.WriteAllText(indexPath, contents);
|
||||||
|
|
||||||
|
_logger.LogInformation("Skip intro button successfully added to web interface");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user