correcting pixiv connection functionality

This commit is contained in:
2026-02-04 12:45:31 -05:00
parent cc19bbd372
commit 2190a9c16b
8 changed files with 315 additions and 13 deletions
+2 -1
View File
@@ -89,7 +89,7 @@ async def list_platforms():
"name": "Pixiv",
"description": "Download artwork from Pixiv artists",
"requires_auth": True,
"auth_type": "cookies",
"auth_type": "token",
"url_pattern": "https://www.pixiv.net/users/{user_id}",
"url_examples": [
"https://www.pixiv.net/users/12345678",
@@ -103,6 +103,7 @@ async def list_platforms():
"bookmarks": "Bookmarked works",
},
"default_config": gdl_service.get_default_config_for_platform("pixiv"),
"notes": "Requires OAuth refresh token. Run 'gallery-dl oauth:pixiv' to obtain one.",
},
"deviantart": {
"name": "DeviantArt",
+8
View File
@@ -387,6 +387,8 @@ class GalleryDLService:
"please login",
"must be logged in",
"authentication required",
"authenticationerror",
"refresh-token", # Pixiv OAuth
"session expired",
"invalid cookie",
"cookies are expired",
@@ -560,6 +562,12 @@ class GalleryDLService:
config["extractor"]["discord"] = {}
config["extractor"]["discord"]["token"] = auth_token
# Add refresh token for Pixiv
if auth_token and platform == "pixiv":
if "pixiv" not in config["extractor"]:
config["extractor"]["pixiv"] = {}
config["extractor"]["pixiv"]["refresh-token"] = auth_token
# Write temporary config file
with tempfile.NamedTemporaryFile(
mode="w",
+1 -1
View File
@@ -207,7 +207,7 @@ async def _download_source_async(source_id: int, download_id: int = None) -> dic
cookies_path = None
auth_token = None
if source_platform == "discord":
if source_platform in ("discord", "pixiv"):
auth_token = await cred_manager.get_token(source_platform)
if not auth_token:
logger.warning(f"No token for {source_platform}, download may fail")
+251 -2
View File
@@ -6,6 +6,18 @@
let discordToken = null;
let discordTokenCapturedAt = null;
// Store for Pixiv OAuth
let pixivRefreshToken = null;
let pixivTokenCapturedAt = null;
let pixivOAuthPending = null; // { codeVerifier, tabId, resolve, reject }
// Pixiv OAuth constants (these are public - used by all Pixiv mobile apps)
const PIXIV_CLIENT_ID = 'MOBrBDS8blbauoSck0ZfDbtuzpyT';
const PIXIV_CLIENT_SECRET = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj';
const PIXIV_OAUTH_URL = 'https://app-api.pixiv.net/web/v1/login';
const PIXIV_TOKEN_URL = 'https://oauth.secure.pixiv.net/auth/token';
const PIXIV_REDIRECT_URI = 'https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback';
// Track initialization state
let initialized = false;
@@ -18,6 +30,7 @@ async function ensureInitialized() {
try {
await api.init();
await loadDiscordToken();
await loadPixivToken();
initialized = true;
console.log('Extension initialized successfully');
} catch (error) {
@@ -66,6 +79,190 @@ async function saveDiscordToken(token) {
console.log('Discord token captured and saved');
}
/**
* Load Pixiv token from storage
*/
async function loadPixivToken() {
const stored = await browser.storage.local.get(['pixivRefreshToken', 'pixivTokenCapturedAt']);
if (stored.pixivRefreshToken) {
pixivRefreshToken = stored.pixivRefreshToken;
pixivTokenCapturedAt = stored.pixivTokenCapturedAt;
console.log('Loaded Pixiv refresh token from storage (captured:', pixivTokenCapturedAt, ')');
}
}
/**
* Save Pixiv refresh token to storage
*/
async function savePixivToken(token) {
pixivRefreshToken = token;
pixivTokenCapturedAt = new Date().toISOString();
await browser.storage.local.set({
pixivRefreshToken: token,
pixivTokenCapturedAt: pixivTokenCapturedAt
});
console.log('Pixiv refresh token captured and saved');
}
/**
* Generate a random code verifier for PKCE (43-128 chars, URL-safe)
*/
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
/**
* Generate code challenge from verifier (SHA-256 hash, base64url encoded)
*/
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
/**
* Base64 URL encode (no padding, URL-safe characters)
*/
function base64UrlEncode(buffer) {
const base64 = btoa(String.fromCharCode(...buffer));
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
/**
* Initiate Pixiv OAuth flow
* Opens a new tab for the user to log in, then captures the authorization code
*/
async function initiatePixivOAuth() {
// Generate PKCE values
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Build OAuth URL
const params = new URLSearchParams({
code_challenge: codeChallenge,
code_challenge_method: 'S256',
client: 'pixiv-android'
});
const authUrl = `${PIXIV_OAUTH_URL}?${params.toString()}`;
console.log('Initiating Pixiv OAuth flow...');
// Create a promise that will be resolved when we get the callback
return new Promise((resolve, reject) => {
// Store the pending OAuth state
pixivOAuthPending = {
codeVerifier,
resolve,
reject,
timeoutId: setTimeout(() => {
pixivOAuthPending = null;
reject(new Error('Pixiv OAuth timed out. Please try again.'));
}, 5 * 60 * 1000) // 5 minute timeout
};
// Open the auth URL in a new tab
browser.tabs.create({ url: authUrl }).then(tab => {
pixivOAuthPending.tabId = tab.id;
console.log('Opened Pixiv OAuth tab:', tab.id);
}).catch(err => {
clearTimeout(pixivOAuthPending.timeoutId);
pixivOAuthPending = null;
reject(err);
});
});
}
/**
* Exchange authorization code for tokens
*/
async function exchangePixivCode(code, codeVerifier) {
console.log('Exchanging Pixiv authorization code for tokens...');
const params = new URLSearchParams({
client_id: PIXIV_CLIENT_ID,
client_secret: PIXIV_CLIENT_SECRET,
code: code,
code_verifier: codeVerifier,
grant_type: 'authorization_code',
include_policy: 'true',
redirect_uri: PIXIV_REDIRECT_URI
});
const response = await fetch(PIXIV_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 11; Pixel 5)'
},
body: params.toString()
});
if (!response.ok) {
const errorText = await response.text();
console.error('Pixiv token exchange failed:', response.status, errorText);
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = await response.json();
console.log('Pixiv token exchange successful');
if (!data.refresh_token) {
throw new Error('No refresh token in response');
}
return data.refresh_token;
}
/**
* Listen for Pixiv OAuth callback
* Intercepts the redirect to pixiv:// scheme and extracts the authorization code
*/
browser.webRequest.onBeforeRequest.addListener(
(details) => {
// Check if this is the OAuth callback
if (!pixivOAuthPending) return;
const url = new URL(details.url);
// Check for the callback URL pattern
if (url.pathname.includes('/callback') && url.searchParams.has('code')) {
const code = url.searchParams.get('code');
console.log('Captured Pixiv OAuth callback with code');
// Get the pending OAuth state
const { codeVerifier, resolve, reject, timeoutId, tabId } = pixivOAuthPending;
pixivOAuthPending = null;
clearTimeout(timeoutId);
// Close the OAuth tab
if (tabId) {
browser.tabs.remove(tabId).catch(() => {});
}
// Exchange the code for tokens
exchangePixivCode(code, codeVerifier)
.then(refreshToken => {
savePixivToken(refreshToken);
resolve(refreshToken);
})
.catch(err => {
reject(err);
});
// Cancel the request (don't actually navigate to the callback URL)
return { cancel: true };
}
},
{ urls: ['*://app-api.pixiv.net/web/v1/users/auth/pixiv/callback*'] },
['blocking']
);
/**
* Listen for Discord API requests to capture Authorization header
*/
@@ -178,6 +375,35 @@ async function exportCookies(platformKey) {
};
}
// Pixiv - use OAuth flow to get refresh token
if (platformKey === 'pixiv') {
// If we already have a token, use it; otherwise initiate OAuth
if (!pixivRefreshToken) {
try {
await initiatePixivOAuth();
} catch (err) {
throw new Error(`Pixiv OAuth failed: ${err.message}`);
}
}
if (!pixivRefreshToken) {
throw new Error('Failed to obtain Pixiv refresh token');
}
credentialData = pixivRefreshToken;
credentialType = 'token';
// Send to backend
const result = await api.uploadCredentials(platformKey, credentialType, credentialData);
return {
success: true,
platform: platformKey,
tokenCapturedAt: pixivTokenCapturedAt,
message: result.message || 'Refresh token exported successfully'
};
}
// Other token-based platforms require manual entry
throw new Error(`${platform.name} requires manual token entry in the web UI.`);
}
@@ -216,8 +442,8 @@ async function exportAllCookies() {
for (const platformKey of platforms) {
const platform = PLATFORMS[platformKey];
// Skip token-based platforms (except Discord if we have a token)
if (platform.authType === 'token' && platformKey !== 'discord') {
// Skip token-based platforms (except Discord and Pixiv if we have tokens)
if (platform.authType === 'token' && !['discord', 'pixiv'].includes(platformKey)) {
results[platformKey] = {
success: false,
platform: platformKey,
@@ -238,6 +464,17 @@ async function exportAllCookies() {
continue;
}
// Skip Pixiv if no token captured (don't auto-trigger OAuth in bulk export)
if (platformKey === 'pixiv' && !pixivRefreshToken) {
results[platformKey] = {
success: false,
platform: platformKey,
skipped: true,
message: 'No token - click Pixiv card to authenticate'
};
continue;
}
try {
results[platformKey] = await exportCookies(platformKey);
} catch (error) {
@@ -275,6 +512,18 @@ async function getPlatformStatus() {
? `Token captured ${new Date(discordTokenCapturedAt).toLocaleString()}`
: 'Open Discord in browser to capture token'
};
} else if (key === 'pixiv') {
status[key] = {
name: platform.name,
authType: 'token',
hasToken: !!pixivRefreshToken,
tokenCapturedAt: pixivTokenCapturedAt,
hasCookies: false,
cookieCount: 0,
note: pixivRefreshToken
? `Token captured ${new Date(pixivTokenCapturedAt).toLocaleString()}`
: 'Click to authenticate with Pixiv'
};
} else {
status[key] = {
name: platform.name,
+4 -1
View File
@@ -19,13 +19,16 @@
"tabs",
"activeTab",
"webRequest",
"webRequestBlocking",
"*://*.patreon.com/*",
"*://*.subscribestar.com/*",
"*://*.subscribestar.adult/*",
"*://*.hentai-foundry.com/*",
"*://*.discord.com/*",
"*://*.pixiv.net/*",
"*://*.deviantart.com/*"
"*://*.deviantart.com/*",
"*://app-api.pixiv.net/*",
"*://oauth.secure.pixiv.net/*"
],
"browser_action": {
+15 -2
View File
@@ -165,9 +165,9 @@ function createPlatformCard(key, platform, status) {
card.className = 'platform-card';
card.dataset.platform = key;
// Discord is clickable if we have a token captured
// Discord and Pixiv are clickable (Pixiv initiates OAuth if no token)
// Other token-based platforms are disabled
const isDisabled = platform.authType === 'token' && key !== 'discord';
const isDisabled = platform.authType === 'token' && !['discord', 'pixiv'].includes(key);
const isDiscordWithoutToken = key === 'discord' && !status.hasToken;
if (isDisabled || isDiscordWithoutToken) {
@@ -236,6 +236,14 @@ function getStatusText(status, platform, key) {
return 'Open Discord in browser to capture token';
}
// Special handling for Pixiv OAuth
if (key === 'pixiv') {
if (status.hasToken) {
return 'Token captured - ready to export';
}
return 'Click to authenticate with Pixiv';
}
if (platform.authType === 'token') {
return 'Manual token entry required';
}
@@ -261,6 +269,11 @@ function getStatusClass(status, platform, key) {
return status.hasToken ? 'ready' : 'no-cookies';
}
// Special handling for Pixiv
if (key === 'pixiv') {
return status.hasToken ? 'ready' : 'no-cookies';
}
if (platform.authType === 'token') {
return 'no-cookies';
}
+19 -2
View File
@@ -104,7 +104,11 @@
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<template v-if="uploadPlatform?.auth_type === 'token'">
<template v-if="uploadPlatformKey === 'pixiv'">
Paste your Pixiv OAuth refresh token below.
Run <code>gallery-dl oauth:pixiv</code> to obtain one.
</template>
<template v-else-if="uploadPlatform?.auth_type === 'token'">
Paste your Discord user token below.
<strong>Never share your token with anyone!</strong>
</template>
@@ -128,7 +132,17 @@
How to get {{ uploadPlatform?.auth_type === 'token' ? 'your token' : 'cookies' }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<template v-if="uploadPlatform?.auth_type === 'token'">
<template v-if="uploadPlatformKey === 'pixiv'">
<ol>
<li>Install gallery-dl: <code>pip install gallery-dl</code></li>
<li>Run: <code>gallery-dl oauth:pixiv</code></li>
<li>A browser window will open for Pixiv login</li>
<li>After authorizing, copy the refresh token displayed</li>
<li>Paste the token here</li>
</ol>
<p class="mt-2 text-caption">The refresh token is a long string that gallery-dl uses to authenticate with Pixiv's API.</p>
</template>
<template v-else-if="uploadPlatform?.auth_type === 'token'">
<ol>
<li>Open Discord in your browser</li>
<li>Press F12 to open Developer Tools</li>
@@ -285,6 +299,9 @@ function openUploadDialog(platform, info) {
}
function getPlaceholder() {
if (uploadPlatformKey.value === 'pixiv') {
return 'AZX1abc2DEF3ghi...'
}
if (uploadPlatform.value?.auth_type === 'token') {
return 'mfa.AbCdEf123456...'
}
+15 -4
View File
@@ -1,6 +1,6 @@
# GallerySubscriber - Project Summary
**Updated:** 2026-02-02 (Added Pixiv and DeviantArt platform support)
**Updated:** 2026-02-04 (Fixed Pixiv authentication - requires OAuth refresh token)
## Overview
@@ -159,9 +159,17 @@ Download (1) ──→ (many) ContentItem
| SubscribeStar Adult | Cookies | images, attachments | Infinite scroll (complete) |
| Hentai Foundry | Cookies | pictures, scraps, stories | All pages (complete) |
| Discord | Token | messages, attachments, embeds | All threads included |
| Pixiv | Cookies | illusts, manga, bookmarks | All works (complete) |
| Pixiv | Refresh Token | illusts, manga, bookmarks | All works (complete) |
| DeviantArt | Cookies (optional) | gallery, scraps, favorites | All deviations (complete) |
**Pixiv Authentication:** Pixiv requires an OAuth refresh token (not cookies). The Firefox extension handles this automatically:
1. Click the Pixiv card in the extension popup
2. A new tab opens for Pixiv login
3. Log in with your Pixiv username/password
4. The extension automatically captures the OAuth token and exports it
Alternatively, you can manually obtain a token by running `gallery-dl oauth:pixiv` and adding it via the web UI.
**Note:** All platforms fetch complete history by default - no depth limits. Gallery-dl paginates through ALL available content.
## Configuration
@@ -266,8 +274,11 @@ docker compose -f docker-compose.dev.yml up
1. User installs Firefox extension
2. User configures API URL and key in extension options
3. Extension detects supported platforms when user visits them
4. User clicks "Export Cookies" in popup
5. Extension sends encrypted cookies to backend API
4. User clicks platform card in popup to export credentials:
- **Cookie-based platforms** (Patreon, SubscribeStar, etc.): Exports browser cookies
- **Discord**: Captures auth token from API requests (browse Discord first)
- **Pixiv**: Opens OAuth login flow, captures refresh token automatically
5. Extension sends encrypted credentials to backend API
6. Backend stores credentials for gallery-dl authentication
## Download Workflow