diff --git a/lib/router.js b/lib/router.js
index b92da7ecc630a5..3e78f10e4c50f6 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -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'));
diff --git a/lib/routes/getitfree/category.js b/lib/routes/getitfree/category.js
deleted file mode 100644
index a76536bc3b2a2f..00000000000000
--- a/lib/routes/getitfree/category.js
+++ /dev/null
@@ -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, ``].filter((str) => !!str).join('
');
- 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,
- };
-};
diff --git a/lib/routes/getitfree/search.js b/lib/routes/getitfree/search.js
deleted file mode 100644
index a419195cdafc95..00000000000000
--- a/lib/routes/getitfree/search.js
+++ /dev/null
@@ -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,
- };
-};
diff --git a/lib/routes/getitfree/utils.js b/lib/routes/getitfree/utils.js
deleted file mode 100644
index 0354ea1cb3677d..00000000000000
--- a/lib/routes/getitfree/utils.js
+++ /dev/null
@@ -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, ``].filter((str) => !!str).join('
'),
- };
- })
- .get();
-
-module.exports = {
- getDeadlineStr,
- parseListItem,
-};
diff --git a/lib/v2/getitfree/index.js b/lib/v2/getitfree/index.js
index 9d9bd971ed1721..2f0097f1bcbc52 100644
--- a/lib/v2/getitfree/index.js
+++ b/lib/v2/getitfree/index.js
@@ -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);
- const $ = cheerio.load(response.data);
+ apiSearchParams.append('_embed', 'true');
+ apiSearchParams.append('per_page', limit);
- let items = $('a.post-title')
- .toArray()
- .map((item) => {
- item = $(item);
+ const apiUrl = bakeUrl(`${apiSlug}/posts`, rootUrl, apiSearchParams);
+ const currentUrl = bakeUrl(getFilterParamsForUrl(filtersWithPair) ?? '', rootUrl, searchParams);
- return {
- title: item.text(),
- link: item.attr('href'),
- };
- });
+ const { data: response } = await got(apiUrl);
- items = await Promise.all(
- items.map((item) =>
- ctx.cache.tryGet(item.link, async () => {
- const detailResponse = await got({
- method: 'get',
- url: item.link,
- });
+ 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(detailResponse.data);
+ const content = cheerio.load(item.content?.rendered ?? item.content);
- content('h3, footer').remove();
+ content('div.mycred-sell-this-wrapper').prevUntil('hr').nextAll().remove();
- item.description = content('.post-content-text').html();
- item.pubDate = parseDate(content('time.entry-date').first().attr('datetime'));
+ 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),
+ };
+ });
- return item;
- })
- )
- );
+ const subtitle = getFilterNameForTitle(filtersWithPair);
ctx.state.data = {
- title: $('title').text(),
- link: currentUrl,
+ ...(await fetchData(currentUrl)),
item: items,
+ title: `Getitfree${subtitle ? ` | ${subtitle}` : ''}`,
};
};
diff --git a/lib/v2/getitfree/maintainer.js b/lib/v2/getitfree/maintainer.js
index 81eb78d7328b63..3eb26bc9ad38ce 100644
--- a/lib/v2/getitfree/maintainer.js
+++ b/lib/v2/getitfree/maintainer.js
@@ -1,3 +1,5 @@
module.exports = {
- '/:category?': ['nczitzk'],
+ '/category/:id': ['sanmmm', 'nczitzk'],
+ '/tag/:id': ['nczitzk'],
+ '/search/:keyword': ['sanmmm', 'nczitzk'],
};
diff --git a/lib/v2/getitfree/radar.js b/lib/v2/getitfree/radar.js
index 5f46146b76751b..d8a6b5315c5bbd 100644
--- a/lib/v2/getitfree/radar.js
+++ b/lib/v2/getitfree/radar.js
@@ -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}` : ''}`;
+ },
},
],
},
diff --git a/lib/v2/getitfree/router.js b/lib/v2/getitfree/router.js
index cf781366fda0a7..c84aaedb2a0785 100644
--- a/lib/v2/getitfree/router.js
+++ b/lib/v2/getitfree/router.js
@@ -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);
+ }
+ });
};
diff --git a/lib/v2/getitfree/util.js b/lib/v2/getitfree/util.js
new file mode 100644
index 00000000000000..f053ceebff4f0c
--- /dev/null
+++ b/lib/v2/getitfree/util.js
@@ -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