From b88daabdfc11da55c26e93c5015d9c42d2069bfc Mon Sep 17 00:00:00 2001 From: Aquabet Date: Thu, 19 Dec 2024 08:25:04 -0800 Subject: [PATCH 1/6] New Routes for UPS Tracking --- lib/routes/ups/namespace.ts | 12 ++++ lib/routes/ups/track.ts | 114 ++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 lib/routes/ups/namespace.ts create mode 100644 lib/routes/ups/track.ts diff --git a/lib/routes/ups/namespace.ts b/lib/routes/ups/namespace.ts new file mode 100644 index 00000000000000..21b8d8adef2ada --- /dev/null +++ b/lib/routes/ups/namespace.ts @@ -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 源。', + }, +}; diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts new file mode 100644 index 00000000000000..2a35fdc0582227 --- /dev/null +++ b/lib/routes/ups/track.ts @@ -0,0 +1,114 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { config } from '@/config'; +import puppeteer from '@/utils/puppeteer'; + +export const route: Route = { + path: '/track/:trackingNumber', + categories: ['other'], + example: '/ups/track/1Z78R1234567890123', + parameters: { trackingNumber: 'The UPS tracking number (e.g., 1Z78R1234567890123).' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['ups.com/track?loc=en_US&tracknum=:trackingNumber'], + target: '/ups/track/:trackingNumber', + }, + ], + name: 'UPS 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.setUserAgent(config.ua); + + await page.setExtraHTTPHeaders({ + 'Accept-Language': 'en-US,en;q=0.9', + }); + + await page.setRequestInterception(true); + + // skip loading images, stylesheets, and fonts + page.on('request', (request) => { + if (['image', 'stylesheet', 'font'].includes(request.resourceType())) { + request.abort(); + } else { + request.continue(); + } + }); + + await page.goto(url, { waitUntil: 'domcontentloaded' }); + + await page.waitForSelector('tr[id^="stApp_ShpmtProg_LVP_progress_row_"]', { + timeout: 30000, + }); + + const content = await page.content(); + browser.close(); + const $ = load(content); + + // Extract tracking events + const items = $('tr[id^="stApp_ShpmtProg_LVP_progress_row_"]') + .toArray() + .flatMap((el) => { + const $el = $(el); + + // Extract status, location, and datetime + const status = $el + .find(`td[id^="stApp_ShpmtProg_LVP_milestone_nameKey_"]`) + .contents() + .filter(function () { + return this.type === 'text' && this.data.trim() !== ''; + }) + .toArray() + .map((element) => element.data) + .join('') + .trim(); + + const location = $el.find(`span[id^="stApp_milestoneLocation"]`).text().trim(); + const dateTimeText = $el.find(`span[id^="stApp_milestoneDateTime"]`).text().trim(); + + if (!dateTimeText) { + return []; + } + + const cleanedDateTimeText = dateTimeText.normalize('NFKC').replaceAll(/\s+/g, ' ').replaceAll('A.M.', 'AM').replaceAll('P.M.', 'PM').trim(); + + // Separate date and time + const [date, time] = cleanedDateTimeText.split(', '); + const formattedDateTime = `${date} ${time}`; + + return { + title: `${status}: ${location}`, + link: url, + description: ` + Status: ${status}
+ Location: ${location}
+ Date and Time: ${date} ${time} + `.trim(), + pubDate: parseDate(formattedDateTime, 'MM/DD/YYYY h:mm A'), + }; + }); + + // Return RSS data + return { + title: `UPS Tracking - ${trackingNumber}`, + link: url, + item: items, + }; +} From 2274ea3df224926858137be6802dce6e6c4c2b62 Mon Sep 17 00:00:00 2001 From: Aquabet Date: Thu, 19 Dec 2024 08:37:41 -0800 Subject: [PATCH 2/6] New Routes for UPS Tracking --- lib/routes/ups/track.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts index 2a35fdc0582227..7547ca1485f6bf 100644 --- a/lib/routes/ups/track.ts +++ b/lib/routes/ups/track.ts @@ -7,8 +7,8 @@ import puppeteer from '@/utils/puppeteer'; export const route: Route = { path: '/track/:trackingNumber', categories: ['other'], - example: '/ups/track/1Z78R1234567890123', - parameters: { trackingNumber: 'The UPS tracking number (e.g., 1Z78R1234567890123).' }, + example: '/ups/track/1Z78R6790470567520', + parameters: { trackingNumber: 'The UPS tracking number (e.g., 1Z78R6790470567520).' }, features: { requireConfig: false, requirePuppeteer: false, From 612d9cd34b99ceb42759180c6e91657af34fa843 Mon Sep 17 00:00:00 2001 From: Aquabet Date: Thu, 19 Dec 2024 09:38:12 -0800 Subject: [PATCH 3/6] trying to fix Vitest puppeteer on Node lts/-1 --- lib/routes/ups/track.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts index 7547ca1485f6bf..c0bd81c12169e4 100644 --- a/lib/routes/ups/track.ts +++ b/lib/routes/ups/track.ts @@ -1,7 +1,6 @@ import { Route } from '@/types'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; -import { config } from '@/config'; import puppeteer from '@/utils/puppeteer'; export const route: Route = { @@ -35,12 +34,6 @@ async function handler(ctx) { const browser = await puppeteer(); const page = await browser.newPage(); - await page.setUserAgent(config.ua); - - await page.setExtraHTTPHeaders({ - 'Accept-Language': 'en-US,en;q=0.9', - }); - await page.setRequestInterception(true); // skip loading images, stylesheets, and fonts From 437d5247a811e437058a35a12d2b37c468084135 Mon Sep 17 00:00:00 2001 From: Aquabet Date: Sun, 22 Dec 2024 12:28:51 -0800 Subject: [PATCH 4/6] Add tracking feature to UPS route - Implemented tracking functionality in `lib/routes/ups/track.ts` --- lib/routes/ups/track.ts | 113 +++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts index c0bd81c12169e4..ca986f49505f15 100644 --- a/lib/routes/ups/track.ts +++ b/lib/routes/ups/track.ts @@ -16,13 +16,7 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, - radar: [ - { - source: ['ups.com/track?loc=en_US&tracknum=:trackingNumber'], - target: '/ups/track/:trackingNumber', - }, - ], - name: 'UPS Tracking', + name: 'Tracking', maintainers: ['Aquabet'], handler, }; @@ -38,7 +32,7 @@ async function handler(ctx) { // skip loading images, stylesheets, and fonts page.on('request', (request) => { - if (['image', 'stylesheet', 'font'].includes(request.resourceType())) { + if (['image', 'stylesheet', 'font', 'ping', 'fetch'].includes(request.resourceType())) { request.abort(); } else { request.continue(); @@ -47,58 +41,69 @@ async function handler(ctx) { await page.goto(url, { waitUntil: 'domcontentloaded' }); - await page.waitForSelector('tr[id^="stApp_ShpmtProg_LVP_progress_row_"]', { - timeout: 30000, - }); + 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(); - browser.close(); + await browser.close(); + const $ = load(content); - // Extract tracking events - const items = $('tr[id^="stApp_ShpmtProg_LVP_progress_row_"]') - .toArray() - .flatMap((el) => { - const $el = $(el); - - // Extract status, location, and datetime - const status = $el - .find(`td[id^="stApp_ShpmtProg_LVP_milestone_nameKey_"]`) - .contents() - .filter(function () { - return this.type === 'text' && this.data.trim() !== ''; - }) - .toArray() - .map((element) => element.data) - .join('') - .trim(); - - const location = $el.find(`span[id^="stApp_milestoneLocation"]`).text().trim(); - const dateTimeText = $el.find(`span[id^="stApp_milestoneDateTime"]`).text().trim(); - - if (!dateTimeText) { - return []; - } - - const cleanedDateTimeText = dateTimeText.normalize('NFKC').replaceAll(/\s+/g, ' ').replaceAll('A.M.', 'AM').replaceAll('P.M.', 'PM').trim(); - - // Separate date and time - const [date, time] = cleanedDateTimeText.split(', '); - const formattedDateTime = `${date} ${time}`; - - return { - title: `${status}: ${location}`, - link: url, - description: ` - Status: ${status}
- Location: ${location}
- Date and Time: ${date} ${time} - `.trim(), - pubDate: parseDate(formattedDateTime, 'MM/DD/YYYY h:mm A'), - }; + const rows = $('tr[id^="stApp_activitydetails_row"]'); + const items: { title: string; link: string; description: string; pubDate: Date }[] = []; + + rows.each((i, el) => { + 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') // add a space between date and time + .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) || ''; + + items.push({ + title: status, + link: url, + description: ` + Status: ${status}
+ Location: ${location}
+ Date and Time: ${dateTimeStr} + `, + pubDate, }); + }); - // Return RSS data return { title: `UPS Tracking - ${trackingNumber}`, link: url, From 66b15c680dbf80cb19eb772d852646d1964e25bf Mon Sep 17 00:00:00 2001 From: Aquabet Date: Sat, 4 Jan 2025 06:57:29 -0800 Subject: [PATCH 5/6] fix: update tracking logic in UPS route --- lib/routes/ups/track.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts index ca986f49505f15..b1b1035fbd9443 100644 --- a/lib/routes/ups/track.ts +++ b/lib/routes/ups/track.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, DataItem } from '@/types'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import puppeteer from '@/utils/puppeteer'; @@ -61,14 +61,13 @@ async function handler(ctx) { const $ = load(content); const rows = $('tr[id^="stApp_activitydetails_row"]'); - const items: { title: string; link: string; description: string; pubDate: Date }[] = []; - rows.each((i, el) => { + 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') // add a space between date and time + .replace(/(\d{1,}\/\d{1,}\/\d{4})(\d{1,}:\d{1,}\s[AP]\.?M\.?)/, '$1 $2') .replaceAll('P.M.', 'PM') .replaceAll('A.M.', 'AM'); @@ -92,7 +91,7 @@ async function handler(ctx) { const status = lines[0]; const location = lines.at(-1) || ''; - items.push({ + const item: DataItem = { title: status, link: url, description: ` @@ -101,7 +100,9 @@ async function handler(ctx) { Date and Time: ${dateTimeStr} `, pubDate, - }); + }; + + return item; }); return { From 8e92abc2a09b2f56a6e79d41dcc1adbf8ca9ca54 Mon Sep 17 00:00:00 2001 From: Aquabet Date: Sun, 5 Jan 2025 07:07:20 -0800 Subject: [PATCH 6/6] Add guid to track request --- lib/routes/ups/track.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/routes/ups/track.ts b/lib/routes/ups/track.ts index b1b1035fbd9443..d04352d6fe674d 100644 --- a/lib/routes/ups/track.ts +++ b/lib/routes/ups/track.ts @@ -94,6 +94,7 @@ async function handler(ctx) { const item: DataItem = { title: status, link: url, + guid: `${trackingNumber}-${i}`, description: ` Status: ${status}
Location: ${location}