scrape_creators 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36ed3616d4e59159e3241d2a07b743ad39aa63ac4adf93e63c7f51fbd0e8f173
4
- data.tar.gz: 946bcdd67f4f3e9d5b53819c5c15ace2a56a148871ed58844479349507d8ac09
3
+ metadata.gz: 81c09defede693eaa2d2a5096d7d9be96179e236e48ff2f49c4bce865be49773
4
+ data.tar.gz: d1978cf6dc4d1d3feaf1fbdb128be6aca6ed45d7269aa1b6991615b25207d509
5
5
  SHA512:
6
- metadata.gz: 27340bf2224cccfc9c5b10041e844e2f202765f34faa5d632f9fd04e7e00b1ea22ccbdcba49ee367af7fad54200618fc108c9ae9b202ac14df93713892c34574
7
- data.tar.gz: 8b4fa602579a360b37af6f80847fe923fa3afb6fb165af8862afd2a81d796ebec4bff0068c2ba2c2781b71d26314669b04ddc4d0d29065fc3068b0e424d9d09a
6
+ metadata.gz: 2db5b1d62794c71c70292bb4225e651ae84cd7558adcb8bb7db4be79b66324096defc3274f9d69c9db4fc86d6299a8b74d16c139bc125c47bca7c78a75f8dca4
7
+ data.tar.gz: ece660c23bb218af965016f85ca918b08c964939ecb3012ac8fb0a7b23bccf7383631975812d35230bef3170139442fd2f25be05b7a4f020c044f5a4cfb1cb97
data/README.md CHANGED
@@ -7,7 +7,7 @@ A zero-dependency Ruby client for the [Scrape Creators API](https://docs.scrapec
7
7
  Add this line to your application's Gemfile:
8
8
 
9
9
  ```ruby
10
- gem 'scrape_creators', '0.6.0'
10
+ gem 'scrape_creators', '0.8.0'
11
11
  ```
12
12
 
13
13
  And then execute:
@@ -85,6 +85,54 @@ info = scraper.post("https://www.instagram.com/p/DKSMEpKRd6h/", download_media:
85
85
  comments = scraper.comments("https://www.instagram.com/p/DKSMEpKRd6h/")
86
86
  ```
87
87
 
88
+ ### Comment pagination
89
+
90
+ `comments` returns the first page by default. Pass `pages:` to collect several pages:
91
+
92
+ ```ruby
93
+ comments = scraper.comments(video_url, source: 'instagram', pages: 5)
94
+ ```
95
+
96
+ Use `comments_page` for an initial batch followed by a Load more button. It returns
97
+ `{ comments:, cursor:, has_more: }` for Instagram, TikTok, and YouTube:
98
+
99
+ ```ruby
100
+ page = scraper.comments_page(video_url, source: 'instagram')
101
+
102
+ # When the user asks for more:
103
+ if page[:has_more]
104
+ page = scraper.comments_page(video_url, source: 'instagram', cursor: page[:cursor])
105
+ end
106
+ ```
107
+
108
+ Pass the returned cursor unchanged. The client translates it to YouTube's
109
+ `continuationToken` parameter and uses `cursor` for Instagram and TikTok. TikTok's
110
+ `has_more` flag is respected; missing or unchanged cursors stop pagination.
111
+ Multi-page collection also stops if a cursor repeats. Failed API responses raise
112
+ `ScrapeCreators::APIError` so callers can retry without losing their place.
113
+
114
+ API documentation: [Instagram](https://docs.scrapecreators.com/v2/instagram/post/comments/),
115
+ [TikTok](https://docs.scrapecreators.com/v1/tiktok/video/comments/),
116
+ [YouTube](https://docs.scrapecreators.com/v1/youtube/video/comments/).
117
+
88
118
  ## License
89
119
 
90
120
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
121
+
122
+ ## Account search
123
+
124
+ ```ruby
125
+ client.search_users("sean walker", source: "instagram")
126
+ client.search_users("sean walker", source: "tiktok")
127
+ client.search_users("sean walker", source: "youtube")
128
+ # => [{ handle: "seanwalker", name: "Sean Walker", avatar_url: "https://..." }]
129
+ ```
130
+
131
+ Returns one page of accounts, deduplicated by handle. YouTube searches channels,
132
+ whose handles can be passed to `posts_page` to fetch Shorts. Results without a
133
+ usable handle are omitted. Blank queries and unsupported sources raise
134
+ `ArgumentError`; unsuccessful API responses raise `ScrapeCreators::APIError`.
135
+
136
+ Endpoints: [Instagram](https://docs.scrapecreators.com/v1/instagram/search/),
137
+ [TikTok](https://docs.scrapecreators.com/v1/tiktok/search/users/),
138
+ [YouTube](https://docs.scrapecreators.com/v1/youtube/search/).
@@ -55,6 +55,38 @@ module ScrapeCreators
55
55
  end
56
56
  end
57
57
 
58
+ def search_users(query, source: INSTAGRAM)
59
+ query = query.to_s.strip
60
+ raise ArgumentError, 'Search query is required' if query.empty?
61
+
62
+ users = case source.to_s
63
+ when INSTAGRAM
64
+ response = get('/v1/instagram/search', query: query)
65
+ search_results(response).dig('data', 'users').to_a.map do |user|
66
+ { handle: user['username'], name: user['full_name'], avatar_url: user['profile_pic_url'] }
67
+ end
68
+ when TIKTOK
69
+ response = get('/v1/tiktok/search/users', query: query, trim: true)
70
+ search_results(response).fetch('users', []).map do |user|
71
+ { handle: user['unique_id'], name: user['nickname'], avatar_url: user.dig('avatar_medium', 'url_list', 0) }
72
+ end
73
+ when YOUTUBE
74
+ response = get('/v1/youtube/search', query: query, type: 'channels')
75
+ search_results(response).fetch('channels', []).map do |channel|
76
+ { handle: channel['handle'], name: channel['title'], avatar_url: channel['thumbnail'] }
77
+ end
78
+ else
79
+ raise ArgumentError, "Unsupported search source: #{source}"
80
+ end
81
+
82
+ users.filter_map do |user|
83
+ handle = user[:handle].to_s.strip.delete_prefix('@')
84
+ next if handle.empty? || handle.match?(%r{[[:space:]/]})
85
+
86
+ user.merge(handle: handle)
87
+ end.uniq { |user| user[:handle].downcase }
88
+ end
89
+
58
90
  def posts(handle, options = {})
59
91
  source = (options[:source] || 'instagram').to_s
60
92
  pages = options[:pages] || options[:max_pages] || 1
@@ -105,12 +137,33 @@ module ScrapeCreators
105
137
  end
106
138
 
107
139
  def comments(url_or_code, options = {})
108
- url = normalize_url(url_or_code)
109
- params = { url: url }.merge(options)
110
- source = options.dig(:source) || 'instagram'
111
- comments_url = comments_url_for(source:)
112
- res = get(comments_url, params)
113
- res.is_a?(Hash) ? (res["comments"] || []) : []
140
+ pages = options[:pages] || options[:max_pages] || 1
141
+ cursor = options[:cursor]
142
+ seen_cursors = {}
143
+ all_comments = []
144
+
145
+ pages.to_i.times do
146
+ break if seen_cursors[cursor.to_s]
147
+
148
+ seen_cursors[cursor.to_s] = true
149
+ page = comments_page(url_or_code, options.merge(cursor: cursor))
150
+ all_comments.concat(page[:comments])
151
+ break unless page[:has_more]
152
+
153
+ cursor = page[:cursor]
154
+ end
155
+
156
+ all_comments
157
+ end
158
+
159
+ def comments_page(url_or_code, options = {})
160
+ source = (options[:source] || INSTAGRAM).to_s
161
+ raise ArgumentError, "Unsupported comments source: #{source}" unless [INSTAGRAM, TIKTOK, YOUTUBE].include?(source)
162
+
163
+ cursor = options[:cursor] || options['cursor']
164
+ params = comments_params(url_or_code, source, cursor, options)
165
+ response = get(comments_url_for(source: source), params)
166
+ comments_result(response, source, cursor)
114
167
  end
115
168
 
116
169
  def get(endpoint, params = {}, options = {})
@@ -153,6 +206,37 @@ module ScrapeCreators
153
206
 
154
207
  private
155
208
 
209
+ def comments_cursor_key(source)
210
+ source == YOUTUBE ? 'continuationToken' : 'cursor'
211
+ end
212
+
213
+ def comments_params(url_or_code, source, cursor, options)
214
+ params = options.reject { |key, _| %w[source cursor pages max_pages].include?(key.to_s) }
215
+ params[:url] = normalize_url(url_or_code)
216
+ params[comments_cursor_key(source)] = cursor unless cursor.nil? || cursor.to_s.empty?
217
+ params
218
+ end
219
+
220
+ def comments_result(response, source, cursor)
221
+ unless response.is_a?(Hash) && response['success'] != false && response['comments'].is_a?(Array)
222
+ raise APIError, 'Unable to fetch comments'
223
+ end
224
+
225
+ next_cursor = response[comments_cursor_key(source)]&.to_s
226
+ next_cursor = nil if next_cursor.to_s.empty? || next_cursor == '0'
227
+ has_more = !next_cursor.nil? && next_cursor != cursor.to_s
228
+ has_more &&= [true, 1, '1'].include?(response['has_more']) if source == TIKTOK
229
+ { comments: response['comments'], cursor: next_cursor, has_more: has_more }
230
+ end
231
+
232
+ def search_results(response)
233
+ unless response.is_a?(Hash) && response['success'] != false
234
+ raise APIError, 'Account search failed'
235
+ end
236
+
237
+ response
238
+ end
239
+
156
240
  def posts_cursor_param_for(source:)
157
241
  case source.to_s
158
242
  when TIKTOK then 'max_cursor'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScrapeCreators
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scrape_creators
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - swlkr