Skip to content

Commit 7a38e27

Browse files
committed
feat!: support cobalt api v10
BREAKING CHANGE: this removes the support for the public v7 api, and changes the endpoints
1 parent 5224198 commit 7a38e27

File tree

4 files changed

+295
-159
lines changed

4 files changed

+295
-159
lines changed

client.go

Lines changed: 98 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,10 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"mime"
109
"net/http"
11-
"net/url"
1210
"path"
1311
)
1412

15-
const (
16-
CobaltPublicAPI = "https://api.cobalt.tools/api"
17-
18-
EndpointJSON = "/json"
19-
EndpointStream = "/stream"
20-
EndpointServerInfo = "/serverInfo"
21-
)
22-
2313
type Cobalt struct {
2414
client *http.Client
2515
apiBaseURL string
@@ -32,41 +22,42 @@ func NewCobaltWithAPI(apiBaseURL string) *Cobalt {
3222
}
3323
}
3424

35-
func NewCobaltWithPublicAPI() *Cobalt {
36-
return &Cobalt{
37-
client: http.DefaultClient,
38-
apiBaseURL: CobaltPublicAPI,
39-
}
40-
}
41-
4225
func (c *Cobalt) WithHTTPClient(client *http.Client) *Cobalt {
4326
c.client = client
4427
return c
4528
}
4629

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

54-
u := fmt.Sprintf("%s%s", c.apiBaseURL, EndpointJSON)
55-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, buff)
38+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiBaseURL, buff)
5639
if err != nil {
5740
return nil, err
5841
}
5942

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

46+
if len(headers)%2 != 0 {
47+
return nil, fmt.Errorf("odd number of headers params, they must be passed as key value pairs")
48+
}
49+
50+
for i := 0; i < len(headers); i += 2 {
51+
req.Header.Add(headers[i], headers[i+1])
52+
}
53+
6354
resp, err := c.client.Do(req)
6455
if err != nil {
6556
return nil, err
6657
}
6758
defer resp.Body.Close()
6859

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

81-
// ParseFilename will try to extract the filename depending on the type of the m.StatusResponse.
82-
// This is intended to be used with the *http.Response when calling the URL pointedb by m.URL
83-
//
84-
// When m.StatusResponse == StatusResponseRedirect, the filename will be set based on the basename of the URL path
85-
//
86-
// When m.StatusResponse == StatusResponseStream, the filename will be extracted from the Content-Disposition header
87-
//
88-
// All other unsupported methods leave the m.Filename empty
89-
// Errors returned are unexpected, and will be a consenquence of a parsing error.
90-
func (m *Media) ParseFilename(resp *http.Response) error {
91-
if m.Status == ResponseStatusError || m.Status == ResponseStatusRateLimit {
92-
return nil
93-
}
94-
95-
if m.Status == ResponseStatusRedirect {
96-
parsedURL, err := url.Parse(m.URL)
97-
if err != nil {
98-
return err
99-
}
100-
m.filename = path.Base(parsedURL.Path)
101-
return nil
102-
}
103-
104-
if m.Status == ResponseStatusStream {
105-
cd := resp.Header.Get("Content-Disposition")
106-
if cd != "" {
107-
_, params, err := mime.ParseMediaType(cd)
108-
if err != nil {
109-
return err
110-
}
111-
if filename, ok := params["filename"]; ok {
112-
m.filename = filename
113-
}
114-
}
115-
}
116-
117-
return nil
72+
// Stream is a helper utility that will return an io.ReadCloser using the URL from this media object
73+
// The returned io.ReadCloser is the Body of *http.Response and must be closed when you are done with the stream.
74+
// When the m.Status == ResponseStatusPicker it will stream the first item from the m.Picker array.
75+
func (m *PostResponse) Stream(ctx context.Context) (io.ReadCloser, error) {
76+
if m.Status != ResponseStatusTunnel && m.Status != ResponseStatusRedirect && m.Status != ResponseStatusPicker {
77+
return nil, fmt.Errorf("unstreamable response type %s", m.Status)
78+
}
79+
80+
url := m.URL
81+
if m.Status == ResponseStatusPicker && len(m.Picker) > 0 {
82+
url = m.Picker[0].URL
83+
}
84+
if len(url) == 0 {
85+
return nil, fmt.Errorf("url is empty, nothing to stream")
86+
}
87+
88+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
resp, err := m.client.Do(req)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
return resp.Body, nil
11899
}
119100

120-
// Filename will return the filename associated with this media. ParseFilename must be called first, either directly or indirectly via m.Stream().
121-
// Not doing so will keep the filename empty.
122-
func (m *Media) Filename() string {
123-
return m.filename
101+
func (c *Cobalt) Get(ctx context.Context, headers ...string) (*GetResponse, error) {
102+
103+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiBaseURL, nil)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
req.Header.Add("Accept", "application/json")
109+
110+
if len(headers)%2 != 0 {
111+
return nil, fmt.Errorf("odd number of headers params, they must be passed as key value pairs")
112+
}
113+
114+
for i := 0; i < len(headers); i += 2 {
115+
req.Header.Add(headers[i], headers[i+1])
116+
}
117+
118+
resp, err := c.client.Do(req)
119+
if err != nil {
120+
return nil, err
121+
}
122+
defer resp.Body.Close()
123+
124+
info := &GetResponse{}
125+
if err := json.NewDecoder(resp.Body).Decode(info); err != nil {
126+
return nil, err
127+
}
128+
129+
return info, nil
124130
}
125131

126-
// Stream is a helper utility that will return an io.ReadCloser using the URL from this media object
127-
// The returned io.ReadCloser is the Body of *http.Response and must be closed when you are done with the stream.
128-
// Stream will also call ParseFilename, so m.Filename() will be set
129-
func (m *Media) Stream(ctx context.Context) (io.ReadCloser, error) {
130-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.URL, nil)
132+
const (
133+
EndpointSession = "session"
134+
)
135+
136+
func (c *Cobalt) Session(ctx context.Context, headers ...string) (*SessionResponse, error) {
137+
138+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.Join(c.apiBaseURL, EndpointSession), nil)
131139
if err != nil {
132140
return nil, err
133141
}
134142

135-
resp, err := m.client.Do(req)
143+
req.Header.Add("Accept", "application/json")
144+
145+
if len(headers)%2 != 0 {
146+
return nil, fmt.Errorf("odd number of headers params, they must be passed as key value pairs")
147+
}
148+
149+
for i := 0; i < len(headers); i += 2 {
150+
req.Header.Add(headers[i], headers[i+1])
151+
}
152+
153+
resp, err := c.client.Do(req)
136154
if err != nil {
137155
return nil, err
138156
}
139-
if err := m.ParseFilename(resp); err != nil {
140-
defer resp.Body.Close()
157+
defer resp.Body.Close()
158+
159+
token := &SessionResponse{}
160+
if err := json.NewDecoder(resp.Body).Decode(token); err != nil {
141161
return nil, err
142162
}
143163

144-
return resp.Body, nil
164+
if token.Status == ResponseStatusError {
165+
return nil, fmt.Errorf("%+v", token.ErrorInfo)
166+
}
167+
168+
return token, nil
145169
}
146170

147171
// CobalAPIError is just a convenient type to convert Media into an error.
148-
type CobaltAPIError Media
172+
type CobaltAPIError PostResponse
149173

150174
func (err CobaltAPIError) Error() string {
151-
return err.Text
175+
return fmt.Sprintf("%+v", err.ErrorInfo)
152176
}

client_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package gobalt
2+
3+
import (
4+
"context"
5+
"net/url"
6+
"testing"
7+
)
8+
9+
var (
10+
urls = []string{
11+
"https://x.com/tonystatovci/status/1856853985149227419?t=WuK-zVfde8WTofpdt7UBaQ&s=19",
12+
}
13+
)
14+
15+
func TestClient(t *testing.T) {
16+
client := NewCobaltWithAPI("http://localhost:9000/")
17+
for _, u := range urls {
18+
pURL, _ := url.Parse(u)
19+
t.Run(pURL.Host, func(t *testing.T) {
20+
media, err := client.Post(context.Background(), PostRequest{URL: u})
21+
if err != nil {
22+
t.Errorf("failed to fetch media for %s url with error: %v", u, err)
23+
}
24+
25+
if len(media.Filename) == 0 {
26+
t.Error("filename was empty")
27+
}
28+
29+
s, err := media.Stream(context.Background())
30+
if err != nil {
31+
t.Errorf("failed to stream media with error: %v", err)
32+
return
33+
}
34+
defer s.Close()
35+
})
36+
}
37+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
module github.com/andresperezl/gobalt
1+
module github.com/andresperezl/gobalt/v2
22

33
go 1.23.0

0 commit comments

Comments
 (0)