Skip to content

Commit 72c627d

Browse files
committed
Add scraper functionality for cafes on the API and static eateries
1 parent 06990da commit 72c627d

2 files changed

Lines changed: 65 additions & 33 deletions

File tree

prisma/scraper.ts

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import cron from 'node-cron';
44
import { join } from 'path';
55

66
import { PrismaClient } from '@prisma/client';
7-
import type { CampusArea, EateryType, PaymentMethod } from '@prisma/client';
7+
import {
8+
type CampusArea,
9+
EateryType,
10+
type PaymentMethod,
11+
} from '@prisma/client';
812

913
import { DEFAULT_IMAGE_URL } from '../src/constants.js';
1014
import {
@@ -18,6 +22,7 @@ import {
1822
import type {
1923
RawDiningItem,
2024
RawEatery,
25+
RawOperatingHourEventMenuCategory,
2126
RawScrapedData,
2227
RawStaticEatery,
2328
} from './scraperTypes.js';
@@ -98,7 +103,7 @@ async function fetchFreedgeDiningItems(): Promise<RawDiningItem[]> {
98103
{ signal: AbortSignal.timeout(10000) },
99104
);
100105

101-
if (response.status > 400) {
106+
if (response.status >= 400) {
102107
console.log(`Failed to fetch freedge data: HTTP ${response.status}`);
103108
return [];
104109
}
@@ -147,6 +152,28 @@ async function fetchFreedgeDiningItems(): Promise<RawDiningItem[]> {
147152
}
148153
}
149154

155+
function fetchCafeMenu(
156+
diningItems: RawDiningItem[],
157+
): RawOperatingHourEventMenuCategory[] {
158+
const grouped: Record<string, RawDiningItem[]> = {};
159+
160+
for (const item of diningItems) {
161+
if (!grouped[item.category]) {
162+
grouped[item.category] = [];
163+
}
164+
165+
grouped[item.category].push(item);
166+
}
167+
168+
return Object.entries(grouped).map(([category, items]) => ({
169+
category,
170+
items: items.map((item) => ({
171+
item: item.item,
172+
healthy: item.healthy,
173+
})),
174+
}));
175+
}
176+
150177
function extractAnnouncements(announcements?: { title: string }[]): string[] {
151178
if (!Array.isArray(announcements)) return [];
152179

@@ -162,11 +189,9 @@ function transformStaticEatery(rawStaticEatery: RawStaticEatery) {
162189
endTimestamp: Date;
163190
menu: Array<{
164191
category: string;
165-
sortIdx: number;
166192
items: Array<{
167193
item: string;
168194
healthy: boolean;
169-
sortIdx: number;
170195
}>;
171196
}>;
172197
}> = [];
@@ -184,7 +209,7 @@ function transformStaticEatery(rawStaticEatery: RawStaticEatery) {
184209
type: event.descr || 'General',
185210
startTimestamp: startDate,
186211
endTimestamp: endDate,
187-
menu: event.menu || [],
212+
menu: fetchCafeMenu(rawStaticEatery.diningItems) || [],
188213
});
189214
}
190215
}
@@ -228,6 +253,7 @@ function transformStaticEatery(rawStaticEatery: RawStaticEatery) {
228253
new Set(rawStaticEatery.payMethods.map(mapPaymentMethod)),
229254
),
230255
eateryTypes: rawStaticEatery.eateryTypes.map(mapEateryType),
256+
diningItems: rawStaticEatery.diningItems,
231257
announcements: extractAnnouncements(rawStaticEatery.announcements),
232258
},
233259
events,
@@ -249,7 +275,7 @@ function transformEatery(rawEatery: RawEatery) {
249275
about: rawEatery.about,
250276
shortAbout: rawEatery.aboutshort,
251277
cornellDining: rawEatery.cornellDining,
252-
menuSummary: rawEatery.opHoursCalcDescr,
278+
menuSummary: 'Cornell Eatery',
253279
imageUrl: imageUrl,
254280
campusArea: mapCampusArea(rawEatery.campusArea),
255281
onlineOrderUrl: rawEatery.onlineOrderUrl,
@@ -258,6 +284,7 @@ function transformEatery(rawEatery: RawEatery) {
258284
latitude: rawEatery.latitude,
259285
longitude: rawEatery.longitude,
260286
location: rawEatery.location,
287+
diningItems: rawEatery.diningItems,
261288
paymentMethods: Array.from(
262289
new Set(rawEatery.payMethods.map(mapPaymentMethod)),
263290
),
@@ -271,31 +298,40 @@ function transformEatery(rawEatery: RawEatery) {
271298
endTimestamp: Date;
272299
menu: Array<{
273300
category: string;
274-
sortIdx: number;
275301
items: Array<{
276302
item: string;
277303
healthy: boolean;
278-
sortIdx: number;
279304
}>;
280305
}>;
281306
}> = [];
282307

308+
let cafeDiningItems: RawOperatingHourEventMenuCategory[] = [];
309+
if (eateryData.eateryTypes.includes(EateryType.CAFE)) {
310+
cafeDiningItems = fetchCafeMenu(eateryData.diningItems);
311+
}
283312
for (const operatingHour of rawEatery.operatingHours) {
284313
for (const event of operatingHour.events) {
285-
events.push({
286-
type: event.descr,
287-
startTimestamp: new Date(event.startTimestamp * 1000),
288-
endTimestamp: new Date(event.endTimestamp * 1000),
289-
menu: event.menu.map((cat) => ({
290-
category: cat.category,
291-
sortIdx: cat.sortIdx,
292-
items: cat.items.map((item) => ({
293-
item: item.item,
294-
healthy: item.healthy,
295-
sortIdx: item.sortIdx,
314+
if (eateryData.eateryTypes.includes(EateryType.CAFE)) {
315+
events.push({
316+
type: event.descr,
317+
startTimestamp: new Date(event.startTimestamp * 1000),
318+
endTimestamp: new Date(event.endTimestamp * 1000),
319+
menu: cafeDiningItems,
320+
});
321+
} else {
322+
events.push({
323+
type: event.descr,
324+
startTimestamp: new Date(event.startTimestamp * 1000),
325+
endTimestamp: new Date(event.endTimestamp * 1000),
326+
menu: event.menu.map((cat) => ({
327+
category: cat.category,
328+
items: cat.items.map((item) => ({
329+
item: item.item,
330+
healthy: item.healthy,
331+
})),
296332
})),
297-
})),
298-
});
333+
});
334+
}
299335
}
300336
}
301337

@@ -372,6 +408,7 @@ async function processAllEateries(
372408
location: string;
373409
paymentMethods: PaymentMethod[];
374410
eateryTypes: EateryType[];
411+
diningItems: RawDiningItem[];
375412
announcements: string[];
376413
};
377414
events: Array<{
@@ -380,11 +417,9 @@ async function processAllEateries(
380417
endTimestamp: Date;
381418
menu: Array<{
382419
category: string;
383-
sortIdx: number;
384420
items: Array<{
385421
item: string;
386422
healthy: boolean;
387-
sortIdx: number;
388423
}>;
389424
}>;
390425
}>;
@@ -402,10 +437,11 @@ async function processAllEateries(
402437
const batch = transformedEateries.slice(i, i + BATCH_SIZE);
403438

404439
await Promise.all(
405-
batch.map(({ eatery, events }) =>
406-
tx.eatery.create({
440+
batch.map(({ eatery, events }) => {
441+
const { diningItems, ...eaterySanitized } = eatery;
442+
return tx.eatery.create({
407443
data: {
408-
...eatery,
444+
...eaterySanitized,
409445
events: {
410446
create: events.map((rawEvent) => ({
411447
type: mapEventType(rawEvent.type),
@@ -424,8 +460,8 @@ async function processAllEateries(
424460
})),
425461
},
426462
},
427-
}),
428-
),
463+
});
464+
}),
429465
);
430466
}
431467
},
@@ -703,7 +739,7 @@ function startScraperScheduler() {
703739
const cronExpression = process.env.SCRAPER_CRON_SCHEDULE || '0 7,19 * * *';
704740

705741
console.log('[Scheduler] Initializing scraper scheduler...');
706-
console.log('[Scheduler] Schedule: Daily at 7:00 AM and 7:00 PM');
742+
console.log(`[Scheduler] Schedule: ${cronExpression} (America/New_York)`);
707743

708744
const task = cron.schedule(cronExpression, runScraperSafely, {
709745
timezone: 'America/New_York',

prisma/scraperTypes.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,10 @@ export type RawCoordinates = {
3535
export type RawOperatingHourEventMenuItem = {
3636
item: string;
3737
healthy: boolean;
38-
sortIdx: number;
3938
};
4039

4140
export type RawOperatingHourEventMenuCategory = {
4241
category: string;
43-
sortIdx: number;
4442
items: RawOperatingHourEventMenuItem[];
4543
};
4644

@@ -63,12 +61,10 @@ export type RawOperatingHour = {
6361
export type RawStaticOperatingHourEventMenuItem = {
6462
item: string;
6563
healthy: boolean;
66-
sortIdx: number;
6764
};
6865

6966
export type RawStaticOperatingHourEventMenuCategory = {
7067
category: string;
71-
sortIdx: number;
7268
items: RawStaticOperatingHourEventMenuItem[];
7369
};
7470

0 commit comments

Comments
 (0)