2024-10-25 14:33:26 -04:00
// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
// SPDX-License-Identifier: GPL-3.0-only.
2024-10-25 13:42:34 -04:00
2024-05-13 14:25:52 +02:00
using System ;
using System.Collections.Generic ;
2024-09-09 20:52:54 +02:00
using System.Linq ;
2024-05-13 14:25:52 +02:00
using System.Threading ;
using System.Threading.Tasks ;
using System.Timers ;
2024-10-20 13:56:09 +02:00
using IntroSkipper.Configuration ;
2024-05-13 14:25:52 +02:00
using MediaBrowser.Common.Extensions ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Session ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Plugins ;
using MediaBrowser.Model.Session ;
using Microsoft.Extensions.Hosting ;
using Microsoft.Extensions.Logging ;
using Timer = System . Timers . Timer ;
2024-10-20 13:56:09 +02:00
namespace IntroSkipper ;
2024-05-13 14:25:52 +02:00
/// <summary>
/// Automatically skip past credit sequences.
/// Commands clients to seek to the end of the credits as soon as they start playing it.
/// </summary>
2024-09-09 20:52:54 +02:00
/// <remarks>
/// Initializes a new instance of the <see cref="AutoSkipCredits"/> class.
/// </remarks>
/// <param name="userDataManager">User data manager.</param>
/// <param name="sessionManager">Session manager.</param>
/// <param name="logger">Logger.</param>
public class AutoSkipCredits (
IUserDataManager userDataManager ,
ISessionManager sessionManager ,
ILogger < AutoSkipCredits > logger ) : IHostedService , IDisposable
2024-05-13 14:25:52 +02:00
{
private readonly object _sentSeekCommandLock = new ( ) ;
2024-09-09 20:52:54 +02:00
private ILogger < AutoSkipCredits > _logger = logger ;
private IUserDataManager _userDataManager = userDataManager ;
private ISessionManager _sessionManager = sessionManager ;
2024-05-13 14:25:52 +02:00
private Timer _playbackTimer = new ( 1000 ) ;
2024-09-09 20:52:54 +02:00
private Dictionary < string , bool > _sentSeekCommand = [ ] ;
private HashSet < string > _clientList = [ ] ;
2024-05-13 14:25:52 +02:00
private void AutoSkipCreditChanged ( object? sender , BasePluginConfiguration e )
{
var configuration = ( PluginConfiguration ) e ;
2024-10-02 19:13:17 -04:00
_clientList = [ . . configuration . ClientList . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ] ;
var newState = configuration . AutoSkipCredits | | ( configuration . SkipButtonVisible & & _clientList . Count > 0 ) ;
2024-05-13 14:25:52 +02:00
_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 ;
}
// 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.
2024-06-12 16:29:07 +02:00
if ( Plugin . Instance ! . Configuration . SkipFirstEpisode & & episodeNumber = = 1 )
2024-05-13 14:25:52 +02:00
{
newState = true ;
}
// Reset the seek command state for this device.
lock ( _sentSeekCommandLock )
{
var device = session . DeviceId ;
2024-05-18 14:20:10 +02:00
_logger . LogDebug ( "Resetting seek command state for session {Session}" , device ) ;
_sentSeekCommand [ device ] = newState ;
2024-05-13 14:25:52 +02:00
}
}
private void PlaybackTimer_Elapsed ( object? sender , ElapsedEventArgs e )
{
2024-10-02 19:13:17 -04:00
foreach ( var session in _sessionManager . Sessions . Where ( s = > Plugin . Instance ! . Configuration . AutoSkipCredits | | ( Plugin . Instance ! . Configuration . SkipButtonVisible & & _clientList . Contains ( s . Client , StringComparer . OrdinalIgnoreCase ) ) ) )
2024-05-13 14:25:52 +02:00
{
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 )
{
2024-05-18 14:20:10 +02:00
if ( _sentSeekCommand . TryGetValue ( deviceId , out var sent ) & & sent )
2024-05-13 14:25:52 +02:00
{
_logger . LogTrace ( "Already sent seek command for session {Session}" , deviceId ) ;
continue ;
}
}
// Assert that credits were detected for this item.
2024-05-18 14:20:10 +02:00
if ( ! Plugin . Instance ! . Credits . TryGetValue ( itemId , out var credit ) | | ! credit . Valid )
2024-05-13 14:25:52 +02:00
{
continue ;
}
2024-08-02 13:41:03 +00:00
// Seek is unreliable if called at the very end of an episode.
2024-09-12 08:37:47 +00:00
var adjustedStart = credit . Start + Plugin . Instance . Configuration . SecondsOfCreditsStartToPlay ;
var adjustedEnd = credit . End - Plugin . Instance . Configuration . RemainingSecondsOfIntro ;
2024-05-13 14:25:52 +02:00
_logger . LogTrace (
"Playback position is {Position}, credits run from {Start} to {End}" ,
position ,
adjustedStart ,
2024-08-02 13:41:03 +00:00
adjustedEnd ) ;
2024-05-13 14:25:52 +02:00
2024-08-02 13:41:03 +00:00
if ( position < adjustedStart | | position > adjustedEnd )
2024-05-13 14:25:52 +02:00
{
continue ;
}
// Notify the user that credits are 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 ) ;
}
2024-05-18 14:20:10 +02:00
_logger . LogDebug ( "Sending seek command to {Session}" , deviceId ) ;
2024-05-13 14:25:52 +02:00
_sessionManager . SendPlaystateCommand (
session . Id ,
session . Id ,
new PlaystateRequest
{
2024-05-18 14:20:10 +02:00
Command = PlaystateCommand . Seek ,
2024-05-13 15:24:15 +02:00
ControllingUserId = session . UserId . ToString ( ) ,
2024-08-02 13:41:03 +00:00
SeekPositionTicks = ( long ) adjustedEnd * TimeSpan . TicksPerSecond ,
2024-05-13 14:25:52 +02:00
} ,
CancellationToken . None ) ;
// Flag that we've sent the seek command so that it's not sent repeatedly
lock ( _sentSeekCommandLock )
{
2024-05-18 14:20:10 +02:00
_logger . LogTrace ( "Setting seek command state for session {Session}" , deviceId ) ;
_sentSeekCommand [ deviceId ] = true ;
2024-05-13 14:25:52 +02:00
}
}
}
/// <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 ;
}
_playbackTimer . Stop ( ) ;
_playbackTimer . Dispose ( ) ;
}
/// <inheritdoc />
public Task StartAsync ( CancellationToken cancellationToken )
{
_logger . LogDebug ( "Setting up automatic credit skipping" ) ;
_userDataManager . UserDataSaved + = UserDataManager_UserDataSaved ;
Plugin . Instance ! . ConfigurationChanged + = AutoSkipCreditChanged ;
// Make the timer restart automatically and set enabled to match the configuration value.
_playbackTimer . AutoReset = true ;
_playbackTimer . Elapsed + = PlaybackTimer_Elapsed ;
AutoSkipCreditChanged ( null , Plugin . Instance . Configuration ) ;
return Task . CompletedTask ;
}
/// <inheritdoc />
public Task StopAsync ( CancellationToken cancellationToken )
{
_userDataManager . UserDataSaved - = UserDataManager_UserDataSaved ;
return Task . CompletedTask ;
}
}