instagrammer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +159 -0
- data/Guardfile +9 -0
- data/LICENSE +20 -0
- data/README.md +100 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/instagrammer.gemspec +38 -0
- data/lib/instagrammer/config/capybara.rb +29 -0
- data/lib/instagrammer/post.rb +84 -0
- data/lib/instagrammer/user.rb +126 -0
- data/lib/instagrammer/version.rb +5 -0
- data/lib/instagrammer.rb +23 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '07960160cb3df5272aeb44b2beeb48706faf57e0f8e81f5ce60229d8212e51e9'
|
4
|
+
data.tar.gz: 61071d880c92b238f9139d553fd54c15f6afa7c11e1ff7779c01842e6e215859
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2ea4a9524f35b453290589e0dd9fc908e8c04867419f7173ead326a145973300e4a4cad83bc17c838e34c40dc59fc9512fd75f0790f7e2ff2cb25606305c07a9
|
7
|
+
data.tar.gz: cfbcada6d316b4bb67ba3ae4099a58e20455026b72e6b6e3d738f64408fb9ec26ffd17dff6b20f0e85c75d091ee9046992767d4e46c68bedb33a572cbbfa9950
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
sudo: false
|
3
|
+
|
4
|
+
language: ruby
|
5
|
+
rvm:
|
6
|
+
- 2.5
|
7
|
+
- 2.6
|
8
|
+
|
9
|
+
cache: bundler
|
10
|
+
bundler_args: --without development --jobs=3 --retry=3 --path=../vendor/bundle
|
11
|
+
|
12
|
+
before_install: gem install bundler -v 2.0.2 --no-document
|
13
|
+
script:
|
14
|
+
- bundle exec rake test
|
15
|
+
- rubocop
|
16
|
+
|
17
|
+
notifications:
|
18
|
+
email: false
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.1.0] - 2019-06-18
|
10
|
+
### Added
|
11
|
+
- Initial release
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
instagrammer (0.1.0)
|
5
|
+
capybara (~> 3.24)
|
6
|
+
webdrivers (~> 4.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
actionpack (5.2.3)
|
12
|
+
actionview (= 5.2.3)
|
13
|
+
activesupport (= 5.2.3)
|
14
|
+
rack (~> 2.0)
|
15
|
+
rack-test (>= 0.6.3)
|
16
|
+
rails-dom-testing (~> 2.0)
|
17
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
18
|
+
actionview (5.2.3)
|
19
|
+
activesupport (= 5.2.3)
|
20
|
+
builder (~> 3.1)
|
21
|
+
erubi (~> 1.4)
|
22
|
+
rails-dom-testing (~> 2.0)
|
23
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
24
|
+
activesupport (5.2.3)
|
25
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
26
|
+
i18n (>= 0.7, < 2)
|
27
|
+
minitest (~> 5.1)
|
28
|
+
tzinfo (~> 1.1)
|
29
|
+
addressable (2.6.0)
|
30
|
+
public_suffix (>= 2.0.2, < 4.0)
|
31
|
+
ast (2.4.0)
|
32
|
+
builder (3.2.3)
|
33
|
+
capybara (3.24.0)
|
34
|
+
addressable
|
35
|
+
mini_mime (>= 0.1.3)
|
36
|
+
nokogiri (~> 1.8)
|
37
|
+
rack (>= 1.6.0)
|
38
|
+
rack-test (>= 0.6.3)
|
39
|
+
regexp_parser (~> 1.5)
|
40
|
+
xpath (~> 3.2)
|
41
|
+
childprocess (1.0.1)
|
42
|
+
rake (< 13.0)
|
43
|
+
coderay (1.1.2)
|
44
|
+
concurrent-ruby (1.1.5)
|
45
|
+
crass (1.0.4)
|
46
|
+
erubi (1.8.0)
|
47
|
+
ffi (1.11.1)
|
48
|
+
formatador (0.2.5)
|
49
|
+
guard (2.15.0)
|
50
|
+
formatador (>= 0.2.4)
|
51
|
+
listen (>= 2.7, < 4.0)
|
52
|
+
lumberjack (>= 1.0.12, < 2.0)
|
53
|
+
nenv (~> 0.1)
|
54
|
+
notiffany (~> 0.0)
|
55
|
+
pry (>= 0.9.12)
|
56
|
+
shellany (~> 0.0)
|
57
|
+
thor (>= 0.18.1)
|
58
|
+
guard-compat (1.2.1)
|
59
|
+
guard-minitest (2.4.6)
|
60
|
+
guard-compat (~> 1.2)
|
61
|
+
minitest (>= 3.0)
|
62
|
+
i18n (1.6.0)
|
63
|
+
concurrent-ruby (~> 1.0)
|
64
|
+
jaro_winkler (1.5.3)
|
65
|
+
listen (3.1.5)
|
66
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
67
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
68
|
+
ruby_dep (~> 1.2)
|
69
|
+
loofah (2.2.3)
|
70
|
+
crass (~> 1.0.2)
|
71
|
+
nokogiri (>= 1.5.9)
|
72
|
+
lumberjack (1.0.13)
|
73
|
+
method_source (0.9.2)
|
74
|
+
mini_mime (1.0.1)
|
75
|
+
mini_portile2 (2.4.0)
|
76
|
+
minitest (5.11.3)
|
77
|
+
nenv (0.3.0)
|
78
|
+
nokogiri (1.10.3)
|
79
|
+
mini_portile2 (~> 2.4.0)
|
80
|
+
notiffany (0.1.1)
|
81
|
+
nenv (~> 0.1)
|
82
|
+
shellany (~> 0.0)
|
83
|
+
parallel (1.17.0)
|
84
|
+
parser (2.6.3.0)
|
85
|
+
ast (~> 2.4.0)
|
86
|
+
pry (0.12.2)
|
87
|
+
coderay (~> 1.1.0)
|
88
|
+
method_source (~> 0.9.0)
|
89
|
+
public_suffix (3.1.0)
|
90
|
+
rack (2.0.7)
|
91
|
+
rack-test (1.1.0)
|
92
|
+
rack (>= 1.0, < 3)
|
93
|
+
rails-dom-testing (2.0.3)
|
94
|
+
activesupport (>= 4.2.0)
|
95
|
+
nokogiri (>= 1.6)
|
96
|
+
rails-html-sanitizer (1.0.4)
|
97
|
+
loofah (~> 2.2, >= 2.2.2)
|
98
|
+
railties (5.2.3)
|
99
|
+
actionpack (= 5.2.3)
|
100
|
+
activesupport (= 5.2.3)
|
101
|
+
method_source
|
102
|
+
rake (>= 0.8.7)
|
103
|
+
thor (>= 0.19.0, < 2.0)
|
104
|
+
rainbow (3.0.0)
|
105
|
+
rake (10.5.0)
|
106
|
+
rb-fsevent (0.10.3)
|
107
|
+
rb-inotify (0.10.0)
|
108
|
+
ffi (~> 1.0)
|
109
|
+
regexp_parser (1.5.1)
|
110
|
+
rubocop (0.71.0)
|
111
|
+
jaro_winkler (~> 1.5.1)
|
112
|
+
parallel (~> 1.10)
|
113
|
+
parser (>= 2.6)
|
114
|
+
rainbow (>= 2.2.2, < 4.0)
|
115
|
+
ruby-progressbar (~> 1.7)
|
116
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
117
|
+
rubocop-performance (1.3.0)
|
118
|
+
rubocop (>= 0.68.0)
|
119
|
+
rubocop-rails (2.0.1)
|
120
|
+
rack (>= 1.1)
|
121
|
+
rubocop (>= 0.70.0)
|
122
|
+
rubocop-rails_config (0.6.2)
|
123
|
+
railties (>= 3.0)
|
124
|
+
rubocop (~> 0.70)
|
125
|
+
rubocop-performance (~> 1.3)
|
126
|
+
rubocop-rails (~> 2.0)
|
127
|
+
ruby-progressbar (1.10.1)
|
128
|
+
ruby_dep (1.5.0)
|
129
|
+
rubyzip (1.2.3)
|
130
|
+
selenium-webdriver (3.142.3)
|
131
|
+
childprocess (>= 0.5, < 2.0)
|
132
|
+
rubyzip (~> 1.2, >= 1.2.2)
|
133
|
+
shellany (0.0.1)
|
134
|
+
thor (0.20.3)
|
135
|
+
thread_safe (0.3.6)
|
136
|
+
tzinfo (1.2.5)
|
137
|
+
thread_safe (~> 0.1)
|
138
|
+
unicode-display_width (1.6.0)
|
139
|
+
webdrivers (4.0.1)
|
140
|
+
nokogiri (~> 1.6)
|
141
|
+
rubyzip (~> 1.0)
|
142
|
+
selenium-webdriver (>= 3.0, < 4.0)
|
143
|
+
xpath (3.2.0)
|
144
|
+
nokogiri (~> 1.8)
|
145
|
+
|
146
|
+
PLATFORMS
|
147
|
+
ruby
|
148
|
+
|
149
|
+
DEPENDENCIES
|
150
|
+
bundler (~> 2.0)
|
151
|
+
guard (~> 2.15)
|
152
|
+
guard-minitest (~> 2.4)
|
153
|
+
instagrammer!
|
154
|
+
minitest (~> 5.0)
|
155
|
+
rake (~> 10.0)
|
156
|
+
rubocop-rails_config
|
157
|
+
|
158
|
+
BUNDLED WITH
|
159
|
+
2.0.2
|
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Richard Venneman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Instagrammer
|
2
|
+
|
3
|
+
Instagrammer lets you fetch Instagram user info and posts. This is done by crawling the Instagram web interface, powered by [Capybara](https://github.com/teamcapybara/capybara/) and a headless Chrome Selenium driver. Read more about the [motivation to build this gem](#motivation)
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/richardvenneman/instagrammer.svg?branch=master)](https://travis-ci.org/richardvenneman/instagrammer)
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/instagrammer.svg)](https://badge.fury.io/rb/instagrammer)
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'instagrammer'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install instagrammer
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### User
|
27
|
+
|
28
|
+
Create a new user with `Instagrammer::User.new("username")` or simply `Instagrammer.new("username")`.
|
29
|
+
|
30
|
+
Accessing certain properties on an account that is private will result in a `PrivateAccount` exception. In some cases Instagram doesn't expose any meta data through. In these cases a `UserInvalid` exception will be raised when accessing certain properties.
|
31
|
+
|
32
|
+
Therefor you can check if the account is scrapable with the `#public?` instance method.
|
33
|
+
|
34
|
+
#### Metadata
|
35
|
+
|
36
|
+
The meta counts data is available for both public as well as private accounts:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
user = Instagrammer.new("richardvenneman")
|
40
|
+
user.follower_count # => "204"
|
41
|
+
user.following_count # => "141"
|
42
|
+
user.post_count # => "91"
|
43
|
+
```
|
44
|
+
|
45
|
+
#### Bio
|
46
|
+
|
47
|
+
Bio info is currently available for public accounts only:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
user = Instagrammer.new("richardvenneman")
|
51
|
+
user.name # => "Richard Venneman"
|
52
|
+
user.username # => "@richardvenneman"
|
53
|
+
user.avatar # => "https://www.instagram.com/static/images/ico/favicon-200.png/ab6eff..."
|
54
|
+
user.bio # => "👨🏻💻 Partner at GoNomadic B.V.\nTraveling and building 🏙 @cityspotters"
|
55
|
+
user.url # => "https://www.cityspotters.com/"
|
56
|
+
```
|
57
|
+
|
58
|
+
#### Posts
|
59
|
+
|
60
|
+
Get the posts for a specific user by using the `#get_posts(_limit_)` user method.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
user = Instagrammer.new("richardvenneman")
|
64
|
+
user.get_posts(3) # => [#<Instagrammer::Post:70223732051200..>, #<Instagrammer::Post:70223732051200..>, #<Instagrammer::Post:70223732051200..>]
|
65
|
+
```
|
66
|
+
|
67
|
+
See below for the available post methods
|
68
|
+
|
69
|
+
### Post
|
70
|
+
|
71
|
+
Create a new post with `Instagrammer::Post.new("shortcode")`.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
post = Instagrammer::Post.new("Bg3VjfwDRDw")
|
75
|
+
post.photo? # => true
|
76
|
+
post.caption # => "🌋 Mount Agung as seen from 🌋 Mount Batur just after sunrise 🌅"
|
77
|
+
post.upload_date # => #<DateTime: 2018-03-28T11:07:26+00:00 ((2458206j,40046s,0n),+0s,2299161j)
|
78
|
+
post.comment_count # => 3
|
79
|
+
post.like_count # => 52
|
80
|
+
post.image_url # => "https://instagram.foem1-1.fna.fbcdn.net/vp/04bffab7e91872110690173cbac1ba28/5D9FDCD0/t51.2885-15/e35/29416707_933709783459981_1377808440356765696_n.jpg?_nc_ht=instagram.foem1-1.fna.fbcdn.net"
|
81
|
+
post.image_urls # => [{:url=>"https://instagram.foem1-1.fna.fbcdn.net/vp/b962b338f5024309e3242ec3e4158681/5DA27835/t51.2885-15/sh0.08/e35/s640x640/29416707_933709783459981_1377808440356765696_n.jpg?_nc_ht=instagram.foem1-1.fna.fbcdn.net", :width=>640}, {:url=>",https://instagram.foem1-1.fna.fbcdn.net/vp/fb1477d8dc17c9d1a6b36c8107b4a5b2/5DC4FA35/t51.2885-15/sh0.08/e35/s750x750/29416707_933709783459981_1377808440356765696_n.jpg?_nc_ht=instagram.foem1-1.fna.fbcdn.net", :width=>750}, {:url=>",https://instagram.foem1-1.fna.fbcdn.net/vp/04bffab7e91872110690173cbac1ba28/5D9FDCD0/t51.2885-15/e35/29416707_933709783459981_1377808440356765696_n.jpg?_nc_ht=instagram.foem1-1.fna.fbcdn.net", :width=>1080}]
|
82
|
+
```
|
83
|
+
|
84
|
+
Additionally video posts are somewhat supported as well. Image URLs and like counts are not available for videos.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
post = Instagrammer::Post.new("Byx0Nd3A3qr")
|
88
|
+
post.video? # => true
|
89
|
+
post.watch_count # => 8035142
|
90
|
+
```
|
91
|
+
|
92
|
+
## Motivation
|
93
|
+
|
94
|
+
The problem with scrapers is that they always brake. Especially Instagram/Facebook seems to put in a lot of effort into this. This gem tries to approach that challenge a bit different than other Ruby Instagram scrapers. Decent test coverage should test the integration continuously and good code quality should allow for quick and easy updates may any changes in the Instagram web interface happen.
|
95
|
+
|
96
|
+
The main focus is currently retrieving user posts with some metadata while maintaining a stable implementation. Therefor I try to avoid naive page selectors and rely on meta data where possible.
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/richardvenneman/instagrammer.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "instagrammer"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "instagrammer/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "instagrammer"
|
9
|
+
spec.version = Instagrammer::VERSION
|
10
|
+
spec.authors = ["Richard Venneman"]
|
11
|
+
spec.email = ["richardvenneman@me.com"]
|
12
|
+
|
13
|
+
spec.summary = "Instagrammer lets you fetch Instagram user info and posts"
|
14
|
+
spec.homepage = "https://github.com/richardvenneman/instagrammer"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/richardvenneman/instagrammer"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/richardvenneman/instagrammer/blob/master/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_dependency "capybara", "~> 3.24"
|
30
|
+
spec.add_dependency "webdrivers", "~> 4.0"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
33
|
+
spec.add_development_dependency "guard", "~> 2.15"
|
34
|
+
spec.add_development_dependency "guard-minitest", "~> 2.4"
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
37
|
+
spec.add_development_dependency "rubocop-rails_config"
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Capybara.register_driver :headless_chrome do |app|
|
4
|
+
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
5
|
+
chromeOptions: { args: %w(headless no-sandbox disable-gpu window-size=1400,1400) }
|
6
|
+
)
|
7
|
+
|
8
|
+
Capybara::Selenium::Driver.new app,
|
9
|
+
browser: :chrome,
|
10
|
+
desired_capabilities: capabilities
|
11
|
+
end
|
12
|
+
|
13
|
+
Capybara.default_driver = :headless_chrome
|
14
|
+
|
15
|
+
Capybara.add_selector(:meta_description) do
|
16
|
+
xpath { ".//meta[@name='description']" }
|
17
|
+
end
|
18
|
+
|
19
|
+
Capybara.add_selector(:json_ld) do
|
20
|
+
xpath { ".//script[@type='application/ld+json']" }
|
21
|
+
end
|
22
|
+
|
23
|
+
Capybara.add_selector(:image) do
|
24
|
+
xpath { ".//img[@srcset]" }
|
25
|
+
end
|
26
|
+
|
27
|
+
Capybara.add_selector(:post_link) do
|
28
|
+
xpath { ".//a[starts-with(@href, '/p/')]" }
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Instagrammer::Post
|
4
|
+
include Capybara::DSL
|
5
|
+
|
6
|
+
attr_reader :shortcode, :image_url, :image_urls
|
7
|
+
|
8
|
+
def initialize(shortcode)
|
9
|
+
@shortcode = shortcode
|
10
|
+
visit_page
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
attributes = %i(shortcode caption upload_date comment_count like_count)
|
15
|
+
attributes += %i(image_url image_urls) if photo?
|
16
|
+
attributes << "watch_count" if video?
|
17
|
+
"#<#{self.class.name}:#{object_id} #{attributes.map { |attr| "#{attr}:#{send(attr).inspect}" }.join(", ")}>"
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
@data["@type"] == "ImageObject" ? :photo : :video
|
22
|
+
end
|
23
|
+
|
24
|
+
def photo?
|
25
|
+
type == :photo
|
26
|
+
end
|
27
|
+
|
28
|
+
def video?
|
29
|
+
type == :video
|
30
|
+
end
|
31
|
+
|
32
|
+
def user
|
33
|
+
Instagrammer::User.new @data["author"]["alternateName"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def caption
|
37
|
+
@data["caption"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def upload_date
|
41
|
+
DateTime.parse @data["uploadDate"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def comment_count
|
45
|
+
@data["commentCount"].to_i
|
46
|
+
end
|
47
|
+
|
48
|
+
def like_count
|
49
|
+
@data["interactionStatistic"]["userInteractionCount"].to_i if photo?
|
50
|
+
end
|
51
|
+
|
52
|
+
def watch_count
|
53
|
+
@data["interactionStatistic"]["userInteractionCount"].to_i if video?
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def visit_page
|
58
|
+
visit "https://www.instagram.com/p/#{@shortcode}/"
|
59
|
+
check_status
|
60
|
+
|
61
|
+
@data = JSON.parse(page.first(:json_ld, visible: false).text(:all))
|
62
|
+
set_image_attributes if photo?
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_status
|
66
|
+
if page.has_content?("Private")
|
67
|
+
raise Instagrammer::PrivatePost.new("Private post: #{@shortcode}")
|
68
|
+
elsif page.has_content?("Sorry")
|
69
|
+
raise Instagrammer::PostNotFound.new("Post not found: #{@shortcode}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
IMAGE_URLS_RE = /(\S+) (\d+)w/
|
74
|
+
def set_image_attributes
|
75
|
+
node = page.find(:image)
|
76
|
+
@image_url = node["src"]
|
77
|
+
@image_urls = node["srcset"].scan(IMAGE_URLS_RE).collect do |match|
|
78
|
+
{
|
79
|
+
url: match[0],
|
80
|
+
width: match[1].to_i
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Instagrammer::User
|
4
|
+
include Capybara::DSL
|
5
|
+
|
6
|
+
attr_reader :posts
|
7
|
+
|
8
|
+
def initialize(username)
|
9
|
+
@username = username.delete_prefix("@")
|
10
|
+
@posts = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
attributes = %i(follower_count following_count post_count name username avatar bio url posts)
|
15
|
+
"#<#{self.class.name}:#{object_id} #{attributes.map { |attr| "#{attr}:#{send(attr).inspect}" }.join(", ")}>"
|
16
|
+
end
|
17
|
+
|
18
|
+
def public?
|
19
|
+
get_data unless @data
|
20
|
+
@status == :public
|
21
|
+
end
|
22
|
+
|
23
|
+
def meta
|
24
|
+
get_data unless @data
|
25
|
+
|
26
|
+
if @status == :not_found
|
27
|
+
raise Instagrammer::UserNotFound.new("Private account: #{@username}")
|
28
|
+
else
|
29
|
+
@meta
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def follower_count
|
34
|
+
meta[:followers]
|
35
|
+
end
|
36
|
+
|
37
|
+
def following_count
|
38
|
+
meta[:following]
|
39
|
+
end
|
40
|
+
|
41
|
+
def post_count
|
42
|
+
meta[:posts]
|
43
|
+
end
|
44
|
+
|
45
|
+
def data
|
46
|
+
get_data unless @data
|
47
|
+
|
48
|
+
case @status
|
49
|
+
when :private then raise Instagrammer::PrivateAccount.new("Private account: #{@username}")
|
50
|
+
when :not_found then raise Instagrammer::UserNotFound.new("User not found: #{@username}")
|
51
|
+
when :invalid then raise Instagrammer::UserInvalid.new("User invalid: #{@username}")
|
52
|
+
else @data
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def name
|
57
|
+
data["name"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def username
|
61
|
+
data["alternateName"].delete_prefix("@")
|
62
|
+
end
|
63
|
+
|
64
|
+
def avatar
|
65
|
+
data["image"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def bio
|
69
|
+
data["description"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def url
|
73
|
+
data["url"]
|
74
|
+
end
|
75
|
+
|
76
|
+
SHORTCODE_RE = /\/p\/(\S+)\/$/
|
77
|
+
def get_posts(limit)
|
78
|
+
shortcodes = []
|
79
|
+
i = 0
|
80
|
+
|
81
|
+
visit "https://www.instagram.com/#{@username}/"
|
82
|
+
while i < limit
|
83
|
+
post_links = page.all(:post_link)
|
84
|
+
|
85
|
+
if limit > post_links.size
|
86
|
+
page.execute_script "window.scrollTo(0,document.body.scrollHeight);"
|
87
|
+
post_links = page.all(:post_link)
|
88
|
+
end
|
89
|
+
|
90
|
+
shortcode = post_links[i]["href"].match(SHORTCODE_RE)[1]
|
91
|
+
shortcodes << shortcode
|
92
|
+
i += 1
|
93
|
+
end
|
94
|
+
|
95
|
+
@posts = shortcodes.collect { |code| Instagrammer::Post.new(code) }
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def get_data
|
100
|
+
visit "https://www.instagram.com/#{@username}/"
|
101
|
+
@status = get_account_status
|
102
|
+
@meta = get_metadata unless @status == :not_found
|
103
|
+
|
104
|
+
if @status == :public
|
105
|
+
node = page.first(:json_ld, visible: false)
|
106
|
+
@data = JSON.parse(node.text(:all))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
META_RE = /(?<followers>\S+) Followers, (?<following>\S+) Following, (?<posts>\S+) Posts/
|
111
|
+
def get_metadata
|
112
|
+
@meta = page.first(:meta_description, visible: false)["content"].match META_RE
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_account_status
|
116
|
+
if page.has_content?("Private")
|
117
|
+
:private
|
118
|
+
elsif page.has_content?("Sorry")
|
119
|
+
:not_found
|
120
|
+
elsif page.find(:json_ld, visible: false)
|
121
|
+
:public
|
122
|
+
end
|
123
|
+
rescue Capybara::ElementNotFound
|
124
|
+
:invalid
|
125
|
+
end
|
126
|
+
end
|
data/lib/instagrammer.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "capybara"
|
4
|
+
require "capybara/dsl"
|
5
|
+
require "webdrivers/chromedriver"
|
6
|
+
|
7
|
+
require "instagrammer/config/capybara"
|
8
|
+
require "instagrammer/post"
|
9
|
+
require "instagrammer/user"
|
10
|
+
require "instagrammer/version"
|
11
|
+
|
12
|
+
module Instagrammer
|
13
|
+
class PrivateAccount < StandardError; end
|
14
|
+
class UserInvalid < StandardError; end
|
15
|
+
class UserNotFound < StandardError; end
|
16
|
+
|
17
|
+
class PrivatePost < StandardError; end
|
18
|
+
class PostNotFound < StandardError; end
|
19
|
+
|
20
|
+
def self.new(username)
|
21
|
+
User.new(username)
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: instagrammer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richard Venneman
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-06-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: capybara
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.24'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.24'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: webdrivers
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.15'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.15'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '5.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '5.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '10.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '10.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rails_config
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- richardvenneman@me.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rubocop.yml"
|
134
|
+
- ".travis.yml"
|
135
|
+
- CHANGELOG.md
|
136
|
+
- Gemfile
|
137
|
+
- Gemfile.lock
|
138
|
+
- Guardfile
|
139
|
+
- LICENSE
|
140
|
+
- README.md
|
141
|
+
- Rakefile
|
142
|
+
- bin/console
|
143
|
+
- bin/setup
|
144
|
+
- instagrammer.gemspec
|
145
|
+
- lib/instagrammer.rb
|
146
|
+
- lib/instagrammer/config/capybara.rb
|
147
|
+
- lib/instagrammer/post.rb
|
148
|
+
- lib/instagrammer/user.rb
|
149
|
+
- lib/instagrammer/version.rb
|
150
|
+
homepage: https://github.com/richardvenneman/instagrammer
|
151
|
+
licenses: []
|
152
|
+
metadata:
|
153
|
+
homepage_uri: https://github.com/richardvenneman/instagrammer
|
154
|
+
source_code_uri: https://github.com/richardvenneman/instagrammer
|
155
|
+
changelog_uri: https://github.com/richardvenneman/instagrammer/blob/master/CHANGELOG.md
|
156
|
+
post_install_message:
|
157
|
+
rdoc_options: []
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - ">="
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubygems_version: 3.0.3
|
172
|
+
signing_key:
|
173
|
+
specification_version: 4
|
174
|
+
summary: Instagrammer lets you fetch Instagram user info and posts
|
175
|
+
test_files: []
|