Properly handle pause and resume events

This commit is contained in:
Shadowghost 2022-04-16 15:42:10 +02:00
parent caef90021b
commit 0985b9455c
3 changed files with 348 additions and 214 deletions

View File

@ -0,0 +1,21 @@
namespace Trakt.Helpers
{
/// <summary>
/// Enum MediaStatus.
/// </summary>
public enum MediaStatus
{
/// <summary>
/// The watching state.
/// </summary>
Watching,
/// <summary>
/// The paused state.
/// </summary>
Paused,
/// <summary>
/// The stopped state.
/// </summary>
Stop
}
}

View File

@ -1,8 +0,0 @@
namespace Trakt;
public enum MediaStatus
{
Watching,
Paused,
Stop
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@ -15,13 +16,13 @@ using Microsoft.Extensions.Logging;
using Trakt.Api;
using Trakt.Helpers;
namespace Trakt;
/// <summary>
/// All communication between the server and the plugins server instance should occur in this class.
/// </summary>
public class ServerMediator : IServerEntryPoint
namespace Trakt
{
/// <summary>
/// All communication between the server and the plugins server instance should occur in this class.
/// </summary>
public class ServerMediator : IServerEntryPoint, IDisposable
{
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<ServerMediator> _logger;
@ -30,16 +31,18 @@ public class ServerMediator : IServerEntryPoint
private TraktApi _traktApi;
private LibraryManagerEventsHelper _libraryManagerEventsHelper;
private Dictionary<string, bool> _playbackPause;
/// <summary>
///
/// Processes server events.
/// </summary>
/// <param name="sessionManager"> </param>
/// <param name="userDataManager"></param>
/// <param name="libraryManager"> </param>
/// <param name="logger"></param>
/// <param name="httpClient"></param>
/// <param name="appHost"></param>
/// <param name="fileSystem"></param>
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
public ServerMediator(
ISessionManager sessionManager,
IUserDataManager userDataManager,
@ -54,6 +57,7 @@ public class ServerMediator : IServerEntryPoint
_userDataManager = userDataManager;
_logger = loggerFactory.CreateLogger<ServerMediator>();
_playbackPause = new Dictionary<string, bool>();
_traktApi = new TraktApi(loggerFactory.CreateLogger<TraktApi>(), httpClientFactory, appHost, userDataManager, fileSystem);
_libraryManagerEventsHelper = new LibraryManagerEventsHelper(loggerFactory.CreateLogger<LibraryManagerEventsHelper>(), _traktApi);
@ -61,45 +65,48 @@ public class ServerMediator : IServerEntryPoint
}
/// <summary>
///
/// User data was saved.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnUserDataSaved(object sender, UserDataSaveEventArgs e)
/// <param name="sender">The sending entity.</param>
/// <param name="userDataSaveEventArgs">The <see cref="UserDataSaveEventArgs"/>.</param>
private void OnUserDataSaved(object sender, UserDataSaveEventArgs userDataSaveEventArgs)
{
// ignore change events for any reason other than manually toggling played.
if (e.SaveReason != UserDataSaveReason.TogglePlayed)
// Ignore change events for any reason other than manually toggling played.
if (userDataSaveEventArgs.SaveReason != UserDataSaveReason.TogglePlayed)
{
return;
}
if (e.Item != null)
if (userDataSaveEventArgs.Item != null)
{
// determine if user has trakt credentials
var traktUser = UserHelper.GetTraktUser(e.UserId);
// Determine if user has trakt.tv credentials
var traktUser = UserHelper.GetTraktUser(userDataSaveEventArgs.UserId);
// Can't progress
if (traktUser == null || !_traktApi.CanSync(e.Item, traktUser))
// Can't progress if user has no trakt.tv credentials
if (traktUser == null || !_traktApi.CanSync(userDataSaveEventArgs.Item, traktUser))
{
return;
}
if (!traktUser.PostSetWatched && !traktUser.PostSetUnwatched)
{
// User doesn't want to post any status changes at all.
// User doesn't want to post any status changes at all
return;
}
// We have a user who wants to post updates and the item is in a trakt monitored location.
_userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(e, traktUser);
// We have a user who wants to post updates and the item is in a trakt.tv monitored location
_userDataManagerEventsHelper.ProcessUserDataSaveEventArgs(userDataSaveEventArgs, traktUser);
}
}
/// <inheritdoc />
/// <summary>
/// Run observer tasks for observed events.
/// </summary>
public Task RunAsync()
{
_userDataManager.UserDataSaved += OnUserDataSaved;
_sessionManager.PlaybackStart += KernelPlaybackStart;
_sessionManager.PlaybackProgress += KernelPlaybackProgress;
_sessionManager.PlaybackStopped += KernelPlaybackStopped;
_libraryManager.ItemAdded += LibraryManagerItemAdded;
_libraryManager.ItemRemoved += LibraryManagerItemRemoved;
@ -107,95 +114,205 @@ public class ServerMediator : IServerEntryPoint
}
/// <summary>
///
/// Library item was removed.
/// Let trakt.tv know which item was removed from the user's library.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
/// <param name="sender">The sending entity.</param>
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private void LibraryManagerItemRemoved(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series))
if (!(itemChangeEventArgs.Item is Movie) && !(itemChangeEventArgs.Item is Episode) && !(itemChangeEventArgs.Item is Series))
{
return;
}
if (e.Item.LocationType == LocationType.Virtual)
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
_libraryManagerEventsHelper.QueueItem(e.Item, EventType.Remove);
_libraryManagerEventsHelper.QueueItem(itemChangeEventArgs.Item, EventType.Remove);
}
/// <summary>
///
/// Library item was added.
/// Let trakt.tv know which item was added to the user's library.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
/// <param name="sender">The sending entity.</param>
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private void LibraryManagerItemAdded(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
// Don't do anything if it's not a supported media type
if (!(e.Item is Movie) && !(e.Item is Episode) && !(e.Item is Series))
if (!(itemChangeEventArgs.Item is Movie) && !(itemChangeEventArgs.Item is Episode) && !(itemChangeEventArgs.Item is Series))
{
return;
}
if (e.Item.LocationType == LocationType.Virtual)
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
{
return;
}
_libraryManagerEventsHelper.QueueItem(e.Item, EventType.Add);
_libraryManagerEventsHelper.QueueItem(itemChangeEventArgs.Item, EventType.Add);
}
/// <summary>
/// Let Trakt.tv know the user has started to watch something
/// Media playback has startet.
/// Let trakt.tv know that the user has started playback.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void KernelPlaybackStart(object sender, PlaybackProgressEventArgs e)
/// <param name="sender">The sending entity.</param>
/// <param name="playbackProgressEventArgs">The <see cref="PlaybackProgressEventArgs"/>.</param>
private async void KernelPlaybackStart(object sender, PlaybackProgressEventArgs playbackProgressEventArgs)
{
try
{
_logger.LogInformation("Playback Started");
_logger.LogDebug("Playback started");
if (e.Users == null || !e.Users.Any() || e.Item == null)
if (playbackProgressEventArgs.Users == null || !playbackProgressEventArgs.Users.Any() || playbackProgressEventArgs.Item == null)
{
_logger.LogError("Event details incomplete. Cannot process current media");
return;
}
foreach (var user in e.Users)
foreach (var user in playbackProgressEventArgs.Users)
{
// Since Jellyfin supports user profiles we need to do a user lookup every time something starts
var traktUser = UserHelper.GetTraktUser(user);
if (traktUser == null)
{
_logger.LogDebug("Could not match user {User} with any stored trakt.tv credentials.", user.Username);
continue;
}
if (!traktUser.Scrobble)
{
_logger.LogDebug("User {User} disabled scrobbling to trakt.", user.Username);
continue;
}
if (!_traktApi.CanSync(playbackProgressEventArgs.Item, traktUser))
{
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackProgressEventArgs.Item.Name, user.Username);
continue;
}
var video = playbackProgressEventArgs.Item as Video;
var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ?
(float)(playbackProgressEventArgs.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
_logger.LogDebug("User {User} started watching item {Item}.", user.Username, playbackProgressEventArgs.Item.Name);
try
{
if (video is Movie movie)
{
_logger.LogDebug("Sending movie playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendMovieStatusUpdateAsync(
movie,
MediaStatus.Watching,
traktUser,
progressPercent).ConfigureAwait(false);
_playbackPause[traktUser.LinkedMbUserId] = false;
}
else if (video is Episode episode)
{
_logger.LogDebug("Sending episode playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendEpisodeStatusUpdateAsync(
episode,
MediaStatus.Watching,
traktUser,
progressPercent).ConfigureAwait(false);
_playbackPause[traktUser.LinkedMbUserId] = false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while sending a playback status update to trakt.tv for user {User}.", user.Username);
}
}
}
/// <summary>
/// Media playback has progressed.
/// Let trakt.tv know that the user has progressed in playback.
/// </summary>
/// <param name="sender">The sending entity.</param>
/// <param name="playbackProgressEventArgs">The <see cref="PlaybackProgressEventArgs"/>.</param>
private async void KernelPlaybackProgress(object sender, PlaybackProgressEventArgs playbackProgressEventArgs)
{
_logger.LogDebug("Playback progressed");
if (playbackProgressEventArgs.Users == null || !playbackProgressEventArgs.Users.Any() || playbackProgressEventArgs.Item == null)
{
_logger.LogError("Event details incomplete. Cannot process current media");
return;
}
foreach (var user in playbackProgressEventArgs.Users)
{
// Since Emby is user profile friendly, I'm going to need to do a user lookup every time something starts
var traktUser = UserHelper.GetTraktUser(user);
if (traktUser == null)
{
_logger.LogInformation("Could not match user with any stored credentials");
_logger.LogDebug("Could not match user {User} with any stored trakt.tv credentials.", user.Username);
continue;
}
if (!traktUser.Scrobble)
{
_logger.LogDebug("User {User} disabled scrobbling to trakt.", user.Username);
continue;
}
if (!_traktApi.CanSync(e.Item, traktUser))
if (!_traktApi.CanSync(playbackProgressEventArgs.Item, traktUser))
{
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackProgressEventArgs.Item.Name, user.Username);
continue;
}
_logger.LogDebug("{UseId} appears to be monitoring {Path}", traktUser.LinkedMbUserId, e.Item.Path);
var video = e.Item as Video;
var video = playbackProgressEventArgs.Item as Video;
var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ?
(float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
(float)(playbackProgressEventArgs.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
_logger.LogDebug("User {User} progressed watching item {Item}.", user.Username, playbackProgressEventArgs.Item.Name);
try
{
if (playbackProgressEventArgs.IsPaused)
{
_logger.LogDebug("Playback paused");
if (video is Movie movie)
{
_logger.LogDebug("Send movie status update");
_logger.LogDebug("Sending movie playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendMovieStatusUpdateAsync(
movie,
MediaStatus.Paused,
traktUser,
progressPercent).ConfigureAwait(false);
_playbackPause[traktUser.LinkedMbUserId] = true;
}
else if (video is Episode episode)
{
_logger.LogDebug("Sending episode playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendEpisodeStatusUpdateAsync(
episode,
MediaStatus.Paused,
traktUser,
progressPercent).ConfigureAwait(false);
_playbackPause[traktUser.LinkedMbUserId] = true;
}
}
else
{
_playbackPause.TryGetValue(traktUser.LinkedMbUserId, out bool paused);
if (paused)
{
_logger.LogDebug("Playback resumed");
if (video is Movie movie)
{
_logger.LogDebug("Sending movie playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendMovieStatusUpdateAsync(
movie,
MediaStatus.Watching,
@ -204,74 +321,74 @@ public class ServerMediator : IServerEntryPoint
}
else if (video is Episode episode)
{
_logger.LogDebug("Send episode status update");
_logger.LogDebug("Sending episode playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendEpisodeStatusUpdateAsync(
episode,
MediaStatus.Watching,
traktUser,
progressPercent).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled sending status update");
_playbackPause[traktUser.LinkedMbUserId] = false;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending watching status update");
_logger.LogError(ex, "Exception occurred while sending a playback status update to trakt.tv for user {User}.", user.Username);
}
}
}
/// <summary>
/// Media playback has stopped. Depending on playback progress, let Trakt.tv know the user has
/// completed watching the item.
/// Media playback has stopped.
/// Depending on playback progress, let trakt.tv know the user has completed watching the item.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void KernelPlaybackStopped(object sender, PlaybackStopEventArgs e)
/// <param name="playbackStoppedEventArgs"></param>
private async void KernelPlaybackStopped(object sender, PlaybackStopEventArgs playbackStoppedEventArgs)
{
if (e.Users == null || !e.Users.Any() || e.Item == null)
if (playbackStoppedEventArgs.Users == null || !playbackStoppedEventArgs.Users.Any() || playbackStoppedEventArgs.Item == null)
{
_logger.LogError("Event details incomplete. Cannot process current media");
return;
}
try
{
_logger.LogInformation("Playback Stopped");
_logger.LogInformation("Playback stopped");
foreach (var user in e.Users)
foreach (var user in playbackStoppedEventArgs.Users)
{
var traktUser = UserHelper.GetTraktUser(user);
if (traktUser == null)
{
_logger.LogError("Could not match trakt user");
_logger.LogDebug("Could not match user {User} with any stored trakt.tv credentials.", user.Username);
continue;
}
if (!traktUser.Scrobble)
{
_logger.LogDebug("User {User} disabled scrobbling to trakt.", user.Username);
continue;
}
if (!_traktApi.CanSync(e.Item, traktUser))
if (!_traktApi.CanSync(playbackStoppedEventArgs.Item, traktUser))
{
_logger.LogDebug("Syncing playback for {Item} is forbidden for user {User}.", playbackStoppedEventArgs.Item.Name, user.Username);
continue;
}
var video = e.Item as Video;
if (e.PlayedToCompletion)
{
_logger.LogInformation("Item is played. Scrobble");
var video = playbackStoppedEventArgs.Item as Video;
try
{
if (playbackStoppedEventArgs.PlayedToCompletion)
{
_logger.LogDebug("User {User} completed watching item {Item}. Scrobbling.", user.Username, playbackStoppedEventArgs.Item.Name);
if (video is Movie movie)
{
_logger.LogDebug("Sending movie playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendMovieStatusUpdateAsync(
movie,
MediaStatus.Stop,
@ -280,6 +397,7 @@ public class ServerMediator : IServerEntryPoint
}
else if (video is Episode episode)
{
_logger.LogDebug("Sending episode playback status update to trakt.tv for user {User}.", user.Username);
await _traktApi.SendEpisodeStatusUpdateAsync(
episode,
MediaStatus.Stop,
@ -287,16 +405,12 @@ public class ServerMediator : IServerEntryPoint
100).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception handled sending status update");
}
}
else
{
var progressPercent = video.RunTimeTicks.HasValue && video.RunTimeTicks != 0 ?
(float)(e.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
_logger.LogInformation("Item Not fully played. Tell trakt.tv we are no longer watching but don't scrobble");
(float)(playbackStoppedEventArgs.PlaybackPositionTicks ?? 0) / video.RunTimeTicks.Value * 100.0f : 0.0f;
_logger.LogDebug("User {User} didn't watch item {Item} until the end. Not scrobbling but stopping playback and sending current playback position.", user.Username, playbackStoppedEventArgs.Item.Name);
if (video is Movie movie)
{
@ -307,11 +421,13 @@ public class ServerMediator : IServerEntryPoint
await _traktApi.SendEpisodeStatusUpdateAsync(video as Episode, MediaStatus.Stop, traktUser, progressPercent).ConfigureAwait(false);
}
}
}
_playbackPause[traktUser.LinkedMbUserId] = false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending scrobble");
_logger.LogError(ex, "Exception occurred while sending a playback status update to trakt.tv for user {User}.", user.Username);
}
}
}
@ -322,6 +438,10 @@ public class ServerMediator : IServerEntryPoint
GC.SuppressFinalize(this);
}
/// <summary>
/// Removes event subscriptions on dispose.
/// </summary>
/// <param name="disposing"><see cref="bool"/> indicating if object is currently disposed</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
@ -337,4 +457,5 @@ public class ServerMediator : IServerEntryPoint
_userDataManagerEventsHelper.Dispose();
}
}
}
}