screenshotcenter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +189 -0
- data/lib/screenshotcenter/client.rb +317 -0
- data/lib/screenshotcenter/errors.rb +38 -0
- data/lib/screenshotcenter/version.rb +3 -0
- data/lib/screenshotcenter.rb +3 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7ed56218652a0790a0d5881620b32144b126e7df27f10a3f4d77a41c6557c864
|
|
4
|
+
data.tar.gz: 3a297128e05c9c57377bfa5215689daa0ca96b416dc88426592a1ee244a2d4cd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 563aacf957e46b95c59bbbf6ec7d7003a15d7725c787a095ee7759585977f849525b906335115c42ab26821c6d2259c1472be7ed0bbfde94bbfffda191544b75
|
|
7
|
+
data.tar.gz: e8a040ff58b5bc2e7841494060a44bc5d941c9a3234c185b9637c9a0158720692d1a498a920ed2f8aedcd3a57b00c0325fe415a23988421b772d357e3b4551ee
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScreenshotCenter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# screenshotcenter
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [ScreenshotCenter](https://screenshotcenter.com) API.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby ≥ 2.5
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gem install screenshotcenter
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or in your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "screenshotcenter"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "screenshotcenter"
|
|
25
|
+
|
|
26
|
+
client = ScreenshotCenter::Client.new(ENV["SCREENSHOTCENTER_API_KEY"])
|
|
27
|
+
|
|
28
|
+
shot = client.screenshot.create("https://example.com")
|
|
29
|
+
result = client.wait_for(shot["id"])
|
|
30
|
+
puts result["url"] # final URL
|
|
31
|
+
puts result["status"] # "finished"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use cases
|
|
35
|
+
|
|
36
|
+
### Geo-targeting
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
shot = client.screenshot.create("https://example.com",
|
|
40
|
+
country: "fr", lang: "fr-FR", tz: "Europe/Paris")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### PDF
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
shot = client.screenshot.create("https://example.com", pdf: true)
|
|
47
|
+
done = client.wait_for(shot["id"])
|
|
48
|
+
client.screenshot.save_pdf(done["id"], "/tmp/page.pdf")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### HTML snapshot
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
shot = client.screenshot.create("https://example.com", html: true)
|
|
55
|
+
done = client.wait_for(shot["id"])
|
|
56
|
+
client.screenshot.save_html(done["id"], "/tmp/page.html")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Video
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
shot = client.screenshot.create("https://example.com",
|
|
63
|
+
video: true, video_length: 5)
|
|
64
|
+
done = client.wait_for(shot["id"])
|
|
65
|
+
client.screenshot.save_video(done["id"], "/tmp/page.webm")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Multiple shots
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
shot = client.screenshot.create("https://example.com", shots: 5)
|
|
72
|
+
done = client.wait_for(shot["id"])
|
|
73
|
+
client.screenshot.save_image(done["id"], "/tmp/shot3.png", shot: 3)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Save all artifacts
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
done = client.wait_for(shot["id"])
|
|
80
|
+
files = client.screenshot.save_all(done["id"], "/tmp/screenshots")
|
|
81
|
+
puts files[:image] # /tmp/screenshots/1001.png
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Batch
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Requires batch worker service to be running
|
|
88
|
+
urls = ["https://example.com", "https://example.org"]
|
|
89
|
+
batch = client.batch.create(urls, "us")
|
|
90
|
+
done = client.batch.wait_for(batch["id"], interval: 3, timeout: 120)
|
|
91
|
+
client.batch.save_zip(done["id"], "/tmp/batch.zip")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Credit balance
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
info = client.account.info
|
|
98
|
+
puts info["balance"]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Error handling
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
begin
|
|
105
|
+
result = client.wait_for(shot["id"], interval: 2, timeout: 60)
|
|
106
|
+
rescue ScreenshotCenter::ScreenshotFailedError => e
|
|
107
|
+
puts "Failed: #{e.reason}"
|
|
108
|
+
rescue ScreenshotCenter::TimeoutError => e
|
|
109
|
+
puts "Timed out after #{e.timeout_ms}ms"
|
|
110
|
+
rescue ScreenshotCenter::ApiError => e
|
|
111
|
+
puts "API error #{e.status}: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API reference
|
|
116
|
+
|
|
117
|
+
### `ScreenshotCenter::Client.new(api_key, base_url:, timeout:, http:)`
|
|
118
|
+
|
|
119
|
+
| Parameter | Default | Description |
|
|
120
|
+
|-----------|---------|-------------|
|
|
121
|
+
| `api_key` | — | Required |
|
|
122
|
+
| `base_url` | production | Override API base URL |
|
|
123
|
+
| `timeout` | 30 | HTTP timeout in seconds |
|
|
124
|
+
| `http` | built-in | Injectable HTTP transport for testing |
|
|
125
|
+
|
|
126
|
+
### `client.screenshot`
|
|
127
|
+
|
|
128
|
+
| Method | Description |
|
|
129
|
+
|--------|-------------|
|
|
130
|
+
| `create(url, **params)` | Create a screenshot |
|
|
131
|
+
| `info(id)` | Get screenshot metadata |
|
|
132
|
+
| `list(**params)` | List screenshots |
|
|
133
|
+
| `search(url, **params)` | Search by URL |
|
|
134
|
+
| `thumbnail(id, **params)` | Raw image bytes |
|
|
135
|
+
| `html(id)` | Raw HTML bytes |
|
|
136
|
+
| `pdf(id)` | Raw PDF bytes |
|
|
137
|
+
| `video(id)` | Raw video bytes |
|
|
138
|
+
| `delete(id, data:)` | Delete a screenshot |
|
|
139
|
+
| `save_image(id, path, **params)` | Save image to disk |
|
|
140
|
+
| `save_html(id, path)` | Save HTML to disk |
|
|
141
|
+
| `save_pdf(id, path)` | Save PDF to disk |
|
|
142
|
+
| `save_video(id, path)` | Save video to disk |
|
|
143
|
+
| `save_all(id, directory, basename:)` | Save all artifacts |
|
|
144
|
+
|
|
145
|
+
### `client.batch`
|
|
146
|
+
|
|
147
|
+
| Method | Description |
|
|
148
|
+
|--------|-------------|
|
|
149
|
+
| `create(urls, country, **params)` | Create a batch |
|
|
150
|
+
| `info(id)` | Get batch status |
|
|
151
|
+
| `list(**params)` | List batches |
|
|
152
|
+
| `download(id)` | Download ZIP bytes |
|
|
153
|
+
| `save_zip(id, path)` | Save ZIP to disk |
|
|
154
|
+
| `wait_for(id, interval:, timeout:)` | Poll until done |
|
|
155
|
+
|
|
156
|
+
### `client.account`
|
|
157
|
+
|
|
158
|
+
| Method | Description |
|
|
159
|
+
|--------|-------------|
|
|
160
|
+
| `info` | Get account info (balance, plan) |
|
|
161
|
+
|
|
162
|
+
### `client.wait_for(id, interval: 2.0, timeout: 120.0)`
|
|
163
|
+
|
|
164
|
+
Poll a screenshot until `finished` or `error`.
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
### Environment variables
|
|
169
|
+
|
|
170
|
+
| Variable | Description |
|
|
171
|
+
|----------|-------------|
|
|
172
|
+
| `SCREENSHOTCENTER_API_KEY` | Required for integration tests |
|
|
173
|
+
| `SCREENSHOTCENTER_BASE_URL` | Override base URL (default: production) |
|
|
174
|
+
|
|
175
|
+
### Running tests
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Unit tests only — no credentials needed
|
|
179
|
+
ruby spec/client_spec.rb
|
|
180
|
+
|
|
181
|
+
# Integration tests against a local instance
|
|
182
|
+
SCREENSHOTCENTER_API_KEY=your_key \
|
|
183
|
+
SCREENSHOTCENTER_BASE_URL=http://localhost:3000/api/v1 \
|
|
184
|
+
ruby spec/integration_spec.rb
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module ScreenshotCenter
|
|
8
|
+
# ScreenshotCenter API client.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# client = ScreenshotCenter::Client.new(ENV["SCREENSHOTCENTER_API_KEY"])
|
|
12
|
+
# shot = client.screenshot.create("https://example.com")
|
|
13
|
+
# result = client.wait_for(shot["id"])
|
|
14
|
+
# puts result["url"]
|
|
15
|
+
class Client
|
|
16
|
+
DEFAULT_BASE_URL = "https://api.screenshotcenter.com/api/v1"
|
|
17
|
+
|
|
18
|
+
attr_reader :screenshot, :batch, :account
|
|
19
|
+
|
|
20
|
+
# @param api_key [String] Your ScreenshotCenter API key (required).
|
|
21
|
+
# @param base_url [String] Override the base URL (optional).
|
|
22
|
+
# @param timeout [Integer] HTTP read/open timeout in seconds (default 30).
|
|
23
|
+
# @param http [#call] Injectable HTTP transport for testing.
|
|
24
|
+
# Signature: http.call(method, uri, body, content_type) -> { status:, body:, content_type: }
|
|
25
|
+
def initialize(api_key, base_url: DEFAULT_BASE_URL, timeout: 30, http: nil)
|
|
26
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
|
27
|
+
|
|
28
|
+
@api_key = api_key
|
|
29
|
+
@base_url = base_url.chomp("/")
|
|
30
|
+
@timeout = timeout
|
|
31
|
+
@http = http || method(:default_http)
|
|
32
|
+
|
|
33
|
+
@screenshot = ScreenshotNamespace.new(self)
|
|
34
|
+
@batch = BatchNamespace.new(self)
|
|
35
|
+
@account = AccountNamespace.new(self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Poll a screenshot until it reaches +finished+ or +error+.
|
|
39
|
+
# @param interval [Float] seconds between polls (default 2)
|
|
40
|
+
# @param timeout [Float] total seconds to wait (default 120)
|
|
41
|
+
def wait_for(id, interval: 2.0, timeout: 120.0)
|
|
42
|
+
deadline = Time.now + timeout
|
|
43
|
+
loop do
|
|
44
|
+
s = screenshot.info(id)
|
|
45
|
+
return s if s["status"] == "finished"
|
|
46
|
+
raise ScreenshotFailedError.new(id, s["error"]) if s["status"] == "error"
|
|
47
|
+
raise TimeoutError.new(id, (timeout * 1000).to_i) if Time.now + interval > deadline
|
|
48
|
+
|
|
49
|
+
sleep interval
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ── Internal helpers (used by namespaces) ─────────────────────────────────
|
|
54
|
+
|
|
55
|
+
# @!visibility private
|
|
56
|
+
def _get(endpoint, params = {})
|
|
57
|
+
resp = @http.call("GET", build_url(endpoint, params), nil, nil)
|
|
58
|
+
parse_response(resp)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @!visibility private
|
|
62
|
+
def _get_bytes(endpoint, params = {})
|
|
63
|
+
resp = @http.call("GET", build_url(endpoint, params), nil, nil)
|
|
64
|
+
raise_if_error(resp)
|
|
65
|
+
resp[:body]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @!visibility private
|
|
69
|
+
def _post(endpoint, body, content_type, params = {})
|
|
70
|
+
resp = @http.call("POST", build_url(endpoint, params), body, content_type)
|
|
71
|
+
parse_response(resp)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def build_url(endpoint, params)
|
|
77
|
+
merged = { key: @api_key }.merge(params)
|
|
78
|
+
parts = []
|
|
79
|
+
merged.each do |k, v|
|
|
80
|
+
next if v.nil?
|
|
81
|
+
if v.is_a?(Array)
|
|
82
|
+
if v.any? { |item| item.is_a?(Hash) }
|
|
83
|
+
parts << [k.to_s, JSON.generate(v)]
|
|
84
|
+
else
|
|
85
|
+
v.each { |item| parts << [k.to_s, item.to_s] }
|
|
86
|
+
end
|
|
87
|
+
elsif v.is_a?(Hash)
|
|
88
|
+
parts << [k.to_s, JSON.generate(v)]
|
|
89
|
+
else
|
|
90
|
+
parts << [k.to_s, v.to_s]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
query = URI.encode_www_form(parts)
|
|
94
|
+
"#{@base_url}#{endpoint}?#{query}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_response(resp)
|
|
98
|
+
raise_if_error(resp)
|
|
99
|
+
data = JSON.parse(resp[:body])
|
|
100
|
+
if data.key?("success")
|
|
101
|
+
raise ApiError.new(data["error"] || "API error", status: resp[:status], code: data["code"], fields: data["fields"]) unless data["success"]
|
|
102
|
+
return data["data"]
|
|
103
|
+
end
|
|
104
|
+
data
|
|
105
|
+
rescue JSON::ParserError
|
|
106
|
+
raise Error, "Invalid JSON response"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def raise_if_error(resp)
|
|
110
|
+
return if resp[:status] >= 200 && resp[:status] < 300
|
|
111
|
+
|
|
112
|
+
data = JSON.parse(resp[:body]) rescue {}
|
|
113
|
+
raise ApiError.new(
|
|
114
|
+
data["error"] || "HTTP #{resp[:status]}",
|
|
115
|
+
status: resp[:status],
|
|
116
|
+
code: data["code"],
|
|
117
|
+
fields: data["fields"]
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def default_http(method, url, body, content_type)
|
|
122
|
+
uri = URI.parse(url)
|
|
123
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
124
|
+
http.use_ssl = uri.scheme == "https"
|
|
125
|
+
http.open_timeout = @timeout
|
|
126
|
+
http.read_timeout = @timeout
|
|
127
|
+
|
|
128
|
+
request = case method
|
|
129
|
+
when "GET" then Net::HTTP::Get.new(uri.request_uri)
|
|
130
|
+
when "POST" then Net::HTTP::Post.new(uri.request_uri)
|
|
131
|
+
end
|
|
132
|
+
if body
|
|
133
|
+
request.body = body
|
|
134
|
+
request["Content-Type"] = content_type if content_type
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
response = http.request(request)
|
|
138
|
+
{ status: response.code.to_i, body: response.body.to_s, content_type: response["Content-Type"].to_s }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ── Namespaces ───────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
class ScreenshotNamespace
|
|
145
|
+
def initialize(client)
|
|
146
|
+
@client = client
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def create(url, **params)
|
|
150
|
+
raise ArgumentError, '"url" is required' if url.nil? || url.empty?
|
|
151
|
+
|
|
152
|
+
@client._get("/screenshot/create", { url: url }.merge(params))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def info(id)
|
|
156
|
+
@client._get("/screenshot/info", { id: id })
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def list(**params)
|
|
160
|
+
@client._get("/screenshot/list", params)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def search(url, **params)
|
|
164
|
+
raise ArgumentError, '"url" is required' if url.nil? || url.empty?
|
|
165
|
+
|
|
166
|
+
@client._get("/screenshot/search", { url: url }.merge(params))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def thumbnail(id, **params)
|
|
170
|
+
@client._get_bytes("/screenshot/thumbnail", { id: id }.merge(params))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def html(id)
|
|
174
|
+
@client._get_bytes("/screenshot/html", { id: id })
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def pdf(id)
|
|
178
|
+
@client._get_bytes("/screenshot/pdf", { id: id })
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def video(id)
|
|
182
|
+
@client._get_bytes("/screenshot/video", { id: id })
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def delete(id, data: "all")
|
|
186
|
+
@client._get("/screenshot/delete", { id: id, data: data })
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ── File-save helpers ──────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
def save_image(id, path, **params)
|
|
192
|
+
write_file(path, thumbnail(id, **params))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def save_pdf(id, path)
|
|
196
|
+
write_file(path, pdf(id))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def save_html(id, path)
|
|
200
|
+
write_file(path, html(id))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def save_video(id, path)
|
|
204
|
+
write_file(path, video(id))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def save_all(id, directory, basename: nil)
|
|
208
|
+
s = info(id)
|
|
209
|
+
stem = basename || id.to_s
|
|
210
|
+
dir = directory.chomp("/")
|
|
211
|
+
FileUtils.mkdir_p(dir)
|
|
212
|
+
saved = { image: nil, html: nil, pdf: nil, video: nil }
|
|
213
|
+
|
|
214
|
+
if s["status"] == "finished"
|
|
215
|
+
p = "#{dir}/#{stem}.png"
|
|
216
|
+
save_image(id, p)
|
|
217
|
+
saved[:image] = p
|
|
218
|
+
end
|
|
219
|
+
if s["has_html"]
|
|
220
|
+
p = "#{dir}/#{stem}.html"
|
|
221
|
+
save_html(id, p)
|
|
222
|
+
saved[:html] = p
|
|
223
|
+
end
|
|
224
|
+
if s["has_pdf"]
|
|
225
|
+
p = "#{dir}/#{stem}.pdf"
|
|
226
|
+
save_pdf(id, p)
|
|
227
|
+
saved[:pdf] = p
|
|
228
|
+
end
|
|
229
|
+
if s["has_video"]
|
|
230
|
+
ext = s["video_format"] || "webm"
|
|
231
|
+
p = "#{dir}/#{stem}.#{ext}"
|
|
232
|
+
save_video(id, p)
|
|
233
|
+
saved[:video] = p
|
|
234
|
+
end
|
|
235
|
+
saved
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def write_file(path, data)
|
|
241
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
242
|
+
File.binwrite(path, data)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
class BatchNamespace
|
|
247
|
+
def initialize(client)
|
|
248
|
+
@client = client
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @param urls [String, Array<String>] newline-separated string or array
|
|
252
|
+
# @param country [String] required
|
|
253
|
+
def create(urls, country, **params)
|
|
254
|
+
raise ArgumentError, '"country" is required' if country.nil? || country.empty?
|
|
255
|
+
|
|
256
|
+
content = urls.is_a?(Array) ? urls.join("\n") : urls.to_s
|
|
257
|
+
body, content_type = build_multipart({ country: country }.merge(params), content)
|
|
258
|
+
@client._post("/batch/create", body, content_type)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def info(id)
|
|
262
|
+
@client._get("/batch/info", { id: id })
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def list(**params)
|
|
266
|
+
@client._get("/batch/list", params)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def download(id)
|
|
270
|
+
@client._get_bytes("/batch/download", { id: id })
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def save_zip(id, path)
|
|
274
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
275
|
+
File.binwrite(path, download(id))
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def wait_for(id, interval: 2.0, timeout: 120.0)
|
|
279
|
+
deadline = Time.now + timeout
|
|
280
|
+
loop do
|
|
281
|
+
b = info(id)
|
|
282
|
+
return b if %w[finished error].include?(b["status"])
|
|
283
|
+
raise TimeoutError.new(id, (timeout * 1000).to_i) if Time.now + interval > deadline
|
|
284
|
+
|
|
285
|
+
sleep interval
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def build_multipart(fields, file_content)
|
|
292
|
+
boundary = "ScBoundary#{Time.now.to_i}"
|
|
293
|
+
body = ""
|
|
294
|
+
fields.each do |name, value|
|
|
295
|
+
body += "--#{boundary}\r\n"
|
|
296
|
+
body += "Content-Disposition: form-data; name=\"#{name}\"\r\n\r\n"
|
|
297
|
+
body += "#{value}\r\n"
|
|
298
|
+
end
|
|
299
|
+
body += "--#{boundary}\r\n"
|
|
300
|
+
body += "Content-Disposition: form-data; name=\"file\"; filename=\"urls.txt\"\r\n"
|
|
301
|
+
body += "Content-Type: text/plain\r\n\r\n"
|
|
302
|
+
body += "#{file_content}\r\n"
|
|
303
|
+
body += "--#{boundary}--\r\n"
|
|
304
|
+
[body, "multipart/form-data; boundary=#{boundary}"]
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
class AccountNamespace
|
|
309
|
+
def initialize(client)
|
|
310
|
+
@client = client
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def info
|
|
314
|
+
@client._get("/account/info")
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module ScreenshotCenter
|
|
2
|
+
# Base error class for all ScreenshotCenter exceptions.
|
|
3
|
+
class Error < StandardError; end
|
|
4
|
+
|
|
5
|
+
# Raised when the API returns a non-2xx status or success: false.
|
|
6
|
+
class ApiError < Error
|
|
7
|
+
attr_reader :status, :code, :fields
|
|
8
|
+
|
|
9
|
+
def initialize(message, status:, code: nil, fields: {})
|
|
10
|
+
super(message)
|
|
11
|
+
@status = status
|
|
12
|
+
@code = code
|
|
13
|
+
@fields = fields || {}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised by +wait_for+ when polling exceeds the timeout.
|
|
18
|
+
class TimeoutError < Error
|
|
19
|
+
attr_reader :screenshot_id, :timeout_ms
|
|
20
|
+
|
|
21
|
+
def initialize(screenshot_id, timeout_ms)
|
|
22
|
+
super("Screenshot #{screenshot_id} did not complete within #{timeout_ms}ms")
|
|
23
|
+
@screenshot_id = screenshot_id
|
|
24
|
+
@timeout_ms = timeout_ms
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Raised by +wait_for+ when the screenshot status is "error".
|
|
29
|
+
class ScreenshotFailedError < Error
|
|
30
|
+
attr_reader :screenshot_id, :reason
|
|
31
|
+
|
|
32
|
+
def initialize(screenshot_id, reason = nil)
|
|
33
|
+
super("Screenshot #{screenshot_id} failed: #{reason || 'unknown error'}")
|
|
34
|
+
@screenshot_id = screenshot_id
|
|
35
|
+
@reason = reason
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: screenshotcenter
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ScreenshotCenter
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 5.15.0
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 5.15.0
|
|
27
|
+
description: Capture web screenshots, PDFs, HTML snapshots, and videos at scale.
|
|
28
|
+
email:
|
|
29
|
+
- support@screenshotcenter.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/screenshotcenter.rb
|
|
37
|
+
- lib/screenshotcenter/client.rb
|
|
38
|
+
- lib/screenshotcenter/errors.rb
|
|
39
|
+
- lib/screenshotcenter/version.rb
|
|
40
|
+
homepage: https://screenshotcenter.com
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata: {}
|
|
44
|
+
post_install_message:
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.5'
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubyforge_project:
|
|
60
|
+
rubygems_version: 2.7.6.3
|
|
61
|
+
signing_key:
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Ruby SDK for the ScreenshotCenter API
|
|
64
|
+
test_files: []
|