new data model: igdb steam douban_game
This commit is contained in:
parent
838d392056
commit
d9639c6024
16 changed files with 5142 additions and 14 deletions
|
@ -5,4 +5,4 @@ from .scrapers import *
|
|||
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')
|
||||
|
|
|
@ -42,6 +42,11 @@ def get_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):
|
||||
def __init__(self, downloader, msg=None):
|
||||
self.url = downloader.url
|
||||
|
@ -95,7 +100,7 @@ class BasicDownloader:
|
|||
# TODO cache = get/set from redis
|
||||
resp = requests.get(url, headers=self.headers, timeout=self.get_timeout())
|
||||
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)
|
||||
else:
|
||||
resp = MockResponse(self.url)
|
||||
|
@ -191,7 +196,15 @@ class ImageDownloaderMixin:
|
|||
|
||||
|
||||
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):
|
||||
|
@ -202,13 +215,9 @@ _local_response_path = str(Path(__file__).parent.parent.parent.absolute()) + '/t
|
|||
|
||||
|
||||
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):
|
||||
self.url = url
|
||||
fn = self.get_mock_file(url)
|
||||
fn = _local_response_path + get_mock_file(url)
|
||||
try:
|
||||
self.content = Path(fn).read_bytes()
|
||||
self.status_code = 200
|
||||
|
|
|
@ -2,10 +2,7 @@ from catalog.common import *
|
|||
|
||||
|
||||
class Game(Item):
|
||||
igdb = LookupIdDescriptor(IdType.IGDB)
|
||||
steam = LookupIdDescriptor(IdType.Steam)
|
||||
douban_game = LookupIdDescriptor(IdType.DoubanGame)
|
||||
igdb = PrimaryLookupIdDescriptor(IdType.IGDB)
|
||||
steam = PrimaryLookupIdDescriptor(IdType.Steam)
|
||||
douban_game = PrimaryLookupIdDescriptor(IdType.DoubanGame)
|
||||
platforms = jsondata.ArrayField(default=list)
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
|
99
catalog/game/tests.py
Normal file
99
catalog/game/tests.py
Normal 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)
|
|
@ -3,8 +3,11 @@ from .apple_podcast import ApplePodcast
|
|||
from .douban_book import DoubanBook
|
||||
from .douban_movie import DoubanMovie
|
||||
from .douban_music import DoubanMusic
|
||||
from .douban_game import DoubanGame
|
||||
from .douban_drama import DoubanDrama
|
||||
from .goodreads import Goodreads
|
||||
from .tmdb import TMDB_Movie
|
||||
from .imdb import IMDB
|
||||
from .spotify import Spotify
|
||||
from .igdb import IGDB
|
||||
from .steam import Steam
|
||||
|
|
76
catalog/sites/douban_game.py
Normal file
76
catalog/sites/douban_game.py
Normal 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
113
catalog/sites/igdb.py
Normal 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
64
catalog/sites/steam.py
Normal 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
|
|
@ -3,6 +3,7 @@ from catalog.book.tests import *
|
|||
from catalog.movie.tests import *
|
||||
from catalog.tv.tests import *
|
||||
from catalog.music.tests import *
|
||||
from catalog.game.tests import *
|
||||
from catalog.podcast.tests import *
|
||||
from catalog.performance.tests import *
|
||||
|
||||
|
|
2717
test_data/https___store_steampowered_com_app_620
Normal file
2717
test_data/https___store_steampowered_com_app_620
Normal file
File diff suppressed because one or more lines are too long
2044
test_data/https___www_douban_com_game_10734307_
Normal file
2044
test_data/https___www_douban_com_game_10734307_
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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]}]
|
File diff suppressed because one or more lines are too long
|
@ -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"}]
|
|
@ -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"}]
|
|
@ -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"}]
|
Loading…
Add table
Reference in a new issue