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..d04352d6fe674d
--- /dev/null
+++ b/lib/routes/ups/track.ts
@@ -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}
+ Location: ${location}
+ Date and Time: ${dateTimeStr}
+ `,
+ pubDate,
+ };
+
+ return item;
+ });
+
+ return {
+ title: `UPS Tracking - ${trackingNumber}`,
+ link: url,
+ item: items,
+ };
+}