Merge pull request #61 from RepoDevil/master

Addressing compatibility issues with JF
This commit is contained in:
TwistedUmbrellaX 2024-03-05 10:19:39 -05:00 committed by GitHub
commit ce4f4e278e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 107 additions and 77 deletions

View File

@ -149,12 +149,21 @@ public class ChapterAnalyzer : IMediaFileAnalyzer
continue;
}
// Check for possibility of overlapping keywords
var overlap = Regex.IsMatch(
next.Name,
expression,
RegexOptions.None,
TimeSpan.FromSeconds(1));
if (overlap)
{
continue;
}
matchingChapter = new(episode.EpisodeId, currentRange);
_logger.LogTrace("{Base}: okay", baseMessage);
if (i > 0)
{
break;
}
break;
}
return matchingChapter;

View File

@ -275,10 +275,10 @@ public class ChromaprintAnalyzer : IMediaFileAnalyzer
{
var modifiedPoint = (uint)(originalPoint + i);
if (rhsIndex.TryGetValue(modifiedPoint, out var value))
if (rhsIndex.TryGetValue(modifiedPoint, out var rhsModifiedPoint))
{
var lhsFirst = (int)lhsIndex[originalPoint];
var rhsFirst = (int)value;
var rhsFirst = (int)rhsModifiedPoint;
indexShifts.Add(rhsFirst - lhsFirst);
}
}

View File

@ -0,0 +1,36 @@
namespace ConfusedPolarBear.Plugin.IntroSkipper;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
/// <summary>
/// Chapter name analyzer.
/// </summary>
public class SegmentAnalyzer : IMediaFileAnalyzer
{
private ILogger<SegmentAnalyzer> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SegmentAnalyzer"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
public SegmentAnalyzer(ILogger<SegmentAnalyzer> logger)
{
_logger = logger;
}
/// <inheritdoc />
public ReadOnlyCollection<QueuedEpisode> AnalyzeMediaFiles(
ReadOnlyCollection<QueuedEpisode> analysisQueue,
AnalysisMode mode,
CancellationToken cancellationToken)
{
return analysisQueue;
}
}

View File

@ -17,11 +17,6 @@ public class PluginConfiguration : BasePluginConfiguration
// ===== Analysis settings =====
/// <summary>
/// Gets or sets a value indicating whether the episode's fingerprint should be cached to the filesystem.
/// </summary>
public bool CacheFingerprints { get; set; } = true;
/// <summary>
/// Gets or sets the max degree of parallelism used when analyzing episodes.
/// </summary>
@ -37,6 +32,16 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary>
public bool AnalyzeSeasonZero { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether the episode's fingerprint should be cached to the filesystem.
/// </summary>
public bool CacheFingerprints { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether analysis will use Chromaprint to determine fingerprints.
/// </summary>
public bool UseChromaprint { get; set; } = true;
// ===== EDL handling =====
/// <summary>

View File

@ -30,11 +30,14 @@
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
<span>Analyze show extras</span>
<span>Analyze season 0</span>
</label>
<div class="fieldDescription">
Analyze show extras (specials).
If checked, season 0 (specials / extras) will be included in analysis.
<br />
Note: Shows containing both a specials and extra folder will identify extras as season 0
and ignore specials, regardless of this setting.
</div>
</div>
@ -44,7 +47,7 @@
</label>
<input id="MaxParallelism" type="number" is="emby-input" min="1" />
<div class="fieldDescription">
Maximum degree of parallelism to use when analyzing episodes.
Maximum number of simultaneous async episode analysis operations.
</div>
</div>
@ -210,6 +213,20 @@
<summary>Process Configuration</summary>
<br/>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="UseChromaprint" type="checkbox" is="emby-checkbox" />
<span>Chromaprint analysis</span>
</label>
<div class="fieldDescription">
If checked, analysis will use Chromaprint to compare episode audio and identify intros.
<br />
<strong>WARNING: Disabling this option may result in incomplete or innaccurate analysis!</strong>
<br />
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
@ -565,6 +582,7 @@
var booleanConfigurationFields = [
"AnalyzeSeasonZero",
"RegenerateEdlFiles",
"UseChromaprint",
"CacheFingerprints",
"AutoSkip",
"SkipFirstEpisode",
@ -727,10 +745,10 @@
rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
if (lhs === undefined) {
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints missing!\n";
timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
}
if (rhs === undefined) {
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints missing!";
timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
}
if (timestampError.value == "") {
timestampError.style.display = "none";

View File

@ -1,51 +1,39 @@
let introSkipper = {
skipSegments: {},
videoPlayer: {},
// .bind() is used here to prevent illegal invocation errors
originalFetch: window.fetch.bind(window),
};
introSkipper.d = function (msg) {
console.debug("[intro skipper]", msg);
console.debug("[intro skipper] ", msg);
}
/** Setup event listeners */
introSkipper.setup = function () {
document.addEventListener("viewshow", introSkipper.viewShow);
window.fetch = introSkipper.fetchWrapper;
introSkipper.d("Registered hooks");
}
/** Wrapper around fetch() that retrieves skip segments for the currently playing item. */
introSkipper.fetchWrapper = async function (...args) {
// Based on JellyScrub's trickplay.js
let [resource, options] = args;
let response = await introSkipper.originalFetch(resource, options);
// Bail early if this isn't a playback info URL
try {
let path = new URL(resource).pathname;
if (!path.includes("/PlaybackInfo")) {
return response;
}
introSkipper.d("retrieving skip segments from URL");
if (!path.includes("/PlaybackInfo")) { return response; }
introSkipper.d("Retrieving skip segments from URL");
introSkipper.d(path);
let id = path.split("/")[2];
introSkipper.skipSegments = await introSkipper.secureFetch(`Episode/${id}/IntroSkipperSegments`);
introSkipper.d("successfully retrieved skip segments");
introSkipper.d("Successfully retrieved skip segments");
introSkipper.d(introSkipper.skipSegments);
}
catch (e) {
console.error("unable to get skip segments from", resource, e);
console.error("Unable to get skip segments from", resource, e);
}
return response;
}
/**
* Event handler that runs whenever the current view changes.
* Used to detect the start of video playback.
@ -53,21 +41,17 @@ introSkipper.fetchWrapper = async function (...args) {
introSkipper.viewShow = function () {
const location = window.location.hash;
introSkipper.d("Location changed to " + location);
if (location !== "#!/video") {
introSkipper.d("Ignoring location change");
return;
}
introSkipper.d("Adding button CSS and element");
introSkipper.injectCss();
introSkipper.injectButton();
introSkipper.d("Hooking video timeupdate");
introSkipper.videoPlayer = document.querySelector("video");
introSkipper.videoPlayer.addEventListener("timeupdate", introSkipper.videoPositionChanged);
}
/**
* Injects the CSS used by the skip intro button.
* Calling this function is a no-op if the CSS has already been injected.
@ -77,9 +61,7 @@ introSkipper.injectCss = function () {
introSkipper.d("CSS already added");
return;
}
introSkipper.d("Adding CSS");
let styleElement = document.createElement("style");
styleElement.id = "introSkipperCss";
styleElement.innerText = `
@ -130,7 +112,6 @@ introSkipper.injectCss = function () {
`;
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.
@ -141,20 +122,16 @@ introSkipper.injectButton = async function () {
if (preExistingButton) {
preExistingButton.style.display = "none";
}
if (introSkipper.testElement(".btnSkipIntro.injected")) {
introSkipper.d("Button already added");
return;
}
introSkipper.d("Adding button");
let config = await introSkipper.secureFetch("Intros/UserInterfaceConfiguration");
if (!config.SkipButtonVisible) {
introSkipper.d("Not adding button: not visible");
return;
}
// Construct the skip button div
const button = document.createElement("div");
button.id = "skipIntro"
@ -168,25 +145,21 @@ introSkipper.injectButton = async function () {
`;
button.dataset["intro_text"] = config.SkipButtonIntroText;
button.dataset["credits_text"] = config.SkipButtonEndCreditsText;
/*
* 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);
}
/** Tests if the OSD controls are visible. */
introSkipper.osdVisible = function () {
const osd = document.querySelector("div.videoOsdBottom");
return osd ? !osd.classList.contains("hide") : false;
}
/** Get the currently playing skippable segment. */
introSkipper.getCurrentSegment = function (position) {
for (let key in introSkipper.skipSegments) {
@ -196,71 +169,49 @@ introSkipper.getCurrentSegment = function (position) {
return segment;
}
}
return { "SegmentType": "None" };
}
/** Playback position changed, check if the skip button needs to be displayed. */
introSkipper.videoPositionChanged = function () {
const skipButton = document.querySelector("#skipIntro");
if (!skipButton) {
return;
}
const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
switch (segment["SegmentType"]) {
case "None":
skipButton.classList.add("hide");
return;
case "Introduction":
skipButton.querySelector("#btnSkipSegmentText").textContent =
skipButton.dataset["intro_text"];
break;
case "Credits":
skipButton.querySelector("#btnSkipSegmentText").textContent =
skipButton.dataset["credits_text"];
break;
}
skipButton.classList.remove("hide");
}
/** Seeks to the end of the intro. */
introSkipper.doSkip = function (e) {
introSkipper.d("Skipping intro");
introSkipper.d(introSkipper.skipSegments);
const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
if (segment["SegmentType"] === "None") {
console.warn("[intro skipper] doSkip() called without an active segment");
return;
}
introSkipper.videoPlayer.currentTime = segment["IntroEnd"];
}
/** Tests if an element with the provided selector exists. */
introSkipper.testElement = function (selector) { return document.querySelector(selector); }
/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
introSkipper.secureFetch = async function (url) {
url = ApiClient.serverAddress() + "/" + url;
const reqInit = {
headers: {
"Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
}
};
const reqInit = { headers: { "Authorization": "MediaBrowser Token=" + ApiClient.accessToken() } };
const res = await fetch(url, reqInit);
if (res.status !== 200) {
throw new Error(`Expected status 200 from ${url}, but got ${res.status}`);
}
if (res.status !== 200) { throw new Error(`Expected status 200 from ${url}, but got ${res.status}`); }
return await res.json();
}
introSkipper.setup();
introSkipper.setup();

View File

@ -2,8 +2,8 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>ConfusedPolarBear.Plugin.IntroSkipper</RootNamespace>
<AssemblyVersion>0.1.15.0</AssemblyVersion>
<FileVersion>0.1.15.0</FileVersion>
<AssemblyVersion>0.1.16.0</AssemblyVersion>
<FileVersion>0.1.16.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>

View File

@ -54,7 +54,7 @@ public class BaseItemAnalyzerTask
CancellationToken cancellationToken)
{
// Assert that ffmpeg with chromaprint is installed
if (!FFmpegWrapper.CheckFFmpegVersion())
if (Plugin.Instance!.Configuration.UseChromaprint && !FFmpegWrapper.CheckFFmpegVersion())
{
throw new FingerprintException(
"ffmpeg with chromaprint is not installed on this system - episodes will not be analyzed. If Jellyfin is running natively, install jellyfin-ffmpeg5. If Jellyfin is running in a container, upgrade it to the latest version of 10.8.0.");
@ -186,7 +186,10 @@ public class BaseItemAnalyzerTask
var analyzers = new Collection<IMediaFileAnalyzer>();
analyzers.Add(new ChapterAnalyzer(_loggerFactory.CreateLogger<ChapterAnalyzer>()));
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
if (Plugin.Instance!.Configuration.UseChromaprint)
{
analyzers.Add(new ChromaprintAnalyzer(_loggerFactory.CreateLogger<ChromaprintAnalyzer>()));
}
if (this._analysisMode == AnalysisMode.Credits)
{

View File

@ -8,6 +8,14 @@
"category": "General",
"imageUrl": "https://raw.githubusercontent.com/jumoog/intro-skipper/master/images/logo.png",
"versions": [
{
"version": "0.1.16.0",
"changelog": "- See the full changelog at [GitHub](https://github.com/jumoog/intro-skipper/blob/master/CHANGELOG.md)\n",
"targetAbi": "10.8.4.0",
"sourceUrl": "https://github.com/jumoog/intro-skipper/releases/download/v0.1.16/intro-skipper-v0.1.16.zip",
"checksum": "8989cbe9c438d5a14fab3002e21c26ba",
"timestamp": "2024-03-04T10:10:35Z"
},
{
"version": "0.1.15.0",
"changelog": "- See the full changelog at [GitHub](https://github.com/jumoog/intro-skipper/blob/master/CHANGELOG.md)\n",