rsshub
Version:
Make RSS Great Again!
403 lines (396 loc) • 15.9 kB
JavaScript
import { n as init_esm_shims, t as __dirname } from "./esm-shims-CzJ_djXG.mjs";
import "./config-C37vj7VH.mjs";
import "./dist-BInvbO1W.mjs";
import "./logger-Czu8UMNd.mjs";
import "./ofetch-BIyrKU3Y.mjs";
import { t as parseDate } from "./parse-date-BrP7mxXf.mjs";
import "./helpers-DxBp0Pty.mjs";
import { t as art } from "./render-BQo6B4tL.mjs";
import { t as got_default } from "./got-KxxWdaxq.mjs";
import { t as timezone } from "./timezone-D8cuwzTY.mjs";
import path from "node:path";
import { load } from "cheerio";
//#region lib/routes/the/util.ts
init_esm_shims();
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$1, filterSearchParams) => {
const keys = Object.keys(filterPairs$1).filter((key$1) => filterPairs$1[key$1]?.length > 0 && (isApi ? Object.hasOwn(filterApiKeys, key$1) : Object.hasOwn(filterKeys, key$1)));
if (keys.length === 0) return filterSearchParams;
const key = keys[0];
const pairs = filterPairs$1[key];
const originalFilters = { ...filterPairs$1 };
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$1, filtersWithPair) => {
const keys = Object.keys(filters$1);
if (keys.length === 0) return filtersWithPair;
const key = keys[0];
const keywords = filters$1[key];
const originalFilters = { ...filters$1 };
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$1 } = await got_default.get(currentUrl);
return response$1;
} 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(art(path.join(__dirname, "templates/description-c3c936c5.art"), { 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 = art(path.join(__dirname, "templates/description-c3c936c5.art"), {
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 };