Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
/run

*.html
*.cookies
*.cookies

local.properties
99 changes: 55 additions & 44 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,65 +1,76 @@
@file:Suppress("OPT_IN_USAGE")

import java.util.*

plugins {
kotlin("jvm") version "1.8.22"
kotlin("plugin.serialization") version "1.8.22"
id("me.him188.maven-central-publish") version "1.0.0-dev-3"
kotlin("multiplatform") version "1.9.10"
kotlin("plugin.serialization") version "1.9.10"
id("com.github.gmazzo.buildconfig") version "3.1.0"
}

group = "xyz.cssxsh.pixiv"
version = "1.3.1"

mavenCentralPublish {
useCentralS01()
singleDevGithubProject("cssxsh", "pixiv-client")
licenseFromGitHubProject("AGPL-3.0")
workingDir = System.getenv("PUBLICATION_TEMP")?.let { file(it).resolve(projectName) }
?: buildDir.resolve("publishing-tmp")
}

repositories {
mavenLocal()
mavenCentral()
}

dependencies {
api("com.squareup:gifencoder:0.10.1")
compileOnly("org.openpnp:opencv:4.6.0-0")
compileOnly("org.seleniumhq.selenium:selenium-java:4.10.0")
testImplementation(kotlin("test"))
testImplementation("org.openpnp:opencv:4.6.0-0")
testImplementation("org.seleniumhq.selenium:selenium-java:4.10.0")
//
api(platform("io.ktor:ktor-bom:2.3.3"))
api("io.ktor:ktor-client-auth")
api("io.ktor:ktor-client-encoding")
api("io.ktor:ktor-client-okhttp")
api("io.ktor:ktor-client-content-negotiation")
api("io.ktor:ktor-serialization-kotlinx-json")
//
api(platform("com.squareup.okhttp3:okhttp-bom:4.11.0"))
api("com.squareup.okhttp3:okhttp-dnsoverhttps")
//
implementation(platform("org.slf4j:slf4j-parent:2.0.7"))
testImplementation("org.slf4j:slf4j-simple")
}

kotlin {
explicitApi()
target.compilations {
all {
kotlinOptions {
jvmTarget = "11"
jvmToolchain(17)
targetHierarchy.default()
jvm()
macosX64()
linuxX64()

sourceSets {
val commonMain by getting {
dependencies {
api(platform("io.ktor:ktor-bom:2.3.3"))
api("io.ktor:ktor-client-auth")
api("io.ktor:ktor-client-encoding")
api("io.ktor:ktor-client-cio")
api("io.ktor:ktor-client-content-negotiation")
api("io.ktor:ktor-serialization-kotlinx-json")
// api(platform("com.squareup.okhttp3:okhttp-bom:4.11.0"))
implementation("com.squareup.okio:okio:3.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")

implementation(platform("org.kotlincrypto.hash:bom:0.3.0"))
implementation("org.kotlincrypto.hash:sha2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
commonTest.configure {
dependencies {
implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}
}

val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp")
}
}

val nativeMain by getting {
dependencies {
implementation("io.ktor:ktor-client-curl")
}
}
}
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
val properties = Properties().apply {
try {
load(rootProject.file("local.properties").reader())
} catch (e: Exception) {
println("local.properties file not found, you won't be able to run tests")
}
}

tasks {
test {
useJUnitPlatform()
}
buildConfig {
buildConfigField("String", "REFRESH_TOKEN", provider { "\"${properties["refresh_token"]}\"" })
}
62 changes: 61 additions & 1 deletion example/99565513.json
Original file line number Diff line number Diff line change
@@ -1 +1,61 @@
{"caption":"now loadingシリーズのケーちゃんです(*‘ω‘ *)","create_date":"2022-07-07T20:21:24+09:00","width":560,"height":315,"id":99565513,"image_urls":{"square_medium":"https://i.pximg.net/c/540x540_10_webp/img-master/img/2022/07/07/20/21/24/99565513_square1200.jpg","medium":"https://i.pximg.net/c/540x540_70/img-master/img/2022/07/07/20/21/24/99565513_master1200.jpg","large":"https://i.pximg.net/c/600x1200_90_webp/img-master/img/2022/07/07/20/21/24/99565513_master1200.jpg"},"is_bookmarked":false,"is_muted":false,"meta_pages":[],"meta_single_page":{"original_image_url":"https://i.pximg.net/img-original/img/2022/07/07/20/21/24/99565513_ugoira0.jpg"},"page_count":1,"restrict":0,"sanity_level":2,"series":null,"tags":[{"name":"うごイラ","translated_name":"动图"},{"name":"アークナイツ","translated_name":"明日方舟"},{"name":"明日方舟","translated_name":"Arknights"},{"name":"Arknights","translated_name":"明日方舟"},{"name":"ケオベ(アークナイツ)","translated_name":"刻俄柏(明日方舟)"}],"title":"ふりふりケーちゃん","tools":[],"total_bookmarks":347,"total_comments":12,"total_view":2626,"type":"ugoira","user":{"account":"sukoru_pisu","id":72778,"is_followed":false,"name":"Ca","profile_image_urls":{"medium":"https://i.pximg.net/user-profile/img/2020/02/29/06/21/21/18012369_ac14844d6d07b1df5f6a12d415d0acfb_170.jpg"}},"visible":true,"x_restrict":0}
{
"caption": "now loadingシリーズのケーちゃんです(*‘ω‘ *)",
"create_date": "2022-07-07T20:21:24+09:00",
"width": 560,
"height": 315,
"id": 99565513,
"image_urls": {
"square_medium": "https://i.pximg.net/c/540x540_10_webp/img-master/img/2022/07/07/20/21/24/99565513_square1200.jpg",
"medium": "https://i.pximg.net/c/540x540_70/img-master/img/2022/07/07/20/21/24/99565513_master1200.jpg",
"large": "https://i.pximg.net/c/600x1200_90_webp/img-master/img/2022/07/07/20/21/24/99565513_master1200.jpg"
},
"is_bookmarked": false,
"is_muted": false,
"meta_pages": [],
"meta_single_page": {
"original_image_url": "https://i.pximg.net/img-original/img/2022/07/07/20/21/24/99565513_ugoira0.jpg"
},
"page_count": 1,
"restrict": 0,
"sanity_level": 2,
"series": null,
"tags": [
{
"name": "うごイラ",
"translated_name": "动图"
},
{
"name": "アークナイツ",
"translated_name": "明日方舟"
},
{
"name": "明日方舟",
"translated_name": "Arknights"
},
{
"name": "Arknights",
"translated_name": "明日方舟"
},
{
"name": "ケオベ(アークナイツ)",
"translated_name": "刻俄柏(明日方舟)"
}
],
"title": "ふりふりケーちゃん",
"tools": [],
"total_bookmarks": 347,
"total_comments": 12,
"total_view": 2626,
"type": "ugoira",
"user": {
"account": "sukoru_pisu",
"id": 72778,
"is_followed": false,
"name": "Ca",
"profile_image_urls": {
"medium": "https://i.pximg.net/user-profile/img/2020/02/29/06/21/21/18012369_ac14844d6d07b1df5f6a12d415d0acfb_170.jpg"
}
},
"visible": true,
"x_restrict": 0
}
72 changes: 71 additions & 1 deletion example/99565513.ugoira.json
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
{"frames":[{"delay":30,"file":"000000.jpg"},{"delay":30,"file":"000001.jpg"},{"delay":40,"file":"000002.jpg"},{"delay":30,"file":"000003.jpg"},{"delay":30,"file":"000004.jpg"},{"delay":40,"file":"000005.jpg"},{"delay":30,"file":"000006.jpg"},{"delay":30,"file":"000007.jpg"},{"delay":40,"file":"000008.jpg"},{"delay":30,"file":"000009.jpg"},{"delay":30,"file":"000010.jpg"},{"delay":40,"file":"000011.jpg"},{"delay":30,"file":"000012.jpg"},{"delay":30,"file":"000013.jpg"},{"delay":40,"file":"000014.jpg"},{"delay":30,"file":"000015.jpg"}],"zip_urls":{"medium":"https://i.pximg.net/img-zip-ugoira/img/2022/07/07/20/21/24/99565513_ugoira600x600.zip"}}
{
"frames": [
{
"delay": 30,
"file": "000000.jpg"
},
{
"delay": 30,
"file": "000001.jpg"
},
{
"delay": 40,
"file": "000002.jpg"
},
{
"delay": 30,
"file": "000003.jpg"
},
{
"delay": 30,
"file": "000004.jpg"
},
{
"delay": 40,
"file": "000005.jpg"
},
{
"delay": 30,
"file": "000006.jpg"
},
{
"delay": 30,
"file": "000007.jpg"
},
{
"delay": 40,
"file": "000008.jpg"
},
{
"delay": 30,
"file": "000009.jpg"
},
{
"delay": 30,
"file": "000010.jpg"
},
{
"delay": 40,
"file": "000011.jpg"
},
{
"delay": 30,
"file": "000012.jpg"
},
{
"delay": 30,
"file": "000013.jpg"
},
{
"delay": 40,
"file": "000014.jpg"
},
{
"delay": 30,
"file": "000015.jpg"
}
],
"zip_urls": {
"medium": "https://i.pximg.net/img-zip-ugoira/img/2022/07/07/20/21/24/99565513_ugoira600x600.zip"
}
}
Empty file removed local.properties
Empty file.
114 changes: 114 additions & 0 deletions pixiv_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python3

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 webbrowser import open as open_url

# Latest app version can be found using GET /v1/application-info/android
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"


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():
code_verifier, code_challenge = oauth_pkce(s256)
login_params = {
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"client": "pixiv-android",
}

open_url(f"{LOGIN_URL}?{urlencode(login_params)}")

try:
code = input("code: ").strip()
except (EOFError, KeyboardInterrupt):
return

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},
)

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},
)
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()
Loading