-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.js
154 lines (142 loc) · 5.43 KB
/
search.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
const axios = require("axios");
const fs = require("fs");
// How often do you want to check for new inventory. Be good citizens and don't spam the API.
const POLLING_INTERVAL_MIN = 60;
// Where do you want the cache file writen to. Writes cache to file after each polling.
const CACHE_OUTPUT_FILE = ".tesla_used_inventory_cache";
// Search query. Set your desired search configuration here.
// There are likely other search filters that could be added here, but these are the ones I cared about.
// To find others, open the Network inspector tab in Chrome and select the options you want, grab the url
// that starts with 'inventory-results', then urldecode the query parameter to get this json.
const getSearchQuery = (outside) => {
return encodeURIComponent(
JSON.stringify({
query: {
model: "mx", // Options: ms, mx, my, m3
condition: "used",
options: {
TRIM: ["75D", "100DE", "P100D"],
AUTOPILOT: [
"AUTOPILOT_FULL_SELF_DRIVING", // If you want FSD capability. This does not guarantee HW3 is installed
],
CABIN_CONFIG: ["SIX"],
},
arrangeby: "Price",
order: "asc",
market: "US",
language: "en",
super_region: "north america",
lng: -104.7754307, // Not sure which is more important, lat/lng or zip/region, or a combination of the 4
lat: 39.866593,
zip: "80022",
range: 0,
region: "CO",
},
offset: 00,
count: 50,
outsideOffset: 0,
outsideSearch: outside, // This query is used twice, once for vehicles in your area, again for outside your area that can be shipped
})
);
};
// Built search urls for nearby and outside (aka the rest of the super region)
const nearbySearchUrl =
"https://www.tesla.com/inventory/api/v1/inventory-results?query=" +
getSearchQuery(false);
const outsideSearchUrl =
"https://www.tesla.com/inventory/api/v1/inventory-results?query=" +
getSearchQuery(true);
// Local memory storage for the vehicles we've come across
// Note: once vehicles are sold, they are not removed from this or cache. a lastSeen key was added to be able to clear old vehicles at a later date
let vehicles = {};
// Try reading from the cache file
try {
const fileData = fs.readFileSync(CACHE_OUTPUT_FILE, "utf8");
if (fileData) {
vehicles = JSON.parse(fileData);
}
} catch (e) {
console.log(
"Error reading cache file. A new file will be created for you now."
);
}
function parseVehicles(results) {
try {
results.forEach((result) => {
if (vehicles[result.VIN] == null) {
console.log(`Found new vehicle: ${result.Year} ${result.TrimName}`);
console.log(`\t https://www.tesla.com/used/${result.VIN}`);
console.log(`\t \$${result.InventoryPrice / 1000}k`);
console.log(`\t ${(result.Odometer / 1000).toFixed(1)}k miles`);
console.log(`\t Vehicle History: ${result.VehicleHistory}`);
console.log(`\t Inventory Reason: ${result.AddToInventoryReason}`);
console.log(
`\t has HW3 installed? ${
result.ManufacturingOptionCodeList.indexOf("APH4") != -1
? "yes"
: "no"
}\n`
);
vehicles[result.VIN] = {
price: result.InventoryPrice,
priceHistory: [result.InventoryPrice],
added: Date.now(),
odo: result.Odometer,
label: `${result.Year} ${result.Model.toUpperCase()} ${
result.TrimName
}`,
};
} else if (vehicles[result.VIN].price != result.InventoryPrice) {
console.log(`Price changed: ${result.Year} ${result.TrimName}`);
console.log(`\t https://www.tesla.com/used/${result.VIN}`);
console.log(`\t Was: \$${vehicles[result.VIN].price / 1000}k`);
console.log(`\t Now: \$${result.InventoryPrice / 1000}k\n`);
vehicles[result.VIN].price = result.InventoryPrice;
vehicles[result.VIN].priceHistory.push(result.InventoryPrice);
}
vehicles[result.VIN].lastSeen = Date.now();
});
} catch (e) {
// If no nearby matches, results will be an object instead of array and will throw an error trying to forEach it.
// I don't currently care about this situation so I just catch and swallow the error, but this could be extended
// to also show non-exact matches to the search if desired.
}
}
function getResults(url) {
return new Promise((resolve, reject) => {
axios
.get(url)
.then(function (response) {
const body = response.data;
const count = body.total_matches_found;
parseVehicles(body.results);
resolve(count);
})
.catch(function (error) {
console.log(error);
});
});
}
async function search() {
let nearbyResults = 0;
let outsideResults = 0;
await Promise.all([
getResults(nearbySearchUrl).then((results) => (nearbyResults = results)),
getResults(outsideSearchUrl).then((results) => (outsideResults = results)),
]);
console.log(
`${new Date().toISOString()}\tNearby matches: ${nearbyResults}\tDistant matches: ${outsideResults}`
);
// Store vehicles to cache file so we can keep accurate history in case the script restarts
fs.writeFile(CACHE_OUTPUT_FILE, JSON.stringify(vehicles), (err) => {
if (err) {
console.error("Error writing cache to file: " + err);
return;
}
});
}
// Start polling, then run search now
setInterval(() => {
search();
}, 1000 * 60 * POLLING_INTERVAL_MIN);
search();