From 39e234f427087b5efb091560847d2b24c3d3a4f6 Mon Sep 17 00:00:00 2001 From: Zhoukun Cheng Date: Wed, 1 May 2024 23:01:15 +0800 Subject: [PATCH] feat(route): add new LinkedIn route params (#15395) --- lib/routes/linkedin/jobs.ts | 80 +++++++++++++++++++++++++++----- lib/routes/linkedin/namespace.ts | 2 +- lib/routes/linkedin/utils.ts | 17 ++++++- 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/lib/routes/linkedin/jobs.ts b/lib/routes/linkedin/jobs.ts index 9cf1fb9676d4..9b87514c3768 100644 --- a/lib/routes/linkedin/jobs.ts +++ b/lib/routes/linkedin/jobs.ts @@ -1,16 +1,21 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; -import { parseJobSearch, KEYWORDS_QUERY_KEY, JOB_TYPES, JOB_TYPES_QUERY_KEY, EXP_LEVELS_QUERY_KEY, parseParamsToSearchParams, EXP_LEVELS, parseParamsToString } from './utils'; +import { EXP_LEVELS, EXP_LEVELS_QUERY_KEY, JOB_TYPES, JOB_TYPES_QUERY_KEY, KEYWORDS_QUERY_KEY, parseJobSearch, parseParamsToSearchParams, parseParamsToString, parseRouteParam } from './utils'; const BASE_URL = 'https://www.linkedin.com/'; const JOB_SEARCH_PATH = '/jobs-guest/jobs/api/seeMoreJobPostings/search'; export const route: Route = { - path: '/jobs/:job_types/:exp_levels/:keywords?', - categories: ['other'], + path: '/jobs/:job_types/:exp_levels/:keywords?/:routeParams?', + categories: ['social-media'], example: '/linkedin/jobs/C-P/1/software engineer', - parameters: { job_types: "See the following table for details, use '-' as delimiter", exp_levels: "See the following table for details, use '-' as delimiter", keywords: 'keywords' }, + parameters: { + job_types: "See the following table for details, use '-' as delimiter", + exp_levels: "See the following table for details, use '-' as delimiter", + keywords: 'keywords', + routeParams: 'additional query parameters, see the table below', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -19,8 +24,29 @@ export const route: Route = { supportPodcast: false, supportScihub: false, }, + radar: [ + { + source: ['www.linkedin.com/jobs/search'], + // Migrate from https://github.com/DIYgod/RSSHub-Radar/blob/096589db99f993c262ec8edb51a5676325439bc5/src/lib/radar-rules.ts#L15501 + target: (params, url) => { + const searchParams = new URLSearchParams(new URL(url).search); + const fJT = parseRouteParam(searchParams.get('f_JT')); + const fE = parseRouteParam(searchParams.get('f_E')); + const keywords = encodeURIComponent(searchParams.get('keywords') || ''); + + const newSearchParams = new URLSearchParams(); + // Copy non-existent key-value pairs from searchParams to newSearchParams + for (const [key, value] of searchParams.entries()) { + if (!['f_JT', 'f_E', 'keywords'].includes(key)) { + newSearchParams.append(key, value); + } + } + return `/linkedin/jobs/${fJT}/${fE}/${keywords}/?${newSearchParams.toString()}`; + }, + }, + ], name: 'Jobs', - maintainers: [], + maintainers: ['BrandNewLifeJackie26', 'zhoukuncheng'], handler, description: `#### \`job_types\` list @@ -34,10 +60,34 @@ export const route: Route = { | --------- | ----------- | --------- | ---------------- | -------- | --- | | 1 | 2 | 3 | 4 | 5 | all | + #### \`routeParams\` additional query parameters + + ##### \`f_WT\` list + + | Onsite | Remote | Hybrid | + | ------ | ------- | ------ | + | 1 | 2 | 3 | + + ##### \`geoId\` + + Geographic location ID. You can find this ID in the URL of a LinkedIn job search page that is filtered by location. + + For example: + 91000012 is the ID of East Asia. + + ##### \`f_TPR\` + + Time posted range. Here are some possible values: + + * \`r86400\`: Past 24 hours + * \`r604800\`: Past week + * \`r2592000\`: Past month + For example: 1. If we want to search software engineer jobs of all levels and all job types, use \`/linkedin/jobs/all/all/software engineer\` 2. If we want to search all entry level contractor/part time software engineer jobs, use \`/linkedin/jobs/P-C/2/software engineer\` + 3. If we want to search remote mid-senior level software engineer jobs in APAC posted within the last month, use \`/linkedin/jobs/F/4/software%20engineer/f_WT=2&geoId=91000003&f_TPR=r2592000\` **To make it easier, the recommended way is to start a search on [LinkedIn](https://www.linkedin.com/jobs/search) and use [RSSHub Radar](https://github.com/DIYgod/RSSHub-Radar) to load the specific feed.**`, }; @@ -45,23 +95,31 @@ export const route: Route = { async function handler(ctx) { const jobTypesParam = parseParamsToSearchParams(ctx.req.param('job_types'), JOB_TYPES); const expLevelsParam = parseParamsToSearchParams(ctx.req.param('exp_levels'), EXP_LEVELS); + const routeParams = new URLSearchParams(ctx.req.param('routeParams')); let url = new URL(JOB_SEARCH_PATH, BASE_URL); + + // keep for backward compatibility url.searchParams.append(KEYWORDS_QUERY_KEY, ctx.req.param('keywords') || ''); url.searchParams.append(JOB_TYPES_QUERY_KEY, jobTypesParam); // see JOB_TYPES in utils.js url.searchParams.append(EXP_LEVELS_QUERY_KEY, expLevelsParam); // see EXPERIENCE_LEVELS in utils.js + + // Add route params to URL + for (const [key, value] of routeParams) { + if (!url.searchParams.has(key)) { + url.searchParams.append(key, value); + } + } url = url.toString(); // Parse job search page - const response = await got({ - method: 'get', - url, - }); - const jobs = parseJobSearch(response.data); + const response = await ofetch(url); + const jobs = parseJobSearch(response); const jobTypes = parseParamsToString(ctx.req.param('job_types'), JOB_TYPES); const expLevels = parseParamsToString(ctx.req.param('exp_levels'), EXP_LEVELS); const feedTitle = 'LinkedIn Job Listing' + (jobTypes ? ` | Job Types: ${jobTypes}` : '') + (expLevels ? ` | Experience Levels: ${expLevels}` : '') + (ctx.req.param('keywords') ? ` | Keywords: ${ctx.req.param('keywords')}` : ''); + return { title: feedTitle, link: url, diff --git a/lib/routes/linkedin/namespace.ts b/lib/routes/linkedin/namespace.ts index fb87f9995359..4069ccd7cd50 100644 --- a/lib/routes/linkedin/namespace.ts +++ b/lib/routes/linkedin/namespace.ts @@ -1,6 +1,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'LinkedIn 领英中国', + name: 'LinkedIn 领英', url: 'linkedin.com', }; diff --git a/lib/routes/linkedin/utils.ts b/lib/routes/linkedin/utils.ts index a4a67e449ee4..d069ddb2280a 100644 --- a/lib/routes/linkedin/utils.ts +++ b/lib/routes/linkedin/utils.ts @@ -40,6 +40,10 @@ const EXP_LEVELS = { * as search param in url */ function parseParamsToSearchParams(params, map) { + if (!params) { + return ''; + } // Handle undefined params + const validParamValues = params.split('-').filter((v) => v in map); return validParamValues.join(','); } @@ -54,6 +58,10 @@ function parseParamsToSearchParams(params, map) { * @returns param value strings separated by ',' */ function parseParamsToString(params, map) { + if (!params) { + return ''; + } // Handle undefined params + const validParamValues = params .split('-') .filter((v) => v in map) @@ -108,4 +116,11 @@ function parseJobDetail(data) { return job; } -export { parseParamsToSearchParams, parseParamsToString, parseJobDetail, parseJobSearch, JOB_TYPES, JOB_TYPES_QUERY_KEY, EXP_LEVELS, EXP_LEVELS_QUERY_KEY, KEYWORDS_QUERY_KEY }; +const parseRouteParam = (searchParam: string | null): string => { + if (!searchParam || typeof searchParam !== 'string') { + return 'all'; + } + return encodeURIComponent(searchParam.split(',').join('-')); +}; + +export { parseParamsToSearchParams, parseParamsToString, parseJobDetail, parseJobSearch, parseRouteParam, JOB_TYPES, JOB_TYPES_QUERY_KEY, EXP_LEVELS, EXP_LEVELS_QUERY_KEY, KEYWORDS_QUERY_KEY };