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.