Add a mirrored class to skip credits
This commit is contained in:
parent
d405ef9a52
commit
42a2339978
223
ConfusedPolarBear.Plugin.IntroSkipper/AutoSkipCredits.cs
Normal file
223
ConfusedPolarBear.Plugin.IntroSkipper/AutoSkipCredits.cs
Normal file
@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ConfusedPolarBear.Plugin.IntroSkipper;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically skip past introduction sequences.
|
||||
/// Commands clients to seek to the end of the intro as soon as they start playing it.
|
||||
/// </summary>
|
||||
public class AutoSkipCredits : IServerEntryPoint
|
||||
{
|
||||
private readonly object _sentSeekCommandLock = new();
|
||||
|
||||
private ILogger<AutoSkipCredits> _logger;
|
||||
private IUserDataManager _userDataManager;
|
||||
private ISessionManager _sessionManager;
|
||||
private System.Timers.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>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If introduction auto skipping is enabled, set it up.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public Task RunAsync()
|
||||
{
|
||||
_logger.LogDebug("Setting up automatic skipping");
|
||||
|
||||
_userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
|
||||
Plugin.Instance!.AutoSkipCreditsChanged += AutoSkipCreditsChanged;
|
||||
|
||||
// Make the timer restart automatically and set enabled to match the configuration value.
|
||||
_playbackTimer.AutoReset = true;
|
||||
_playbackTimer.Elapsed += PlaybackTimer_Elapsed;
|
||||
|
||||
AutoSkipCreditsChanged(null, EventArgs.Empty);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void AutoSkipCreditsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var newState = Plugin.Instance!.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;
|
||||
}
|
||||
|
||||
// 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 a credit was detected for this item.
|
||||
if (!Plugin.Instance!.Credits.TryGetValue(itemId, out var intro) || !intro.Valid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seek is unreliable if called at the very start of an episode.
|
||||
var adjustedStart = Math.Max(5, intro.IntroStart);
|
||||
|
||||
_logger.LogTrace(
|
||||
"Playback position is {Position}, intro runs from {Start} to {End}",
|
||||
position,
|
||||
adjustedStart,
|
||||
intro.IntroEnd);
|
||||
|
||||
if (position < adjustedStart || position > intro.IntroEnd)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Notify the user that an introduction is 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 introEnd = (long)intro.IntroEnd;
|
||||
|
||||
_sessionManager.SendPlaystateCommand(
|
||||
session.Id,
|
||||
session.Id,
|
||||
new PlaystateRequest
|
||||
{
|
||||
Command = PlaystateCommand.Seek,
|
||||
ControllingUserId = session.UserId.ToString("N"),
|
||||
SeekPositionTicks = introEnd * 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;
|
||||
}
|
||||
|
||||
_userDataManager.UserDataSaved -= UserDataManager_UserDataSaved;
|
||||
_playbackTimer.Stop();
|
||||
_playbackTimer.Dispose();
|
||||
}
|
||||
}
|
@ -127,6 +127,11 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
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>
|
||||
/// Gets or sets the seconds before the intro starts to show the skip prompt at.
|
||||
/// </summary>
|
||||
@ -198,6 +203,11 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// </summary>
|
||||
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; } = "Intro skipped";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of threads for an ffmpeg process.
|
||||
/// </summary>
|
||||
|
@ -344,6 +344,19 @@
|
||||
<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 class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
|
||||
@ -435,6 +448,16 @@
|
||||
Message shown after automatically skipping an introduction. Leave blank to disable notification.
|
||||
</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>
|
||||
</fieldset>
|
||||
|
||||
@ -609,7 +632,8 @@
|
||||
// UI customization
|
||||
"SkipButtonIntroText",
|
||||
"SkipButtonEndCreditsText",
|
||||
"AutoSkipNotificationText"
|
||||
"AutoSkipNotificationText",
|
||||
"AutoSkipCreditsNotificationText"
|
||||
]
|
||||
|
||||
var booleanConfigurationFields = [
|
||||
@ -620,6 +644,7 @@
|
||||
"UseChromaprint",
|
||||
"CacheFingerprints",
|
||||
"AutoSkip",
|
||||
"AutoSkipCredits",
|
||||
"SkipFirstEpisode",
|
||||
"PersistSkipButton",
|
||||
"SkipButtonVisible"
|
||||
@ -644,6 +669,8 @@
|
||||
var autoSkip = document.querySelector("input#AutoSkip");
|
||||
var skipFirstEpisode = document.querySelector("div#divSkipFirstEpisode");
|
||||
var autoSkipNotificationText = document.querySelector("div#divAutoSkipNotificationText");
|
||||
var autoSkipCredits = document.querySelector("input#AutoSkipCredits");
|
||||
var autoSkipCreditsNotificationText = document.querySelector("div#divAutoSkipCreditsNotificationText");
|
||||
|
||||
async function autoSkipChanged() {
|
||||
if (autoSkip.checked) {
|
||||
@ -657,6 +684,16 @@
|
||||
|
||||
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 showAdjustment = document.querySelector("div#divShowPromptAdjustment");
|
||||
var hideAdjustment = document.querySelector("div#divHidePromptAdjustment");
|
||||
@ -996,6 +1033,7 @@
|
||||
}
|
||||
|
||||
autoSkipChanged();
|
||||
autoSkipCreditsChanged();
|
||||
persistSkipChanged();
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
|
@ -144,6 +144,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// </summary>
|
||||
public event EventHandler? AutoSkipChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Fired after configuration has been saved so the auto skip timer can be stopped or started.
|
||||
/// </summary>
|
||||
public event EventHandler? AutoSkipCreditsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether analysis is running.
|
||||
/// </summary>
|
||||
@ -355,6 +360,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
private void OnConfigurationChanged(object? sender, BasePluginConfiguration e)
|
||||
{
|
||||
AutoSkipChanged?.Invoke(this, EventArgs.Empty);
|
||||
AutoSkipCreditsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
Loading…
x
Reference in New Issue
Block a user