rsshub
Version:
Make RSS Great Again!
290 lines (287 loc) • 12 kB
JavaScript
import "./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 got_default } from "./got-KxxWdaxq.mjs";
import { load } from "cheerio";
//#region lib/routes/getitfree/util.ts
const rootUrl = "https://getitfree.cn";
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 {Object} filterPairs - The filter pairs object.
* e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`.
* @param {string} pairKey - The filter pair key.
* e.g. `{ id: ..., name: ..., slug: ... }`.
* @param {boolean} [isApi=false] - IIndicates if the search parameters are for API.
* @returns {URLSearchParams} The baked filter search parameters.
*/
const bakeFilterSearchParams = (filterPairs, pairKey, isApi = false) => {
/**
* Bake filters recursively.
*
* @param {Object} filterPairs - The filter pairs object.
* e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`.
* @param {URLSearchParams} filterSearchParams - The filter search parameters.
* e.g. `category=a,b&tag=c`.
* @returns {URLSearchParams} 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 = Object.assign({}, filterPairs$1);
delete originalFilters[key];
filterSearchParams.append(getFilterKeyForSearchParams(key, isApi), pairs.map((pair) => Object.hasOwn(pair, pairKey) ? pair[pairKey] : pair).join(","));
return bakeFilters(originalFilters, filterSearchParams);
};
return bakeFilters(filterPairs, new URLSearchParams());
};
/**
* Bake filters with pair.
*
* @param {Object} filters - The filters object.
* e.g. `{ category: [ a, b ], tag: [ c ] }`.
* @returns {Promise<Object>} The baked filters.
* e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`.
*/
const bakeFiltersWithPair = async (filters) => {
/**
* Bake keywords recursively.
*
* @param {string} key - The key.
* e.g. `category` or `tag`.
* @param {Array<string>} keywords - The keywords.
* e.g. `[ a, b ]`.
* @returns {Promise<Array<Object>>} 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);
return [...filter?.id && filter?.slug ? [{
id: filter.id,
name: filter.name,
slug: filter.slug
}] : [], ...await bakeKeywords(key, rest)];
};
/**
* Bake filters recursively.
*
* @param {Object} filters - The filters object.
* e.g. `{ category: [ a, b ], tag: [ c ] }`.
* @param {Object} filtersWithPair - The filters with pairs.
* e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`.
* @returns {Promise<Object>} 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 = Object.assign({}, 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 {string} url - The URL.
* @param {string} rootUrl - The root URL.
* @param {URLSearchParams} [searchParams=new URLSearchParams()] - The search parameters.
* @returns {string} The baked URL.
*/
const bakeUrl = (url, rootUrl$1, searchParams = new URLSearchParams()) => {
const searchParamsStr = String(searchParams) ? `?${searchParams}` : "";
return new URL(`${url}${searchParamsStr}`, rootUrl$1).href;
};
/**
* Fetch data from the specified URL.
*
* @param {string} url - The URL to fetch data from.
* @returns {Promise<Object>} A promise that resolves to an object containing the fetched data
* to be added into `ctx.state.data`.
*/
const fetchData = async (url) => {
/**
* Request URLs recursively.
*
* @param {Array<string>} urls - The URLs to request.
* @returns {Promise<undefined|Object>} 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(currentUrl);
return response$1;
} catch {
return requestUrls(remainingUrls);
}
};
const response = await requestUrls([url, rootUrl]);
if (!response) return {};
const $ = load(response);
const title = $("title").text().split(/\|/)[0];
const image = new URL("wp-content/uploads/site_logo.png", rootUrl).href;
const icon = new URL($("link[rel=\"shortcut icon\"]").prop("href"), rootUrl).href;
return {
title,
link: url,
description: $("meta[name=\"description\"]").prop("content"),
language: $("html").prop("lang"),
image,
icon,
logo: icon,
subtitle: title.split(/【/)[0],
author: $("h1.logo a").prop("title"),
allowEmpty: true
};
};
/**
* Get filter by key and keyword.
*
* @param {string} key - The key.
* e.g. `category` or `tag`.
* @param {string} keyword - The keywords.
* e.g. `keyword1`.
* @returns {Promise<Object|undefined>} A promise that resolves to the filter object if found,
* or undefined if not found.
*/
const getFilterByKeyAndKeyword = async (key, keyword) => {
const apiFilterUrl = new URL(`${apiSlug}/${getFilterKeyForSearchParams(key, true)}`, rootUrl).href;
const { data: response } = await got_default(apiFilterUrl, { searchParams: { search: keyword } });
return response.length > 0 ? response[0] : void 0;
};
/**
* Get filter key for search parameters.
*
* @param {string} key - The key. e.g. `category` or `tag`.
* @param {boolean} [isApi=false] - Indicates whether the key is for the API.
* @returns {string|undefined} 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 names for titles.
*
* @param {Object} filterPairs - The filter pairs object.
* e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`.
* @returns {string} A string containing the joined filter names for titles.
* e.g. `name1,name2`.
*/
const getFilterNameForTitle = (filterPairs) => Object.values(filterPairs).flat().map((pair) => pair?.name ?? pair?.slug ?? pair).filter(Boolean).join(",");
/**
* Get filter parameters for URL.
*
* @param {Object} filterPairs - The filter pairs object.
* e.g. `{ category: [ { id: ..., name: ..., slug: ... }, { id: ..., name: ..., slug: ... } ], tag: [ { id: ..., name: ..., slug: ... } ] }`.
* @returns {string|undefined} The filter parameters for the URL, or undefined if no filters are available.
*/
const getFilterParamsForUrl = (filterPairs) => {
const keys = Object.keys(filterPairs).filter((key$1) => filterPairs[key$1]);
if (keys.length === 0) return;
const key = keys[0];
return `${key}/${filterPairs[key].map((pair) => pair.slug).join("/")}`;
};
/**
* Parse filter string into filters object.
*
* @param {string} filterStr - The filter string to parse.
* e.g. `category/a,b/tag/c`.
* @returns {Object} The parsed filters object.
* e.g. `{ category: [ a, b ], tag: [ c ] }`.
*/
const parseFilterStr = (filterStr) => {
/**
* Parse filter string recursively.
*
* @param {string} filterStr - The remaining filter string to parse.
* e.g. `category/a,b/tag/c`.
* @param {Object} filters - The accumulated filters object.
* e.g. `{ category: [ a, b ], tag: [ c ] }`.
* @param {string} filterKey - The current filter key.
* e.g. `category` or `tag`.
* @returns {Object} The parsed filters object.
*/
const parseStr = (filterStr$1, filters = {}, filterKey) => {
if (!filterStr$1) return filters;
const [word, ...rest] = filterStr$1.split(/\/|,/);
const isKey = Object.hasOwn(filterApiKeys, word);
const key = isKey ? word : filterKey;
const newFilters = {
...filters,
[key]: [...filters[key] || [], ...isKey ? [] : [word]]
};
return parseStr(rest.join("/"), newFilters, key);
};
return parseStr(filterStr, {});
};
//#endregion
//#region lib/routes/getitfree/index.ts
const route = {
path: "/:filter{.+}?",
name: "Unknown",
maintainers: [],
handler
};
async function handler(ctx) {
const filter = ctx.req.param("filter");
const limit = ctx.req.query("limit") ? Number.parseInt(ctx.req.query("limit"), 10) : 50;
const filters = parseFilterStr(filter);
const filtersWithPair = await bakeFiltersWithPair(filters);
const searchParams = bakeFilterSearchParams(filters, "name", false);
const apiSearchParams = bakeFilterSearchParams(filtersWithPair, "id", true);
apiSearchParams.append("_embed", "true");
apiSearchParams.append("per_page", limit);
const apiUrl = bakeUrl(`${apiSlug}/posts`, rootUrl, apiSearchParams);
const currentUrl = bakeUrl(getFilterParamsForUrl(filtersWithPair) ?? "", rootUrl, searchParams);
const { data: response } = await got_default(apiUrl);
const items = (Array.isArray(response) ? response : JSON.parse(response.match(/(\[.*])$/)[1])).slice(0, limit).map((item) => {
const terminologies = item._embedded["wp:term"];
const content = load(item.content?.rendered ?? item.content);
content("div.mycred-sell-this-wrapper").prevUntil("hr").nextAll().remove();
return {
title: item.title?.rendered ?? item.title,
link: item.link,
description: content.html(),
author: item._embedded.author.map((a) => a.name).join("/"),
category: [...new Set(terminologies.flat().map((c) => c.name))],
guid: item.guid?.rendered ?? item.guid,
pubDate: parseDate(item.date_gmt),
updated: parseDate(item.modified_gmt)
};
});
const subtitle = getFilterNameForTitle(filtersWithPair);
return {
...await fetchData(currentUrl),
item: items,
title: `Getitfree${subtitle ? ` | ${subtitle}` : ""}`
};
}
//#endregion
export { route };