From 20ef6f2956fc7827d2c6ca9a220afd089b0ffaff Mon Sep 17 00:00:00 2001 From: "Weijan(David) :Duan" Date: Sun, 22 Sep 2024 09:55:49 +0800 Subject: [PATCH] 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 --- lib/routes/espn/namespace.ts | 6 ++ lib/routes/espn/news.ts | 120 ++++++++++++++++++++++++++++ lib/routes/espn/templates/media.art | 19 +++++ 3 files changed, 145 insertions(+) create mode 100644 lib/routes/espn/namespace.ts create mode 100644 lib/routes/espn/news.ts create mode 100644 lib/routes/espn/templates/media.art diff --git a/lib/routes/espn/namespace.ts b/lib/routes/espn/namespace.ts new file mode 100644 index 0000000000000..c148e7dbd47ad --- /dev/null +++ b/lib/routes/espn/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'ESPN', + url: 'espn.com', +}; diff --git a/lib/routes/espn/news.ts b/lib/routes/espn/news.ts new file mode 100644 index 0000000000000..44dd72c0a781f --- /dev/null +++ b/lib/routes/espn/news.ts @@ -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, + }; + }, +}; diff --git a/lib/routes/espn/templates/media.art b/lib/routes/espn/templates/media.art new file mode 100644 index 0000000000000..e1e52bccfac80 --- /dev/null +++ b/lib/routes/espn/templates/media.art @@ -0,0 +1,19 @@ +{{ if video.src }} + + {{ if video.title || video.description }} +
+ {{ if video.title }}
{{ video.title }}
{{ /if }} + {{ if video.description }}

{{ video.description }}

{{ /if }} +
+ {{ /if }} +{{ /if }} + +{{ if image.src }} +
+ {{ image.alt }} + {{ if image.caption }}
{{ image.caption }}
{{ /if }} + {{ if image.credit }}{{ image.credit }}{{ /if }} +
+{{ /if }}