jellyfin-user-clone/main.go

204 lines
4.7 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
jellyFrom, err := NewJelly(ctx, "squeaky2x3", "aFJKcZ4fUuN9FZ", "5c1de748f61145a085f272aea527c759", "213abf9acbe84d9fb9c3b06bbe1eec3b")
if err != nil {
log.Fatal(err)
}
log.Printf("from %+v", jellyFrom)
jellyTo, err := NewJelly(ctx, "belly", "qBentOcpHMUjhD", "497b212a22e34b54be091055edbe264d", "b71b931108ba4323b75b675871a7738f")
if err != nil {
log.Fatal(err)
}
log.Printf("to %+v", jellyTo)
folders, err := jellyFrom.VirtualFolders(ctx)
if err != nil {
log.Fatal(err)
}
for len(folders) > 0 {
folder := folders[0]
folders = folders[1:]
items, err := jellyFrom.ListFolder(ctx, folder)
if err != nil {
log.Fatal(err)
}
for _, item := range items {
b, _ := json.MarshalIndent(item, "", " ")
log.Fatalf("%s", b)
}
}
log.Fatalf("not impl: %+v => %+v", jellyFrom, jellyTo)
}
type Jelly struct {
u string
apitoken string
accesstoken string
}
func NewJelly(ctx context.Context, u, p, apitoken, optAccessToken string) (Jelly, error) {
jelly := Jelly{u: u, apitoken: apitoken, accesstoken: optAccessToken}
if optAccessToken != "" {
} else if accessToken, err := jelly.Login(ctx, p); err != nil {
return Jelly{}, err
} else {
jelly.accesstoken = accessToken
}
return jelly, nil
}
type FolderItem struct {
Name string
Id string
IsFolder bool
Parent Folder
}
func (jelly Jelly) ListFolder(ctx context.Context, folder Folder) ([]FolderItem, error) {
resp, err := jelly.do(ctx, http.MethodGet, fmt.Sprintf("/Items?parentId=%s", folder.Id), nil)
if err != nil {
return nil, err
}
a := resp.(map[string]any)["Items"].([]any)
result := make([]FolderItem, len(a))
for i := range a {
m := a[i].(map[string]any)
result[i] = FolderItem{
Name: m["Name"].(string),
Id: m["Id"].(string),
IsFolder: m["IsFolder"].(bool),
Parent: folder,
}
}
return result, nil
}
// get+set item via /UserItems/{itemId}/UserData
type Folder struct {
Name string
Id string
}
func (jelly Jelly) VirtualFolders(ctx context.Context) ([]Folder, error) {
resp, err := jelly.do(ctx, http.MethodGet, "/Library/VirtualFolders", nil)
if err != nil {
return nil, err
}
a := resp.([]any)
result := make([]Folder, len(a))
for i, v := range a {
m := v.(map[string]any)
result[i] = Folder{
Id: m["ItemId"].(string),
Name: m["Name"].(string),
}
}
return result, nil
}
func (jelly Jelly) Login(ctx context.Context, p string) (string, error) {
resp, err := jelly.post(ctx, "/Users/AuthenticateByName", map[string]string{
"Username": jelly.u,
"Pw": p,
}, "Authorization", jelly._authHeader(jelly.apitoken))
if err != nil {
return "", err
}
m, _ := resp.(map[string]any)
if m == nil {
return "", fmt.Errorf("no map response")
}
token, _ := m["AccessToken"]
if token == nil {
return "", fmt.Errorf("no .AccessToken response")
}
s, _ := token.(string)
return s, nil
}
func (jelly Jelly) post(ctx context.Context, path string, body any, headers ...string) (any, error) {
return jelly.do(ctx, http.MethodPost, path, body, headers...)
}
func (jelly Jelly) do(ctx context.Context, method, path string, body any, headers ...string) (any, error) {
resp, err := jelly._do(ctx, method, path, body, headers...)
if err != nil {
return nil, err
}
var result any
err = json.NewDecoder(resp.Body).Decode(&result)
return result, err
}
func (jelly Jelly) _do(ctx context.Context, method, path string, body any, headers ...string) (*http.Response, error) {
t := http.Transport{
DisableKeepAlives: true,
}
c := http.Client{
Timeout: time.Minute,
Transport: &t,
}
b, _ := json.Marshal(body)
req, err := http.NewRequest(method, fmt.Sprintf("https://jellyfin.home.blapointe.com%s", path), bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", jelly.authHeader())
req = req.WithContext(ctx)
for i := 0; i+1 < len(headers); i += 2 {
req.Header.Set(headers[i], headers[i+1])
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ = io.ReadAll(resp.Body)
resp.Body = io.NopCloser(bytes.NewReader(b))
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("(%d) %s", resp.StatusCode, b)
}
return resp, nil
}
func (jelly Jelly) authHeader() string {
return jelly._authHeader(jelly.accesstoken)
}
func (jelly Jelly) _authHeader(token string) string {
return fmt.Sprintf("MediaBrowser Token=%q, Client=%q, Version=%q, DeviceId=%q, Device=%q", token, "client", "version", "deviceId"+jelly.u, "device")
}