UNPKG

rsshub

Version:
414 lines (406 loc) 16.4 kB
import "./dist-Bog6z_OM.mjs"; import "./config-MQ-7ebJ_.mjs"; import "./logger-C4avouvV.mjs"; import "./ofetch-DKl_Ma9g.mjs"; import { t as parseDate } from "./parse-date-r5WDWHno.mjs"; import "./helpers-Bo4JQYdN.mjs"; import { t as got_default } from "./got-CO-ndVl2.mjs"; import { t as timezone } from "./timezone-CA9Huotp.mjs"; import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime"; import { load } from "cheerio"; import { renderToString } from "hono/jsx/dom/server"; import { raw } from "hono/html"; //#region lib/routes/the/templates/description.tsx const resolveImageAlt = (image) => image?.height ?? image?.width ?? image?.alt; const renderDescription = ({ images, intro, description }) => renderToString(/* @__PURE__ */ jsxs(Fragment, { children: [ images?.length ? images.map((image) => image?.src ? /* @__PURE__ */ jsx("figure", { children: /* @__PURE__ */ jsx("img", { src: image.src, alt: resolveImageAlt(image) }) }, image.src) : null) : null, intro ? /* @__PURE__ */ jsx("blockquote", { children: intro }) : null, description ? /* @__PURE__ */ jsx(Fragment, { children: raw(description) }) : null ] })); //#endregion //#region lib/routes/the/util.ts const apiSlug = "wp-json/wp/v2"; const filterKeys = { search: "s" }; const filterApiKeys = { category: "categories", tag: "tags", search: void 0 }; const filterApiKeysWithNoId = new Set(["search"]); /** * Bake filter search parameters. * * @param filterPairs - The filter pairs object. * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. * @param pairKey - The filter pair key. * e.g. `{ id: ..., name: ..., slug: ... }`. * @param isApi - Indicates if the search parameters are for API. * @returns The baked filter search parameters. */ const bakeFilterSearchParams = (filterPairs, pairKey, isApi = false) => { /** * Bake filters recursively. * * @param filterPairs - The filter pairs object. * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. * @param filterSearchParams - The filter search parameters. * e.g. `category=a,b&tag=c`. * @returns The baked filter search parameters. * e.g. `category=a,b&tag=c`. */ const bakeFilters = (filterPairs, filterSearchParams) => { const keys = Object.keys(filterPairs).filter((key) => filterPairs[key]?.length > 0 && (isApi ? Object.hasOwn(filterApiKeys, key) : Object.hasOwn(filterKeys, key))); if (keys.length === 0) return filterSearchParams; const key = keys[0]; const pairs = filterPairs[key]; const originalFilters = { ...filterPairs }; delete originalFilters[key]; const filterKey = getFilterKeyForSearchParams(key, isApi); const pairValues = pairs.map((pair) => Object.hasOwn(pair, pairKey) ? pair[pairKey] : pair); if (filterKey) filterSearchParams.append(filterKey, pairValues.join(",")); return bakeFilters(originalFilters, filterSearchParams); }; return bakeFilters(filterPairs, new URLSearchParams()); }; /** * Bake filters with pair. * * @param filters - The filters object. * e.g. `{ category: [ a, b ], tag: [ c ] }`. * @returns The baked filters. * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. */ const bakeFiltersWithPair = async (filters, rootUrl) => { /** * Bake keywords recursively. * * @param key - The key. * e.g. `category` or `tag`. * @param keywords - The keywords. * e.g. `[ a, b ]`. * @returns The baked keywords. * e.g. `[ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ]`. */ const bakeKeywords = async (key, keywords) => { if (keywords.length === 0) return []; const [keyword, ...rest] = keywords; const filter = await getFilterByKeyAndKeyword(key, keyword, rootUrl); return [...filter?.id && filter?.slug ? [{ id: filter.id, name: filter.name, slug: filter.slug }] : [], ...await bakeKeywords(key, rest)]; }; /** * Bake filters recursively. * * @param filters - The filters object. * e.g. `{ category: [ a, b ], tag: [ c ] }`. * @param filtersWithPair - The filters with pairs. * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. * @returns The baked filters. * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. */ const bakeFilters = async (filters, filtersWithPair) => { const keys = Object.keys(filters); if (keys.length === 0) return filtersWithPair; const key = keys[0]; const keywords = filters[key]; const originalFilters = { ...filters }; delete originalFilters[key]; return bakeFilters(originalFilters, { ...filtersWithPair, [key]: filterApiKeysWithNoId.has(key) ? keywords : await bakeKeywords(key, keywords) }); }; return await bakeFilters(filters, {}); }; /** * Bake URL with search parameters. * * @param url - The URL. * @param rootUrl - The root URL. * @param searchParams - The search parameters. * @returns The baked URL. */ const bakeUrl = (url, rootUrl, searchParams = new URLSearchParams()) => { const searchParamsStr = searchParams.toString(); return `${rootUrl}/${url}${searchParamsStr ? `?${searchParamsStr}` : ""}`; }; /** * Fetch data from the specified URL. * * @param url - The URL to fetch data from. * @param rootUrl - The root URL. * @returns A promise that resolves to an object containing the fetched data to be added into `ctx.state.data`. */ const fetchData = async (url, rootUrl) => { /** * Request URLs recursively. * * @param urls - The URLs to request. * @returns A promise that resolves to the response data or undefined if no response is available. */ const requestUrls = async (urls) => { if (urls.length === 0) return; const [currentUrl, ...remainingUrls] = urls; try { const { data: response } = await got_default.get(currentUrl); return response; } catch { return requestUrls(remainingUrls); } }; const response = await requestUrls([url, rootUrl]); if (!response) return {}; const $ = load(response); const title = $("title").first().text(); const image = new URL("wp-content/uploads/site_logo.png", rootUrl).href; return { title, description: $("meta[property=\"og:description\"]").attr("content") || $("meta[name=\"description\"]").attr("content"), link: url, allowEmpty: true, image, author: $("meta[property=\"og:site_name\"]").attr("content"), language: $("html").attr("lang") }; }; /** * Get filter by key and keyword. * * @param key - The key. * e.g. `category` or `tag`. * @param keyword - The keywords. * e.g. `keyword1`. * @returns A promise that resolves to the filter object if found, or undefined if not found. */ const getFilterByKeyAndKeyword = async (key, keyword, rootUrl) => { const { data: response } = await got_default(`${rootUrl}/${apiSlug}/${getFilterKeyForSearchParams(key, true)}`, { searchParams: { search: keyword } }); return response.length > 0 ? response[0] : void 0; }; /** * Get filter key for search parameters. * * @param key - The key. e.g. `category` or `tag`. * @param isApi - Indicates whether the key is for the API. * @returns The filter key for search parameters, or undefined if not found. * e.g. `categories` or `tags`. */ const getFilterKeyForSearchParams = (key, isApi = false) => { const keys = isApi ? filterApiKeys : filterKeys; return Object.hasOwn(keys, key) ? keys[key] ?? key : void 0; }; /** * Get filter parameters for URL. * * @param filterPairs - The filter pairs object. * e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`. * @returns The filter parameters for the URL, or undefined if no filters are available. */ const getFilterParamsForUrl = (filterPairs) => { const keys = Object.keys(filterPairs).filter((key) => filterPairs[key].length > 0 && !Object.hasOwn(filterKeys, key)); if (keys.length === 0) return; return filterPairs[keys[0]].map((pair) => pair.slug).join("/"); }; /** * Parses a filter string into a filters object. * * @param filterStr - The filter string to parse. * e.g. `category/a,b/tag/c`. * @returns The parsed filters object. * e.g. `{ category: [ 'a', 'b' ], tag: [ 'c' ] }`. */ const parseFilterStr = (filterStr) => { /** * Recursively parses a filter string. * * @param remainingStr - The remaining filter string to parse. * e.g. `category/a,b/tag/c`. * @param filters - The accumulated filters object. * e.g. `{ category: [ a, b ], tag: [ c ] }`. * @param currentKey - The current filter key. * e.g. `category` or `tag`. * @returns The parsed filters object. */ const parseStr = (remainingStr, filters = {}, currentKey) => { if (!remainingStr) return filters; const [word, ...rest] = remainingStr.split(/\/|,/); const isKey = Object.hasOwn(filterApiKeys, word); const key = isKey ? word : currentKey; const newFilters = key ? { ...filters, [key]: [...filters[key] || [], ...isKey ? [] : [word]] } : filters; return parseStr(rest.join("/"), newFilters, key); }; return parseStr(filterStr, {}); }; //#endregion //#region lib/routes/the/index.ts const handler = async (ctx) => { const { filter } = ctx.req.param(); const limit = ctx.req.query("limit") ? Number.parseInt(ctx.req.query("limit"), 10) : 40; const rootUrl = "https://the.bi/s"; const filters = parseFilterStr(filter); const filtersWithPair = await bakeFiltersWithPair(filters, rootUrl); const searchParams = bakeFilterSearchParams(filters, "name", false); const apiSearchParams = bakeFilterSearchParams(filtersWithPair, "id", true); apiSearchParams.append("_embed", "true"); apiSearchParams.append("per_page", String(limit)); apiSearchParams.append("page", "1"); const apiUrl = bakeUrl(`${apiSlug}/posts`, rootUrl, apiSearchParams); const currentUrl = bakeUrl(getFilterParamsForUrl(filtersWithPair) ?? "", rootUrl, searchParams); const { data: response } = await got_default(apiUrl); const items = response.slice(0, limit).map((item) => { const terminologies = item._embedded["wp:term"]; const guid = item.guid?.rendered ?? item.guid; const $$ = load(item.content?.rendered ?? item.content); const publication = $$("a[id='publication']").text(); const image = $$("img#poster").prop("data-srcset"); $$("figure.graf").each((_, el) => { el = $$(el); const imgEl = el.find("img"); el.replaceWith(renderDescription({ images: imgEl ? [{ src: imgEl.prop("src"), width: imgEl.prop("width"), height: imgEl.prop("height") }] : void 0 })); }); const title = $$("h1").text(); const intro = $$("h2").text(); $$("h1").parent().remove(); const description = renderDescription({ images: image ? [{ src: image }] : void 0, intro, description: $$.html() }); return { title: item.title?.rendered ?? item.title ?? title, description, pubDate: timezone(parseDate(item.date_gmt), 0), updated: timezone(parseDate(item.modified_gmt), 0), link: item.link, category: [...new Set(terminologies.flat().map((c) => c.name))], author: [...item._embedded.author, { name: publication }], guid, id: guid, content: { html: description, text: $$.text() } }; }); return { ...await fetchData(currentUrl, rootUrl), item: items }; }; const route = { path: "/:filter{.+}?", name: "分类", url: "the.bi", maintainers: ["nczitzk"], handler, example: "/the", parameters: { filter: "过滤器,见下方描述" }, description: `::: tip 如果你想订阅特定类别或标签,可以在路由中填写 filter 参数。\`/category/rawmw7dsta2jew\` 可以实现订阅 [剩余价值](https://the.bi/s/rawmw7dsta2jew) 类别。此时,路由是 [\`/the/category/rawmw7dsta2jew/\`](https://rsshub.app/the/category/rawmw7dsta2jew). 你还可以订阅多个类别。\`/category/rawmw7dsta2jew,rawbcvxkktdkq8/\` 可以实现同时订阅 [剩余价值](https://the.bi/s/rawmw7dsta2jew) 和 [打江山](https://the.bi/s/rawbcvxkktdkq8) 两个类别。此时,路由是 [\`/the/category/rawmw7dsta2jew,rawbcvxkktdkq8\`](https://rsshub.app/the/category/rawmw7dsta2jew,rawbcvxkktdkq8). 类别和标签也可以合并订阅。\`/category/rawmw7dsta2jew/tag/raweekl3na8trq\` 订阅 [剩余价值](https://the.bi/s/rawmw7dsta2jew) 类别和 [动物](https://the.bi/s/raweekl3na8trq) 标签。此时,路由是 [\`/the/category/rawmw7dsta2jew/tag/raweekl3na8trq\`](https://rsshub.app/the/category/rawmw7dsta2jew/tag/raweekl3na8trq). 你还可以搜索关键字。\`/search/中国\` 搜索关键字 [中国](https://the.bi/s/?s=中国)。在这种情况下,路径是 [\`/the/search/中国\`](https://rsshub.app/the/search/中国). ::: | 分类 | ID | | ---------------------------------------------- | ---------------------------------------------------------------- | | [时局图](https://the.bi/s/rawj7o4ypewv94) | [rawj7o4ypewv94](https://rsshub.app/the/category/rawj7o4ypewv94) | | [剩余价值](https://the.bi/s/rawmw7dsta2jew) | [rawmw7dsta2jew](https://rsshub.app/the/category/rawmw7dsta2jew) | | [打江山](https://the.bi/s/rawbcvxkktdkq8) | [rawbcvxkktdkq8](https://rsshub.app/the/category/rawbcvxkktdkq8) | | [中国经济](https://the.bi/s/raw4krvx85dh27) | [raw4krvx85dh27](https://rsshub.app/the/category/raw4krvx85dh27) | | [水深火热](https://the.bi/s/rawtn8jpsc6uvv) | [rawtn8jpsc6uvv](https://rsshub.app/the/category/rawtn8jpsc6uvv) | | [东升西降](https://the.bi/s/rawai5kd4z15il) | [rawai5kd4z15il](https://rsshub.app/the/category/rawai5kd4z15il) | | [大局 & 大棋](https://the.bi/s/raw2efkzejrsx8) | [raw2efkzejrsx8](https://rsshub.app/the/category/raw2efkzejrsx8) | | [境外势力](https://the.bi/s/rawmpalhnlphuc) | [rawmpalhnlphuc](https://rsshub.app/the/category/rawmpalhnlphuc) | | [副刊](https://the.bi/s/rawxght2jr2u5z) | [rawxght2jr2u5z](https://rsshub.app/the/category/rawxght2jr2u5z) | | [天高地厚](https://the.bi/s/rawrsnh9zakqdx) | [rawrsnh9zakqdx](https://rsshub.app/the/category/rawrsnh9zakqdx) | | [Oyster](https://the.bi/s/rawdhl9hugdfn9) | [rawdhl9hugdfn9](https://rsshub.app/the/category/rawdhl9hugdfn9) | `, categories: ["new-media"], features: { requireConfig: false, requirePuppeteer: false, antiCrawler: false, supportRadar: true, supportBT: false, supportPodcast: false, supportScihub: false }, radar: [ { source: ["the.bi/s/:category?"], target: (params) => { const category = params.category; return `/the${category ? `/category/${category}` : ""}`; } }, { title: "时局图", source: ["the.bi/s/rawj7o4ypewv94"], target: "/category/rawj7o4ypewv94" }, { title: "剩余价值", source: ["the.bi/s/rawmw7dsta2jew"], target: "/category/rawmw7dsta2jew" }, { title: "打江山", source: ["the.bi/s/rawbcvxkktdkq8"], target: "/category/rawbcvxkktdkq8" }, { title: "中国经济", source: ["the.bi/s/raw4krvx85dh27"], target: "/category/raw4krvx85dh27" }, { title: "水深火热", source: ["the.bi/s/rawtn8jpsc6uvv"], target: "/category/rawtn8jpsc6uvv" }, { title: "东升西降", source: ["the.bi/s/rawai5kd4z15il"], target: "/category/rawai5kd4z15il" }, { title: "大局 & 大棋", source: ["the.bi/s/raw2efkzejrsx8"], target: "/category/raw2efkzejrsx8" }, { title: "境外势力", source: ["the.bi/s/rawmpalhnlphuc"], target: "/category/rawmpalhnlphuc" }, { title: "副刊", source: ["the.bi/s/rawxght2jr2u5z"], target: "/category/rawxght2jr2u5z" }, { title: "天高地厚", source: ["the.bi/s/rawrsnh9zakqdx"], target: "/category/rawrsnh9zakqdx" }, { title: "Oyster", source: ["the.bi/s/rawdhl9hugdfn9"], target: "/category/rawdhl9hugdfn9" } ] }; //#endregion export { handler, route };