From d8efe7b4c307bbcf32c6244d934b4d32019e570d Mon Sep 17 00:00:00 2001 From: Andvari <31068367+dzx-dzx@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:17:12 +0800 Subject: [PATCH] feat(apnews): Mobile API (#18819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(apnews): Mobile API * . * 更新 mobile-api.ts * 更新 mobile-api.ts * . --- lib/routes/apnews/api.ts | 77 --------------------------- lib/routes/apnews/mobile-api.ts | 93 +++++++++++++++++++++++++++++++++ lib/routes/apnews/utils.ts | 4 +- 3 files changed, 95 insertions(+), 79 deletions(-) delete mode 100644 lib/routes/apnews/api.ts create mode 100644 lib/routes/apnews/mobile-api.ts diff --git a/lib/routes/apnews/api.ts b/lib/routes/apnews/api.ts deleted file mode 100644 index f0edc8582..000000000 --- a/lib/routes/apnews/api.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Route, ViewType } from '@/types'; -import { fetchArticle } from './utils'; -import ofetch from '@/utils/ofetch'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/api/:tags?', - categories: ['traditional-media', 'popular'], - example: '/apnews/api/apf-topnews', - view: ViewType.Articles, - parameters: { - tags: { - description: 'Getting a list of articles from a public API based on tags.', - options: [ - { value: 'apf-topnews', label: 'Top News' }, - { value: 'apf-sports', label: 'Sports' }, - { value: 'apf-politics', label: 'Politics' }, - { value: 'apf-entertainment', label: 'Entertainment' }, - { value: 'apf-usnews', label: 'US News' }, - { value: 'apf-oddities', label: 'Oddities' }, - { value: 'apf-Travel', label: 'Travel' }, - { value: 'apf-technology', label: 'Technology' }, - { value: 'apf-lifestyle', label: 'Lifestyle' }, - { value: 'apf-business', label: 'Business' }, - { value: 'apf-Health', label: 'Health' }, - { value: 'apf-science', label: 'Science' }, - { value: 'apf-intlnews', label: 'International News' }, - ], - default: 'apf-topnews', - }, - }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['apnews.com/'], - }, - ], - name: 'News', - maintainers: ['dzx-dzx'], - handler, -}; - -async function handler(ctx) { - const { tags = 'apf-topnews' } = ctx.req.param(); - const apiRootUrl = 'https://afs-prod.appspot.com/api/v2/feed/tag'; - const url = `${apiRootUrl}?tags=${tags}`; - const res = await ofetch(url); - - const list = res.cards - .map((e) => ({ - title: e.contents[0]?.headline, - link: e.contents[0]?.localLinkUrl, - pubDate: timezone(parseDate(e.publishedDate), 0), - category: e.tagObjs.map((tag) => tag.name), - updated: timezone(parseDate(e.contents[0]?.updated), 0), - description: e.contents[0]?.storyHTML, - author: e.contents[0]?.reporters.map((author) => ({ name: author.displayName })), - })) - .sort((a, b) => b.pubDate - a.pubDate) - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); - - const items = ctx.req.query('fulltext') === 'true' ? await Promise.all(list.map((item) => fetchArticle(item))) : list; - - return { - title: `${res.tagObjs[0].name} - AP News`, - item: items, - link: 'https://apnews.com', - }; -} diff --git a/lib/routes/apnews/mobile-api.ts b/lib/routes/apnews/mobile-api.ts new file mode 100644 index 000000000..078688595 --- /dev/null +++ b/lib/routes/apnews/mobile-api.ts @@ -0,0 +1,93 @@ +import { Route, ViewType } from '@/types'; +import { asyncPoolAll, fetchArticle } from './utils'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/mobile/:path{.+}?', + categories: ['traditional-media'], + example: '/apnews/mobile/ap-top-news', + view: ViewType.Articles, + parameters: { + path: { + description: 'Corresponding path from AP News website', + default: 'ap-top-news', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['apnews.com/'], + }, + ], + name: 'News (from mobile client API)', + maintainers: ['dzx-dzx'], + handler, +}; + +async function handler(ctx) { + const path = ctx.req.param('path') ? `/${ctx.req.param('path')}` : '/hub/ap-top-news'; + const apiRootUrl = 'https://apnews.com/graphql/delivery/ap/v1'; + const res = await ofetch(apiRootUrl, { + query: { + operationName: 'ContentPageQuery', + variables: { path }, + extensions: { persistedQuery: { version: 1, sha256Hash: '3bc305abbf62e9e632403a74cc86dc1cba51156d2313f09b3779efec51fc3acb' } }, + }, + }); + + const screen = res.data.Screen; + + const list = [...screen.main.filter((e) => e.__typename === 'ColumnContainer').flatMap((_) => _.columns), ...screen.main.filter((e) => e.__typename !== 'ColumnContainer')] + .filter((e) => e.__typename !== 'GoogleDfPAdModule') + .flatMap((e) => { + switch (e.__typename) { + case 'PageListModule': + return e.items; + case 'VideoPlaylistModule': + return e.playlist; + default: + return; + } + }) + .filter(Boolean) + .map((e) => { + if (e.__typename === 'PagePromo') { + return { + title: e.title, + link: e.url, + pubDate: parseDate(e.publishDateStamp), + category: e.category, + description: e.description, + guid: e.id, + }; + } else if (e.__typename === 'VideoPlaylistItem') { + return { + title: e.title, + link: e.url, + description: e.description, + guid: e.contentId, + }; + } else { + return; + } + }) + .filter(Boolean) + .sort((a, b) => b.pubDate - a.pubDate) + .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); + + const items = ctx.req.query('fulltext') === 'true' ? await asyncPoolAll(10, list, (item) => fetchArticle(item)) : list; + + return { + title: screen.category ?? screen.title, + item: items, + link: 'https://apnews.com', + }; +} diff --git a/lib/routes/apnews/utils.ts b/lib/routes/apnews/utils.ts index 5db057510..ed833750c 100644 --- a/lib/routes/apnews/utils.ts +++ b/lib/routes/apnews/utils.ts @@ -38,6 +38,7 @@ export function fetchArticle(item) { $('div.Enhancement').remove(); const section = $("meta[property='article:section']").attr('content'); return { + ...item, title: ldjson.headline, pubDate: parseDate(ldjson.datePublished), updated: parseDate(ldjson.dateModified), @@ -45,7 +46,6 @@ export function fetchArticle(item) { category: [...(section ? [section] : []), ...(ldjson.keywords ?? [])], guid: $("meta[name='brightspot.contentId']").attr('content'), author: ldjson.author, - ...item, }; } else { // Live @@ -56,11 +56,11 @@ export function fetchArticle(item) { const pubDate = url.hash ? parseDate(Number.parseInt($(url.hash).parent().attr('data-posted-date-timestamp'), 10)) : parseDate(ldjson.coverageStartTime); return { + ...item, category: ldjson.keywords, pubDate, description, guid: $("meta[name='brightspot.contentId']").attr('content'), - ...item, }; } });