From bcfc4332b68618bc18debf462eb83f568142b436 Mon Sep 17 00:00:00 2001 From: lukepulverenti <3607463+LukePulverenti@users.noreply.github.com> Date: Sun, 2 Mar 2025 23:58:55 -0500 Subject: [PATCH] update to multi-user configs --- Simkl-Emby/API/ServerEndpoint.cs | 14 - Simkl-Emby/API/SimklApi.cs | 22 +- .../Configuration/PluginConfiguration.cs | 29 +- Simkl-Emby/Configuration/configPage.html | 261 ------------------ Simkl-Emby/Configuration/simkl.html | 47 ++++ Simkl-Emby/Configuration/simkl.js | 80 ++++++ Simkl-Emby/Configuration/simkladmin.html | 16 ++ Simkl-Emby/Configuration/simkladmin.js | 16 ++ Simkl-Emby/Plugin.cs | 32 ++- Simkl-Emby/Services/Scrobbler.cs | 60 +++- Simkl-Emby/Simkl.csproj | 16 +- Simkl-Emby/Simkl.sln | 25 ++ Simkl-Emby/SimklFeature.cs | 26 ++ 13 files changed, 332 insertions(+), 312 deletions(-) delete mode 100644 Simkl-Emby/Configuration/configPage.html create mode 100644 Simkl-Emby/Configuration/simkl.html create mode 100644 Simkl-Emby/Configuration/simkl.js create mode 100644 Simkl-Emby/Configuration/simkladmin.html create mode 100644 Simkl-Emby/Configuration/simkladmin.js create mode 100644 Simkl-Emby/Simkl.sln create mode 100644 Simkl-Emby/SimklFeature.cs diff --git a/Simkl-Emby/API/ServerEndpoint.cs b/Simkl-Emby/API/ServerEndpoint.cs index 80c3a19..69dd996 100644 --- a/Simkl-Emby/API/ServerEndpoint.cs +++ b/Simkl-Emby/API/ServerEndpoint.cs @@ -22,14 +22,6 @@ public class GetPinStatus : IReturn public string user_code { get; set; } } - [Route("/Simkl/users/settings/{userId}", "GET")] - public class GetUserSettings : IReturn - { - // Note: In the future, when we'll have config for more than one user, we'll use a parameter - [ApiMember(Name = "id", Description = "emby's user id", IsRequired = true, DataType = "Guid", ParameterType = "path", Verb = "GET")] - public string userId { get; set; } - } - class ServerEndpoint : IService, IHasResultFactory { public IHttpResultFactory ResultFactory { get; set; } @@ -55,11 +47,5 @@ public CodeStatusResponse Get(GetPinStatus request) { return _api.getCodeStatus(request.user_code).Result; } - - public UserSettings Get(GetUserSettings request) - { - _logger.Debug(_json.SerializeToString(request)); - return _api.getUserSettings(Plugin.Instance.Configuration.getByGuid(request.userId).userToken).Result; - } } } diff --git a/Simkl-Emby/API/SimklApi.cs b/Simkl-Emby/API/SimklApi.cs index 0337780..84380b1 100644 --- a/Simkl-Emby/API/SimklApi.cs +++ b/Simkl-Emby/API/SimklApi.cs @@ -11,6 +11,9 @@ using Simkl.Api.Responses; using Simkl.Api.Exceptions; using MediaBrowser.Model.Dto; +using Simkl.Configuration; +using Emby.Web.GenericEdit.Validation; +using MediaBrowser.Controller.Library; namespace Simkl.Api { @@ -20,6 +23,7 @@ public class SimklApi private readonly IJsonSerializer _json; private readonly ILogger _logger; private readonly IHttpClient _httpClient; + private readonly IUserManager _userManager; /* BASIC API THINGS */ public const string BASE_URL = @"https://api.simkl.com"; @@ -50,11 +54,12 @@ private HttpRequestOptions GetOptions(string userToken = null) return options; } - public SimklApi(IJsonSerializer json, ILogger logger, IHttpClient httpClient) + public SimklApi(IJsonSerializer json, ILogger logger, IHttpClient httpClient, IUserManager userManager) { _json = json; _logger = logger; _httpClient = httpClient; + _userManager = userManager; } public async Task getCode() @@ -131,10 +136,10 @@ private static SimklHistory createHistoryFromItem(BaseItemDto item) { } /* NOW EVERYTHING RELATED TO SCROBBLING */ - public async Task<(bool success, BaseItemDto item)> markAsWatched(BaseItemDto item, string userToken) + public async Task<(bool success, BaseItemDto item)> markAsWatched(BaseItemDto item, UserConfig userConfig, long embyUserId) { SimklHistory history = createHistoryFromItem(item); - SyncHistoryResponse r = await SyncHistoryAsync(history, userToken); + SyncHistoryResponse r = await SyncHistoryAsync(history, userConfig, embyUserId); _logger.Debug("Response: " + _json.SerializeToString(r)); if (history.movies.Count == r.added.movies && history.shows.Count == r.added.shows) return (true, item); @@ -148,7 +153,7 @@ private static SimklHistory createHistoryFromItem(BaseItemDto item) { (history, item) = await getHistoryFromFileName(item, false); } - r = await SyncHistoryAsync(history, userToken); + r = await SyncHistoryAsync(history, userConfig, embyUserId); _logger.Debug("Response: " + _json.SerializeToString(r)); return (history.movies.Count == r.added.movies && history.shows.Count == r.added.shows, item); @@ -160,14 +165,19 @@ private static SimklHistory createHistoryFromItem(BaseItemDto item) { /// History object /// User token /// - public async Task SyncHistoryAsync(SimklHistory history, string userToken) + public async Task SyncHistoryAsync(SimklHistory history, UserConfig userConfig, long embyUserId) { + var userToken = userConfig.userToken; + try { _logger.Info("Syncing History: " + _json.SerializeToString(history)); return _json.DeserializeFromStream(await _post("/sync/history", userToken, history)); } catch (MediaBrowser.Model.Net.HttpException e) when (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) { _logger.Error("Invalid user token " + userToken + ", deleting"); - Plugin.Instance.deleteUserToken(userToken); + + userConfig.userToken = null; + + _userManager.SetTypedUserSetting(embyUserId, ConfigurationFactory.ConfigKey, userConfig); throw new InvalidTokenException("Invalid user token " + userToken); } } diff --git a/Simkl-Emby/Configuration/PluginConfiguration.cs b/Simkl-Emby/Configuration/PluginConfiguration.cs index 3c981a3..54a7b87 100644 --- a/Simkl-Emby/Configuration/PluginConfiguration.cs +++ b/Simkl-Emby/Configuration/PluginConfiguration.cs @@ -2,6 +2,10 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Plugins; using System.Linq; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using System.Collections.Generic; namespace Simkl.Configuration { @@ -10,15 +14,28 @@ namespace Simkl.Configuration /// public class PluginConfiguration : BasePluginConfiguration { + // TODO: Delete this a couple weeks after the plugin has gotten updated, to avoid any erroneous upgrades from happening when they shouldn't, and to avoid accidentally writing new code that touches it. public UserConfig[] userConfigs { get; set; } + } - public PluginConfiguration() { - userConfigs = new UserConfig[]{}; - } - - public UserConfig getByGuid(string guid) + public class ConfigurationFactory : IUserConfigurationFactory + { + public IEnumerable GetConfigurations() { - return userConfigs.Where(c => c.guid == guid).FirstOrDefault(); + return new[] + { + new SimklConfigStore + { + ConfigurationType = typeof(UserConfig), + Key = ConfigKey + } + }; } + + public static string ConfigKey = "simkl"; + } + + public class SimklConfigStore : ConfigurationStore + { } } diff --git a/Simkl-Emby/Configuration/configPage.html b/Simkl-Emby/Configuration/configPage.html deleted file mode 100644 index 2be274c..0000000 --- a/Simkl-Emby/Configuration/configPage.html +++ /dev/null @@ -1,261 +0,0 @@ - - - - Simkl's TV Tracker - - -
-
-
-

Simkl's TV Tracker

-
-
- -
- - - -
-
-
- -
- - \ No newline at end of file diff --git a/Simkl-Emby/Configuration/simkl.html b/Simkl-Emby/Configuration/simkl.html new file mode 100644 index 0000000..7712ce2 --- /dev/null +++ b/Simkl-Emby/Configuration/simkl.html @@ -0,0 +1,47 @@ +
+
+ +
+ +
+

It seems you are not logged in, do you wish to log in?

+ + +
+
+

Logging In

+
+

+ 900 seconds remaining + +
+
+

Hello again USERNAME!

+ +

Scrobbling options:

+
+ +
+
+ +
+
+ +
+ Percentage watched needed to scrobble +
+
+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/Simkl-Emby/Configuration/simkl.js b/Simkl-Emby/Configuration/simkl.js new file mode 100644 index 0000000..f1dc405 --- /dev/null +++ b/Simkl-Emby/Configuration/simkl.js @@ -0,0 +1,80 @@ +define([ + "loading", + "baseView", + "emby-input", + "emby-button", + "emby-select", + "emby-checkbox" + +], function (loading, BaseView) { + + "use strict"; + + function fetchExistingConfiguration(userId) { + + return ApiClient.getTypedUserSettings(userId, 'simkl'); + } + + function loadConfiguration(userId, form) { + + fetchExistingConfiguration(userId).then(function (currentUserConfig) { + + // TODO: fill user with data from currentUserConfig + + loading.hide(); + }); + } + + function onSubmit(ev) { + ev.preventDefault(); + loading.show(); + + var instance = this; + + var form = ev.currentTarget; + + var currentUserId = instance.params.userId; + + fetchExistingConfiguration(currentUserId).then(function (currentUserConfig) { + + // TODO: update currentUserConfig with data from UI + + + ApiClient.updateTypedUserSettings(currentUserId, 'simkl', currentUserConfig).then( + function (result) { + Dashboard.processPluginConfigurationUpdateResult(result); + loadConfiguration(currentUserId, form); + } + ); + }); + + return false; + } + + function View(view, params) { + + BaseView.apply(this, arguments); + + var form = view.querySelector("form"); + form.addEventListener("submit", onSubmit.bind(this)); + } + + Object.assign(View.prototype, BaseView.prototype); + + View.prototype.onResume = function (options) { + + BaseView.prototype.onResume.apply(this, arguments); + + if (options.refresh) { + loading.show(); + + var view = this.view; + var form = view.querySelector("form"); + var instance = this; + + loadConfiguration(instance.params.userId, form); + } + }; + + return View; +}); diff --git a/Simkl-Emby/Configuration/simkladmin.html b/Simkl-Emby/Configuration/simkladmin.html new file mode 100644 index 0000000..1754fd5 --- /dev/null +++ b/Simkl-Emby/Configuration/simkladmin.html @@ -0,0 +1,16 @@ +
+
+ +
+

Simkl settings have been moved to user settings so that each user can setup their own Simkl integration.

+ +

To setup Simkl, users can click their user profile icon, then locate the Simkl option within settings.

+ +

As the admin, you can still setup Simkl for your users by going to the Users management screen, then click on a user, then click Edit this user's profile.

+ +

+ Note to devs: Remove this page after a while, once users have adjusted to the new way of getting to Simkl user settings. +

+
+
+
\ No newline at end of file diff --git a/Simkl-Emby/Configuration/simkladmin.js b/Simkl-Emby/Configuration/simkladmin.js new file mode 100644 index 0000000..10ddcf0 --- /dev/null +++ b/Simkl-Emby/Configuration/simkladmin.js @@ -0,0 +1,16 @@ +define([ + "baseView" + +], function (BaseView) { + + "use strict"; + + function View(view, params) { + + BaseView.apply(this, arguments); + } + + Object.assign(View.prototype, BaseView.prototype); + + return View; +}); diff --git a/Simkl-Emby/Plugin.cs b/Simkl-Emby/Plugin.cs index f4c65b4..b15e12f 100644 --- a/Simkl-Emby/Plugin.cs +++ b/Simkl-Emby/Plugin.cs @@ -15,6 +15,9 @@ namespace Simkl { public class Plugin : BasePlugin, IHasWebPages, IHasThumbImage { + public static string StaticId = "Simkl"; + public static string StaticName = "Simkl"; + // public override string Name { get { return "Simkl TV Tracker"; } } // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-operator#expression-body-definition public override string Name => "Simkl TV Tracker"; @@ -34,8 +37,26 @@ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) { new PluginPageInfo { - Name = "Simkl", - EmbeddedResourcePath = "Simkl.Configuration.configPage.html" + Name = "simkl_admin", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.simkladmin.html" + }, + new PluginPageInfo + { + Name = "simkljs", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.simkl.js" + }, + new PluginPageInfo + { + Name = StaticName, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.simkl.html", + EnableInUserMenu = true, + //EnableInMainMenu = true, + FeatureId = StaticId + }, + new PluginPageInfo + { + Name = "simkladminjs", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.simkladmin.js" } }; @@ -48,12 +69,5 @@ public ImageFormat ThumbImageFormat { public Stream GetThumbImage() { return GetType().Assembly.GetManifestResourceStream("Simkl.emby_thumb.jpg"); } - - public void deleteUserToken(string userToken) { - foreach (UserConfig config in Configuration.userConfigs) { - if (config.userToken == userToken) config.userToken = ""; - } - SaveConfiguration(); - } } } diff --git a/Simkl-Emby/Services/Scrobbler.cs b/Simkl-Emby/Services/Scrobbler.cs index 4c3d920..f0f577f 100644 --- a/Simkl-Emby/Services/Scrobbler.cs +++ b/Simkl-Emby/Services/Scrobbler.cs @@ -2,7 +2,7 @@ using System.Threading; using System.IO; using System.Collections.Generic; -using System.Linq; +using System.Linq; using Simkl.Api; using Simkl.Api.Exceptions; @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using Simkl.Configuration; +using Simkl.Api.Objects; namespace Simkl.Services { @@ -27,6 +28,7 @@ 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 IUserManager _userManager; //private readonly INotificationManager _notifications; private SimklApi _api; private Dictionary lastScrobbled; // Library ID of last scrobbled item @@ -34,29 +36,61 @@ public class Scrobbler : IServerEntryPoint // public static Scrobbler Instance; Instance = this public Scrobbler(IJsonSerializer json, ISessionManager sessionManager, ILogManager logManager, - IHttpClient httpClient/*, INotificationManager notifications*/) + IHttpClient httpClient/*, INotificationManager notifications*/, IUserManager userManager) { _json = json; _sessionManager = sessionManager; _logger = logManager.GetLogger("Simkl Scrobbler"); - // _notifications = notifications; - _api = new SimklApi(json, _logger, httpClient); - lastScrobbled = new Dictionary(); + // _notifications = notifications; + _api = new SimklApi(json, _logger, httpClient, userManager); + lastScrobbled = new Dictionary(); nextTry = DateTime.UtcNow; + _userManager = userManager; } public void Run() { + UpgradeDataIfNeeded(); _sessionManager.PlaybackProgress += embyPlaybackProgress; } + private void UpgradeDataIfNeeded() + { + // TODO: Delete this a couple weeks after the plugin has gotten updated, to avoid any erroneous upgrades from happening when they shouldn't. + + var userConfigs = Plugin.Instance.Configuration.userConfigs; + if (userConfigs != null) + { + foreach (var userConfig in userConfigs) + { + try + { + UpgradeData(userConfig); + } + catch (Exception ex) + { + _logger.ErrorException("Error upgrading user config", ex); + } + } + + Plugin.Instance.Configuration.userConfigs = null; + Plugin.Instance.SaveConfiguration(); + } + } + + private void UpgradeData(UserConfig userConfig) + { + // what emby user does this belong to? + } + public void Dispose() { _sessionManager.PlaybackProgress -= embyPlaybackProgress; _api = null; } - public static bool canBeScrobbled(UserConfig config, SessionInfo session) { + public static bool canBeScrobbled(UserConfig config, SessionInfo session) + { float percentageWatched = (float)(session.PlayState.PositionTicks) / (float)(session.NowPlayingItem.RunTimeTicks) * 100f; // If percentage watched is below minimum, can't scrobble @@ -80,17 +114,20 @@ public static bool canBeScrobbled(UserConfig config, SessionInfo session) { return false; }*/ - + private async void embyPlaybackProgress(object sessions, PlaybackProgressEventArgs e) { - try{ + try + { string sid = e.PlaySessionId, uid = e.Session.UserId, npid = e.Session.NowPlayingItem.Id; try { if (DateTime.UtcNow < nextTry) return; nextTry = DateTime.UtcNow.AddSeconds(30); - UserConfig userConfig = Plugin.Instance.PluginConfiguration.getByGuid(uid); + var embyUserId = _userManager.GetInternalId(uid); + + var userConfig = (UserConfig)_userManager.GetTypedUserSetting(embyUserId, ConfigurationFactory.ConfigKey); if (userConfig == null || userConfig.userToken == "") { _logger.Error("Can't scrobble: User " + e.Session.UserName + " not logged in (" + (userConfig == null) + ")"); @@ -112,7 +149,7 @@ private async void embyPlaybackProgress(object sessions, PlaybackProgressEventAr e.Session.NowPlayingItem.Path, sid); _logger.Debug("Item: " + _json.SerializeToString(e.MediaInfo)); - var response = await _api.markAsWatched(e.MediaInfo, userConfig.userToken); + var response = await _api.markAsWatched(e.MediaInfo, userConfig, embyUserId); if (response.success) { _logger.Debug("Scrobbled without errors"); @@ -140,7 +177,8 @@ await _notifications.SendNotification( _logger.Error(ex.StackTrace); } } - catch (Exception expt){ + catch (Exception expt) + { _logger.Error("No object: " + expt.Message); _logger.Error(expt.StackTrace); } diff --git a/Simkl-Emby/Simkl.csproj b/Simkl-Emby/Simkl.csproj index 7d44aee..b75ee6a 100644 --- a/Simkl-Emby/Simkl.csproj +++ b/Simkl-Emby/Simkl.csproj @@ -2,8 +2,8 @@ netstandard2.0 - 1.0.6.0 - 1.0.6.0 + 1.0.8.0 + 1.0.8.0 David Davó Simkl 0.1.6 @@ -17,18 +17,24 @@ - + - + + + + - + + + + diff --git a/Simkl-Emby/Simkl.sln b/Simkl-Emby/Simkl.sln new file mode 100644 index 0000000..a858304 --- /dev/null +++ b/Simkl-Emby/Simkl.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35013.160 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Simkl", "Simkl.csproj", "{7538A9A1-122C-406D-9E63-FA67F14BC946}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7538A9A1-122C-406D-9E63-FA67F14BC946}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7538A9A1-122C-406D-9E63-FA67F14BC946}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7538A9A1-122C-406D-9E63-FA67F14BC946}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7538A9A1-122C-406D-9E63-FA67F14BC946}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {85618944-88F4-4EC7-9E0A-A56876E5CCE8} + EndGlobalSection +EndGlobal diff --git a/Simkl-Emby/SimklFeature.cs b/Simkl-Emby/SimklFeature.cs new file mode 100644 index 0000000..6dbc6db --- /dev/null +++ b/Simkl-Emby/SimklFeature.cs @@ -0,0 +1,26 @@ +using Emby.Features; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Simkl +{ + /// + /// This registers a feature in user permissions for each user, so that admins can restrict use of it + /// + public class SimklFeature : IFeatureFactory + { + public List GetFeatureInfos(string language) + { + return new List + { + new FeatureInfo + { + Id = Plugin.StaticId, + Name = Plugin.StaticName, + FeatureType = FeatureType.User + } + }; + } + } +}