Skip to content

Commit

Permalink
Add a mirrored class to skip credits
Browse files Browse the repository at this point in the history
  • Loading branch information
AbandonedCart committed Apr 10, 2024
1 parent d405ef9 commit 42a2339
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 1 deletion.
223 changes: 223 additions & 0 deletions ConfusedPolarBear.Plugin.IntroSkipper/AutoSkipCredits.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ public PluginConfiguration()
/// </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>
Expand Down Expand Up @@ -198,6 +203,11 @@ public PluginConfiguration()
/// </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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -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>

Expand Down Expand Up @@ -609,7 +632,8 @@ <h3>Fingerprint Visualizer</h3>
// UI customization
"SkipButtonIntroText",
"SkipButtonEndCreditsText",
"AutoSkipNotificationText"
"AutoSkipNotificationText",
"AutoSkipCreditsNotificationText"
]

var booleanConfigurationFields = [
Expand All @@ -620,6 +644,7 @@ <h3>Fingerprint Visualizer</h3>
"UseChromaprint",
"CacheFingerprints",
"AutoSkip",
"AutoSkipCredits",
"SkipFirstEpisode",
"PersistSkipButton",
"SkipButtonVisible"
Expand All @@ -644,6 +669,8 @@ <h3>Fingerprint Visualizer</h3>
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) {
Expand All @@ -657,6 +684,16 @@ <h3>Fingerprint Visualizer</h3>

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");
Expand Down Expand Up @@ -996,6 +1033,7 @@ <h3>Fingerprint Visualizer</h3>
}

autoSkipChanged();
autoSkipCreditsChanged();
persistSkipChanged();

Dashboard.hideLoadingMsg();
Expand Down
6 changes: 6 additions & 0 deletions ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -355,6 +360,7 @@ internal void UpdateTimestamps(Dictionary<Guid, Intro> newTimestamps, AnalysisMo
private void OnConfigurationChanged(object? sender, BasePluginConfiguration e)
{
AutoSkipChanged?.Invoke(this, EventArgs.Empty);
AutoSkipCreditsChanged?.Invoke(this, EventArgs.Empty);
}

/// <summary>
Expand Down

0 comments on commit 42a2339

Please sign in to comment.