Spaces:
Running
Running
| 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"<y>{method}</y> <u>{url}</u>") | |
| 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"<m>Set-Cookie</m> <r>{domain}</r> <g>{name}</g> -> {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: | |
| def s256(data: bytes): | |
| return urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode() | |
| 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()) | |