using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Jellyfin.Plugin.Simkl.API; using Jellyfin.Plugin.Simkl.API.Exceptions; using Jellyfin.Plugin.Simkl.Configuration; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Simkl.Services { /// /// Playback progress scrobbler. /// public class PlaybackScrobbler : IServerEntryPoint { private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions private readonly ILogger _logger; private readonly Dictionary _lastScrobbled; // Library ID of last scrobbled item private readonly SimklApi _simklApi; private DateTime _nextTry; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the . public PlaybackScrobbler( ISessionManager sessionManager, ILogger logger, SimklApi simklApi) { _sessionManager = sessionManager; _logger = logger; _simklApi = simklApi; _lastScrobbled = new Dictionary(); _nextTry = DateTime.UtcNow; } /// public Task RunAsync() { _sessionManager.PlaybackProgress += OnPlaybackProgress; _sessionManager.PlaybackStopped += OnPlaybackStopped; return Task.CompletedTask; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Dispose. /// /// Dispose all resources. protected virtual void Dispose(bool disposing) { if (disposing) { _sessionManager.PlaybackProgress -= OnPlaybackProgress; _sessionManager.PlaybackStopped -= OnPlaybackStopped; } } private static bool CanBeScrobbled(UserConfig config, SessionInfo session, BaseItemDto mediaInfo, bool playedToCompletion) { if (!playedToCompletion) { if (session.NowPlayingItem.RunTimeTicks != null) { var percentageWatched = session.PlayState.PositionTicks / (float)session.NowPlayingItem.RunTimeTicks * 100f; // If percentage watched is below minimum, can't scrobble if (percentageWatched < config.ScrobblePercentage) { return false; } } // If it's below minimum length, can't scrobble if (session.NowPlayingItem.RunTimeTicks < 60 * 10000 * config.MinLength) { return false; } } return mediaInfo.Type switch { nameof(Movie) => config.ScrobbleMovies, nameof(Episode) => config.ScrobbleShows, _ => false }; } private async void OnPlaybackProgress(object? sessions, PlaybackProgressEventArgs e) { if (DateTime.UtcNow < _nextTry) { return; } _nextTry = DateTime.UtcNow.AddSeconds(30); await ScrobbleSession(e, false); } private async void OnPlaybackStopped(object? sessions, PlaybackStopEventArgs e) { await ScrobbleSession(e, e.PlayedToCompletion); } private async Task ScrobbleSession(PlaybackProgressEventArgs eventArgs, bool playedToCompletion) { try { var userId = eventArgs.Session.UserId; var userConfig = SimklPlugin.Instance?.Configuration.GetByGuid(userId); if (userConfig == null || string.IsNullOrEmpty(userConfig.UserToken)) { _logger.LogError( "Can't scrobble: User {UserName} not logged in ({UserConfigStatus})", eventArgs.Session.UserName, userConfig == null); return; } if (!CanBeScrobbled(userConfig, eventArgs.Session, eventArgs.MediaInfo, playedToCompletion)) { return; } if (_lastScrobbled.ContainsKey(eventArgs.Session.Id) && _lastScrobbled[eventArgs.Session.Id] == eventArgs.MediaInfo.Id) { _logger.LogDebug("Already scrobbled {ItemName} for {UserName}", eventArgs.MediaInfo.Name, eventArgs.Session.UserName); return; } _logger.LogInformation( "Trying to scrobble {Name} ({NowPlayingId}) for {UserName} ({UserId}) - {PlayingItemPath} on {SessionId}", eventArgs.MediaInfo.Name, eventArgs.MediaInfo.Id, eventArgs.Session.UserName, userId, eventArgs.MediaInfo.Path, eventArgs.Session.Id); var response = await _simklApi.MarkAsWatched(eventArgs.MediaInfo, userConfig.UserToken); if (response.Success) { _logger.LogDebug("Scrobbled without errors"); _lastScrobbled[eventArgs.Session.Id] = eventArgs.MediaInfo.Id; } } catch (InvalidTokenException) { _logger.LogDebug("Deleted user token"); } catch (InvalidDataException ex) { _logger.LogError(ex, "Couldn't scrobble"); _lastScrobbled[eventArgs.Session.Id] = eventArgs.MediaInfo.Id; } catch (Exception ex) { _logger.LogError(ex, "Caught unknown exception while trying to scrobble"); } } } }