|
#!/usr/bin/env python |
|
|
|
import time |
|
import json |
|
import re |
|
import requests |
|
|
|
from argparse import ArgumentParser |
|
from base64 import urlsafe_b64encode |
|
from hashlib import sha256 |
|
from pprint import pprint |
|
from secrets import token_urlsafe |
|
from sys import exit |
|
from urllib.parse import urlencode |
|
from selenium import webdriver |
|
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities |
|
|
|
|
|
# Latest app version can be found using GET /v1/application-info/android |
|
USER_AGENT = "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)" |
|
REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback" |
|
LOGIN_URL = "https://app-api.pixiv.net/web/v1/login" |
|
AUTH_TOKEN_URL = "https://oauth.secure.pixiv.net/auth/token" |
|
CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT" |
|
CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj" |
|
REQUESTS_KWARGS = { |
|
# 'proxies': { |
|
# 'https': 'http://127.0.0.1:1087', |
|
# }, |
|
# 'verify': False |
|
} |
|
|
|
def s256(data): |
|
"""S256 transformation method.""" |
|
|
|
return urlsafe_b64encode(sha256(data).digest()).rstrip(b"=").decode("ascii") |
|
|
|
|
|
def oauth_pkce(transform): |
|
"""Proof Key for Code Exchange by OAuth Public Clients (RFC7636).""" |
|
|
|
code_verifier = token_urlsafe(32) |
|
code_challenge = transform(code_verifier.encode("ascii")) |
|
|
|
return code_verifier, code_challenge |
|
|
|
|
|
def print_auth_token_response(response): |
|
data = response.json() |
|
|
|
try: |
|
access_token = data["access_token"] |
|
refresh_token = data["refresh_token"] |
|
except KeyError: |
|
print("error:") |
|
pprint(data) |
|
exit(1) |
|
|
|
print("access_token:", access_token) |
|
print("refresh_token:", refresh_token) |
|
print("expires_in:", data.get("expires_in", 0)) |
|
|
|
|
|
def login(): |
|
caps = DesiredCapabilities.CHROME.copy() |
|
caps["goog:loggingPrefs"] = {"performance": "ALL"} # enable performance logs |
|
|
|
driver = webdriver.Chrome("./chromedriver", desired_capabilities=caps) |
|
|
|
code_verifier, code_challenge = oauth_pkce(s256) |
|
login_params = { |
|
"code_challenge": code_challenge, |
|
"code_challenge_method": "S256", |
|
"client": "pixiv-android", |
|
} |
|
print("[INFO] Gen code_verifier:", code_verifier) |
|
|
|
driver.get(f"{LOGIN_URL}?{urlencode(login_params)}") |
|
|
|
while True: |
|
# wait for login |
|
if driver.current_url[:40] == "https://accounts.pixiv.net/post-redirect": |
|
break |
|
time.sleep(1) |
|
|
|
# filter code url from performance logs |
|
code = None |
|
for row in driver.get_log('performance'): |
|
data = json.loads(row.get("message", {})) |
|
message = data.get("message", {}) |
|
if message.get("method") == "Network.requestWillBeSent": |
|
url = message.get("params", {}).get("documentURL") |
|
if url[:8] == "pixiv://": |
|
code = re.search(r'code=([^&]*)', url).groups()[0] |
|
break |
|
|
|
driver.close() |
|
|
|
print("[INFO] Get code:", code) |
|
|
|
response = requests.post( |
|
AUTH_TOKEN_URL, |
|
data={ |
|
"client_id": CLIENT_ID, |
|
"client_secret": CLIENT_SECRET, |
|
"code": code, |
|
"code_verifier": code_verifier, |
|
"grant_type": "authorization_code", |
|
"include_policy": "true", |
|
"redirect_uri": REDIRECT_URI, |
|
}, |
|
headers={ |
|
"user-agent": USER_AGENT, |
|
"app-os-version": "14.6", |
|
"app-os": "ios", |
|
}, |
|
**REQUESTS_KWARGS |
|
) |
|
|
|
print_auth_token_response(response) |
|
|
|
|
|
def refresh(refresh_token): |
|
response = requests.post( |
|
AUTH_TOKEN_URL, |
|
data={ |
|
"client_id": CLIENT_ID, |
|
"client_secret": CLIENT_SECRET, |
|
"grant_type": "refresh_token", |
|
"include_policy": "true", |
|
"refresh_token": refresh_token, |
|
}, |
|
headers={ |
|
"user-agent": USER_AGENT, |
|
"app-os-version": "14.6", |
|
"app-os": "ios", |
|
}, |
|
**REQUESTS_KWARGS |
|
) |
|
print_auth_token_response(response) |
|
|
|
|
|
def main(): |
|
parser = ArgumentParser() |
|
subparsers = parser.add_subparsers() |
|
parser.set_defaults(func=lambda _: parser.print_usage()) |
|
login_parser = subparsers.add_parser("login") |
|
login_parser.set_defaults(func=lambda _: login()) |
|
refresh_parser = subparsers.add_parser("refresh") |
|
refresh_parser.add_argument("refresh_token") |
|
refresh_parser.set_defaults(func=lambda ns: refresh(ns.refresh_token)) |
|
args = parser.parse_args() |
|
args.func(args) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
There seems to be no problem here.
api.set_auth(access_token)will provide theAuthorization: Bearer token, which can be used continuously until an authentication failure is returned.I am worried that Pixiv has added IP or other statistical dimensions to trigger frequency limits. You can try using a 'clean' account, and manually verify it after triggering the
Rate Limit.