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 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
@@ -0,0 +1,3 @@
1
+ module ScreenshotCenter
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "screenshotcenter/version"
2
+ require_relative "screenshotcenter/errors"
3
+ require_relative "screenshotcenter/client"
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: []