Jump to content

Any c# gurus out there? Help needed For ExternalId Class


mickle026

Recommended Posts

mickle026

OK,

I have a problem which I wish to solve, but I alone seem unable to....

I can create ExternalIds without issue, and I can check in my code if the ExternalId Class even exists, but I want to be able to create and destroy these classes depending on what else is loaded elsewhere in a plugin or my other plugins.

 

List the classes

            Type[] types = Assembly.GetExecutingAssembly().GetTypes();
            List<Type> myTypes = new List<Type>();
            foreach (Type t in types)
            {
                if (t.Namespace == "NamespaceImChecking")
                LogInfoTXT(MyLog, DateTime.Now.ToString("dd.MMM.yyy  -  HH'.'mm'.'ss") + $" [Test] {t.Name}");
            }

External Ids Duplicate if I use them in more than one plugin, but I want the option to be there whichever plugin I have installed. 

I have a few ideas.

1.  Create a single plugin that simply adds the external providers I wish to use, but no other functionality.  So they are available in the UI.
(this is the route I have done so far)

2.  Enable and dissable ExternalIds depending on what is already loaded.

3.  A mixture of both, enable/dissable and allow user to config.

 

A mixture of both would better suit my needs, as then I can add config options to the plugin pages to switch them on/off as needed in the webui/menus.

However to do that I would have to create the ExternalId on demand, I looked on line and it shows expandoobject and others, but I am lost here

Does anyone have any clues how to do this? Is it possible?

 

Ie, I want to be able to add a checkbox to the UI to turn on/off an id like this:  ie, enable/dissable a class.

        public class ExternalBluRayComPerson : IExternalId
        {

            public string Name => "blu-ray.com";

            public string ProviderName => "BRC";

            public string Key => "BRC";

            public string UrlFormatString => null;

            public bool Supports(IHasProviderIds item) => item is Person;
        }

 

Link to comment
Share on other sites

It is possible to add code at runtime with Reflection.Emit (complicated), but that won't help you here, because Emby is loading types from plugins at the time when a plugin is loaded and before any plugin code is executed. You could probably get some code running before that happens,  by using a module initializer (complicated again) - but that still wouldn't work, because even when you would create classes using Emit at that point in time, Emby wouldn't get them, because it reads the types from your plugin assembly and the emitted types wouldn't be part of that.

The most natural way for this would be to have a common assembly (library/dll) used by all of your plugins, but Emby doesn't allow your plugin dll to have and bring-in any dependencies. While there is some way (again complicated) to include dependency libs in a plugin, this won't work properly when you have multiple plugins with the same dependency lib.

So, it seems to me that you've already found the best possible way: a separate plugin for managing the IDs. You can "switch" those on and of by returning always 'false' from 'Supports' and returning a nonsense Key (be sure to use different nonsense keys for each).

Edited by softworkz
  • Thanks 2
Link to comment
Share on other sites

mickle026

 

Looks as if the only way for me to prevent duplication in the ui is a standalone dll with them and leave them out of other plugins.

Screenshot2023-05-06at02-52-47MIKE-PC.png.e4f4d44be35f9da2e36a6d599bcdf4db.png

 

Being in the UI part is not strictly necessary, (just convenient) as they work without even being included in the UI, I can add them and read them and write them in code without the class, but I would have prefered them in the UI to be toggled on/off.

 

Thanks for taking the time to reply.

  • Like 1
Link to comment
Share on other sites

Cheesegeezer

Just thinking about this coukd the BasePlugin be loaded and allow for an interface be used for those types? Then the other plugins could set a Task.Delay before initialisation of server entry point. 
 

or is the plugin architecture just purely a reflection loaded type and doesn’t allow for interaction between other plugins.

having said that, there is c# and JS code that allows you to get the options(config) for sure!!! and this can be done by pluginId or by name also. 

not sure if that might help you.

Link to comment
Share on other sites

1 hour ago, mickle026 said:

Looks as if the only way for me to prevent duplication in the ui is a standalone dll with them and leave them out of other plugins.

 

7 hours ago, softworkz said:

The most natural way for this would be to have a common assembly (library/dll) used by all of your plugins, but Emby doesn't allow your plugin dll to have and bring-in any dependencies. While there is some way (again complicated) to include dependency libs in a plugin, this won't work properly when you have multiple plugins with the same dependency lib.

 

Link to comment
Share on other sites

Cheesegeezer
2 minutes ago, softworkz said:

 

 

I hear you chief, as you are part of the core team you have access to way more stuff, “we” plugin devs are quite resourceful in find workarounds. Hehe.

this is why i made suggestions for mickle. I really think the best way is to access the base plugin UI from the other plugins he wants to use. This can be triggers at schedule task run time rather than server entrypoint. 
 

not sure if you agree? I’m also quite interested in this topic. It’s a great one that Mickle has posted

Link to comment
Share on other sites

43 minutes ago, Cheesegeezer said:

or is the plugin architecture just purely a reflection loaded type and doesn’t allow for interaction between other plugins

That's difficult and likely to go wrong and have strange effects. For example, you can't control the order in which Emby loads the plugins and if one plugin requires a reference to another plugin, and that plugin isn't available at load time, then it might not get loaded properly.

Of course you can do things like "dynamic" (not nice) or use some framework-provided mechanism for communication (which would be without strong typing).

Other possibilities would be using a shared file to communicate, named pipes or RPC  (Windows only), the network stack via local loopback interface... That are the first few things that come to my mind.

Link to comment
Share on other sites

Cheesegeezer
3 minutes ago, softworkz said:

That's difficult and likely to go wrong and have strange effects. For example, you can't control the order in which Emby loads the plugins and if one plugin requires a reference to another plugin, and that plugin isn't available at load time, then it might not get loaded properly.

completely agree with this

3 minutes ago, softworkz said:

Of course you can do things like "dynamic" (not nice) or use some framework-provided mechanism for communication (which would be without strong typing).

yup more generic, depends on how much reliance is required from base side… coukd get ugly.

3 minutes ago, softworkz said:

Other possibilities would be using a shared file to communicate, named pipes or RPC  (Windows only), the network stack via local loopback interface... That are the first few things that come to my mind.

Not a good solution, as it’s isolated to one system and Emby is multi OS friendly. 
 

thanks Softy! Appreciate your informative responses 👍👍

Link to comment
Share on other sites

10 minutes ago, Cheesegeezer said:

Not a good solution, as it’s isolated to one system and Emby is multi OS friendly. 

Shared file and IP loopback would work cross-platform.

But here's a better and easier idea (there are really a lot for ways):

                var queue = new ConcurrentQueue<Tuple<string, string>>();
                AppDomain.CurrentDomain.SetData("MyPluginMessageQueue", queue);

All plugins check with .GetData() on load whether the queue exists already, if not it creates on and registers it in the AppDomain.

Now, all plugins - once loaded - will have access to the same queue. That queue can be used to send and receive messages between plugins.

You can achieve working with typed data by serializing (e.g. to JSON). You can have the same classes in multiple plugins (but important: each under a different namespace!).
Then you can serialize and deserialize the data and work with strongly typed classes in all plugins.

That's just a rough example, but it's a very robust and reliable method. 

Edited by softworkz
  • Thanks 1
Link to comment
Share on other sites

Cheesegeezer


 

3 minutes ago, softworkz said:

Shared file and IP loopback would work cross-platform.

But here's a better and easier idea (there are really a lot for ways):

                var queue = new ConcurrentQueue<Tuple<string, string>>();
                AppDomain.CurrentDomain.SetData("MyPluginMessageQueue", queue);

All plugins check with .GetData() on load whether the queue exists already, if not it creates on and registers it in the AppDomain.

Now, all plugins - once loaded - will have access to the same queue. That queue can be used to send and receive messages between plugins.

You can achieve working with typed data by serializing (e.g. to JSON). You can have the same classes in multiple plugins (but important: each under a different namespace!).
Then you can serialize and deserialize the data and work with strongly typed classes in all plugins.

That's just a rough example, but it's a very robust and reliable method. 

Hence… why you are the guru!! 

nice solution!! Hope it help mickle out and it maybe useful for me also in the future.

thanks fella!! Danke!! 

  • Like 1
Link to comment
Share on other sites

4 minutes ago, Cheesegeezer said:

thanks fella!! Danke!! 

Sure, you're welcome!

There's an even easier way, if the communication between plugins doesn't involve a high volume of communication: Emby SDK Reference: IConfigurationManager

You can do as follows:

So essentially, you'd "mis"-use the config store for communication. This would be fine for low-volume communication at least.

Link to comment
Share on other sites

Cheesegeezer
1 hour ago, Cheesegeezer said:

having said that, there is c# and JS code that allows you to get the options(config) for sure!!! and this can be done by pluginId or by name also. 

 

4 minutes ago, softworkz said:

So essentially, you'd "mis"-use the config store for communication. This would be fine for low-volume communication at least

Yep this was my initial thoughts if a low info exchange was required 

  • Like 1
Link to comment
Share on other sites

1 minute ago, Cheesegeezer said:

Yep this was my initial thoughts if a low info exchange was required 

Sorry, I had missed that. 😬

Eventually it really depends on the use case and the things mentioned above should be appropriate for @mickle026's case.

Final note: Of course almost everything is possible in some way, but as this is about Emby, suggesting such things would be like shooting in my own foot, so in this context we really need to stick to reliable methods that are easy to understand and easy to implement (and safe for Emby Server 🙂 )

  • Agree 1
Link to comment
Share on other sites

Cheesegeezer

💯 agree! The reality is to deliver a robust solution.. and using the emby internal Interfaces are the best way to deliver the plugin, as they have been written to ensure stability.

great discussion tho!! And i need to go and look at some concepts I’ve not come across yet… loopbacks is one for sure 👍

  • Agree 1
Link to comment
Share on other sites

mickle026
18 hours ago, softworkz said:

So, it seems to me that you've already found the best possible way: a separate plugin for managing the IDs. You can "switch" those on and of by returning always 'false' from 'Supports' and returning a nonsense Key (be sure to use different nonsense keys for each).

So how do I do this? - its beyond my skill set I believe,  but i am trying - still learning this c# stuff :)

The code below doesn't seem to work

I put some code in the IService Get Function just to test this idea and see If I can get it to work.

[Route("/APID/ToggleId", "GET", Summary = "ToggleId")]
        public class ToggleId : IReturn<string>
        {
            public string Text { get; set; }
        }
        public string Get(ToggleId result)
        {
            Person item = new Person();
            IExternalId toggle = new ExternalBluRayComPerson();
          
            toggle.Supports(item).Equals(false);
            toggle.Key.Equals("Gobbledegook");
        
            return _json.SerializeToString(new ToggleId()
            {
                Text = $"Test Succeeded - Check if the Id is visible in metadata editor!"
            });
        }

 

The ID is still visible, so I am doing something wrong.

 

Thanks

Link to comment
Share on other sites

    public  class MyExternalId : IExternalId
    {
        public static bool IsEnabled { get; set; } = true;

        public string Name
        {
            get { return "My External ID Example"; }
        }

        public string Key
        {
            get
            {
                if (IsEnabled)
                {
                    return "MyExtId";
                }

                return "dkjgfogihrgtoihn";
            }
        }

        public string UrlFormatString
        {
            get { return "http://my.extid.com/api?redir={0}"; }
        }

        public bool Supports(IHasProviderIds item)
        {
            if (IsEnabled)
            {
                return item is Series;
            }
            
            return false;
        }
    }
  • Thanks 1
Link to comment
Share on other sites

mickle026

Ah!

You are creating an IsEnabled option to the provider! - I never even thought of that 🤔 idea!

Thank you so much! 

 

Link to comment
Share on other sites

It's a class and you cannot know when and where any instances of this class are created by Emby. The key to the solution is not that IsEnabled is a property of the provider class. The key is that it's static:.It is not bound to any specific instance of the class - it's rather a global singleton value, and so it doesn't matter when and how many instances Emby may create.

Using statics is generally not a good pattern, but as we're not into professional application design patterns here, this is nice for its simplicity. The static property could be located  anywhere, that doesn't matter. 
Or - like mentioned above - you could store the settings (that indicate which IDs to enable and which not) in the AppDomain or In Emby's Emby SDK Reference: IConfigurationManager.

In the latter case, you would add a constructor to the ID class like follows. This works, because Emby server is using dependency injection to create object instance.

    public  class MyExternalId : IExternalId
    {
        private readonly IConfigurationManager configurationManager;
        private bool? cachedIsEnabled;

        /// <summary>Initializes a new instance of the <see cref="T:System.Object" /> class.</summary>
        public MyExternalId(IConfigurationManager configurationManager)
        {
            this.configurationManager = configurationManager;
        }

        private bool IsEnabled {
            get
            {
                if (!cachedIsEnabled.HasValue)
                {
                    var config = this.configurationManager.GetConfiguration("MyConfigKey") as string;
                    // now determine for the config string whether this ID should be enabled, so true or false for the next line:
                    this.cachedIsEnabled = false;
                }

                return cachedIsEnabled.Value;
            }
        }

        public string Name
        {
            get { return "My External ID Example"; }
        }

        public string Key
        {
            get
            {
                if (this.IsEnabled)
                {
                    return "MyExtId";
                }

                return "dkjgfogihrgtoihn";
            }
        }

        public string UrlFormatString
        {
            get { return "http://my.extid.com/api?redir={0}"; }
        }

        public bool Supports(IHasProviderIds item)
        {
            if (this.IsEnabled)
            {
                return item is Series;
            }
            
            return false;
        }
    }

 

In this case the IsEnabled property is playing a totally different role: It's not static and not public. Here, you don't make any setting on the class (instance) from outside to switch on or off, but instead, the class finds that out on its own by reading the configuration. This is a much better approach, because all functionality is encapsulated inside that class and you don't need any code from elsewhere to set the state. There's a lot of potential for errors when such dependencies exist, i.e. when something needs to happen at one place in order to get something working properly at a different place, while you don't have any control over which happens first...

  • Thanks 1
Link to comment
Share on other sites

mickle026

Thanks @softworkz, your answers are always so informative and helpful :)

I am pretty sure I will also try to use this approach in my future apps.

  • Thanks 1
Link to comment
Share on other sites

mickle026

This is exactly what I needed, Big Thankyou to @softworkz:)

 

I'll post here what I did in case it can help others....

 

I set them all to off in their classes, so they are all off by default.

public static bool IsEnabled { get; set; } = false;

Used My config page to turn them on as required. This is a sample image I have more than this.

Screenshot2023-05-07at17-14-21MIKE-PC.png.bfe1ac417cc071246b8e7b8e6f6b17dc.png

Screenshot2023-05-07at17-14-40MIKE-PC.png.481d2d546ae8d93e92019d0133056371.png

Set my IService to call a static void from within the Get rather than doing it all in the Get method, so its shared call so ServerEntryPoint can also access it too

        [Route("/APID/ToggleId", "GET", Summary = "ToggleId")]
        public class ToggleId : IReturn<string>
        {
            public string Text { get; set; }
        }
        public string Get(ToggleId result)
        {
            config = Plugin.Instance.Configuration;
            Current = this;

            UpdateProviderToggleSettings();

            return _json.SerializeToString(new ToggleId()
            {
                Text = "Setting Toggled!"
            });

        }
 public static void UpdateProviderToggleSettings()
        {
            config = Plugin.Instance.Configuration;

		if (config.MMAboutUrl)
        { ExternalAboutPage1.IsEnabled = true; }
        else
        { ExternalAboutPage1.IsEnabled = false; }
   
   // rest of code
}

Then to get around them not loading at emby start utilised the server entry point to load the config and toggle the true/false settings whether they are on or off

        public void Run() 
        {
            var config = Plugin.Instance.Configuration;  
            Plugin.Instance.UpdateConfiguration(Plugin.Instance.Configuration);

            My_Provider_Ids.UpdateProviderToggleSettings();
        }

It all works , I'm pleased about this as it was becoming a bit of a nightmare.

 

The External Id works fine without the UI programatically, but it is also very handy to do some of the edits in the UI :) :) , this enables me to use it as and when i want :) 

Edited by mickle026
  • Like 2
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...