[pixiv] update (#1304)

- remove login with username & password
- require a refresh token
- add 'oauth:pixiv' functionality

See also:
- https://github.com/upbit/pixivpy/issues/158
- https://gist.github.com/ZipFile/c9ebedb224406f4f11845ab700124362
pull/1317/head
Mike Fährmann 4 years ago
parent 79c0fc249b
commit 8974f0361c
No known key found for this signature in database
GPG Key ID: 5680CA389D365A88

@ -214,7 +214,7 @@ Username & Password
Some extractors require you to provide valid login credentials in the form of Some extractors require you to provide valid login credentials in the form of
a username & password pair. This is necessary for a username & password pair. This is necessary for
``pixiv``, ``nijie``, and ``seiga`` ``nijie`` and ``seiga``
and optional for and optional for
``aryion``, ``aryion``,
``danbooru``, ``danbooru``,
@ -237,7 +237,7 @@ You can set the necessary information in your configuration file
{ {
"extractor": { "extractor": {
"pixiv": { "seiga": {
"username": "<username>", "username": "<username>",
"password": "<password>" "password": "<password>"
} }

@ -272,7 +272,6 @@ Description
Specifying a username and password is required for Specifying a username and password is required for
* ``pixiv``
* ``nijie`` * ``nijie``
* ``seiga`` * ``seiga``

@ -103,7 +103,7 @@ PhotoVogue https://www.vogue.it/en/photovogue/ User Profiles
Piczel https://piczel.tv/ Folders, individual Images, User Profiles Piczel https://piczel.tv/ Folders, individual Images, User Profiles
Pillowfort https://www.pillowfort.social/ Posts, User Profiles Pillowfort https://www.pillowfort.social/ Posts, User Profiles
Pinterest https://www.pinterest.com/ |pinterest-C| Supported Pinterest https://www.pinterest.com/ |pinterest-C| Supported
Pixiv https://www.pixiv.net/ |pixiv-C| Required Pixiv https://www.pixiv.net/ |pixiv-C| `OAuth <https://github.com/mikf/gallery-dl#oauth>`__
Pixnet https://www.pixnet.net/ Folders, individual Images, Sets, User Profiles Pixnet https://www.pixnet.net/ Folders, individual Images, Sets, User Profiles
Plurk https://www.plurk.com/ Posts, Timelines Plurk https://www.plurk.com/ Posts, Timelines
Pornhub https://www.pornhub.com/ Galleries, User Profiles Pornhub https://www.pornhub.com/ Galleries, User Profiles

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2017-2020 Mike Fährmann # Copyright 2017-2021 Mike Fährmann
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as # it under the terms of the GNU General Public License version 2 as
@ -9,10 +9,12 @@
"""Utility classes to setup OAuth and link accounts to gallery-dl""" """Utility classes to setup OAuth and link accounts to gallery-dl"""
from .common import Extractor, Message from .common import Extractor, Message
from . import deviantart, flickr, reddit, smugmug, tumblr from . import deviantart, flickr, pixiv, reddit, smugmug, tumblr
from .. import text, oauth, util, config, exception from .. import text, oauth, util, config, exception
from ..cache import cache from ..cache import cache
import urllib.parse import urllib.parse
import hashlib
import base64
REDIRECT_URI_LOCALHOST = "http://localhost:6414/" REDIRECT_URI_LOCALHOST = "http://localhost:6414/"
REDIRECT_URI_HTTPS = "https://mikf.github.io/gallery-dl/oauth-redirect.html" REDIRECT_URI_HTTPS = "https://mikf.github.io/gallery-dl/oauth-redirect.html"
@ -62,14 +64,14 @@ class OAuthBase(Extractor):
self.client.send(b"HTTP/1.1 200 OK\r\n\r\n" + msg.encode()) self.client.send(b"HTTP/1.1 200 OK\r\n\r\n" + msg.encode())
self.client.close() self.client.close()
def open(self, url, params): def open(self, url, params, recv=None):
"""Open 'url' in browser amd return response parameters""" """Open 'url' in browser amd return response parameters"""
import webbrowser import webbrowser
url += "?" + urllib.parse.urlencode(params) url += "?" + urllib.parse.urlencode(params)
if not self.config("browser", True) or not webbrowser.open(url): if not self.config("browser", True) or not webbrowser.open(url):
print("Please open this URL in your browser:") print("Please open this URL in your browser:")
print(url, end="\n\n", flush=True) print(url, end="\n\n", flush=True)
return self.recv() return (recv or self.recv)()
def _oauth1_authorization_flow( def _oauth1_authorization_flow(
self, request_token_url, authorize_url, access_token_url): self, request_token_url, authorize_url, access_token_url):
@ -362,6 +364,69 @@ class OAuthMastodon(OAuthBase):
return data return data
class OAuthPixiv(OAuthBase):
subcategory = "pixiv"
pattern = "oauth:pixiv$"
def items(self):
yield Message.Version, 1
code_verifier = util.generate_token(32)
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(
digest).rstrip(b"=").decode("ascii")
url = "https://app-api.pixiv.net/web/v1/login"
params = {
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"client": "pixiv-android",
}
code = self.open(url, params, self._input)
url = "https://oauth.secure.pixiv.net/auth/token"
headers = {
"User-Agent": "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)",
}
data = {
"client_id" : self.oauth_config(
"client-id" , pixiv.PixivAppAPI.CLIENT_ID),
"client_secret" : self.oauth_config(
"client-secret", pixiv.PixivAppAPI.CLIENT_SECRET),
"code" : code,
"code_verifier" : code_verifier,
"grant_type" : "authorization_code",
"include_policy": "true",
"redirect_uri" : "https://app-api.pixiv.net"
"/web/v1/users/auth/pixiv/callback",
}
data = self.session.post(url, headers=headers, data=data).json()
if "error" in data:
print(data)
if data["error"] == "invalid_request":
print("'code' expired, try again")
return
token = data["refresh_token"]
if self.cache:
username = self.oauth_config("username")
pixiv._refresh_token_cache.update(username, token)
self.log.info("Writing 'refresh-token' to cache")
print(self._generate_message(("refresh-token",), (token,)))
def _input(self):
print("""
1) Open your browser's Developer Tools (F12) and switch to the Network tab
2) Login
4) Select the last network monitor entry ('callback?state=...')
4) Copy its 'code' query parameter, paste it below, and press Enter
""")
code = input("code: ")
return code.rpartition("=")[2].strip()
MASTODON_MSG_TEMPLATE = """ MASTODON_MSG_TEMPLATE = """
Your 'access-token' is Your 'access-token' is

@ -510,49 +510,48 @@ class PixivAppAPI():
def __init__(self, extractor): def __init__(self, extractor):
self.extractor = extractor self.extractor = extractor
self.log = extractor.log self.log = extractor.log
self.username, self.password = extractor._get_auth_info() self.username = extractor._get_auth_info()[0]
self.user = None self.user = None
self.client_id = extractor.config(
"client-id", self.CLIENT_ID)
self.client_secret = extractor.config(
"client-secret", self.CLIENT_SECRET)
extractor.session.headers.update({ extractor.session.headers.update({
"App-OS" : "ios", "App-OS" : "ios",
"App-OS-Version": "10.3.1", "App-OS-Version": "13.1.2",
"App-Version": "6.7.1", "App-Version" : "7.7.6",
"User-Agent": "PixivIOSApp/6.7.1 (iOS 10.3.1; iPhone8,1)", "User-Agent" : "PixivIOSApp/7.7.6 (iOS 13.1.2; iPhone11,8)",
"Referer" : "https://app-api.pixiv.net/", "Referer" : "https://app-api.pixiv.net/",
}) })
self.client_id = extractor.config(
"client-id", self.CLIENT_ID)
self.client_secret = extractor.config(
"client-secret", self.CLIENT_SECRET)
token = extractor.config("refresh-token")
if token is None or token == "cache":
token = _refresh_token_cache(self.username)
self.refresh_token = token
def login(self): def login(self):
"""Login and gain an access token""" """Login and gain an access token"""
self.user, auth = self._login_impl(self.username, self.password) self.user, auth = self._login_impl(self.username)
self.extractor.session.headers["Authorization"] = auth self.extractor.session.headers["Authorization"] = auth
@cache(maxage=3600, keyarg=1) @cache(maxage=3600, keyarg=1)
def _login_impl(self, username, password): def _login_impl(self, username):
if not username or not password: if not self.refresh_token:
raise exception.AuthenticationError( raise exception.AuthenticationError(
"Username and password required") "'refresh-token' required.\n"
"Run `gallery-dl oauth:pixiv` to get one.")
self.log.info("Refreshing access token")
url = "https://oauth.secure.pixiv.net/auth/token" url = "https://oauth.secure.pixiv.net/auth/token"
data = { data = {
"client_id" : self.client_id, "client_id" : self.client_id,
"client_secret" : self.client_secret, "client_secret" : self.client_secret,
"get_secure_url": 1, "grant_type" : "refresh_token",
"refresh_token" : self.refresh_token,
"get_secure_url": "1",
} }
refresh_token = _refresh_token_cache(username)
if refresh_token:
self.log.info("Refreshing access token")
data["grant_type"] = "refresh_token"
data["refresh_token"] = refresh_token
else:
self.log.info("Logging in as %s", username)
data["grant_type"] = "password"
data["username"] = username
data["password"] = password
time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00") time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00")
headers = { headers = {
@ -565,11 +564,9 @@ class PixivAppAPI():
url, method="POST", headers=headers, data=data, fatal=False) url, method="POST", headers=headers, data=data, fatal=False)
if response.status_code >= 400: if response.status_code >= 400:
self.log.debug(response.text) self.log.debug(response.text)
raise exception.AuthenticationError() raise exception.AuthenticationError("Invalid refresh token")
data = response.json()["response"] data = response.json()["response"]
if not refresh_token:
_refresh_token_cache.update(username, data["refresh_token"])
return data["user"], "Bearer " + data["access_token"] return data["user"], "Bearer " + data["access_token"]
def illust_detail(self, illust_id): def illust_detail(self, illust_id):

@ -189,7 +189,7 @@ AUTH_MAP = {
"patreon" : _COOKIES, "patreon" : _COOKIES,
"pawoo" : _OAUTH, "pawoo" : _OAUTH,
"pinterest" : "Supported", "pinterest" : "Supported",
"pixiv" : "Required", "pixiv" : _OAUTH,
"reddit" : _OAUTH, "reddit" : _OAUTH,
"sankaku" : "Supported", "sankaku" : "Supported",
"seiga" : "Required", "seiga" : "Required",

Loading…
Cancel
Save