mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
jellyseerr: start API client
Currently uses an API key (Seems simpler for the user than importing the jfa-go user and granting perms). Strategy as follows: * MustGetUser(jfID) function checks the cache for user, if not, calls Jellyseerr's importer passing jfID. From either, the user object is returned, which (in later commits) can be used to update the user's email (and potentially other info). My API key is in there rn but its for a local testing instance, who cares.
This commit is contained in:
parent
dabef831d7
commit
9c34192b4f
7
jellyseerr/go.mod
Normal file
7
jellyseerr/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/hrfee/jfa-go/jellyseerr
|
||||||
|
|
||||||
|
replace github.com/hrfee/jfa-go/common => ../common
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769 // indirect
|
238
jellyseerr/jellyseerr.go
Normal file
238
jellyseerr/jellyseerr.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package jellyseerr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
API_SUFFIX = "/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Jellyseerr represents a running Jellyseerr instance.
|
||||||
|
type Jellyseerr struct {
|
||||||
|
server, key string
|
||||||
|
header map[string]string
|
||||||
|
httpClient *http.Client
|
||||||
|
userCache map[string]User // Map of jellyfin IDs to users
|
||||||
|
cacheExpiry time.Time
|
||||||
|
cacheLength time.Duration
|
||||||
|
timeoutHandler common.TimeoutHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJellyseerr returns an Ombi object.
|
||||||
|
func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Jellyseerr {
|
||||||
|
if !strings.HasSuffix(server, API_SUFFIX) {
|
||||||
|
server = server + API_SUFFIX
|
||||||
|
}
|
||||||
|
return &Jellyseerr{
|
||||||
|
server: server,
|
||||||
|
key: key,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
header: map[string]string{
|
||||||
|
"X-Api-Key": key,
|
||||||
|
},
|
||||||
|
cacheLength: time.Duration(30) * time.Minute,
|
||||||
|
cacheExpiry: time.Now(),
|
||||||
|
timeoutHandler: timeoutHandler,
|
||||||
|
userCache: map[string]User{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// does a GET and returns the response as a string.
|
||||||
|
func (js *Jellyseerr) getJSON(url string, params map[string]string, queryParams url.Values) (string, int, error) {
|
||||||
|
if js.key == "" {
|
||||||
|
return "", 401, fmt.Errorf("No API key provided")
|
||||||
|
}
|
||||||
|
var req *http.Request
|
||||||
|
if params != nil {
|
||||||
|
jsonParams, _ := json.Marshal(params)
|
||||||
|
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), bytes.NewBuffer(jsonParams))
|
||||||
|
} else {
|
||||||
|
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), nil)
|
||||||
|
}
|
||||||
|
for name, value := range js.header {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
resp, err := js.httpClient.Do(req)
|
||||||
|
defer js.timeoutHandler()
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
if resp.StatusCode == 401 {
|
||||||
|
return "", 401, fmt.Errorf("Invalid API Key")
|
||||||
|
}
|
||||||
|
return "", resp.StatusCode, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var data io.Reader
|
||||||
|
switch resp.Header.Get("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
data, _ = gzip.NewReader(resp.Body)
|
||||||
|
default:
|
||||||
|
data = resp.Body
|
||||||
|
}
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
_, err = io.Copy(buf, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", 500, err
|
||||||
|
}
|
||||||
|
return buf.String(), resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// does a POST and optionally returns response as string. Returns a string instead of an io.reader bcs i couldn't get it working otherwise.
|
||||||
|
func (js *Jellyseerr) send(mode string, url string, data interface{}, response bool, headers map[string]string) (string, int, error) {
|
||||||
|
responseText := ""
|
||||||
|
params, _ := json.Marshal(data)
|
||||||
|
req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params))
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
for name, value := range js.header {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
for name, value := range headers {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
resp, err := js.httpClient.Do(req)
|
||||||
|
defer js.timeoutHandler()
|
||||||
|
if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
|
||||||
|
if resp.StatusCode == 401 {
|
||||||
|
return "", 401, fmt.Errorf("Invalid API Key")
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
if response {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var out io.Reader
|
||||||
|
switch resp.Header.Get("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
out, _ = gzip.NewReader(resp.Body)
|
||||||
|
default:
|
||||||
|
out = resp.Body
|
||||||
|
}
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
_, err = io.Copy(buf, out)
|
||||||
|
if err != nil {
|
||||||
|
return "", 500, err
|
||||||
|
}
|
||||||
|
responseText = buf.String()
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||||
|
return js.send("POST", url, data, response, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) put(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||||
|
return js.send("PUT", url, data, response, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"jellyfinUserIds": jfIDs,
|
||||||
|
}
|
||||||
|
resp, status, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
||||||
|
var data []User
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
for _, u := range data {
|
||||||
|
if u.JellyfinUserID != "" {
|
||||||
|
js.userCache[u.JellyfinUserID] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) getUsers() error {
|
||||||
|
if js.cacheExpiry.After(time.Now()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
js.cacheExpiry = time.Now().Add(js.cacheLength)
|
||||||
|
pageCount := 1
|
||||||
|
pageIndex := 0
|
||||||
|
for {
|
||||||
|
res, err := js.getUserPage(0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, u := range res.Results {
|
||||||
|
if u.JellyfinUserID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
js.userCache[u.JellyfinUserID] = u
|
||||||
|
}
|
||||||
|
pageCount = res.Page.Pages
|
||||||
|
pageIndex++
|
||||||
|
if pageIndex >= pageCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("take", "30")
|
||||||
|
params.Add("skip", strconv.Itoa(page))
|
||||||
|
params.Add("sort", "created")
|
||||||
|
resp, status, err := js.getJSON(js.server+"/user", nil, params)
|
||||||
|
var data GetUsersDTO
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetUser provides the same function as ImportFromJellyfin, but will always return the user,
|
||||||
|
// even if they already existed.
|
||||||
|
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
|
||||||
|
js.getUsers()
|
||||||
|
if u, ok := js.userCache[jfID]; ok {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
users, err := js.ImportFromJellyfin(jfID)
|
||||||
|
var u User
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
if len(users) != 0 {
|
||||||
|
return users[0], err
|
||||||
|
}
|
||||||
|
if u, ok := js.userCache[jfID]; ok {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
return u, fmt.Errorf("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) Me() (User, error) {
|
||||||
|
resp, status, err := js.getJSON(js.server+"/auth/me", nil, url.Values{})
|
||||||
|
var data User
|
||||||
|
data.ID = -1
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
49
jellyseerr/jellyseerr_test.go
Normal file
49
jellyseerr/jellyseerr_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package jellyseerr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
API_KEY = "MTcyMjI2MDM2MTYyMzMxNDZkZmYyLTE4MzMtNDUyNy1hODJlLTI0MTZkZGUyMDg2Ng=="
|
||||||
|
URI = "http://localhost:5055"
|
||||||
|
)
|
||||||
|
|
||||||
|
func client() *Jellyseerr {
|
||||||
|
return NewJellyseerr(URI, API_KEY, common.NewTimeoutHandler("Jellyseerr", URI, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMe(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
u, err := js.Me()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if u.ID < 0 {
|
||||||
|
t.Fatalf("returned no user %+v\n", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportFromJellyfin(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
list, err := js.ImportFromJellyfin("6b75e189efb744f583aa2e8e9cee41d3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if len(list) == 0 {
|
||||||
|
t.Fatalf("returned no users")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustGetUser(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
u, err := js.MustGetUser("8c9d25c070d641cd8ad9cf825f622a16")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if u.ID < 0 {
|
||||||
|
t.Fatalf("returned no users")
|
||||||
|
}
|
||||||
|
}
|
41
jellyseerr/models.go
Normal file
41
jellyseerr/models.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package jellyseerr
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Permissions int `json:"permissions"`
|
||||||
|
Warnings []any `json:"warnings"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PlexUsername string `json:"plexUsername"`
|
||||||
|
JellyfinUsername string `json:"jellyfinUsername"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
RecoveryLinkExpirationDate any `json:"recoveryLinkExpirationDate"`
|
||||||
|
UserType int `json:"userType"`
|
||||||
|
PlexID string `json:"plexId"`
|
||||||
|
JellyfinUserID string `json:"jellyfinUserId"`
|
||||||
|
JellyfinDeviceID string `json:"jellyfinDeviceId"`
|
||||||
|
JellyfinAuthToken string `json:"jellyfinAuthToken"`
|
||||||
|
PlexToken string `json:"plexToken"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
MovieQuotaLimit any `json:"movieQuotaLimit"`
|
||||||
|
MovieQuotaDays any `json:"movieQuotaDays"`
|
||||||
|
TvQuotaLimit any `json:"tvQuotaLimit"`
|
||||||
|
TvQuotaDays any `json:"tvQuotaDays"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
RequestCount int `json:"requestCount"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo struct {
|
||||||
|
Pages int `json:"pages"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
Results int `json:"results"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsersDTO struct {
|
||||||
|
Page PageInfo `json:"pageInfo"`
|
||||||
|
Results []User `json:"results"`
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user