rockauto_api 0.1.0 → 0.2.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 +4 -4
- data/README.md +380 -0
- data/lib/rockauto_api/client.rb +44 -13
- data/lib/rockauto_api/endpoints/account.rb +45 -10
- data/lib/rockauto_api/endpoints/part_categories.rb +9 -16
- data/lib/rockauto_api/endpoints/part_search.rb +93 -5
- data/lib/rockauto_api/models/part.rb +1 -0
- data/lib/rockauto_api/parsers/fitment_parser.rb +11 -2
- data/lib/rockauto_api/version.rb +1 -1
- data/lib/rockauto_api.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d861f195fe9c02dc5737f5756f0c3e7248ddad192a380913dd9e95a65d327b8
|
|
4
|
+
data.tar.gz: 93ef1dddb194b74a1d091039460b3586466f1e225bba2289b0da43cfe5253e02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4606e5e6fe6ea4b7301722ddaba983dfab6de91c47ea8510054898b1d95f292b44c4adf2e98ce5bd44bca10d8dc2f7c86b539df7f888863b0b86e821f809d91
|
|
7
|
+
data.tar.gz: 0d6ec3fdb8ba7f234e91dbfcbd31b504d44dc53b10ab3c1d3610e28a63dd3c4f983a79b077f3312c3922f1e3c824fb6c1688aeebf7e1c404c8716482e38ba7d0
|
data/README.md
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# RockautoApi
|
|
2
|
+
|
|
3
|
+
An unofficial Ruby API client for [RockAuto.com](https://www.rockauto.com). Browse vehicle catalogs, search parts by number, look up fitments, check order status, and manage your RockAuto account.
|
|
4
|
+
|
|
5
|
+
**Disclaimer:** This gem is for **educational and research purposes only**. It is not affiliated with, endorsed by, or officially connected to RockAuto LLC. Automated scraping of RockAuto.com may violate their Terms of Service. Use responsibly and respect rate limits.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "rockauto_api"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install directly:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
gem install rockauto_api
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require "rockauto_api"
|
|
35
|
+
|
|
36
|
+
client = RockautoApi::Client.new
|
|
37
|
+
|
|
38
|
+
# Browse what RockAuto sells
|
|
39
|
+
makes = client.get_makes
|
|
40
|
+
makes.makes #=> ["ACURA", "AUDI", "BMW", ...]
|
|
41
|
+
|
|
42
|
+
years = client.get_years_for_make("AUDI")
|
|
43
|
+
years.years #=> [1980, 1981, ..., 2026]
|
|
44
|
+
|
|
45
|
+
models = client.get_models_for_make_year("AUDI", 2019)
|
|
46
|
+
models.models #=> ["A4", "A5", "A6", "A7", ...]
|
|
47
|
+
|
|
48
|
+
engines = client.get_engines_for_vehicle("AUDI", 2019, "A4")
|
|
49
|
+
engines.engines #=> [Engine(description: "2.0L L4 Turbocharged", carcode: "3443561"), ...]
|
|
50
|
+
|
|
51
|
+
# Get part categories for a vehicle
|
|
52
|
+
categories = client.get_part_categories("AUDI", 2019, "A4", "3443561")
|
|
53
|
+
categories.categories #=> [PartCategory(name: "Belt Drive"), ...]
|
|
54
|
+
|
|
55
|
+
# Get parts in a category
|
|
56
|
+
parts = client.get_parts_by_category("AUDI", 2019, "A4", "3443561", "Belt Drive")
|
|
57
|
+
parts.parts #=> [PartInfo, ...]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## All Endpoints
|
|
63
|
+
|
|
64
|
+
### Vehicle Catalog
|
|
65
|
+
|
|
66
|
+
Browse the vehicle catalog to find makes, years, models, and engines.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# List all makes
|
|
70
|
+
makes = client.get_makes
|
|
71
|
+
makes.makes #=> ["ACURA", "AUDI", "BMW", ...]
|
|
72
|
+
makes.count #=> Integer
|
|
73
|
+
|
|
74
|
+
# List years for a make
|
|
75
|
+
years = client.get_years_for_make("ACURA")
|
|
76
|
+
years.make #=> "ACURA"
|
|
77
|
+
years.years #=> [1986, 1987, ..., 2026]
|
|
78
|
+
years.count #=> Integer
|
|
79
|
+
|
|
80
|
+
# List models for a make and year
|
|
81
|
+
models = client.get_models_for_make_year("ACURA", 2020)
|
|
82
|
+
models.make #=> "ACURA"
|
|
83
|
+
models.year #=> 2020
|
|
84
|
+
models.models #=> ["MDX", "TLX", ...]
|
|
85
|
+
models.count #=> Integer
|
|
86
|
+
|
|
87
|
+
# List engines for a specific vehicle
|
|
88
|
+
engines = client.get_engines_for_vehicle("ACURA", 2020, "MDX")
|
|
89
|
+
engines.make #=> "ACURA"
|
|
90
|
+
engines.year #=> 2020
|
|
91
|
+
engines.model #=> "MDX"
|
|
92
|
+
engines.engines #=> [Engine(description, carcode, href), ...]
|
|
93
|
+
engines.count #=> Integer
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Part Categories
|
|
97
|
+
|
|
98
|
+
Get the part categories available for a specific vehicle, and parts within a category.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Get all part categories for a vehicle
|
|
102
|
+
categories = client.get_part_categories("ACURA", 2020, "MDX", "3444459")
|
|
103
|
+
categories.make #=> "ACURA"
|
|
104
|
+
categories.year #=> 2020
|
|
105
|
+
categories.model #=> "MDX"
|
|
106
|
+
categories.carcode #=> "3444459"
|
|
107
|
+
categories.categories #=> [PartCategory(name, group_name, href), ...]
|
|
108
|
+
categories.count #=> Integer
|
|
109
|
+
|
|
110
|
+
# Get parts in a category
|
|
111
|
+
parts = client.get_parts_by_category("ACURA", 2020, "MDX", "3444459", "Brake & Wheel Hub")
|
|
112
|
+
parts.make #=> "ACURA"
|
|
113
|
+
parts.year #=> 2020
|
|
114
|
+
parts.model #=> "MDX"
|
|
115
|
+
parts.carcode #=> "3444459"
|
|
116
|
+
parts.category #=> "Brake & Wheel Hub"
|
|
117
|
+
parts.parts #=> [PartInfo, ...]
|
|
118
|
+
parts.count #=> Integer
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Part Search
|
|
122
|
+
|
|
123
|
+
Search for parts by number, name, or browse available manufacturers, groups, and types.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Browse available manufacturers (cached for 24 hours)
|
|
127
|
+
manufacturers = client.get_manufacturers
|
|
128
|
+
manufacturers.manufacturers #=> [PartSearchOption(value, text), ...]
|
|
129
|
+
manufacturers.lookup("Bosch") #=> PartSearchOption(value: "128", text: "Bosch")
|
|
130
|
+
|
|
131
|
+
# Browse part groups (cached for 24 hours)
|
|
132
|
+
groups = client.get_part_groups
|
|
133
|
+
groups.part_groups #=> [PartSearchOption(value, text), ...]
|
|
134
|
+
|
|
135
|
+
# Browse part types (cached for 24 hours)
|
|
136
|
+
types = client.get_part_types
|
|
137
|
+
types.part_types #=> [PartSearchOption(value, text), ...]
|
|
138
|
+
|
|
139
|
+
# Search parts by number
|
|
140
|
+
results = client.search_parts_by_number("FG0326")
|
|
141
|
+
results.parts #=> [PartInfo, ...]
|
|
142
|
+
results.count #=> Integer
|
|
143
|
+
results.search_term #=> "FG0326"
|
|
144
|
+
results.manufacturer #=> "All"
|
|
145
|
+
results.part_group #=> "All"
|
|
146
|
+
|
|
147
|
+
# Search with filters
|
|
148
|
+
results = client.search_parts_by_number(
|
|
149
|
+
"FG0326",
|
|
150
|
+
manufacturer: "Bosch",
|
|
151
|
+
part_group: "Brakes",
|
|
152
|
+
part_type: "Pads"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Include fitment data (triggers additional API call per part)
|
|
156
|
+
results = client.search_parts_by_number("FG0326", include_fitments: true)
|
|
157
|
+
results.parts.first.buyers_guide #=> BuyersGuideResult
|
|
158
|
+
|
|
159
|
+
# Look up what a part is called
|
|
160
|
+
results = client.what_is_part_called("brake pad")
|
|
161
|
+
results.results #=> [WhatIsPartCalledResult(main_category, subcategory, full_path), ...]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Fitment / Buyers Guide
|
|
165
|
+
|
|
166
|
+
Get vehicle fitment information for a specific part.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
fitment = client.get_fitment_for_part(listing_data)
|
|
170
|
+
# listing_data comes from PartInfo#listing_data or can be constructed manually
|
|
171
|
+
fitment.part_number #=> "FG0326"
|
|
172
|
+
fitment.brand #=> "Bosch"
|
|
173
|
+
fitment.fitments #=> [FitmentInfo(year, make, model, engine, ...), ...]
|
|
174
|
+
fitment.count #=> Integer
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Order Status
|
|
178
|
+
|
|
179
|
+
Look up order status and request order lists (no login required).
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# Look up an order by email and order number
|
|
183
|
+
result = client.lookup_order_status("you@example.com", "RA-123456")
|
|
184
|
+
result.success #=> true/false
|
|
185
|
+
result.order #=> OrderStatus (if found)
|
|
186
|
+
result.error #=> OrderStatusError (if not found)
|
|
187
|
+
|
|
188
|
+
# Order status details
|
|
189
|
+
order = result.order
|
|
190
|
+
order.order_number #=> "RA-123456"
|
|
191
|
+
order.order_date #=> "2024-01-15"
|
|
192
|
+
order.status #=> "Shipped"
|
|
193
|
+
order.items #=> [OrderItem(part_number, description, quantity, ...), ...]
|
|
194
|
+
order.billing #=> BillingInfo(subtotal, shipping_cost, tax, total, ...)
|
|
195
|
+
order.shipping #=> ShippingInfo(method, carrier, tracking_number, ...)
|
|
196
|
+
order.notes #=> String
|
|
197
|
+
order.return_eligibility #=> String
|
|
198
|
+
|
|
199
|
+
# Request RockAuto to email you a list of your orders
|
|
200
|
+
client.request_order_list(:email, "you@example.com") #=> true
|
|
201
|
+
|
|
202
|
+
# Request via SMS
|
|
203
|
+
client.request_order_list(:sms, "+15551234567") #=> true
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Account (Authenticated)
|
|
207
|
+
|
|
208
|
+
Requires logging in with valid RockAuto credentials.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
# Configure credentials globally
|
|
212
|
+
RockautoApi.configure do |config|
|
|
213
|
+
config.credentials = { email: "you@example.com", password: "your_password" }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Or pass credentials per-session
|
|
217
|
+
client = RockautoApi::Client.new
|
|
218
|
+
client.login("you@example.com", "your_password")
|
|
219
|
+
|
|
220
|
+
# Check login status
|
|
221
|
+
client.authenticated? #=> true/false
|
|
222
|
+
|
|
223
|
+
# Get saved addresses
|
|
224
|
+
addresses = client.get_saved_addresses
|
|
225
|
+
addresses.addresses #=> [SavedAddress(name, full_name, address_line1, city, state, ...), ...]
|
|
226
|
+
addresses.count #=> Integer
|
|
227
|
+
addresses.has_default #=> true/false
|
|
228
|
+
|
|
229
|
+
# Get saved vehicles
|
|
230
|
+
vehicles = client.get_saved_vehicles
|
|
231
|
+
vehicles.vehicles #=> [SavedVehicle(year, make, model, engine, carcode, ...), ...]
|
|
232
|
+
vehicles.count #=> Integer
|
|
233
|
+
|
|
234
|
+
# Get order history
|
|
235
|
+
history = client.get_order_history
|
|
236
|
+
history.orders #=> [OrderHistoryItem(order_number, date, status, total, vehicle), ...]
|
|
237
|
+
history.count #=> Integer
|
|
238
|
+
history.search_time #=> "2026-06-28T..."
|
|
239
|
+
|
|
240
|
+
# Get full account activity (addresses + vehicles)
|
|
241
|
+
activity = client.get_account_activity
|
|
242
|
+
activity.saved_addresses #=> SavedAddressesResult
|
|
243
|
+
activity.saved_vehicles #=> SavedVehiclesResult
|
|
244
|
+
|
|
245
|
+
# Add an external order to your account
|
|
246
|
+
client.add_external_order("you@example.com", "RA-123456") #=> true
|
|
247
|
+
|
|
248
|
+
# Logout
|
|
249
|
+
client.logout #=> true
|
|
250
|
+
client.authenticated? #=> false
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Tools
|
|
254
|
+
|
|
255
|
+
Browse RockAuto's tool catalog.
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
# Get tool categories
|
|
259
|
+
categories = client.get_tool_categories
|
|
260
|
+
categories.categories #=> [ToolCategory(name, group_name, href, level), ...]
|
|
261
|
+
categories.count #=> Integer
|
|
262
|
+
|
|
263
|
+
# Get tools in a category
|
|
264
|
+
tools = client.get_tools_by_category("/en/tools/?parttype=260")
|
|
265
|
+
tools.tools #=> [ToolInfo(name, part_number, brand, description, ...), ...]
|
|
266
|
+
tools.count #=> Integer
|
|
267
|
+
tools.category #=> "?parttype=260"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Configuration
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
RockautoApi.configure do |config|
|
|
276
|
+
config.default_mobile = true # Use mobile site headers (slimmer HTML)
|
|
277
|
+
config.request_timeout = 30 # HTTP timeout in seconds
|
|
278
|
+
config.credentials = { email: "me@example.com", password: "s3cret" }
|
|
279
|
+
config.cache = Rails.cache # Use Rails cache for 24h/7d TTLs
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Rails Integration
|
|
286
|
+
|
|
287
|
+
The gem includes a Railtie that automatically hooks into `Rails.cache`. No extra setup needed:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
# Gemfile
|
|
291
|
+
gem "rockauto_api"
|
|
292
|
+
|
|
293
|
+
# config/initializers/rockauto_api.rb
|
|
294
|
+
RockautoApi.configure do |config|
|
|
295
|
+
config.default_mobile = false
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# app/models/part_lookup.rb
|
|
299
|
+
class PartLookup
|
|
300
|
+
def search(query)
|
|
301
|
+
client = RockautoApi::Client.new
|
|
302
|
+
client.search_parts_by_number(query)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Error Handling
|
|
310
|
+
|
|
311
|
+
All errors inherit from `RockautoApi::Error`:
|
|
312
|
+
|
|
313
|
+
| Error Class | When It's Raised |
|
|
314
|
+
|---|---|
|
|
315
|
+
| `RockautoApi::AuthenticationError` | Calling authenticated methods without logging in |
|
|
316
|
+
| `RockautoApi::NetworkError` | HTTP request fails (timeout, connection error) |
|
|
317
|
+
| `RockautoApi::CaptchaError` | RockAuto returns a CAPTCHA challenge |
|
|
318
|
+
| `RockautoApi::ParseError` | Failed to parse HTML response |
|
|
319
|
+
| `RockautoApi::NotFoundError` | Resource not found |
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
begin
|
|
325
|
+
client.get_saved_addresses
|
|
326
|
+
rescue RockautoApi::AuthenticationError => e
|
|
327
|
+
puts "Please login first: #{e.message}"
|
|
328
|
+
rescue RockautoApi::NetworkError => e
|
|
329
|
+
puts "Network issue: #{e.message}"
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Development
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
git clone https://github.com/bendangelo/rockauto_api
|
|
339
|
+
cd rockauto_api
|
|
340
|
+
bin/setup
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Run the test suite:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
bundle exec rspec
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Tests use [VCR](https://github.com/vcr/vcr) to record and replay HTTP interactions. To re-record cassettes with live data:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
rm -rf spec/cassettes/
|
|
353
|
+
bundle exec rspec
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
New cassettes will be created recording real HTTP requests. Sensitive data (credentials, cookies) is automatically filtered.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Contributing
|
|
361
|
+
|
|
362
|
+
1. Fork the repository
|
|
363
|
+
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
364
|
+
3. Commit your changes (`git commit -am "Add my feature"`)
|
|
365
|
+
4. Push to the branch (`git push origin feature/my-feature`)
|
|
366
|
+
5. Open a Pull Request
|
|
367
|
+
|
|
368
|
+
Please include tests for any new functionality and ensure the full suite passes.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## License
|
|
373
|
+
|
|
374
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Disclaimer
|
|
379
|
+
|
|
380
|
+
This software is provided for **educational and research purposes only**. It is not affiliated with, endorsed by, or officially connected to RockAuto LLC. Automated access to RockAuto.com may violate their Terms of Service. Users are solely responsible for ensuring their use complies with all applicable terms and laws. The authors assume no liability for any misuse.
|
data/lib/rockauto_api/client.rb
CHANGED
|
@@ -68,6 +68,7 @@ module RockautoApi
|
|
|
68
68
|
|
|
69
69
|
resp = @conn.get("/")
|
|
70
70
|
@nck_token = Parsers::HtmlHelpers.extract_javascript_variable(resp.body, "_nck")
|
|
71
|
+
@nck_token ||= Parsers::HtmlHelpers.extract_csrf_token(resp.body, "_nck")
|
|
71
72
|
@jnck_token = @nck_token ? CGI.escape(@nck_token) : nil
|
|
72
73
|
@session_initialized = true
|
|
73
74
|
end
|
|
@@ -106,6 +107,7 @@ module RockautoApi
|
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
def post_with_csrf(url, form_data)
|
|
110
|
+
init_session!
|
|
109
111
|
page_resp = @conn.get(url)
|
|
110
112
|
nck = Parsers::HtmlHelpers.extract_csrf_token(page_resp.body)
|
|
111
113
|
form_data["_nck"] = nck if nck
|
|
@@ -125,24 +127,52 @@ module RockautoApi
|
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
if make
|
|
128
|
-
payload =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"fetching" => true,
|
|
135
|
-
"max_group_index" => 363,
|
|
136
|
-
"mkt_US" => true,
|
|
137
|
-
"mkt_CA" => false,
|
|
138
|
-
"mkt_MX" => false
|
|
139
|
-
}
|
|
140
|
-
}
|
|
130
|
+
payload = navnode_fetch_payload(
|
|
131
|
+
make: make,
|
|
132
|
+
nodetype: "make",
|
|
133
|
+
label: make,
|
|
134
|
+
href: "#{BASE_URL}/en/catalog/#{make.downcase}"
|
|
135
|
+
)
|
|
141
136
|
call_catalog_api("navnode_fetch", payload)
|
|
142
137
|
end
|
|
143
138
|
end
|
|
144
139
|
|
|
140
|
+
def navnode_fetch_payload(make:, nodetype:, label: nil, href: nil, year: nil, model: nil, carcode: nil)
|
|
141
|
+
jsn = {
|
|
142
|
+
"tab" => "catalog",
|
|
143
|
+
"make" => make,
|
|
144
|
+
"nodetype" => nodetype,
|
|
145
|
+
"jsdata" => {
|
|
146
|
+
"markets" => [
|
|
147
|
+
{"c" => "US", "y" => "Y", "i" => "Y"},
|
|
148
|
+
{"c" => "CA", "y" => "Y", "i" => "Y"},
|
|
149
|
+
{"c" => "MX", "y" => "Y", "i" => "Y"}
|
|
150
|
+
],
|
|
151
|
+
"mktlist" => "US,CA,MX",
|
|
152
|
+
"showForMarkets" => {"US" => true, "CA" => true, "MX" => true},
|
|
153
|
+
"importanceByMarket" => {"US" => "Y", "CA" => "Y", "MX" => "Y"},
|
|
154
|
+
"Show" => 1
|
|
155
|
+
},
|
|
156
|
+
"loaded" => false,
|
|
157
|
+
"expand_after_load" => true,
|
|
158
|
+
"fetching" => true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
jsn["year"] = year.to_s if year
|
|
162
|
+
jsn["model"] = model if model
|
|
163
|
+
jsn["carcode"] = carcode if carcode
|
|
164
|
+
jsn["label"] = label if label
|
|
165
|
+
jsn["href"] = href if href
|
|
166
|
+
jsn["labelset"] = true if label || href
|
|
167
|
+
jsn["jump_to_after_expand"] = true if nodetype == "make"
|
|
168
|
+
jsn["dont_change_url"] = true if nodetype == "make"
|
|
169
|
+
jsn["has_more_auto_open_steps"] = true if nodetype == "make"
|
|
170
|
+
|
|
171
|
+
{ "jsn" => jsn, "max_group_index" => 388 }
|
|
172
|
+
end
|
|
173
|
+
|
|
145
174
|
def get(path)
|
|
175
|
+
init_session!
|
|
146
176
|
resp = @conn.get(path)
|
|
147
177
|
resp.body
|
|
148
178
|
rescue Faraday::Error => e
|
|
@@ -150,6 +180,7 @@ module RockautoApi
|
|
|
150
180
|
end
|
|
151
181
|
|
|
152
182
|
def post(path, body = nil)
|
|
183
|
+
init_session!
|
|
153
184
|
resp = @conn.post(path, body)
|
|
154
185
|
resp.body
|
|
155
186
|
rescue Faraday::Error => e
|
|
@@ -8,23 +8,41 @@ module RockautoApi
|
|
|
8
8
|
ORDER_HISTORY_URL = "/en/orderhistory/"
|
|
9
9
|
|
|
10
10
|
def login(email, password)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
init_session!
|
|
12
|
+
|
|
13
|
+
form_data = {
|
|
14
|
+
"loginaction" => "login",
|
|
15
|
+
"accountemail" => email,
|
|
16
|
+
"captchacode" => "",
|
|
17
|
+
"passworddecoy" => "",
|
|
18
|
+
"password" => password,
|
|
19
|
+
"passwordconfirmdecoy" => "",
|
|
20
|
+
"passwordconfirm" => "",
|
|
21
|
+
"keepsignin" => "false",
|
|
22
|
+
"async" => "1",
|
|
23
|
+
"accountlogin_php" => "1"
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
resp = account_api_post(form_data)
|
|
27
|
+
|
|
28
|
+
result = JSON.parse(resp.body)
|
|
29
|
+
@authenticated = result["message"]&.include?("Successful") || false
|
|
21
30
|
@authenticated
|
|
22
|
-
rescue
|
|
31
|
+
rescue StandardError
|
|
23
32
|
@authenticated = false
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
def logout
|
|
27
|
-
|
|
36
|
+
init_session!
|
|
37
|
+
|
|
38
|
+
form_data = {
|
|
39
|
+
"loginaction" => "logout",
|
|
40
|
+
"async" => "1",
|
|
41
|
+
"accountlogin_php" => "1"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
account_api_post(form_data)
|
|
45
|
+
|
|
28
46
|
@authenticated = false
|
|
29
47
|
true
|
|
30
48
|
rescue StandardError
|
|
@@ -154,6 +172,23 @@ module RockautoApi
|
|
|
154
172
|
def require_authentication!
|
|
155
173
|
raise AuthenticationError, "Not authenticated. Call login(email, password) first." unless @authenticated
|
|
156
174
|
end
|
|
175
|
+
|
|
176
|
+
def account_api_post(form_data)
|
|
177
|
+
Faraday.new(url: Client::BASE_URL) do |f|
|
|
178
|
+
f.request :url_encoded
|
|
179
|
+
f.use :cookie_jar
|
|
180
|
+
f.adapter Faraday.default_adapter
|
|
181
|
+
f.options.timeout = RockautoApi.configuration&.request_timeout || 30
|
|
182
|
+
@conn.headers["Cookie"].to_s.split(";").each do |cookie|
|
|
183
|
+
name, val = cookie.strip.split("=", 2)
|
|
184
|
+
f.headers["Cookie"] = "#{f.headers['Cookie']}; #{name}=#{val}" if name && val
|
|
185
|
+
end
|
|
186
|
+
f.headers["User-Agent"] = Client::MOBILE_HEADERS["User-Agent"]
|
|
187
|
+
f.headers["Referer"] = "#{Client::BASE_URL}/"
|
|
188
|
+
f.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
|
189
|
+
f.headers["X-Requested-With"] = "XMLHttpRequest"
|
|
190
|
+
end.post("/catalog/catalogapi.php", form_data)
|
|
191
|
+
end
|
|
157
192
|
end
|
|
158
193
|
end
|
|
159
194
|
end
|
|
@@ -4,22 +4,15 @@ module RockautoApi
|
|
|
4
4
|
module Endpoints
|
|
5
5
|
module PartCategories
|
|
6
6
|
def get_part_categories(make, year, model, carcode)
|
|
7
|
-
payload =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"fetching" => true,
|
|
17
|
-
"max_group_index" => 0,
|
|
18
|
-
"mkt_US" => true,
|
|
19
|
-
"mkt_CA" => false,
|
|
20
|
-
"mkt_MX" => false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
7
|
+
payload = navnode_fetch_payload(
|
|
8
|
+
make: make,
|
|
9
|
+
nodetype: "model",
|
|
10
|
+
label: model,
|
|
11
|
+
href: "#{Client::BASE_URL}/en/catalog/#{make.downcase},#{year},#{model.downcase}",
|
|
12
|
+
year: year,
|
|
13
|
+
model: model,
|
|
14
|
+
carcode: carcode
|
|
15
|
+
)
|
|
23
16
|
|
|
24
17
|
response = call_catalog_api("navnode_fetch", payload)
|
|
25
18
|
html = response.dig("html_fill_sections", "navchildren[]") || ""
|
|
@@ -62,24 +62,51 @@ module RockautoApi
|
|
|
62
62
|
type_value = match&.value || ""
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
init_session!
|
|
66
|
+
|
|
67
|
+
page_resp = @conn.get("/en/partsearch/")
|
|
68
|
+
nck = Parsers::HtmlHelpers.extract_csrf_token(page_resp.body)
|
|
69
|
+
|
|
65
70
|
form_data = {
|
|
71
|
+
"_nck" => nck || "",
|
|
72
|
+
"_jnck" => @jnck_token || "",
|
|
66
73
|
"dopartsearch" => "1",
|
|
67
74
|
"partsearch[partnum][partsearch_007]" => part_number,
|
|
68
75
|
"partsearch[manufacturer][partsearch_007]" => man_value,
|
|
69
76
|
"partsearch[partgroup][partsearch_007]" => group_value,
|
|
70
77
|
"partsearch[parttype][partsearch_007]" => type_value,
|
|
71
78
|
"partsearch[partname][partsearch_007]" => part_name || "",
|
|
72
|
-
"partsearch[do][partsearch_007]" => "Search"
|
|
79
|
+
"partsearch[do][partsearch_007]" => "Search",
|
|
80
|
+
"func" => "sendparttabsearch",
|
|
81
|
+
"payload" => "{}",
|
|
82
|
+
"api_json_request" => "1",
|
|
83
|
+
"sctchecked" => "1",
|
|
84
|
+
"scbeenloaded" => "false",
|
|
85
|
+
"curCartGroupID" => ""
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
resp = Faraday.new(url: "https://www.rockauto.com") do |f|
|
|
89
|
+
f.request :url_encoded
|
|
90
|
+
f.use :cookie_jar
|
|
91
|
+
f.adapter Faraday.default_adapter
|
|
92
|
+
f.options.timeout = RockautoApi.configuration&.request_timeout || 30
|
|
93
|
+
f.headers["X-Requested-With"] = "XMLHttpRequest"
|
|
94
|
+
f.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
|
95
|
+
f.headers["User-Agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"
|
|
96
|
+
f.headers["Referer"] = "https://www.rockauto.com/en/partsearch/"
|
|
97
|
+
@conn.headers["Cookie"].to_s.split(";").each do |cookie|
|
|
98
|
+
name, val = cookie.strip.split("=", 2)
|
|
99
|
+
f.headers["Cookie"] = "#{f.headers['Cookie']}; #{name}=#{val}" if name && val
|
|
100
|
+
end
|
|
101
|
+
end.post("catalog/catalogapi.php", form_data)
|
|
77
102
|
|
|
78
|
-
|
|
103
|
+
response = JSON.parse(resp.body)
|
|
104
|
+
|
|
105
|
+
parts = parse_part_search_json(response)
|
|
79
106
|
parts = parts.map do |p|
|
|
80
107
|
attrs = p.to_h
|
|
81
108
|
if include_fitments && attrs[:listing_data]
|
|
82
|
-
|
|
109
|
+
attrs[:buyers_guide] = get_fitment_for_part(attrs[:listing_data])
|
|
83
110
|
end
|
|
84
111
|
Models::PartInfo.new(**attrs)
|
|
85
112
|
end
|
|
@@ -91,6 +118,8 @@ module RockautoApi
|
|
|
91
118
|
manufacturer: manufacturer || "All",
|
|
92
119
|
part_group: part_group || "All"
|
|
93
120
|
)
|
|
121
|
+
rescue Faraday::Error => e
|
|
122
|
+
raise NetworkError, "Part search failed: #{e.message}"
|
|
94
123
|
end
|
|
95
124
|
|
|
96
125
|
def what_is_part_called(search_query)
|
|
@@ -146,6 +175,65 @@ module RockautoApi
|
|
|
146
175
|
data.empty? ? nil : data
|
|
147
176
|
end
|
|
148
177
|
|
|
178
|
+
def parse_part_search_json(response)
|
|
179
|
+
html = response["searchnoderesults"]
|
|
180
|
+
return [] unless html && !html.empty?
|
|
181
|
+
|
|
182
|
+
doc = Nokogiri::HTML(html)
|
|
183
|
+
parse_part_search_listings(doc)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parse_part_search_listings(doc)
|
|
187
|
+
doc.css(".listing-container-c").map { |container|
|
|
188
|
+
begin
|
|
189
|
+
next nil if container.at_css(".listing-final-partnumber").nil?
|
|
190
|
+
|
|
191
|
+
part_number = container.at_css(".listing-final-partnumber")&.text&.strip || "Unknown"
|
|
192
|
+
brand = container.at_css(".listing-final-manufacturer")&.text&.strip
|
|
193
|
+
price_el = container.at_css(".listing-price")
|
|
194
|
+
price = price_el&.text&.strip
|
|
195
|
+
image_elem = container.at_css("img.listing-inline-image") || container.at_css("img")
|
|
196
|
+
image_url = Parsers::HtmlHelpers.make_absolute_url(image_elem["src"]) if image_elem&.attr("src")
|
|
197
|
+
info_link = container.at_css("a[href*='moreinfo']")
|
|
198
|
+
info_url = Parsers::HtmlHelpers.make_absolute_url(info_link["href"]) if info_link&.attr("href")
|
|
199
|
+
name_text = container.at_css(".listing-text-row b")&.text&.strip
|
|
200
|
+
name = name_text || "#{brand} #{part_number}"
|
|
201
|
+
category_elem = container.at_css(".listing-footnote-text")
|
|
202
|
+
category = category_elem&.text&.strip
|
|
203
|
+
|
|
204
|
+
supplement_input = container.at_css("input[name='listing_data_supplemental'], input[id^='listing_data_supplemental']")
|
|
205
|
+
essential_input = container.at_css("input[name^='listing_data_essential'], input[id^='listing_data_essential']")
|
|
206
|
+
listing_data = nil
|
|
207
|
+
if supplement_input || essential_input
|
|
208
|
+
listing_data = {}
|
|
209
|
+
if essential_input
|
|
210
|
+
ess = JSON.parse(essential_input["value"] || "{}") rescue {}
|
|
211
|
+
listing_data["groupindex"] = ess["groupindex"].to_s if ess["groupindex"]
|
|
212
|
+
listing_data["car"] = { "carcode" => ess["carcode"], "parttype" => ess["parttype"], "partkey" => ess["partkey"] }
|
|
213
|
+
end
|
|
214
|
+
if supplement_input
|
|
215
|
+
supp = JSON.parse(supplement_input["value"] || "{}") rescue {}
|
|
216
|
+
listing_data["supplemental"] = { "partnumber" => supp["partnumber"], "catalogname" => supp["catalogname"] }
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
Models::PartInfo.new(
|
|
221
|
+
name: name,
|
|
222
|
+
part_number: part_number,
|
|
223
|
+
brand: brand,
|
|
224
|
+
price: price,
|
|
225
|
+
url: nil,
|
|
226
|
+
image_url: image_url,
|
|
227
|
+
info_url: info_url,
|
|
228
|
+
category: category,
|
|
229
|
+
listing_data: listing_data
|
|
230
|
+
)
|
|
231
|
+
rescue StandardError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
}.compact
|
|
235
|
+
end
|
|
236
|
+
|
|
149
237
|
def parse_what_is_called_results(doc)
|
|
150
238
|
doc.css("a").map { |a|
|
|
151
239
|
text = a.text.strip
|
|
@@ -15,6 +15,7 @@ module RockautoApi
|
|
|
15
15
|
attribute? :specifications, Types::String.optional
|
|
16
16
|
attribute? :compatibility_notes, Types::String.optional
|
|
17
17
|
attribute? :listing_data, Types::Hash
|
|
18
|
+
attribute? :buyers_guide, Types.Instance(RockautoApi::Models::BuyersGuideResult)
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
class PartSearchResult < Dry::Struct
|
|
@@ -14,12 +14,21 @@ module RockautoApi
|
|
|
14
14
|
next if cells.first.match?(/\A\s*(?:Year|Make|Model)\s*\z/i)
|
|
15
15
|
|
|
16
16
|
year = cells[0].to_i
|
|
17
|
+
if year.zero?
|
|
18
|
+
year = cells[2].to_i
|
|
19
|
+
make = cells[0] || "Unknown"
|
|
20
|
+
model = cells[1] || "Unknown"
|
|
21
|
+
else
|
|
22
|
+
make = cells[1] || "Unknown"
|
|
23
|
+
model = cells[2] || "Unknown"
|
|
24
|
+
end
|
|
25
|
+
|
|
17
26
|
next if year.zero?
|
|
18
27
|
|
|
19
28
|
fitments << Models::FitmentInfo.new(
|
|
20
29
|
year: year,
|
|
21
|
-
make:
|
|
22
|
-
model:
|
|
30
|
+
make: make,
|
|
31
|
+
model: model,
|
|
23
32
|
engine: cells[3],
|
|
24
33
|
transmission: cells[4],
|
|
25
34
|
drivetrain: cells[5],
|
data/lib/rockauto_api/version.rb
CHANGED
data/lib/rockauto_api.rb
CHANGED
|
@@ -13,8 +13,8 @@ require_relative "rockauto_api/configuration"
|
|
|
13
13
|
require_relative "rockauto_api/errors"
|
|
14
14
|
require_relative "rockauto_api/cache"
|
|
15
15
|
require_relative "rockauto_api/models/vehicle"
|
|
16
|
-
require_relative "rockauto_api/models/part"
|
|
17
16
|
require_relative "rockauto_api/models/fitment"
|
|
17
|
+
require_relative "rockauto_api/models/part"
|
|
18
18
|
require_relative "rockauto_api/models/order"
|
|
19
19
|
require_relative "rockauto_api/models/account"
|
|
20
20
|
require_relative "rockauto_api/models/tool"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rockauto_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben D'Angelo
|
|
@@ -141,6 +141,7 @@ executables: []
|
|
|
141
141
|
extensions: []
|
|
142
142
|
extra_rdoc_files: []
|
|
143
143
|
files:
|
|
144
|
+
- README.md
|
|
144
145
|
- lib/rockauto_api.rb
|
|
145
146
|
- lib/rockauto_api/cache.rb
|
|
146
147
|
- lib/rockauto_api/client.rb
|