hikerapi 0.1.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/README.md +111 -0
- data/lib/hikerapi/chunk_iterator.rb +56 -0
- data/lib/hikerapi/client.rb +131 -0
- data/lib/hikerapi/configuration.rb +14 -0
- data/lib/hikerapi/error.rb +17 -0
- data/lib/hikerapi/media.rb +78 -0
- data/lib/hikerapi/media_iterator.rb +17 -0
- data/lib/hikerapi/user.rb +104 -0
- data/lib/hikerapi/version.rb +5 -0
- data/lib/hikerapi.rb +30 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3984a16a595e5ac7953be0212d6b792ffe42f334cc699c8ac9f08eacd0f597bc
|
|
4
|
+
data.tar.gz: 8974f4b13520002f7c2cc71be6bad4f28a1aeff1c882102fd799c2fd338cf6c7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5000559541b7f8dc4fa2b33b3ff241b0ffab1e3db27e1564cad8ae1b5c2ba6004d18fb576058d7a54a47970a48f45550a1d44e88fa44a1d44826c00a32fba837
|
|
7
|
+
data.tar.gz: bd0397ee8c291519ded4002997299ee2e0b983b0f18137fa2f829ca9b210bd1764f26805fd70ca3384b679eb5f5b2324d0c52a9f414d9401335c2095c0f93e3e
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Hikerapi
|
|
2
|
+
|
|
3
|
+
A zero-dependency Ruby client for [HikerAPI](https://hikerapi.com/) - an Instagram Data API for developers.
|
|
4
|
+
It uses only Ruby standard library (`Net::HTTP`, `JSON`, `URI`, `Zlib`).
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add this line to your application's `Gemfile`:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'hikerapi', path: '../hikerapi' # Or git/gem repository reference
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
$ bundle install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
You can provide your HikerAPI key via an environment variable:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export HIKER_API_KEY='your_access_key_here'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or via a Rails initializer or configuration block:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require 'hikerapi'
|
|
32
|
+
|
|
33
|
+
Hikerapi.configure do |config|
|
|
34
|
+
config.api_key = 'your_access_key_here'
|
|
35
|
+
# config.api_base_url = 'https://api.hikerapi.com' # default
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
client = Hikerapi::Client.new
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or pass it when initializing the client:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
client = Hikerapi::Client.new(api_key: 'your_access_key_here')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### User Information
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
client = Hikerapi::Client.new
|
|
53
|
+
|
|
54
|
+
# Get user info by username
|
|
55
|
+
user = client.user.by_username('nike')
|
|
56
|
+
puts user['pk'] # User ID
|
|
57
|
+
puts user['biography']
|
|
58
|
+
puts user['external_url']
|
|
59
|
+
|
|
60
|
+
# Get user info by ID
|
|
61
|
+
user = client.user.by_id('1234567')
|
|
62
|
+
|
|
63
|
+
# Get user info by profile URL
|
|
64
|
+
user = client.user.by_url('https://www.instagram.com/nike/')
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### User Medias (Posts) & Cursor Iteration
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# Get user posts iterator (by user_id)
|
|
71
|
+
iterator = client.user.medias('787132')
|
|
72
|
+
|
|
73
|
+
# Get only the first chunk [ items, end_cursor ]:
|
|
74
|
+
items, end_cursor = client.user.medias('787132').each_chunk.first
|
|
75
|
+
# or:
|
|
76
|
+
items, end_cursor = client.user.medias_chunk('787132')
|
|
77
|
+
|
|
78
|
+
# Iterate chunk-by-chunk with end_cursor:
|
|
79
|
+
client.user.medias('787132').each_chunk do |chunk, end_cursor|
|
|
80
|
+
puts "Fetched #{chunk.size} posts. Next cursor: #{end_cursor}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Iterate item-by-item across all chunks automatically:
|
|
84
|
+
client.user.medias('787132').each do |post|
|
|
85
|
+
puts post['code']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Standard Enumerable methods work directly:
|
|
89
|
+
recent_5 = client.user.medias('787132').take(5)
|
|
90
|
+
|
|
91
|
+
# Fetch user clips / reels
|
|
92
|
+
reels = client.user.clips('nike')
|
|
93
|
+
# or using shortcut:
|
|
94
|
+
reels = client.reels('nike')
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Media Details & Comments
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Fetch comments for a post or reel (by shortcode, media ID, or URL)
|
|
101
|
+
comments = client.media.comments('DalGZj5uimd')
|
|
102
|
+
# or using shortcut:
|
|
103
|
+
comments = client.comments('https://www.instagram.com/p/DalGZj5uimd/')
|
|
104
|
+
|
|
105
|
+
# Fetch single media details by shortcode
|
|
106
|
+
media = client.media.by_code('DalGZj5uimd')
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hikerapi
|
|
4
|
+
class ChunkIterator
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
attr_reader :options, :fetch_proc
|
|
8
|
+
|
|
9
|
+
def initialize(options = {}, &fetch_proc)
|
|
10
|
+
@options = options.dup
|
|
11
|
+
@fetch_proc = fetch_proc
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fetch_chunk(cursor = options[:end_cursor] || options[:max_id])
|
|
15
|
+
fetch_proc.call(cursor)
|
|
16
|
+
end
|
|
17
|
+
alias page fetch_chunk
|
|
18
|
+
alias first_chunk fetch_chunk
|
|
19
|
+
|
|
20
|
+
def each
|
|
21
|
+
return to_enum(:each) unless block_given?
|
|
22
|
+
|
|
23
|
+
cursor = options[:end_cursor] || options[:max_id]
|
|
24
|
+
|
|
25
|
+
loop do
|
|
26
|
+
chunk, next_cursor = fetch_chunk(cursor)
|
|
27
|
+
break if chunk.nil? || chunk.empty?
|
|
28
|
+
|
|
29
|
+
chunk.each { |item| yield item }
|
|
30
|
+
|
|
31
|
+
break if next_cursor.nil? || next_cursor.to_s.empty?
|
|
32
|
+
break if next_cursor == cursor
|
|
33
|
+
|
|
34
|
+
cursor = next_cursor
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def each_chunk
|
|
39
|
+
return to_enum(:each_chunk) unless block_given?
|
|
40
|
+
|
|
41
|
+
cursor = options[:end_cursor] || options[:max_id]
|
|
42
|
+
|
|
43
|
+
loop do
|
|
44
|
+
chunk, next_cursor = fetch_chunk(cursor)
|
|
45
|
+
break if chunk.nil? || chunk.empty?
|
|
46
|
+
|
|
47
|
+
yield chunk
|
|
48
|
+
|
|
49
|
+
break if next_cursor.nil? || next_cursor.to_s.empty?
|
|
50
|
+
break if next_cursor == cursor
|
|
51
|
+
|
|
52
|
+
cursor = next_cursor
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'zlib'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
|
|
9
|
+
module Hikerapi
|
|
10
|
+
class Client
|
|
11
|
+
attr_reader :config, :api_key
|
|
12
|
+
|
|
13
|
+
def initialize(config = nil, api_key: nil)
|
|
14
|
+
@config = config || Hikerapi.configuration
|
|
15
|
+
@api_key = api_key || @config.api_key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def user
|
|
19
|
+
@user ||= User.new(self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def media
|
|
23
|
+
@media ||= Media.new(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def posts(username_or_id_or_url, options = {})
|
|
27
|
+
user.medias(username_or_id_or_url, options)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reels(username_or_id_or_url, options = {})
|
|
31
|
+
user.clips(username_or_id_or_url, options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def comments(media_id_or_code_or_url, options = {})
|
|
35
|
+
media.comments(media_id_or_code_or_url, options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get(endpoint, params = {}, options = {})
|
|
39
|
+
request(endpoint, method: :get, params: params, options: options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def post(endpoint, params = {}, options = {})
|
|
43
|
+
request(endpoint, method: :post, params: params, options: options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def request(endpoint, method: :get, params: {}, options: {})
|
|
47
|
+
current_key = options[:api_key] || api_key
|
|
48
|
+
if current_key.nil? || current_key.to_s.strip.empty?
|
|
49
|
+
raise ConfigurationError, "HikerAPI key is missing. Set ENV['HIKER_API_KEY'], configure via Hikerapi.configure { |c| c.api_key = '...' }, or pass api_key: to Client.new"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
clean_endpoint = endpoint.to_s.start_with?('/') ? endpoint.to_s : "/#{endpoint}"
|
|
53
|
+
base_url = options[:api_base_url] || config.api_base_url
|
|
54
|
+
url_str = "#{base_url}#{clean_endpoint}"
|
|
55
|
+
|
|
56
|
+
uri = URI.parse(url_str)
|
|
57
|
+
|
|
58
|
+
if method == :get
|
|
59
|
+
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
|
60
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
61
|
+
else
|
|
62
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
|
63
|
+
req['Content-Type'] = 'application/json'
|
|
64
|
+
req.body = JSON.generate(params) if params && !params.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
req['x-access-key'] = current_key
|
|
68
|
+
req['Accept'] = 'application/json'
|
|
69
|
+
req['Accept-Encoding'] = 'gzip, deflate'
|
|
70
|
+
|
|
71
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
72
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
73
|
+
http.open_timeout = options[:open_timeout] || config.open_timeout
|
|
74
|
+
http.read_timeout = options[:read_timeout] || config.read_timeout
|
|
75
|
+
|
|
76
|
+
max_retries = options[:max_retries] || 3
|
|
77
|
+
retries = 0
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
res = http.request(req)
|
|
81
|
+
parse_response(res)
|
|
82
|
+
rescue APIError => e
|
|
83
|
+
if e.status == 429 && retries < max_retries
|
|
84
|
+
retries += 1
|
|
85
|
+
delay = (res.respond_to?(:[]) && res['Retry-After']) ? res['Retry-After'].to_f : (1.2 * retries)
|
|
86
|
+
warn "HikerAPI 429 Rate Limited. Retrying in #{delay}s... (attempt #{retries}/#{max_retries})"
|
|
87
|
+
sleep(delay)
|
|
88
|
+
retry
|
|
89
|
+
end
|
|
90
|
+
raise e
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def parse_response(response)
|
|
97
|
+
body = response.body
|
|
98
|
+
|
|
99
|
+
if response['Content-Encoding'] == 'gzip' && body && !body.empty?
|
|
100
|
+
begin
|
|
101
|
+
body = Zlib::GzipReader.new(StringIO.new(body)).read
|
|
102
|
+
rescue Zlib::Error, Zlib::GzipFile::Error
|
|
103
|
+
# Decompression fallback
|
|
104
|
+
end
|
|
105
|
+
elsif response['Content-Encoding'] == 'deflate' && body && !body.empty?
|
|
106
|
+
begin
|
|
107
|
+
body = Zlib::Inflate.inflate(body)
|
|
108
|
+
rescue Zlib::Error
|
|
109
|
+
# Decompression fallback
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
parsed = begin
|
|
114
|
+
JSON.parse(body)
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
body
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
120
|
+
error_msg = if parsed.is_a?(Hash)
|
|
121
|
+
parsed['detail'] || parsed['message'] || parsed['error'] || response.message
|
|
122
|
+
else
|
|
123
|
+
response.message
|
|
124
|
+
end
|
|
125
|
+
raise APIError.new("HikerAPI Error (#{response.code}): #{error_msg}", status: response.code.to_i, response_body: body)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
parsed
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hikerapi
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_key, :api_base_url, :open_timeout, :read_timeout
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@api_key = ENV['HIKER_API_KEY']
|
|
9
|
+
@api_base_url = 'https://api.hikerapi.com'
|
|
10
|
+
@open_timeout = 15
|
|
11
|
+
@read_timeout = 60
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hikerapi
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class APIError < Error
|
|
9
|
+
attr_reader :status, :response_body
|
|
10
|
+
|
|
11
|
+
def initialize(message, status: nil, response_body: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@status = status
|
|
14
|
+
@response_body = response_body
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Hikerapi
|
|
6
|
+
class Media
|
|
7
|
+
attr_reader :client
|
|
8
|
+
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Fetch media details by shortcode
|
|
14
|
+
# @param code_or_url [String] Post shortcode or URL (e.g. "DalGZj5uimd" or "https://www.instagram.com/p/DalGZj5uimd/")
|
|
15
|
+
def by_code(code_or_url, options = {})
|
|
16
|
+
code = extract_shortcode(code_or_url)
|
|
17
|
+
client.get('/v1/media/by/code', { code: code }.merge(options))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Fetch media details by ID
|
|
21
|
+
# @param media_id [String, Integer] Media ID
|
|
22
|
+
def by_id(media_id, options = {})
|
|
23
|
+
client.get('/v1/media/by/id', { id: media_id }.merge(options))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Fetch media details by URL
|
|
27
|
+
# @param url [String] Post/reel URL
|
|
28
|
+
def by_url(url, options = {})
|
|
29
|
+
client.get('/v1/media/by/url', { url: url }.merge(options))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Fetch comments iterator for a media post or reel
|
|
33
|
+
# @param media_id_or_code_or_url [String, Integer] Media ID, shortcode, or post URL
|
|
34
|
+
# @param options [Hash] Additional options (e.g., max_id: '...')
|
|
35
|
+
def comments(media_id_or_code_or_url, options = {}, &block)
|
|
36
|
+
iterator = ChunkIterator.new(options) do |cursor|
|
|
37
|
+
opts = options.dup
|
|
38
|
+
opts[:max_id] = cursor if cursor && !cursor.to_s.empty?
|
|
39
|
+
comments_chunk(media_id_or_code_or_url, opts)
|
|
40
|
+
end
|
|
41
|
+
block_given? ? iterator.each(&block) : iterator
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Fetch a single chunk of comments for a media post or reel
|
|
45
|
+
# @param media_id_or_code_or_url [String, Integer] Media ID, shortcode, or post URL
|
|
46
|
+
# @param options [Hash] Additional options
|
|
47
|
+
def comments_chunk(media_id_or_code_or_url, options = {})
|
|
48
|
+
params = build_media_param(media_id_or_code_or_url).merge(options)
|
|
49
|
+
client.get('/v1/media/comments/chunk', params)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def extract_shortcode(input)
|
|
55
|
+
str = input.to_s.strip
|
|
56
|
+
return str if str.empty?
|
|
57
|
+
|
|
58
|
+
if str.include?('/p/') || str.include?('/reel/') || str.include?('/tv/')
|
|
59
|
+
match = str.match(%r{/(?:p|reel|tv)/([^/?#]+)})
|
|
60
|
+
return match[1] if match
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
str.split('/').reject(&:empty?).last.to_s.split('?').first
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_media_param(input)
|
|
67
|
+
str = input.to_s.strip
|
|
68
|
+
if str.match?(/\A\d+_\d+\z/) || str.match?(/\A\d+\z/)
|
|
69
|
+
{ media_id: str }
|
|
70
|
+
elsif str.start_with?('http://', 'https://') || str.length < 20
|
|
71
|
+
code = extract_shortcode(str)
|
|
72
|
+
{ code: code }
|
|
73
|
+
else
|
|
74
|
+
{ media_id: str }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hikerapi
|
|
4
|
+
class MediaIterator < ChunkIterator
|
|
5
|
+
attr_reader :user, :user_id
|
|
6
|
+
|
|
7
|
+
def initialize(user, user_id, options = {})
|
|
8
|
+
@user = user
|
|
9
|
+
@user_id = user_id.to_s
|
|
10
|
+
super(options) do |cursor|
|
|
11
|
+
opts = options.dup
|
|
12
|
+
opts[:end_cursor] = cursor if cursor && !cursor.to_s.empty?
|
|
13
|
+
user.medias_chunk(user_id, opts)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Hikerapi
|
|
6
|
+
class User
|
|
7
|
+
attr_reader :client
|
|
8
|
+
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Fetch user info by username
|
|
14
|
+
# @param username_or_url [String] Username, @handle, or Instagram profile URL
|
|
15
|
+
def by_username(username_or_url, options = {})
|
|
16
|
+
username = extract_username(username_or_url)
|
|
17
|
+
client.get('/v1/user/by/username', { username: username }.merge(options))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Fetch user info by ID
|
|
21
|
+
# @param user_id [String, Integer] User ID (pk)
|
|
22
|
+
def by_id(user_id, options = {})
|
|
23
|
+
client.get('/v1/user/by/id', { user_id: user_id }.merge(options))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Fetch user info by URL
|
|
27
|
+
# @param url [String] Instagram profile URL
|
|
28
|
+
def by_url(url, options = {})
|
|
29
|
+
client.get('/v1/user/by/url', { url: url }.merge(options))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Fetch user posts (medias) iterator
|
|
33
|
+
# @param user_id [String, Integer] User ID
|
|
34
|
+
# @param options [Hash] Additional options (e.g., end_cursor: '...')
|
|
35
|
+
def medias(user_id, options = {}, &block)
|
|
36
|
+
iterator = MediaIterator.new(self, user_id, options)
|
|
37
|
+
block_given? ? iterator.each(&block) : iterator
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Fetch a single chunk of user medias with cursor
|
|
41
|
+
# @param user_id [String, Integer] User ID
|
|
42
|
+
# @param options [Hash] Additional options (e.g., end_cursor: '...')
|
|
43
|
+
def medias_chunk(user_id, options = {})
|
|
44
|
+
params = { user_id: user_id.to_s }.merge(options)
|
|
45
|
+
params.delete(:end_cursor) if params[:end_cursor].nil? || params[:end_cursor].to_s.empty?
|
|
46
|
+
client.get('/v1/user/medias/chunk', params)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Fetch user clips / reels iterator
|
|
51
|
+
# @param user_id [String, Integer] User ID
|
|
52
|
+
# @param options [Hash] Additional options (e.g., end_cursor: '...')
|
|
53
|
+
def clips(user_id, options = {}, &block)
|
|
54
|
+
iterator = ChunkIterator.new(options) do |cursor|
|
|
55
|
+
opts = options.dup
|
|
56
|
+
opts[:end_cursor] = cursor if cursor && !cursor.to_s.empty?
|
|
57
|
+
clips_chunk(user_id, opts)
|
|
58
|
+
end
|
|
59
|
+
block_given? ? iterator.each(&block) : iterator
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Fetch single chunk of user clips / reels
|
|
63
|
+
# @param user_id [String, Integer] User ID
|
|
64
|
+
# @param options [Hash] Additional options (e.g., end_cursor: '...')
|
|
65
|
+
def clips_chunk(user_id, options = {})
|
|
66
|
+
params = { user_id: user_id.to_s }.merge(options)
|
|
67
|
+
params.delete(:end_cursor) if params[:end_cursor].nil? || params[:end_cursor].to_s.empty?
|
|
68
|
+
client.get('/v1/user/clips/chunk', params)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fetch user about info
|
|
72
|
+
# @param user_id [String, Integer] User ID
|
|
73
|
+
def about(user_id, options = {})
|
|
74
|
+
client.get('/v1/user/about', { user_id: user_id }.merge(options))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def extract_username(input)
|
|
80
|
+
str = input.to_s.strip
|
|
81
|
+
return str if str.empty?
|
|
82
|
+
|
|
83
|
+
if str.start_with?('http://', 'https://')
|
|
84
|
+
uri = URI.parse(str) rescue nil
|
|
85
|
+
if uri && uri.path
|
|
86
|
+
parts = uri.path.split('/').reject(&:empty?)
|
|
87
|
+
first_part = parts.first
|
|
88
|
+
return first_part.delete_prefix('@') if first_part && !%w[p reel tv].include?(first_part)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
str.split('/').reject(&:empty?).last.to_s.split('?').first.delete_prefix('@')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_user_param(input)
|
|
96
|
+
str = input.to_s.strip
|
|
97
|
+
if str.match?(/\A\d+\z/)
|
|
98
|
+
{ user_id: str }
|
|
99
|
+
else
|
|
100
|
+
{ username: extract_username(str) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/hikerapi.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hikerapi/version"
|
|
4
|
+
require_relative "hikerapi/error"
|
|
5
|
+
require_relative "hikerapi/configuration"
|
|
6
|
+
require_relative "hikerapi/chunk_iterator"
|
|
7
|
+
require_relative "hikerapi/media_iterator"
|
|
8
|
+
require_relative "hikerapi/user"
|
|
9
|
+
require_relative "hikerapi/media"
|
|
10
|
+
require_relative "hikerapi/client"
|
|
11
|
+
|
|
12
|
+
module Hikerapi
|
|
13
|
+
class << self
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configuration=(config)
|
|
19
|
+
@configuration = config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield(configuration)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset
|
|
27
|
+
@configuration = Configuration.new
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hikerapi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- swlkr
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Zero-dependency Ruby client for HikerAPI (https://hikerapi.com/) to fetch
|
|
13
|
+
Instagram user profile data, posts, reels, and comments.
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- README.md
|
|
19
|
+
- lib/hikerapi.rb
|
|
20
|
+
- lib/hikerapi/chunk_iterator.rb
|
|
21
|
+
- lib/hikerapi/client.rb
|
|
22
|
+
- lib/hikerapi/configuration.rb
|
|
23
|
+
- lib/hikerapi/error.rb
|
|
24
|
+
- lib/hikerapi/media.rb
|
|
25
|
+
- lib/hikerapi/media_iterator.rb
|
|
26
|
+
- lib/hikerapi/user.rb
|
|
27
|
+
- lib/hikerapi/version.rb
|
|
28
|
+
homepage: https://github.com/swlkr/hikerapi
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata: {}
|
|
32
|
+
rdoc_options: []
|
|
33
|
+
require_paths:
|
|
34
|
+
- lib
|
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 3.0.0
|
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '0'
|
|
45
|
+
requirements: []
|
|
46
|
+
rubygems_version: 3.6.9
|
|
47
|
+
specification_version: 4
|
|
48
|
+
summary: Ruby client for HikerAPI Instagram API
|
|
49
|
+
test_files: []
|