diff --git a/lib/routes/psyche/namespace.ts b/lib/routes/psyche/namespace.ts new file mode 100644 index 000000000..ece31fee1 --- /dev/null +++ b/lib/routes/psyche/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Psyche', + url: 'psyche.co', +}; diff --git a/lib/routes/psyche/templates/essay.art b/lib/routes/psyche/templates/essay.art new file mode 100644 index 000000000..5a2fab892 --- /dev/null +++ b/lib/routes/psyche/templates/essay.art @@ -0,0 +1,3 @@ + +{{@ authorsBio }} +{{@ content}} \ No newline at end of file diff --git a/lib/routes/psyche/templates/video.art b/lib/routes/psyche/templates/video.art new file mode 100644 index 000000000..d1f546c39 --- /dev/null +++ b/lib/routes/psyche/templates/video.art @@ -0,0 +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://www.youtube-nocookie.com/embed/" + video }} +{{ /if }} + + +{{@ article.credits}} +{{@ article.description}} diff --git a/lib/routes/psyche/topic.ts b/lib/routes/psyche/topic.ts new file mode 100644 index 000000000..d273aff6d --- /dev/null +++ b/lib/routes/psyche/topic.ts @@ -0,0 +1,46 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import { ofetch } from 'ofetch'; +import { getData } from './utils'; + +export const route: Route = { + path: '/topic/:topic', + categories: ['new-media'], + example: '/psyche/topic/therapeia', + parameters: { topic: 'Topic' }, + radar: [ + { + source: ['psyche.co/:topic'], + }, + ], + name: 'Topics', + maintainers: ['emdoe'], + handler, + description: 'Supported categories: Therapeia, Eudaimonia, and Poiesis.', +}; + +async function handler(ctx) { + const url = `https://psyche.co/${ctx.req.param('topic')}`; + const response = await ofetch(url); + const $ = load(response); + + const data = JSON.parse($('script#__NEXT_DATA__').text()); + const articles = data.props.pageProps.articles; + const prefix = `https://psyche.co/_next/data/${data.buildId}`; + const list = Object.keys(articles).flatMap((type) => + articles[type].edges.map((item) => ({ + title: item.node.title, + link: `https://psyche.co/${type}/${item.node.slug}`, + json: `${prefix}/${type}/${item.node.slug}.json`, + })) + ); + + const items = await getData(list); + + return { + title: `Psyche | ${data.props.pageProps.section.title}`, + link: url, + description: data.props.pageProps.section.metaDescription, + item: items, + }; +} diff --git a/lib/routes/psyche/type.ts b/lib/routes/psyche/type.ts new file mode 100644 index 000000000..6647dd87a --- /dev/null +++ b/lib/routes/psyche/type.ts @@ -0,0 +1,53 @@ +import { Route } from '@/types'; +import { load } from 'cheerio'; +import { getData } from './utils'; +import { ofetch } from 'ofetch'; + +export const route: Route = { + path: '/type/:type', + categories: ['new-media'], + example: '/psyche/type/ideas', + parameters: { type: 'Type' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['psyche.co/:type'], + }, + ], + name: 'Types', + maintainers: ['emdoe'], + handler, + description: `Supported types: Ideas, Guides, and Films.`, +}; + +async function handler(ctx) { + const type = ctx.req.param('type'); + const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1); + + const url = `https://psyche.co/${type}`; + const response = await ofetch(url); + const $ = load(response); + + const data = JSON.parse($('script#__NEXT_DATA__').text()); + const prefix = `https://psyche.co/_next/data/${data.buildId}`; + const list = data.props.pageProps.articles.map((item) => ({ + title: item.title, + link: `${url}/${item.slug}`, + json: `${prefix}/${type}/${item.slug}.json`, + })); + + const items = await getData(list); + + return { + title: `Psyche | ${capitalizedType}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/psyche/utils.ts b/lib/routes/psyche/utils.ts new file mode 100644 index 000000000..7e0612949 --- /dev/null +++ b/lib/routes/psyche/utils.ts @@ -0,0 +1,107 @@ +import { getCurrentPath } from '@/utils/helpers'; +const __dirname = getCurrentPath(import.meta.url); + +import cache from '@/utils/cache'; +import { ofetch } from 'ofetch'; +import { load } from 'cheerio'; +import { art } from '@/utils/render'; +import path from 'node:path'; + +const getImageById = async (id) => { + const response = await ofetch('https://api.aeonmedia.co/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: 'query getImageById($id: ID!) { image(id: $id) { id url alt caption width height } }', + variables: { id, site: 'Aeon' }, + operationName: 'getImageById', + }), + }); + + return response.data.image.url; +}; + +function format(article) { + const type = article.type.toLowerCase(); + + let block = ''; + let banner = ''; + let authorsBio = ''; + + switch (type) { + case 'film': + block = art(path.join(__dirname, 'templates/video.art'), { article }); + + break; + + case 'guide': { + banner = article.imageSquare?.url; + authorsBio = article.authors.map((author) => author.bio).join(' '); + + const sectionNames = ['Need To Know', 'What To Do', 'Key Points', 'Learn More', 'Links & Books']; + const sections = Object.keys(article).filter((key) => key.startsWith('section') && key !== 'section'); + const content = sections + .map((section) => { + const capture = load(article[section]); + capture('p.pullquote').remove(); + const sectionName = sectionNames.shift(); + return `

${sectionName}

` + capture.html(); + }) + .join(''); + + block = art(path.join(__dirname, 'templates/essay.art'), { banner, authorsBio, content }); + + break; + } + case 'idea': { + banner = article.imageLandscape?.url; + authorsBio = article.authors.map((author) => author.bio).join(' '); + + const capture = load(article.body); + capture('p.pullquote').remove(); + block = art(path.join(__dirname, 'templates/essay.art'), { banner, authorsBio, content: capture.html() }); + + break; + } + default: + break; + } + return block; +} + +const getData = async (list) => { + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const data = await ofetch(item.json); + const article = data.pageProps.article; + item.pubDate = new Date(article.publishedAt).toUTCString(); + const content = format(article); + const capture = load(content); + await Promise.all( + capture('dl > dt') + .toArray() + .map(async (item) => { + const id = capture(item).text(); + const image = await getImageById(id); + capture(item).replaceWith(`${id}`); + }) + ); + + let authors = ''; + authors = article.type === 'film' ? article.creditsShort : article.authors.map((author) => author.name).join(', '); + + item.description = capture.html(); + item.author = authors; + + return item; + }) + ) + ); + + return items; +}; + +export { getData };