获取switch游戏历史的一种方法

发布于 2022-08-01  7480 次阅读


众所周知在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


人生二十年,与天地长久相较,如梦又似幻;一度得生者,岂有不灭者乎?