feat(route/newslaundary): add newslaundary (#19576)
* init newslaundary * correct path * add youtube support * skip first entry
This commit is contained in:
parent
c6f96ef5a9
commit
37bbf928f2
|
|
@ -0,0 +1,30 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection, rootUrl } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/explainer',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/explainer',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/explainer'],
|
||||
target: '/explainer',
|
||||
},
|
||||
],
|
||||
name: 'Explainer',
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(): Promise<Data> {
|
||||
return await fetchCollection('explainer', `${rootUrl}/explainer`);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'Newslaundry',
|
||||
url: 'newslaundry.com',
|
||||
categories: ['new-media'],
|
||||
lang: 'en',
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection, rootUrl } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/nl-cheatsheet',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/nl-cheatsheet',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/collection/nl-cheatsheet'],
|
||||
target: '/nl-cheatsheet',
|
||||
},
|
||||
],
|
||||
name: 'Explains',
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(): Promise<Data> {
|
||||
return await fetchCollection('nl-cheatsheet', `${rootUrl}/collection/nl-cheatsheet`);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection, rootUrl } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/nl-collaborations',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/nl-collaborations',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/nl-collaborations'],
|
||||
target: '/nl-collaborations',
|
||||
},
|
||||
],
|
||||
name: 'NL Collaboration',
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(): Promise<Data> {
|
||||
return await fetchCollection('nl-collaborations', `${rootUrl}/nl-collaborations`);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection, rootUrl } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/podcast/:category?',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/podcast',
|
||||
parameters: { category: 'Podcast category, see below for details' },
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: true,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/podcast'],
|
||||
target: '/podcast',
|
||||
},
|
||||
{
|
||||
source: ['newslaundry.com/collection/nl-hafta-podcast'],
|
||||
target: '/podcast/nl-hafta',
|
||||
},
|
||||
{
|
||||
source: ['newslaundry.com/podcast/whats-your-ism'],
|
||||
target: '/podcast/whats-your-ism',
|
||||
},
|
||||
],
|
||||
name: 'Podcast',
|
||||
description: `| Category | URL |
|
||||
| -------- | --- |
|
||||
| All Podcasts | [/podcast](https://rsshub.app/newslaundry/podcast) |
|
||||
| NL Hafta | [/podcast/nl-hafta](https://rsshub.app/newslaundry/podcast/nl-hafta) |
|
||||
| What's Your Ism? | [/podcast/whats-your-ism](https://rsshub.app/newslaundry/podcast/whats-your-ism) |`,
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(ctx): Promise<Data> {
|
||||
const category = ctx.req.param('category');
|
||||
|
||||
// Map category to collection slug and URL
|
||||
const categoryMap = {
|
||||
'nl-hafta': {
|
||||
slug: 'nl-hafta-podcast',
|
||||
url: `${rootUrl}/collection/nl-hafta-podcast`,
|
||||
},
|
||||
'whats-your-ism': {
|
||||
slug: 'whats-your-ism-podcast-newslaundry-hindi',
|
||||
url: `${rootUrl}/podcast/whats-your-ism`,
|
||||
},
|
||||
};
|
||||
|
||||
// For main podcast route, skip the first item
|
||||
const skipFirstItem = !category;
|
||||
|
||||
return category && categoryMap[category] ? await fetchCollection(categoryMap[category].slug, categoryMap[category].url) : await fetchCollection('podcast', undefined, skipFirstItem);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/reports',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/reports',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/reports'],
|
||||
target: '/reports',
|
||||
},
|
||||
],
|
||||
name: 'Reports',
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(): Promise<Data> {
|
||||
return await fetchCollection('reports', undefined, true);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/shot',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/shot',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/shot'],
|
||||
target: '/shot',
|
||||
},
|
||||
],
|
||||
name: 'Shot',
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(): Promise<Data> {
|
||||
return await fetchCollection('shot');
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fetchCollection } from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/subscriber-only',
|
||||
view: ViewType.Articles,
|
||||
categories: ['new-media'],
|
||||
example: '/newslaundry/subscriber-only',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['newslaundry.com/subscriber-only'],
|
||||
target: '/subscriber-only',
|
||||
},
|
||||
],
|
||||
name: 'Subscriber Only',
|
||||
maintainers: ['Rjnishant530'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(): Promise<Data> {
|
||||
return await fetchCollection('subscriber-only');
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{{if subheadline}}
|
||||
<p><strong>{{subheadline}}</strong></p>
|
||||
{{/if}}
|
||||
|
||||
{{if heroImage}}
|
||||
<figure>
|
||||
<img src="{{heroImage}}" alt="{{heroAlt}}">
|
||||
<figcaption>{{heroCaption}}{{if heroAttribution}} ({{heroAttribution}}){{/if}}</figcaption>
|
||||
</figure>
|
||||
{{/if}}
|
||||
|
||||
{{each elements}}
|
||||
{{if $value.type === 'text'}}
|
||||
{{@ $value.text}}
|
||||
{{else if $value.type === 'image'}}
|
||||
<figure>
|
||||
<img src="{{$value.url}}" alt="{{$value.alt}}">
|
||||
<figcaption>{{$value.title}}</figcaption>
|
||||
</figure>
|
||||
{{else if $value.type === 'jsembed'}}
|
||||
{{@ $value.content}}
|
||||
{{else if $value.type === 'youtube-video'}}
|
||||
<figure>
|
||||
<iframe width="560" height="315" src="{{$value.embedUrl}}" frameborder="0" allowfullscreen></iframe>
|
||||
<figcaption><a href="{{$value.url}}" target="_blank" rel="noopener noreferrer">Watch on YouTube</a></figcaption>
|
||||
</figure>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import { Data, DataItem } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import { art } from '@/utils/render';
|
||||
import path from 'node:path';
|
||||
|
||||
export const rootUrl = 'https://www.newslaundry.com';
|
||||
|
||||
export async function fetchCollection(collectionSlug: string, customUrl?: string, skipFirstItem: boolean = false) {
|
||||
const apiUrl = `${rootUrl}/api/v1/collections/${collectionSlug}`;
|
||||
const currentUrl = customUrl || `${rootUrl}/${collectionSlug}`;
|
||||
|
||||
const response = await ofetch(apiUrl);
|
||||
|
||||
if (!response.items || !response.items.length) {
|
||||
throw new Error('No articles found');
|
||||
}
|
||||
|
||||
// Skip first item if requested
|
||||
const itemsToProcess = skipFirstItem ? response.items.slice(1) : response.items;
|
||||
const items = itemsToProcess.map((item) => processStory(item.story));
|
||||
|
||||
return {
|
||||
title: `${response.name} - Newslaundry`,
|
||||
description: response.summary || `${response.name} articles from Newslaundry`,
|
||||
link: currentUrl,
|
||||
item: items,
|
||||
language: 'en',
|
||||
logo: `${rootUrl}/favicon.ico`,
|
||||
icon: `${rootUrl}/favicon.ico`,
|
||||
} as Data;
|
||||
}
|
||||
|
||||
function processStory(story: any): DataItem {
|
||||
const articleUrl = story.url;
|
||||
const pubDate = story['published-at'] ? parseDate(story['published-at']) : null;
|
||||
|
||||
// Prepare data for template
|
||||
const heroImage = story['hero-image-s3-key'] ? `https://media.assettype.com/${story['hero-image-s3-key']}?auto=format%2Ccompress&fit=max&dpr=1.0&format=webp` : null;
|
||||
|
||||
// Extract elements from cards
|
||||
const elements =
|
||||
story.cards?.flatMap(
|
||||
(card) =>
|
||||
card?.['story-elements']
|
||||
?.map((element) => {
|
||||
if (element.type === 'text' && element.text) {
|
||||
return {
|
||||
type: 'text',
|
||||
text: element.text,
|
||||
};
|
||||
} else if (element.type === 'image' && element['image-s3-key']) {
|
||||
return {
|
||||
type: 'image',
|
||||
url: `https://media.assettype.com/${element['image-s3-key']}?auto=format%2Ccompress&format=webp`,
|
||||
alt: element['alt-text'] || '',
|
||||
title: element.title || '',
|
||||
};
|
||||
} else if (element.type === 'jsembed' && element['embed-js']) {
|
||||
try {
|
||||
return {
|
||||
type: 'jsembed',
|
||||
content: Buffer.from(element['embed-js'], 'base64').toString(),
|
||||
};
|
||||
} catch {
|
||||
// Skip if unable to decode
|
||||
return null;
|
||||
}
|
||||
} else if (element.type === 'youtube-video' && element.url) {
|
||||
return {
|
||||
type: 'youtube-video',
|
||||
url: element.url,
|
||||
embedUrl: element['embed-url'] || '',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) || []
|
||||
) || [];
|
||||
|
||||
// Render content using template
|
||||
const content = art(path.join(__dirname, 'templates/description.art'), {
|
||||
heroImage,
|
||||
heroAlt: story['hero-image-alt-text'] || '',
|
||||
heroCaption: story['hero-image-caption'] || '',
|
||||
heroAttribution: story['hero-image-attribution'],
|
||||
elements,
|
||||
subheadline: story.subheadline,
|
||||
});
|
||||
|
||||
// Extract author information
|
||||
const authors =
|
||||
story.authors?.map((author) => ({
|
||||
name: author.name,
|
||||
url: author.slug ? `${rootUrl}/author/${author.slug}` : undefined,
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
title: story.headline,
|
||||
link: articleUrl,
|
||||
image: heroImage,
|
||||
description: content || story.subheadline,
|
||||
pubDate,
|
||||
updated: story['last-correction-published-at'] ? parseDate(story['last-correction-published-at']) : undefined,
|
||||
author: authors.length > 0 ? authors : story['author-name'],
|
||||
category: story.tags?.map((tag) => tag.name) || [],
|
||||
} as DataItem;
|
||||
}
|
||||
Loading…
Reference in New Issue