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 +4 -4
- data/README.md +49 -1
- data/lib/scrape_creators/client.rb +90 -6
- data/lib/scrape_creators/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81c09defede693eaa2d2a5096d7d9be96179e236e48ff2f49c4bce865be49773
|
|
4
|
+
data.tar.gz: d1978cf6dc4d1d3feaf1fbdb128be6aca6ed45d7269aa1b6991615b25207d509
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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'
|