feat(route): add Metacritic (#13447)

* feat(route): add Metacritic

* fix typo

* Update lib/v2/metacritic/index.js

---------
This commit is contained in:
Ethan Shen 2023-10-04 21:06:52 +08:00 committed by GitHub
parent 2775e7a47e
commit 67d4a7ed3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 290 additions and 27 deletions

View File

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

121
lib/v2/metacritic/index.js Normal file
View File

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

View File

@ -0,0 +1,5 @@
module.exports = {
'/game/:sort?/:filter?': ['HenryQW', 'nczitzk'],
'/movie/:sort?/:filter?': ['nczitzk'],
'/tv/:sort?/:filter?': ['nczitzk'],
};

View File

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

View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/:type?/:sort?/:filter?', require('./'));
};

View File

@ -0,0 +1,19 @@
{{ if image }}
<figure>
<img
src="{{ image.src }}"
{{ if image.alt }}
alt="{{ image.alt }}"
{{ /if }}
>
</figure>
{{ /if }}
{{ if description }}
<p>{{ description }}</p>
{{ /if }}
{{ if score }}
<span>Metascore:</span>
{{ score }}
{{ /if }}

42
lib/v2/metacritic/util.js Normal file
View File

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

View File

@ -332,32 +332,6 @@ So the route is [`/itch/devlogs/teamterrible/the-baby-in-yellow`](https://rsshub
<Route author="dearrrfish" example="/maxnews/dota2" path="maxnews/dota2" />
## Metacritic {#metacritic}
### Game Releases {#metacritic-game-releases}
<Route author="HenryQW" example="/metacritic/release/switch/coming" path="/metacritic/release/:platform/:type?/:sort?" paramsDesc={['console platform', 'release type, default to `new`', 'sorting type, default to `date`']}>
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 |
</Route>
## Minecraft {#minecraft}
### Java Game Update {#minecraft-java-game-update}

View File

@ -1298,6 +1298,62 @@ IPFS 网关有可能失效,那时候换成其他网关。
</Route>
## Metacritic {#metacritic}
### Games {#metacritic-games}
<Route author="HenryQW nczitzk" example="/metacritic/game" path="/metacritic/game/:sort?/:filter?" paramsDesc={['Sort, see below, `new` for Newest Releases by default', 'Filter']} radar="1" rssbud="1">
| 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 <https://www.metacritic.com/browse/game/all/all/all-time/new/?platform=ps5&genre=action>. 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)
:::
</Route>
### Movies {#metacritic-movies}
<Route author="nczitzk" example="/metacritic/movie" path="/metacritic/movie/:sort?/:filter?" paramsDesc={['Sort, see below, `new` for Newest Releases by default', 'Filter']} radar="1" rssbud="1">
| 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 <https://www.metacritic.com/browse/movie/all/all/all-time/new/?network=netflix&genre=action>. 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)
:::
</Route>
### TV Shows {#metacritic-tv-shows}
<Route author="nczitzk" example="/metacritic/tv" path="/metacritic/tv/:sort?/:filter?" paramsDesc={['Sort, see below, `new` for Newest Releases by default', 'Filter']} radar="1" rssbud="1">
| 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 <https://www.metacritic.com/browse/tv/all/all/all-time/new/?network=prime-video&genre=documentary>. 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)
:::
</Route>
## Mirror {#mirror}
### User {#mirror-user}