feat(route): add feed for ESPN News (#16731)
* feat(route): add feed for ESPN News * fix(route): change feeds and full-text fetching way based on code reviews * fix(route): filter feeds by accepted type first * fix(route): changes based on code reviews * feat: support video playback * fix: out of bound
This commit is contained in:
parent
f8ec86e235
commit
20ef6f2956
|
|
@ -0,0 +1,6 @@
|
|||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'ESPN',
|
||||
url: 'espn.com',
|
||||
};
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { Route } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import * as cheerio from 'cheerio';
|
||||
import path from 'path';
|
||||
import { getCurrentPath } from '@/utils/helpers';
|
||||
import { art } from '@/utils/render';
|
||||
|
||||
const __dirname = getCurrentPath(import.meta.url);
|
||||
|
||||
const renderMedia = (media) =>
|
||||
art(path.join(__dirname, 'templates', 'media.art'), {
|
||||
video: {
|
||||
cover: media.posterImages?.full?.href || media.posterImages?.default?.href,
|
||||
src: media.links?.source.mezzanine?.href || media.links?.source.HD?.href || media.links?.source.full?.href || media.links?.source.href,
|
||||
title: media.title,
|
||||
description: media.description,
|
||||
},
|
||||
image: {
|
||||
src: media.url,
|
||||
alt: media.alt,
|
||||
caption: media.caption,
|
||||
credit: media.credit,
|
||||
},
|
||||
});
|
||||
|
||||
const junkPattern = /inline\d+|alsosee/;
|
||||
const mediaPattern = /(photo|video)(\d+)/;
|
||||
|
||||
export const route: Route = {
|
||||
path: '/news/:sport',
|
||||
name: 'News',
|
||||
maintainers: ['GymRat102'],
|
||||
example: '/espn/news/nba',
|
||||
categories: ['traditional-media'],
|
||||
parameters: { sport: 'sport category, can be nba, nfl, mlb, nhl etc.' },
|
||||
description: `Get the news feed of the sport you love on ESPN.
|
||||
| Sport | sport | Sport | sport |
|
||||
|----------------------|---------|----------------|---------|
|
||||
| 🏀NBA | nba | 🎾Tennis | tennis |
|
||||
| 🏀WNBA | wnba | ⛳️Golf | golf |
|
||||
| 🏈NFL | nfl | 🏏Cricket | cricket |
|
||||
| ⚾️MLB | mlb | ⚽️Soccer | soccer |
|
||||
| 🏒NHL | nhl | 🏎️F1 | f1 |
|
||||
| ⛹️College Basketball | ncb | 🥊MMA | mma |
|
||||
| 🏟️️College Football | ncf | 🏈UFL | ufl |
|
||||
| 🏉Rugby | rugby | 🃏Poker | poker |`,
|
||||
radar: [
|
||||
{
|
||||
source: ['espn.com/:sport*'],
|
||||
target: '/news/:sport',
|
||||
},
|
||||
],
|
||||
handler: async (ctx) => {
|
||||
const { sport = 'nba' } = ctx.req.param();
|
||||
const response = await ofetch(`https://onefeed.fan.api.espn.com/apis/v3/cached/contentEngine/oneFeed/leagues/${sport}?offset=0`, {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const handledTypes = new Set(['HeadlineNews', 'Story', 'Media', 'Shortstop']);
|
||||
const list = response.feed
|
||||
.filter((item) => handledTypes.has(item.data.now[0].type))
|
||||
.map((item) => {
|
||||
const itemDetail = item.data.now[0];
|
||||
const itemType = itemDetail.type;
|
||||
|
||||
return {
|
||||
title: itemDetail.headline,
|
||||
link: itemDetail.links.web.href,
|
||||
author: itemDetail.byline,
|
||||
pubDate: item.date,
|
||||
// for videos and shortstops, no need to extract full text below
|
||||
description: itemType === 'Media' ? renderMedia(itemDetail.video[0]) : itemType === 'Shortstop' ? itemDetail.headline : '',
|
||||
};
|
||||
});
|
||||
|
||||
const items = await Promise.all(
|
||||
list.map((item) =>
|
||||
cache.tryGet(item.link, async () => {
|
||||
if (item.description === '') {
|
||||
const article = await ofetch(`${item.link}?xhr=1`, {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const $ = cheerio.load(article.content.story, null, false);
|
||||
$('*').each((_, ele) => {
|
||||
if (junkPattern.test(ele.name)) {
|
||||
$(ele).remove();
|
||||
}
|
||||
if (mediaPattern.test(ele.name)) {
|
||||
const mediaType = ele.name.match(mediaPattern)[1] === 'photo' ? 'images' : 'video';
|
||||
const mediaIndex = Number.parseInt(ele.name.match(mediaPattern)[2]) - 1;
|
||||
const media = article.content[mediaType][mediaIndex];
|
||||
if (media) {
|
||||
$(ele).replaceWith(renderMedia(media));
|
||||
} else {
|
||||
$(ele).remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
item.description = $.html();
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
title: `ESPN ${sport.toUpperCase()} News`,
|
||||
link: `https://www.espn.com/espn/rss/${sport}/news`,
|
||||
item: items,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{{ if video.src }}
|
||||
<video controls preload="metadata" width="100%" height="auto" cover="{{ video.cover }}">
|
||||
<source src="{{ video.src }}" type="video/mp4">
|
||||
</video>
|
||||
{{ if video.title || video.description }}
|
||||
<div>
|
||||
{{ if video.title }}<div><b>{{ video.title }}</b></div>{{ /if }}
|
||||
{{ if video.description }}<p>{{ video.description }}</p>{{ /if }}
|
||||
</div>
|
||||
{{ /if }}
|
||||
{{ /if }}
|
||||
|
||||
{{ if image.src }}
|
||||
<figure>
|
||||
<img src="{{ image.src }}" alt="{{ image.alt }}">
|
||||
{{ if image.caption }}<figcaption>{{ image.caption }}</figcaption>{{ /if }}
|
||||
{{ if image.credit }}<cite>{{ image.credit }}</cite>{{ /if }}
|
||||
</figure>
|
||||
{{ /if }}
|
||||
Loading…
Reference in New Issue