Skip to content

Commit

Permalink
feat!: support cobalt api v10
Browse files Browse the repository at this point in the history
BREAKING CHANGE: this removes the support for the public v7 api, and changes the endpoints
  • Loading branch information
andresperezl committed Nov 16, 2024
1 parent 5224198 commit 7a38e27
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 159 deletions.
172 changes: 98 additions & 74 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,10 @@ import (
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"path"
)

const (
CobaltPublicAPI = "https://api.cobalt.tools/api"

EndpointJSON = "/json"
EndpointStream = "/stream"
EndpointServerInfo = "/serverInfo"
)

type Cobalt struct {
client *http.Client
apiBaseURL string
Expand All @@ -32,41 +22,42 @@ func NewCobaltWithAPI(apiBaseURL string) *Cobalt {
}
}

func NewCobaltWithPublicAPI() *Cobalt {
return &Cobalt{
client: http.DefaultClient,
apiBaseURL: CobaltPublicAPI,
}
}

func (c *Cobalt) WithHTTPClient(client *http.Client) *Cobalt {
c.client = client
return c
}

// Get will return a Response from where the file can be downloaded
func (c *Cobalt) Get(ctx context.Context, params Request) (*Media, error) {
// Post will return a PostResponse from where the file can be downloaded
// headers are passed as key value pairs. Examples `"API-KEY", "MyApiKey"`
func (c *Cobalt) Post(ctx context.Context, params PostRequest, headers ...string) (*PostResponse, error) {
buff := &bytes.Buffer{}
if err := json.NewEncoder(buff).Encode(params); err != nil {
return nil, err
}

u := fmt.Sprintf("%s%s", c.apiBaseURL, EndpointJSON)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, buff)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiBaseURL, buff)
if err != nil {
return nil, err
}

req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")

if len(headers)%2 != 0 {
return nil, fmt.Errorf("odd number of headers params, they must be passed as key value pairs")
}

for i := 0; i < len(headers); i += 2 {
req.Header.Add(headers[i], headers[i+1])
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

media := &Media{client: c.client}
media := &PostResponse{client: c.client}
if err := json.NewDecoder(resp.Body).Decode(media); err != nil {
return nil, err
}
Expand All @@ -78,75 +69,108 @@ func (c *Cobalt) Get(ctx context.Context, params Request) (*Media, error) {
return media, nil
}

// ParseFilename will try to extract the filename depending on the type of the m.StatusResponse.
// This is intended to be used with the *http.Response when calling the URL pointedb by m.URL
//
// When m.StatusResponse == StatusResponseRedirect, the filename will be set based on the basename of the URL path
//
// When m.StatusResponse == StatusResponseStream, the filename will be extracted from the Content-Disposition header
//
// All other unsupported methods leave the m.Filename empty
// Errors returned are unexpected, and will be a consenquence of a parsing error.
func (m *Media) ParseFilename(resp *http.Response) error {
if m.Status == ResponseStatusError || m.Status == ResponseStatusRateLimit {
return nil
}

if m.Status == ResponseStatusRedirect {
parsedURL, err := url.Parse(m.URL)
if err != nil {
return err
}
m.filename = path.Base(parsedURL.Path)
return nil
}

if m.Status == ResponseStatusStream {
cd := resp.Header.Get("Content-Disposition")
if cd != "" {
_, params, err := mime.ParseMediaType(cd)
if err != nil {
return err
}
if filename, ok := params["filename"]; ok {
m.filename = filename
}
}
}

return nil
// Stream is a helper utility that will return an io.ReadCloser using the URL from this media object
// The returned io.ReadCloser is the Body of *http.Response and must be closed when you are done with the stream.
// When the m.Status == ResponseStatusPicker it will stream the first item from the m.Picker array.
func (m *PostResponse) Stream(ctx context.Context) (io.ReadCloser, error) {
if m.Status != ResponseStatusTunnel && m.Status != ResponseStatusRedirect && m.Status != ResponseStatusPicker {
return nil, fmt.Errorf("unstreamable response type %s", m.Status)
}

url := m.URL
if m.Status == ResponseStatusPicker && len(m.Picker) > 0 {
url = m.Picker[0].URL
}
if len(url) == 0 {
return nil, fmt.Errorf("url is empty, nothing to stream")
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}

resp, err := m.client.Do(req)
if err != nil {
return nil, err
}

return resp.Body, nil
}

// Filename will return the filename associated with this media. ParseFilename must be called first, either directly or indirectly via m.Stream().
// Not doing so will keep the filename empty.
func (m *Media) Filename() string {
return m.filename
func (c *Cobalt) Get(ctx context.Context, headers ...string) (*GetResponse, error) {

req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiBaseURL, nil)
if err != nil {
return nil, err
}

req.Header.Add("Accept", "application/json")

if len(headers)%2 != 0 {
return nil, fmt.Errorf("odd number of headers params, they must be passed as key value pairs")
}

for i := 0; i < len(headers); i += 2 {
req.Header.Add(headers[i], headers[i+1])
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

info := &GetResponse{}
if err := json.NewDecoder(resp.Body).Decode(info); err != nil {
return nil, err
}

return info, nil
}

// Stream is a helper utility that will return an io.ReadCloser using the URL from this media object
// The returned io.ReadCloser is the Body of *http.Response and must be closed when you are done with the stream.
// Stream will also call ParseFilename, so m.Filename() will be set
func (m *Media) Stream(ctx context.Context) (io.ReadCloser, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.URL, nil)
const (
EndpointSession = "session"
)

func (c *Cobalt) Session(ctx context.Context, headers ...string) (*SessionResponse, error) {

req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.Join(c.apiBaseURL, EndpointSession), nil)
if err != nil {
return nil, err
}

resp, err := m.client.Do(req)
req.Header.Add("Accept", "application/json")

if len(headers)%2 != 0 {
return nil, fmt.Errorf("odd number of headers params, they must be passed as key value pairs")
}

for i := 0; i < len(headers); i += 2 {
req.Header.Add(headers[i], headers[i+1])
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if err := m.ParseFilename(resp); err != nil {
defer resp.Body.Close()
defer resp.Body.Close()

token := &SessionResponse{}
if err := json.NewDecoder(resp.Body).Decode(token); err != nil {
return nil, err
}

return resp.Body, nil
if token.Status == ResponseStatusError {
return nil, fmt.Errorf("%+v", token.ErrorInfo)
}

return token, nil
}

// CobalAPIError is just a convenient type to convert Media into an error.
type CobaltAPIError Media
type CobaltAPIError PostResponse

func (err CobaltAPIError) Error() string {
return err.Text
return fmt.Sprintf("%+v", err.ErrorInfo)
}
37 changes: 37 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gobalt

import (
"context"
"net/url"
"testing"
)

var (
urls = []string{
"https://x.com/tonystatovci/status/1856853985149227419?t=WuK-zVfde8WTofpdt7UBaQ&s=19",
}
)

func TestClient(t *testing.T) {
client := NewCobaltWithAPI("http://localhost:9000/")
for _, u := range urls {
pURL, _ := url.Parse(u)
t.Run(pURL.Host, func(t *testing.T) {
media, err := client.Post(context.Background(), PostRequest{URL: u})
if err != nil {
t.Errorf("failed to fetch media for %s url with error: %v", u, err)
}

if len(media.Filename) == 0 {
t.Error("filename was empty")
}

s, err := media.Stream(context.Background())
if err != nil {
t.Errorf("failed to stream media with error: %v", err)
return
}
defer s.Close()
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/andresperezl/gobalt
module github.com/andresperezl/gobalt/v2

go 1.23.0
Loading

0 comments on commit 7a38e27

Please sign in to comment.