├── package.json ├── util ├── description.ts ├── dealerCategory.ts ├── zap.ts ├── twilio.ts ├── getCheckCode.ts └── vins.ts ├── fetchDealer.ts ├── README.md ├── fetchSingleVin.ts ├── .gitignore ├── tsconfig.json └── yarn.lock /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rav4scraper", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "license": "MIT", 6 | "dependencies": { 7 | "dotenv": "^10.0.0", 8 | "node-fetch": "^2.6.6", 9 | "ts-node": "^10.4.0", 10 | "twilio": "^3.72.0", 11 | "typescript": "^4.5.2" 12 | }, 13 | "devDependencies": { 14 | "@types/node-fetch": "2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /util/description.ts: -------------------------------------------------------------------------------- 1 | import { getCategoryDescription } from "./dealerCategory"; 2 | import { oregonDealers } from "./dealers"; 3 | import { getTrim } from "./vins"; 4 | 5 | export function getDescription(data: any) { 6 | const res = [ 7 | new Date(), 8 | data.vin, 9 | data.extColor.marketingName, 10 | data.price.totalMsrp, 11 | data.dealerCd, 12 | oregonDealers[data.dealerCd], 13 | data.holdStatus, 14 | data.eta?.currFromDate, 15 | data.eta?.currToDate, 16 | getCategoryDescription(data.dealerCategory), 17 | getTrim(data.vin), 18 | ]; 19 | return `${res},\n`; 20 | } 21 | -------------------------------------------------------------------------------- /util/dealerCategory.ts: -------------------------------------------------------------------------------- 1 | // https://www.reddit.com/r/rav4club/comments/ret484/question_about_updates_i_can_see_on_my_new_rav4/hoeaq6j/?utm_source=reddit&utm_medium=web2x&context=3 2 | // https://www.reddit.com/r/rav4prime/comments/k2i669/certified_toyota_sales_consultant/gdvol4h/?utm_source=reddit&utm_medium=web2x&context=3 3 | // https://www.rav4world.com/threads/roro-ships-toyohashi-port-to-north-america.313558/page-20#post-2899472 4 | 5 | const map: Record = { 6 | F: "freight", 7 | A: "allocated", 8 | G: "ground", 9 | }; 10 | export function getCategoryDescription(category?: string): string { 11 | return `${category || ""} (${map[category] || ("unknown" as string)})`; 12 | } 13 | -------------------------------------------------------------------------------- /util/zap.ts: -------------------------------------------------------------------------------- 1 | const ZAP_URL = process.env.ZAP_URL; 2 | 3 | async function sendZap(line: string) { 4 | const fields = line.split(","); 5 | console.log(fields); 6 | const data = { 7 | DateFound: fields[0], 8 | VIN: `https://guest.dealer.toyota.com/v-spec/${fields[1]}/detail`, 9 | Color: fields[2], 10 | MSRP: fields[3], 11 | DealerCode: fields[4], 12 | Dealer: fields[5], 13 | Status: fields[6], 14 | ETAStart: fields[7], 15 | ETAEnd: fields[8], 16 | }; 17 | const res = await fetch(ZAP_URL!, { 18 | headers: { 19 | Accept: "application/json", 20 | "Content-Type": "application/json", 21 | }, 22 | method: "POST", 23 | body: JSON.stringify(data), 24 | }); 25 | console.log(await res.json()); 26 | } 27 | -------------------------------------------------------------------------------- /util/twilio.ts: -------------------------------------------------------------------------------- 1 | import Twilio from "twilio"; 2 | const accountSid = process.env.TWILIO_ACCOUNT_SID; 3 | const token = process.env.TWILIO_AUTH_TOKEN; 4 | const fromNumber = process.env.TWILIO_FROM_NUMBER; 5 | 6 | var twilio = Twilio(accountSid, token, { 7 | region: "us1", 8 | edge: "umatilla", 9 | }); 10 | 11 | export async function send(body: string, toNumber: string) { 12 | console.log(`${toNumber}: ${body}`); 13 | const res = await twilio.messages.create({ 14 | from: fromNumber, 15 | to: toNumber, 16 | body, 17 | }); 18 | console.log(`Message sent. ${res.sid}`); 19 | } 20 | 21 | export async function sendMessage(body: string) { 22 | await Promise.all( 23 | [""].map(async (num) => await send(body, num)) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /fetchDealer.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | 3 | async function fetchCode(code: string) { 4 | try { 5 | const res = await fetch(`https://www.toyota.com/dealers/dealer/${code}`); 6 | 7 | if (res.status === 200) { 8 | const f = await res.json(); 9 | console.log(f); 10 | 11 | return f; 12 | } else if (res.status !== 404) { 13 | console.log(res); 14 | console.log("received status: ", res.status); 15 | } 16 | } catch (e) { 17 | console.log(e); 18 | } 19 | } 20 | 21 | async function run(code: string) { 22 | const res = await fetchCode(code); 23 | console.log(res); 24 | } 25 | 26 | run(process.argv[2]); 27 | 28 | // https://www.reddit.com/r/rav4prime/comments/j4k0v9/rav4_prime_spreadsheet/ggxu3zc/?utm_source=reddit&utm_medium=web2x&context=3 29 | -------------------------------------------------------------------------------- /util/getCheckCode.ts: -------------------------------------------------------------------------------- 1 | const values = [ 2 | 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 0, 7, 0, 9, 2, 3, 4, 5, 6, 7, 8, 9, 3 | ]; 4 | const weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; 5 | 6 | export function getCheckCode(vin: string) { 7 | if (vin.length !== 17) { 8 | throw new Error("invalid length"); 9 | } 10 | if (vin[7]! == "-") { 11 | throw new Error('Missing "-" at position 7'); 12 | } 13 | 14 | let sum = 0; 15 | let c; 16 | let val = 0; 17 | for (let i = 0; i < 17; i++) { 18 | c = vin[i]; 19 | if (c >= "A" && c <= "Z") { 20 | val = values[c.charCodeAt(0) - "A".charCodeAt(0)]; 21 | } else if (c >= "0" && c <= "9") { 22 | val = c.charCodeAt(0) - "0".charCodeAt(0); 23 | } else if (c !== "-") { 24 | throw new Error(`bad char: ${c}`); 25 | } 26 | sum = sum + weights[i] * val; 27 | } 28 | sum = sum % 11; 29 | 30 | if (sum == 10) return "X"; 31 | else return "" + sum; 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A collection of utils for working with the Toyota API in search of a Rav4 Prime. 2 | 3 | The scraping part is left as an exercise to the reader, but here is the basic algorithm: 4 | 5 | 1. Iterate through each number in the range of current vins + some buffer for new ones. 6 | 2. For each number, build all possible prime vins using `allVins` in `uitl/vins`. 7 | 3. Fetch each vin using `fetchVin` in `uitl/vins`. If it does not 404, it exists and is allocated. 8 | 4. To tell if it's a new vin, write out the description from `util/description` for each vin seen to a file as you go. Read the same file on subsequent runs, and you can check if it's new by virture of the vin not being present in the file (just a simple `fileContents.includes(vin)`) 9 | 10 | Prior art and reference: 11 | 12 | - https://docs.google.com/spreadsheets/d/1-_tNaeb4kP87UZSR4JdKxzCjF_OxIfBvTWFGc4-OwqA/edit#gid=1148882028 13 | - https://docs.google.com/spreadsheets/d/1tgQXcM27dEnbUzm-oe1BNzmjlV2lSguLBdf5Zr4MYUw/edit#gid=888114205 14 | - https://www.reddit.com/r/rav4prime/comments/j4k0v9/rav4_prime_spreadsheet/ggxu3zc/?utm_source=reddit&utm_medium=web2x&context=3 15 | - https://www.reddit.com/r/rav4prime/comments/j1ia2t/rav4_prime_xse_premium_75008000/g709wtd/ 16 | 17 | ## Setup 18 | 19 | ``` 20 | yarn install 21 | ``` 22 | 23 | ## Running scripts 24 | 25 | ``` 26 | yarn ts-node --transpile-only