|<\/p>/g; +const emptyRegex = /
]*>( |\s)<\/p>/g; + +const parseNewsList = async (url, ctx) => { + const resp = await got(url); + const $ = cheerio.load(resp.data, { + xml: { + xmlMode: true, + }, + }); + const urls = $('urlset url'); + return urls + .toArray() + .slice(0, ctx.query.limit ? parseInt(ctx.query.limit) : 50) + .map((u) => { + u = $(u); + const item = { + title: u.find('news\\:title').text(), + link: u.find('loc').text(), + pubDate: parseDate(u.find('news\\:publication_date').text()), + }; + return item; + }); +}; + +const parseArticle = async (item, ctx) => + await ctx.cache.tryGet(item.link, async () => { + const group = regex + .map((r) => r.exec(item.link)) + .filter((e) => e && e.groups) + .map((a) => a && a.groups)[0]; + if (group) { + const { page, link } = group; + + if (apiEndpoints[page]) { + const api = apiEndpoints[page]; + const apiUrl = `${api.url}${link}`; + let res; + try { + res = await got(apiUrl, { headers }); + } catch (err) { + // fallback + if (err.name && (err.name === 'HTTPError' || err.name === 'RequestError')) { + try { + res = await got(item.link, { headers }); + } catch (err) { + // return the default one + return { + title: item.title, + link: item.link, + pubDate: item.pubDate, + }; + } + } + } + + // Blocked by PX3, return the default + const redirectUrls = res.redirectUrls.map(String); + if (redirectUrls.some((r) => new URL(r).pathname === '/tosv2.html')) { + return { + title: item.title, + link: item.link, + pubDate: item.pubDate, + }; + } + + switch (page) { + case 'audio': + return await parseAudioPage(res, api, item); + case 'videos': + return await parseVideoPage(res, api, item); + case 'photo-essays': + return await parsePhotoEssaysPage(res, api, item); + case 'features/': // single features page + return await parseFeaturePage(res, api, item); + default: + return await parseOtherPage(res, api, item); + } + } + } + return item; + }); + +const parseAudioPage = async (res, api, item) => { + const audio_json = JSON.parse(cheerio.load(res.data)(api.sel).html()).props.pageProps; + const episode = audio_json.episode; + const rss_item = { + title: episode.title || item.title, + link: audio_json.pageInfo.canonicalUrl || item.link, + guid: `bloomberg:${episode.id}`, + description: (await processBody(episode.articleBody, audio_json)).replaceAll(emptyRegex, ''), + pubDate: parseDate(episode.publishedAt) || item.pubDate, + author: audio_json.hero.showTitle, + media: { + content: { url: episode.image }, + thumbnails: { url: episode.image }, + }, + enclosure_type: 'audio/mpeg', + enclosure_url: episode.url, + itunes_item_image: episode.image || audio_json.pageInfo.image.url, + }; + return rss_item; +}; + +const parseVideoPage = async (res, api, item) => { + const json = cheerio.load(res.data)(api.sel).text().trim().replace('window.__PRELOADED_STATE__ = ', '').slice(0, -1); + const article_json = JSON.parse(json); + const video_story = article_json.video?.videoStory ?? article_json.quicktakeVideo?.videoStory; + if (video_story) { + const desc = await processVideo(video_story.video.bmmrId, video_story.summary.html.replaceAll(emptyRegex, '')); + const rss_item = { + title: video_story.headline.text || item.title, + link: video_story.url || item.link, + guid: `bloomberg:${video_story.id}`, + description: art(path.join(__dirname, 'templates/video_media.art'), desc), + pubDate: parseDate(video_story.publishedAt) || item.pubDate, + media: { + content: { url: video_story.video?.thumbnail.url || '' }, + thumbnails: { url: video_story.video?.thumbnail.url || '' }, + }, + category: desc.keywords ?? [], + }; + return rss_item; + } + return item; +}; + +const parsePhotoEssaysPage = async (res, api, item) => { + const $ = cheerio.load(res.data.html); + const article_json = $(api.sel) + .toArray() + .map((e) => JSON.parse($(e).html())) + .reduce((pv, cv) => ({ ...pv, ...cv }), {}); + const rss_item = { + title: article_json.headline || item.title, + link: article_json.canonical || item.link, + guid: `bloomberg:${article_json.id}`, + description: (await processBody(article_json.body, article_json)).replaceAll(emptyRegex, ''), + pubDate: item.pubDate, + author: article_json.authors?.map((a) => a.name).join(', ') ?? [], + }; + return rss_item; +}; + +const parseFeaturePage = async (res, api, item) => { + const json = cheerio.load(res.data)(api.sel).text().trim().split('\n')[0].replace('var ds__data = ', '').slice(0, -1); + const article_json = JSON.parse(JSON.parse(json)); + const meta = article_json.meta; + + const desc = article_json.blocks.map(async (b) => { + if (b.type === 'Paragraph') { + return b.props.text.html; + } + if (b.type === 'ImageBlock') { + const image = { src: b.props?.src || '', alt: b.props?.alt || '', caption: b.props?.caption || '', credit: b.props?.credit || '' }; + return art(path.join(__dirname, 'templates/image_figure.art'), image); + } + if (b.type === 'Lede') { + return await processLedeMedia(b); + } + return ''; + }); + const rss_item = { + title: meta.title || item.title, + link: item.link, + description: (await Promise.all(desc)).join(''), + pubDate: parseDate(meta.pubDateTime) || item.pubDate, + author: meta.authors?.map((a) => a.name).join(', ') ?? [], + media: { + content: { url: meta.socialImage?.url ?? '' }, + thumbnails: { url: meta.socialImage?.url ?? '' }, + }, + category: + meta.keywords + .split(',') + .map((e) => e.trim()) + .filter((e) => !!e) ?? [], + }; + return rss_item; +}; + +const parseOtherPage = async function (res, api, item) { + const article_json = JSON.parse( + cheerio + .load(res.data.html ?? res.data)(api.sel) + .html() + ); + const story_json = article_json.story; + const body_html = story_json.body; + const media_img = story_json.ledeImageUrl || Object.values(story_json.imageAttachments ?? {})[0]?.baseUrl; + + const rss_item = { + title: story_json.textHeadline || item.title, + link: story_json.canonical || item.link, + guid: `bloomberg:${story_json.id}`, + description: (processHeadline(story_json) + (await processLedeMedia(story_json)) + (await processBody(body_html, story_json))).replaceAll(emptyRegex, ''), + pubDate: parseDate(story_json.publishedAt) || item.pubDate, + author: story_json.authors?.map((a) => a.name).join(', ') ?? [], + category: story_json.mostRelevantTags ?? [], + media: { + content: { url: media_img }, + thumbnails: { url: media_img }, + }, + }; + return rss_item; +}; + +const processHeadline = (story_json) => { + const dek = story_json.dek || ''; + const abs = story_json.abstract?.map((a) => `