众所周知在switch的账号信息里能看到账号的游戏游玩记录。而我这个博客的主题里有追番这个展示自己最近b站所看番剧的功能页(虽然已经不怎么在辣鸡b站上看番了)。所以我考虑加上一个展示自己的switch游戏记录的页面。
那要完成这个功能最重要的肯定还是数据了。首先查了一下发现web端还真没有显示switch游戏记录的地方。那咋办呢,给switch抓包肯定不是那么容易的(没研究过,但我觉得没破解机应该是不可行的)。不过好在发现一个20年4月的新闻提到任天堂新推出的My Nintendo App带有"遊んだ記録"功能。于是找个安卓模拟器抓一下包理论上就ok啦。
直接说结论:
获取游玩记录的接口
GET https://mypage-api.entry.nintendo.co.jp/api/v1/users/me/play_histories
需要两个头
Authorization: token.TokenType+" "+token.AccessToken
User-Agent: com.nintendo.znej/1.13.0 (Android/7.1.2)
这里的token需要通过session_token来获取
POST https://accounts.nintendo.com/connect/1.0.0/api/token
{"client_id":"***********","session_token":"**********","grant_type":"urn:ietf:params:oauth:grant-type:jwt-bearer-session-token"}
而client_id和session_token是app登陆时从web端获得的,就是上面的oauth,二进制选手不是很懂,反正知道咋获取就完事了,不选择抓包的话可以请求下面这个
GET https://accounts.nintendo.com/connect/1.0.0/authorize?state=&redirect_uri=npf{client_id}%3A%2F%2Fauth&client_id={client_id}&scope=openid%20user%20user.mii%20user.email%20user.links%5B%5D.id&response_type=session_token_code&session_token_code_challenge={challange}&session_token_code_challenge_method=S256&theme=login_form
因为session_token有两年的有效期,所以我在博客上的option里加入了client_id和session_token两个字段,然后请求上面两个url就完事了。前端也不会写,直接套了追番的那个。
顺带一提其实这个play_histories的数据是相当完整的,甚至可以进一步获取每个游戏分别在哪一天玩了多久。但公开展示就不显示那么清楚了hhh。
另外因为每次都请求任天堂的数据增加了响应时间,导致打开的速度很慢,怎么优化的问题就放在下次再说了。因为连续上班太累了orz。
2022.9.26更新示例代码(部分复制自splatnet2statink):
import requests
import json
import base64
import hashlib
import re
import sys
import os
from bs4 import BeautifulSoup
class nsession:
def __init__(self, client_id='5c38e31cd085304b') -> None:
self.session = requests.Session()
self.client_id = client_id
self.ua = 'com.nintendo.znej/1.13.0 (Android/7.1.2)'
def log_in(self):
'''Logs in to a Nintendo Account and returns a session_token.'''
auth_code_verifier = base64.urlsafe_b64encode(os.urandom(32))
auth_cv_hash = hashlib.sha256()
auth_cv_hash.update(auth_code_verifier.replace(b"=", b""))
auth_code_challenge = base64.urlsafe_b64encode(auth_cv_hash.digest())
app_head = {
'Host': 'accounts.nintendo.com',
'Connection': 'keep-alive',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8n',
'DNT': '1',
'Accept-Encoding': 'gzip,deflate,br',
}
body = {
'state': '',
'redirect_uri': 'npf{}://auth'.format(self.client_id),
'client_id': self.client_id,
'scope': 'openid user user.mii user.email user.links[].id',
'response_type': 'session_token_code',
'session_token_code_challenge': auth_code_challenge.replace(b"=", b""),
'session_token_code_challenge_method': 'S256',
'theme': 'login_form'
}
url = 'https://accounts.nintendo.com/connect/1.0.0/authorize'
r = self.session.get(url, headers=app_head, params=body)
post_login = r.history[0].url
print("\nMake sure you have fully read the \"Cookie generation\" section of the readme before proceeding. To manually input a cookie instead, enter \"skip\" at the prompt below.")
print("\nNavigate to this URL in your browser:")
print(post_login)
print("Log in, right click the \"Select this account\" button, copy the link address, and paste it below:")
while True:
try:
use_account_url = input("")
if use_account_url == "skip":
return "skip"
session_token_code = re.search('de=(.*)&', use_account_url)
return self.get_session_token(session_token_code.group(1), auth_code_verifier)
except KeyboardInterrupt:
print("\nBye!")
sys.exit(1)
except AttributeError:
print("Malformed URL. Please try again, or press Ctrl+C to exit.")
print("URL:", end=' ')
except KeyError: # session_token not found
print(
"\nThe URL has expired. Please log out and back into your Nintendo Account and try again.")
sys.exit(1)
def get_session_token(self, session_token_code, auth_code_verifier):
'''Helper function for log_in().'''
app_head = {
'User-Agent': self.ua,
'Accept-Language': 'en-US',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'accounts.nintendo.com',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip'
}
body = {
'client_id': self.client_id,
'session_token_code': session_token_code,
'session_token_code_verifier': auth_code_verifier.replace(b"=", b"")
}
url = 'https://accounts.nintendo.com/connect/1.0.0/api/session_token'
r = self.session.post(url, headers=app_head, data=body)
self.session_token = json.loads(r.text)["session_token"]
def get_access_token(self):
body = '{"client_id":"' + self.client_id + '","session_token":"' + self.session_token + \
'","grant_type":"urn:ietf:params:oauth:grant-type:jwt-bearer-session-token"}'
url = 'https://accounts.nintendo.com/connect/1.0.0/api/token'
r = self.session.post(
url, headers={'Content-Type': 'application/json'}, data=body)
self.access_token = json.loads(r.text)
def get_history(self):
url = 'https://mypage-api.entry.nintendo.co.jp/api/v1/users/me/play_histories'
header = {
'Authorization': self.access_token['token_type'] + ' ' + self.access_token['access_token'],
'User-Agent': self.ua,
}
r = self.session.get(url, headers=header)
return r
ns = nsession()
session_token = ns.log_in()
print('your session_token:')
print(ns.session_token)
ns.get_access_token()
print(ns.access_token)
r = ns.get_history()
print(r.text)
2022.10.10 更新,感谢@CafeAuLait提醒,通过titleId爬取eshop港服商店的信息补充游戏中文标题,详见评论区,暂未解决港服商店未上架游戏的中文标题解析方法,这部分建议手动或者不管了。。。
接口:
$url = 'https://ec.nintendo.com/apps/' . $item["titleId"] . '/HK';
2023.12.11 更新,mypage-api变更了,dns直接都没了,重新抓包了下新api,只需要替换url即可,其他不变,新url:https://news-api.entry.nintendo.co.jp/api/v1.1/users/me/play_histories
Comments | 6 条评论
博主 Asteroid
大佬你好,请问这个api是只能获取到日服的账号嘛?
博主 Aris
@Asteroid 我测试应该是可以用其他服账号的,详细获取session_token的方法可以参考https://github.com/frozenpandaman/splatnet2statink/blob/master/iksm.py,我转区到港服之后session_token会失效,重新获取之后可以正常工作,之后抽空我写详细点吧
博主 Asteroid
@Aris 感谢大佬
博主 CafeAuLait
感谢分享!
请问有些游戏是港服独占中文的情况(游戏中只有简繁中文和韩文,比如港版《牧场物语:重返矿石镇》)。这时候,获取到的 playHistories 中游戏名和图标都是对应韩文的版本。
是否有已知的方法获取到中文版游戏名称和封面?
港版《牧场物语:重返矿石镇》信息如下:
{
"titleId": "0100C2D00EBD4000",
"titleName": "목장이야기 다시 만난 미네랄 타운의 친구들",
"deviceType": "HAC",
"imageUrl": "https://atum-img-lp1.cdn.nintendo.net/i/c/76a3f1b667574819a60ecddaefce48ff_256",
"lastUpdatedAt": "2022-10-06T09:51:08+09:00",
"firstPlayedAt": "2019-12-30T11:37:52+09:00",
"lastPlayedAt": "2020-01-07T05:47:55+09:00",
"totalPlayedDays": 9,
"totalPlayedMinutes": 1353
},
博主 Aris
@CafeAuLait 我的十三机兵防卫圈也是显示韩文,而且在官方app上也是同样的,所以应该没法直接从该接口获得游戏的中文名称,但如果把游戏的titleId拿去搜索,可以在第三方的站点上获取到游戏的中文名(https://tinfoil.io/Title/0100C2D00EBD4000),但这个方法缺乏可靠性,不过仔细看的话它还有跳转到eshop商店的链接(https://ec.nintendo.com/apps/0100C2D00EBD4000/HK)所以可以直接通过titleID跳转到香港eshop,直接从商店页面爬取游戏名称,这样就靠谱了,但就是不清楚如果港服商店没有会怎样,可能需要做好异常处理
博主 CafeAuLait
@Aris 感谢!