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") }