1package jellyfin23import (4 "context"5 "fmt"6 "net/http"7 "net/url"8)910// QuickConnect is a code sign-in in progress. Code is shown to the user,11// who enters it in a signed-in Jellyfin app; Secret stays on the server.12type QuickConnect struct{ Code, Secret string }1314// anonymous is a client that hasn't signed in, for Quick Connect.15func anonymous(server, deviceID string, hc *http.Client) *Client {16 if hc == nil {17 hc = defaultHTTP18 }19 return &Client{s: &session{server: Account{Server: server}.baseURL(), deviceID: deviceID}, hc: hc}20}2122// QuickConnectEnabled reports whether the server allows signing in with a23// code.24func QuickConnectEnabled(ctx context.Context, server string, hc *http.Client) (bool, error) {25 var on bool26 _, err := anonymous(server, "", hc).do(ctx, http.MethodGet, "/QuickConnect/Enabled", nil, nil, "", &on)27 return on, err28}2930// StartQuickConnect starts a code sign-in. Pass the same deviceID to31// FinishQuickConnect.32func StartQuickConnect(ctx context.Context, server, deviceID string, hc *http.Client) (QuickConnect, error) {33 var qc QuickConnect34 status, err := anonymous(server, deviceID, hc).do(ctx, http.MethodPost, "/QuickConnect/Initiate", nil, nil, "", &qc)35 if status == http.StatusUnauthorized || status == http.StatusForbidden {36 return qc, fmt.Errorf("jellyfin: %s doesn't allow Quick Connect", server)37 }38 if err == nil && (qc.Code == "" || qc.Secret == "") {39 err = fmt.Errorf("jellyfin: %s: Quick Connect gave no code", server)40 }41 return qc, err42}4344// CheckQuickConnect reports whether the user has approved the code. It45// fails once the code has expired.46func CheckQuickConnect(ctx context.Context, server, secret string, hc *http.Client) (approved bool, err error) {47 var r struct{ Authenticated bool }48 status, err := anonymous(server, "", hc).do(ctx, http.MethodGet, "/QuickConnect/Connect", url.Values{"secret": {secret}}, nil, "", &r)49 if status == http.StatusNotFound {50 return false, fmt.Errorf("jellyfin: the Quick Connect code expired")51 }52 return r.Authenticated, err53}5455// FinishQuickConnect trades an approved code's secret for an account that56// signs in with a token issued to deviceID.57func FinishQuickConnect(ctx context.Context, server, secret, deviceID string, hc *http.Client) (Account, error) {58 c := anonymous(server, deviceID, hc)59 var auth struct {60 AccessToken string61 User struct {62 ID string `json:"Id"`63 Name string64 }65 }66 body := map[string]string{"Secret": secret}67 if _, err := c.do(ctx, http.MethodPost, "/Users/AuthenticateWithQuickConnect", nil, body, "", &auth); err != nil {68 return Account{}, err69 }70 if auth.AccessToken == "" {71 return Account{}, fmt.Errorf("jellyfin: %s: Quick Connect gave no token", server)72 }73 return Account{Server: c.s.server, User: auth.User.Name, UserID: auth.User.ID, Token: auth.AccessToken, DeviceID: deviceID}, nil74}