Merge pull request #61 from RepoDevil/master
Addressing compatibility issues with JF
This commit is contained in:
commit
ce4f4e278e
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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";
|
||||
|
@ -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();
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user