Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions internal/clients/jellyfin/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
58 changes: 46 additions & 12 deletions internal/clients/jellyfin/quickconnect.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jellyfin

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
23 changes: 15 additions & 8 deletions internal/clients/jellyfin/quickconnect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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, "")
Expand Down