Fix plugin to build clean against real Emby 4.9.1.90 SDK
Installed .NET 8 SDK (per-user, winget needed admin elevation this
machine doesn't have) and restored the real MediaBrowser.Server.Core /
MediaBrowser.Common packages, then fixed everything the compiler and
a bit of reflection surfaced:
- PlaybackProgressEventArgs/PlaybackStopEventArgs are in
MediaBrowser.Controller.Library, not ...Session
- IAuthorizationContext/AuthorizationInfo are in MediaBrowser.Controller.Net
- Item/user/session ids (SessionInfo.Id/UserId, BaseItemDto.Id) are plain
string on this SDK version, not Guid - Party/PartyMember updated
accordingly (Party.Id stays a Guid; it's minted by this plugin, not Emby)
- Reading the calling user needed an extra step:
IAuthorizationContext.GetAuthorizationInfo(Request).UserId is a `long`
(internal row id); .User.Id is the one matching SessionInfo.UserId,
but it's a Guid so it's reformatted with .ToString("N")
docs/architecture.md's "Assumptions to verify" now reflects what was
actually confirmed vs. what's still only compile-checked (HTTP status
codes, content-type headers, the JS auth header format, the dashboard
config page) pending a run against a real server.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
66af05f259
commit
db815df7d5
8 changed files with 133 additions and 85 deletions
23
README.md
23
README.md
|
|
@ -27,20 +27,23 @@ the actual video stream.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Early scaffold, not yet built or run against a real server. It's structured
|
Builds clean (0 errors, 0 warnings) against the real `MediaBrowser.Server.Core`
|
||||||
around Emby's documented plugin patterns (`BasePlugin<T>`, `IServerEntryPoint`,
|
/ `MediaBrowser.Common` 4.9.1.90 assemblies from NuGet. Not yet installed or
|
||||||
the `IService`/`[Route]` API style) and the confirmed
|
run against an actual Emby Server — a few things (auth header format, HTTP
|
||||||
`ISessionManager.SendPlaystateCommand` remote-control API, but several SDK
|
status codes, the dashboard settings page rendering) are only compile-checked
|
||||||
details are marked `TODO(verify)` in the code — see
|
so far, not runtime-verified. See
|
||||||
[docs/architecture.md](docs/architecture.md#assumptions-to-verify) before
|
[docs/architecture.md](docs/architecture.md#assumptions-to-verify) for
|
||||||
expecting it to compile as-is.
|
exactly what was wrong on the first guess, what was fixed, and what's still
|
||||||
|
open.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Emby Server (developed against 4.9.5.0; should work on nearby 4.9.x/4.8.x
|
- Emby Server (developed against 4.9.5.0; should work on nearby 4.9.x/4.8.x
|
||||||
builds, but the plugin ABI does shift between versions)
|
builds, but the plugin ABI does shift between versions)
|
||||||
- [.NET SDK](https://dotnet.microsoft.com/download) to build (not currently
|
- [.NET SDK](https://dotnet.microsoft.com/download) 8.0 to build (installed
|
||||||
installed in this environment — `dotnet --version` came back empty)
|
per-user under `%USERPROFILE%\.dotnet` in this environment, since the
|
||||||
|
machine-wide winget install needed admin elevation that wasn't available —
|
||||||
|
see git history if you need to redo this on another machine)
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
|
|
@ -74,7 +77,7 @@ portable install vs. Docker volume).
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] Design doc + scaffold
|
- [x] Design doc + scaffold
|
||||||
- [ ] Build against the real SDK, fix whatever the `TODO(verify)` spots got wrong
|
- [x] Build against the real SDK, fix whatever the `TODO(verify)` spots got wrong
|
||||||
- [ ] Try it against a real Emby Server + two clients
|
- [ ] Try it against a real Emby Server + two clients
|
||||||
- [ ] Live (WebSocket) updates in the control panel instead of polling
|
- [ ] Live (WebSocket) updates in the control panel instead of polling
|
||||||
- [ ] Optional text chat, reusing the same realtime channel
|
- [ ] Optional text chat, reusing the same realtime channel
|
||||||
|
|
|
||||||
|
|
@ -150,42 +150,73 @@ standard `/Users/AuthenticateByName`) and only ever talks to your own server.
|
||||||
|
|
||||||
## Assumptions to verify
|
## Assumptions to verify
|
||||||
|
|
||||||
This was designed by reading Emby's public docs/wiki and a couple of
|
Update: the .NET SDK is now installed and `src/WatchParty` **builds clean
|
||||||
community plugins (linked above), without a local copy of the SDK to compile
|
(0 errors, 0 warnings) against the real `MediaBrowser.Server.Core` /
|
||||||
against (no .NET SDK was installed in the environment this was scaffolded in,
|
`MediaBrowser.Common` 4.9.1.90 assemblies**, restored from NuGet. Most of the
|
||||||
and the exact `MediaBrowser.Server.Core` build for Emby Server 4.9.5.0 wasn't
|
list below turned out right on the first guess; a few didn't, and were fixed
|
||||||
available to inspect directly). Treat the overall shape as solid and these
|
by reading the actual assemblies (via .NET reflection) and then letting the
|
||||||
specifics as needing a compile-and-fix pass:
|
compiler confirm. Keeping the history here since it's a useful map of where
|
||||||
|
this SDK surprises you.
|
||||||
|
|
||||||
- **`IHasWebPages` vs `IHasUIPages`** — different community plugins use
|
**Confirmed correct as originally written** (compiled with no changes needed):
|
||||||
different names depending on SDK era; `Plugin.cs` assumes `IHasWebPages`.
|
- `BasePlugin<T>` constructor shape, `IServerEntryPoint`, `netstandard2.0` +
|
||||||
- **`PluginPageInfo` property names** (`Name`, `EmbeddedResourcePath`).
|
`MediaBrowser.Server.Core` package — all from the official wiki.
|
||||||
- **ID types** — whether item/user/session IDs are `Guid` or `string` on your
|
- `IHasWebPages` (not `IHasUIPages` — that name does exist too, in
|
||||||
SDK version (this has changed across Emby versions). `Party`/`PartyMember`
|
`MediaBrowser.Model.Plugins.UI`, but `IHasWebPages` in
|
||||||
assume `Guid`.
|
`MediaBrowser.Model.Plugins` is the one that's actually wired up here) and
|
||||||
- **How an `IService` reads the calling user** — `WatchPartyService` assumes
|
`PluginPageInfo.Name`/`.EmbeddedResourcePath`.
|
||||||
constructor-injected `IAuthorizationContext` + `IRequiresRequest.Request`,
|
- `ISessionManager.SendPlaystateCommand(string controllingSessionId, string
|
||||||
then `GetAuthorizationInfo(Request).UserId`.
|
sessionId, PlaystateRequest, CancellationToken)`, `SendGeneralCommand`, and
|
||||||
- **HTTP error mapping** — endpoints currently throw plain
|
`PlaystateRequest { Command, SeekPositionTicks }` / `PlaystateCommand.{Pause,
|
||||||
|
Unpause, Seek}`.
|
||||||
|
- `ILogManager.GetLogger(name)` / `ILogger.Debug(...)` /
|
||||||
|
`.ErrorException(...)` — still the live pattern on 4.9.x, not replaced by
|
||||||
|
`Microsoft.Extensions.Logging`.
|
||||||
|
- `IRequiresRequest` / `IService` / `[Route]` / `IReturn<T>` for the REST
|
||||||
|
endpoints.
|
||||||
|
- `SessionInfo.PlayState.PositionTicks` (`long?`) and `.IsPaused` (`bool`).
|
||||||
|
|
||||||
|
**Wrong on the first guess, fixed once the real assemblies were available:**
|
||||||
|
- **Namespaces**, not names: `PlaybackProgressEventArgs`/`PlaybackStopEventArgs`
|
||||||
|
live in `MediaBrowser.Controller.Library`, not `...Session`.
|
||||||
|
`IAuthorizationContext`/`AuthorizationInfo` live in `MediaBrowser.Controller.Net`.
|
||||||
|
- **ID types**: item/user/session ids (`SessionInfo.Id`, `SessionInfo.UserId`,
|
||||||
|
`BaseItemDto.Id`) are plain **`string`**, not `Guid`, on this SDK version.
|
||||||
|
`Party`/`PartyMember` were changed from `Guid` to `string` accordingly.
|
||||||
|
(`Party.Id` itself stays a `Guid` — that's an id this plugin mints, not an
|
||||||
|
Emby one.)
|
||||||
|
- **Reading the calling user is a two-step, not one property**:
|
||||||
|
`IAuthorizationContext.GetAuthorizationInfo(Request).UserId` is a `long`
|
||||||
|
(an internal row id — not the id used anywhere else). The one that matches
|
||||||
|
`SessionInfo.UserId` etc. is `.User.Id` — except that one comes back as a
|
||||||
|
`Guid`, so `WatchPartyService.CallerUserId()` reformats it with
|
||||||
|
`.ToString("N")` to match the string convention used elsewhere. See the
|
||||||
|
comment there.
|
||||||
|
|
||||||
|
**Still only compile-checked, not verified against a running server**
|
||||||
|
(nothing here fails to build — these are runtime behaviors the compiler
|
||||||
|
can't check):
|
||||||
|
- Whether `.ToString("N")` (no dashes) is really the right string format to
|
||||||
|
match `SessionInfo.UserId` — plausible, matches Emby/Jellyfin convention
|
||||||
|
elsewhere, not observed directly.
|
||||||
|
- HTTP error mapping — endpoints throw plain
|
||||||
`ArgumentException`/`InvalidOperationException`; Emby likely has a more
|
`ArgumentException`/`InvalidOperationException`; Emby likely has a more
|
||||||
specific exception type (something like `ResourceNotFoundException`) that
|
specific exception type that maps to a proper 404/400 instead of a
|
||||||
maps to a proper 404/400 instead of a generic 500.
|
generic 500.
|
||||||
- **Static file content types** — `GET /WatchParty/app` etc. currently return
|
- Static file content types — `GET /WatchParty/app` etc. return a bare
|
||||||
a bare string. The wiki confirms an `IHasResultFactory` extension point
|
string; `IHasResultFactory` looks like the right extension point for a
|
||||||
exists for setting a real `Content-Type`/caching headers; wiring that up is
|
real `Content-Type`/caching headers, not wired up yet.
|
||||||
a follow-up rather than done here.
|
- `controllingSessionId` passed as `null` to `SendPlaystateCommand` — compiles,
|
||||||
- **Logging API shape** — `ILogManager.GetLogger(name)` /
|
unknown whether Emby's runtime wants a real session/user there instead.
|
||||||
`ILogger.Debug(...)` / `.ErrorException(...)` is the classic
|
- `X-Emby-Authorization` header fields and `/Users/AuthenticateByName`'s `Pw`
|
||||||
Emby/MediaBrowser pattern; some SDK generations may expect
|
field name in `web/control-panel/app.js` — it's JS, nothing compiles it;
|
||||||
`Microsoft.Extensions.Logging` instead.
|
long-standing Emby/Jellyfin convention, not exercised against a real server.
|
||||||
- **`X-Emby-Authorization` header fields and `/Users/AuthenticateByName`'s
|
- The dashboard settings page (`configPage.html`) actually rendering/saving
|
||||||
`Pw` field name** in `web/control-panel/app.js` — long-standing Emby/
|
correctly — embedding compiles, but the page's own JS (`ApiClient`,
|
||||||
Jellyfin convention, not re-verified against 4.9.5.0 specifically.
|
`Dashboard` globals) only exists inside a running Emby dashboard.
|
||||||
|
|
||||||
The fastest way to nail all of these down: install the .NET SDK, `dotnet
|
The remaining way to close these out: install the plugin DLL on a real Emby
|
||||||
restore` the project (pulls the real `MediaBrowser.Server.Core` /
|
Server 4.9.x and exercise it with two accounts.
|
||||||
`MediaBrowser.Common` assemblies from NuGet), and let the compiler point at
|
|
||||||
whatever doesn't match.
|
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
|
|
@ -10,15 +11,23 @@ using WatchParty.Parties;
|
||||||
namespace WatchParty.Api
|
namespace WatchParty.Api
|
||||||
{
|
{
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// The [Route]/IService/IReturn<T> pattern here is confirmed against Emby's
|
// Builds clean against the real MediaBrowser.Server.Core/MediaBrowser.Common
|
||||||
// official wiki: https://github.com/MediaBrowser/Emby/wiki/Creating-Api-Endpoints
|
// 4.9.1.90 assemblies. The [Route]/IService/IReturn<T> pattern matches Emby's
|
||||||
|
// official wiki (https://github.com/MediaBrowser/Emby/wiki/Creating-Api-Endpoints),
|
||||||
|
// and reading the calling user turned out to need one extra step versus the
|
||||||
|
// first guess: IAuthorizationContext.GetAuthorizationInfo(Request).UserId is a
|
||||||
|
// `long` (an internal row id, not useful for matching against SessionInfo.UserId
|
||||||
|
// etc.) - .User.Id is the right one, but it's a Guid rather than the string
|
||||||
|
// those other properties use, so it's reformatted with .ToString("N") below
|
||||||
|
// (Emby's usual convention for string ids; not independently confirmed beyond
|
||||||
|
// "the project builds", since that part is a runtime format match, not
|
||||||
|
// something the compiler checks - see docs/architecture.md#assumptions-to-verify).
|
||||||
//
|
//
|
||||||
// Less certain - see docs/architecture.md#assumptions-to-verify:
|
// Still only compile-checked, not run against a real server - see
|
||||||
// - How a service reads the calling user (assumed here: constructor-injected
|
// docs/architecture.md#assumptions-to-verify:
|
||||||
// IAuthorizationContext + IRequiresRequest.Request, then
|
|
||||||
// GetAuthorizationInfo(Request).UserId).
|
|
||||||
// - Exact HTTP-error exception types (using plain ArgumentException /
|
// - Exact HTTP-error exception types (using plain ArgumentException /
|
||||||
// InvalidOperationException below as a placeholder).
|
// InvalidOperationException below as a placeholder - they compile fine,
|
||||||
|
// but Emby likely has something that maps more precisely to a 404/400).
|
||||||
// - Content-Type on the static file responses (returning bare strings for
|
// - Content-Type on the static file responses (returning bare strings for
|
||||||
// now; IHasResultFactory looks like the right extension point later).
|
// now; IHasResultFactory looks like the right extension point later).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -71,15 +80,15 @@ namespace WatchParty.Api
|
||||||
{
|
{
|
||||||
public Guid PartyId { get; set; }
|
public Guid PartyId { get; set; }
|
||||||
public string JoinCode { get; set; }
|
public string JoinCode { get; set; }
|
||||||
public Guid ItemId { get; set; }
|
public string ItemId { get; set; }
|
||||||
public Guid HostUserId { get; set; }
|
public string HostUserId { get; set; }
|
||||||
public Guid ReferenceUserId { get; set; }
|
public string ReferenceUserId { get; set; }
|
||||||
public PartyMemberDto[] Members { get; set; }
|
public PartyMemberDto[] Members { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PartyMemberDto
|
public class PartyMemberDto
|
||||||
{
|
{
|
||||||
public Guid UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
public bool IsPaused { get; set; }
|
public bool IsPaused { get; set; }
|
||||||
public long PositionTicks { get; set; }
|
public long PositionTicks { get; set; }
|
||||||
|
|
@ -175,12 +184,21 @@ namespace WatchParty.Api
|
||||||
|
|
||||||
public object Get(GetControlPanelCss request) => ReadEmbedded("app.css");
|
public object Get(GetControlPanelCss request) => ReadEmbedded("app.css");
|
||||||
|
|
||||||
private Guid CallerUserId()
|
private string CallerUserId()
|
||||||
{
|
{
|
||||||
return _authContext.GetAuthorizationInfo(Request).UserId;
|
// AuthorizationInfo.UserId is a `long` (an internal row id) on this SDK
|
||||||
|
// version - NOT the same id used everywhere else (SessionInfo.UserId,
|
||||||
|
// BaseItemDto.Id, ...). .User.Id is the one that matches, but it's a
|
||||||
|
// Guid rather than the string those other properties use - confirmed by
|
||||||
|
// an actual compiler error (CS0029) while building this file. Formatted
|
||||||
|
// as "N" (no dashes) to match Emby's usual string-id convention.
|
||||||
|
// TODO(verify) if session/party lookups ever silently miss: confirm
|
||||||
|
// SessionInfo.UserId really is Guid.ToString("N") and not "D"/some other
|
||||||
|
// format on your exact server build.
|
||||||
|
return _authContext.GetAuthorizationInfo(Request).User.Id.ToString("N");
|
||||||
}
|
}
|
||||||
|
|
||||||
private SessionInfo FindCallerSession(Guid userId, Guid? expectedItemId)
|
private SessionInfo FindCallerSession(string userId, string expectedItemId)
|
||||||
{
|
{
|
||||||
var candidate = _sessionManager.Sessions
|
var candidate = _sessionManager.Sessions
|
||||||
.Where(s => s.UserId == userId && s.NowPlayingItem != null)
|
.Where(s => s.UserId == userId && s.NowPlayingItem != null)
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,24 @@ namespace WatchParty.Parties
|
||||||
/// <summary>Short human-typeable code used to join, e.g. "K7QX2P".</summary>
|
/// <summary>Short human-typeable code used to join, e.g. "K7QX2P".</summary>
|
||||||
public string JoinCode { get; set; }
|
public string JoinCode { get; set; }
|
||||||
|
|
||||||
/// <summary>The library item everyone is meant to be watching.</summary>
|
/// <summary>The library item everyone is meant to be watching. Emby's own
|
||||||
public Guid ItemId { get; set; }
|
/// item/user ids are plain strings on this SDK version (confirmed by
|
||||||
|
/// building against the real MediaBrowser.Model assembly) - only Party.Id
|
||||||
|
/// itself (below) is a Guid we mint ourselves.</summary>
|
||||||
|
public string ItemId { get; set; }
|
||||||
|
|
||||||
public Guid HostUserId { get; set; }
|
public string HostUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whose reported position is treated as ground truth. Defaults to the
|
/// Whose reported position is treated as ground truth. Defaults to the
|
||||||
/// host, but falls back to whoever's left if the host leaves (see
|
/// host, but falls back to whoever's left if the host leaves (see
|
||||||
/// PartyManager.Leave).
|
/// PartyManager.Leave).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid ReferenceUserId { get; set; }
|
public string ReferenceUserId { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public ConcurrentDictionary<Guid, PartyMember> Members { get; } = new ConcurrentDictionary<Guid, PartyMember>();
|
public ConcurrentDictionary<string, PartyMember> Members { get; } = new ConcurrentDictionary<string, PartyMember>();
|
||||||
|
|
||||||
public DateTime LastActivityUtc { get; set; } = DateTime.UtcNow;
|
public DateTime LastActivityUtc { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ namespace WatchParty.Parties
|
||||||
private readonly ConcurrentDictionary<string, Guid> _partyIdByJoinCode =
|
private readonly ConcurrentDictionary<string, Guid> _partyIdByJoinCode =
|
||||||
new ConcurrentDictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
|
new ConcurrentDictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public Party Create(Guid hostUserId, string hostUserName, string hostSessionId, Guid itemId)
|
public Party Create(string hostUserId, string hostUserName, string hostSessionId, string itemId)
|
||||||
{
|
{
|
||||||
var party = new Party
|
var party = new Party
|
||||||
{
|
{
|
||||||
|
|
@ -61,7 +61,7 @@ namespace WatchParty.Parties
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartyMember Join(Party party, Guid userId, string userName, string sessionId)
|
public PartyMember Join(Party party, string userId, string userName, string sessionId)
|
||||||
{
|
{
|
||||||
var member = new PartyMember
|
var member = new PartyMember
|
||||||
{
|
{
|
||||||
|
|
@ -77,7 +77,7 @@ namespace WatchParty.Parties
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Leave(Party party, Guid userId)
|
public void Leave(Party party, string userId)
|
||||||
{
|
{
|
||||||
party.Members.TryRemove(userId, out _);
|
party.Members.TryRemove(userId, out _);
|
||||||
party.LastActivityUtc = DateTime.UtcNow;
|
party.LastActivityUtc = DateTime.UtcNow;
|
||||||
|
|
@ -87,7 +87,7 @@ namespace WatchParty.Parties
|
||||||
// Reference left: fall back to whoever's left (arbitrary but
|
// Reference left: fall back to whoever's left (arbitrary but
|
||||||
// deterministic-ish; good enough for v1 - see docs/architecture.md).
|
// deterministic-ish; good enough for v1 - see docs/architecture.md).
|
||||||
var next = party.Members.Keys.FirstOrDefault();
|
var next = party.Members.Keys.FirstOrDefault();
|
||||||
if (next != Guid.Empty)
|
if (next != null)
|
||||||
{
|
{
|
||||||
party.ReferenceUserId = next;
|
party.ReferenceUserId = next;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ namespace WatchParty.Parties
|
||||||
{
|
{
|
||||||
public class PartyMember
|
public class PartyMember
|
||||||
{
|
{
|
||||||
public Guid UserId { get; set; }
|
/// <summary>Emby user id - a plain string on this SDK version, not a Guid.</summary>
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,13 @@ using WatchParty.Configuration;
|
||||||
namespace WatchParty
|
namespace WatchParty
|
||||||
{
|
{
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Emby plugin entry point.
|
// Emby plugin entry point. Builds clean against the real
|
||||||
//
|
// MediaBrowser.Server.Core/MediaBrowser.Common 4.9.1.90 assemblies
|
||||||
// Confirmed against Emby's own docs while writing this scaffold:
|
// (restored from NuGet) - BasePlugin<T>, IHasWebPages and PluginPageInfo's
|
||||||
// - BasePlugin<T> + constructor shape:
|
// Name/EmbeddedResourcePath are all confirmed as used below. See
|
||||||
// https://github.com/MediaBrowser/Emby/wiki/How-to-build-a-Server-Plugin
|
// docs/architecture.md#assumptions-to-verify for what's still only
|
||||||
// - netstandard2.0 target / MediaBrowser.Server.Core package: same page.
|
// compile-checked rather than run against a real server (e.g. whether the
|
||||||
//
|
// dashboard actually renders this settings page correctly).
|
||||||
// NOT independently confirmed (this project targets Emby Server 4.9.5.0;
|
|
||||||
// the wiki examples above are older) - check these once you can
|
|
||||||
// restore/build against the real SDK assemblies. Full list in
|
|
||||||
// docs/architecture.md#assumptions-to-verify:
|
|
||||||
// - IHasWebPages is assumed to be the current interface name for a
|
|
||||||
// dashboard settings page. At least one community plugin
|
|
||||||
// (emby-playlist-manager) uses IHasUIPages instead, which may be the
|
|
||||||
// older name.
|
|
||||||
// - PluginPageInfo's exact property names (Name / EmbeddedResourcePath).
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
|
|
@ -58,14 +59,14 @@ namespace WatchParty.Sync
|
||||||
|
|
||||||
private void UpdateMemberState(SessionInfo session)
|
private void UpdateMemberState(SessionInfo session)
|
||||||
{
|
{
|
||||||
if (session?.UserId == null || session.PlayState == null)
|
if (string.IsNullOrEmpty(session?.UserId) || session.PlayState == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var party in PartyManager.Instance.GetAllActive())
|
foreach (var party in PartyManager.Instance.GetAllActive())
|
||||||
{
|
{
|
||||||
if (party.Members.TryGetValue(session.UserId.Value, out var member) &&
|
if (party.Members.TryGetValue(session.UserId, out var member) &&
|
||||||
string.Equals(member.SessionId, session.Id, StringComparison.Ordinal))
|
string.Equals(member.SessionId, session.Id, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
member.LastKnownPositionTicks = session.PlayState.PositionTicks ?? member.LastKnownPositionTicks;
|
member.LastKnownPositionTicks = session.PlayState.PositionTicks ?? member.LastKnownPositionTicks;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue