From 67d4a7ed3f877a8ceac6caebe874c4ce5c210bd8 Mon Sep 17 00:00:00 2001 From: Ethan Shen <42264778+nczitzk@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:06:52 +0800 Subject: [PATCH] feat(route): add Metacritic (#13447) * feat(route): add Metacritic * fix typo * Update lib/v2/metacritic/index.js --------- --- lib/router.js | 2 +- lib/v2/metacritic/index.js | 121 ++++++++++++++++++++ lib/v2/metacritic/maintainer.js | 5 + lib/v2/metacritic/radar.js | 43 +++++++ lib/{routes => v2}/metacritic/release.js | 0 lib/v2/metacritic/router.js | 3 + lib/v2/metacritic/templates/description.art | 19 +++ lib/v2/metacritic/util.js | 42 +++++++ website/docs/routes/game.md | 26 ----- website/docs/routes/new-media.md | 56 +++++++++ 10 files changed, 290 insertions(+), 27 deletions(-) create mode 100644 lib/v2/metacritic/index.js create mode 100644 lib/v2/metacritic/maintainer.js create mode 100644 lib/v2/metacritic/radar.js rename lib/{routes => v2}/metacritic/release.js (100%) create mode 100644 lib/v2/metacritic/router.js create mode 100644 lib/v2/metacritic/templates/description.art create mode 100644 lib/v2/metacritic/util.js diff --git a/lib/router.js b/lib/router.js index 3abc28d7c..f44c806c1 100644 --- a/lib/router.js +++ b/lib/router.js @@ -992,7 +992,7 @@ router.get('/polimi/news/:language?', lazyloadRouteHandler('./routes/polimi/news router.get('/dekudeals/:type', lazyloadRouteHandler('./routes/dekudeals')); // Metacritic -router.get('/metacritic/release/:platform/:type/:sort?', lazyloadRouteHandler('./routes/metacritic/release')); +// router.get('/metacritic/release/:platform/:type/:sort?', lazyloadRouteHandler('./routes/metacritic/release')); // 快科技(原驱动之家) // router.get('/kkj/news', lazyloadRouteHandler('./routes/kkj/news')); diff --git a/lib/v2/metacritic/index.js b/lib/v2/metacritic/index.js new file mode 100644 index 000000000..519fb6729 --- /dev/null +++ b/lib/v2/metacritic/index.js @@ -0,0 +1,121 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const { parseDate } = require('@/utils/parse-date'); +const { art } = require('@/utils/render'); +const path = require('path'); + +const { sorts, types } = require('./util'); + +module.exports = async (ctx) => { + const { type = 'game', sort = 'new', filter } = ctx.params; + const limit = ctx.query.limit ? parseInt(ctx.query.limit, 10) : 50; + + const rootUrl = 'https://www.metacritic.com'; + const rootApiUrl = 'https://fandom-prod.apigee.net'; + const apiUrl = new URL('v1/xapi/finder/metacritic/web', rootApiUrl).href; + + const currentUrlObject = new URL(`/browse/${type}/all/all/all-time/${sort}/${filter ? `?${filter}` : ''}`, rootUrl); + const currentUrlParams = currentUrlObject.searchParams; + const currentUrl = currentUrlObject.href; + + const { data: currentResponse } = await got(currentUrl); + + const apiKey = currentResponse.match(/apiKey=(.*?)&/)[1]; + + const searchParams = { + sortBy: `-${sorts[sort].id}`, + productType: types[type].id, + limit, + apiKey, + }; + + const genres = currentUrlParams.getAll('genre').join(',').toLowerCase(); + const releaseTypes = currentUrlParams.getAll('releaseType').join(','); + + if (genres) { + searchParams.genres = genres; + } + + if (releaseTypes) { + searchParams.releaseType = releaseTypes; + } + + const platforms = currentUrlParams.getAll('platform'); + const networks = currentUrlParams.getAll('network'); + + if (platforms.length || networks.length) { + const labels = {}; + const labelPattern = '{label:"([^"]+)",value:(\\d+),href:a,meta:{mcDisplayWeight'; + + for (const m of currentResponse.match(new RegExp(labelPattern, 'g'))) { + const matches = m.match(new RegExp(labelPattern)); + + labels[ + matches[1] + .toLowerCase() + .split(/(\s\(|\\u002f(?!\s))/)[0] + .replace(/-/g, '---') + .replace(/\s\/\s/g, '-or-') + .replace(/\+/g, '-plus') + .replace(/\s/g, '-') + ] = matches[2]; + } + + if (platforms.length) { + searchParams.gamePlatformIds = platforms + .map((p) => (labels.hasOwnProperty(p) ? labels[p] : undefined)) + .filter((p) => p) + .join(','); + } + + if (networks.length) { + searchParams.streamingNetworkIds = networks + .map((p) => (labels.hasOwnProperty(p) ? labels[p] : undefined)) + .filter((p) => p) + .join(','); + } + } + + const { data: response } = await got(apiUrl, { + searchParams, + }); + + const items = response.data.items.slice(0, limit).map((item) => ({ + title: item.title, + link: new URL(`${type}/${item.slug}`, rootUrl).href, + description: art(path.join(__dirname, 'templates/description.art'), { + image: item.image + ? { + src: new URL(`a/img/catalog${item.image.bucketPath}`, rootUrl).href, + alt: item.image.alt, + } + : undefined, + description: item.description, + score: item.criticScoreSummary?.score ?? undefined, + }), + category: item.genres.map((c) => c.name), + guid: `metacritic-${item.id}`, + pubDate: parseDate(item.releaseDate), + upvotes: item.criticScoreSummary?.positiveCount ? parseInt(item.criticScoreSummary?.positiveCount, 10) : 0, + downvotes: item.criticScoreSummary?.negativeCount ? parseInt(item.criticScoreSummary?.negativeCount, 10) : 0, + comments: item.criticScoreSummary?.reviewCount ? parseInt(item.criticScoreSummary?.reviewCount, 10) : 0, + })); + + const $ = cheerio.load(currentResponse); + + const icon = new URL($('meta[data-hid="msapplication-task-metacritic"]').prop('content').split('icon-uri=').pop(), rootUrl).href; + + ctx.state.data = { + item: items, + title: $('title').text(), + link: currentUrl, + description: $('meta[name="description"]').prop('content'), + language: $('html').prop('lang'), + image: $('link[rel="icon"]').prop('content'), + icon, + logo: icon, + subtitle: $('meta[name="msapplication-tooltip"]').prop('content'), + author: $('meta[name="twitter:site"]').prop('content'), + allowEmpty: true, + }; +}; diff --git a/lib/v2/metacritic/maintainer.js b/lib/v2/metacritic/maintainer.js new file mode 100644 index 000000000..800f9838c --- /dev/null +++ b/lib/v2/metacritic/maintainer.js @@ -0,0 +1,5 @@ +module.exports = { + '/game/:sort?/:filter?': ['HenryQW', 'nczitzk'], + '/movie/:sort?/:filter?': ['nczitzk'], + '/tv/:sort?/:filter?': ['nczitzk'], +}; diff --git a/lib/v2/metacritic/radar.js b/lib/v2/metacritic/radar.js new file mode 100644 index 000000000..1f26d8901 --- /dev/null +++ b/lib/v2/metacritic/radar.js @@ -0,0 +1,43 @@ +module.exports = { + 'metacritic.com': { + _name: 'Metacritic', + '.': [ + { + title: 'Games', + docs: 'https://docs.rsshub.app/routes/new-media#metacritic-games', + source: ['/browse/game/:params*'], + target: (params, url) => { + url = new URL(url); + const sort = params.params.split(/\//).pop(); + const filter = url.searchParams.toString(); + + return `/metacritic/game${sort ? `/${sort}${filter ? `/${filter}` : ''}` : ''}`; + }, + }, + { + title: 'Movies', + docs: 'https://docs.rsshub.app/routes/new-media#metacritic-movies', + source: ['/browse/movie/:params*'], + target: (params, url) => { + url = new URL(url); + const sort = params.params.split(/\//).pop(); + const filter = url.searchParams.toString(); + + return `/metacritic/movie${sort ? `/${sort}${filter ? `/${filter}` : ''}` : ''}`; + }, + }, + { + title: 'TV Shows', + docs: 'https://docs.rsshub.app/routes/new-media#metacritic-tv-shows', + source: ['/browse/tv/:params*'], + target: (params, url) => { + url = new URL(url); + const sort = params.params.split(/\//).pop(); + const filter = url.searchParams.toString(); + + return `/metacritic/tv${sort ? `/${sort}${filter ? `/${filter}` : ''}` : ''}`; + }, + }, + ], + }, +}; diff --git a/lib/routes/metacritic/release.js b/lib/v2/metacritic/release.js similarity index 100% rename from lib/routes/metacritic/release.js rename to lib/v2/metacritic/release.js diff --git a/lib/v2/metacritic/router.js b/lib/v2/metacritic/router.js new file mode 100644 index 000000000..3990e3947 --- /dev/null +++ b/lib/v2/metacritic/router.js @@ -0,0 +1,3 @@ +module.exports = (router) => { + router.get('/:type?/:sort?/:filter?', require('./')); +}; diff --git a/lib/v2/metacritic/templates/description.art b/lib/v2/metacritic/templates/description.art new file mode 100644 index 000000000..1ee8bd077 --- /dev/null +++ b/lib/v2/metacritic/templates/description.art @@ -0,0 +1,19 @@ +{{ if image }} +
+ {{ image.alt }} +
+{{ /if }} + +{{ if description }} +

{{ description }}

+{{ /if }} + +{{ if score }} + Metascore: + {{ score }} +{{ /if }} \ No newline at end of file diff --git a/lib/v2/metacritic/util.js b/lib/v2/metacritic/util.js new file mode 100644 index 000000000..4ed8130a1 --- /dev/null +++ b/lib/v2/metacritic/util.js @@ -0,0 +1,42 @@ +const sorts = { + metascore: { + id: 'metaScore', + name: 'Metascore', + }, + userscore: { + id: 'userScore', + name: 'User Score', + }, + popular: { + id: 'popularityCount', + name: 'Most Popular', + }, + new: { + id: 'releaseDate', + name: 'Releases', + }, +}; + +const types = { + game: { + id: 'games', + name: 'Games', + }, + movie: { + id: 'movies', + name: 'Movies', + }, + tv: { + id: 'tv', + name: 'TV Shows', + }, + albums: { + id: 'albums', + name: 'Music', + }, +}; + +module.exports = { + sorts, + types, +}; diff --git a/website/docs/routes/game.md b/website/docs/routes/game.md index 6df19a1bf..b1fcff9d9 100644 --- a/website/docs/routes/game.md +++ b/website/docs/routes/game.md @@ -332,32 +332,6 @@ So the route is [`/itch/devlogs/teamterrible/the-baby-in-yellow`](https://rsshub -## Metacritic {#metacritic} - -### Game Releases {#metacritic-game-releases} - - - -Platforms supported: - -| PS 4 | Xbox One | Switch | PC | Wii U | 3DS | PS Vita | iOS | -| ---- | -------- | ------ | --- | ----- | --- | ------- | --- | -| ps4 | xboxone | switch | pc | wii-u | 3ds | vita | ios | - -Release types, default to `new`: - -| New | Coming Soon | All | -| --- | ----------- | --- | -| new | coming | all | - -Sorting types, default to `date`: - -| Date | Metacritic Score | User Score | -| ---- | ---------------- | ---------- | -| date | metascore | userscore | - - - ## Minecraft {#minecraft} ### Java Game Update {#minecraft-java-game-update} diff --git a/website/docs/routes/new-media.md b/website/docs/routes/new-media.md index 4ca95912b..ef1557afc 100644 --- a/website/docs/routes/new-media.md +++ b/website/docs/routes/new-media.md @@ -1298,6 +1298,62 @@ IPFS 网关有可能失效,那时候换成其他网关。 +## Metacritic {#metacritic} + +### Games {#metacritic-games} + + + +| Metascore | User Score | Most Popular | Newest Releases | +| --------- | ---------- | ------------ | --------------- | +| metascore | userscore | popular | new | + +:::tip + +The Filter parameter comes from the corresponding page URL. The following is an example: + +The URL of [Action Games to Play on PS5](https://www.metacritic.com/browse/game/all/all/all-time/new/?platform=ps5&genre=action) is . The Filter parameter is `platform=ps5&genre=action` and the route is [`/metacritic/game/new/platform=ps5&genre=action`](https://rsshub.app/metacritic/game/new/platform=ps5&genre=action) + +::: + + + +### Movies {#metacritic-movies} + + + +| Metascore | User Score | Most Popular | Newest Releases | +| --------- | ---------- | ------------ | --------------- | +| metascore | userscore | popular | new | + +:::tip + +The Filter parameter comes from the corresponding page URL. The following is an example: + +The URL of [Action Movies to Watch on Netflix](https://www.metacritic.com/browse/movie/all/all/all-time/new/?network=netflix&genre=action) is . The Filter parameter is `network=netflix&genre=action` and the route is [`/metacritic/movie/new/network=netflix&genre=action`](https://rsshub.app/metacritic/movie/new/network=netflix&genre=action) + +::: + + + +### TV Shows {#metacritic-tv-shows} + + + +| Metascore | User Score | Most Popular | Newest Releases | +| --------- | ---------- | ------------ | --------------- | +| metascore | userscore | popular | new | + +:::tip + +The Filter parameter comes from the corresponding page URL. The following is an example: + +The URL of [Documentary TV Shows to Watch on Prime Video](https://www.metacritic.com/browse/tv/all/all/all-time/new/?network=prime-video&genre=documentary) is . The Filter parameter is `network=prime-video&genre=documentary` and the route is [`/metacritic/tv/new/network=prime-video&genre=documentary`](https://rsshub.app/metacritic/tv/new/network=prime-video&genre=documentary) + +::: + + + ## Mirror {#mirror} ### User {#mirror-user}