fix(historyimport): use standard Jellyfin authorization header (#572)

Co-authored-by: OpenAI Codex (GPT-5) <codex@openai.com>
This commit is contained in:
Tomislav Filipcic
2026-08-11 10:19:56 -04:00
committed by GitHub
co-authored by OpenAI Codex
parent 383a2cce71
commit ceb3393992
2 changed files with 72 additions and 5 deletions
+9 -5
View File
@@ -69,7 +69,7 @@ func (c *JellyfinClient) AuthenticateServerUser(ctx context.Context, baseURL, us
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Emby-Authorization", jellyfinAuthorizationHeader())
setJellyfinAuthorizationHeader(req, jellyfinAuthorizationHeader())
var resp jellyfinServerAuthResponse
err = c.doJSON(req, &resp)
if err == nil {
@@ -128,7 +128,7 @@ func (c *JellyfinClient) FetchItemsByIDs(ctx context.Context, auth jellyfinLocal
if err != nil {
return nil, err
}
req.Header.Set("X-Emby-Authorization", jellyfinAuthorizationHeaderWithToken(auth.AccessToken))
setJellyfinAuthorizationHeader(req, jellyfinAuthorizationHeaderWithToken(auth.AccessToken))
var payload jellyfinItemsResponse
if err := c.doJSON(req, &payload); err != nil {
@@ -156,7 +156,7 @@ func (c *JellyfinClient) FetchResumableItems(ctx context.Context, auth jellyfinL
if err != nil {
return nil, err
}
req.Header.Set("X-Emby-Authorization", jellyfinAuthorizationHeaderWithToken(auth.AccessToken))
setJellyfinAuthorizationHeader(req, jellyfinAuthorizationHeaderWithToken(auth.AccessToken))
var payload jellyfinItemsResponse
if err := c.doJSON(req, &payload); err != nil {
@@ -191,7 +191,7 @@ func (c *JellyfinClient) fetchPagedItems(ctx context.Context, auth jellyfinLocal
if err != nil {
return nil, err
}
req.Header.Set("X-Emby-Authorization", jellyfinAuthorizationHeaderWithToken(auth.AccessToken))
setJellyfinAuthorizationHeader(req, jellyfinAuthorizationHeaderWithToken(auth.AccessToken))
var payload jellyfinItemsResponse
if err := c.doJSON(req, &payload); err != nil {
@@ -244,13 +244,17 @@ func jellyfinAuthorizationHeaderWithToken(token string) string {
return jellyfinAuthorizationHeader() + `, Token="` + token + `"`
}
func setJellyfinAuthorizationHeader(req *http.Request, value string) {
req.Header.Set("Authorization", value)
}
// ListUsers returns all user accounts on the Jellyfin server using an admin API token.
func (c *JellyfinClient) ListUsers(ctx context.Context, baseURL, adminToken string) ([]ExternalUser, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(baseURL, "/")+"/Users", nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Emby-Authorization", jellyfinAuthorizationHeaderWithToken(adminToken))
setJellyfinAuthorizationHeader(req, jellyfinAuthorizationHeaderWithToken(adminToken))
var users []struct {
ID string `json:"Id"`
Name string `json:"Name"`
@@ -9,12 +9,74 @@ import (
"testing"
)
func assertJellyfinAuthorization(t *testing.T, r *http.Request, token string) {
t.Helper()
want := `MediaBrowser Client="watch-importer", Device="Silo", DeviceId="silo-history-import", Version="1.0.0"`
if token != "" {
want += `, Token="` + token + `"`
}
if got := r.Header.Get("Authorization"); got != want {
t.Fatalf("Authorization = %q, want %q", got, want)
}
if got := r.Header.Get("X-Emby-Authorization"); got != "" {
t.Fatalf("X-Emby-Authorization = %q, want empty", got)
}
}
func TestJellyfinAuthenticateServerUser_UsesStandardAuthorizationHeader(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.URL.Path; got != "/Users/AuthenticateByName" {
t.Fatalf("path = %q, want /Users/AuthenticateByName", got)
}
assertJellyfinAuthorization(t, r, "")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"AccessToken":"user-token","User":{"Id":"user-1"}}`))
}))
defer server.Close()
client := NewJellyfinClient()
auth, err := client.AuthenticateServerUser(context.Background(), server.URL, "alice", "password")
if err != nil {
t.Fatalf("AuthenticateServerUser returned error: %v", err)
}
if auth.UserID != "user-1" || auth.AccessToken != "user-token" {
t.Fatalf("auth = %+v, want user-1 with user-token", auth)
}
}
func TestJellyfinListUsers_UsesStandardAuthorizationHeader(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.URL.Path; got != "/Users" {
t.Fatalf("path = %q, want /Users", got)
}
assertJellyfinAuthorization(t, r, "admin-token")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"Id":"user-1","Name":"Alice"},{"Id":"","Name":"Missing ID"}]`))
}))
defer server.Close()
client := NewJellyfinClient()
users, err := client.ListUsers(context.Background(), server.URL, "admin-token")
if err != nil {
t.Fatalf("ListUsers returned error: %v", err)
}
if len(users) != 1 || users[0].ID != "user-1" || users[0].Name != "Alice" {
t.Fatalf("users = %+v, want Alice (user-1)", users)
}
}
func TestJellyfinFetchResumableItems_IncludesExpectedQueryAndPaginates(t *testing.T) {
t.Parallel()
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
assertJellyfinAuthorization(t, r, "token-1")
if got := r.URL.Path; got != "/UserItems/Resume" {
t.Fatalf("path = %q, want /UserItems/Resume", got)
@@ -71,6 +133,7 @@ func TestJellyfinFetchItems_PaginatesPlayedItems(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
assertJellyfinAuthorization(t, r, "token-1")
if got := r.URL.Path; got != "/Users/user-1/Items" {
t.Fatalf("path = %q, want /Users/user-1/Items", got)