From d7b9887ee6252c1c31a513e3fa785d30071d025c Mon Sep 17 00:00:00 2001 From: jguitton Date: Wed, 27 May 2020 08:24:25 +0200 Subject: [PATCH 1/2] add opencritic ratings and extend game model and services with support for multiple ratings * opencritic client/service that searches game id and fetches review scores. * new service that fetches and aggregates ratings from all known rating providers * game rating as three-state: no rating (not found), rating without score (unrated), rating with score (rated). * games can have muliple ratings from different review providers. * game score is calculated from all ratings with score. * adjusted views (default and cards). * some logging changes --- models/Rating.js | 7 +++ routes/cardsRoute.js | 7 +-- routes/defaultRoute.js | 5 ++- servicelayer.js | 6 ++- services/gameService.js | 25 +++++++---- services/metacriticService.js | 19 +++++++- services/opencriticService.js | 79 ++++++++++++++++++++++++++++++++++ services/ratingService.js | 30 +++++++++++++ services/scoreUpdateService.js | 19 +++++++- settings.js | 1 + views/cards.pug | 7 +-- views/games.pug | 10 +++-- 12 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 models/Rating.js create mode 100644 services/opencriticService.js create mode 100644 services/ratingService.js diff --git a/models/Rating.js b/models/Rating.js new file mode 100644 index 0000000..6a8c7b0 --- /dev/null +++ b/models/Rating.js @@ -0,0 +1,7 @@ +function Rating(score, link, service) { + this.score = score; + this.link = link; + this.service = service; +} + +module.exports = Rating; \ No newline at end of file diff --git a/routes/cardsRoute.js b/routes/cardsRoute.js index 85ce588..fb140a6 100644 --- a/routes/cardsRoute.js +++ b/routes/cardsRoute.js @@ -2,9 +2,10 @@ module.exports = (dataService) => { return (req,res) => { - const ratedGames = dataService.getRatedGames().sort( (a, b) => b.score - a.score); - const unratedGames = dataService.getUnratedGames(); - const games = ratedGames.concat(unratedGames); + const ratedGames = dataService.getRatedGames().sort( (a, b) => b.rating.score - a.rating.score); + const withoutScore = dataService.getGamesWithoutScore(); + const unratedGames = dataService.getGamesWithoutRating(); + const games = ratedGames.concat(withoutScore, unratedGames); res.render('cards', { gamesList: games }); }; }; \ No newline at end of file diff --git a/routes/defaultRoute.js b/routes/defaultRoute.js index ee0cf85..b38890e 100644 --- a/routes/defaultRoute.js +++ b/routes/defaultRoute.js @@ -5,8 +5,9 @@ const fs = require('fs'); module.exports = (dataService, fetchService) => { return (req,res) => { const ratedGames = dataService.getRatedGames().sort( (a, b) => b.score - a.score); - const unratedGames = dataService.getUnratedGames(); - const games = ratedGames.concat(unratedGames); + const withoutScore = dataService.getGamesWithoutScore(); + const unratedGames = dataService.getGamesWithoutRating(); + const games = ratedGames.concat(withoutScore, unratedGames); const lastUpdate = fetchService.getLastFetchDate(); const versionInfo = Settings.buildVersion || Settings.buildVersion || 'local build'; res.render('games', { title: games.length + ' Games on Sale', gamesList: games, lastFetch: lastUpdate, version: versionInfo }); diff --git a/servicelayer.js b/servicelayer.js index a2ebade..8738bc3 100644 --- a/servicelayer.js +++ b/servicelayer.js @@ -2,10 +2,12 @@ module.exports = (dir) => { const nintendo = require(dir + '/nintendoShopService')(); const metacritic = require(dir + '/metacriticService')(); + const opencritic = require(dir + '/opencriticService')(); + const rating = require(dir + '/ratingService')(opencritic, metacritic); const data = require(dir + '/gameService')(); const saleHistory = require(dir + '/saleHistoryService')(); const fetch = require(dir + '/fetchService')(data, nintendo, saleHistory); - const scoreUpdate = require(dir + '/scoreUpdateService')(data, metacritic); + const scoreUpdate = require(dir + '/scoreUpdateService')(data, rating); const cronjobFetch = require(dir + '/fetchCronjob')(fetch); const cronjobScore = require(dir + '/scoreCronjob')(scoreUpdate); return { @@ -13,6 +15,8 @@ module.exports = (dir) => { saleHistory, nintendo, metacritic, + opencritic, + rating, fetch, scoreUpdate, cronjobFetch, diff --git a/services/gameService.js b/services/gameService.js index dc520a2..eb9f666 100644 --- a/services/gameService.js +++ b/services/gameService.js @@ -23,11 +23,21 @@ module.exports = () => { }, getRatedGames: () => { - return db.games.find().filter(g => (g.score != undefined)); + return db.games.find() + .filter(g => (g.rating !== undefined)) + .filter(g => (g.rating.score !== undefined)); }, - getUnratedGames: () => { - return db.games.find().filter(g => (g.score == undefined)); + // rating website link exists, but rating is not available yet + getGamesWithoutScore: () => { + return db.games.find() + .filter(g => (g.rating !== undefined)) + .filter(g => (g.rating.score === undefined)); + }, + + // could not be found at any rating website + getGamesWithoutRating: () => { + return db.games.find().filter(g => (g.rating === undefined)); }, getStats: () => { @@ -62,16 +72,13 @@ module.exports = () => { db.games.update(queryNotFound, update, options); }, - setMetacritInfo: (id, rating, url) => { - const score = (rating === 'tbd') ? 0 : rating; - + setRating: (id, rating) => { let query = { _id: id }; let update = { - score: score, - metacriticUrl: url - } + rating: rating + }; db.games.update(query, update); } } diff --git a/services/metacriticService.js b/services/metacriticService.js index cbe012d..a31abc2 100644 --- a/services/metacriticService.js +++ b/services/metacriticService.js @@ -1,5 +1,8 @@ 'use strict'; +const axios = require('axios').default; +const Rating = require('../models/Rating'); const Settings = require('../settings'); +const service = 'metacritic'; const metacriticScrape = require('./metacriticScrapeService'); const platformIdSwitch = 268409; @@ -24,8 +27,22 @@ function getSearchTitle(title) { .trim(); } +async function getRatingFor(title) { + try { + let rating = await getRatingForSwitchGame(title); // TODO + if (!rating.error && rating.known) { + let url = 'https://www.metacritic.com' + guessGameUrl('switch', title); + return new Rating(rating.score, url, service); + } + return new Rating(undefined, undefined, service); + } catch (error) { + return new Rating(undefined, undefined, service); + } +} + module.exports = () => { return { - searchSwitchGame + searchSwitchGame: searchSwitchGame, + getRatingFor: getRatingFor }; } \ No newline at end of file diff --git a/services/opencriticService.js b/services/opencriticService.js new file mode 100644 index 0000000..e6403a7 --- /dev/null +++ b/services/opencriticService.js @@ -0,0 +1,79 @@ +'use strict'; +const axios = require('axios').default; +const Rating = require('../models/Rating'); +const Settings = require('../settings'); +const service = 'opencritic'; + +async function searchGame(title) { + const titleEncoded = encodeURIComponent(title); + return await axios({ + method: 'get', + url: Settings.opencriticBase + '/meta/search', + params: { + criteria: titleEncoded + }, + }).then(response => { + // get first search result + let first = response.data[0]; + // check dist and discard if bigger than 0.59 + if (first.dist <= 0.59) { + return {error: false, id: first.id, name: first.name}; + } + console.log('[info] opencritic - ' + title + ' - not found - distance too high'); + return {error: false, id: undefined, name: undefined}; + }).catch( () => { + console.log('[error] opencritic - ' + title + ' - search request failed - criteria: ' + titleEncoded); + return {error: true, id: undefined, name: undefined}; + }); +} + +async function getScore(id) { + return await axios({ + method: 'get', + url: Settings.opencriticBase + '/game/' + id, + }).then(response => { + let score = (response.data.medianScore > -1) ? response.data.medianScore : undefined; + return {error: false, score: score}; + }).catch( () => { + console.log('[error] opencritic - score request failed - id: ' + id); + return {error: true, score: undefined}; + }); +} + +async function getRatingForWithErrors(title) { + let searchResult = await searchGame(title); + if (searchResult.error) { + return {error: true, rating: undefined}; + } + if (searchResult.id === undefined) { + return {error: false, rating: undefined}; + } + + let link = 'https://opencritic.com/game/' + searchResult.id + '/' + searchResult.name.replace(/ /g, "-"); + let rating = await getScore(searchResult.id); + if (rating.error) { + + return {error: true, rating: new Rating(undefined, link)}; + } + return {error: false, rating: new Rating(rating.score, link)}; +} + +async function getRatingFor(title) { + let searchResult = await searchGame(title); + if (searchResult.error || searchResult.id === undefined) { + return new Rating(undefined, undefined, service); + } + + let link = 'https://opencritic.com/game/' + searchResult.id + '/' + searchResult.name.replace(/ /g, "-"); + let rating = await getScore(searchResult.id); + if (rating.error) { + return new Rating(undefined, link, service); + } + return new Rating(rating.score, link, service); +} + +module.exports = () => { + return { + getRatingFor: getRatingFor + } +}; \ No newline at end of file diff --git a/services/ratingService.js b/services/ratingService.js new file mode 100644 index 0000000..1881d86 --- /dev/null +++ b/services/ratingService.js @@ -0,0 +1,30 @@ +'use strict'; +const Rating = require('../models/Rating'); + +module.exports = (opencritic, metacritic) => { + return { + getRatingFor: async (title) => { + let ratings = []; + let score = undefined; + + let queryOC = opencritic.getRatingFor(title); + let queryMC = metacritic.getRatingFor(title); + + let ratingOC = await queryOC; + let ratingMC = await queryMC; + [ratingOC, ratingMC].map((rating) => { + if (rating.link !== undefined) { + ratings.push(rating); + } + }); + + let scores = ratings.filter( x => x.score !== undefined ); + if (scores.length > 0) { + let sum = scores.reduce((previous, current) => ({ score: previous.score + current.score })); + score = sum.score / scores.length; + } + + return {score: score, ratings: ratings}; + } + } +}; \ No newline at end of file diff --git a/services/scoreUpdateService.js b/services/scoreUpdateService.js index 3c66b92..8fa929b 100644 --- a/services/scoreUpdateService.js +++ b/services/scoreUpdateService.js @@ -17,10 +17,25 @@ const getBestMatchTitle = (title, titlesFromMC) => { return bestMatch; } -module.exports = (dataService, metacriticService) => { +module.exports = (dataService, ratingService) => { return { lastUpdate: undefined, - checkAndUpdateScores: () => { + checkAndUpdateScores: async () => { + let games = dataService.getGamesWithoutRating(); + let failedGames = []; + await Promise.all(games.map( async game => { + let rating = await ratingService.getRatingFor(game.title); + if (rating.ratings && rating.ratings.length) { + dataService.setRating(game._id, rating); + } + else { + failedGames.push(game); + } + })); + console.log("Finished score update. Total failures: " + failedGames.length + " out of " + games.length); + return {checked: games.length, updated: (games.length - failedGames.length), failures: failedGames.length}; + }, + checkAndUpdateScoresDWBranch: () => { let games = dataService.getUnratedGames(); var i = 0; diff --git a/settings.js b/settings.js index dc59fde..b69c72a 100644 --- a/settings.js +++ b/settings.js @@ -2,5 +2,6 @@ require('dotenv').config({ silent: true }); module.exports = { port: process.env.PORT || 3000, + opencriticBase: process.env.API_BASE_OPENCRITIC || 'https://api.opencritic.com/api', buildVersion: process.env.IMAGE_VERSION || undefined }; \ No newline at end of file diff --git a/views/cards.pug b/views/cards.pug index efd31bd..63d309a 100644 --- a/views/cards.pug +++ b/views/cards.pug @@ -8,7 +8,8 @@ mixin game(item) .switcharoo-card_priceContainer(style='flex-grow: 1') mdc-typography.mdc-typography--headline6(style='margin-right: .31rem; opacity: .3; text-decoration: line-through') €#{item.priceRegular} span.mdc-typography.mdc-typography--headline6 €#{item.saleDetails.price} - .mdc-typography.mdc-typography--headline6 #{item.score} + if item.rating + .mdc-typography.mdc-typography--headline6 #{item.rating.score} .mdc-card__media.mdc-card__media--square(style='background-image: url(' + item.imageUrl + ')') .switcharoo-card__info(style='padding: 8px;') .mdc-typography.mdc-typography--headline6(style="font-size: .88rem;") #{item.title} @@ -17,8 +18,8 @@ mixin game(item) .mdc-card__action-icons a(href='https://www.nintendo.de' + item.nintendoUrl rel="noopener noreferrer" target="_blank") button.material-icons.mdc-icon-button.mdc-card__action.mdc-card__action--icon(title='Nintendo Store') storefront - if item.score != null - a(href=item.metacriticUrl rel="noopener noreferrer" target="_blank") + if item.rating != null + a(href=item.rating.ratings[0].link rel="noopener noreferrer" target="_blank") button.material-icons.mdc-icon-button.mdc-card__action.mdc-card__action--icon(title='Metacritic') show_chart block content diff --git a/views/games.pug b/views/games.pug index 4a28da2..e9f76bb 100644 --- a/views/games.pug +++ b/views/games.pug @@ -2,10 +2,12 @@ extends layout mixin game(item) tr(class="mdc-data-table__row").content - if item.score == null + if (item.rating == null) td - + else if (item.rating.score == null) + td tbd else - td.metascore #{item.score} + td.metascore #{item.rating.score} td #{item.discount} td #{item.title} td #{item.saleDetails.price} @@ -14,8 +16,8 @@ mixin game(item) td a(class="mdc-icon-button meterial-icons" href='https://www.nintendo.de' + item.nintendoUrl rel="noopener noreferrer" target="_blank") i(class="material-icons mdc-button__icon") storefront - if item.score != null && item.score > -1 - a(class="mdc-icon-button meterial-icons" href=item.metacriticUrl rel="noopener noreferrer" target="_blank") + if (item.rating != null && item.rating.ratings.length > 0) + a(class="mdc-icon-button meterial-icons" href=item.rating.ratings[0].link rel="noopener noreferrer" target="_blank") i(class="material-icons mdc-button__icon") show_chart block content From 26b6405c3809d582a82e2117973faeea5f9be0e6 Mon Sep 17 00:00:00 2001 From: jguitton Date: Mon, 8 Jun 2020 13:17:11 +0200 Subject: [PATCH 2/2] fixes after merge * change retry to reset resource * refactored callback handling for metacritics provider * tried to streamline promise handling * changed games model to more or less flat entity * adjusted games repo functions --- router.js | 2 +- routes/cardsRoute.js | 2 +- routes/defaultRoute.js | 2 +- routes/{retryRoute.js => resetRoute.js} | 4 +- servicelayer.js | 2 +- services/fetchService.js | 4 +- services/gameService.js | 57 +++++++------ services/metacriticScrapeService.js | 6 +- services/metacriticService.js | 47 ++++++++--- services/opencriticService.js | 76 ++++++++--------- services/ratingService.js | 103 ++++++++++++++++++------ services/scoreCronjob.js | 2 +- services/scoreUpdateService.js | 57 +------------ views/cards.pug | 12 ++- views/games.pug | 10 +-- 15 files changed, 206 insertions(+), 180 deletions(-) rename routes/{retryRoute.js => resetRoute.js} (56%) diff --git a/router.js b/router.js index 9e83f5f..b318af3 100644 --- a/router.js +++ b/router.js @@ -7,7 +7,7 @@ module.exports = (dir, s) => {//s is the services layer. router.get('/manual', require(dir + 'manualRoute.js')()); router.get('/cards', require(dir + 'cardsRoute.js')(s.data)); router.get('/stats', require(dir + 'statsRoute.js')(s.data)); - router.get('/retry', require(dir + 'retryRoute.js')(s.data, s.scoreUpdate)); + router.get('/reset', require(dir + 'resetRoute.js')(s.data, s.scoreUpdate)); //router.post('/updategame', require(dir + 'updateScoreForGame.js')(s.data, s.metacritic)); return router; }; \ No newline at end of file diff --git a/routes/cardsRoute.js b/routes/cardsRoute.js index fb140a6..2bf3956 100644 --- a/routes/cardsRoute.js +++ b/routes/cardsRoute.js @@ -2,7 +2,7 @@ module.exports = (dataService) => { return (req,res) => { - const ratedGames = dataService.getRatedGames().sort( (a, b) => b.rating.score - a.rating.score); + const ratedGames = dataService.getRatedGames().sort( (a, b) => b.rating_score - a.rating_score); const withoutScore = dataService.getGamesWithoutScore(); const unratedGames = dataService.getGamesWithoutRating(); const games = ratedGames.concat(withoutScore, unratedGames); diff --git a/routes/defaultRoute.js b/routes/defaultRoute.js index b38890e..273cb3d 100644 --- a/routes/defaultRoute.js +++ b/routes/defaultRoute.js @@ -4,7 +4,7 @@ const fs = require('fs'); module.exports = (dataService, fetchService) => { return (req,res) => { - const ratedGames = dataService.getRatedGames().sort( (a, b) => b.score - a.score); + const ratedGames = dataService.getRatedGames().sort( (a, b) => b.rating_score - a.rating_score); const withoutScore = dataService.getGamesWithoutScore(); const unratedGames = dataService.getGamesWithoutRating(); const games = ratedGames.concat(withoutScore, unratedGames); diff --git a/routes/retryRoute.js b/routes/resetRoute.js similarity index 56% rename from routes/retryRoute.js rename to routes/resetRoute.js index e73dc4b..4eefac8 100644 --- a/routes/retryRoute.js +++ b/routes/resetRoute.js @@ -3,8 +3,8 @@ module.exports = (gameService, scoreUpdateService) => { return async (req, res) => { - gameService.retry(); - scoreUpdateService.checkAndUpdateScores(); + gameService.resetRatingForGamesWithoutScore(); + await scoreUpdateService.checkAndUpdateScores(); res.sendStatus(202); }; }; \ No newline at end of file diff --git a/servicelayer.js b/servicelayer.js index 8738bc3..d2d432c 100644 --- a/servicelayer.js +++ b/servicelayer.js @@ -3,8 +3,8 @@ module.exports = (dir) => { const nintendo = require(dir + '/nintendoShopService')(); const metacritic = require(dir + '/metacriticService')(); const opencritic = require(dir + '/opencriticService')(); - const rating = require(dir + '/ratingService')(opencritic, metacritic); const data = require(dir + '/gameService')(); + const rating = require(dir + '/ratingService')(opencritic, metacritic, data); const saleHistory = require(dir + '/saleHistoryService')(); const fetch = require(dir + '/fetchService')(data, nintendo, saleHistory); const scoreUpdate = require(dir + '/scoreUpdateService')(data, rating); diff --git a/services/fetchService.js b/services/fetchService.js index e5d4f2a..cbea190 100644 --- a/services/fetchService.js +++ b/services/fetchService.js @@ -9,6 +9,8 @@ function Game(game) { this.imageUrl = 'https:' + game.image_url_sq_s; this.priceRegular = game.price_regular_f; this.nintendoUrl = game.url; + this.rating_available = false; + this.rating_hasScore = false; } function Sleep(milliseconds) { @@ -42,7 +44,7 @@ module.exports = (dataService, nintendoService, saleService) => { if (gamesToAdd != undefined && gamesToAdd.length >= 0) { const priceInfos = await nintendoService.getPriceInfoForGames(gamesToAdd.map(game => game.nsId)); gamesToAddWithSaleDetails = gamesToAdd - .filter(game => priceInfos.get(game.nsId) != undefined) // remove games without price discount + .filter(game => typeof priceInfos.get(game.nsId) !== 'undefined') // remove games without price discount .map(game => { const info = priceInfos.get(game.nsId); const historyEntry = saleService.addSale(game.nsId, new Sale(info.price, info.start, info.end)); diff --git a/services/gameService.js b/services/gameService.js index eb9f666..1158c58 100644 --- a/services/gameService.js +++ b/services/gameService.js @@ -23,61 +23,66 @@ module.exports = () => { }, getRatedGames: () => { - return db.games.find() - .filter(g => (g.rating !== undefined)) - .filter(g => (g.rating.score !== undefined)); + return db.games.find({rating_available: true, rating_hasScore: true}); }, // rating website link exists, but rating is not available yet getGamesWithoutScore: () => { - return db.games.find() - .filter(g => (g.rating !== undefined)) - .filter(g => (g.rating.score === undefined)); + return db.games.find({rating_available: true, rating_hasScore: false}); }, // could not be found at any rating website getGamesWithoutRating: () => { - return db.games.find().filter(g => (g.rating === undefined)); + return db.games.find({rating_available: false}); }, getStats: () => { - const tbd = db.games.find().filter(g => (g.score === 0)).length; - const notFound = db.games.find().filter(g => (g.score === -1)).length; - return { tbd: tbd, notFound: notFound } + const gameOnSale = db.games.count(); + const gamesWithRating = module.exports().getRatedGames().length; + const tbd = module.exports().getGamesWithoutScore().length; + const notFound = module.exports().getGamesWithoutRating().length; + return { notFound: notFound, unrated: tbd, rated: gamesWithRating, total: gameOnSale}; }, saveGame: (game) => { db.games.save(game); }, - retry: () => { - + // FIXME - resets everything. Seems like using a query with more than one parameter might be broken? + resetRatingForGamesWithoutScore: () => { const options = { multi: true - } - - const queryTba = { - score: 0 }; - - const queryNotFound = { - score: -1 + const queryWithoutScore = { + //rating_score: undefined + //not working as expected + rating_available: true, + rating_hasScore: false }; - const update = { - score: undefined - } + rating_providers: undefined, + rating_available: false, + rating_hasScore: false, + rating_score: undefined + }; + db.games.update(queryWithoutScore, update, options); + }, - db.games.update(queryTba, update, options); - db.games.update(queryNotFound, update, options); + getRatingsFromProviders: (id) => { + return db.games.findOne({ _id: id }).rating_providers; }, - setRating: (id, rating) => { + setRating: (id, score, ratings) => { + const hasScore = typeof score !== 'undefined'; + const hasRating = (typeof ratings !== 'undefined' && ratings.length > 0); let query = { _id: id }; let update = { - rating: rating + rating_available: hasRating, + rating_hasScore: hasScore, + rating_providers: ratings, + rating_score: score }; db.games.update(query, update); } diff --git a/services/metacriticScrapeService.js b/services/metacriticScrapeService.js index db132ac..1400e40 100644 --- a/services/metacriticScrapeService.js +++ b/services/metacriticScrapeService.js @@ -4,9 +4,9 @@ var request = require('request') , cheerio = require('cheerio') , extend = require('extend'); -var url = 'https://www.metacritic.com/' - , urlSearchAll = 'search/{0}/{1}/results' - , urlSearchAllByPlatform = 'search/{0}/{1}/results?search_type=advanced&plats[{2}]=1' +var url = 'https://www.metacritic.com' + , urlSearchAll = '/search/{0}/{1}/results' + , urlSearchAllByPlatform = '/search/{0}/{1}/results?search_type=advanced&plats[{2}]=1' , urlPage = '?page={0}' , currentPage = 0; diff --git a/services/metacriticService.js b/services/metacriticService.js index a31abc2..0bb58ac 100644 --- a/services/metacriticService.js +++ b/services/metacriticService.js @@ -1,20 +1,36 @@ 'use strict'; -const axios = require('axios').default; const Rating = require('../models/Rating'); -const Settings = require('../settings'); const service = 'metacritic'; const metacriticScrape = require('./metacriticScrapeService'); - +const { promisify } = require('util'); +const levenshtein = require('fast-levenshtein'); const platformIdSwitch = 268409; const categoryGame = 'game'; -const searchSwitchGame = async (title, cb) => { +const asyncScrapeSearch = promisify(metacriticScrape.Search); + +async function searchSwitchGame(title) { const searchTitle = getSearchTitle(title); console.log(`${title} = ${searchTitle}`); const options = { text: searchTitle, category: categoryGame, platformId: platformIdSwitch }; + return asyncScrapeSearch(options); +} + +const getBestMatchTitle = (title, titlesFromMC) => { + var lowestScore = 99; + var bestMatch = titlesFromMC[0]; - metacriticScrape.Search(options, cb); + titlesFromMC.forEach(game => { + const score = levenshtein.get(title, game.title); + if (score < lowestScore) { + lowestScore = score; + bestMatch = game; + } + }); + + console.log(`Found best match for "${title}" with a score of ${lowestScore}: "${bestMatch.title}"`); + return bestMatch; } function getSearchTitle(title) { @@ -29,14 +45,21 @@ function getSearchTitle(title) { async function getRatingFor(title) { try { - let rating = await getRatingForSwitchGame(title); // TODO - if (!rating.error && rating.known) { - let url = 'https://www.metacritic.com' + guessGameUrl('switch', title); - return new Rating(rating.score, url, service); + const list = await searchSwitchGame(title); + const bestMatch = getBestMatchTitle(title, list); + console.debug(`${title}: ${bestMatch.metascore}, ${bestMatch.link}`); + let score = (bestMatch.metascore !== 'tbd') ? parseInt(bestMatch.metascore) : undefined; + return new Rating(score, bestMatch.link, service); + } + catch (err) { + console.error(`failed to fetch score for "${title}": ${err}`); + if (err === 'No results') { + return new Rating(undefined, undefined, service); + } + else { + // error but no results... what should we do here? + return new Rating(undefined, undefined, service); } - return new Rating(undefined, undefined, service); - } catch (error) { - return new Rating(undefined, undefined, service); } } diff --git a/services/opencriticService.js b/services/opencriticService.js index e6403a7..9b3d3dc 100644 --- a/services/opencriticService.js +++ b/services/opencriticService.js @@ -5,15 +5,16 @@ const Settings = require('../settings'); const service = 'opencritic'; async function searchGame(title) { - const titleEncoded = encodeURIComponent(title); - return await axios({ - method: 'get', - url: Settings.opencriticBase + '/meta/search', - params: { - criteria: titleEncoded - }, - }).then(response => { - // get first search result + try { + const titleEncoded = encodeURIComponent(title); + const response = await axios({ + method: 'get', + url: Settings.opencriticBase + '/meta/search', + params: { + criteria: titleEncoded + }, + }); + let first = response.data[0]; // check dist and discard if bigger than 0.59 if (first.dist <= 0.59) { @@ -21,55 +22,42 @@ async function searchGame(title) { } console.log('[info] opencritic - ' + title + ' - not found - distance too high'); return {error: false, id: undefined, name: undefined}; - }).catch( () => { + } + catch (e) { console.log('[error] opencritic - ' + title + ' - search request failed - criteria: ' + titleEncoded); - return {error: true, id: undefined, name: undefined}; - }); + throw Error('opencritic - ' + title + ' - search request failed - criteria: ' + titleEncoded); + } } async function getScore(id) { - return await axios({ - method: 'get', - url: Settings.opencriticBase + '/game/' + id, - }).then(response => { + try { + const response = await axios({ + method: 'get', + url: Settings.opencriticBase + '/game/' + id, + }); let score = (response.data.medianScore > -1) ? response.data.medianScore : undefined; return {error: false, score: score}; - }).catch( () => { - console.log('[error] opencritic - score request failed - id: ' + id); - return {error: true, score: undefined}; - }); -} - -async function getRatingForWithErrors(title) { - let searchResult = await searchGame(title); - if (searchResult.error) { - return {error: true, rating: undefined}; } - if (searchResult.id === undefined) { - return {error: false, rating: undefined}; - } - - let link = 'https://opencritic.com/game/' + searchResult.id + '/' + searchResult.name.replace(/ /g, "-"); - let rating = await getScore(searchResult.id); - if (rating.error) { - - return {error: true, rating: new Rating(undefined, link)}; + catch (e) { + console.log('[error] opencritic - score request failed - id: ' + id); + throw Error('opencritic - score request failed - id: ' + id); } - return {error: false, rating: new Rating(rating.score, link)}; } async function getRatingFor(title) { - let searchResult = await searchGame(title); - if (searchResult.error || searchResult.id === undefined) { - return new Rating(undefined, undefined, service); + let link = undefined; + try { + const searchResult = await searchGame(title); + if (typeof searchResult.id === 'undefined') { + return new Rating(undefined, undefined, service); + } + link = 'https://opencritic.com/game/' + searchResult.id + '/' + searchResult.name.replace(/ /g, "-"); + let rating = await getScore(searchResult.id); + return new Rating(rating.score, link, service); } - - let link = 'https://opencritic.com/game/' + searchResult.id + '/' + searchResult.name.replace(/ /g, "-"); - let rating = await getScore(searchResult.id); - if (rating.error) { + catch (e) { return new Rating(undefined, link, service); } - return new Rating(rating.score, link, service); } module.exports = () => { diff --git a/services/ratingService.js b/services/ratingService.js index 1881d86..3615297 100644 --- a/services/ratingService.js +++ b/services/ratingService.js @@ -1,30 +1,87 @@ 'use strict'; -const Rating = require('../models/Rating'); -module.exports = (opencritic, metacritic) => { - return { - getRatingFor: async (title) => { - let ratings = []; - let score = undefined; - - let queryOC = opencritic.getRatingFor(title); - let queryMC = metacritic.getRatingFor(title); - - let ratingOC = await queryOC; - let ratingMC = await queryMC; - [ratingOC, ratingMC].map((rating) => { - if (rating.link !== undefined) { - ratings.push(rating); - } - }); - - let scores = ratings.filter( x => x.score !== undefined ); - if (scores.length > 0) { - let sum = scores.reduce((previous, current) => ({ score: previous.score + current.score })); - score = sum.score / scores.length; +/** + * + * @param providerQueries + * @returns average Score and ratings obtained from game rating providers + * + * Tries to query all rating providers and calculates the average score from all obtained ratings. But it might + * occur that the game is listed at the given providers, but hasn't got any score yet. In such cases the returned + * average score will be set to '-1'. + */ +async function getRatingFromProviders(providerQueries) { + try { + let ratings = []; + let avgScore = -1; + + const results = await Promise.all(providerQueries); + results.map((rating) => { + if (typeof rating.link !== 'undefined') { + ratings.push(rating); } + }); + + let scores = ratings.filter( x => typeof x.score !== 'undefined' ); + if (scores.length > 0) { + let sum = scores.reduce((previous, current) => ({ score: previous.score + current.score })); + avgScore = sum.score / scores.length; + } + + return {avgScore: avgScore, ratings: ratings}; + } + catch (err) { + console.log(err); + } +} + +function getRatingUpdate(ratingsFromRepo, ratingFromProvider) { + let ratingsToReturn = []; + let avgScore = undefined; - return {score: score, ratings: ratings}; + // add rating from provider or replace existing rating + if (typeof ratingsFromRepo !== 'undefined') { + ratingsToReturn = ratingsFromRepo.filter(entry => entry.service !== ratingFromProvider.service) + } + if (typeof ratingFromProvider.link !== 'undefined') { + // add rating + ratingsToReturn.push(ratingFromProvider); + } + else { + // game wasn't found + } + + // update average score + let scores = ratingsToReturn.filter( x => typeof x.score !== 'undefined' ); + if (scores.length > 0) { + let sum = scores.reduce((previous, current) => ({ score: previous.score + current.score })); + avgScore = Math.round(sum.score / scores.length); + } + + return {score: avgScore, ratings: ratingsToReturn}; +} + +module.exports = (opencritic, metacritic, gameRepo) => { + return { + updateRatingOf: async (game) => { + opencritic.getRatingFor(game.title) + .then( (rating) => { + const ratingsFromRepo = gameRepo.getRatingsFromProviders(game._id); + const update = getRatingUpdate(ratingsFromRepo, rating); + gameRepo.setRating(game._id, update.score, update.ratings); + }); + metacritic.getRatingFor(game.title) + .then((rating) => { + const ratingsFromRepo = gameRepo.getRatingsFromProviders(game._id); + const update = getRatingUpdate(ratingsFromRepo, rating); + gameRepo.setRating(game._id, update.score, update.ratings); + }); + // let queryOC = opencritic.getRatingFor(game.title); + //let queryMC = metacritic.getRatingFor(game.title); + // const rating = await getRatingFromProviders([queryOC, queryMC]); + // update rating only if any rating (with or without score) was returned from the given rating providers + // if (typeof rating.ratings !== 'undefined' && rating.ratings.length) { + // gameRepo.setRating(game._id, rating); + // } } } }; \ No newline at end of file diff --git a/services/scoreCronjob.js b/services/scoreCronjob.js index 307bb8f..391da05 100644 --- a/services/scoreCronjob.js +++ b/services/scoreCronjob.js @@ -5,6 +5,6 @@ const ScoreCronjob = require('cron').CronJob; module.exports = async (scoreUpdateService) => { return new ScoreCronjob('0 */5 * * * *', async function () { console.log('cron job started - score update using metacritic'); - scoreUpdateService.checkAndUpdateScores(); + await scoreUpdateService.checkAndUpdateScores(); }, null, true, ''); }; \ No newline at end of file diff --git a/services/scoreUpdateService.js b/services/scoreUpdateService.js index 8fa929b..0733d7b 100644 --- a/services/scoreUpdateService.js +++ b/services/scoreUpdateService.js @@ -1,66 +1,13 @@ 'use strict'; -const levenshtein = require('fast-levenshtein'); - -const getBestMatchTitle = (title, titlesFromMC) => { - var lowestScore = 99; - var bestMatch = titlesFromMC[0]; - - titlesFromMC.forEach(game => { - const score = levenshtein.get(title, game.title); - if (score < lowestScore) { - lowestScore = score; - bestMatch = game; - } - }); - - console.log(`Found best match for "${title}" with a score of ${lowestScore}: "${bestMatch.title}"`); - return bestMatch; -} module.exports = (dataService, ratingService) => { return { lastUpdate: undefined, checkAndUpdateScores: async () => { let games = dataService.getGamesWithoutRating(); - let failedGames = []; - await Promise.all(games.map( async game => { - let rating = await ratingService.getRatingFor(game.title); - if (rating.ratings && rating.ratings.length) { - dataService.setRating(game._id, rating); - } - else { - failedGames.push(game); - } - })); - console.log("Finished score update. Total failures: " + failedGames.length + " out of " + games.length); - return {checked: games.length, updated: (games.length - failedGames.length), failures: failedGames.length}; - }, - checkAndUpdateScoresDWBranch: () => { - let games = dataService.getUnratedGames(); - - var i = 0; - - games.slice(0, 100).map(game => { - metacriticService.searchSwitchGame(game.title, (err, list) => { - if (err) { - console.error(`failed to fetch score for "${game.title}": ${err}`); - if (err === 'No results') { - dataService.setMetacritInfo(game._id, -1, null); - } - } else if (list && list[0]) { - const bestMatch = getBestMatchTitle(game.title, list); - - console.debug(`${game.title}: ${bestMatch.metascore}, ${bestMatch.link}`); - dataService.setMetacritInfo(game._id, bestMatch.metascore, bestMatch.link); - } - }); + games.slice(0,100).map(async game => { + await ratingService.updateRatingOf(game); }) - }, - getLastUpdateDate: () => { - if (this.lastUpdate != null) - return Date(this.lastUpdate).toString(); - else - return 'not updated yet' } } }; \ No newline at end of file diff --git a/views/cards.pug b/views/cards.pug index 63d309a..2169a86 100644 --- a/views/cards.pug +++ b/views/cards.pug @@ -8,8 +8,12 @@ mixin game(item) .switcharoo-card_priceContainer(style='flex-grow: 1') mdc-typography.mdc-typography--headline6(style='margin-right: .31rem; opacity: .3; text-decoration: line-through') €#{item.priceRegular} span.mdc-typography.mdc-typography--headline6 €#{item.saleDetails.price} - if item.rating - .mdc-typography.mdc-typography--headline6 #{item.rating.score} + if item.rating_hasScore + .mdc-typography.mdc-typography--headline6 #{item.rating_score} + else if item.rating_available + .mdc-typography.mdc-typography--headline6 tbd + else + .mdc-typography.mdc-typography--headline6 ? .mdc-card__media.mdc-card__media--square(style='background-image: url(' + item.imageUrl + ')') .switcharoo-card__info(style='padding: 8px;') .mdc-typography.mdc-typography--headline6(style="font-size: .88rem;") #{item.title} @@ -18,8 +22,8 @@ mixin game(item) .mdc-card__action-icons a(href='https://www.nintendo.de' + item.nintendoUrl rel="noopener noreferrer" target="_blank") button.material-icons.mdc-icon-button.mdc-card__action.mdc-card__action--icon(title='Nintendo Store') storefront - if item.rating != null - a(href=item.rating.ratings[0].link rel="noopener noreferrer" target="_blank") + if item.rating_available + a(href=item.rating_providers[0].link rel="noopener noreferrer" target="_blank") button.material-icons.mdc-icon-button.mdc-card__action.mdc-card__action--icon(title='Metacritic') show_chart block content diff --git a/views/games.pug b/views/games.pug index e9f76bb..a4ca434 100644 --- a/views/games.pug +++ b/views/games.pug @@ -2,12 +2,12 @@ extends layout mixin game(item) tr(class="mdc-data-table__row").content - if (item.rating == null) + if !item.rating_available td - - else if (item.rating.score == null) + else if !item.rating_hasScore td tbd else - td.metascore #{item.rating.score} + td.metascore #{item.rating_score} td #{item.discount} td #{item.title} td #{item.saleDetails.price} @@ -16,8 +16,8 @@ mixin game(item) td a(class="mdc-icon-button meterial-icons" href='https://www.nintendo.de' + item.nintendoUrl rel="noopener noreferrer" target="_blank") i(class="material-icons mdc-button__icon") storefront - if (item.rating != null && item.rating.ratings.length > 0) - a(class="mdc-icon-button meterial-icons" href=item.rating.ratings[0].link rel="noopener noreferrer" target="_blank") + if rating_available + a(class="mdc-icon-button meterial-icons" href=item.rating_providers[0].link rel="noopener noreferrer" target="_blank") i(class="material-icons mdc-button__icon") show_chart block content