2024-04-10 00:37:55 -04:00
|
|
|
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>
|
2024-04-13 12:35:43 -04:00
|
|
|
/// Automatically skip past credit sequences.
|
|
|
|
/// Commands clients to seek to the end of the credits as soon as they start playing it.
|
2024-04-10 00:37:55 -04:00
|
|
|
/// </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>
|
2024-04-13 12:35:43 -04:00
|
|
|
/// If credits auto skipping is enabled, set it up.
|
2024-04-10 00:37:55 -04:00
|
|
|
/// </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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-13 12:35:43 -04:00
|
|
|
// Assert that credits were detected for this item.
|
|
|
|
if (!Plugin.Instance!.Credits.TryGetValue(itemId, out var credit) || !credit.Valid)
|
2024-04-10 00:37:55 -04:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Seek is unreliable if called at the very start of an episode.
|
2024-04-13 12:35:43 -04:00
|
|
|
var adjustedStart = Math.Max(5, credit.IntroStart);
|
2024-04-10 00:37:55 -04:00
|
|
|
|
|
|
|
_logger.LogTrace(
|
2024-04-13 12:35:43 -04:00
|
|
|
"Playback position is {Position}, credits run from {Start} to {End}",
|
2024-04-10 00:37:55 -04:00
|
|
|
position,
|
|
|
|
adjustedStart,
|
2024-04-13 12:35:43 -04:00
|
|
|
credit.IntroEnd);
|
2024-04-10 00:37:55 -04:00
|
|
|
|
2024-04-13 12:35:43 -04:00
|
|
|
if (position < adjustedStart || position > credit.IntroEnd)
|
2024-04-10 00:37:55 -04:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-04-13 12:35:43 -04:00
|
|
|
// Notify the user that credits are being skipped for them.
|
2024-04-10 00:37:55 -04:00
|
|
|
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);
|
|
|
|
|
2024-04-13 12:35:43 -04:00
|
|
|
var creditEnd = (long)credit.IntroEnd;
|
2024-04-10 00:37:55 -04:00
|
|
|
|
|
|
|
_sessionManager.SendPlaystateCommand(
|
|
|
|
session.Id,
|
|
|
|
session.Id,
|
|
|
|
new PlaystateRequest
|
|
|
|
{
|
|
|
|
Command = PlaystateCommand.Seek,
|
|
|
|
ControllingUserId = session.UserId.ToString("N"),
|
2024-04-13 12:35:43 -04:00
|
|
|
SeekPositionTicks = creditEnd * TimeSpan.TicksPerSecond,
|
2024-04-10 00:37:55 -04:00
|
|
|
},
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|