feat(route): add 正版中国分类 & 标签 & 搜索 (#13769)

* feat(route): add 正版中国分类 & 标签 & 搜索

* fix: remove unnecessary parameter

---------
This commit is contained in:
Ethan Shen 2023-11-14 02:05:44 +08:00 committed by GitHub
parent 073021d5b2
commit 4cd614fea2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 478 additions and 200 deletions

View File

@ -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'));

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};

View File

@ -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}` : ''}`,
};
};

View File

@ -1,3 +1,5 @@
module.exports = {
'/:category?': ['nczitzk'],
'/category/:id': ['sanmmm', 'nczitzk'],
'/tag/:id': ['nczitzk'],
'/search/:keyword': ['sanmmm', 'nczitzk'],
};

View File

@ -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}` : ''}`;
},
},
],
},

View File

@ -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);
}
});
};

326
lib/v2/getitfree/util.js Normal file
View File

@ -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,
};

View File

@ -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}

View File

@ -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"/>