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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hikerapi
4
+ VERSION = "0.1.0"
5
+ 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: []