From 058901cd0569e0e20278a2f3331dd05ea68721a3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 24 Jan 2026 23:15:03 -0500 Subject: [PATCH] added discord functionality to extension and polished some of downloader process. --- backend/app/api/credentials.py | 5 +- backend/app/services/gallery_dl.py | 29 ++++++- backend/app/tasks/downloads.py | 15 +++- extension/background/background.js | 125 ++++++++++++++++++++++++++--- extension/manifest.json | 1 + extension/popup/popup.js | 39 +++++++-- 6 files changed, 186 insertions(+), 28 deletions(-) diff --git a/backend/app/api/credentials.py b/backend/app/api/credentials.py index c96aa0a..2260a92 100644 --- a/backend/app/api/credentials.py +++ b/backend/app/api/credentials.py @@ -172,11 +172,12 @@ async def verify_credentials(): # TODO: Implement actual verification by making test request # For now, just mark as verified - credential.last_verified = datetime.utcnow() + verified_at = datetime.utcnow() + credential.last_verified = verified_at await session.commit() return jsonify({ "platform": platform, "is_valid": True, - "last_verified": credential.last_verified.isoformat(), + "last_verified": verified_at.isoformat(), }) diff --git a/backend/app/services/gallery_dl.py b/backend/app/services/gallery_dl.py index 1dd40ae..7bac65c 100644 --- a/backend/app/services/gallery_dl.py +++ b/backend/app/services/gallery_dl.py @@ -134,13 +134,16 @@ class GalleryDLService: - 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 PLATFORM_DEFAULTS = { "patreon": { "content_types": ["images", "image_large", "attachments", "postfile", "content"], "directory": ["{date:%Y-%m-%d}_{id}_{title[:40]}"], "filename": "{num:>02}_{filename}.{extension}", + # Additional options + "videos": True, # Download video content + "embeds": True, # Download embedded URLs (YouTube, etc.) }, "subscribestar": { "content_types": ["all"], @@ -151,11 +154,16 @@ class GalleryDLService: "content_types": ["pictures"], "directory": [], # Flat structure within platform folder "filename": "{category}_{index:>03}_{title[:50]}.{extension}", + "include": "all", # Include all content types }, "discord": { "content_types": ["all"], - "directory": ["{channel}"], - "filename": "{date:%Y%m%d}_{message_id}_{filename}.{extension}", + "directory": ["{channel[name]}"], + "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 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) if platform == "patreon": # Patreon uses "files" array for content types @@ -266,7 +279,7 @@ class GalleryDLService: elif platform == "hentaifoundry": # HentaiFoundry uses "include" for content filtering if "pictures" in source_config.content_types or "all" in source_config.content_types: - platform_config["include"] = "pictures" + platform_config["include"] = "all" # Directory pattern if source_config.directory_pattern: @@ -342,6 +355,7 @@ class GalleryDLService: platform: str, source_config: Optional[SourceConfig] = None, cookies_path: Optional[str] = None, + auth_token: Optional[str] = None, ) -> DownloadResult: """Execute a gallery-dl download. @@ -351,6 +365,7 @@ class GalleryDLService: platform: Platform name (patreon, subscribestar, etc.) source_config: Optional per-source configuration overrides cookies_path: Optional path to cookies file + auth_token: Optional auth token (for Discord and other token-based platforms) Returns: DownloadResult with success status and details @@ -377,6 +392,12 @@ class GalleryDLService: if 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 with tempfile.NamedTemporaryFile( mode="w", diff --git a/backend/app/tasks/downloads.py b/backend/app/tasks/downloads.py index 042289d..6133157 100644 --- a/backend/app/tasks/downloads.py +++ b/backend/app/tasks/downloads.py @@ -95,10 +95,18 @@ async def _download_source_async(source_id: int) -> dict: try: # Get credentials 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"]: - logger.warning(f"No credentials for {source.platform}, download may fail") + # Discord uses token auth, others use cookies + 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 source_config = SourceConfig.from_dict(source.metadata_ or {}) @@ -111,6 +119,7 @@ async def _download_source_async(source_id: int) -> dict: platform=source.platform, source_config=source_config, cookies_path=cookies_path, + auth_token=auth_token, ) # Update download record diff --git a/extension/background/background.js b/extension/background/background.js index a8d7405..2292487 100644 --- a/extension/background/background.js +++ b/extension/background/background.js @@ -2,16 +2,75 @@ * Background script - handles cookie operations and API calls */ +// Store for captured Discord token +let discordToken = null; +let discordTokenCapturedAt = null; + // Initialize API on startup browser.runtime.onInstalled.addListener(async () => { console.log('GallerySubscriber extension installed'); await api.init(); + await loadDiscordToken(); }); browser.runtime.onStartup.addListener(async () => { 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 browser.runtime.onMessage.addListener((message, sender, sendResponse) => { handleMessage(message) @@ -70,8 +129,27 @@ async function exportCookies(platformKey) { let credentialType; if (platform.authType === 'token') { - // Discord and other token-based platforms require manual entry - throw new Error(`${platform.name} requires manual token entry in the web UI. Token extraction is not supported for security reasons.`); + // Discord - use captured token from webRequest listener + 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 @@ -108,8 +186,8 @@ async function exportAllCookies() { for (const platformKey of platforms) { const platform = PLATFORMS[platformKey]; - // Skip token-based platforms - if (platform.authType === 'token') { + // Skip token-based platforms (except Discord if we have a token) + if (platform.authType === 'token' && platformKey !== 'discord') { results[platformKey] = { success: false, platform: platformKey, @@ -119,6 +197,17 @@ async function exportAllCookies() { 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 { results[platformKey] = await exportCookies(platformKey); } catch (error) { @@ -144,13 +233,27 @@ async function getPlatformStatus() { try { if (platform.authType === 'token') { // Token-based platforms - status[key] = { - name: platform.name, - authType: 'token', - hasCookies: false, - cookieCount: 0, - note: platform.note || 'Requires manual token entry' - }; + if (key === 'discord') { + status[key] = { + name: platform.name, + authType: 'token', + hasToken: !!discordToken, + 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 { // Cookie-based platforms const cookies = await extractCookiesForPlatform(key); diff --git a/extension/manifest.json b/extension/manifest.json index 405f44c..d2cfc83 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -9,6 +9,7 @@ "storage", "tabs", "activeTab", + "webRequest", "*://*.patreon.com/*", "*://*.subscribestar.com/*", "*://*.subscribestar.adult/*", diff --git a/extension/popup/popup.js b/extension/popup/popup.js index 626596b..926caa8 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -86,13 +86,17 @@ function createPlatformCard(key, platform, status) { card.className = 'platform-card'; card.dataset.platform = key; - const isDisabled = platform.authType === 'token'; - if (isDisabled) { + // Discord is clickable if we have a token captured + // 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'); } - const statusText = getStatusText(status, platform); - const statusClass = getStatusClass(status, platform); + const statusText = getStatusText(status, platform, key); + const statusClass = getStatusClass(status, platform, key); card.innerHTML = `
@@ -109,7 +113,7 @@ function createPlatformCard(key, platform, status) { `; - if (!isDisabled) { + if (!isDisabled && !isDiscordWithoutToken) { card.addEventListener('click', () => exportPlatformCookies(key, card)); } @@ -120,9 +124,18 @@ function createPlatformCard(key, platform, status) { * Get status text for display * @param {object} status - Platform status * @param {object} platform - Platform definition + * @param {string} key - Platform key * @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') { return 'Manual token entry required'; } @@ -139,9 +152,15 @@ function getStatusText(status, platform) { * Get status CSS class * @param {object} status - Platform status * @param {object} platform - Platform definition + * @param {string} key - Platform key * @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') { return 'no-cookies'; } @@ -172,7 +191,11 @@ async function exportPlatformCookies(platformKey, cardElement) { if (result.error) { showError(result.error); } 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 await loadPlatformStatus(); }