ginjaninja 624 Posted 11 hours ago Posted 11 hours ago (edited) I wish to make my plugin available to end users on the Webclient, so i have registered a page controller / page just for users with EnableInUserMenu = true, It works fine other than i have to press f5 after every action for non admins. Claude tells me its becuase the Emby page wiring is hard coded to "sendmessagetoadminsessions" and not "sendmessagetousersessions"...but regardless does this make any sense and is there a way to make pages responsive to users. https://github.com/ginjaninja1/ManageComingSoon/tree/main/UI testing on 4.10.0.21 thanks Edited 11 hours ago by ginjaninja
ginjaninja 624 Posted 7 hours ago Author Posted 7 hours ago (edited) 4 hours ago, Luke said: Hi, after what actions? Thanks @Luke, bear with me , becuase i am guessing what matters to investigating this. I have a genericui buttonitem with a command handle "AddToList" public override Task<IPluginUIView> RunCommand(string itemId, string commandId, string data) { if (commandId == "AddToList") { HandleAddToList(); return Task.FromResult<IPluginUIView>(this); } Claude says the smoking gun in is PageControllerHostbase in that it is the something in the chain hardwired to "SendMessageToAdminSessions" // Emby.Web.GenericUI.Control.PageHosts.PageControllerHostBase protected void NotifyPageChangeDelayed(IUIView uiView) { Task.Run(async delegate { await Task.Delay(500).ConfigureAwait(continueOnCapturedContext: false); UIViewInfo data = new UIViewInfo(uiView) { IsPageChangeInfo = true }; await sessionManager.SendMessageToAdminSessions("UIPageInfoChanged", data, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: false); }); } private void CurrentUIViewOnUIViewInfoChanged( object sender, GenericEventArgs<IUIView> e) { if (IsInitialized) { this.UIPageInfoChanged?.Invoke(this, e); UIViewInfo data = new UIViewInfo(e.Argument); sessionManager.SendMessageToAdminSessions( "UIPageInfoChanged", data, CancellationToken.None); } } Edited 7 hours ago by ginjaninja
ginjaninja 624 Posted 6 hours ago Author Posted 6 hours ago (edited) Woohoo, Claude came good with a bit of finesseing and gave me a solution. https://github.com/ginjaninja1/ManageComingSoon/tree/TestuserpageswithoutcallingUIViewInfoChanged/UI/UserAddMovie Claudes solution was to give me a page which did not rely on UIViewInfoChanged, and every button action has to immediately return a view. Is this close to the intended approach? User facing plugins are supported but you have to be careful with what responsiveness you can expect and avoid RaiseUIViewInfoChanged? Spoiler Emby GenericUI Plugin Views for Ordinary Web Users Purpose This document describes what an Emby plugin developer can and cannot reliably implement in a GenericUI page used by a non-administrator through the Emby web client. It is based on inspection of the GenericUI SDK implementation and a working ordinary-user page. The central distinction is between: Updates returned directly to the browser from a command request. Updates pushed later through UIViewInfoChanged. The first works for ordinary web users. The second is routed only to administrator sessions by the current GenericUI implementation. 1. The Reliable User-Space Interaction Model A user-facing GenericUI page should be designed around this sequence: User clicks a button or changes a control ↓ Web client sends a GenericUI command ↓ Plugin RunCommand() executes ↓ Plugin performs the requested operation ↓ Plugin rebuilds ContentData ↓ Plugin returns an IPluginUIView ↓ Emby returns UIViewInfo in the HTTP response ↓ The calling browser renders the updated page The core method is: public override Task<IPluginUIView> RunCommand( string itemId, string commandId, string data) or its asynchronous equivalent: public override async Task<IPluginUIView> RunCommand( string itemId, string commandId, string data) The command must return a view whose ContentData already represents the result of the action. For example: public override async Task<IPluginUIView> RunCommand( string itemId, string commandId, string data) { if (commandId == "Search") { await SearchAsync().ConfigureAwait(false); RebuildContent(); return this; } return await base.RunCommand(itemId, commandId, data) .ConfigureAwait(false); } This path does not rely on a websocket notification. 2. The Unsupported Push Model The following pattern is not reliable for an ordinary user: ModifyState(); RaiseUIViewInfoChanged(); The SDK handles the event through an internal route equivalent to: UIViewInfoChanged ↓ GenericUI page host ↓ SendMessageToAdminSessions("UIPageInfoChanged", ...) A non-admin browser does not receive that pushed update. This affects any operation whose visible result occurs after RunCommand() has already returned. Examples include: background search completion; scheduled-task progress; timer-driven UI changes; task-manager event handlers; tracker events raised by another thread; cross-user state changes; automatic completion notifications; live progress bars. The server-side work may happen correctly, but the ordinary user's open page will not be notified through the standard GenericUI push mechanism. 3. Page Registration A normal user page should be distinct from the administration or configuration page. A typical page declaration is: PageInfo = new PluginPageInfo { Name = "ManageComingSoonUser", DisplayName = "Manage Coming Soon", EnableInUserMenu = true, EnableInMainMenu = false, IsMainConfigPage = false, MenuIcon = "upcoming" }; Important properties: Name Must uniquely identify the controller. It should not share the same page name as the administrator controller. EnableInUserMenu Makes the page discoverable in the ordinary user's menu. This is a visibility setting, not a security boundary. IsMainConfigPage Should be false for an ordinary-user feature page. A configuration page may be handled by the web client as an administrator-oriented plugin-management page. IPluginPageSecurity Should be implemented to enforce actual server-side access rules. Menu visibility alone is not authorization. An ordinary-user page may permit any authenticated user: public Task CheckIsUserAuthorised( UserDto user, IPluginUIView requestedView) { if (user == null) { throw new UnauthorizedAccessException( "You must be signed in."); } return Task.CompletedTask; } An administrator page can separately require: user.Policy.IsAdministrator 4. Supported UI Building Blocks The following GenericUI concepts work well for ordinary users when their result is returned from RunCommand(). Text and numeric input Supported examples: movie title; optional year; search terms; paths; notes; simple settings; identifiers. The browser posts the edited ContentData with the command, allowing the plugin to read the current values. Buttons Buttons are a natural fit for request-response pages. Examples: Search; Submit; Remove; Select; Retry; Confirm; Cancel; Clear; Use Manual Entry. A button command can perform work, rebuild the page and return the result. Toggles Toggles can work reliably when changing them causes a command and the returned view contains the new checked state. Examples: include in submission; use TMDB match; enable an option; choose a preferred destination. Do not depend on a later push to correct the toggle state. Generic item lists GenericItemList and GenericListItem are supported. A user page can present: search results; candidate matches; submitted requests; validation failures; destination conflicts; completed submissions; selectable rows; nested candidate rows. The complete list should be rebuilt before returning the command response. Candidate selection A TMDB search can return several candidates. The user may select a candidate with a command such as: Select_<entryId>_<candidateIndex> The handler can: read the selected result; update the page's state; rebuild the list; return the updated view. This does not require RaiseUIViewInfoChanged(). Validation and errors Validation messages work well because they are known during the command. Examples: movie title is required; year is invalid; TMDB key is unavailable; no destination is configured; target folder already exists; duplicate request; provider search failed. The handler sets a StatusItem, rebuilds the view and returns it. Navigation A command can return a different IPluginUIView. For example: if (commandId == "OpenConfirmation") { return new ConfirmationPageView(...); } The calling browser receives the replacement view directly. Navigation does not inherently require RaiseUIViewInfoChanged(). Awaited external service calls Moderately short asynchronous work can be awaited inside RunCommand(). Examples: TMDB title search; retrieving candidate details; checking a destination; validating a server-side setting; loading a modest result list. Pattern: var results = await tmdbService.SearchAsync(...); BuildResults(results); return this; The browser waits for the command response and then renders the finished result. 5. Supported, but With Design Constraints Some features are possible but should be shaped around the request-response model. Tracker-backed state A shared tracker may still be used. It was not inherently necessary to remove AddMovieTracker from the user page. A tracker can provide: persistence across page navigation; shared submission state; duplicate detection; queued items; previously submitted requests; completion history. The limitation is only how tracker changes reach the open browser. Safe pattern: RunCommand ↓ mutate tracker ↓ read current tracker snapshot ↓ rebuild ContentData ↓ return view Also safe: User reopens page ↓ CreateDefaultPageView ↓ read tracker snapshot ↓ display latest state Unsafe expectation: Background task mutates tracker ↓ RaiseUIViewInfoChanged ↓ ordinary user's page updates automatically Destination conflict checks Point-in-time conflict checks are fully suitable. They can run: after a TMDB match is selected; immediately before submission; when the user retries; when the page is opened; when the user explicitly asks to recheck. Continuous server-side conflict polling is not useful for an ordinary user's open page unless another browser request retrieves the new result. Completed and submitted lists The page can show a history of: submitted; queued; completed; failed. It simply cannot update itself automatically when those states change later. The latest snapshot can be shown: when the page opens; after another user command; through a deliberate status command; after a normal browser page refresh. Multiple movie entries Bulk input is possible. For example: Dune Part Two;2024|Gladiator II|The Batman;2022 The command can await all provider searches and return the completed candidate list. The practical limitation is request duration. Searching too many entries in one HTTP request may feel slow or risk a timeout. A reasonable page may impose a small limit such as five or ten searches per command. Cast and expanded information Candidate information can be displayed. The important part is that the information fetch is awaited: await FetchCastAsync(...); RebuildContent(); return this; The old pattern: Task.Run(() => FetchCastAsync(...)); return this; would return before the result exists and would therefore need a later push. 6. Long-Running Server Work Some operations should not remain inside the HTTP request. Examples: creating media folders; writing metadata; downloading artwork; scanning an Emby library; refreshing metadata; processing many movies; moving files; work that might take minutes. The correct user-page pattern is: User presses Submit ↓ Validate request ↓ Create or queue server-side work ↓ Return immediate acknowledgement For example: Batman (2026) has been submitted for addition. The page does not need to wait for completion. The task can continue through the existing scheduled-task or queue infrastructure. The user page may show a snapshot of current task state when it is next opened, but should not promise live progress. 7. UI Features That Do Not Work Reliably as Live Features Live progress bars A progress bar can be rendered, but it will not automatically advance for an ordinary user after the command response ends. A static snapshot such as “40% when this page was opened” is possible. A live progress bar depending on repeated UIViewInfoChanged pushes is not. Automatic status changes The following sequence is not visible automatically: Queued ↓ Adding ↓ Downloading poster ↓ Refreshing library ↓ Completed The server may perform all stages, but the ordinary user's open page will not receive each transition through GenericUI's standard push route. Server-side timers A server timer can mutate state but cannot independently update the ordinary user's browser. A timer ending with: RaiseUIViewInfoChanged(); still uses the admin-only route. Task event subscriptions Handlers such as: taskManager.TaskExecuting += ... taskManager.TaskCompleted += ... may be useful for maintaining internal state. They are not a reliable mechanism for visually updating an ordinary user's currently open page. Detached asynchronous work Avoid: Task.Run(() => SearchAsync()); return Task.FromResult<IPluginUIView>(this); when the user needs to see the search result. The browser receives the page before the work completes. The eventual result then requires a push, polling or another request. Use: await SearchAsync(); RebuildContent(); return this; instead. Cross-session live synchronization If one user submits a movie, another user's already-open page will not automatically update. Both users can see shared state the next time their browser sends a command or reloads the page. 8. Polling and Refresh Options Manual browser refresh Reloading or reopening the page causes the controller to build a fresh view. This is simple but should not be required for normal command results. Refresh command A visible button can send a command that rebuilds the latest state. This works technically but may be unnecessary for a simple submit page. It is appropriate for pages where users occasionally want to check whether queued work has completed. Browser-side polling In principle, a browser can repeatedly send a GenericUI command such as: GetCurrentStatus and render each returned view. However, this requires support in the web page/client behaviour for invoking commands periodically. A plugin cannot obtain automatic non-admin updates merely by running a server-side polling timer. The distinction is: Server-side polling + RaiseUIViewInfoChanged = still admin-only versus: Browser sends repeated command requests = returned directly to that browser Unless browser polling has been explicitly implemented and tested, it should not be treated as a standard GenericUI capability. 9. What Was Necessarily Removed From the Standalone User Page The following removals were necessary for a page that does not depend on the admin-only push route: RaiseUIViewInfoChanged; coalesced broadcast timers; automatic UI refresh triggered by tracker events; task-completion events whose purpose was to update the page; detached TMDB searches; server-side polling intended to redraw the open page; live progress updates; automatic transitions displayed after the command ended. These were the parts directly linked to the unsupported delivery mechanism. 10. What Was Removed Conservatively The standalone page was intentionally reduced further than strictly necessary. The following features could be restored safely with suitable command-response design: shared AddMovieTracker state; multiple simultaneous movie entries; candidate sub-lists; expanded TMDB information; cast information; duplicate detection; destination conflict detection; submitted-history list; completed-history list; retry commands; remove commands; toggles; bulk selection; Add All submission; richer status summaries; icons and status styling; static progress snapshots; rows showing queued or completed state. They were removed initially to prove the ordinary-user interaction model with the smallest possible surface area. They are not inherently administrator-only. The test is always: Can the feature produce its visible result before RunCommand() returns, or display a state snapshot when the page is opened? If yes, it can generally be used in the ordinary-user page. 11. Recommended Design for Manage Coming Soon The user page can remain simple without being primitive. Stage 1: Input Fields: Movie title Optional year Commands: Search TMDB Submit Without Matching Stage 2: Match review Show: normalised title; release year; overview; several possible matches when ambiguous. Commands: Select; Search Again; Use Manual Entry; Submit. All provider work should be awaited inside the corresponding command. Stage 3: Submission acknowledgement After validation: add the confirmed request to the shared queue or tracker; start the server-side task if needed; return an acknowledgement immediately. Example: Dune: Part Two (2024) has been submitted for addition. Optional information: The server will process this request in the background. No live progress is required. Optional status area When the page opens, it may show recent requests: Submitted Added Failed This is a snapshot, not a live feed. A small “Check submitted requests” command could rebuild this list without requiring a full browser reload. 12. Recommended Separation Between User and Admin Pages User page Optimised for: request; normalisation; confirmation; submission; clear validation; immediate acknowledgement. It should use complete command-response interactions. Administrator page May retain: tracker event subscriptions; progress bars; scheduled-task status; live queue views; detailed diagnostics; automatic background updates; RaiseUIViewInfoChanged; configuration; Make Live operations; troubleshooting information. The two pages may share: services; models; TMDB code; tracker or queue; conflict-checking helpers; submission pipeline. They should not need to share the same page-view implementation. 13. Decision Checklist Before adding functionality to an ordinary-user GenericUI page, ask: Does the action begin with a browser command? If no, it cannot directly return an updated view to that browser. Can the necessary work finish within the command request? If yes, await it, rebuild the page and return the view. Is the work too long for an HTTP request? Queue it and return an acknowledgement. Does the UI need to update after the request has ended? It will require another browser request, page reopening, explicit refresh, or tested browser-side polling. Does the implementation call RaiseUIViewInfoChanged()? Treat that only as an administrator-page enhancement, not as the ordinary-user delivery mechanism. Is the state stored outside the page view? That is acceptable and often desirable. Read a fresh snapshot whenever constructing a command response or opening the page. 14. Core Rule For an ordinary user in the Emby web client: A UI change is reliable when it is contained in the IPluginUIView returned from the user's own GenericUI command request. A UI change is not reliably delivered when it occurs later and depends on: RaiseUIViewInfoChanged() Design user pages around complete interactions rather than live server-pushed dashboards. Edited 2 hours ago by ginjaninja
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now