diff --git a/lib/v2/alternativeto/maintainer.js b/lib/v2/alternativeto/maintainer.js new file mode 100644 index 000000000..7d0330583 --- /dev/null +++ b/lib/v2/alternativeto/maintainer.js @@ -0,0 +1,4 @@ +module.exports = { + '/software/:name/:routeParams?': ['JimenezLi'], + '/platform/:name/:routeParams?': ['JimenezLi'], +}; diff --git a/lib/v2/alternativeto/platform.js b/lib/v2/alternativeto/platform.js new file mode 100644 index 000000000..fef2e5525 --- /dev/null +++ b/lib/v2/alternativeto/platform.js @@ -0,0 +1,32 @@ +const { baseURL, puppeteerGet } = require('./utils'); +const cheerio = require('cheerio'); + +module.exports = async (ctx) => { + const name = ctx.params.name; + const query = new URLSearchParams(ctx.params.routeParams); + const link = `https://alternativeto.net/platform/${name}/?${query.toString()}`; + + // use Puppeteer due to the obstacle by cloudflare challenge + const html = await puppeteerGet(link, ctx.cache); + const $ = cheerio.load(html); + + ctx.state.data = { + title: $('.Heading_h1___Cf5Y').text().trim(), + description: $('.intro-text').text().trim(), + link, + item: $('.AppListItem_appInfo__h9cWP') + .toArray() + .map((element) => { + const item = $(element); + const title = item.find('.Heading_h2___LwQD').text().trim(); + const link = `${baseURL}${item.find('.Heading_h2___LwQD a').attr('href')}`; + const description = item.find('.AppListItem_description__wtODK').text().trim(); + + return { + title, + link, + description, + }; + }), + }; +}; diff --git a/lib/v2/alternativeto/radar.js b/lib/v2/alternativeto/radar.js new file mode 100644 index 000000000..e33e25eaf --- /dev/null +++ b/lib/v2/alternativeto/radar.js @@ -0,0 +1,19 @@ +module.exports = { + 'alternativeto.net': { + _name: 'AlternativeTo', + www: [ + { + title: 'Software Alternatives', + docs: 'https://docs.rsshub.app/routes/programming#alternativeto-software-alternatives', + source: '/software/:name', + target: '/software/:name', + }, + { + title: 'Platform Software', + docs: 'https://docs.rsshub.app/routes/programming#alternativeto-platform-software', + source: '/platform/:name', + target: '/platform/:name', + }, + ], + }, +}; diff --git a/lib/v2/alternativeto/router.js b/lib/v2/alternativeto/router.js new file mode 100644 index 000000000..985663ff9 --- /dev/null +++ b/lib/v2/alternativeto/router.js @@ -0,0 +1,4 @@ +module.exports = (router) => { + router.get('/software/:name/:routeParams?', require('./software')); + router.get('/platform/:name/:routeParams?', require('./platform')); +}; diff --git a/lib/v2/alternativeto/software.js b/lib/v2/alternativeto/software.js new file mode 100644 index 000000000..a54d54de7 --- /dev/null +++ b/lib/v2/alternativeto/software.js @@ -0,0 +1,32 @@ +const { baseURL, puppeteerGet } = require('./utils'); +const cheerio = require('cheerio'); + +module.exports = async (ctx) => { + const name = ctx.params.name; + const query = new URLSearchParams(ctx.params.routeParams); + const link = `https://alternativeto.net/software/${name}/?${query.toString()}`; + + // use Puppeteer due to the obstacle by cloudflare challenge + const html = await puppeteerGet(link, ctx.cache); + const $ = cheerio.load(html); + + ctx.state.data = { + title: $('.Heading_h1___Cf5Y').text().trim(), + description: $('.intro-text').text().trim(), + link, + item: $('.AppListItem_appInfo__h9cWP') + .toArray() + .map((element) => { + const item = $(element); + const title = item.find('.Heading_h2___LwQD').text().trim(); + const link = `${baseURL}${item.find('.Heading_h2___LwQD a').attr('href')}`; + const description = item.find('.AppListItem_description__wtODK').text().trim(); + + return { + title, + link, + description, + }; + }), + }; +}; diff --git a/lib/v2/alternativeto/utils.js b/lib/v2/alternativeto/utils.js new file mode 100644 index 000000000..1ecbc0243 --- /dev/null +++ b/lib/v2/alternativeto/utils.js @@ -0,0 +1,22 @@ +const baseURL = 'https://alternativeto.net'; + +const puppeteerGet = (url, cache) => + cache.tryGet(url, async () => { + const browser = await require('@/utils/puppeteer')(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (request) => { + request.resourceType() === 'document' ? request.continue() : request.abort(); + }); + await page.goto(url, { + waitUntil: 'domcontentloaded', + }); + const html = await page.evaluate(() => document.documentElement.innerHTML); + browser.close(); + return html; + }); + +module.exports = { + baseURL, + puppeteerGet, +}; diff --git a/website/docs/routes/programming.md b/website/docs/routes/programming.md index c82c88b2f..6163e73cc 100644 --- a/website/docs/routes/programming.md +++ b/website/docs/routes/programming.md @@ -82,6 +82,24 @@ You have the option to utilize the main heading or use individual categories as +## AlternativeTo {#alternativeto} + +### Software Alternatives {#alternativeto-software-alternatives} + + + +> routeParms can be copied from original site URL, example: `/alternativeto/software/cpp/license=opensource&platform=windows` + + + +### Platform Software {#alternativeto-platform-software} + + + +> routeParms can be copied from original site URL, example: `/alternativeto/platform/firefox/license=free` + + + ## AtCoder {#atcoder} ### Present Contests {#atcoder-present-contests}