scrape_creators 0.5.0 → 0.7.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: c214fa7c8b7b56a2483d3432420003745f73747e8391100312168466f9f03c7b
4
- data.tar.gz: 0ec20deecab206e3ba0f54707abbfee7e624485d5f7954311bdb34552b207f40
3
+ metadata.gz: 385c61c807a5dd2f2f1d7106b9430991d97ba68d5e894b6675d58d324243e1c3
4
+ data.tar.gz: 6293f02f4f4724cc2e6b1a11447e66c38ac2440db19f2b31b362a1a260757d81
5
5
  SHA512:
6
- metadata.gz: f15f5722980fca1ccb48a32e1d62927324ae944c81132cc3210f2a35d12209033ef244804c988a9f4b3924c28f78c9ddca63bf66e29dd0e24e9477195ad95aed
7
- data.tar.gz: 20aadfbd40d3362a8d6fbbe52223fb1bb4baafdaec99406e9a9bbe477e8ab1fd40b29bf7279fdfc813a88c3126237a5bd598bf7ccf2db5c0adb133e7c79d4c66
6
+ metadata.gz: 16846f8a031d0629aece90c2b59ae7f59026a4de1507f0e17a35d036b7c18efb185cdc619babf04bbbe1b81dc4b792ce186d576e58dc7cbd8bea8e33098e6d59
7
+ data.tar.gz: e7d80c9c3d21e7b1482776421c9f70ab6a33759ff3e8ec20d674e2b638dd146084917cb4966158b9b2451a8077ea72981f0500b767a9c8a333b407a58ea9f5c7
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.2.0'
10
+ gem 'scrape_creators', '0.6.0'
11
11
  ```
12
12
 
13
13
  And then execute:
@@ -40,6 +40,39 @@ scraper = ScrapeCreators::Client.new
40
40
  posts = scraper.posts("zuck")
41
41
  ```
42
42
 
43
+ ### Fetch TikTok Profile Videos
44
+
45
+ ```ruby
46
+ posts = scraper.posts("zuck", source: 'tiktok')
47
+ ```
48
+
49
+ ### Fetch YouTube Channel Shorts
50
+
51
+ ```ruby
52
+ posts = scraper.posts("zuck", source: 'youtube')
53
+ ```
54
+
55
+ ### Cursor-based pagination
56
+
57
+ All three platforms support cursor-based pagination. By default `posts` returns the first page only. Pass `pages:` to fetch more (e.g. `5` for the first five pages of a creator's feed):
58
+
59
+ ```ruby
60
+ posts = scraper.posts("zuck", pages: 5) # Instagram
61
+ posts = scraper.posts("zuck", source: 'tiktok', pages: 5) # TikTok
62
+ posts = scraper.posts("zuck", source: 'youtube', pages: 5) # YouTube Shorts
63
+ ```
64
+
65
+ If you need finer control, `posts_page` returns `{ items:, cursor:, has_more: }` so you can page through the full timeline yourself:
66
+
67
+ ```ruby
68
+ page = scraper.posts_page("zuck", source: 'tiktok')
69
+ loop do
70
+ page[:items].each { |item| puts item['desc'] }
71
+ break unless page[:has_more] && page[:cursor]
72
+ page = scraper.posts_page("zuck", source: 'tiktok', cursor: page[:cursor])
73
+ end
74
+ ```
75
+
43
76
  ### Fetch Instagram Post / Reel Info
44
77
 
45
78
  ```ruby
@@ -50,18 +83,26 @@ info = scraper.post("https://www.instagram.com/p/DKSMEpKRd6h/", download_media:
50
83
 
51
84
  ```ruby
52
85
  comments = scraper.comments("https://www.instagram.com/p/DKSMEpKRd6h/")
53
-
54
- page = scraper.comments_page("https://www.instagram.com/p/DKSMEpKRd6h/")
55
- page[:comments]
56
- page[:cursor]
57
- page[:has_more]
58
-
59
- next_page = scraper.comments_page(
60
- "https://www.instagram.com/p/DKSMEpKRd6h/",
61
- cursor: page[:cursor]
62
- )
63
86
  ```
64
87
 
65
88
  ## License
66
89
 
67
90
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
91
+
92
+ ## Account search
93
+
94
+ ```ruby
95
+ client.search_users("sean walker", source: "instagram")
96
+ client.search_users("sean walker", source: "tiktok")
97
+ client.search_users("sean walker", source: "youtube")
98
+ # => [{ handle: "seanwalker", name: "Sean Walker", avatar_url: "https://..." }]
99
+ ```
100
+
101
+ Returns one page of accounts, deduplicated by handle. YouTube searches channels,
102
+ whose handles can be passed to `posts_page` to fetch Shorts. Results without a
103
+ usable handle are omitted. Blank queries and unsupported sources raise
104
+ `ArgumentError`; unsuccessful API responses raise `ScrapeCreators::APIError`.
105
+
106
+ Endpoints: [Instagram](https://docs.scrapecreators.com/v1/instagram/search/),
107
+ [TikTok](https://docs.scrapecreators.com/v1/tiktok/search/users/),
108
+ [YouTube](https://docs.scrapecreators.com/v1/youtube/search/).
@@ -55,51 +55,96 @@ module ScrapeCreators
55
55
  end
56
56
  end
57
57
 
58
- def posts(handle, options = {})
59
- source = (options[:source] || 'instagram').to_s
60
- params = { handle: handle }.merge(options.reject { |k, _| k.to_s == 'source' })
61
- posts_url = posts_url_for(source: source)
62
- res = get(posts_url, params)
63
- key = posts_key_for(source: source)
64
- res.is_a?(Hash) ? (res[key] || []) : []
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 }
65
88
  end
66
89
 
67
- def post(url_or_code, options = {})
90
+ def posts(handle, options = {})
68
91
  source = (options[:source] || 'instagram').to_s
69
- url = normalize_url(url_or_code)
70
- post_url = post_url_for(source: source)
71
- params = { url: url }.merge(options.reject { |k, _| k.to_s == 'source' })
72
- get(post_url, params)
73
- end
92
+ pages = options[:pages] || options[:max_pages] || 1
93
+ all_items = []
94
+ cursor = nil
95
+ has_more = true
96
+
97
+ pages.to_i.times do
98
+ break unless has_more
99
+
100
+ page = posts_page(handle, options.merge(cursor: cursor))
101
+ items = Array(page[:items])
102
+ all_items.concat(items)
103
+ cursor = page[:cursor]
104
+ has_more = page[:has_more] && !cursor.to_s.empty? && !items.empty?
105
+ end
74
106
 
75
- def comments(url_or_code, options = {})
76
- comments_page(url_or_code, options)[:comments]
107
+ all_items
77
108
  end
78
109
 
79
- def comments_page(url_or_code, options = {})
110
+ def posts_page(handle, options = {})
80
111
  source = (options[:source] || 'instagram').to_s
81
- url = normalize_url(url_or_code)
82
- extra = options.reject { |k, _| %w[source cursor continuationToken continuation_token].include?(k.to_s) }
83
- params = { url: url }.merge(extra)
84
- cursor = options[:cursor] || options['cursor'] || options[:continuationToken] || options['continuationToken'] || options[:continuation_token]
112
+ params = { handle: handle }
113
+ cursor = options[:cursor] || options['cursor']
85
114
  if cursor && !cursor.to_s.empty?
86
- if source == YOUTUBE
87
- params[:continuationToken] = cursor
88
- else
89
- params[:cursor] = cursor
90
- end
115
+ params[posts_cursor_param_for(source:)] = cursor
91
116
  end
117
+ extra = options.reject { |k, _| %w[source cursor max_pages pages].include?(k.to_s) }
118
+ params.merge!(extra)
92
119
 
93
- res = get(comments_url_for(source: source), params)
94
- comments = res.is_a?(Hash) ? (res['comments'] || []) : []
95
- next_cursor = next_comments_cursor(res, source)
120
+ res = get(posts_url_for(source:), params)
121
+ key = posts_key_for(source:)
122
+ items = res.is_a?(Hash) ? (res[key] || []) : []
123
+ next_cursor = next_posts_cursor(res, source)
96
124
  {
97
- comments: comments,
125
+ items: items,
98
126
  cursor: next_cursor,
99
- has_more: comments_has_more?(res, source, next_cursor)
127
+ has_more: posts_has_more?(res, source, next_cursor)
100
128
  }
101
129
  end
102
130
 
131
+ def post(url_or_code, options = {})
132
+ url = normalize_url(url_or_code)
133
+ source = options.dig(:source) || 'instagram'
134
+ post_url = post_url_for(source:)
135
+ params = { url: url }.merge(options)
136
+ get(post_url, params)
137
+ end
138
+
139
+ def comments(url_or_code, options = {})
140
+ url = normalize_url(url_or_code)
141
+ params = { url: url }.merge(options)
142
+ source = options.dig(:source) || 'instagram'
143
+ comments_url = comments_url_for(source:)
144
+ res = get(comments_url, params)
145
+ res.is_a?(Hash) ? (res["comments"] || []) : []
146
+ end
147
+
103
148
  def get(endpoint, params = {}, options = {})
104
149
  request(endpoint, method: :get, params: params, options: options)
105
150
  end
@@ -140,23 +185,41 @@ module ScrapeCreators
140
185
 
141
186
  private
142
187
 
143
- def next_comments_cursor(res, source)
188
+ def search_results(response)
189
+ unless response.is_a?(Hash) && response['success'] != false
190
+ raise APIError, 'Account search failed'
191
+ end
192
+
193
+ response
194
+ end
195
+
196
+ def posts_cursor_param_for(source:)
197
+ case source.to_s
198
+ when TIKTOK then 'max_cursor'
199
+ when INSTAGRAM then 'next_max_id'
200
+ when YOUTUBE then 'continuationToken'
201
+ else 'cursor'
202
+ end
203
+ end
204
+
205
+ def next_posts_cursor(res, source)
144
206
  return nil unless res.is_a?(Hash)
145
207
 
146
- cursor = if source == YOUTUBE
147
- res['continuationToken']
148
- else
149
- res['cursor']
208
+ cursor = case source.to_s
209
+ when TIKTOK then res['max_cursor']
210
+ when INSTAGRAM then res['next_max_id']
211
+ when YOUTUBE then res['continuationToken'] || res['continuation_token']
212
+ else res['cursor']
150
213
  end
151
- return nil if cursor.nil? || cursor.to_s.empty?
214
+ return nil if cursor.nil? || cursor.to_s.empty? || cursor.to_s == '0'
152
215
 
153
216
  cursor
154
217
  end
155
218
 
156
- def comments_has_more?(res, source, next_cursor)
219
+ def posts_has_more?(res, source, next_cursor)
157
220
  return false unless res.is_a?(Hash)
158
221
 
159
- if source == TIKTOK
222
+ if source.to_s == TIKTOK
160
223
  res['has_more'].to_i == 1
161
224
  else
162
225
  !next_cursor.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScrapeCreators
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.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.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - swlkr