rumble_bundle 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -1
- data/README.md +6 -0
- data/bin/setup +1 -3
- data/config/environment.rb +4 -3
- data/{bin → exe}/rumble_bundle +0 -0
- data/lib/rumble_bundle/bundle.rb +5 -14
- data/lib/rumble_bundle/cli.rb +32 -13
- data/lib/rumble_bundle/product.rb +1 -2
- data/lib/rumble_bundle/scraper.rb +70 -66
- data/lib/rumble_bundle/tier.rb +3 -0
- data/lib/rumble_bundle/version.rb +1 -1
- data/rumble_bundle.gemspec +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab912ac3d3ab1fa15544e0d5dde4421d1a9fb30c
|
4
|
+
data.tar.gz: '0479427cfff6e9e85a85d04917aaa9718f93c863'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9675a0ac23f75668bbf7ec75f61d13635b7d224d4cd240f93240c129f9c3ddb7033bf8eb7d1b6218c75d513a72f8f12bf157b95002d32718262b99918aaa1941
|
7
|
+
data.tar.gz: 54f374f52c02574af06e8e06ea3649d5dc82173f68e9b8e3213168d8677d6ca6a789c0210e24003a782607a2141c28bbd53cbfee66a7533efa37681a7fec2820
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -27,3 +27,9 @@ Then follow the on-screen prompts. :)
|
|
27
27
|
## Contributing
|
28
28
|
|
29
29
|
Bug reports and pull requests are welcome on GitHub at https://github.com/repromancer/rumble_bundle.
|
30
|
+
|
31
|
+
## License
|
32
|
+
|
33
|
+
This is free and unencumbered software released into the public domain.
|
34
|
+
|
35
|
+
For more information, please refer to the included UNLICENSE.md or <https://unlicense.org>
|
data/bin/setup
CHANGED
data/config/environment.rb
CHANGED
@@ -4,7 +4,8 @@ require 'nokogiri'
|
|
4
4
|
|
5
5
|
require 'rumble_bundle'
|
6
6
|
|
7
|
-
require_relative '../lib/rumble_bundle/cli'
|
8
|
-
require_relative '../lib/rumble_bundle/scraper'
|
9
7
|
require_relative '../lib/rumble_bundle/bundle'
|
10
|
-
require_relative '../lib/rumble_bundle/
|
8
|
+
require_relative '../lib/rumble_bundle/tier'
|
9
|
+
require_relative '../lib/rumble_bundle/product'
|
10
|
+
require_relative '../lib/rumble_bundle/scraper'
|
11
|
+
require_relative '../lib/rumble_bundle/cli'
|
data/{bin → exe}/rumble_bundle
RENAMED
File without changes
|
data/lib/rumble_bundle/bundle.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
class RumbleBundle::Bundle
|
2
2
|
|
3
|
-
attr_accessor :name, :
|
3
|
+
attr_accessor :name, :url, :tiers, :charities, :total_msrp
|
4
4
|
|
5
5
|
@@all = []
|
6
6
|
|
@@ -8,22 +8,13 @@ class RumbleBundle::Bundle
|
|
8
8
|
@@all
|
9
9
|
end
|
10
10
|
|
11
|
-
def initialize
|
12
|
-
data_hash.each{|key, val| self.send("#{key}=", val)}
|
11
|
+
def initialize
|
13
12
|
self.class.all << self
|
14
13
|
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
#associate incoming products with Bundle
|
21
|
-
@products = array.tap do |products|
|
22
|
-
products.each do |product|
|
23
|
-
product.bundle = self
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
15
|
+
def products
|
16
|
+
# Leverage Bundle#tiers to expose a flat array of a Bundle's Products.
|
17
|
+
self.tiers.collect{|t| t.products}.flatten
|
27
18
|
end
|
28
19
|
|
29
20
|
end
|
data/lib/rumble_bundle/cli.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
class RumbleBundle::CLI
|
2
2
|
|
3
|
+
# Scrape site and allow user to begin browsing.
|
3
4
|
def start
|
4
5
|
puts ""
|
5
6
|
puts "Fetching data from HumbleBundle.com..."
|
@@ -12,6 +13,7 @@ class RumbleBundle::CLI
|
|
12
13
|
end
|
13
14
|
|
14
15
|
|
16
|
+
|
15
17
|
def query
|
16
18
|
|
17
19
|
bundles = RumbleBundle::Bundle.all
|
@@ -21,22 +23,28 @@ class RumbleBundle::CLI
|
|
21
23
|
puts "Enter 'quit' to leave the program."
|
22
24
|
puts ""
|
23
25
|
|
26
|
+
# List Bundles in numbered order
|
24
27
|
bundles.each.with_index(1) do |bundle, i|
|
25
28
|
puts "[#{i}] #{bundle.name}"
|
26
29
|
end
|
27
30
|
|
31
|
+
# Get user input
|
28
32
|
puts ""
|
29
33
|
print " "
|
30
34
|
input = gets.strip.downcase
|
31
35
|
puts ""
|
32
36
|
|
37
|
+
# If input is a valid number, display the corresponding Bundle
|
33
38
|
if input.to_i.between?(1, bundles.length)
|
34
39
|
display_bundle(bundles[input.to_i - 1])
|
40
|
+
|
41
|
+
# If input is an invalid number, restart #query
|
35
42
|
elsif input.to_i < 0 || input.to_i > bundles.length
|
36
43
|
puts ""
|
37
44
|
puts "Sorry! Please enter a valid number or command."
|
38
45
|
query
|
39
46
|
|
47
|
+
# Check for 'help' or 'quit' commands, else pass input to #filter
|
40
48
|
else
|
41
49
|
case input
|
42
50
|
when 'help'
|
@@ -50,9 +58,14 @@ class RumbleBundle::CLI
|
|
50
58
|
|
51
59
|
end
|
52
60
|
|
61
|
+
|
62
|
+
|
53
63
|
def filter(input)
|
64
|
+
# Compare input to recognized tags
|
54
65
|
filters = %w[drm-free steam-key windows mac linux android]
|
55
66
|
valid = filters & input.split
|
67
|
+
|
68
|
+
# If input is valid, collect an array of all Products, remove any that do not match search tags, then display results in a formatted list
|
56
69
|
if valid.any?
|
57
70
|
filtered = RumbleBundle::Product.all.collect{|p| p}
|
58
71
|
valid.each do |filter|
|
@@ -73,8 +86,13 @@ class RumbleBundle::CLI
|
|
73
86
|
filtered.collect{|p| p.bundle }.uniq.each do |bundle|
|
74
87
|
puts "#{bundle.name} (#{bundle.url})"
|
75
88
|
puts ""
|
76
|
-
|
77
|
-
|
89
|
+
bundle.tiers.each do |tier|
|
90
|
+
if filtered.detect{|p| p.tier == tier}
|
91
|
+
puts " #{tier.description}", ""
|
92
|
+
filtered.each{|p| display_product(p) if p.tier == tier}
|
93
|
+
puts ""
|
94
|
+
end
|
95
|
+
end
|
78
96
|
end
|
79
97
|
puts "---------------------------------------------------------------"
|
80
98
|
else
|
@@ -95,25 +113,24 @@ class RumbleBundle::CLI
|
|
95
113
|
|
96
114
|
end
|
97
115
|
|
116
|
+
|
117
|
+
|
118
|
+
# Display formatted information for given Bundle
|
98
119
|
def display_bundle(bundle)
|
99
120
|
puts "---------------------------------------------------------------"
|
100
121
|
puts "#{bundle.name} (#{bundle.url})"
|
101
122
|
puts "#{bundle.total_msrp}!"
|
102
123
|
puts ""
|
103
|
-
puts "Supports:"
|
124
|
+
puts " Supports:"
|
104
125
|
bundle.charities.each do |c|
|
105
|
-
print "
|
126
|
+
print " #{c}"
|
106
127
|
puts ","
|
107
128
|
end
|
108
|
-
puts "
|
129
|
+
puts " or a Charity of Your Choice"
|
109
130
|
puts "", ""
|
110
131
|
bundle.tiers.each do |tier|
|
111
|
-
puts tier, ""
|
112
|
-
|
113
|
-
if product.tier == tier
|
114
|
-
display_product(product)
|
115
|
-
end
|
116
|
-
end
|
132
|
+
puts " #{tier.description}", ""
|
133
|
+
tier.products.each{|p| display_product(p)}
|
117
134
|
puts ""
|
118
135
|
end
|
119
136
|
puts "---------------------------------------------------------------"
|
@@ -122,15 +139,16 @@ class RumbleBundle::CLI
|
|
122
139
|
end
|
123
140
|
|
124
141
|
def display_product(product)
|
125
|
-
print "
|
142
|
+
print " #{product.name}"
|
126
143
|
print " (#{product.platforms.join(", ")})" if product.platforms.any?
|
127
144
|
print " (DRM-Free!)" if product.drm_free
|
128
145
|
print " (w/Steam Key!)" if product.steam_key
|
129
146
|
puts ""
|
130
|
-
puts "
|
147
|
+
puts " #{product.subtitle}" if product.subtitle
|
131
148
|
puts ""
|
132
149
|
end
|
133
150
|
|
151
|
+
# Display help
|
134
152
|
def help
|
135
153
|
puts <<~HEREDOC
|
136
154
|
---------------------------------------------------------------
|
@@ -153,6 +171,7 @@ class RumbleBundle::CLI
|
|
153
171
|
query
|
154
172
|
end
|
155
173
|
|
174
|
+
# Quit
|
156
175
|
def quit
|
157
176
|
exit
|
158
177
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class RumbleBundle::Scraper
|
2
2
|
|
3
3
|
def initialize
|
4
|
+
# Set URLs used as starting points for scraping
|
4
5
|
@base_url = 'https://www.humblebundle.com'
|
5
6
|
@main_pages = [
|
6
7
|
'https://www.humblebundle.com/',
|
@@ -11,15 +12,18 @@ class RumbleBundle::Scraper
|
|
11
12
|
|
12
13
|
|
13
14
|
def crawl_site
|
14
|
-
|
15
|
+
# For each main page
|
15
16
|
@main_pages.each do |page|
|
17
|
+
# scrape the bundle from that page
|
16
18
|
first_page = Nokogiri::HTML(open(page))
|
17
19
|
scrape_bundle(first_page, page)
|
18
20
|
|
21
|
+
# and if there are any sub-pages
|
19
22
|
if first_page.at_css(".js-highlight.subtab-button")
|
20
23
|
tab_bar = first_page.css(".js-highlight.subtab-button")
|
21
24
|
links = tab_bar.collect{|a| @base_url + a.attr("href") }
|
22
25
|
|
26
|
+
# scrape them for bundles too
|
23
27
|
links.each do |link|
|
24
28
|
scrape_bundle(Nokogiri::HTML(open(link)), link)
|
25
29
|
end
|
@@ -32,96 +36,96 @@ class RumbleBundle::Scraper
|
|
32
36
|
|
33
37
|
def scrape_bundle(html, url)
|
34
38
|
|
35
|
-
|
36
|
-
'name' => '',
|
37
|
-
'tiers' => [],
|
38
|
-
'products' => [],
|
39
|
-
'charities' => [],
|
40
|
-
'total_msrp' => '',
|
41
|
-
'url' => url
|
42
|
-
}
|
39
|
+
RumbleBundle::Bundle.new.tap do |bundle|
|
43
40
|
|
44
|
-
|
41
|
+
bundle.url = url
|
45
42
|
|
46
|
-
|
43
|
+
# Scrape Bundle metadata
|
47
44
|
|
48
|
-
|
49
|
-
html.css(".main-content-row").each do |tier|
|
45
|
+
bundle.name = html.css("title").text.chomp("(pay what you want and help charity)").strip
|
50
46
|
|
51
|
-
|
52
|
-
tier_name = tier.css(".dd-header-headline").text.strip
|
53
|
-
if tier_name.downcase.include?("charity")
|
54
|
-
break
|
55
|
-
end
|
47
|
+
bundle.charities = html.css(".charity-image-wrapper img").collect{|img| img.attr("alt")}
|
56
48
|
|
57
|
-
bundle
|
49
|
+
bundle.total_msrp = html.css('.hr-tagline-text').detect{|e| e.text.include?("worth")}.text.strip
|
58
50
|
|
59
|
-
#and instantiate products from tier
|
60
|
-
tier.css(".game-boxes").each do |box|
|
61
|
-
scrape_product(box, tier_name).tap do |product|
|
62
|
-
bundle['products'] << product
|
63
|
-
end
|
64
|
-
end
|
65
51
|
|
66
|
-
|
52
|
+
bundle.tiers = Array.new.tap do |bundle_tiers|
|
53
|
+
|
54
|
+
# For each 'main content row' on the page, instantiate a Tier with its Products and add that Tier to the Bundle's @tiers array
|
55
|
+
html.css(".main-content-row").each do |row|
|
56
|
+
|
57
|
+
row_title = row.css(".dd-header-headline").text.strip
|
67
58
|
|
68
|
-
|
59
|
+
# Stop iterating if scraper has reached a row which is not a Tier
|
60
|
+
if row_title.downcase.include?("charity")
|
61
|
+
break
|
62
|
+
end
|
69
63
|
|
70
|
-
|
64
|
+
bundle_tiers << RumbleBundle::Tier.new.tap do |tier|
|
65
|
+
tier.description = row_title
|
66
|
+
tier.bundle = bundle
|
67
|
+
tier.products = Array.new.tap do |tier_products|
|
68
|
+
row.css(".game-boxes").each do |box|
|
69
|
+
scrape_product(box, bundle, tier).tap do |product|
|
70
|
+
tier_products << product
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
71
75
|
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
72
81
|
end
|
73
82
|
|
74
83
|
|
84
|
+
# Self-explanatory
|
85
|
+
def scrape_product(box, bundle, tier)
|
75
86
|
|
76
|
-
|
87
|
+
RumbleBundle::Product.new.tap do |product|
|
77
88
|
|
78
|
-
|
79
|
-
|
80
|
-
'subtitle' => '',
|
81
|
-
'bundle' => '',
|
82
|
-
'tier' => '',
|
83
|
-
'platforms' => [],
|
84
|
-
'drm_free' => nil,
|
85
|
-
'steam_key' => nil
|
86
|
-
}
|
89
|
+
product.bundle = bundle
|
90
|
+
product.tier = tier
|
87
91
|
|
88
|
-
|
89
|
-
product['tier'] = tier
|
90
|
-
product['subtitle'] = if box.at_css(".subtitle")
|
91
|
-
box.css(".subtitle .callout-msrp").remove
|
92
|
-
if box.css(".subtitle").text.strip != ""
|
93
|
-
box.css(".subtitle").text.strip
|
94
|
-
else
|
95
|
-
nil
|
96
|
-
end
|
97
|
-
end
|
92
|
+
product.name = box.css(".dd-image-box-caption").text.strip
|
98
93
|
|
99
|
-
|
100
|
-
|
101
|
-
|
94
|
+
product.subtitle = if box.at_css(".subtitle")
|
95
|
+
box.css(".subtitle .callout-msrp").remove
|
96
|
+
if box.css(".subtitle").text.strip != ""
|
97
|
+
box.css(".subtitle").text.strip
|
98
|
+
else
|
99
|
+
nil
|
100
|
+
end
|
102
101
|
end
|
103
102
|
|
104
|
-
|
105
|
-
|
106
|
-
|
103
|
+
product.platforms = Array.new.tap do |platforms|
|
104
|
+
if box.at_css(".dd-availability-icon > i.hb-android")
|
105
|
+
platforms << 'Android'
|
106
|
+
end
|
107
107
|
|
108
|
-
|
109
|
-
|
110
|
-
|
108
|
+
if box.at_css(".dd-availability-icon > i.hb-linux")
|
109
|
+
platforms << 'Linux'
|
110
|
+
end
|
111
111
|
|
112
|
-
|
113
|
-
|
114
|
-
|
112
|
+
if box.at_css(".dd-availability-icon > i.hb-windows")
|
113
|
+
platforms << 'Windows'
|
114
|
+
end
|
115
115
|
|
116
|
-
|
116
|
+
if box.at_css(".dd-availability-icon > i.hb-osx")
|
117
|
+
platforms << 'Mac'
|
118
|
+
end
|
117
119
|
|
118
|
-
|
119
|
-
|
120
|
+
end
|
121
|
+
|
122
|
+
product.drm_free = box.at_css(".dd-availability-icon > i.hb-drmfree") ?
|
123
|
+
true : false
|
120
124
|
|
121
|
-
|
122
|
-
|
125
|
+
product.steam_key = box.at_css(".dd-availability-icon > i.hb-steam") ?
|
126
|
+
true : false
|
123
127
|
|
124
|
-
|
128
|
+
end
|
125
129
|
|
126
130
|
end
|
127
131
|
|
data/rumble_bundle.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rumble_bundle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Summers
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -88,14 +88,15 @@ files:
|
|
88
88
|
- Rakefile
|
89
89
|
- UNLICENSE.md
|
90
90
|
- bin/console
|
91
|
-
- bin/rumble_bundle
|
92
91
|
- bin/setup
|
93
92
|
- config/environment.rb
|
93
|
+
- exe/rumble_bundle
|
94
94
|
- lib/rumble_bundle.rb
|
95
95
|
- lib/rumble_bundle/bundle.rb
|
96
96
|
- lib/rumble_bundle/cli.rb
|
97
97
|
- lib/rumble_bundle/product.rb
|
98
98
|
- lib/rumble_bundle/scraper.rb
|
99
|
+
- lib/rumble_bundle/tier.rb
|
99
100
|
- lib/rumble_bundle/version.rb
|
100
101
|
- rumble_bundle.gemspec
|
101
102
|
- spec.md
|