import hashlib
import sys
from base64 import urlsafe_b64encode
from secrets import token_urlsafe
from typing import Any, Callable, Optional, TypeVar
from urllib.parse import parse_qs, urlencode
import requests
from loguru import logger as _logger
from PyQt6.QtCore import QUrl
from PyQt6.QtNetwork import QNetworkCookie
from PyQt6.QtWebEngineCore import (
QWebEngineUrlRequestInfo,
QWebEngineUrlRequestInterceptor,
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QMainWindow,
QPlainTextEdit,
QPushButton,
QVBoxLayout,
QWidget,
)
USER_AGENT = "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)"
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"
app = QApplication(sys.argv)
logger = _logger.opt(colors=True)
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
code_listener: Optional[Callable[[str], None]] = None
def __init__(self):
super().__init__()
def interceptRequest(self, info: QWebEngineUrlRequestInfo) -> None:
method = info.requestMethod().data().decode()
url = info.requestUrl().url()
if (
self.code_listener
and "app-api.pixiv.net" in info.requestUrl().host()
and info.requestUrl().path().endswith("callback")
):
query = parse_qs(info.requestUrl().query())
code, *_ = query["code"]
self.code_listener(code)
logger.debug(f"{method} {url}")
class WebView(QWebEngineView):
def __init__(self):
super().__init__()
self.cookies: dict[str, str] = {}
page = self.page()
assert page is not None
profile = page.profile()
assert profile is not None
profile.setHttpUserAgent(USER_AGENT)
page.contentsSize().setHeight(768)
page.contentsSize().setWidth(432)
self.interceptor = RequestInterceptor()
profile.setUrlRequestInterceptor(self.interceptor)
cookie_store = profile.cookieStore()
assert cookie_store is not None
cookie_store.cookieAdded.connect(self._on_cookie_added)
self.setFixedHeight(896)
self.setFixedWidth(414)
self.start("about:blank")
def start(self, goto: str):
self.page().profile().cookieStore().deleteAllCookies() # type: ignore
self.cookies.clear()
self.load(QUrl(goto))
def _on_cookie_added(self, cookie: QNetworkCookie):
domain = cookie.domain()
name = cookie.name().data().decode()
value = cookie.value().data().decode()
self.cookies[name] = value
logger.debug(f"Set-Cookie {domain} {name} -> {value!r}")
class ResponseDataWidget(QWidget):
def __init__(self, webview: WebView):
super().__init__()
self.webview = webview
layout = QVBoxLayout()
self.cookie_paste = QPlainTextEdit()
self.cookie_paste.setDisabled(True)
self.cookie_paste.setPlaceholderText("得到的登录数据将会展示在这里")
layout.addWidget(self.cookie_paste)
copy_button = QPushButton()
copy_button.clicked.connect(self._on_clipboard_copy)
copy_button.setText("复制上述登录数据到剪贴板")
layout.addWidget(copy_button)
self.setLayout(layout)
def _on_clipboard_copy(self, checked: bool):
if paste_string := self.cookie_paste.toPlainText().strip():
app.clipboard().setText(paste_string) # type: ignore
_T = TypeVar("_T", bound="LoginPhrase")
class LoginPhrase:
@staticmethod
def s256(data: bytes):
return urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()
@classmethod
def oauth_pkce(cls) -> tuple[str, str]:
code_verifier = token_urlsafe(32)
code_challenge = cls.s256(code_verifier.encode())
return code_verifier, code_challenge
def __init__(self: _T, url_open_callback: Callable[[str, _T], None]):
self.code_verifier, self.code_challenge = self.oauth_pkce()
login_params = {
"code_challenge": self.code_challenge,
"code_challenge_method": "S256",
"client": "pixiv-android",
}
login_url = f"{LOGIN_URL}?{urlencode(login_params)}"
url_open_callback(login_url, self)
def code_received(self, code: str):
response = requests.post(
AUTH_TOKEN_URL,
data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"code_verifier": self.code_verifier,
"grant_type": "authorization_code",
"include_policy": "true",
"redirect_uri": REDIRECT_URI,
},
headers={"User-Agent": USER_AGENT},
)
response.raise_for_status()
data: dict[str, Any] = response.json()
access_token = data["access_token"]
refresh_token = data["refresh_token"]
expires_in = data.get("expires_in", 0)
return_text = ""
return_text += f"access_token: {access_token}\n"
return_text += f"refresh_token: {refresh_token}\n"
return_text += f"expires_in: {expires_in}\n"
return return_text
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Pixiv login helper")
layout = QHBoxLayout()
self.webview = WebView()
layout.addWidget(self.webview)
self.form = ResponseDataWidget(self.webview)
layout.addWidget(self.form)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
if __name__ == "__main__":
window = MainWindow()
window.show()
def url_open_callback(url: str, login_phrase: LoginPhrase):
def code_listener(code: str):
response = login_phrase.code_received(code)
window.form.cookie_paste.setPlainText(response)
window.webview.interceptor.code_listener = code_listener
window.webview.start(url)
LoginPhrase(url_open_callback)
exit(app.exec())