Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

112 lines
4.0 KiB
Go

package embyapi
import (
"fmt"
"net/http"
"strings"
"github.com/bjarneo/cliamp/internal/appmeta"
"github.com/bjarneo/cliamp/provider"
)
// dialect captures the handful of behaviors that differ between Emby and
// Jellyfin. Everything else in Client is shared.
type dialect interface {
name() string // error-wrapping prefix
pingPath() string // endpoint Ping hits
metaKey() string // playlist.Track ProviderMeta key
applyAuth(req *http.Request, token, userID, deviceID string) // set auth headers
discoverUserID(c *Client) (string, error) // user-id discovery strategy
}
// embyDialect speaks Emby's `Authorization: Emby ...` scheme and discovers the
// user id with an API-key fallback.
type embyDialect struct{}
func (embyDialect) name() string { return "emby" }
func (embyDialect) pingPath() string { return "/System/Info" }
func (embyDialect) metaKey() string { return provider.MetaEmbyID }
func (embyDialect) applyAuth(req *http.Request, token, userID, deviceID string) {
if token != "" {
req.Header.Set("X-Emby-Token", token)
req.Header.Set("Authorization", embyAuthHeader(userID, token, deviceID))
} else {
req.Header.Set("Authorization", embyUnauthHeader(deviceID))
}
}
func (embyDialect) discoverUserID(c *Client) (string, error) {
// Try /Users/Me first (works for session tokens from password auth).
var me userDTO
if err := c.get("/Users/Me", nil, &me); err == nil && me.ID != "" {
c.setUserID(me.ID)
return me.ID, nil
}
// Fall back to /Users for API key auth (server-level key has no "me").
var users []userDTO
if err := c.get("/Users", nil, &users); err != nil {
return "", fmt.Errorf("emby: could not discover user id (set user_id in config): %w", err)
}
// Prefer user matching the configured username; otherwise take first entry.
for _, u := range users {
if strings.EqualFold(u.Name, c.user) {
c.setUserID(u.ID)
return u.ID, nil
}
}
if c.user != "" {
return "", fmt.Errorf("emby: user %q not found — check the user name in config", c.user)
}
if len(users) > 0 && users[0].ID != "" {
c.setUserID(users[0].ID)
return users[0].ID, nil
}
return "", fmt.Errorf("emby: could not discover user id — set user_id in config")
}
// unauthHeader / authHeader build Emby's Authorization header values.
func embyUnauthHeader(deviceID string) string {
return fmt.Sprintf(`Emby Client="%s", Device="%s", DeviceId="%s", Version="%s"`,
appmeta.ClientName(), appmeta.DeviceName(), deviceID, appmeta.Version())
}
func embyAuthHeader(userID, token, deviceID string) string {
if userID != "" {
return fmt.Sprintf(`Emby UserId="%s", Client="%s", Device="%s", DeviceId="%s", Version="%s", Token="%s"`,
userID, appmeta.ClientName(), appmeta.DeviceName(), deviceID, appmeta.Version(), token)
}
return fmt.Sprintf(`Emby Client="%s", Device="%s", DeviceId="%s", Version="%s", Token="%s"`,
appmeta.ClientName(), appmeta.DeviceName(), deviceID, appmeta.Version(), token)
}
// jellyfinDialect speaks Jellyfin's `X-Emby-Authorization: MediaBrowser ...`
// scheme and discovers the user id from /Users/Me only.
type jellyfinDialect struct{}
func (jellyfinDialect) name() string { return "jellyfin" }
func (jellyfinDialect) pingPath() string { return "/Users/Me" }
func (jellyfinDialect) metaKey() string { return provider.MetaJellyfinID }
func (jellyfinDialect) applyAuth(req *http.Request, token, _, deviceID string) {
if token != "" {
req.Header.Set("X-Emby-Token", token)
}
req.Header.Set("X-Emby-Authorization",
fmt.Sprintf(`MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"`,
appmeta.ClientName(), appmeta.DeviceName(), deviceID, appmeta.Version()))
}
func (jellyfinDialect) discoverUserID(c *Client) (string, error) {
var u userDTO
if err := c.get("/Users/Me", nil, &u); err != nil {
return "", err
}
if u.ID == "" {
return "", fmt.Errorf("jellyfin: current user response missing id")
}
c.setUserID(u.ID)
return u.ID, nil
}