0.2.65 - End Credits wiring / Gateway: 0.1.45 - Credits

This commit is contained in:
ponzischeme89
2026-08-15 21:11:43 +12:00
parent d5d47473a2
commit e528d04b43
19 changed files with 574 additions and 65 deletions
+57 -8
View File
@@ -24,6 +24,7 @@ import (
type Client struct {
baseURL string
publicURL string
mediaURL string
clientName string
// gatewayClientName identifies the requests the gateway makes on its own behalf, so
// Emby's device list separates a television from the server standing behind it. The
@@ -72,8 +73,17 @@ type ItemsResult struct {
type AuthResult struct {
User struct {
ID string `json:"Id"`
Name string `json:"Name"`
ID string `json:"Id"`
Name string `json:"Name"`
Policy struct {
// IsAdministrator is a pointer because "Emby says this person is not an
// administrator" and "Emby did not answer the question" are different facts
// that must be acted on differently: the first refuses the console, the second
// is worth asking again rather than locking an operator out of their own
// server. Emby returns the policy in the authentication response itself, so
// the ordinary path costs no extra request.
IsAdministrator *bool `json:"IsAdministrator"`
} `json:"Policy"`
} `json:"User"`
AccessToken string `json:"AccessToken"`
ServerID string `json:"ServerId"`
@@ -83,7 +93,8 @@ type User struct {
ID string `json:"Id"`
Name string `json:"Name"`
Policy struct {
IsDisabled bool `json:"IsDisabled"`
IsDisabled bool `json:"IsDisabled"`
IsAdministrator bool `json:"IsAdministrator"`
} `json:"Policy"`
}
@@ -174,6 +185,18 @@ func New(baseURL, publicURL, clientName, gatewayClientName string, timeout time.
}
}
// SetMediaURL names the address server-side media reads go to, which is only ever credits
// detection. Unset — or set to nothing — media reads use the same address as metadata.
//
// A setter rather than a constructor argument because it is one deployment's answer to one
// subsystem's problem, and threading it through four test call sites that will never use it
// would be paying for it everywhere to spend it in one place. Call it during start-up,
// before anything is serving: nothing here is synchronised, for the same reason the client's
// other addresses are not.
func (c *Client) SetMediaURL(mediaURL string) {
c.mediaURL = strings.TrimRight(strings.TrimSpace(mediaURL), "/")
}
// Authenticate signs a television in. cred carries the device the record is created for
// and that set's ClientVersion — Emby stamps it on the record, so an empty one leaves the
// entry claiming the gateway's own build. cred.Gateway marks a sign-in the gateway is
@@ -247,6 +270,22 @@ func (c *Client) DeleteDevice(
return nil
}
// UserByID reads one account with that account's own token. It exists for the admin gate
// and is asked only when an authentication response carried no policy at all, which is
// why it takes the credentials it does: the question is "who did I just authenticate",
// and an Emby user may always read themselves.
func (c *Client) UserByID(ctx context.Context, cred Credentials, userID string) (*User, error) {
req, err := c.newRequest(ctx, http.MethodGet, "/Users/"+url.PathEscape(userID), nil, cred, nil)
if err != nil {
return nil, err
}
var user User
if err := c.do(req, &user); err != nil {
return nil, err
}
return &user, nil
}
// Users returns the household accounts visible to an administrative/service token.
// It is used only by the background For You builder, never on a television request.
func (c *Client) Users(ctx context.Context, cred Credentials) ([]User, error) {
@@ -593,19 +632,29 @@ func (c *Client) StreamURL(cred Credentials, itemID string) string {
return fmt.Sprintf("%s/Videos/%s/stream?%s", c.publicURL, url.PathEscape(itemID), params.Encode())
}
// InternalStreamURL is the same file, addressed the way the *gateway* reaches Emby rather
// InternalStreamURL is the same file, addressed the way the *gateway* reads bytes rather
// than the way a television does.
//
// It exists for credits detection, which is the only thing that reads media bytes on the
// server side. Using StreamURL there would send a ranged read out to the public address and
// back in through the reverse proxy — the two containers are on the same network, and a scan
// has no business leaving it. Nothing here is ever handed to a client.
// server side, and it reads them as ranged requests over the tail of a file. Using StreamURL
// there would send every one of those out to the public address and back in through the
// reverse proxy — and where a proxy buffers the response, the ranged read stops being a
// ranged read and the whole economy of the scan is gone. Nothing here is ever handed to a
// client.
//
// mediaURL is preferred and baseURL is the fallback, because how the gateway reaches Emby
// for *metadata* is not evidence about the best path for megabytes: a deployment whose
// MEMBY_EMBY_URL is a public hostname is an ordinary one, not a misconfiguration.
func (c *Client) InternalStreamURL(cred Credentials, itemID string) string {
host := c.mediaURL
if host == "" {
host = c.baseURL
}
params := url.Values{}
params.Set("static", "true")
params.Set("api_key", cred.Token)
params.Set("DeviceId", cred.DeviceID)
return fmt.Sprintf("%s/Videos/%s/stream?%s", c.baseURL, url.PathEscape(itemID), params.Encode())
return fmt.Sprintf("%s/Videos/%s/stream?%s", host, url.PathEscape(itemID), params.Encode())
}
// DeliveryURL converts a PlaybackInfo URL into a TV-reachable, authenticated URL.