diff --git a/lib/routes/aeon/category.ts b/lib/routes/aeon/category.ts index adb68f0a3e6fbc..7e4a87ecdec678 100644 --- a/lib/routes/aeon/category.ts +++ b/lib/routes/aeon/category.ts @@ -1,13 +1,24 @@ import { Route } from '@/types'; -import { load } from 'cheerio'; -import got from '@/utils/got'; -import { getData } from './utils'; +import ofetch from '@/utils/ofetch'; +import { getBuildId, getData } from './utils'; +import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/category/:category', categories: ['new-media', 'popular'], example: '/aeon/category/philosophy', - parameters: { category: 'Category' }, + parameters: { + category: { + description: 'Category', + options: [ + { value: 'philosophy', label: 'Philosophy' }, + { value: 'science', label: 'Science' }, + { value: 'psychology', label: 'Psychology' }, + { value: 'society', label: 'Society' }, + { value: 'culture', label: 'Culture' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -18,34 +29,40 @@ export const route: Route = { }, radar: [ { - source: ['aeon.aeon.co/:category'], + source: ['aeon.co/:category'], }, ], name: 'Categories', maintainers: ['emdoe'], handler, - description: `Supported categories: Philosophy, Science, Psychology, Society, and Culture.`, }; async function handler(ctx) { - const url = `https://aeon.co/${ctx.req.param('category')}`; - const { data: response } = await got(url); - const $ = load(response); + const category = ctx.req.param('category').toLowerCase(); + const url = `https://aeon.co/category/${category}`; + const buildId = await getBuildId(); + const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${category}.json`); - const data = JSON.parse($('script#__NEXT_DATA__').text()); + const section = response.pageProps.section; - const list = data.props.pageProps.section.articles.edges.map((item) => ({ - title: item.node.title, - author: item.node.authors.map((author) => author.displayName).join(', '), - link: `https://aeon.co/${item.node.type.toLowerCase()}s/${item.node.slug}`, + const list = section.articles.edges.map(({ node }) => ({ + title: node.title, + description: node.standfirstLong, + author: node.authors.map((author) => author.displayName).join(', '), + link: `https://aeon.co/${node.type}s/${node.slug}`, + pubDate: parseDate(node.createdAt), + category: [node.section.title, ...node.topics.map((topic) => topic.title)], + image: node.image.url, + type: node.type, + slug: node.slug, })); - const items = await getData(ctx, list); + const items = await getData(list); return { - title: `AEON | ${data.props.pageProps.section.title}`, + title: `AEON | ${section.title}`, link: url, - description: data.props.pageProps.section.metaDescription, + description: section.metaDescription, item: items, }; } diff --git a/lib/routes/aeon/templates/essay.art b/lib/routes/aeon/templates/essay.art index 5a2fab892134eb..8fed51cad667f4 100644 --- a/lib/routes/aeon/templates/essay.art +++ b/lib/routes/aeon/templates/essay.art @@ -1,3 +1,9 @@ - +{{ if banner.url }} +
+ {{ banner.alt }} + {{ if banner.caption }} +
{{ banner.caption }}
+ {{ /if }} +{{ /if }} {{@ authorsBio }} -{{@ content}} \ No newline at end of file +{{@ content }} diff --git a/lib/routes/aeon/templates/video.art b/lib/routes/aeon/templates/video.art index d1f546c3981a3f..c3d67356151f6b 100644 --- a/lib/routes/aeon/templates/video.art +++ b/lib/routes/aeon/templates/video.art @@ -1,10 +1,10 @@ {{ set video = article.hosterId }} {{ if article.hoster === 'vimeo' }} - {{ set video = "https://player.vimeo.com/video/" + video + "?dnt=1"}} -{{ else if article.hoster == 'youtube' }} + {{ set video = "https://player.vimeo.com/video/" + video + "?dnt=1" }} +{{ else if article.hoster === 'youtube' }} {{ set video = "https://www.youtube-nocookie.com/embed/" + video }} {{ /if }} -{{@ article.credits}} -{{@ article.description}} +{{@ article.credits }} +{{@ article.description }} diff --git a/lib/routes/aeon/type.ts b/lib/routes/aeon/type.ts index 8b78bfc4334d7e..2994f7f0909f48 100644 --- a/lib/routes/aeon/type.ts +++ b/lib/routes/aeon/type.ts @@ -1,13 +1,22 @@ import { Route } from '@/types'; -import { load } from 'cheerio'; -import got from '@/utils/got'; -import { getData } from './utils'; +import ofetch from '@/utils/ofetch'; +import { getBuildId, getData } from './utils'; +import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/:type', categories: ['new-media', 'popular'], example: '/aeon/essays', - parameters: { type: 'Type' }, + parameters: { + type: { + description: 'Type', + options: [ + { value: 'essays', label: 'Essays' }, + { value: 'videos', label: 'Videos' }, + { value: 'audio', label: 'Audio' }, + ], + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -18,7 +27,7 @@ export const route: Route = { }, radar: [ { - source: ['aeon.aeon.co/:type'], + source: ['aeon.co/:type'], }, ], name: 'Types', @@ -26,28 +35,30 @@ export const route: Route = { handler, description: `Supported types: Essays, Videos, and Audio. - Compared to the official one, the RSS feed generated by RSSHub not only has more fine-grained options, but also eliminates pull quotes, which can't be easily distinguished from other paragraphs by any RSS reader, but only disrupt the reading flow. This feed also provides users with a bio of the author at the top. - - However, The content generated under \`audio\` does not contain links to audio files.`, + Compared to the official one, the RSS feed generated by RSSHub not only has more fine-grained options, but also eliminates pull quotes, which can't be easily distinguished from other paragraphs by any RSS reader, but only disrupt the reading flow. This feed also provides users with a bio of the author at the top.`, }; async function handler(ctx) { const type = ctx.req.param('type'); - const binaryType = type === 'videos' ? 'videos' : 'essays'; const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1); + const buildId = await getBuildId(); const url = `https://aeon.co/${type}`; - const { data: response } = await got(url); - const $ = load(response); - - const data = JSON.parse($('script#__NEXT_DATA__').text()); + const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${type}.json`); - const list = data.props.pageProps.articles.map((item) => ({ - title: item.title, - link: `https://aeon.co/${binaryType}/${item.slug}`, + const list = response.pageProps.articles.map((node) => ({ + title: node.title, + description: node.standfirstLong, + author: node.authors.map((author) => author.displayName).join(', '), + link: `https://aeon.co/${node.type}s/${node.slug}`, + pubDate: parseDate(node.createdAt), + category: [node.section.title, ...node.topics.map((topic) => topic.title)], + image: node.image.url, + type: node.type, + slug: node.slug, })); - const items = await getData(ctx, list); + const items = await getData(list); return { title: `AEON | ${capitalizedType}`, diff --git a/lib/routes/aeon/utils.ts b/lib/routes/aeon/utils.ts index 1cd3a9eee3e2b3..e18421579cd485 100644 --- a/lib/routes/aeon/utils.ts +++ b/lib/routes/aeon/utils.ts @@ -3,28 +3,61 @@ const __dirname = getCurrentPath(import.meta.url); import cache from '@/utils/cache'; import { load } from 'cheerio'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { art } from '@/utils/render'; import path from 'node:path'; +import { config } from '@/config'; +import { parseDate } from '@/utils/parse-date'; -const getData = async (ctx, list) => { +export const getBuildId = () => + cache.tryGet( + 'aeon:buildId', + async () => { + const response = await ofetch('https://aeon.co'); + const $ = load(response); + const nextData = JSON.parse($('script#__NEXT_DATA__').text()); + return nextData.buildId; + }, + config.cache.routeExpire, + false + ); + +const getData = async (list) => { const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const { data: response } = await got(item.link); - const $ = load(response); + const buildId = await getBuildId(); + const response = await ofetch(`https://aeon.co/_next/data/${buildId}/${item.type}s/${item.slug}.json?id=${item.slug}`); - const data = JSON.parse($('script#__NEXT_DATA__').text()); - const type = data.props.pageProps.article.type.toLowerCase(); + const data = response.pageProps.article; + const type = data.type.toLowerCase(); - item.pubDate = new Date(data.props.pageProps.article.publishedAt).toUTCString(); + item.pubDate = parseDate(data.publishedAt); if (type === 'video') { - item.description = art(path.join(__dirname, 'templates/video.art'), { article: data.props.pageProps.article }); + item.description = art(path.join(__dirname, 'templates/video.art'), { article: data }); } else { - // Essay or Audio - // But unfortunately, the method based on __NEXT_DATA__ - // does not include the information of the audio link. + if (data.audio?.id) { + const response = await ofetch('https://api.aeonmedia.co/graphql', { + method: 'POST', + body: { + query: `query getAudio($audioId: ID!) { + audio(id: $audioId) { + id + streamUrl + } + }`, + variables: { + audioId: data.audio.id, + }, + operationName: 'getAudio', + }, + }); + + delete item.image; + item.enclosure_url = response.data.audio.streamUrl; + item.enclosure_type = 'audio/mpeg'; + } // Besides, it seems that the method based on __NEXT_DATA__ // does not include the information of the two-column @@ -32,14 +65,11 @@ const getData = async (ctx, list) => { // e.g. https://aeon.co/essays/how-to-mourn-a-forest-a-lesson-from-west-papua . // But that's very rare. - item.author = data.props.pageProps.article.authors.map((author) => author.name).join(', '); - - const article = data.props.pageProps.article; - const capture = load(article.body); - const banner = article.image?.url; + const capture = load(data.body, null, false); + const banner = data.image; capture('p.pullquote').remove(); - const authorsBio = article.authors.map((author) => '

' + author.name + author.authorBio.replaceAll(/^

/g, ' ')).join(''); + const authorsBio = data.authors.map((author) => '

' + author.name + author.authorBio.replaceAll(/^

/g, ' ')).join(''); item.description = art(path.join(__dirname, 'templates/essay.art'), { banner, authorsBio, content: capture.html() }); }