diff --git a/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj b/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj
index 580728e..a84f577 100644
--- a/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj
+++ b/Jellyfin.Plugin.Simkl/Jellyfin.Plugin.Simkl.csproj
@@ -1,21 +1,22 @@
- netstandard2.1
+ net5.0
1.0.0.0
1.0.0.0
true
true
- CA1707;CA1819;SA1300
+ enable
-
+
+
-
+
@@ -28,5 +29,9 @@
../jellyfin.ruleset
+
+
+
+
diff --git a/Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs b/Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs
new file mode 100644
index 0000000..a96e4d0
--- /dev/null
+++ b/Jellyfin.Plugin.Simkl/PluginServiceRegistrator.cs
@@ -0,0 +1,16 @@
+using Jellyfin.Plugin.Simkl.API;
+using MediaBrowser.Common.Plugins;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Jellyfin.Plugin.Simkl
+{
+ ///
+ public class PluginServiceRegistrator : IPluginServiceRegistrator
+ {
+ ///
+ public void RegisterServices(IServiceCollection serviceCollection)
+ {
+ serviceCollection.AddScoped
();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs b/Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs
new file mode 100644
index 0000000..ca7209e
--- /dev/null
+++ b/Jellyfin.Plugin.Simkl/Services/PlaybackScrobbler.cs
@@ -0,0 +1,178 @@
+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");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Jellyfin.Plugin.Simkl/Services/Scrobbler.cs b/Jellyfin.Plugin.Simkl/Services/Scrobbler.cs
deleted file mode 100644
index 32b3c10..0000000
--- a/Jellyfin.Plugin.Simkl/Services/Scrobbler.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Plugin.Simkl.API;
-using Jellyfin.Plugin.Simkl.API.Exceptions;
-using Jellyfin.Plugin.Simkl.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Plugin.Simkl.Services
-{
- ///
- public class Scrobbler : IServerEntryPoint
- {
- private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions
- private readonly ILogger _logger;
- private readonly IJsonSerializer _json;
- private readonly INotificationManager _notifications;
- private readonly Dictionary _lastScrobbled; // Library ID of last scrobbled item
- private SimklApi _api;
- private DateTime _nextTry;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- public Scrobbler(
- IJsonSerializer json,
- ISessionManager sessionManager,
- ILoggerFactory loggerFactory,
- IHttpClient httpClient,
- INotificationManager notifications)
- {
- _json = json;
- _sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger();
- _notifications = notifications;
- _api = new SimklApi(json, loggerFactory.CreateLogger(), httpClient);
- _lastScrobbled = new Dictionary();
- _nextTry = DateTime.UtcNow;
- }
-
- ///
- public Task RunAsync()
- {
- _sessionManager.PlaybackProgress += OnPlaybackProgress;
- 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;
- _api = null;
- }
- }
-
- ///
- /// Session can be scrobbled.
- ///
- /// The plugin configuration.
- /// The session.
- /// If session can be scrobbled.
- private static bool CanBeScrobbled(UserConfig config, SessionInfo session)
- {
- 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;
- }
-
- var item = session.FullNowPlayingItem;
- return item switch
- {
- Movie _ => config.ScrobbleMovies,
- Episode _ => config.ScrobbleShows,
- _ => false
- };
- }
-
- private bool CanSendNotification(BaseItemDto item)
- {
- if (item.IsMovie == true || item.Type == "Movie")
- {
- return _notifications.GetNotificationTypes().Any(t => t.Type == SimklNotificationsFactory.NotificationMovieType && t.Enabled);
- }
-
- if (item.IsSeries == true || item.Type == "Episode")
- {
- return _notifications.GetNotificationTypes().Any(t => t.Type == SimklNotificationsFactory.NotificationShowType && t.Enabled);
- }
-
- return false;
- }
-
- private async void OnPlaybackProgress(object sessions, PlaybackProgressEventArgs e)
- {
- var sid = e.PlaySessionId;
- Guid uid = e.Session.UserId, npid = e.Session.NowPlayingItem.Id;
- try
- {
- if (DateTime.UtcNow < _nextTry)
- {
- return;
- }
-
- _nextTry = DateTime.UtcNow.AddSeconds(30);
-
- var userConfig = SimklPlugin.Instance.Configuration.GetByGuid(uid);
- if (userConfig == null || string.IsNullOrEmpty(userConfig.UserToken))
- {
- _logger.LogError("Can't scrobble: User " + e.Session.UserName + " not logged in (" + (userConfig == null) + ")");
- return;
- }
-
- if (!CanBeScrobbled(userConfig, e.Session))
- {
- return;
- }
-
- if (_lastScrobbled.ContainsKey(sid) && _lastScrobbled[sid] == npid)
- {
- _logger.LogDebug("Already scrobbled {0} for {1}", e.Session.NowPlayingItem.Name, e.Session.UserName);
- return;
- }
-
- _logger.LogDebug(_json.SerializeToString(e.Session.NowPlayingItem));
- _logger.LogInformation(
- "Trying to scrobble {0} ({1}) for {2} ({3}) - {4} on {5}",
- e.Session.NowPlayingItem.Name,
- npid,
- e.Session.UserName,
- uid,
- e.Session.NowPlayingItem.Path,
- sid);
-
- var response = await _api.MarkAsWatched(e.MediaInfo, userConfig.UserToken).ConfigureAwait(false);
- if (response.success)
- {
- _logger.LogDebug("Scrobbled without errors");
- _lastScrobbled[sid] = npid;
-
- if (CanSendNotification(response.item))
- {
- await _notifications.SendNotification(
- SimklNotificationsFactory.GetNotificationRequest(response.item, e.Session.UserId),
- e.Session.FullNowPlayingItem,
- CancellationToken.None)
- .ConfigureAwait(false);
- }
- }
- }
- catch (InvalidTokenException)
- {
- _logger.LogDebug("Deleted user token");
- }
- catch (InvalidDataException ex)
- {
- _logger.LogError(ex, "Couldn't scrobble.");
- _lastScrobbled[sid] = npid;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Caught unknown exception while trying to scrobble.");
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs b/Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs
deleted file mode 100644
index b7eae2d..0000000
--- a/Jellyfin.Plugin.Simkl/Services/SimklNotificationsFactory.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-
-namespace Jellyfin.Plugin.Simkl.Services
-{
- ///
- public class SimklNotificationsFactory : INotificationTypeFactory
- {
- ///
- /// Notification category.
- ///
- public const string NotificationCategory = "Simkl Scrobbling";
-
- ///
- /// Notification movie type.
- ///
- public const string NotificationMovieType = "SimklScrobblingMovie";
-
- ///
- /// Notification show type.
- ///
- public const string NotificationShowType = "SimklScrobblingShow";
-
- ///
- public IEnumerable GetNotificationTypes()
- {
- yield return new NotificationTypeInfo
- {
- Type = NotificationMovieType,
- Name = "Scrobbling Movie",
- Category = NotificationCategory,
- Enabled = true,
- IsBasedOnUserEvent = false
- };
-
- yield return new NotificationTypeInfo
- {
- Type = NotificationShowType,
- Name = "Scrobbling TV Show",
- Category = NotificationCategory,
- Enabled = true,
- IsBasedOnUserEvent = false
- };
- }
-
- ///
- /// Get notification request.
- ///
- /// Item.
- /// USer id.
- /// The notification request.
- public static NotificationRequest GetNotificationRequest(BaseItemDto item, Guid userId)
- {
- var nr = new NotificationRequest
- {
- Date = DateTime.UtcNow,
- UserIds = new[] { userId },
- SendToUserMode = SendToUserType.Custom
- };
-
- // TODO: Set url parameter to simkl's movie url
- if (item.IsMovie == true || item.Type == "Movie")
- {
- nr.NotificationType = NotificationMovieType;
- nr.Name = "Movie Scrobbled to Simkl";
- nr.Description = "The movie " + item.Name;
- nr.Description += " has been scrobbled to your account";
- }
-
- if (item.IsSeries == true || item.Type == "Episode")
- {
- nr.NotificationType = NotificationShowType;
- nr.Name = "Episode Scrobbled to Simkl";
- nr.Description = item.SeriesName;
- nr.Description += " S" + item.ParentIndexNumber + ":E" + item.IndexNumber;
- nr.Description += " - " + item.Name;
- nr.Description += " has been scrobbled to your account";
- }
-
- return nr;
- }
- }
-}
\ No newline at end of file
diff --git a/Jellyfin.Plugin.Simkl/SimklPlugin.cs b/Jellyfin.Plugin.Simkl/SimklPlugin.cs
index 254e4db..4b3d0ad 100644
--- a/Jellyfin.Plugin.Simkl/SimklPlugin.cs
+++ b/Jellyfin.Plugin.Simkl/SimklPlugin.cs
@@ -27,13 +27,13 @@ namespace Jellyfin.Plugin.Simkl
///
/// Gets the current instance of the plugin.
///
- public static SimklPlugin Instance { get; private set; }
+ public static SimklPlugin? Instance { get; private set; }
///
public override Guid Id => new Guid("07CAEF58-A94B-4211-A62C-F9774E04EBDB");
///
- public override string Name => "Simkl TV Tracker";
+ public override string Name => "Simkl";
///
public override string Description => "Scrobble your watched Movies, TV Shows and Anime to Simkl and share your progress with friends!";
diff --git a/README.md b/README.md
index 4865a74..15585f0 100644
--- a/README.md
+++ b/README.md
@@ -5,25 +5,11 @@
Repository Url:
https://repo.codyrobibero.dev/manifest.json
-## How to enable notifications when something is marked as watched
-1. On Jellyfin's dashboard, you'll have to go to the bottom and, on expert options, select "Notifications"
-2. There, on section "Simkl Scrobbling", enable both notifications
-
-## How to enable debugging
-To report a bug or an error, we'll need more info to know how to fix it. To send us the needed reports you'll need to
-first enable debug logging.
-
-1. On Jellyfin's dashboard, scroll to the bottom and, on expert options, select "Logs" (right above "Notifications")
-2. Click on "Enable debug logging"
-3. Restart the server and reproduce the error
-
-Now you can contact us to try and fix the problem
-
## Current features
- Multi-user support
- Auto scrobble Movies and TV Shows at given percentage to Simkl
-- Easy login using pin (no more putting passwords with the TV remote)
+- Easy login using pin
- If scrobbling fails, search it using filename using Simkl's API and then scrobble it
-- Send notifications about scrobbling
-Modified for Jellyfin from https://github.com/SIMKL/Emby/
\ No newline at end of file
+## Future features
+- Sync all watch status with Simkl
\ No newline at end of file
diff --git a/build.yaml b/build.yaml
index 516657d..e1698f9 100644
--- a/build.yaml
+++ b/build.yaml
@@ -2,7 +2,7 @@
name: "Simkl"
guid: "07CAEF58-A94B-4211-A62C-F9774E04EBDB"
version: "1.0.0.0"
-targetAbi: "10.6.0.0"
+targetAbi: "10.7.0.0"
owner: "crobibero"
overview: "Scrobble to Simkl"
description: >