0.2.57 - Traliers bug fixes
This commit is contained in:
@@ -25,6 +25,12 @@ type Client struct {
|
||||
baseURL string
|
||||
publicURL 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
|
||||
// admin and installer sign-ins are the ones an operator sees: those are the gateway
|
||||
// asking, not a set in a living room, and reporting them as a television made the
|
||||
// list claim a device that does not exist.
|
||||
gatewayClientName string
|
||||
// gatewayVersion labels the requests the gateway makes on its own behalf — the
|
||||
// library sync, the health probe, device cleanup — which belong to no television and
|
||||
// so have no app version to report.
|
||||
@@ -42,6 +48,12 @@ type Credentials struct {
|
||||
// as it reported in X-Memby-Version. Emby shows it beside the device, so a blank one
|
||||
// makes every set in the house look like the same build.
|
||||
ClientVersion string
|
||||
// Gateway marks a request the gateway makes for itself — the library sync, the health
|
||||
// probe, device cleanup, an operator signing into the admin console — rather than on
|
||||
// behalf of a television. It is stated rather than inferred from a missing token or
|
||||
// version: an old app reports neither, and misreading one as the server would put a
|
||||
// television in Emby's list under the wrong name.
|
||||
Gateway bool
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
@@ -133,12 +145,24 @@ func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("emby: status %d: %s", e.StatusCode, e.Body)
|
||||
}
|
||||
|
||||
func New(baseURL, publicURL, clientName string, timeout time.Duration) *Client {
|
||||
// DefaultGatewayClientName is what Emby records for a request the gateway makes for
|
||||
// itself. It is deliberately not the product name: this travels to whatever Emby does
|
||||
// with its own logs, and it must never read as one of the household's televisions.
|
||||
const DefaultGatewayClientName = "MbyGateway"
|
||||
|
||||
// New builds a client. clientName identifies a television on the wire to Emby and
|
||||
// gatewayClientName identifies the gateway itself; a blank gateway name falls back to
|
||||
// DefaultGatewayClientName rather than borrowing the television's.
|
||||
func New(baseURL, publicURL, clientName, gatewayClientName string, timeout time.Duration) *Client {
|
||||
if strings.TrimSpace(gatewayClientName) == "" {
|
||||
gatewayClientName = DefaultGatewayClientName
|
||||
}
|
||||
return &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
publicURL: strings.TrimRight(publicURL, "/"),
|
||||
clientName: clientName,
|
||||
gatewayVersion: buildinfo.Version(),
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
publicURL: strings.TrimRight(publicURL, "/"),
|
||||
clientName: clientName,
|
||||
gatewayClientName: gatewayClientName,
|
||||
gatewayVersion: buildinfo.Version(),
|
||||
http: &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
@@ -150,16 +174,21 @@ func New(baseURL, publicURL, clientName string, timeout time.Duration) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate signs a television in. clientVersion is the app version that set
|
||||
// reported; it is what Emby stamps on the device record it creates here, so an empty one
|
||||
// leaves the entry claiming the gateway's own build.
|
||||
func (c *Client) Authenticate(ctx context.Context, username, password, deviceID, deviceName, clientVersion string) (*AuthResult, error) {
|
||||
// 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
|
||||
// making for itself (the admin console and the web installer), which Emby then records
|
||||
// under the gateway's own client name rather than as a television.
|
||||
func (c *Client) Authenticate(ctx context.Context, username, password string, cred Credentials) (*AuthResult, error) {
|
||||
body, err := json.Marshal(map[string]string{"Username": username, "Pw": password})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := c.newRequest(ctx, http.MethodPost, "/Users/AuthenticateByName", nil,
|
||||
Credentials{DeviceID: deviceID, DeviceName: deviceName, ClientVersion: clientVersion},
|
||||
Credentials{
|
||||
DeviceID: cred.DeviceID, DeviceName: cred.DeviceName,
|
||||
ClientVersion: cred.ClientVersion, Gateway: cred.Gateway,
|
||||
},
|
||||
bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -610,7 +639,8 @@ func (c *Client) SubtitleURL(cred Credentials, itemID, mediaSourceID string, ind
|
||||
|
||||
// Ping checks that Emby is reachable, for readiness probes.
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
req, err := c.newRequest(ctx, http.MethodGet, "/System/Info/Public", nil, Credentials{}, nil)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, "/System/Info/Public", nil,
|
||||
Credentials{Gateway: true}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -657,7 +687,13 @@ func (c *Client) newRequest(ctx context.Context, method, path string, params url
|
||||
}
|
||||
deviceName := strings.TrimSpace(cred.DeviceName)
|
||||
if deviceName == "" {
|
||||
// A gateway request is not a television, so it must not fall back to the
|
||||
// unnamed-set placeholder: that put the server in Emby's device list wearing a
|
||||
// name that reads as somebody's TV.
|
||||
deviceName = "Memby TV"
|
||||
if cred.Gateway {
|
||||
deviceName = c.gatewayClientName
|
||||
}
|
||||
}
|
||||
// The version Emby records is the television's app version, not a constant: every
|
||||
// device in the dashboard read as one build before this, so there was no way to tell
|
||||
@@ -666,10 +702,14 @@ func (c *Client) newRequest(ctx context.Context, method, path string, params url
|
||||
if version == "" {
|
||||
version = c.gatewayVersion
|
||||
}
|
||||
clientName := c.clientName
|
||||
if cred.Gateway {
|
||||
clientName = c.gatewayClientName
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Emby-Authorization", fmt.Sprintf(
|
||||
`MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"`,
|
||||
c.clientName, strings.ReplaceAll(deviceName, `"`, ""), deviceID,
|
||||
clientName, strings.ReplaceAll(deviceName, `"`, ""), deviceID,
|
||||
strings.ReplaceAll(version, `"`, ""),
|
||||
))
|
||||
if cred.Token != "" {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// name is never the product name, and the version is the set's own build rather than a
|
||||
// constant that made every device look alike.
|
||||
func TestAuthHeaderCarriesClientVersion(t *testing.T) {
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", time.Second)
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", "MbyGateway", time.Second)
|
||||
|
||||
req, err := client.newRequest(
|
||||
context.Background(), http.MethodGet, "/Items", nil,
|
||||
@@ -30,7 +30,7 @@ func TestAuthHeaderCarriesClientVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthHeaderFallsBackToGatewayVersion(t *testing.T) {
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", time.Second)
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", "MbyGateway", time.Second)
|
||||
|
||||
req, err := client.newRequest(
|
||||
context.Background(), http.MethodGet, "/Items", nil, Credentials{}, nil,
|
||||
@@ -46,3 +46,52 @@ func TestAuthHeaderFallsBackToGatewayVersion(t *testing.T) {
|
||||
t.Fatalf("auth header %q does not carry the gateway build %q", got, client.gatewayVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// A request the gateway makes for itself — the sync, the health probe, an operator
|
||||
// signing into the admin console — is not a television, and Emby's device list said it
|
||||
// was. It reports the gateway's own name and never falls back to the unnamed-set
|
||||
// placeholder, which is what made the server read as somebody's TV.
|
||||
func TestAuthHeaderNamesTheGatewayForItsOwnRequests(t *testing.T) {
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", "MbyGateway", time.Second)
|
||||
|
||||
req, err := client.newRequest(
|
||||
context.Background(), http.MethodGet, "/Items", nil, Credentials{Gateway: true}, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := req.Header.Get("X-Emby-Authorization")
|
||||
if !strings.Contains(got, `Client="MbyGateway"`) {
|
||||
t.Fatalf("gateway request did not report the gateway client name: %q", got)
|
||||
}
|
||||
if strings.Contains(got, "Memby") {
|
||||
t.Fatalf("gateway request carried the product name to Emby: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A television's request must keep reporting the television's client name, whatever the
|
||||
// gateway calls itself — the two identities are separate rows in Emby's device list.
|
||||
func TestAuthHeaderKeepsTelevisionClientName(t *testing.T) {
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", "MbyGateway", time.Second)
|
||||
|
||||
req, err := client.newRequest(
|
||||
context.Background(), http.MethodGet, "/Items", nil,
|
||||
Credentials{DeviceID: "tv-1", DeviceName: "Living room", ClientVersion: "0.2.57"}, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := req.Header.Get("X-Emby-Authorization"); !strings.Contains(got, `Client="MbyATV"`) {
|
||||
t.Fatalf("television request did not report the app client name: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A blank gateway name must not silently become the television's, which would put the
|
||||
// server back in the device list as a set.
|
||||
func TestGatewayClientNameFallsBackToDefault(t *testing.T) {
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", " ", time.Second)
|
||||
if client.gatewayClientName != DefaultGatewayClientName {
|
||||
t.Fatalf("gateway client name = %q, want %q",
|
||||
client.gatewayClientName, DefaultGatewayClientName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ func TestDirectPlayVideoCodecsIncludeHEVCOnlyForCapableClient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Emby shows the device profile's name in its playback device list, so it carries the
|
||||
// client identity — never the product name, which is what it said before.
|
||||
func TestAndroidTVProfileReportsTheClientIdentityNotTheProductName(t *testing.T) {
|
||||
profile := androidTVDeviceProfile(PlaybackCapabilities{})
|
||||
if got := profile["Name"]; got != "MbyATV" {
|
||||
t.Fatalf("device profile name = %v, want MbyATV", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndroidTVProfileConstrainsCodecLevelAndResolution(t *testing.T) {
|
||||
profile := androidTVDeviceProfile(PlaybackCapabilities{
|
||||
H264Profiles: []string{"baseline", "main", "high"},
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestHideFromResumeUsesDedicatedEmbyEndpoint(t *testing.T) {
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
client := New(upstream.URL, upstream.URL, "MbyATV", time.Second)
|
||||
client := New(upstream.URL, upstream.URL, "MbyATV", "MbyGateway", time.Second)
|
||||
got, err := client.HideFromResume(
|
||||
context.Background(),
|
||||
Credentials{UserID: "user-1", Token: "token"},
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSubtitleURLUsesCanonicalVTTEndpoint(t *testing.T) {
|
||||
client := New("http://emby:8096", "https://emby.example", "Memby", time.Second)
|
||||
client := New("http://emby:8096", "https://emby.example", "MbyATV", "MbyGateway", time.Second)
|
||||
got := client.SubtitleURL(
|
||||
Credentials{Token: "a b"},
|
||||
"item id",
|
||||
|
||||
@@ -11,6 +11,14 @@ package emby
|
||||
|
||||
import "strconv"
|
||||
|
||||
// deviceProfileName is what Emby records against a playback session, and it appears in
|
||||
// the dashboard's device and playback lists. It is the client identity on the wire, not
|
||||
// the product name — it read as "Memby Android TV" there, which is exactly the name that
|
||||
// has no business travelling to somebody else's logs — and it must match the literal the
|
||||
// television sends on the direct path (DeviceProfile.embyAndroidTv), or one set playing
|
||||
// both ways appears twice.
|
||||
const deviceProfileName = "MbyATV"
|
||||
|
||||
var alwaysDecodableAudioCodecs = []string{
|
||||
"aac", "mp3", "flac", "opus", "vorbis", "pcm_s16le", "pcm_s24le",
|
||||
}
|
||||
@@ -44,7 +52,7 @@ func androidTVDeviceProfile(capabilities PlaybackCapabilities) map[string]any {
|
||||
audioCodecs := directPlayAudioCodecs(capabilities)
|
||||
transcodeAudio := transcodeAudioCodecs(capabilities)
|
||||
return map[string]any{
|
||||
"Name": "Memby Android TV", "SupportedMediaTypes": "Video",
|
||||
"Name": deviceProfileName, "SupportedMediaTypes": "Video",
|
||||
"DirectPlayProfiles": []map[string]string{
|
||||
{
|
||||
"Container": "mkv,mp4,m4v,mov,ts,mpegts", "VideoCodec": videoCodecs,
|
||||
|
||||
Reference in New Issue
Block a user