feat(route): add 正版中国分类 & 标签 & 搜索 (#13769)
* feat(route): add 正版中国分类 & 标签 & 搜索 * fix: remove unnecessary parameter ---------
This commit is contained in:
parent
073021d5b2
commit
4cd614fea2
|
|
@ -1090,8 +1090,8 @@ router.get('/wikihow/index', lazyloadRouteHandler('./routes/wikihow/index.js'));
|
|||
router.get('/wikihow/category/:category/:type', lazyloadRouteHandler('./routes/wikihow/category.js'));
|
||||
|
||||
// 正版中国
|
||||
router.get('/getitfree/category/:category?', lazyloadRouteHandler('./routes/getitfree/category.js'));
|
||||
router.get('/getitfree/search/:keyword?', lazyloadRouteHandler('./routes/getitfree/search.js'));
|
||||
// router.get('/getitfree/category/:category?', lazyloadRouteHandler('./routes/getitfree/category.js'));
|
||||
// router.get('/getitfree/search/:keyword?', lazyloadRouteHandler('./routes/getitfree/search.js'));
|
||||
|
||||
// 万联网
|
||||
router.get('/10000link/news/:category?', lazyloadRouteHandler('./routes/10000link/news'));
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
const got = require('@/utils/got');
|
||||
const cheerio = require('cheerio');
|
||||
const utils = require('./utils');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const { category = 'all' } = ctx.params;
|
||||
const baseUrl = `https://getitfree.cn`;
|
||||
const categoryToLabel = {
|
||||
8: 'PC',
|
||||
17: 'Android',
|
||||
50: 'Mac',
|
||||
309: '限时折扣',
|
||||
310: '限时免费',
|
||||
311: '永久免费',
|
||||
312: 'UWP',
|
||||
all: '全部文章',
|
||||
};
|
||||
|
||||
let item = [];
|
||||
if (category === 'all') {
|
||||
const res = await got(baseUrl);
|
||||
const $ = cheerio.load(res.data);
|
||||
$('#page-content .top-content .top-slide > .item')
|
||||
.toArray()
|
||||
.forEach((ele) => {
|
||||
const $item = cheerio.load(ele);
|
||||
const infoNode = $item('.slider-image > a');
|
||||
const title = infoNode.attr('title');
|
||||
const link = infoNode.attr('href');
|
||||
const thumbnail = infoNode.attr('style').replace(/background-image:url\(|\)/g, '');
|
||||
const deadlineStr = utils.getDeadlineStr($item, '.countDownCont');
|
||||
const description = [title, deadlineStr, `<a href="${thumbnail}"/>`].filter((str) => !!str).join('<br/>');
|
||||
item.push({
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
});
|
||||
});
|
||||
item = item.concat(utils.parseListItem($, '.main-content'));
|
||||
} else {
|
||||
const res = await got(baseUrl, {
|
||||
searchParams: {
|
||||
action: 'fa_load_postlist',
|
||||
paged: 1,
|
||||
category,
|
||||
},
|
||||
});
|
||||
const $ = cheerio.load(res.data);
|
||||
item = utils.parseListItem($, '.ajax-load-con');
|
||||
}
|
||||
|
||||
const categoryLabel = categoryToLabel[category];
|
||||
ctx.state.data = {
|
||||
title: `正版中国 - ${categoryLabel}`,
|
||||
description: `正版中国 - ${categoryLabel}`,
|
||||
link: baseUrl,
|
||||
item,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
const got = require('@/utils/got');
|
||||
const cheerio = require('cheerio');
|
||||
const qs = require('query-string');
|
||||
const utils = require('./utils');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const { keyword } = ctx.params;
|
||||
const link = 'https://getitfree.cn';
|
||||
const query = {
|
||||
s: keyword,
|
||||
};
|
||||
const res = await got(link, {
|
||||
query,
|
||||
});
|
||||
const $ = cheerio.load(res.data);
|
||||
|
||||
const item = utils.parseListItem($, '#page-content');
|
||||
|
||||
ctx.state.data = {
|
||||
title: `正版中国搜索 - ${keyword}`,
|
||||
description: `正版中国搜索 - ${keyword}`,
|
||||
link: `${link}?${qs.stringify(query)}`,
|
||||
item,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
const cheerio = require('cheerio');
|
||||
const dayJs = require('dayjs');
|
||||
require('dayjs/locale/zh-cn');
|
||||
|
||||
const getDeadlineStr = ($, countDownNodeSelector) => {
|
||||
const countDownNode = $(countDownNodeSelector);
|
||||
let deadlineStr = '';
|
||||
if (countDownNode.text().includes('活动已经结束')) {
|
||||
return '截至日期: 活动已经结束';
|
||||
}
|
||||
const countdownCodeStr = countDownNode.children('script').html() || '';
|
||||
const arr = /ShowCountDown\("\d*"\s*,\s*([\d,]+)\s*\)/.exec(countdownCodeStr.trim());
|
||||
if (arr) {
|
||||
const dateStr = arr[1].replace(/,/g, (...args) => {
|
||||
const index = args[args.length - 2];
|
||||
return index === 10 ? ' ' : index < 10 ? '-' : ':';
|
||||
});
|
||||
deadlineStr = `截至日期: ${dayJs(dateStr).locale('zh-cn').format('YYYY-MM-DD HH:mm:ss')}`;
|
||||
}
|
||||
return deadlineStr;
|
||||
};
|
||||
|
||||
const parseListItem = ($, listSelector) =>
|
||||
$(`${listSelector} .content-box`)
|
||||
.map((_, ele) => {
|
||||
const $item = cheerio.load(ele);
|
||||
const infoNode = $item('.posts-default-title > h2 > a');
|
||||
const title = infoNode.attr('title');
|
||||
const link = infoNode.attr('href');
|
||||
const pubDateStr = $item('.posts-default-info .icon-time').text();
|
||||
const pubDate = new Date(pubDateStr).toUTCString();
|
||||
const thumbnail = $item('.posts-default-img img').attr('data-original');
|
||||
const deadlineStr = getDeadlineStr($item, '.countDownCont');
|
||||
const digest = $item('.posts-default-content > .posts-text').text().trim();
|
||||
return {
|
||||
title,
|
||||
link,
|
||||
pubDate,
|
||||
description: [title, deadlineStr, digest, `<img src="${thumbnail}"/>`].filter((str) => !!str).join('<br/>'),
|
||||
};
|
||||
})
|
||||
.get();
|
||||
|
||||
module.exports = {
|
||||
getDeadlineStr,
|
||||
parseListItem,
|
||||
};
|
||||
|
|
@ -2,53 +2,61 @@ const got = require('@/utils/got');
|
|||
const cheerio = require('cheerio');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
|
||||
const {
|
||||
apiSlug,
|
||||
rootUrl,
|
||||
|
||||
bakeFilterSearchParams,
|
||||
bakeFiltersWithPair,
|
||||
bakeUrl,
|
||||
fetchData,
|
||||
getFilterNameForTitle,
|
||||
getFilterParamsForUrl,
|
||||
parseFilterStr,
|
||||
} = require('./util');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const category = ctx.params.category ?? '';
|
||||
const { filter } = ctx.params;
|
||||
const limit = ctx.query.limit ? parseInt(ctx.query.limit, 10) : 50;
|
||||
|
||||
const rootUrl = 'https://www.getitfree.cn';
|
||||
const currentUrl = `${rootUrl}${category ? `/category/${category}` : ''}/page/1`;
|
||||
const filters = parseFilterStr(filter);
|
||||
const filtersWithPair = await bakeFiltersWithPair(filters);
|
||||
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: currentUrl,
|
||||
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(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 = cheerio.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([].concat(...terminologies).map((c) => c.name))],
|
||||
guid: item.guid?.rendered ?? item.guid,
|
||||
pubDate: parseDate(item.date_gmt),
|
||||
updated: parseDate(item.modified_gmt),
|
||||
};
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
let items = $('a.post-title')
|
||||
.toArray()
|
||||
.map((item) => {
|
||||
item = $(item);
|
||||
|
||||
return {
|
||||
title: item.text(),
|
||||
link: item.attr('href'),
|
||||
};
|
||||
});
|
||||
|
||||
items = await Promise.all(
|
||||
items.map((item) =>
|
||||
ctx.cache.tryGet(item.link, async () => {
|
||||
const detailResponse = await got({
|
||||
method: 'get',
|
||||
url: item.link,
|
||||
});
|
||||
|
||||
const content = cheerio.load(detailResponse.data);
|
||||
|
||||
content('h3, footer').remove();
|
||||
|
||||
item.description = content('.post-content-text').html();
|
||||
item.pubDate = parseDate(content('time.entry-date').first().attr('datetime'));
|
||||
|
||||
return item;
|
||||
})
|
||||
)
|
||||
);
|
||||
const subtitle = getFilterNameForTitle(filtersWithPair);
|
||||
|
||||
ctx.state.data = {
|
||||
title: $('title').text(),
|
||||
link: currentUrl,
|
||||
...(await fetchData(currentUrl)),
|
||||
item: items,
|
||||
title: `Getitfree${subtitle ? ` | ${subtitle}` : ''}`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
module.exports = {
|
||||
'/:category?': ['nczitzk'],
|
||||
'/category/:id': ['sanmmm', 'nczitzk'],
|
||||
'/tag/:id': ['nczitzk'],
|
||||
'/search/:keyword': ['sanmmm', 'nczitzk'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,8 +5,25 @@ module.exports = {
|
|||
{
|
||||
title: '分类',
|
||||
docs: 'https://docs.rsshub.app/routes/shopping#zheng-ban-zhong-guo-fen-lei',
|
||||
source: ['/category/:category', '/'],
|
||||
target: '/getitfree/:category?',
|
||||
source: ['/category/:id'],
|
||||
target: '/getitfree/category/:id',
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
docs: 'https://docs.rsshub.app/routes/shopping#zheng-ban-zhong-guo-biao-qian',
|
||||
source: ['/tag/:id'],
|
||||
target: '/getitfree/tag/:id',
|
||||
},
|
||||
{
|
||||
title: '搜索',
|
||||
docs: 'https://docs.rsshub.app/routes/shopping#zheng-ban-zhong-guo-sou-suo',
|
||||
source: ['/'],
|
||||
target: (_, url) => {
|
||||
url = new URL(url);
|
||||
const keyword = url.searchParams.get('s');
|
||||
|
||||
return `/getitfree/search${keyword ? `/${keyword}` : ''}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
module.exports = function (router) {
|
||||
router.get('/:category?', require('./index'));
|
||||
module.exports = (router) => {
|
||||
router.get('/:filter*', (ctx) => {
|
||||
const { filter } = ctx.params;
|
||||
|
||||
if (filter && !filter.includes('/') && !filter.includes(',')) {
|
||||
ctx.redirect(`/getitfree/category/${filter}`);
|
||||
} else {
|
||||
return require('./')(ctx);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,326 @@
|
|||
const got = require('@/utils/got');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
const rootUrl = 'https://getitfree.cn';
|
||||
const apiSlug = 'wp-json/wp/v2';
|
||||
|
||||
const filterKeys = {
|
||||
search: 's',
|
||||
};
|
||||
|
||||
const filterApiKeys = {
|
||||
category: 'categories',
|
||||
tag: 'tags',
|
||||
search: undefined,
|
||||
};
|
||||
|
||||
const filterApiKeysWithNoId = ['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, filterSearchParams) => {
|
||||
const keys = Object.keys(filterPairs).filter((key) => filterPairs[key]?.length > 0 && (isApi ? filterApiKeys.hasOwnProperty(key) : filterKeys.hasOwnProperty(key)));
|
||||
|
||||
if (keys.length === 0) {
|
||||
return filterSearchParams;
|
||||
}
|
||||
|
||||
const key = keys[0];
|
||||
const pairs = filterPairs[key];
|
||||
|
||||
const originalFilters = Object.assign({}, filterPairs);
|
||||
delete originalFilters[key];
|
||||
|
||||
filterSearchParams.append(getFilterKeyForSearchParams(key, isApi), pairs.map((pair) => (pair.hasOwnProperty(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, filtersWithPair) => {
|
||||
const keys = Object.keys(filters);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return filtersWithPair;
|
||||
}
|
||||
|
||||
const key = keys[0];
|
||||
const keywords = filters[key];
|
||||
|
||||
const originalFilters = Object.assign({}, filters);
|
||||
delete originalFilters[key];
|
||||
|
||||
return bakeFilters(originalFilters, {
|
||||
...filtersWithPair,
|
||||
[key]: filterApiKeysWithNoId.includes(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, searchParams = new URLSearchParams()) => {
|
||||
const searchParamsStr = String(searchParams) ? `?${searchParams}` : '';
|
||||
|
||||
return new URL(`${url}${searchParamsStr}`, rootUrl).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 undefined;
|
||||
}
|
||||
|
||||
const [currentUrl, ...remainingUrls] = urls;
|
||||
try {
|
||||
const { data: response } = await got(currentUrl);
|
||||
return response;
|
||||
} catch (e) {
|
||||
return requestUrls(remainingUrls);
|
||||
}
|
||||
};
|
||||
|
||||
const response = await requestUrls([url, rootUrl]);
|
||||
|
||||
if (!response) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const $ = cheerio.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(apiFilterUrl, {
|
||||
searchParams: {
|
||||
search: keyword,
|
||||
},
|
||||
});
|
||||
|
||||
return response.length > 0 ? response[0] : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 keys.hasOwnProperty(key) ? keys[key] ?? key : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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((name) => name)
|
||||
.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) => filterPairs[key]);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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, filters = {}, filterKey = undefined) => {
|
||||
if (!filterStr) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
const [word, ...rest] = filterStr.split(/\/|,/);
|
||||
|
||||
const isKey = filterApiKeys.hasOwnProperty(word);
|
||||
const key = isKey ? word : filterKey;
|
||||
|
||||
const newFilters = {
|
||||
...filters,
|
||||
[key]: [...(filters[key] || []), ...(isKey ? [] : [word])],
|
||||
};
|
||||
|
||||
return parseStr(rest.join('/'), newFilters, key);
|
||||
};
|
||||
|
||||
return parseStr(filterStr, {});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
apiSlug,
|
||||
rootUrl,
|
||||
|
||||
bakeFilterSearchParams,
|
||||
bakeFiltersWithPair,
|
||||
bakeUrl,
|
||||
fetchData,
|
||||
getFilterNameForTitle,
|
||||
getFilterParamsForUrl,
|
||||
parseFilterStr,
|
||||
};
|
||||
|
|
@ -1239,24 +1239,6 @@ Refer to [the list of supported currencies](https://wise.com/tools/exchange-rate
|
|||
|
||||
</Route>
|
||||
|
||||
## 正版中国 {#zheng-ban-zhong-guo}
|
||||
|
||||
### 分类列表 {#zheng-ban-zhong-guo-fen-lei-lie-biao}
|
||||
|
||||
<Route author="sanmmm" example="/getitfree/category/8" path="/getitfree/category/:category?" paramsDesc={['内容类型, 默认为`全部`']}>
|
||||
|
||||
类型
|
||||
|
||||
| 全部文章 | 永久免费 | 限时折扣 | 限时免费 | PC | Mac | Android | UWP |
|
||||
|----------|----------|----------|----------|----|-----|---------|-----|
|
||||
| all | 311 | 309 | 310 | 8 | 50 | 17 | 312 |
|
||||
|
||||
</Route>
|
||||
|
||||
### 搜索 {#zheng-ban-zhong-guo-sou-suo}
|
||||
|
||||
<Route author="sanmmm" example="/getitfree/search/windows" path="/getitfree/search/:keyword" paramsDesc={['搜索关键词']}/>
|
||||
|
||||
## 智联招聘 {#zhi-lian-zhao-pin}
|
||||
|
||||
### 搜索 {#zhi-lian-zhao-pin-sou-suo}
|
||||
|
|
|
|||
|
|
@ -711,11 +711,77 @@ For instance, in `https://www.zagg.com/en_us/new-arrivals?brand=164&cat=3038%2C3
|
|||
|
||||
### 分类 {#zheng-ban-zhong-guo-fen-lei}
|
||||
|
||||
<Route author="nczitzk" example="/getitfree" path="/getitfree/:category?" paramsDesc={['分类,见下表,默认为所有类别']}>
|
||||
<Route author="sanmmm nczitzk" example="/getitfree/category/pc" path="/getitfree/category/:id" paramsDesc={['分类,见下表,可在对应分类页中找到,默认为所有类别']} radar="1" rssbud="1">
|
||||
|
||||
| 所有类别 | Android | iOS | Mac | PC | UWP | 公告 | 永久免费 | 限时免费 | 限时折扣 |
|
||||
| -------- | ------- | --- | --- | -- | --- | ------------ | -------- | -------- | -------- |
|
||||
| | android | ios | mac | pc | uwp | notification | free | giveaway | discount |
|
||||
:::tip
|
||||
|
||||
可以叠加使用得到分类结果并集,如 [`/getitfree/category/pc,android`](https://rsshub.app/getitfree/category/pc,android)
|
||||
|
||||
:::
|
||||
|
||||
| 所有类别 | Android | iOS | Mac | PC | UWP | 公告 | 永久免费 | 限时免费 | 正版折扣 |
|
||||
| -------- | ------- | --- | --- | --- | --- | ------------ | -------- | -------- | -------- |
|
||||
| | android | ios | mac | pc | uwp | notification | free | giveaway | discount |
|
||||
|
||||
</Route>
|
||||
|
||||
### 标签 {#zheng-ban-zhong-guo-biao-qian}
|
||||
|
||||
<Route author="nczitzk" example="/getitfree/tag/ai" path="/getitfree/tag/:id" paramsDesc={['标签,见下表,可在对应标签页中找到,默认为所有标签']} radar="1" rssbud="1">
|
||||
|
||||
:::tip
|
||||
|
||||
可以叠加使用得到标签结果并集,如 [`/getitfree/tag/ai,office`](https://rsshub.app/getitfree/tag/ai,office)
|
||||
|
||||
:::
|
||||
|
||||
| AI | Android | DVD | GIF | iCloud |
|
||||
| --- | ------- | --- | --- | ------ |
|
||||
|
||||
| ICON | iOS | iPhone | Leawo | Mac |
|
||||
| ---- | --- | ------ | ----- | --- |
|
||||
|
||||
| Markdown | OCR | Office | Office 办公 | PDF |
|
||||
| -------- | --- | ------ | ----------- | --- |
|
||||
|
||||
| PDF 转换 | Windows | 下载 | 书签管理 | 健康 |
|
||||
| -------- | ------- | ---- | -------- | ---- |
|
||||
|
||||
| 全平台 | 办公软件 | 加密保护 | 卸载/清理/优化 | 卸载清理 |
|
||||
| ------ | -------- | -------- | -------------- | -------- |
|
||||
|
||||
| 图像处理 | 图片/编辑/管理 | 壁纸 | 备份/恢复/加密 | 备份恢复 |
|
||||
| -------- | -------------- | ---- | -------------- | -------- |
|
||||
|
||||
| 字体 | 学习 | 密码管理 | 导航 | 开始菜单 |
|
||||
| ---- | ---- | -------- | ---- | -------- |
|
||||
|
||||
| 录屏截图 | 影音/刻录/转换 | 投屏镜像 | 提醒 | 摄影 |
|
||||
| -------- | -------------- | -------- | ---- | ---- |
|
||||
|
||||
| 播放器 | 收藏夹 | 效率工具 | 数据传输 | 数据恢复 |
|
||||
| ------ | ------ | -------- | -------- | -------- |
|
||||
|
||||
| 数据擦除 | 文件管理 | 文档/转换/压缩 | 文档扫描 | 日历 |
|
||||
| -------- | -------- | -------------- | -------- | ---- |
|
||||
|
||||
| 杀毒防护 | 查重 | 正版特惠 | 水印 | 清理优化 |
|
||||
| -------- | ---- | -------- | ---- | -------- |
|
||||
|
||||
| 滤镜 | 硬件外设 | 硬盘/分区/驱动 | 硬盘检测 | 稍后阅读 |
|
||||
| ---- | -------- | -------------- | -------- | -------- |
|
||||
|
||||
| 笔记 | 系统工具 | 素材 | 网络/安全/传输 | 网络安全 |
|
||||
| ---- | -------- | ---- | -------------- | -------- |
|
||||
|
||||
| 色彩 | 视频剪辑 | 视频转换 | 记账 | 设计 |
|
||||
| ---- | -------- | -------- | ---- | ---- |
|
||||
|
||||
| 软件卸载 | 远程控制 | 音乐 | 音频 | 驱动 |
|
||||
| -------- | -------- | ---- | ---- | ---- |
|
||||
|
||||
</Route>
|
||||
|
||||
### 搜索 {#zheng-ban-zhong-guo-sou-suo}
|
||||
|
||||
<Route author="sanmmm nczitzk" example="/getitfree/search/windows" path="/getitfree/search/:keyword" paramsDesc={['关键字']} radar="1" rssbud="1"/>
|
||||
Loading…
Reference in New Issue