added discord functionality to extension and polished some of downloader process.

This commit is contained in:
Bryan Van Deusen
2026-01-24 23:15:03 -05:00
parent b9b8048a2d
commit 058901cd05
6 changed files with 186 additions and 28 deletions
+3 -2
View File
@@ -172,11 +172,12 @@ async def verify_credentials():
# TODO: Implement actual verification by making test request # TODO: Implement actual verification by making test request
# For now, just mark as verified # For now, just mark as verified
credential.last_verified = datetime.utcnow() verified_at = datetime.utcnow()
credential.last_verified = verified_at
await session.commit() await session.commit()
return jsonify({ return jsonify({
"platform": platform, "platform": platform,
"is_valid": True, "is_valid": True,
"last_verified": credential.last_verified.isoformat(), "last_verified": verified_at.isoformat(),
}) })
+25 -4
View File
@@ -134,13 +134,16 @@ class GalleryDLService:
- Managing temporary config files - Managing temporary config files
""" """
# Platform-specific default content types # Platform-specific default content types and options
# Directory patterns are relative - subscription_name/platform is prepended automatically # Directory patterns are relative - subscription_name/platform is prepended automatically
PLATFORM_DEFAULTS = { PLATFORM_DEFAULTS = {
"patreon": { "patreon": {
"content_types": ["images", "image_large", "attachments", "postfile", "content"], "content_types": ["images", "image_large", "attachments", "postfile", "content"],
"directory": ["{date:%Y-%m-%d}_{id}_{title[:40]}"], "directory": ["{date:%Y-%m-%d}_{id}_{title[:40]}"],
"filename": "{num:>02}_{filename}.{extension}", "filename": "{num:>02}_{filename}.{extension}",
# Additional options
"videos": True, # Download video content
"embeds": True, # Download embedded URLs (YouTube, etc.)
}, },
"subscribestar": { "subscribestar": {
"content_types": ["all"], "content_types": ["all"],
@@ -151,11 +154,16 @@ class GalleryDLService:
"content_types": ["pictures"], "content_types": ["pictures"],
"directory": [], # Flat structure within platform folder "directory": [], # Flat structure within platform folder
"filename": "{category}_{index:>03}_{title[:50]}.{extension}", "filename": "{category}_{index:>03}_{title[:50]}.{extension}",
"include": "all", # Include all content types
}, },
"discord": { "discord": {
"content_types": ["all"], "content_types": ["all"],
"directory": ["{channel}"], "directory": ["{channel[name]}"],
"filename": "{date:%Y%m%d}_{message_id}_{filename}.{extension}", "filename": "{date:%Y%m%d}_{id}_{filename}.{extension}",
# Additional options
"embeds": True, # Download embedded URLs
"stickers": True, # Download stickers
"reactions": False, # Skip reaction images
}, },
} }
@@ -256,6 +264,11 @@ class GalleryDLService:
# Build platform-specific section # Build platform-specific section
platform_config = {} platform_config = {}
# Apply all platform defaults first (videos, embeds, stickers, etc.)
for key, value in platform_defaults.items():
if key not in ["content_types", "directory", "filename"]:
platform_config[key] = value
# Content types (platform-specific handling) # Content types (platform-specific handling)
if platform == "patreon": if platform == "patreon":
# Patreon uses "files" array for content types # Patreon uses "files" array for content types
@@ -266,7 +279,7 @@ class GalleryDLService:
elif platform == "hentaifoundry": elif platform == "hentaifoundry":
# HentaiFoundry uses "include" for content filtering # HentaiFoundry uses "include" for content filtering
if "pictures" in source_config.content_types or "all" in source_config.content_types: if "pictures" in source_config.content_types or "all" in source_config.content_types:
platform_config["include"] = "pictures" platform_config["include"] = "all"
# Directory pattern # Directory pattern
if source_config.directory_pattern: if source_config.directory_pattern:
@@ -342,6 +355,7 @@ class GalleryDLService:
platform: str, platform: str,
source_config: Optional[SourceConfig] = None, source_config: Optional[SourceConfig] = None,
cookies_path: Optional[str] = None, cookies_path: Optional[str] = None,
auth_token: Optional[str] = None,
) -> DownloadResult: ) -> DownloadResult:
"""Execute a gallery-dl download. """Execute a gallery-dl download.
@@ -351,6 +365,7 @@ class GalleryDLService:
platform: Platform name (patreon, subscribestar, etc.) platform: Platform name (patreon, subscribestar, etc.)
source_config: Optional per-source configuration overrides source_config: Optional per-source configuration overrides
cookies_path: Optional path to cookies file cookies_path: Optional path to cookies file
auth_token: Optional auth token (for Discord and other token-based platforms)
Returns: Returns:
DownloadResult with success status and details DownloadResult with success status and details
@@ -377,6 +392,12 @@ class GalleryDLService:
if cookies_path: if cookies_path:
config["extractor"]["cookies"] = cookies_path config["extractor"]["cookies"] = cookies_path
# Add auth token for Discord
if auth_token and platform == "discord":
if "discord" not in config["extractor"]:
config["extractor"]["discord"] = {}
config["extractor"]["discord"]["token"] = auth_token
# Write temporary config file # Write temporary config file
with tempfile.NamedTemporaryFile( with tempfile.NamedTemporaryFile(
mode="w", mode="w",
+12 -3
View File
@@ -95,10 +95,18 @@ async def _download_source_async(source_id: int) -> dict:
try: try:
# Get credentials # Get credentials
cred_manager = CredentialManager(session) cred_manager = CredentialManager(session)
cookies_path = await cred_manager.get_cookies_path(source.platform) cookies_path = None
auth_token = None
if not cookies_path and source.platform in ["patreon", "subscribestar"]: # Discord uses token auth, others use cookies
logger.warning(f"No credentials for {source.platform}, download may fail") if source.platform == "discord":
auth_token = await cred_manager.get_token(source.platform)
if not auth_token:
logger.warning(f"No token for {source.platform}, download may fail")
else:
cookies_path = await cred_manager.get_cookies_path(source.platform)
if not cookies_path and source.platform in ["patreon", "subscribestar"]:
logger.warning(f"No credentials for {source.platform}, download may fail")
# Build source config from metadata # Build source config from metadata
source_config = SourceConfig.from_dict(source.metadata_ or {}) source_config = SourceConfig.from_dict(source.metadata_ or {})
@@ -111,6 +119,7 @@ async def _download_source_async(source_id: int) -> dict:
platform=source.platform, platform=source.platform,
source_config=source_config, source_config=source_config,
cookies_path=cookies_path, cookies_path=cookies_path,
auth_token=auth_token,
) )
# Update download record # Update download record
+114 -11
View File
@@ -2,16 +2,75 @@
* Background script - handles cookie operations and API calls * Background script - handles cookie operations and API calls
*/ */
// Store for captured Discord token
let discordToken = null;
let discordTokenCapturedAt = null;
// Initialize API on startup // Initialize API on startup
browser.runtime.onInstalled.addListener(async () => { browser.runtime.onInstalled.addListener(async () => {
console.log('GallerySubscriber extension installed'); console.log('GallerySubscriber extension installed');
await api.init(); await api.init();
await loadDiscordToken();
}); });
browser.runtime.onStartup.addListener(async () => { browser.runtime.onStartup.addListener(async () => {
await api.init(); await api.init();
await loadDiscordToken();
}); });
/**
* Load Discord token from storage
*/
async function loadDiscordToken() {
const stored = await browser.storage.local.get(['discordToken', 'discordTokenCapturedAt']);
if (stored.discordToken) {
discordToken = stored.discordToken;
discordTokenCapturedAt = stored.discordTokenCapturedAt;
console.log('Loaded Discord token from storage (captured:', discordTokenCapturedAt, ')');
}
}
/**
* Save Discord token to storage
*/
async function saveDiscordToken(token) {
discordToken = token;
discordTokenCapturedAt = new Date().toISOString();
await browser.storage.local.set({
discordToken: token,
discordTokenCapturedAt: discordTokenCapturedAt
});
console.log('Discord token captured and saved');
}
/**
* Listen for Discord API requests to capture Authorization header
*/
browser.webRequest.onSendHeaders.addListener(
(details) => {
// Look for Authorization header
const authHeader = details.requestHeaders.find(
h => h.name.toLowerCase() === 'authorization'
);
if (authHeader && authHeader.value) {
// Discord user tokens don't have "Bearer " prefix
// Bot tokens have "Bot " prefix - we want user tokens
const token = authHeader.value;
// Only capture if it looks like a user token (not a bot token)
if (!token.startsWith('Bot ') && token.length > 50) {
// Only save if it's different from what we have
if (token !== discordToken) {
saveDiscordToken(token);
}
}
}
},
{ urls: ['*://discord.com/api/*', '*://*.discord.com/api/*'] },
['requestHeaders']
);
// Handle messages from popup and options pages // Handle messages from popup and options pages
browser.runtime.onMessage.addListener((message, sender, sendResponse) => { browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message) handleMessage(message)
@@ -70,8 +129,27 @@ async function exportCookies(platformKey) {
let credentialType; let credentialType;
if (platform.authType === 'token') { if (platform.authType === 'token') {
// Discord and other token-based platforms require manual entry // Discord - use captured token from webRequest listener
throw new Error(`${platform.name} requires manual token entry in the web UI. Token extraction is not supported for security reasons.`); if (platformKey === 'discord') {
if (!discordToken) {
throw new Error(`No Discord token captured. Please open Discord in this browser and interact with it (e.g., switch channels) to capture the token.`);
}
credentialData = discordToken;
credentialType = 'token';
// Send to backend
const result = await api.uploadCredentials(platformKey, credentialType, credentialData);
return {
success: true,
platform: platformKey,
tokenCapturedAt: discordTokenCapturedAt,
message: result.message || 'Token exported successfully'
};
}
// Other token-based platforms require manual entry
throw new Error(`${platform.name} requires manual token entry in the web UI.`);
} }
// Cookie-based auth // Cookie-based auth
@@ -108,8 +186,8 @@ async function exportAllCookies() {
for (const platformKey of platforms) { for (const platformKey of platforms) {
const platform = PLATFORMS[platformKey]; const platform = PLATFORMS[platformKey];
// Skip token-based platforms // Skip token-based platforms (except Discord if we have a token)
if (platform.authType === 'token') { if (platform.authType === 'token' && platformKey !== 'discord') {
results[platformKey] = { results[platformKey] = {
success: false, success: false,
platform: platformKey, platform: platformKey,
@@ -119,6 +197,17 @@ async function exportAllCookies() {
continue; continue;
} }
// Skip Discord if no token captured
if (platformKey === 'discord' && !discordToken) {
results[platformKey] = {
success: false,
platform: platformKey,
skipped: true,
message: 'No token captured - open Discord in browser first'
};
continue;
}
try { try {
results[platformKey] = await exportCookies(platformKey); results[platformKey] = await exportCookies(platformKey);
} catch (error) { } catch (error) {
@@ -144,13 +233,27 @@ async function getPlatformStatus() {
try { try {
if (platform.authType === 'token') { if (platform.authType === 'token') {
// Token-based platforms // Token-based platforms
status[key] = { if (key === 'discord') {
name: platform.name, status[key] = {
authType: 'token', name: platform.name,
hasCookies: false, authType: 'token',
cookieCount: 0, hasToken: !!discordToken,
note: platform.note || 'Requires manual token entry' tokenCapturedAt: discordTokenCapturedAt,
}; hasCookies: false,
cookieCount: 0,
note: discordToken
? `Token captured ${new Date(discordTokenCapturedAt).toLocaleString()}`
: 'Open Discord in browser to capture token'
};
} else {
status[key] = {
name: platform.name,
authType: 'token',
hasCookies: false,
cookieCount: 0,
note: platform.note || 'Requires manual token entry'
};
}
} else { } else {
// Cookie-based platforms // Cookie-based platforms
const cookies = await extractCookiesForPlatform(key); const cookies = await extractCookiesForPlatform(key);
+1
View File
@@ -9,6 +9,7 @@
"storage", "storage",
"tabs", "tabs",
"activeTab", "activeTab",
"webRequest",
"*://*.patreon.com/*", "*://*.patreon.com/*",
"*://*.subscribestar.com/*", "*://*.subscribestar.com/*",
"*://*.subscribestar.adult/*", "*://*.subscribestar.adult/*",
+31 -8
View File
@@ -86,13 +86,17 @@ function createPlatformCard(key, platform, status) {
card.className = 'platform-card'; card.className = 'platform-card';
card.dataset.platform = key; card.dataset.platform = key;
const isDisabled = platform.authType === 'token'; // Discord is clickable if we have a token captured
if (isDisabled) { // Other token-based platforms are disabled
const isDisabled = platform.authType === 'token' && key !== 'discord';
const isDiscordWithoutToken = key === 'discord' && !status.hasToken;
if (isDisabled || isDiscordWithoutToken) {
card.classList.add('disabled'); card.classList.add('disabled');
} }
const statusText = getStatusText(status, platform); const statusText = getStatusText(status, platform, key);
const statusClass = getStatusClass(status, platform); const statusClass = getStatusClass(status, platform, key);
card.innerHTML = ` card.innerHTML = `
<div class="platform-icon" style="background-color: ${platform.color}"> <div class="platform-icon" style="background-color: ${platform.color}">
@@ -109,7 +113,7 @@ function createPlatformCard(key, platform, status) {
</span> </span>
`; `;
if (!isDisabled) { if (!isDisabled && !isDiscordWithoutToken) {
card.addEventListener('click', () => exportPlatformCookies(key, card)); card.addEventListener('click', () => exportPlatformCookies(key, card));
} }
@@ -120,9 +124,18 @@ function createPlatformCard(key, platform, status) {
* Get status text for display * Get status text for display
* @param {object} status - Platform status * @param {object} status - Platform status
* @param {object} platform - Platform definition * @param {object} platform - Platform definition
* @param {string} key - Platform key
* @returns {string} * @returns {string}
*/ */
function getStatusText(status, platform) { function getStatusText(status, platform, key) {
// Special handling for Discord token-based auth
if (key === 'discord') {
if (status.hasToken) {
return 'Token captured - ready to export';
}
return 'Open Discord in browser to capture token';
}
if (platform.authType === 'token') { if (platform.authType === 'token') {
return 'Manual token entry required'; return 'Manual token entry required';
} }
@@ -139,9 +152,15 @@ function getStatusText(status, platform) {
* Get status CSS class * Get status CSS class
* @param {object} status - Platform status * @param {object} status - Platform status
* @param {object} platform - Platform definition * @param {object} platform - Platform definition
* @param {string} key - Platform key
* @returns {string} * @returns {string}
*/ */
function getStatusClass(status, platform) { function getStatusClass(status, platform, key) {
// Special handling for Discord
if (key === 'discord') {
return status.hasToken ? 'ready' : 'no-cookies';
}
if (platform.authType === 'token') { if (platform.authType === 'token') {
return 'no-cookies'; return 'no-cookies';
} }
@@ -172,7 +191,11 @@ async function exportPlatformCookies(platformKey, cardElement) {
if (result.error) { if (result.error) {
showError(result.error); showError(result.error);
} else { } else {
showSuccess(`${PLATFORMS[platformKey].name}: ${result.cookieCount} cookies exported!`); // Handle different response types (cookies vs token)
const successMsg = result.cookieCount !== undefined
? `${PLATFORMS[platformKey].name}: ${result.cookieCount} cookies exported!`
: `${PLATFORMS[platformKey].name}: Token exported successfully!`;
showSuccess(successMsg);
// Refresh status // Refresh status
await loadPlatformStatus(); await loadPlatformStatus();
} }