rsshub
Version:
Make RSS Great Again!
582 lines (581 loc) • 21.3 kB
JavaScript
import { t as rofetch } from "./ofetch-C3ts-Hud.mjs";
import { t as parseDate } from "./parse-date-Cr0wQjV_.mjs";
import { t as cache_default } from "./cache-BkqOokyU.mjs";
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
import { load } from "cheerio";
import { renderToString } from "hono/jsx/dom/server";
import { destr } from "destr";
import { raw } from "hono/html";
//#region lib/routes/bloomberg/templates/audio-media.tsx
const renderAudioMedia = ({ img, src, caption, credit }) => renderToString(/* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("img", { src: img }), /* @__PURE__ */ jsxs("audio", {
controls: true,
children: [/* @__PURE__ */ jsx("source", {
src,
type: "audio/mpeg"
}), "Your browser does not support the audio element."]
})] }), /* @__PURE__ */ jsxs("figcaption", { children: [/* @__PURE__ */ jsx("div", {
class: "caption",
children: caption ? raw(caption) : null
}), /* @__PURE__ */ jsx("div", {
class: "credit",
children: credit ? raw(credit) : null
})] })] }));
//#endregion
//#region lib/routes/bloomberg/templates/chart-media.tsx
const renderChartMedia = ({ chart }) => renderToString(/* @__PURE__ */ jsxs("figure", { children: [
chart.title ? /* @__PURE__ */ jsx(Fragment, { children: chart.title }) : null,
chart.subtitle ? /* @__PURE__ */ jsx("p", { children: chart.subtitle }) : null,
/* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx("img", {
src: chart.fallback,
alt: chart.chartAlt,
loading: "lazy",
style: "display:block; margin-left:auto; margin-right:auto; width:100%;"
}) }),
/* @__PURE__ */ jsx("iframe", {
id: chart.chartId,
title: chart.title,
referrerpolicy: "no-referrer",
width: "100%",
height: "150vh",
frameborder: "0",
marginheight: "0",
marginwidth: "0",
loading: "lazy",
scrolling: "no",
style: "border:0; margin:0; padding:0; width:100%; height:150vh;",
src: chart.url
}),
chart.source ? /* @__PURE__ */ jsxs("figcaption", { children: [/* @__PURE__ */ jsx("div", {
class: "source",
children: raw(chart.source)
}), chart.footnote ? /* @__PURE__ */ jsx("p", { children: chart.footnote }) : null] }) : null
] }));
//#endregion
//#region lib/routes/bloomberg/templates/image-figure.tsx
const renderImageFigure = ({ src, alt, caption, credit }) => renderToString(/* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
src,
alt,
loading: "lazy",
style: "display:block; margin-left:auto; margin-right:auto; width:100%;"
}), caption || credit ? /* @__PURE__ */ jsxs("figcaption", { children: [/* @__PURE__ */ jsx("div", {
class: "caption",
children: caption ? raw(caption) : null
}), /* @__PURE__ */ jsx("div", {
class: "credit",
children: credit ? raw(credit) : null
})] }) : null] }));
//#endregion
//#region lib/routes/bloomberg/templates/video-media.tsx
const renderVideoMedia = ({ stream, mp4, coverUrl, caption }) => renderToString(/* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsxs("video", {
controls: true,
playsinline: true,
"webkit-playsinline": "true",
"x5-playsinline": "true",
"x5-video-player-type": "h5",
"x5-video-orientation": "landscape|portrait",
"x5-video-player-fullscreen": "true",
"x-webkit-airplay": "allow",
preload: "metadata",
poster: coverUrl,
children: [stream ? /* @__PURE__ */ jsx("source", {
src: stream,
type: "application/x-mpegURL"
}) : null, mp4 ? /* @__PURE__ */ jsx("source", {
src: mp4,
type: "video/mp4"
}) : null]
}), caption ? /* @__PURE__ */ jsx("figcaption", { children: /* @__PURE__ */ jsx("div", {
class: "caption",
children: raw(caption)
}) }) : null] }));
//#endregion
//#region lib/routes/bloomberg/templates/lede-media.tsx
const renderLedeMedia = (media) => {
if (media?.kind === "video") return renderVideoMedia(media.video ?? {});
if (media?.kind === "image") return renderToString(/* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
src: media.src,
alt: media.description,
loading: "lazy",
style: "display:block; margin-left:auto; margin-right:auto; width:100%;"
}), media.caption ? /* @__PURE__ */ jsxs("figcaption", { children: [/* @__PURE__ */ jsx("div", {
class: "caption",
children: raw(media.caption)
}), /* @__PURE__ */ jsx("div", {
class: "credit",
children: media.credit ? raw(media.credit) : null
})] }) : null] }));
return "";
};
//#endregion
//#region lib/routes/bloomberg/utils.ts
const rootUrl = "https://www.bloomberg.com/feeds";
const idSel = "script[id^=\"article-info\"][type=\"application/json\"], script[class^=\"article-info\"][type=\"application/json\"], script#dvz-config";
const idUrl = "https://www.bloomberg.com/article/api/story/id/";
const headers = {
accept: "application/json",
"cache-control": "no-cache",
referer: "https://www.bloomberg.com"
};
const apiEndpoints = {
articles: { url: "https://www.bloomberg.com/article/api/story/slug/" },
features: { url: "https://www.bloomberg.com/article/api/story/slug/" },
audio: {
url: "https://www.bloomberg.com/news/audio/",
sel: "script#__NEXT_DATA__"
},
videos: {
url: "https://www.bloomberg.com/news/videos/",
sel: "script"
},
newsletters: { url: "https://www.bloomberg.com/article/api/story/slug/" },
"photo-essays": {
url: "https://www.bloomberg.com/javelin/api/photo-essay_transporter/",
sel: "script[type = \"application/json\"][data-component-props]"
},
"features/": {
url: "https://www.bloomberg.com/features/",
sel: idSel,
prop: "id"
}
};
const regex = [/\/(?<page>[\w-]*)\/(?<link>\d{4}-\d{2}-\d{2}\/.*)/, /(?<!news|politics)\/(?<page>features\/|graphics\/)(?<link>.*)/];
const capRegex = /<p>|<\/p>/g;
const emptyRegex = /<p\b[^>]*>( |\s)<\/p>/g;
const redirectGot = (url) => rofetch.raw(url, {
headers,
parseResponse: (responseText) => ({
data: destr(responseText),
body: responseText
})
});
const parseArticle = (item) => cache_default.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 (Object.hasOwn(apiEndpoints, page)) {
const api = { ...apiEndpoints[page] };
let res;
try {
const apiUrl = `${api.url}${link}`;
res = await redirectGot(apiUrl);
} catch (error) {
const err = error;
if (err.name && [
"HTTPError",
"RequestError",
"FetchError"
].includes(err.name)) try {
res = await redirectGot(item.link);
} catch {
return {
title: item.title,
link: item.link,
pubDate: item.pubDate,
description: item.description
};
}
}
if (res.redirected && new URL(res.url).pathname === "/tosv2.html" || res.status === 404) return {
title: item.title,
link: item.link,
pubDate: item.pubDate,
description: item.description
};
switch (page) {
case "audio": return parseAudioPage(res._data, api, item);
case "videos": return parseVideoPage(res._data, api, item);
case "photo-essays": return parsePhotoEssaysPage(res._data, api, item);
case "features/": return parseReactRendererPage(res._data, api, item);
default: return parseStoryJson(res._data.data, item);
}
}
}
return item;
});
const parseAudioPage = async (res, api, item) => {
const audio_json = JSON.parse(load(res.data)(api.sel).html() ?? "").props.pageProps;
const episode = audio_json.episode;
return {
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
};
};
const parseVideoPage = async (res, api, item) => {
const $ = load(res.data);
const json = $(api.sel).filter((i, el) => $(el).text().includes("__PRELOADED_STATE__")).text().trim().match(/window\.__PRELOADED_STATE__ = (.*?);/)?.[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, ""));
return {
title: video_story.headline.text || item.title,
link: video_story.url || item.link,
guid: `bloomberg:${video_story.id}`,
description: renderVideoMedia(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 item;
};
const parsePhotoEssaysPage = async (res, api, item) => {
const $ = load(res.data.html);
const article_json = {};
for (const e of $(api.sel).toArray()) {
const raw = $(e).html();
if (raw !== null) Object.assign(article_json, JSON.parse(raw));
}
return {
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(", ") ?? []
};
};
const parseReactRendererPage = async (res, api, item) => {
const json = load(res.data)(api.sel).text().trim();
const story_id = JSON.parse(json)[api.prop];
try {
const res = await redirectGot(`${idUrl}${story_id}`);
return await parseStoryJson(res._data, item);
} catch (error) {
const err = error;
if (err.name && [
"HTTPError",
"RequestError",
"FetchError"
].includes(err.name)) return {
title: item.title,
link: item.link,
pubDate: item.pubDate
};
}
};
const parseStoryJson = async (story_json, item) => {
const media_img = story_json.ledeImageUrl || Object.values(story_json.imageAttachments ?? {})[0]?.url;
return {
title: story_json.headline || item.title,
link: story_json.url || item.link,
guid: `bloomberg:${story_json.id}`,
description: processHeadline(story_json) + await processLedeMedia(story_json) + await documentToHtmlString(story_json.body || ""),
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 }
}
};
};
const processHeadline = (story_json) => {
const dek = story_json.dek || story_json.summary || story_json.headline || "";
const abs = story_json.abstract?.map((a) => `<li>${a}</li>`).join("");
return abs ? dek + `<ul>${abs}</ul>` : dek;
};
const processLedeMedia = async (story_json) => {
if (story_json.ledeMediaKind) {
const kind = story_json.ledeMediaKind;
return renderLedeMedia({
kind: story_json.ledeMediaKind,
caption: story_json.ledeCaption?.replaceAll(capRegex, "") ?? "",
description: story_json.ledeDescription?.replaceAll(capRegex, "") ?? "",
credit: story_json.ledeCredit?.replaceAll(capRegex, "") ?? "",
src: story_json.ledeImageUrl,
video: kind === "video" && await processVideo(story_json.ledeAttachment.bmmrId)
});
}
if (story_json.lede) {
const lede = story_json.lede;
return renderImageFigure({
src: lede.url,
alt: lede.alt || lede.title,
caption: lede.caption?.replaceAll(capRegex, "") ?? "",
credit: lede.credit?.replaceAll(capRegex, "") ?? ""
});
}
if (story_json.imageAttachments) {
const attachment = Object.values(story_json.imageAttachments)[0];
if (attachment) return renderImageFigure({
src: attachment.baseUrl || attachment.url,
alt: attachment.alt || attachment.title,
caption: attachment.caption?.replaceAll(capRegex, "") ?? "",
credit: attachment.credit?.replaceAll(capRegex, "") ?? ""
});
return "";
}
if (story_json.type === "Lede") {
const props = story_json.props;
return renderLedeMedia({
kind: props.media,
caption: props.caption?.replaceAll(capRegex, "") ?? "",
description: props.dek?.replaceAll(capRegex, "") ?? "",
credit: props.credit?.replaceAll(capRegex, "") ?? "",
src: props.url
});
}
};
const processBody = async (body_html, story_json) => {
const removeSel = [
"meta",
"*[class$=\"-footnotes\"]",
"*[class$=\"for-you\"]",
"*[class$=\"-newsletter\"]",
"*[class$=\"page-ad\"]",
"*[class$=\"-recirc\"]",
"*[data-ad-placeholder=\"Advertisement\"]"
];
const $ = load(body_html);
for (const sel of removeSel) $(sel).remove();
$(".paywall").removeAttr("class");
for await (const e of $("figure")) {
const imageType = $(e).data("image-type");
const type = $(e).data("type");
let new_figure = "";
if (imageType === "audio") {
let audio = {};
if (story_json.audios) {
const attachment = story_json.audios.find((a) => a.id.toString() === $(e).data("id").toString());
audio = {
img: attachment.image?.url || $(e).find("img").attr("src"),
src: attachment.url || $(e).find("audio source").attr("src"),
caption: $(e).find("[class$=\"text\"]").html()?.trim() ?? "",
credit: $(e).find("[class$=\"credit\"]").html()?.trim() ?? ""
};
}
if (story_json.episode) {
const episode = story_json.episode;
audio = {
src: episode.url,
img: episode.image || story_json.pageInfo.image.url,
caption: episode.description || ($(e).find("[class$=\"text\"]").html()?.trim() ?? ""),
credit: (episode.credits.map((c) => c.name).join(", ") ?? []) || ($(e).find("[class$=\"credit\"]").html()?.trim() ?? "")
};
}
new_figure = renderAudioMedia(audio);
} else if (imageType === "video") {
if (story_json.videoAttachments) {
const attachment = story_json.videoAttachments[$(e).data("id")];
new_figure = renderVideoMedia(await processVideo(attachment.bmmrId));
}
} else if (imageType === "photo" || imageType === "image" || type === "image") {
let src, alt;
if (story_json.imageAttachments) {
const attachment = story_json.imageAttachments[$(e).data("id")];
alt = attachment?.alt || $(e).find("img").attr("alt")?.trim();
src = attachment?.baseUrl;
} else {
alt = $(e).find("img").attr("alt").trim();
src = $(e).find("img").data("native-src");
}
const caption = $(e).find("[class$=\"text\"], .caption, .photo-essay__text").html()?.trim() ?? "";
const credit = $(e).find("[class$=\"credit\"], .credit, .photo-essay__source").html()?.trim() ?? "";
new_figure = renderImageFigure({
src,
alt,
caption,
credit
});
}
$(new_figure).insertAfter(e);
$(e).remove();
}
return $.html();
};
const processVideo = async (bmmrId, summary) => {
const api = `https://www.bloomberg.com/multimedia/api/embed?id=${bmmrId}`;
const res = await redirectGot(api);
if (res.redirected && new URL(res.url).pathname === "/tosv2.html" || res.status === 404) return {
stream: "",
mp4: "",
coverUrl: "",
caption: summary
};
if (res._data.data) {
const video_json = res._data.data;
return {
stream: video_json.streams ? video_json.streams[0]?.url : "",
mp4: video_json.downloadURLs ? video_json.downloadURLs["600"] : "",
coverUrl: video_json.thumbnail?.baseUrl ?? "",
caption: video_json.description || video_json.title || summary
};
}
return {
stream: "",
mp4: "",
coverUrl: "",
caption: summary
};
};
const nodeRenderers = {
paragraph: async (node, nextNode) => `<p>${await nextNode(node.content)}</p>`,
text: (node) => {
const { attributes: attr, value: val } = node;
if (attr?.emphasis && attr.strong) return `<strong><em>${val}</em></strong>`;
if (attr?.emphasis) return `<em>${val}</em>`;
if (attr?.strong) return `<strong>${val}</strong>`;
return val;
},
"inline-newsletter": async (node, nextNode) => `<div>${await nextNode(node.content)}</div>`,
"inline-recirc": async (node, nextNode) => `<div>${await nextNode(node.content)}</div>`,
heading: async (node, nextNode) => {
const nodeData = node.data;
if (nodeData.level === 2 || nodeData.level === 3) return `<h3>${await nextNode(node.content)}</h3>`;
},
link: async (node, nextNode) => {
const dest = node.data.destination;
const web = dest.web;
const bbg = dest.bbg;
const title = node.data.title;
if (web) return `<a href="${web}" title="${title}" target="_blank">${await nextNode(node.content)}</a>`;
if (bbg && bbg.startsWith("bbg://news/stories")) {
const o = bbg.split("bbg://news/stories/").pop();
return `<a href="${"https://www.bloomberg.com/news/terminal/".concat(o)}" title="${title}" target="_blank">${await nextNode(node.content)}</a>`;
}
return String(await nextNode(node.content));
},
entity: async (node, nextNode) => {
const t = node.subType;
const web = node.data.link.destination.web;
if (t === "person") return nextNode(node.content);
if (t === "story") {
if (web) return `<a href="${web}" target="_blank">${await nextNode(node.content)}</a>`;
const a = node.data.story.identifiers.suid;
return `<a href="${"https://www.bloomberg.com/news/terminal/".concat(a)}" target="_blank">${await nextNode(node.content)}</a>`;
}
if (t === "security") {
const s = node.data.security.identifiers.parsekey;
if (s) {
const c = s.split(" ");
return `<a href="${[..."https://www.bloomberg.com/quote/".concat(c[0], ":"), c[1]]}" target="_blank">${await nextNode(node.content)}</a>`;
}
}
return nextNode(node.content);
},
br: () => "<br/>",
hr: () => "<br/>",
ad: () => {},
blockquote: async (node, nextNode) => `<blockquote>${await nextNode(node.content)}</blockquote>`,
quote: async (node, nextNode) => `<blockquote>${await nextNode(node.content)}</blockquote>`,
aside: async (node, nextNode) => `<aside>${await nextNode(node.content)}</aside>`,
list: async (node, nextNode) => {
const t = node.subType;
if (t === "unordered") return `<ul>${await nextNode(node.content)}</ul>`;
if (t === "ordered") return `<ol>${await nextNode(node.content)}</ol>`;
},
listItem: async (node, nextNode) => `<li>${await nextNode(node.content)}</li>`,
media: async (node) => {
const t = node.subType;
if (t === "chart" && node.data.attachment) {
if (node.data.attachment.creator === "TOASTER") {
const c = node.data.chart;
const e = {
src: c && c.fallback || "",
chart: node.data.attachment,
id: c && c.id || "",
alt: c && c.alt || ""
};
const w = e.chart;
return renderChartMedia({ chart: {
source: w.source,
footnote: w.footnote,
url: w.url,
title: w.title,
subtitle: w.subtitle,
chartId: "toaster-chart-".concat(e.id),
chartAlt: e.alt,
fallback: e.src
} });
}
return renderImageFigure({
alt: node.data.attachment?.footnote || "",
caption: node.data.attachment?.title + node.data.attachment.subtitle || "",
credit: node.data.attachment?.source || "",
src: node.data.chart?.fallback || ""
});
}
if (t === "photo") {
const h = node.data;
let img = "";
if (h.attachment) img = renderImageFigure({
src: h.photo?.src,
alt: h.photo?.alt,
caption: h.photo?.caption,
credit: h.photo?.credit
});
if (h.link && h.link.destination && h.link.destination.web) return `<a href="${h.link.destination.web}" target="_blank">${img}</a>`;
return img;
}
if (t === "video") {
const h = node.data;
const id = h.attachment?.id;
if (id) return renderVideoMedia(await processVideo(id, h.attachment?.title));
}
if (t === "audio" && node.data.attachment) {
const B = node.data.attachment;
const P = B.title;
const D = B.url;
const M = B.image;
if (P && D) return renderAudioMedia({
src: D,
img: M.url,
caption: P,
credit: ""
});
}
return "";
},
tabularData: async (node, nextNode) => `<table>${await nextNode(node.content)}</table>`,
columns: (node) => {
return `<tr>${node.data.definitions.map((e) => ({
title: e.title,
span: e.colSpan || 1,
type: e.dataType
})).map((e) => `<th colspan=${e.span}>${e.title}</th>`)}</tr>`;
},
row: async (node, nextNode) => `<tr>${await nextNode(node.content)}</tr>`,
cell: async (node, nextNode) => {
return `<td data-coltype=${{
"news-rsf-table-number": "number",
"news-rsf-table-string": "text"
}[node.data.class] || "text"} colspan=${node.data.colspan}>${await nextNode(node.content)}</td>`;
}
};
const nextNode = async (nodes) => {
return await nodeListToHtmlString(nodes);
};
const nodeToHtmlString = async (node, obj) => {
if (!node.type || !Object.hasOwn(nodeRenderers, node.type)) return `<node>${node.type}</node>`;
return await nodeRenderers[node.type](node, nextNode, obj);
};
const nodeListToHtmlString = async (nodes) => {
return (await Promise.all(nodes.map(async (node, index) => {
return await nodeToHtmlString(node, {
index,
prev: nodes[index - 1]?.type,
next: nodes[index + 1]?.type
});
}))).join("");
};
const documentToHtmlString = async (document) => {
if (!document || !document.content) return "";
return await nodeListToHtmlString(document.content);
};
//#endregion
export { rootUrl as n, parseArticle as t };