port AutoSkipCredits forward
This commit is contained in:
parent
805a799de7
commit
bbfc9e71a3
236
ConfusedPolarBear.Plugin.IntroSkipper/AutoSkipCredits.cs
Normal file
236
ConfusedPolarBear.Plugin.IntroSkipper/AutoSkipCredits.cs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Timers;
|
||||||
|
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Session;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MediaBrowser.Model.Session;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Automatically skip past credit sequences.
|
||||||
|
/// Commands clients to seek to the end of the credits as soon as they start playing it.
|
||||||
|
/// </summary>
|
||||||
|
public class AutoSkipCredits : IHostedService, IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _sentSeekCommandLock = new();
|
||||||
|
|
||||||
|
private ILogger<AutoSkipCredits> _logger;
|
||||||
|
private IUserDataManager _userDataManager;
|
||||||
|
private ISessionManager _sessionManager;
|
||||||
|
private Timer _playbackTimer = new(1000);
|
||||||
|
private Dictionary<string, bool> _sentSeekCommand;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AutoSkipCredits"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userDataManager">User data manager.</param>
|
||||||
|
/// <param name="sessionManager">Session manager.</param>
|
||||||
|
/// <param name="logger">Logger.</param>
|
||||||
|
public AutoSkipCredits(
|
||||||
|
IUserDataManager userDataManager,
|
||||||
|
ISessionManager sessionManager,
|
||||||
|
ILogger<AutoSkipCredits> logger)
|
||||||
|
{
|
||||||
|
_userDataManager = userDataManager;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
_logger = logger;
|
||||||
|
_sentSeekCommand = new Dictionary<string, bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AutoSkipCreditChanged(object? sender, BasePluginConfiguration e)
|
||||||
|
{
|
||||||
|
var configuration = (PluginConfiguration)e;
|
||||||
|
var newState = configuration.AutoSkipCredits;
|
||||||
|
_logger.LogDebug("Setting playback timer enabled to {NewState}", newState);
|
||||||
|
_playbackTimer.Enabled = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserDataManager_UserDataSaved(object? sender, UserDataSaveEventArgs e)
|
||||||
|
{
|
||||||
|
var itemId = e.Item.Id;
|
||||||
|
var newState = false;
|
||||||
|
var episodeNumber = e.Item.IndexNumber.GetValueOrDefault(-1);
|
||||||
|
|
||||||
|
// Ignore all events except playback start & end
|
||||||
|
if (e.SaveReason != UserDataSaveReason.PlaybackStart && e.SaveReason != UserDataSaveReason.PlaybackFinished)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the session for this item.
|
||||||
|
SessionInfo? session = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var needle in _sessionManager.Sessions)
|
||||||
|
{
|
||||||
|
if (needle.UserId == e.UserId && needle.NowPlayingItem.Id == itemId)
|
||||||
|
{
|
||||||
|
session = needle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Unable to find session for {Item}", itemId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is NullReferenceException || ex is ResourceNotFoundException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the first episode in the season, and SkipFirstEpisode is false, pretend that we've already sent the seek command for this playback session.
|
||||||
|
if (!Plugin.Instance!.Configuration.SkipFirstEpisode && episodeNumber == 1)
|
||||||
|
{
|
||||||
|
newState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the seek command state for this device.
|
||||||
|
lock (_sentSeekCommandLock)
|
||||||
|
{
|
||||||
|
var device = session.DeviceId;
|
||||||
|
|
||||||
|
_logger.LogDebug("Resetting seek command state for session {Session}", device);
|
||||||
|
_sentSeekCommand[device] = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaybackTimer_Elapsed(object? sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
foreach (var session in _sessionManager.Sessions)
|
||||||
|
{
|
||||||
|
var deviceId = session.DeviceId;
|
||||||
|
var itemId = session.NowPlayingItem.Id;
|
||||||
|
var position = session.PlayState.PositionTicks / TimeSpan.TicksPerSecond;
|
||||||
|
|
||||||
|
// Don't send the seek command more than once in the same session.
|
||||||
|
lock (_sentSeekCommandLock)
|
||||||
|
{
|
||||||
|
if (_sentSeekCommand.TryGetValue(deviceId, out var sent) && sent)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Already sent seek command for session {Session}", deviceId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that credits were detected for this item.
|
||||||
|
if (!Plugin.Instance!.Credits.TryGetValue(itemId, out var credit) || !credit.Valid)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek is unreliable if called at the very start of an episode.
|
||||||
|
var adjustedStart = Math.Max(5, credit.IntroStart);
|
||||||
|
|
||||||
|
_logger.LogTrace(
|
||||||
|
"Playback position is {Position}, credits run from {Start} to {End}",
|
||||||
|
position,
|
||||||
|
adjustedStart,
|
||||||
|
credit.IntroEnd);
|
||||||
|
|
||||||
|
if (position < adjustedStart || position > credit.IntroEnd)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the user that credits are being skipped for them.
|
||||||
|
var notificationText = Plugin.Instance!.Configuration.AutoSkipCreditsNotificationText;
|
||||||
|
if (!string.IsNullOrWhiteSpace(notificationText))
|
||||||
|
{
|
||||||
|
_sessionManager.SendMessageCommand(
|
||||||
|
session.Id,
|
||||||
|
session.Id,
|
||||||
|
new MessageCommand
|
||||||
|
{
|
||||||
|
Header = string.Empty, // some clients require header to be a string instead of null
|
||||||
|
Text = notificationText,
|
||||||
|
TimeoutMs = 2000,
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Sending seek command to {Session}", deviceId);
|
||||||
|
|
||||||
|
var creditEnd = (long)credit.IntroEnd;
|
||||||
|
|
||||||
|
_sessionManager.SendPlaystateCommand(
|
||||||
|
session.Id,
|
||||||
|
session.Id,
|
||||||
|
new PlaystateRequest
|
||||||
|
{
|
||||||
|
Command = PlaystateCommand.Seek,
|
||||||
|
ControllingUserId = session.UserId.ToString("N"),
|
||||||
|
SeekPositionTicks = creditEnd * TimeSpan.TicksPerSecond,
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
// Flag that we've sent the seek command so that it's not sent repeatedly
|
||||||
|
lock (_sentSeekCommandLock)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Setting seek command state for session {Session}", deviceId);
|
||||||
|
_sentSeekCommand[deviceId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Protected dispose.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">Dispose.</param>
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_playbackTimer.Stop();
|
||||||
|
_playbackTimer.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Setting up automatic credit skipping");
|
||||||
|
|
||||||
|
_userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
|
||||||
|
Plugin.Instance!.ConfigurationChanged += AutoSkipCreditChanged;
|
||||||
|
|
||||||
|
// Make the timer restart automatically and set enabled to match the configuration value.
|
||||||
|
_playbackTimer.AutoReset = true;
|
||||||
|
_playbackTimer.Elapsed += PlaybackTimer_Elapsed;
|
||||||
|
|
||||||
|
AutoSkipCreditChanged(null, Plugin.Instance.Configuration);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_userDataManager.UserDataSaved -= UserDataManager_UserDataSaved;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -133,6 +133,11 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AutoSkip { get; set; }
|
public bool AutoSkip { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether credits should be automatically skipped.
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoSkipCredits { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the seconds before the intro starts to show the skip prompt at.
|
/// Gets or sets the seconds before the intro starts to show the skip prompt at.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -204,6 +209,11 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string AutoSkipNotificationText { get; set; } = "Intro skipped";
|
public string AutoSkipNotificationText { get; set; } = "Intro skipped";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the notification text sent after automatically skipping credits.
|
||||||
|
/// </summary>
|
||||||
|
public string AutoSkipCreditsNotificationText { get; set; } = "Credits skipped";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the number of threads for an ffmpeg process.
|
/// Gets or sets the number of threads for an ffmpeg process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -7,19 +7,19 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
|
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
|
||||||
data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-linkbutton">
|
data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-linkbutton">
|
||||||
<div data-role="content">
|
<div data-role="content">
|
||||||
<style>
|
<style>
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: inherit;
|
width: inherit;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 1.0em;
|
font-size: 1.0em;
|
||||||
outline: 2px solid rgba(155, 155, 155, 0.5);
|
outline: 2px solid rgba(155, 155, 155, 0.5);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="content-primary">
|
<div class="content-primary">
|
||||||
@ -356,11 +356,25 @@
|
|||||||
<div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
|
<div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label">
|
<label class="emby-checkbox-label">
|
||||||
<input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
|
<input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
|
||||||
<span>Ignore intro in the first episode of a season</span>
|
<span>Play intro for first episode of a season</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
If checked, auto skip will ignore introduction in the first episode of a season.<br />
|
If checked, auto skip will play the introduction of the first episode in a season.<br />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label class="emby-checkbox-label">
|
||||||
|
<input id="AutoSkipCredits" type="checkbox" is="emby-checkbox" />
|
||||||
|
<span>Automatically skip credits</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="fieldDescription">
|
||||||
|
If checked, credits will be automatically skipped. If you access Jellyfin through a
|
||||||
|
reverse proxy, it must be configured to proxy web
|
||||||
|
sockets.<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -455,6 +469,16 @@
|
|||||||
Message shown after automatically skipping an introduction. Leave blank to disable notification.
|
Message shown after automatically skipping an introduction. Leave blank to disable notification.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="divAutoSkipCreditsNotificationText" class="inputContainer">
|
||||||
|
<label class="inputLabel" for="AutoSkipCreditsNotificationText">
|
||||||
|
Automatic skip notification message
|
||||||
|
</label>
|
||||||
|
<input id="AutoSkipCreditsNotificationText" type="text" is="emby-input" />
|
||||||
|
<div class="fieldDescription">
|
||||||
|
Message shown after automatically skipping credits. Leave blank to disable notification.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@ -511,9 +535,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="timestampErrorDiv" style="display:none">
|
<div id="timestampErrorDiv" style="display:none">
|
||||||
<textarea id="timestampError" rows="2" cols="75" readonly></textarea>
|
<textarea id="timestampError" rows="2" cols="75" readonly></textarea>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Fingerprint Visualizer</h3>
|
<h3>Fingerprint Visualizer</h3>
|
||||||
@ -524,8 +548,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<td style="min-width: 100px; font-weight: bold">Key</td>
|
<td style="min-width: 100px; font-weight: bold">Key</td>
|
||||||
<td style="font-weight: bold">Function</td>
|
<td style="font-weight: bold">Function</td>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@ -631,7 +655,8 @@
|
|||||||
// UI customization
|
// UI customization
|
||||||
"SkipButtonIntroText",
|
"SkipButtonIntroText",
|
||||||
"SkipButtonEndCreditsText",
|
"SkipButtonEndCreditsText",
|
||||||
"AutoSkipNotificationText"
|
"AutoSkipNotificationText",
|
||||||
|
"AutoSkipCreditsNotificationText"
|
||||||
]
|
]
|
||||||
|
|
||||||
var booleanConfigurationFields = [
|
var booleanConfigurationFields = [
|
||||||
@ -642,6 +667,7 @@
|
|||||||
"UseChromaprint",
|
"UseChromaprint",
|
||||||
"CacheFingerprints",
|
"CacheFingerprints",
|
||||||
"AutoSkip",
|
"AutoSkip",
|
||||||
|
"AutoSkipCredits",
|
||||||
"SkipFirstEpisode",
|
"SkipFirstEpisode",
|
||||||
"PersistSkipButton",
|
"PersistSkipButton",
|
||||||
"SkipButtonVisible"
|
"SkipButtonVisible"
|
||||||
@ -666,6 +692,8 @@
|
|||||||
var autoSkip = document.querySelector("input#AutoSkip");
|
var autoSkip = document.querySelector("input#AutoSkip");
|
||||||
var skipFirstEpisode = document.querySelector("div#divSkipFirstEpisode");
|
var skipFirstEpisode = document.querySelector("div#divSkipFirstEpisode");
|
||||||
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
|
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
|
||||||
|
var autoSkipCredits = document.querySelector("input#AutoSkipCredits");
|
||||||
|
var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText");
|
||||||
|
|
||||||
async function autoSkipChanged() {
|
async function autoSkipChanged() {
|
||||||
if (autoSkip.checked) {
|
if (autoSkip.checked) {
|
||||||
@ -679,6 +707,16 @@
|
|||||||
|
|
||||||
autoSkip.addEventListener("change", autoSkipChanged);
|
autoSkip.addEventListener("change", autoSkipChanged);
|
||||||
|
|
||||||
|
async function autoSkipCreditsChanged() {
|
||||||
|
if (autoSkipCredits.checked) {
|
||||||
|
autoSkipCreditsNotificationText.style.display = 'unset';
|
||||||
|
} else {
|
||||||
|
autoSkipCreditsNotificationText.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
autoSkipCredits.addEventListener("change", autoSkipCreditsChanged);
|
||||||
|
|
||||||
var persistSkip = document.querySelector("input#PersistSkipButton");
|
var persistSkip = document.querySelector("input#PersistSkipButton");
|
||||||
var showAdjustment = document.querySelector("div#divShowPromptAdjustment");
|
var showAdjustment = document.querySelector("div#divShowPromptAdjustment");
|
||||||
var hideAdjustment = document.querySelector("div#divHidePromptAdjustment");
|
var hideAdjustment = document.querySelector("div#divHidePromptAdjustment");
|
||||||
@ -879,16 +917,16 @@
|
|||||||
// make an authenticated GET to the server and parse the response as JSON
|
// make an authenticated GET to the server and parse the response as JSON
|
||||||
async function getJson(url) {
|
async function getJson(url) {
|
||||||
return await fetchWithAuth(url, "GET")
|
return await fetchWithAuth(url, "GET")
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
return r.json();
|
return r.json();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.debug(err);
|
console.debug(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// make an authenticated fetch to the server
|
// make an authenticated fetch to the server
|
||||||
@ -1018,6 +1056,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
autoSkipChanged();
|
autoSkipChanged();
|
||||||
|
autoSkipCreditsChanged();
|
||||||
persistSkipChanged();
|
persistSkipChanged();
|
||||||
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
|
@ -13,6 +13,7 @@ namespace ConfusedPolarBear.Plugin.IntroSkipper
|
|||||||
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
|
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
|
||||||
{
|
{
|
||||||
serviceCollection.AddHostedService<AutoSkip>();
|
serviceCollection.AddHostedService<AutoSkip>();
|
||||||
|
serviceCollection.AddHostedService<AutoSkipCredits>();
|
||||||
serviceCollection.AddHostedService<Entrypoint>();
|
serviceCollection.AddHostedService<Entrypoint>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user