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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eff8195831ca08f029eb1128c3fbdc9a8bc5c6441a41d3eba61b2279add15d46
4
- data.tar.gz: e961fb95855b0e0d7433ae5c9122ae3a755763e4a5ad99ce22bc0966759ebd9b
3
+ metadata.gz: 2d861f195fe9c02dc5737f5756f0c3e7248ddad192a380913dd9e95a65d327b8
4
+ data.tar.gz: 93ef1dddb194b74a1d091039460b3586466f1e225bba2289b0da43cfe5253e02
5
5
  SHA512:
6
- metadata.gz: e5b8f220ea632303e38d8a34a9ef4bddc5481b2d2c618bc73de1bd286af05cfe8e6ad0fddf621c311cb175b38735a1bb7f04ba0edb3a192a015dbc574622a8d4
7
- data.tar.gz: '078556f85c37110c00410c72602a46757c6e8d0fb344f74f1f3d8c14d3e5f0ad6a6870715d6ced9e3c060fe56d6dae593d41460b921aec8e3e64e8362d36ac2c'
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.
@@ -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
- "jsn" => {
130
- "make" => make,
131
- "nodetype" => "make",
132
- "loaded" => false,
133
- "expand_after_load" => true,
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
- payload = {
12
- "jsn" => {
13
- "email" => email,
14
- "password" => password,
15
- "keep_me_logged_in" => false
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
- response = call_catalog_api("login", payload)
20
- @authenticated = response["success"] == true || response.dig("response", "success") == true
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 NetworkError
31
+ rescue StandardError
23
32
  @authenticated = false
24
33
  end
25
34
 
26
35
  def logout
27
- response = call_catalog_api("logout", {})
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
- "jsn" => {
9
- "make" => make,
10
- "year" => year.to_s,
11
- "model" => model,
12
- "carcode" => carcode,
13
- "nodetype" => "model",
14
- "loaded" => false,
15
- "expand_after_load" => true,
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
- html = post_with_csrf("/en/partsearch/", form_data)
76
- doc = Nokogiri::HTML(html)
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
- parts = parse_part_search_results(doc)
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
- fitment_result = get_fitment_for_part(attrs[:listing_data])
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: cells[1] || "Unknown",
22
- model: cells[2] || "Unknown",
30
+ make: make,
31
+ model: model,
23
32
  engine: cells[3],
24
33
  transmission: cells[4],
25
34
  drivetrain: cells[5],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RockautoApi
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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