diff --git a/internal/clients/jellyfin/adapter_test.go b/internal/clients/jellyfin/adapter_test.go index 0ec8965..0c27afc 100644 --- a/internal/clients/jellyfin/adapter_test.go +++ b/internal/clients/jellyfin/adapter_test.go @@ -83,8 +83,8 @@ func TestAdapter_QuickConnectInitiate_TranslatesUpstreamUnavailable(t *testing.T func TestAdapter_QuickConnectAuthenticate_TranslatesPending(t *testing.T) { a := newAdapterServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/QuickConnect/Authenticate" { - http.Error(w, "", http.StatusBadRequest) + if r.URL.Path == "/QuickConnect/Connect" { + _ = json.NewEncoder(w).Encode(map[string]any{"Authenticated": false}) return } http.NotFound(w, r) @@ -97,14 +97,17 @@ func TestAdapter_QuickConnectAuthenticate_TranslatesPending(t *testing.T) { func TestAdapter_QuickConnectAuthenticate_TranslatesResult(t *testing.T) { a := newAdapterServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/QuickConnect/Authenticate" { + switch r.URL.Path { + case "/QuickConnect/Connect": + _ = json.NewEncoder(w).Encode(map[string]any{"Authenticated": true}) + case "/Users/AuthenticateWithQuickConnect": _ = json.NewEncoder(w).Encode(map[string]any{ "AccessToken": "tok-qc", "User": map[string]any{"Id": "jf-2", "Name": "bob"}, }) - return + default: + http.NotFound(w, r) } - http.NotFound(w, r) }) res, err := a.QuickConnectAuthenticate(context.Background(), "secret") if err != nil { diff --git a/internal/clients/jellyfin/quickconnect.go b/internal/clients/jellyfin/quickconnect.go index 9bbf7ba..dae2ef2 100644 --- a/internal/clients/jellyfin/quickconnect.go +++ b/internal/clients/jellyfin/quickconnect.go @@ -1,6 +1,7 @@ package jellyfin import ( + "bytes" "context" "encoding/json" "errors" @@ -48,18 +49,19 @@ func (c *Client) QuickConnectInitiate(ctx context.Context) (*QuickConnectInitiat } func (c *Client) QuickConnectAuthenticate(ctx context.Context, secret string) (*AuthResult, error) { - raw, err := url.JoinPath(c.baseURL, "QuickConnect", "Authenticate") + connectRaw, err := url.JoinPath(c.baseURL, "QuickConnect", "Connect") if err != nil { return nil, err } - u, err := url.Parse(raw) + connectURL, err := url.Parse(connectRaw) if err != nil { return nil, err } - q := u.Query() + q := connectURL.Query() q.Set("secret", secret) - u.RawQuery = q.Encode() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil) + connectURL.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, connectURL.String(), nil) if err != nil { return nil, err } @@ -71,12 +73,44 @@ func (c *Client) QuickConnectAuthenticate(ctx context.Context, secret string) (* } defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - case http.StatusBadRequest: + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%w: Connect status %d", ErrUpstreamUnavailable, resp.StatusCode) + } + + var state struct { + Authenticated bool `json:"Authenticated"` + } + if err := json.NewDecoder(resp.Body).Decode(&state); err != nil { + return nil, fmt.Errorf("%w: decode Connect: %v", ErrUpstreamUnavailable, err) + } + if !state.Authenticated { return nil, ErrQuickConnectPending - default: - return nil, fmt.Errorf("%w: status %d", ErrUpstreamUnavailable, resp.StatusCode) + } + + authRaw, err := url.JoinPath(c.baseURL, "Users", "AuthenticateWithQuickConnect") + if err != nil { + return nil, err + } + body, err := json.Marshal(map[string]string{"Secret": secret}) + if err != nil { + return nil, err + } + + req2, err := http.NewRequestWithContext(ctx, http.MethodPost, authRaw, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req2.Header.Set("Content-Type", "application/json") + req2.Header.Set("X-Emby-Authorization", authHeader("api-proxy-qc")) + + resp2, err := c.hc.Do(req2) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrUpstreamUnavailable, err) + } + defer resp2.Body.Close() + + if resp2.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%w: AuthenticateWithQuickConnect status %d", ErrUpstreamUnavailable, resp2.StatusCode) } var parsed struct { @@ -86,8 +120,8 @@ func (c *Client) QuickConnectAuthenticate(ctx context.Context, secret string) (* Name string `json:"Name"` } `json:"User"` } - if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { - return nil, fmt.Errorf("%w: decode: %v", ErrUpstreamUnavailable, err) + if err := json.NewDecoder(resp2.Body).Decode(&parsed); err != nil { + return nil, fmt.Errorf("%w: decode auth: %v", ErrUpstreamUnavailable, err) } return &AuthResult{ AccessToken: parsed.AccessToken, diff --git a/internal/clients/jellyfin/quickconnect_test.go b/internal/clients/jellyfin/quickconnect_test.go index aa85af7..e94a456 100644 --- a/internal/clients/jellyfin/quickconnect_test.go +++ b/internal/clients/jellyfin/quickconnect_test.go @@ -33,14 +33,17 @@ func TestQuickConnectInitiate(t *testing.T) { func TestQuickConnectAuthenticate_Approved(t *testing.T) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/QuickConnect/Authenticate" { + switch r.URL.Path { + case "/QuickConnect/Connect": + _ = json.NewEncoder(w).Encode(map[string]any{"Authenticated": true}) + case "/Users/AuthenticateWithQuickConnect": + _ = json.NewEncoder(w).Encode(map[string]any{ + "AccessToken": "tok-qc", + "User": map[string]any{"Id": "jf-user-1", "Name": "alice"}, + }) + default: http.NotFound(w, r) - return } - _ = json.NewEncoder(w).Encode(map[string]any{ - "AccessToken": "tok-qc", - "User": map[string]any{"Id": "jf-user-1", "Name": "alice"}, - }) })) defer s.Close() @@ -55,8 +58,12 @@ func TestQuickConnectAuthenticate_Approved(t *testing.T) { } func TestQuickConnectAuthenticate_NotYetApproved(t *testing.T) { - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - http.Error(w, "", http.StatusBadRequest) + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/QuickConnect/Connect" { + _ = json.NewEncoder(w).Encode(map[string]any{"Authenticated": false}) + return + } + http.NotFound(w, r) })) defer s.Close() c := New(s.URL, "")