2022-06-07 18:33:59 -05:00
using System ;
using System.Collections.Generic ;
2024-09-09 20:52:54 +02:00
using System.Linq ;
2022-06-07 18:33:59 -05:00
using System.Threading ;
using System.Threading.Tasks ;
using System.Timers ;
2024-04-12 14:09:19 +02:00
using ConfusedPolarBear.Plugin.IntroSkipper.Configuration ;
2022-06-09 15:38:30 -05:00
using MediaBrowser.Common.Extensions ;
2022-06-07 18:33:59 -05:00
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Session ;
using MediaBrowser.Model.Entities ;
2024-04-12 14:09:19 +02:00
using MediaBrowser.Model.Plugins ;
2022-06-07 18:33:59 -05:00
using MediaBrowser.Model.Session ;
2024-03-22 23:41:58 +01:00
using Microsoft.Extensions.Hosting ;
2022-06-07 18:33:59 -05:00
using Microsoft.Extensions.Logging ;
2024-04-20 12:58:29 +02:00
using Timer = System . Timers . Timer ;
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
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>
/// <remarks>
/// Initializes a new instance of the <see cref="AutoSkip"/> class.
/// </remarks>
/// <param name="userDataManager">User data manager.</param>
/// <param name="sessionManager">Session manager.</param>
/// <param name="logger">Logger.</param>
public class AutoSkip (
IUserDataManager userDataManager ,
ISessionManager sessionManager ,
ILogger < AutoSkip > logger ) : IHostedService , IDisposable
2022-06-07 18:33:59 -05:00
{
2024-10-16 16:05:59 +02:00
private readonly object _sentSeekCommandLock = new ( ) ;
private ILogger < AutoSkip > _logger = logger ;
private IUserDataManager _userDataManager = userDataManager ;
private ISessionManager _sessionManager = sessionManager ;
private Timer _playbackTimer = new ( 1000 ) ;
private Dictionary < string , bool > _sentSeekCommand = [ ] ;
private HashSet < string > _clientList = [ ] ;
private void AutoSkipChanged ( object? sender , BasePluginConfiguration e )
2022-06-07 18:33:59 -05:00
{
2024-10-16 16:05:59 +02:00
var configuration = ( PluginConfiguration ) e ;
_clientList = [ . . configuration . ClientList . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ] ;
var newState = configuration . AutoSkip | | _clientList . Count > 0 ;
_logger . LogDebug ( "Setting playback timer enabled to {NewState}" , newState ) ;
_playbackTimer . Enabled = newState ;
}
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
private void UserDataManager_UserDataSaved ( object? sender , UserDataSaveEventArgs e )
{
var itemId = e . Item . Id ;
var newState = false ;
var episodeNumber = e . Item . IndexNumber . GetValueOrDefault ( - 1 ) ;
2024-10-16 14:47:20 +02:00
2024-10-16 16:05:59 +02:00
// Ignore all events except playback start & end
if ( e . SaveReason ! = UserDataSaveReason . PlaybackStart & & e . SaveReason ! = UserDataSaveReason . PlaybackFinished )
{
return ;
}
2024-10-16 14:47:20 +02:00
2024-10-16 16:05:59 +02:00
// Lookup the session for this item.
SessionInfo ? session = null ;
2024-10-16 14:47:20 +02:00
2024-10-16 16:05:59 +02:00
try
{
foreach ( var needle in _sessionManager . Sessions )
2024-10-16 14:47:20 +02:00
{
2024-10-16 16:05:59 +02:00
if ( needle . UserId = = e . UserId & & needle . NowPlayingItem . Id = = itemId )
2022-06-07 18:33:59 -05:00
{
2024-10-16 16:05:59 +02:00
session = needle ;
break ;
2024-10-16 14:47:20 +02:00
}
}
2024-10-16 16:05:59 +02:00
if ( session = = null )
2022-06-07 18:33:59 -05:00
{
2024-10-16 16:05:59 +02:00
_logger . LogInformation ( "Unable to find session for {Item}" , itemId ) ;
2022-06-07 18:33:59 -05:00
return ;
}
2024-10-16 16:05:59 +02:00
}
catch ( Exception ex ) when ( ex is NullReferenceException | | ex is ResourceNotFoundException )
{
return ;
}
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
// 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 ;
}
2022-09-02 01:54:49 -05:00
2024-10-16 16:05:59 +02:00
// Reset the seek command state for this device.
lock ( _sentSeekCommandLock )
{
var device = session . DeviceId ;
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
_logger . LogDebug ( "Resetting seek command state for session {Session}" , device ) ;
_sentSeekCommand [ device ] = newState ;
2022-06-07 18:33:59 -05:00
}
2024-10-16 16:05:59 +02:00
}
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
private void PlaybackTimer_Elapsed ( object? sender , ElapsedEventArgs e )
{
foreach ( var session in _sessionManager . Sessions . Where ( s = > Plugin . Instance ! . Configuration . AutoSkip | | _clientList . Contains ( s . Client , StringComparer . OrdinalIgnoreCase ) ) )
2022-06-07 18:33:59 -05:00
{
2024-10-16 16:05:59 +02:00
var deviceId = session . DeviceId ;
var itemId = session . NowPlayingItem . Id ;
var position = session . PlayState . PositionTicks / TimeSpan . TicksPerSecond ;
2024-10-16 14:47:20 +02:00
2024-10-16 16:05:59 +02:00
// Don't send the seek command more than once in the same session.
lock ( _sentSeekCommandLock )
{
if ( _sentSeekCommand . TryGetValue ( deviceId , out var sent ) & & sent )
2024-10-16 14:47:20 +02:00
{
2024-10-16 16:05:59 +02:00
_logger . LogTrace ( "Already sent seek command for session {Session}" , deviceId ) ;
2024-10-16 14:47:20 +02:00
continue ;
}
2024-10-16 16:05:59 +02:00
}
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
// Assert that an intro was detected for this item.
if ( ! Plugin . Instance ! . Intros . TryGetValue ( itemId , out var intro ) | | ! intro . Valid )
{
continue ;
}
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
// Seek is unreliable if called at the very start of an episode.
var adjustedStart = Math . Max ( 1 , intro . Start + Plugin . Instance . Configuration . SecondsOfIntroStartToPlay ) ;
var adjustedEnd = intro . End - Plugin . Instance . Configuration . RemainingSecondsOfIntro ;
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
_logger . LogTrace (
"Playback position is {Position}, intro runs from {Start} to {End}" ,
position ,
adjustedStart ,
adjustedEnd ) ;
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
if ( position < adjustedStart | | position > adjustedEnd )
{
continue ;
}
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
// Notify the user that an introduction is being skipped for them.
var notificationText = Plugin . Instance . Configuration . AutoSkipNotificationText ;
if ( ! string . IsNullOrWhiteSpace ( notificationText ) )
{
_sessionManager . SendMessageCommand (
session . Id ,
session . Id ,
new MessageCommand
2024-10-16 14:47:20 +02:00
{
2024-10-16 16:05:59 +02:00
Header = string . Empty , // some clients require header to be a string instead of null
Text = notificationText ,
TimeoutMs = 2000 ,
} ,
CancellationToken . None ) ;
2022-06-07 18:33:59 -05:00
}
2024-10-16 16:05:59 +02:00
_logger . LogDebug ( "Sending seek command to {Session}" , deviceId ) ;
2022-06-07 18:33:59 -05:00
2024-10-16 16:05:59 +02:00
_sessionManager . SendPlaystateCommand (
session . Id ,
session . Id ,
new PlaystateRequest
{
Command = PlaystateCommand . Seek ,
ControllingUserId = session . UserId . ToString ( ) ,
SeekPositionTicks = ( long ) adjustedEnd * TimeSpan . TicksPerSecond ,
} ,
CancellationToken . None ) ;
// Flag that we've sent the seek command so that it's not sent repeatedly
lock ( _sentSeekCommandLock )
2024-10-16 14:47:20 +02:00
{
2024-10-16 16:05:59 +02:00
_logger . LogTrace ( "Setting seek command state for session {Session}" , deviceId ) ;
_sentSeekCommand [ deviceId ] = true ;
2024-10-16 14:47:20 +02:00
}
}
2024-10-16 16:05:59 +02:00
}
/// <summary>
/// Dispose.
/// </summary>
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
2024-03-22 23:41:58 +01:00
2024-10-16 16:05:59 +02:00
/// <summary>
/// Protected dispose.
/// </summary>
/// <param name="disposing">Dispose.</param>
protected virtual void Dispose ( bool disposing )
{
if ( ! disposing )
2024-10-16 14:47:20 +02:00
{
2024-10-16 16:05:59 +02:00
return ;
}
2024-03-22 23:41:58 +01:00
2024-10-16 16:05:59 +02:00
_playbackTimer . Stop ( ) ;
_playbackTimer . Dispose ( ) ;
}
2024-03-22 23:41:58 +01:00
2024-10-16 16:05:59 +02:00
/// <inheritdoc />
public Task StartAsync ( CancellationToken cancellationToken )
{
_logger . LogDebug ( "Setting up automatic skipping" ) ;
2024-03-22 23:41:58 +01:00
2024-10-16 16:05:59 +02:00
_userDataManager . UserDataSaved + = UserDataManager_UserDataSaved ;
Plugin . Instance ! . ConfigurationChanged + = AutoSkipChanged ;
2024-03-22 23:41:58 +01:00
2024-10-16 16:05:59 +02:00
// Make the timer restart automatically and set enabled to match the configuration value.
_playbackTimer . AutoReset = true ;
_playbackTimer . Elapsed + = PlaybackTimer_Elapsed ;
2024-10-16 14:47:20 +02:00
2024-10-16 16:05:59 +02:00
AutoSkipChanged ( null , Plugin . Instance . Configuration ) ;
return Task . CompletedTask ;
}
/// <inheritdoc />
public Task StopAsync ( CancellationToken cancellationToken )
{
_userDataManager . UserDataSaved - = UserDataManager_UserDataSaved ;
return Task . CompletedTask ;
2024-03-22 23:41:58 +01:00
}
2022-06-07 18:33:59 -05:00
}