Skip to content

Commit

Permalink
feat(route): add new LinkedIn route params (#15395)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhoukuncheng committed May 1, 2024
1 parent e2f463a commit 39e234f
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 13 deletions.
80 changes: 69 additions & 11 deletions lib/routes/linkedin/jobs.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -34,34 +60,66 @@ 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.**`,
};

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,
Expand Down
2 changes: 1 addition & 1 deletion lib/routes/linkedin/namespace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Namespace } from '@/types';

export const namespace: Namespace = {
name: 'LinkedIn 领英中国',
name: 'LinkedIn 领英',
url: 'linkedin.com',
};
17 changes: 16 additions & 1 deletion lib/routes/linkedin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(',');
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 };

0 comments on commit 39e234f

Please sign in to comment.