ConfusedPolarBear 0d44d33010 Catch ResourceNotFoundException
Thrown if the user abruptly stops playback
2022-06-09 15:38:30 -05:00

212 lines
6.6 KiB
C#

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 AutoSkip : IServerEntryPoint
{
private readonly object _sentSeekCommandLock = new();
private ILogger<AutoSkip> _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="AutoSkip"/> class.
/// </summary>
/// <param name="userDataManager">User data manager.</param>
/// <param name="sessionManager">Session manager.</param>
/// <param name="logger">Logger.</param>
public AutoSkip(
IUserDataManager userDataManager,
ISessionManager sessionManager,
ILogger<AutoSkip> 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()
{
if (!Plugin.Instance!.Configuration.AutoSkip)
{
_logger.LogDebug("Not setting up automatic skipping");
return Task.CompletedTask;
}
_logger.LogDebug("Setting up automatic skipping");
_userDataManager.UserDataSaved += UserDataManager_UserDataSaved;
_playbackTimer.AutoReset = true;
_playbackTimer.Elapsed += PlaybackTimer_Elapsed;
_playbackTimer.Start();
return Task.CompletedTask;
}
private void UserDataManager_UserDataSaved(object? sender, UserDataSaveEventArgs e)
{
var itemId = e.Item.Id;
// 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] = false;
}
}
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[deviceId])
{
_logger.LogTrace("Already sent seek command for session {Session}", deviceId);
continue;
}
}
// Assert that an intro was detected for this item.
if (!Plugin.Instance!.Intros.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.
_sessionManager.SendMessageCommand(
session.Id,
session.Id,
new MessageCommand()
{
Text = "Automatically skipped intro",
TimeoutMs = 2000,
},
CancellationToken.None);
// Send the seek command
_logger.LogDebug("Sending seek command to {Session}", deviceId);
_sessionManager.SendPlaystateCommand(
session.Id,
session.Id,
new PlaystateRequest
{
Command = PlaystateCommand.Seek,
ControllingUserId = session.UserId.ToString("N"),
SeekPositionTicks = (long)intro.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();
}
}