Skip to content

Commit

Permalink
feat(route): Add New Route for UPS Tracking (#17941)
Browse files Browse the repository at this point in the history
* New Routes for UPS Tracking

* New Routes for UPS Tracking

* trying to fix Vitest puppeteer on Node lts/-1

* Add tracking feature to UPS route

- Implemented tracking functionality in `lib/routes/ups/track.ts`

* fix: update tracking logic in UPS route

* Add guid to track request
  • Loading branch information
Aquabet authored Jan 5, 2025
1 parent 238955b commit 38ec24d
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 0 deletions.
12 changes: 12 additions & 0 deletions lib/routes/ups/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Namespace } from '@/types';

export const namespace: Namespace = {
name: 'UPS',
url: 'ups.com',
description: 'United Parcel Service (UPS) updates, news, and tracking RSS feeds.',

zh: {
name: 'UPS(联合包裹服务公司)',
description: '联合包裹服务公司(UPS)的更新、新闻和追踪 RSS 源。',
},
};
114 changes: 114 additions & 0 deletions lib/routes/ups/track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Route, DataItem } from '@/types';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import puppeteer from '@/utils/puppeteer';

export const route: Route = {
path: '/track/:trackingNumber',
categories: ['other'],
example: '/ups/track/1Z78R6790470567520',
parameters: { trackingNumber: 'The UPS tracking number (e.g., 1Z78R6790470567520).' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: 'Tracking',
maintainers: ['Aquabet'],
handler,
};

async function handler(ctx) {
const { trackingNumber } = ctx.req.param();
const url = `https://www.ups.com/track?loc=en_US&tracknum=${trackingNumber}`;

const browser = await puppeteer();
const page = await browser.newPage();

await page.setRequestInterception(true);

// skip loading images, stylesheets, and fonts
page.on('request', (request) => {
if (['image', 'stylesheet', 'font', 'ping', 'fetch'].includes(request.resourceType())) {
request.abort();
} else {
request.continue();
}
});

await page.goto(url, { waitUntil: 'domcontentloaded' });

const viewDetailsButton = '#st_App_View_Details';
try {
await page.waitForSelector(viewDetailsButton);
await page.click(viewDetailsButton);
} catch {
return {
title: `UPS Tracking - ${trackingNumber}`,
link: url,
item: [],
};
}

await page.waitForSelector('tr[id^="stApp_activitydetails_row"]');

const content = await page.content();
await browser.close();

const $ = load(content);

const rows = $('tr[id^="stApp_activitydetails_row"]');

const items: DataItem[] = rows.toArray().map((el, i) => {
const dateTimeRaw = $(el).find(`#stApp_activitiesdateTime${i}`).text() || 'Not Provided';

const dateTimeStr = dateTimeRaw
.trim()
.replace(/(\d{1,}\/\d{1,}\/\d{4})(\d{1,}:\d{1,}\s[AP]\.?M\.?)/, '$1 $2')
.replaceAll('P.M.', 'PM')
.replaceAll('A.M.', 'AM');

const pubDate = parseDate(dateTimeStr);

const activityCellText = $(el)
.find(`#stApp_milestoneActivityLocation${i}`)
.text()
.trim()
.replaceAll(/\s*\n+\s*/g, '\n');

const lines = activityCellText
.split('\n')
.map((l) => l.trim())
.filter(Boolean);

// Situation 0: There is text within the strong element
// Example: ["Delivered", "DELIVERED", "REDMOND, WA, United States"]
// Situation 1: strong is empty => the first line in lines is the status
// Example: ["Departed from Facility", "Seattle, WA, United States"]
const status = lines[0];
const location = lines.at(-1) || '';

const item: DataItem = {
title: status,
link: url,
guid: `${trackingNumber}-${i}`,
description: `
Status: ${status} <br>
Location: ${location} <br>
Date and Time: ${dateTimeStr}
`,
pubDate,
};

return item;
});

return {
title: `UPS Tracking - ${trackingNumber}`,
link: url,
item: items,
};
}

0 comments on commit 38ec24d

Please sign in to comment.