new data model: igdb steam douban_game

This commit is contained in:
Your Name 2022-12-08 23:58:44 +00:00
parent 838d392056
commit d9639c6024
16 changed files with 5142 additions and 14 deletions

View file

@ -5,4 +5,4 @@ from .scrapers import *
from . import jsondata from . import jsondata
__all__ = ('IdType', 'Item', 'ExternalResource', 'ResourceContent', 'ParseError', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'get_mock_mode', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP') __all__ = ('IdType', 'Item', 'ExternalResource', 'ResourceContent', 'ParseError', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'get_mock_mode', 'get_mock_file', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP')

View file

@ -42,6 +42,11 @@ def get_mock_mode():
return _mock_mode return _mock_mode
def get_mock_file(url):
fn = re.sub(r'[^\w]', '_', url)
return re.sub(r'_key_[A-Za-z0-9]+', '_key_19890604', fn)
class DownloadError(Exception): class DownloadError(Exception):
def __init__(self, downloader, msg=None): def __init__(self, downloader, msg=None):
self.url = downloader.url self.url = downloader.url
@ -95,7 +100,7 @@ class BasicDownloader:
# TODO cache = get/set from redis # TODO cache = get/set from redis
resp = requests.get(url, headers=self.headers, timeout=self.get_timeout()) resp = requests.get(url, headers=self.headers, timeout=self.get_timeout())
if settings.DOWNLOADER_SAVEDIR: if settings.DOWNLOADER_SAVEDIR:
with open(settings.DOWNLOADER_SAVEDIR + '/' + re.sub(r'[^\w]', '_', url), 'w', encoding='utf-8') as fp: with open(settings.DOWNLOADER_SAVEDIR + '/' + get_mock_file(url), 'w', encoding='utf-8') as fp:
fp.write(resp.text) fp.write(resp.text)
else: else:
resp = MockResponse(self.url) resp = MockResponse(self.url)
@ -191,7 +196,15 @@ class ImageDownloaderMixin:
class BasicImageDownloader(ImageDownloaderMixin, BasicDownloader): class BasicImageDownloader(ImageDownloaderMixin, BasicDownloader):
pass @classmethod
def download_image(cls, image_url, page_url):
imgdl = cls(image_url, page_url)
try:
image = imgdl.download().content
image_extention = imgdl.extention
return image, image_extention
except Exception:
return None, None
class ProxiedImageDownloader(ImageDownloaderMixin, ProxiedDownloader): class ProxiedImageDownloader(ImageDownloaderMixin, ProxiedDownloader):
@ -202,13 +215,9 @@ _local_response_path = str(Path(__file__).parent.parent.parent.absolute()) + '/t
class MockResponse: class MockResponse:
def get_mock_file(self, url):
fn = _local_response_path + re.sub(r'[^\w]', '_', url)
return re.sub(r'_key_[A-Za-z0-9]+', '_key_19890604', fn)
def __init__(self, url): def __init__(self, url):
self.url = url self.url = url
fn = self.get_mock_file(url) fn = _local_response_path + get_mock_file(url)
try: try:
self.content = Path(fn).read_bytes() self.content = Path(fn).read_bytes()
self.status_code = 200 self.status_code = 200

View file

@ -2,10 +2,7 @@ from catalog.common import *
class Game(Item): class Game(Item):
igdb = LookupIdDescriptor(IdType.IGDB) igdb = PrimaryLookupIdDescriptor(IdType.IGDB)
steam = LookupIdDescriptor(IdType.Steam) steam = PrimaryLookupIdDescriptor(IdType.Steam)
douban_game = LookupIdDescriptor(IdType.DoubanGame) douban_game = PrimaryLookupIdDescriptor(IdType.DoubanGame)
platforms = jsondata.ArrayField(default=list) platforms = jsondata.ArrayField(default=list)
class Meta:
proxy = True

99
catalog/game/tests.py Normal file
View file

@ -0,0 +1,99 @@
from django.test import TestCase
from catalog.common import *
from catalog.models import *
class IGDBTestCase(TestCase):
def test_parse(self):
t_id_type = IdType.IGDB
t_id_value = 'portal-2'
t_url = 'https://www.igdb.com/games/portal-2'
site = SiteList.get_site_by_id_type(t_id_type)
self.assertIsNotNone(site)
self.assertEqual(site.validate_url(t_url), True)
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.url, t_url)
self.assertEqual(site.id_value, t_id_value)
@use_local_response
def test_scrape(self):
t_url = 'https://www.igdb.com/games/portal-2'
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.ready, False)
site.get_resource_ready()
self.assertEqual(site.ready, True)
self.assertEqual(site.resource.metadata['title'], 'Portal 2')
self.assertIsInstance(site.resource.item, Game)
self.assertEqual(site.resource.item.steam, '620')
@use_local_response
def test_scrape_non_steam(self):
t_url = 'https://www.igdb.com/games/the-legend-of-zelda-breath-of-the-wild'
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.ready, False)
site.get_resource_ready()
self.assertEqual(site.ready, True)
self.assertEqual(site.resource.metadata['title'], 'The Legend of Zelda: Breath of the Wild')
self.assertIsInstance(site.resource.item, Game)
self.assertEqual(site.resource.item.primary_lookup_id_type, IdType.IGDB)
self.assertEqual(site.resource.item.primary_lookup_id_value, 'the-legend-of-zelda-breath-of-the-wild')
class SteamTestCase(TestCase):
def test_parse(self):
t_id_type = IdType.Steam
t_id_value = '620'
t_url = 'https://store.steampowered.com/app/620/Portal_2/'
t_url2 = 'https://store.steampowered.com/app/620'
site = SiteList.get_site_by_id_type(t_id_type)
self.assertIsNotNone(site)
self.assertEqual(site.validate_url(t_url), True)
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.url, t_url2)
self.assertEqual(site.id_value, t_id_value)
@use_local_response
def test_scrape(self):
t_url = 'https://store.steampowered.com/app/620/Portal_2/'
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.ready, False)
site.get_resource_ready()
self.assertEqual(site.ready, True)
self.assertEqual(site.resource.metadata['title'], 'Portal 2')
self.assertEqual(site.resource.metadata['brief'], '“终身测试计划”现已升级,您可以为您自己或您的好友设计合作谜题!')
self.assertIsInstance(site.resource.item, Game)
self.assertEqual(site.resource.item.steam, '620')
class DoubanGameTestCase(TestCase):
def test_parse(self):
t_id_type = IdType.DoubanGame
t_id_value = '10734307'
t_url = 'https://www.douban.com/game/10734307/'
site = SiteList.get_site_by_id_type(t_id_type)
self.assertIsNotNone(site)
self.assertEqual(site.validate_url(t_url), True)
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.url, t_url)
self.assertEqual(site.id_value, t_id_value)
@use_local_response
def test_scrape(self):
t_url = 'https://www.douban.com/game/10734307/'
site = SiteList.get_site_by_url(t_url)
self.assertEqual(site.ready, False)
site.get_resource_ready()
self.assertEqual(site.ready, True)
self.assertEqual(site.resource.metadata['title'], '传送门2 Portal 2')
self.assertIsInstance(site.resource.item, Game)
self.assertEqual(site.resource.item.douban_game, '10734307')
class MultiGameSitesTestCase(TestCase):
@use_local_response
def test_games(self):
url1 = 'https://www.igdb.com/games/portal-2'
url2 = 'https://store.steampowered.com/app/620/Portal_2/'
p1 = SiteList.get_site_by_url(url1).get_resource_ready()
p2 = SiteList.get_site_by_url(url2).get_resource_ready()
self.assertEqual(p1.item.id, p2.item.id)

View file

@ -3,8 +3,11 @@ from .apple_podcast import ApplePodcast
from .douban_book import DoubanBook from .douban_book import DoubanBook
from .douban_movie import DoubanMovie from .douban_movie import DoubanMovie
from .douban_music import DoubanMusic from .douban_music import DoubanMusic
from .douban_game import DoubanGame
from .douban_drama import DoubanDrama from .douban_drama import DoubanDrama
from .goodreads import Goodreads from .goodreads import Goodreads
from .tmdb import TMDB_Movie from .tmdb import TMDB_Movie
from .imdb import IMDB from .imdb import IMDB
from .spotify import Spotify from .spotify import Spotify
from .igdb import IGDB
from .steam import Steam

View file

@ -0,0 +1,76 @@
from catalog.common import *
from catalog.models import *
from .douban import DoubanDownloader
import dateparser
import logging
_logger = logging.getLogger(__name__)
@SiteList.register
class DoubanGame(AbstractSite):
ID_TYPE = IdType.DoubanGame
URL_PATTERNS = [r"\w+://www\.douban\.com/game/(\d+)/{0,1}", r"\w+://m.douban.com/game/subject/(\d+)/{0,1}"]
WIKI_PROPERTY_ID = ''
DEFAULT_MODEL = Game
@classmethod
def id_to_url(self, id_value):
return "https://www.douban.com/game/" + id_value + "/"
def scrape(self):
content = DoubanDownloader(self.url).download().html()
elem = content.xpath("//div[@id='content']/h1/text()")
title = elem[0].strip() if len(elem) else None
if not title:
raise ParseError(self, "title")
other_title_elem = content.xpath(
"//dl[@class='game-attr']//dt[text()='别名:']/following-sibling::dd[1]/text()")
other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None
developer_elem = content.xpath(
"//dl[@class='game-attr']//dt[text()='开发商:']/following-sibling::dd[1]/text()")
developer = developer_elem[0].strip().split(' / ') if developer_elem else None
publisher_elem = content.xpath(
"//dl[@class='game-attr']//dt[text()='发行商:']/following-sibling::dd[1]/text()")
publisher = publisher_elem[0].strip().split(' / ') if publisher_elem else None
platform_elem = content.xpath(
"//dl[@class='game-attr']//dt[text()='平台:']/following-sibling::dd[1]/a/text()")
platform = platform_elem if platform_elem else None
genre_elem = content.xpath(
"//dl[@class='game-attr']//dt[text()='类型:']/following-sibling::dd[1]/a/text()")
genre = None
if genre_elem:
genre = [g for g in genre_elem if g != '游戏']
date_elem = content.xpath(
"//dl[@class='game-attr']//dt[text()='发行日期:']/following-sibling::dd[1]/text()")
release_date = dateparser.parse(date_elem[0].strip()).strftime('%Y-%m-%d') if date_elem else None
brief_elem = content.xpath("//div[@class='mod item-desc']/p/text()")
brief = '\n'.join(brief_elem) if brief_elem else None
img_url_elem = content.xpath(
"//div[@class='item-subject-info']/div[@class='pic']//img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
pd = ResourceContent(metadata={
'title': title,
'other_title': other_title,
'developer': developer,
'publisher': publisher,
'release_date': release_date,
'genre': genre,
'platform': platform,
'brief': brief,
'cover_image_url': img_url
})
if pd.metadata["cover_image_url"]:
pd.cover_image, pd.cover_image_extention = BasicImageDownloader.download_image(pd.metadata['cover_image_url'], self.url)
return pd

113
catalog/sites/igdb.py Normal file
View file

@ -0,0 +1,113 @@
"""
IGDB
use (e.g. "portal-2") as id, which is different from real id in IGDB API
"""
from catalog.common import *
from catalog.models import *
from django.conf import settings
from igdb.wrapper import IGDBWrapper
import requests
import datetime
import json
import logging
_logger = logging.getLogger(__name__)
def _igdb_access_token():
try:
token = requests.post(f'https://id.twitch.tv/oauth2/token?client_id={settings.IGDB_CLIENT_ID}&client_secret={settings.IGDB_CLIENT_SECRET}&grant_type=client_credentials').json()['access_token']
except Exception:
_logger.error('unable to obtain IGDB token')
token = '<invalid>'
return token
_wrapper = IGDBWrapper(settings.IGDB_CLIENT_ID, _igdb_access_token())
def search_igdb_by_3p_url(steam_url):
r = IGDB.api_query('websites', f'fields *, game.*; where url = "{steam_url}";')
if not r:
return None
r = sorted(r, key=lambda w: w['game']['id'])
return IGDB(url=r[0]['game']['url'])
@SiteList.register
class IGDB(AbstractSite):
ID_TYPE = IdType.IGDB
URL_PATTERNS = [r"\w+://www\.igdb\.com/games/([a-zA-Z0-9\-_]+)"]
WIKI_PROPERTY_ID = '?'
DEFAULT_MODEL = Game
@classmethod
def id_to_url(self, id_value):
return "https://www.igdb.com/games/" + id_value
@classmethod
def api_query(cls, p, q):
key = 'igdb:' + p + '/' + q
if get_mock_mode():
r = BasicDownloader(key).download().json()
else:
r = json.loads(_wrapper.api_request(p, q))
if settings.DOWNLOADER_SAVEDIR:
with open(settings.DOWNLOADER_SAVEDIR + '/' + get_mock_file(key), 'w', encoding='utf-8') as fp:
fp.write(json.dumps(r))
return r
def scrape(self):
fields = '*, cover.url, genres.name, platforms.name, involved_companies.*, involved_companies.company.name'
r = self.api_query('games', f'fields {fields}; where url = "{self.url}";')[0]
brief = r['summary'] if 'summary' in r else ''
brief += "\n\n" + r['storyline'] if 'storyline' in r else ''
developer = None
publisher = None
release_date = None
genre = None
platform = None
if 'involved_companies' in r:
developer = next(iter([c['company']['name'] for c in r['involved_companies'] if c['developer']]), None)
publisher = next(iter([c['company']['name'] for c in r['involved_companies'] if c['publisher']]), None)
if 'platforms' in r:
ps = sorted(r['platforms'], key=lambda p: p['id'])
platform = [(p['name'] if p['id'] != 6 else 'Windows') for p in ps]
if 'first_release_date' in r:
release_date = datetime.datetime.fromtimestamp(r['first_release_date'], datetime.timezone.utc).strftime('%Y-%m-%d')
if 'genres' in r:
genre = [g['name'] for g in r['genres']]
websites = self.api_query('websites', f'fields *; where game.url = "{self.url}";')
steam_url = None
official_site = None
for website in websites:
if website['category'] == 1:
official_site = website['url']
elif website['category'] == 13:
steam_url = website['url']
pd = ResourceContent(metadata={
'title': r['name'],
'other_title': None,
'developer': developer,
'publisher': publisher,
'release_date': release_date,
'genre': genre,
'platform': platform,
'brief': brief,
'official_site': official_site,
'igdb_id': r['id'],
'cover_image_url': 'https:' + r['cover']['url'].replace('t_thumb', 't_cover_big'),
})
if steam_url:
pd.lookup_ids[IdType.Steam] = SiteList.get_site_by_id_type(IdType.Steam).url_to_id(steam_url)
if pd.metadata["cover_image_url"]:
imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url)
try:
pd.cover_image = imgdl.download().content
pd.cover_image_extention = imgdl.extention
except Exception:
_logger.debug(f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}')
return pd

64
catalog/sites/steam.py Normal file
View file

@ -0,0 +1,64 @@
from catalog.common import *
from catalog.models import *
from .igdb import search_igdb_by_3p_url
import dateparser
import logging
_logger = logging.getLogger(__name__)
@SiteList.register
class Steam(AbstractSite):
ID_TYPE = IdType.Steam
URL_PATTERNS = [r"\w+://store\.steampowered\.com/app/(\d+)"]
WIKI_PROPERTY_ID = '?'
DEFAULT_MODEL = Game
@classmethod
def id_to_url(self, id_value):
return "https://store.steampowered.com/app/" + str(id_value)
def scrape(self):
i = search_igdb_by_3p_url(self.url)
pd = i.scrape() if i else ResourceContent()
headers = BasicDownloader.headers.copy()
headers['Host'] = 'store.steampowered.com'
headers['Cookie'] = "wants_mature_content=1; birthtime=754700401;"
content = BasicDownloader(self.url, headers=headers).download().html()
title = content.xpath("//div[@class='apphub_AppName']/text()")[0]
developer = content.xpath("//div[@id='developers_list']/a/text()")
publisher = content.xpath("//div[@class='glance_ctn']//div[@class='dev_row'][2]//a/text()")
release_date = dateparser.parse(
content.xpath(
"//div[@class='release_date']/div[@class='date']/text()")[0]
).strftime('%Y-%m-%d')
genre = content.xpath(
"//div[@class='details_block']/b[2]/following-sibling::a/text()")
platform = ['PC']
brief = content.xpath(
"//div[@class='game_description_snippet']/text()")[0].strip()
# try Steam images if no image from IGDB
if pd.cover_image is None:
pd.metadata['cover_image_url'] = content.xpath("//img[@class='game_header_image_full']/@src")[0].replace("header.jpg", "library_600x900.jpg")
pd.cover_image, pd.cover_image_extention = BasicImageDownloader.download_image(pd.metadata['cover_image_url'], self.url)
if pd.cover_image is None:
pd.metadata['cover_image_url'] = content.xpath("//img[@class='game_header_image_full']/@src")[0]
pd.cover_image, pd.cover_image_extention = BasicImageDownloader.download_image(pd.metadata['cover_image_url'], self.url)
# merge data from IGDB, use localized Steam data if available
d = {
'developer': developer,
'publisher': publisher,
'release_date': release_date,
'genre': genre,
'platform': platform,
}
d.update(pd.metadata)
pd.metadata = d
if title:
pd.metadata['title'] = title
if brief:
pd.metadata['brief'] = brief
return pd

View file

@ -3,6 +3,7 @@ from catalog.book.tests import *
from catalog.movie.tests import * from catalog.movie.tests import *
from catalog.tv.tests import * from catalog.tv.tests import *
from catalog.music.tests import * from catalog.music.tests import *
from catalog.game.tests import *
from catalog.podcast.tests import * from catalog.podcast.tests import *
from catalog.performance.tests import * from catalog.performance.tests import *

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
[{"id": 72, "age_ratings": [11721, 32022, 47683, 47684, 47685, 47686, 47687, 91785], "aggregated_rating": 92.4444444444444, "aggregated_rating_count": 13, "alternative_names": [50135], "artworks": [36972], "bundles": [55025, 191406], "category": 0, "collection": 87, "cover": {"id": 82660, "url": "//images.igdb.com/igdb/image/upload/t_thumb/co1rs4.jpg"}, "created_at": 1297956069, "dlcs": [99969, 114140], "external_games": [15150, 73156, 81867, 92870, 92979, 137388, 189642, 214010, 245334, 403070, 1303428, 1929756, 1931953, 2082680, 2161690, 2590310, 2600814], "first_release_date": 1303171200, "follows": 971, "franchises": [1724], "game_engines": [3], "game_modes": [1, 2, 3, 4], "genres": [{"id": 5, "name": "Shooter"}, {"id": 8, "name": "Platform"}, {"id": 9, "name": "Puzzle"}, {"id": 31, "name": "Adventure"}], "involved_companies": [{"id": 106733, "company": {"id": 56, "name": "Valve Corporation"}, "created_at": 1598486400, "developer": true, "game": 72, "porting": false, "publisher": true, "supporting": false, "updated_at": 1598486400, "checksum": "fa403088-a40a-1d83-16be-a68849472a6d"}, {"id": 106734, "company": {"id": 1, "name": "Electronic Arts"}, "created_at": 1598486400, "developer": false, "game": 72, "porting": false, "publisher": true, "supporting": false, "updated_at": 1598486400, "checksum": "53e59e19-f746-1195-c4e7-2b388e621317"}], "keywords": [350, 453, 575, 592, 1026, 1158, 1181, 1293, 1440, 1559, 1761, 2071, 2800, 3984, 4004, 4134, 4145, 4162, 4266, 4345, 4363, 4428, 4575, 4578, 4617, 4644, 4725, 4888, 4944, 4956, 4974, 5185, 5261, 5633, 5772, 5935, 5938, 5956, 6137, 6326, 6735, 6854, 7079, 7172, 7313, 7535, 7570, 7579, 8141, 8262, 8896, 9814, 10435, 11023, 11208, 12516, 14224, 18139, 18567, 27032], "multiplayer_modes": [11591, 11592, 11593, 11594, 11595], "name": "Portal 2", "platforms": [{"id": 3, "name": "Linux"}, {"id": 6, "name": "PC (Microsoft Windows)"}, {"id": 9, "name": "PlayStation 3"}, {"id": 12, "name": "Xbox 360"}, {"id": 14, "name": "Mac"}], "player_perspectives": [1], "rating": 91.6894220983232, "rating_count": 2765, "release_dates": [104964, 104965, 208203, 208204, 208205, 208206, 208207, 208208], "screenshots": [725, 726, 727, 728, 729], "similar_games": [71, 1877, 7350, 11646, 16992, 22387, 28070, 55038, 55190, 56033], "slug": "portal-2", "storyline": "You lost your memory, you are alone in a world full of danger, and your mission is survive using your mind. The only way to get out from this hell is.....Hi i'm GLAdOS, and welcome to the amazing world of portal 2, here i will expose you to a lot of tests, and try to k.. help Aperture Science envolve in a new era.\nYour job is advance in the levels i propose and get better and better, you will have an portal gun to help you, and remember nothing is impossible if you try, and try again and again and again....\nThe puzzles are waiting for you!", "summary": "Sequel to the acclaimed Portal (2007), Portal 2 pits the protagonist of the original game, Chell, and her new robot friend, Wheatley, against more puzzles conceived by GLaDOS, an A.I. with the sole purpose of testing the Portal Gun's mechanics and taking revenge on Chell for the events of Portal. As a result of several interactions and revelations, Chell once again pushes to escape Aperture Science Labs.", "tags": [1, 18, 27, 268435461, 268435464, 268435465, 268435487, 536871262, 536871365, 536871487, 536871504, 536871938, 536872070, 536872093, 536872205, 536872352, 536872471, 536872673, 536872983, 536873712, 536874896, 536874916, 536875046, 536875057, 536875074, 536875178, 536875257, 536875275, 536875340, 536875487, 536875490, 536875529, 536875556, 536875637, 536875800, 536875856, 536875868, 536875886, 536876097, 536876173, 536876545, 536876684, 536876847, 536876850, 536876868, 536877049, 536877238, 536877647, 536877766, 536877991, 536878084, 536878225, 536878447, 536878482, 536878491, 536879053, 536879174, 536879808, 536880726, 536881347, 536881935, 536882120, 536883428, 536885136, 536889051, 536889479, 536897944], "themes": [1, 18, 27], "total_rating": 92.0669332713838, "total_rating_count": 2778, "updated_at": 1670514780, "url": "https://www.igdb.com/games/portal-2", "videos": [432, 16451, 17844, 17845], "websites": [17869, 17870, 41194, 41195, 150881, 150882, 150883, 296808], "checksum": "bcca1b61-2b30-13b8-a0ec-faf45d2ffdad", "game_localizations": [726]}]

View file

@ -0,0 +1 @@
[{"id": 17870, "category": 13, "game": {"id": 72, "age_ratings": [11721, 32022, 47683, 47684, 47685, 47686, 47687, 91785], "aggregated_rating": 92.4444444444444, "aggregated_rating_count": 13, "alternative_names": [50135], "artworks": [36972], "bundles": [55025, 191406], "category": 0, "collection": 87, "cover": 82660, "created_at": 1297956069, "dlcs": [99969, 114140], "external_games": [15150, 73156, 81867, 92870, 92979, 137388, 189642, 214010, 245334, 403070, 1303428, 1929756, 1931953, 2082680, 2161690, 2590310, 2600814], "first_release_date": 1303171200, "follows": 971, "franchises": [1724], "game_engines": [3], "game_modes": [1, 2, 3, 4], "genres": [5, 8, 9, 31], "involved_companies": [106733, 106734], "keywords": [350, 453, 575, 592, 1026, 1158, 1181, 1293, 1440, 1559, 1761, 2071, 2800, 3984, 4004, 4134, 4145, 4162, 4266, 4345, 4363, 4428, 4575, 4578, 4617, 4644, 4725, 4888, 4944, 4956, 4974, 5185, 5261, 5633, 5772, 5935, 5938, 5956, 6137, 6326, 6735, 6854, 7079, 7172, 7313, 7535, 7570, 7579, 8141, 8262, 8896, 9814, 10435, 11023, 11208, 12516, 14224, 18139, 18567, 27032], "multiplayer_modes": [11591, 11592, 11593, 11594, 11595], "name": "Portal 2", "platforms": [3, 6, 9, 12, 14], "player_perspectives": [1], "rating": 91.6894220983232, "rating_count": 2765, "release_dates": [104964, 104965, 208203, 208204, 208205, 208206, 208207, 208208], "screenshots": [725, 726, 727, 728, 729], "similar_games": [71, 1877, 7350, 11646, 16992, 22387, 28070, 55038, 55190, 56033], "slug": "portal-2", "storyline": "You lost your memory, you are alone in a world full of danger, and your mission is survive using your mind. The only way to get out from this hell is.....Hi i'm GLAdOS, and welcome to the amazing world of portal 2, here i will expose you to a lot of tests, and try to k.. help Aperture Science envolve in a new era.\nYour job is advance in the levels i propose and get better and better, you will have an portal gun to help you, and remember nothing is impossible if you try, and try again and again and again....\nThe puzzles are waiting for you!", "summary": "Sequel to the acclaimed Portal (2007), Portal 2 pits the protagonist of the original game, Chell, and her new robot friend, Wheatley, against more puzzles conceived by GLaDOS, an A.I. with the sole purpose of testing the Portal Gun's mechanics and taking revenge on Chell for the events of Portal. As a result of several interactions and revelations, Chell once again pushes to escape Aperture Science Labs.", "tags": [1, 18, 27, 268435461, 268435464, 268435465, 268435487, 536871262, 536871365, 536871487, 536871504, 536871938, 536872070, 536872093, 536872205, 536872352, 536872471, 536872673, 536872983, 536873712, 536874896, 536874916, 536875046, 536875057, 536875074, 536875178, 536875257, 536875275, 536875340, 536875487, 536875490, 536875529, 536875556, 536875637, 536875800, 536875856, 536875868, 536875886, 536876097, 536876173, 536876545, 536876684, 536876847, 536876850, 536876868, 536877049, 536877238, 536877647, 536877766, 536877991, 536878084, 536878225, 536878447, 536878482, 536878491, 536879053, 536879174, 536879808, 536880726, 536881347, 536881935, 536882120, 536883428, 536885136, 536889051, 536889479, 536897944], "themes": [1, 18, 27], "total_rating": 92.0669332713838, "total_rating_count": 2778, "updated_at": 1670514780, "url": "https://www.igdb.com/games/portal-2", "videos": [432, 16451, 17844, 17845], "websites": [17869, 17870, 41194, 41195, 150881, 150882, 150883, 296808], "checksum": "bcca1b61-2b30-13b8-a0ec-faf45d2ffdad", "game_localizations": [726]}, "trusted": true, "url": "https://store.steampowered.com/app/620", "checksum": "5281f967-6dfe-7658-96c6-af00ce010bbc"}]

View file

@ -0,0 +1 @@
[{"id": 17869, "category": 1, "game": 72, "trusted": false, "url": "http://www.thinkwithportals.com/", "checksum": "c40d590f-93bd-b86e-243c-73746c08be3b"}, {"id": 17870, "category": 13, "game": 72, "trusted": true, "url": "https://store.steampowered.com/app/620", "checksum": "5281f967-6dfe-7658-96c6-af00ce010bbc"}, {"id": 41194, "category": 3, "game": 72, "trusted": true, "url": "https://en.wikipedia.org/wiki/Portal_2", "checksum": "7354f471-16d6-5ed9-b4e4-049cceaab562"}, {"id": 41195, "category": 4, "game": 72, "trusted": true, "url": "https://www.facebook.com/Portal", "checksum": "035f6b48-3be1-77d5-1567-cf6fd8116ee7"}, {"id": 150881, "category": 9, "game": 72, "trusted": true, "url": "https://www.youtube.com/user/Valve", "checksum": "c1d4afb9-e96d-02f1-73bd-3384622e6aee"}, {"id": 150882, "category": 5, "game": 72, "trusted": true, "url": "https://twitter.com/valvesoftware", "checksum": "62bb9586-3293-bb01-f675-d65323ae371c"}, {"id": 150883, "category": 2, "game": 72, "trusted": false, "url": "https://theportalwiki.com/wiki/Portal_2", "checksum": "af689276-28c8-b145-7b19-f1d7df878c2a"}, {"id": 296808, "category": 6, "game": 72, "trusted": true, "url": "https://www.twitch.tv/directory/game/Portal%202", "checksum": "65629340-6190-833d-41b1-8eaf31918df3"}]

View file

@ -0,0 +1 @@
[{"id": 12644, "category": 1, "game": 7346, "trusted": false, "url": "http://www.zelda.com/breath-of-the-wild/", "checksum": "3d2ca280-a2d0-5664-c8a5-69eeeaf13558"}, {"id": 12645, "category": 2, "game": 7346, "trusted": false, "url": "http://zelda.wikia.com/wiki/The_Legend_of_Zelda:_Breath_of_the_Wild", "checksum": "d5cb4657-dc8e-9de1-9643-b1ef64812d9f"}, {"id": 12646, "category": 3, "game": 7346, "trusted": true, "url": "https://en.wikipedia.org/wiki/The_Legend_of_Zelda:_Breath_of_the_Wild", "checksum": "c4570c3c-3a04-8d24-399a-0c04a17e7c56"}, {"id": 65034, "category": 14, "game": 7346, "trusted": true, "url": "https://www.reddit.com/r/Breath_of_the_Wild", "checksum": "f60505b3-18a4-3d60-9db2-febe4c6cb492"}, {"id": 169666, "category": 6, "game": 7346, "trusted": true, "url": "https://www.twitch.tv/nintendo", "checksum": "e2b20791-a9c4-76ad-4d76-3e7abc9148bb"}, {"id": 169667, "category": 9, "game": 7346, "trusted": true, "url": "https://www.youtube.com/nintendo", "checksum": "1e1c08ba-8f89-b567-0029-1d8aac22d147"}, {"id": 169668, "category": 4, "game": 7346, "trusted": true, "url": "https://www.facebook.com/Nintendo", "checksum": "046d8c8e-8f1d-8813-1266-c2911f490ba7"}, {"id": 169669, "category": 5, "game": 7346, "trusted": true, "url": "https://twitter.com/NintendoAmerica", "checksum": "e06dd12f-b6c5-ef72-f287-a9cebba12fa1"}, {"id": 169670, "category": 8, "game": 7346, "trusted": true, "url": "https://www.instagram.com/nintendo", "checksum": "dbff9e02-e9c2-f395-7e48-7e70cf58225c"}]