feat(route): support psyche.co (#15772)

* support psyche

* Update lib/routes/psyche/topic.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* Update lib/routes/psyche/topic.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* Update lib/routes/psyche/topic.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* graphql + _next/data/buildID

* add link

* delete json

* cache link

* hotfix: redundent 's'

---------
This commit is contained in:
Enoch Ma 2024-06-02 07:15:26 +02:00 committed by GitHub
parent 6a24c14974
commit ec4a03e221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 225 additions and 0 deletions

View File

@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Psyche',
url: 'psyche.co',
};

View File

@ -0,0 +1,3 @@
<img src="{{ banner }}" alt="">
{{@ authorsBio }}
{{@ content}}

View File

@ -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 }}
<iframe width="672" height="377" src="{{ video }}" frameborder="0" allowfullscreen></iframe>
{{@ article.credits}}
{{@ article.description}}

View File

@ -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,
};
}

53
lib/routes/psyche/type.ts Normal file
View File

@ -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,
};
}

107
lib/routes/psyche/utils.ts Normal file
View File

@ -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 `<h2>${sectionName}</h2>` + 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(`<img src="${image}" alt="${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 };