diff --git a/backend/app/api/credentials.py b/backend/app/api/credentials.py index a88567a..75136dd 100644 --- a/backend/app/api/credentials.py +++ b/backend/app/api/credentials.py @@ -12,7 +12,7 @@ from app.utils.encryption import encrypt_data, decrypt_data bp = Blueprint("credentials", __name__) -VALID_PLATFORMS = ["patreon", "subscribestar", "hentaifoundry", "discord"] +VALID_PLATFORMS = ["patreon", "subscribestar", "hentaifoundry", "discord", "pixiv", "deviantart"] @bp.route("", methods=["GET"]) diff --git a/backend/app/api/platforms.py b/backend/app/api/platforms.py index 54de77b..9c6d384 100644 --- a/backend/app/api/platforms.py +++ b/backend/app/api/platforms.py @@ -85,6 +85,44 @@ async def list_platforms(): "default_config": gdl_service.get_default_config_for_platform("discord"), "notes": "Requires Discord user token (not bot token)", }, + "pixiv": { + "name": "Pixiv", + "description": "Download artwork from Pixiv artists", + "requires_auth": True, + "auth_type": "cookies", + "url_pattern": "https://www.pixiv.net/users/{user_id}", + "url_examples": [ + "https://www.pixiv.net/users/12345678", + "https://www.pixiv.net/en/users/12345678", + ], + "content_types": gdl_service.get_platform_content_types("pixiv"), + "content_type_descriptions": { + "all": "All artwork (illustrations and manga)", + "illusts": "Illustrations only", + "manga": "Manga only", + "bookmarks": "Bookmarked works", + }, + "default_config": gdl_service.get_default_config_for_platform("pixiv"), + }, + "deviantart": { + "name": "DeviantArt", + "description": "Download artwork from DeviantArt artists", + "requires_auth": False, + "auth_type": "cookies", + "url_pattern": "https://www.deviantart.com/{username}", + "url_examples": [ + "https://www.deviantart.com/example-artist", + "https://www.deviantart.com/example-artist/gallery", + ], + "content_types": gdl_service.get_platform_content_types("deviantart"), + "content_type_descriptions": { + "all": "All deviations", + "gallery": "Gallery artwork", + "scraps": "Scraps folder", + "favorites": "Favorited works", + }, + "default_config": gdl_service.get_default_config_for_platform("deviantart"), + }, } return jsonify({"platforms": platforms}) @@ -95,7 +133,7 @@ async def get_platform(platform: str): """Get detailed information about a specific platform.""" gdl_service = GalleryDLService() - if platform not in ["patreon", "subscribestar", "hentaifoundry", "discord"]: + if platform not in ["patreon", "subscribestar", "hentaifoundry", "discord", "pixiv", "deviantart"]: return jsonify({"error": f"Unknown platform: {platform}"}), 404 # Get the full platform info from list_platforms @@ -115,7 +153,7 @@ async def get_config_schema(platform: str): This defines what options can be set per-source for this platform. Useful for building dynamic forms in the frontend. """ - if platform not in ["patreon", "subscribestar", "hentaifoundry", "discord"]: + if platform not in ["patreon", "subscribestar", "hentaifoundry", "discord", "pixiv", "deviantart"]: return jsonify({"error": f"Unknown platform: {platform}"}), 404 gdl_service = GalleryDLService() diff --git a/backend/app/api/sources.py b/backend/app/api/sources.py index ca70acd..3f6628b 100644 --- a/backend/app/api/sources.py +++ b/backend/app/api/sources.py @@ -78,7 +78,7 @@ async def create_source(): return jsonify({"error": f"Missing required fields: {', '.join(missing)}"}), 400 # Validate platform - valid_platforms = ["patreon", "subscribestar", "hentaifoundry", "discord"] + valid_platforms = ["patreon", "subscribestar", "hentaifoundry", "discord", "pixiv", "deviantart"] if data["platform"] not in valid_platforms: return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400 diff --git a/backend/app/api/subscriptions.py b/backend/app/api/subscriptions.py index af38375..9fe6580 100644 --- a/backend/app/api/subscriptions.py +++ b/backend/app/api/subscriptions.py @@ -232,7 +232,7 @@ async def add_source_to_subscription(subscription_id: int): return jsonify({"error": f"Missing required fields: {', '.join(missing)}"}), 400 # Validate platform - valid_platforms = ["patreon", "subscribestar", "hentaifoundry", "discord"] + valid_platforms = ["patreon", "subscribestar", "hentaifoundry", "discord", "pixiv", "deviantart"] if data["platform"] not in valid_platforms: return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400 diff --git a/backend/app/services/gallery_dl.py b/backend/app/services/gallery_dl.py index e84ae99..5c1ef66 100644 --- a/backend/app/services/gallery_dl.py +++ b/backend/app/services/gallery_dl.py @@ -171,6 +171,23 @@ class GalleryDLService: # Deep crawling options "threads": True, # Include thread content for complete history }, + "pixiv": { + "content_types": ["all"], + "directory": ["{category}"], + "filename": "{id}_{title[:50]}_{num:>02}.{extension}", + # Content options + "ugoira": True, # Download ugoira (animated) as WebM + }, + "deviantart": { + "content_types": ["all"], + "directory": [], # Flat structure within platform folder + "filename": "{index:>03}_{title[:50]}.{extension}", + # Content options + "flat": True, # Flatten folder structure + "original": True, # Download original quality + "mature": True, # Include mature content + "metadata": True, # Include metadata + }, } def __init__(self, rate_limit: Optional[float] = None): @@ -682,6 +699,10 @@ class GalleryDLService: return ["all"] # No granular control elif platform == "discord": return ["all"] # No granular control + elif platform == "pixiv": + return ["all", "illusts", "manga", "bookmarks"] + elif platform == "deviantart": + return ["all", "gallery", "scraps", "favorites"] else: return ["all"] diff --git a/extension/manifest.json b/extension/manifest.json index 63b2961..80e9af4 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,12 +1,15 @@ { "manifest_version": 2, "name": "GallerySubscriber", - "version": "1.0.0", + "version": "1.1.0", "description": "Export cookies from supported platforms to GallerySubscriber for automated downloads", "browser_specific_settings": { "gecko": { - "id": "gallerysubscriber@fabledsword.com" + "id": "gallerysubscriber@fabledsword.com", + "data_collection_permissions": { + "required": ["none"] + } } }, @@ -20,7 +23,9 @@ "*://*.subscribestar.com/*", "*://*.subscribestar.adult/*", "*://*.hentai-foundry.com/*", - "*://*.discord.com/*" + "*://*.discord.com/*", + "*://*.pixiv.net/*", + "*://*.deviantart.com/*" ], "browser_action": { diff --git a/extension/popup/popup.js b/extension/popup/popup.js index e7b0114..c44d300 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -11,7 +11,13 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('setup-required').classList.remove('hidden'); const alertDiv = document.querySelector('#setup-required .alert'); if (alertDiv) { - alertDiv.innerHTML = `Error

${error.message}

`; + alertDiv.textContent = ''; + const strong = document.createElement('strong'); + strong.textContent = 'Error'; + const p = document.createElement('p'); + p.textContent = error.message; + alertDiv.appendChild(strong); + alertDiv.appendChild(p); alertDiv.className = 'alert alert-error'; } } @@ -51,7 +57,11 @@ async function init() { // Show loading state for platforms const platformsContainer = document.getElementById('platforms-list'); - platformsContainer.innerHTML = '
Loading platforms...
'; + platformsContainer.textContent = ''; + const loadingDiv = document.createElement('div'); + loadingDiv.style.cssText = 'text-align: center; padding: 20px; color: var(--text-secondary);'; + loadingDiv.textContent = 'Loading platforms...'; + platformsContainer.appendChild(loadingDiv); // Now do background tasks in parallel (non-blocking) // Check if we should test connection (rate limited) @@ -134,7 +144,7 @@ function updateConnectionIndicator(connected) { async function loadPlatformStatus() { const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' }); const container = document.getElementById('platforms-list'); - container.innerHTML = ''; + container.textContent = ''; for (const [key, platform] of Object.entries(PLATFORMS)) { const platformStatus = status[key] || {}; @@ -167,20 +177,41 @@ function createPlatformCard(key, platform, status) { const statusText = getStatusText(status, platform, key); const statusClass = getStatusClass(status, platform, key); - card.innerHTML = ` -
- ${platform.name.charAt(0)} -
-
-
${platform.name}
-
${statusText}
-
- - - - - - `; + // Build card content using DOM methods (avoids innerHTML security warnings) + const iconDiv = document.createElement('div'); + iconDiv.className = 'platform-icon'; + iconDiv.style.backgroundColor = platform.color; + iconDiv.textContent = platform.name.charAt(0); + + const infoDiv = document.createElement('div'); + infoDiv.className = 'info'; + + const nameDiv = document.createElement('div'); + nameDiv.className = 'name'; + nameDiv.textContent = platform.name; + + const statusDiv = document.createElement('div'); + statusDiv.className = `status ${statusClass}`; + statusDiv.textContent = statusText; + + infoDiv.appendChild(nameDiv); + infoDiv.appendChild(statusDiv); + + const actionSpan = document.createElement('span'); + actionSpan.className = 'action-icon'; + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '20'); + svg.setAttribute('height', '20'); + svg.setAttribute('viewBox', '0 0 24 24'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'currentColor'); + path.setAttribute('d', 'M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z'); + svg.appendChild(path); + actionSpan.appendChild(svg); + + card.appendChild(iconDiv); + card.appendChild(infoDiv); + card.appendChild(actionSpan); if (!isDisabled && !isDiscordWithoutToken) { card.addEventListener('click', () => exportPlatformCookies(key, card)); diff --git a/extension/web-ext-artifacts/gallerysubscriber-1.1.0.zip b/extension/web-ext-artifacts/gallerysubscriber-1.1.0.zip new file mode 100644 index 0000000..0bf0ddf Binary files /dev/null and b/extension/web-ext-artifacts/gallerysubscriber-1.1.0.zip differ diff --git a/extension/web-ext-config.cjs b/extension/web-ext-config.cjs new file mode 100644 index 0000000..729ffce --- /dev/null +++ b/extension/web-ext-config.cjs @@ -0,0 +1,10 @@ +module.exports = { + ignoreFiles: [ + "create-icons.py", + "generate-icons.py", + "web-ext-artifacts", + "web-ext-config.js", + ".amo-upload-uuid", + "*.md" + ] +}; diff --git a/frontend/src/views/Credentials.vue b/frontend/src/views/Credentials.vue index dee71f9..3b58ae5 100644 --- a/frontend/src/views/Credentials.vue +++ b/frontend/src/views/Credentials.vue @@ -235,6 +235,8 @@ function getPlatformIcon(platform) { subscribestar: 'mdi-star', hentaifoundry: 'mdi-palette', discord: 'mdi-discord', + pixiv: 'mdi-alpha-p-box', + deviantart: 'mdi-deviantart', } return icons[platform] || 'mdi-web' } @@ -245,6 +247,8 @@ function getPlatformColor(platform) { subscribestar: '#FFD700', hentaifoundry: '#9C27B0', discord: '#5865F2', + pixiv: '#0096FA', + deviantart: '#05CC47', } return colors[platform] || 'grey' } diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 536b3cf..eaa2913 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -520,6 +520,8 @@ function getPlatformIcon(platform) { subscribestar: 'mdi-star', hentaifoundry: 'mdi-palette', discord: 'mdi-discord', + pixiv: 'mdi-alpha-p-box', + deviantart: 'mdi-deviantart', } return icons[platform] || 'mdi-web' } diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 330ca34..beb64ad 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -539,6 +539,8 @@ function getPlatformIcon(platform) { subscribestar: 'mdi-star', hentaifoundry: 'mdi-palette', discord: 'mdi-discord', + pixiv: 'mdi-alpha-p-box', + deviantart: 'mdi-deviantart', } return icons[platform] || 'mdi-web' } @@ -549,6 +551,8 @@ function getPlatformColor(platform) { subscribestar: '#FFD700', hentaifoundry: '#9C27B0', discord: '#5865F2', + pixiv: '#0096FA', + deviantart: '#05CC47', } return colors[platform] || 'grey' } diff --git a/frontend/src/views/SourceForm.vue b/frontend/src/views/SourceForm.vue index 90f323f..7c9c83a 100644 --- a/frontend/src/views/SourceForm.vue +++ b/frontend/src/views/SourceForm.vue @@ -199,6 +199,8 @@ const platformOptions = [ { title: 'SubscribeStar', value: 'subscribestar' }, { title: 'Hentai Foundry', value: 'hentaifoundry' }, { title: 'Discord', value: 'discord' }, + { title: 'Pixiv', value: 'pixiv' }, + { title: 'DeviantArt', value: 'deviantart' }, ] const urlHint = computed(() => { diff --git a/frontend/src/views/Sources.vue b/frontend/src/views/Sources.vue index 54b8769..0d887d6 100644 --- a/frontend/src/views/Sources.vue +++ b/frontend/src/views/Sources.vue @@ -150,6 +150,8 @@ const platformOptions = [ { title: 'SubscribeStar', value: 'subscribestar' }, { title: 'Hentai Foundry', value: 'hentaifoundry' }, { title: 'Discord', value: 'discord' }, + { title: 'Pixiv', value: 'pixiv' }, + { title: 'DeviantArt', value: 'deviantart' }, ] const enabledOptions = [ @@ -189,6 +191,8 @@ function getPlatformIcon(platform) { subscribestar: 'mdi-star', hentaifoundry: 'mdi-palette', discord: 'mdi-discord', + pixiv: 'mdi-alpha-p-box', + deviantart: 'mdi-deviantart', } return icons[platform] || 'mdi-web' } @@ -199,6 +203,8 @@ function getPlatformColor(platform) { subscribestar: 'amber', hentaifoundry: 'purple', discord: 'indigo', + pixiv: 'blue', + deviantart: 'green', } return colors[platform] || 'grey' } diff --git a/frontend/src/views/Subscriptions.vue b/frontend/src/views/Subscriptions.vue index bdf456b..f58e3bb 100644 --- a/frontend/src/views/Subscriptions.vue +++ b/frontend/src/views/Subscriptions.vue @@ -381,6 +381,8 @@ const platformOptions = [ { title: 'SubscribeStar', value: 'subscribestar' }, { title: 'Hentai Foundry', value: 'hentaifoundry' }, { title: 'Discord', value: 'discord' }, + { title: 'Pixiv', value: 'pixiv' }, + { title: 'DeviantArt', value: 'deviantart' }, ] const headers = [ @@ -423,6 +425,8 @@ function getPlatformIcon(platform) { subscribestar: 'mdi-star', hentaifoundry: 'mdi-palette', discord: 'mdi-discord', + pixiv: 'mdi-alpha-p-box', + deviantart: 'mdi-deviantart', } return icons[platform] || 'mdi-web' } @@ -433,6 +437,8 @@ function getPlatformColor(platform) { subscribestar: 'amber', hentaifoundry: 'purple', discord: 'indigo', + pixiv: 'blue', + deviantart: 'green', } return colors[platform] || 'grey' } diff --git a/summary.md b/summary.md index 4550c7a..4893105 100644 --- a/summary.md +++ b/summary.md @@ -1,10 +1,10 @@ # GallerySubscriber - Project Summary -**Updated:** 2026-01-30 (Error recognition improvements, analysis tooling) +**Updated:** 2026-02-02 (Added Pixiv and DeviantArt platform support) ## Overview -GallerySubscriber is a Docker-based web application that automates downloading content from subscription platforms (Patreon, SubscribeStar, Discord, Hentai Foundry). It combines a Vue 3 web dashboard, Firefox extension for credential export, and a Python backend with Celery for background task scheduling. +GallerySubscriber is a Docker-based web application that automates downloading content from subscription platforms (Patreon, SubscribeStar, Discord, Hentai Foundry, Pixiv, DeviantArt). It combines a Vue 3 web dashboard, Firefox extension for credential export, and a Python backend with Celery for background task scheduling. **Core Purpose:** Automatically download and organize content from multiple subscription platforms using gallery-dl as the underlying download engine. @@ -159,6 +159,8 @@ 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) | +| DeviantArt | Cookies (optional) | gallery, scraps, favorites | All deviations (complete) | **Note:** All platforms fetch complete history by default - no depth limits. Gallery-dl paginates through ALL available content.