refactor(platforms): retire deviantart end-to-end (#3069)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
Executes the 2026-07-05 product decision (FC downloaders = art-dedicated
services only), which removed Twitter/X and Bluesky but left deviantart
fully wired for seven weeks — the half-retired state rule 22 exists to
prevent.
Removed: the PlatformInfo module and its registry entry, the gallery-dl
extractor block, extension_service's artist-page pattern, the extension's
PLATFORMS + PLATFORM_ARTIST_PATTERNS entries, its manifest host permission
and content-script match, the frontend icon/colour/label, and the operator-
facing "supported platforms" list that still advertised it.
Two judgment calls, both recorded in migration 0088:
* existing `source` rows are DISABLED, not deleted. The row is the only
record of the artist's DeviantArt URL. Disabling is also required for
correctness rather than tidiness: with the platform unregistered the
download path falls through to gallery-dl, which carries its OWN
deviantart extractor, so an enabled row would have kept downloading
from a dropped platform.
* the `credential` row IS deleted — a live session cookie for a site FC
will never call again.
Adds the invariant whose absence is why manifest.json drifted in the first
place: nothing tied its domain lists back to the platform table. The
extension suite now asserts both directions, plus that no host permission
belongs to an unclaimed domain (`*://*/*` exempted — FC is self-hosted at
an operator-chosen URL the extension cannot enumerate).
Extension version 1.0.10 -> 1.0.11: ci.yml's guard hard-fails a packaged
extension change without a bump. No release is cut — build.yml's
sign-extension job only runs on main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""retire deviantart (#3069) — quiesce the rows the dropped platform leaves behind
|
||||
|
||||
`deviantart` is no longer a registered platform, so nothing can create or edit a
|
||||
source with that key any more. Existing rows are a different question, and the
|
||||
two tables want opposite treatment:
|
||||
|
||||
* `source` rows are DISABLED, not deleted. The row is the only place the
|
||||
artist's DeviantArt URL is recorded, and losing it is unrecoverable — whereas
|
||||
a disabled row is visible in the UI and reversible by hand. Disabling is also
|
||||
required for correctness, not just tidiness: with the platform unregistered
|
||||
the download path falls through to gallery-dl, which carries its OWN built-in
|
||||
deviantart extractor, so an enabled row would have gone on downloading from a
|
||||
platform the product dropped.
|
||||
|
||||
* the `credential` row IS deleted. It is an encrypted DeviantArt session cookie
|
||||
for a site FC will never call again — keeping a live credential we have no
|
||||
use for is strictly worse than dropping it, and re-exporting from the browser
|
||||
is the recovery path if that judgment is ever wrong.
|
||||
|
||||
A no-op on an instance that never had a DeviantArt source, which is the
|
||||
expected case.
|
||||
|
||||
Revision ID: 0088
|
||||
Revises: 0087
|
||||
Create Date: 2026-08-27
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0088"
|
||||
down_revision: Union[str, None] = "0087"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_RETIRED = "deviantart"
|
||||
|
||||
# Written into last_error so the disabled row explains itself in the UI rather
|
||||
# than looking like an unexplained toggle someone flipped.
|
||||
_REASON = (
|
||||
"Platform retired: FabledCurator no longer supports DeviantArt "
|
||||
"(dropped 2026-08-27, #3069). This source was disabled automatically; "
|
||||
"the URL is kept for reference and the row can be deleted by hand."
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
disabled = conn.execute(
|
||||
sa.text(
|
||||
"UPDATE source SET enabled = false, last_error = :reason "
|
||||
"WHERE platform = :p AND enabled = true"
|
||||
),
|
||||
{"reason": _REASON, "p": _RETIRED},
|
||||
).rowcount
|
||||
creds = conn.execute(
|
||||
sa.text("DELETE FROM credential WHERE platform = :p"), {"p": _RETIRED}
|
||||
).rowcount
|
||||
print(f"0088: disabled {disabled} deviantart source(s), removed {creds} credential(s)")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Re-enabling is deliberately NOT done: the platform is gone from the
|
||||
# registry, so a re-enabled source would still have no backend to run on.
|
||||
# Clearing the stamped reason is the only half that means anything.
|
||||
op.get_bind().execute(
|
||||
sa.text("UPDATE source SET last_error = NULL WHERE platform = :p AND last_error = :reason"),
|
||||
{"p": _RETIRED, "reason": _REASON},
|
||||
)
|
||||
@@ -181,7 +181,7 @@ def _augment_cookies(platform: str, netscape: str) -> str:
|
||||
"""Delegate to the platform's `augment_cookies` hook if one is
|
||||
registered (subscribestar, hentaifoundry, etc. — see
|
||||
`services/platforms/<name>.py`). No-op when the platform doesn't
|
||||
register a hook (Patreon, DeviantArt). Centralizing the
|
||||
register a hook (Patreon, Discord). Centralizing the
|
||||
quirks-per-platform in the platforms package means adding a new
|
||||
platform's cookie quirks doesn't require touching this file."""
|
||||
info = PLATFORMS.get(platform)
|
||||
|
||||
@@ -31,9 +31,8 @@ from .pixiv_ingester import PixivIngester
|
||||
from .subscribestar_ingester import SubscribeStarIngester
|
||||
|
||||
# Platforms whose download + verify go through the native ingester rather than
|
||||
# gallery-dl. gallery-dl still serves the rest (hentaifoundry, discord,
|
||||
# deviantart — the latter slated for retirement, not migration) until they
|
||||
# migrate too.
|
||||
# gallery-dl. gallery-dl still serves the rest (hentaifoundry, discord) until
|
||||
# they migrate too.
|
||||
NATIVE_INGESTER_PLATFORMS = frozenset({"patreon", "subscribestar", "pixiv"})
|
||||
|
||||
# Mirrors patreon_resolver._CAMPAIGNS_URL — surfaced in resolution-failure
|
||||
|
||||
@@ -55,12 +55,6 @@ _PLATFORM_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
||||
r"^https?://(?:www\.)?hentai-foundry\.com/user/(?P<slug>[^/?#]+)",
|
||||
re.IGNORECASE,
|
||||
)),
|
||||
("deviantart", re.compile(
|
||||
r"^https?://(?:www\.)?deviantart\.com/"
|
||||
r"(?!home$|watch\b|tag\b|browse\b)"
|
||||
r"(?P<slug>[^/?#]+)/?$",
|
||||
re.IGNORECASE,
|
||||
)),
|
||||
("pixiv", re.compile(
|
||||
r"^https?://(?:www\.)?pixiv\.net/(?:en/)?users/(?P<slug>\d+)",
|
||||
re.IGNORECASE,
|
||||
|
||||
@@ -299,8 +299,9 @@ class GalleryDLService:
|
||||
# (services/patreon_ingester.py), not gallery-dl.
|
||||
PLATFORM_DEFAULTS = {
|
||||
# subscribestar removed — native-ingester platform now (#71); pixiv
|
||||
# removed likewise (#129). The remaining entries are the gallery-dl
|
||||
# platforms not yet migrated.
|
||||
# removed likewise (#129); deviantart removed at #3069 as a dropped
|
||||
# platform, not a migrated one. The remaining entries are the
|
||||
# gallery-dl platforms not yet migrated.
|
||||
"hentaifoundry": {
|
||||
"content_types": ["all"],
|
||||
"directory": [],
|
||||
@@ -316,15 +317,6 @@ class GalleryDLService:
|
||||
"reactions": False,
|
||||
"threads": True,
|
||||
},
|
||||
"deviantart": {
|
||||
"content_types": ["all"],
|
||||
"directory": [],
|
||||
"filename": "{index:>03}_{title[:50]}.{extension}",
|
||||
"flat": True,
|
||||
"original": True,
|
||||
"mature": True,
|
||||
"metadata": True,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -8,9 +8,10 @@ PLATFORMS below. Sidecar parsing, cookie materialization, and
|
||||
|
||||
Lifted from GallerySubscriber's
|
||||
~/Nextcloud/Projects/GallerySubscriber/backend/app/api/platforms.py
|
||||
and ~/.../extension/lib/platforms.js. Six platforms; auth_type and
|
||||
and ~/.../extension/lib/platforms.js. Five platforms; auth_type and
|
||||
URL patterns match GS exactly so the existing browser extension
|
||||
hits FC unmodified.
|
||||
hits FC unmodified. deviantart was dropped at #3069 (2026-08-27) —
|
||||
FC downloaders are art-dedicated services only.
|
||||
"""
|
||||
|
||||
from .base import (
|
||||
@@ -18,7 +19,6 @@ from .base import (
|
||||
DEFAULT_EXTERNAL_POST_ID_KEYS,
|
||||
PlatformInfo,
|
||||
)
|
||||
from .deviantart import INFO as _DEVIANTART
|
||||
from .discord import INFO as _DISCORD
|
||||
from .hentaifoundry import INFO as _HENTAIFOUNDRY
|
||||
from .patreon import INFO as _PATREON
|
||||
@@ -33,7 +33,6 @@ PLATFORMS: dict[str, PlatformInfo] = {
|
||||
_HENTAIFOUNDRY,
|
||||
_DISCORD,
|
||||
_PIXIV,
|
||||
_DEVIANTART,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class PlatformInfo:
|
||||
# Synthesize a post permalink from sidecar data. Required when
|
||||
# gallery-dl's `url` field is the file/CDN URL rather than the post
|
||||
# permalink (subscribestar/pixiv/hf/discord). None = trust the bare
|
||||
# `url` field (patreon, deviantart).
|
||||
# `url` field (patreon).
|
||||
derive_post_url: Callable[[dict], str | None] | None = None
|
||||
|
||||
# Post-process the materialized cookies.txt for gallery-dl. Used by
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
"""DeviantArt — no exercised quirks yet.
|
||||
|
||||
No operator-owned DeviantArt archive existed at the 2026-05-27 sidecar
|
||||
audit, so we don't know yet whether DA's gallery-dl sidecars are
|
||||
well-behaved or have their own quirks. When DA gets exercised for the
|
||||
first time, add `derive_post_url` / `augment_cookies` here as needed.
|
||||
"""
|
||||
|
||||
from .base import GD_DEFAULTS, PlatformInfo
|
||||
|
||||
INFO = PlatformInfo(
|
||||
key="deviantart",
|
||||
name="DeviantArt",
|
||||
description="Download artwork from DeviantArt artists",
|
||||
auth_type="cookies",
|
||||
requires_auth=False,
|
||||
url_pattern=r"^https?://(www\.)?deviantart\.com/",
|
||||
url_examples=[
|
||||
"https://www.deviantart.com/example-artist",
|
||||
"https://www.deviantart.com/example-artist/gallery",
|
||||
],
|
||||
default_config={**GD_DEFAULTS, "content_types": ["gallery"]},
|
||||
)
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
# FabledCurator Firefox Extension
|
||||
|
||||
Self-hosted Firefox extension that pushes session cookies from supported
|
||||
platforms (Patreon, SubscribeStar, Hentai-Foundry, Discord, Pixiv,
|
||||
DeviantArt) into FabledCurator, and lets you add a creator as a Source
|
||||
from their page in one click.
|
||||
platforms (Patreon, SubscribeStar, Hentai-Foundry, Discord, Pixiv)
|
||||
into FabledCurator, and lets you add a creator as a Source from their
|
||||
page in one click.
|
||||
|
||||
## Install (operator)
|
||||
|
||||
|
||||
@@ -68,16 +68,6 @@ const PLATFORMS = {
|
||||
urlPattern: /^https?:\/\/(www\.)?pixiv\.net/,
|
||||
note: 'Click to authenticate via OAuth',
|
||||
},
|
||||
deviantart: {
|
||||
name: 'DeviantArt',
|
||||
domains: ['.deviantart.com', 'www.deviantart.com', 'deviantart.com'],
|
||||
authType: 'cookies',
|
||||
color: '#05CC47',
|
||||
urlPattern: /^https?:\/\/(www\.)?deviantart\.com/,
|
||||
// DA's logged-in-only endpoints sit behind their internal _napi
|
||||
// namespace which shifts; skipping verify until a stable check
|
||||
// surfaces. Same posture as SubscribeStar.
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -98,7 +88,6 @@ const PLATFORM_ARTIST_PATTERNS = {
|
||||
patreon: /^https?:\/\/(www\.)?patreon\.com\/(?:cw\/|c\/)?(?!(?:home|search|messages|notifications|library|settings|posts)(?:[\/?#]|$))[^/?#]+/i,
|
||||
subscribestar: /^https?:\/\/(www\.)?subscribestar\.(com|adult)\/(?!feed$|messages$|library$)[^/?#]+\/?$/i,
|
||||
hentaifoundry: /^https?:\/\/(www\.)?hentai-foundry\.com\/user\/[^/?#]+/i,
|
||||
deviantart: /^https?:\/\/(www\.)?deviantart\.com\/(?!home$|watch\b|tag\b|browse\b)[^/?#]+\/?$/i,
|
||||
pixiv: /^https?:\/\/(www\.)?pixiv\.net\/(en\/)?users\/\d+/i,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "FabledCurator",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
|
||||
|
||||
"browser_specific_settings": {
|
||||
@@ -33,7 +33,6 @@
|
||||
"*://*.hentai-foundry.com/*",
|
||||
"*://*.discord.com/*",
|
||||
"*://*.pixiv.net/*",
|
||||
"*://*.deviantart.com/*",
|
||||
"*://app-api.pixiv.net/*",
|
||||
"*://oauth.secure.pixiv.net/*",
|
||||
"*://*/*"
|
||||
@@ -61,7 +60,6 @@
|
||||
"*://*.subscribestar.com/*",
|
||||
"*://*.subscribestar.adult/*",
|
||||
"*://*.hentai-foundry.com/*",
|
||||
"*://*.deviantart.com/*",
|
||||
"*://*.pixiv.net/*"
|
||||
],
|
||||
"js": ["lib/platforms.js", "content/content-script.js"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fabledcurator-extension",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"private": true,
|
||||
"description": "Firefox extension for FabledCurator",
|
||||
"comment_ignore_files": "The --ignore-files list comes from scripts/packaging.sh, the single source of truth shared with ci.yml's guard and the derived-version patch count. `set -f` is REQUIRED before the substitution: without it the shell globs `test/**` against the working tree and silently narrows the pattern to whatever files happen to exist.",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import { loadLib } from './helpers/loadLib.js'
|
||||
|
||||
const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const manifest = JSON.parse(readFileSync(path.join(EXT_DIR, 'manifest.json'), 'utf8'))
|
||||
|
||||
const { getPlatformFromUrl, isArtistPage, PLATFORMS, PLATFORM_ARTIST_PATTERNS } = loadLib(
|
||||
'platforms.js',
|
||||
['getPlatformFromUrl', 'isArtistPage', 'PLATFORMS', 'PLATFORM_ARTIST_PATTERNS']
|
||||
@@ -13,7 +19,6 @@ describe('getPlatformFromUrl', () => {
|
||||
expect(getPlatformFromUrl('https://www.hentai-foundry.com/user/someone')).toBe('hentaifoundry')
|
||||
expect(getPlatformFromUrl('https://discord.com/channels/@me')).toBe('discord')
|
||||
expect(getPlatformFromUrl('https://www.pixiv.net/en/users/123')).toBe('pixiv')
|
||||
expect(getPlatformFromUrl('https://www.deviantart.com/someone')).toBe('deviantart')
|
||||
})
|
||||
|
||||
it('accepts http as well as https, with or without www', () => {
|
||||
@@ -26,6 +31,15 @@ describe('getPlatformFromUrl', () => {
|
||||
expect(getPlatformFromUrl('https://not-patreon.com/Atole')).toBe(null)
|
||||
expect(getPlatformFromUrl('')).toBe(null)
|
||||
})
|
||||
|
||||
it('returns null for deviantart, retired at #3069', () => {
|
||||
// The 2026-07-05 product decision (FC downloaders = art-dedicated services
|
||||
// only) left deviantart wired for seven weeks. Asserting the negative is
|
||||
// what keeps a partial retirement from being re-completed by accident.
|
||||
expect(getPlatformFromUrl('https://www.deviantart.com/someone')).toBe(null)
|
||||
expect(PLATFORMS.deviantart).toBeUndefined()
|
||||
expect(PLATFORM_ARTIST_PATTERNS.deviantart).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isArtistPage', () => {
|
||||
@@ -72,12 +86,6 @@ describe('isArtistPage', () => {
|
||||
expect(isArtistPage('https://www.pixiv.net/en/artworks/999', 'pixiv')).toBe(false)
|
||||
})
|
||||
|
||||
it('excludes DeviantArt navigation roots', () => {
|
||||
expect(isArtistPage('https://www.deviantart.com/someone', 'deviantart')).toBe(true)
|
||||
expect(isArtistPage('https://www.deviantart.com/home', 'deviantart')).toBe(false)
|
||||
expect(isArtistPage('https://www.deviantart.com/watch', 'deviantart')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for a platform with no artist pattern (discord)', () => {
|
||||
expect(isArtistPage('https://discord.com/channels/@me', 'discord')).toBe(false)
|
||||
})
|
||||
@@ -116,7 +124,6 @@ describe('platform table integrity', () => {
|
||||
patreon: 'https://www.patreon.com/cw/Atole',
|
||||
subscribestar: 'https://subscribestar.adult/someone',
|
||||
hentaifoundry: 'https://www.hentai-foundry.com/user/someone',
|
||||
deviantart: 'https://www.deviantart.com/someone',
|
||||
pixiv: 'https://www.pixiv.net/en/users/12345'
|
||||
}
|
||||
for (const [key, url] of Object.entries(samples)) {
|
||||
@@ -125,3 +132,59 @@ describe('platform table integrity', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('manifest.json agrees with the platform table', () => {
|
||||
// #3069: deviantart was dropped from the product in July but survived in
|
||||
// manifest.json until late August, because NOTHING tied the manifest's
|
||||
// domain lists back to PLATFORMS. These two specs are that tie. Both
|
||||
// directions matter: a stale match ships host access the product decided
|
||||
// not to use, and a missing one silently kills the Add-to-FC button.
|
||||
const matches = manifest.content_scripts[0].matches
|
||||
// '*://*.patreon.com/*' -> '.patreon.com', the form PLATFORMS.domains uses.
|
||||
const hostOf = (m) => m.replace(/^\*:\/\/\*/, '').replace(/\/\*$/, '')
|
||||
|
||||
it('injects the content script only on domains a platform claims', () => {
|
||||
for (const m of matches) {
|
||||
const host = hostOf(m)
|
||||
const owner = Object.entries(PLATFORMS).find(
|
||||
([, p]) => p.domains.includes(host)
|
||||
)
|
||||
expect(owner, `no platform claims content-script match "${m}"`).toBeTruthy()
|
||||
// The content script exists to draw the Add-as-source button, so a
|
||||
// platform with no artist pattern (discord) has no business here.
|
||||
expect(
|
||||
PLATFORM_ARTIST_PATTERNS[owner[0]],
|
||||
`"${m}" injects for ${owner[0]}, which has no artist pattern`
|
||||
).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('injects on every platform that has an artist pattern', () => {
|
||||
const covered = new Set(
|
||||
matches
|
||||
.map(hostOf)
|
||||
.map((h) => Object.entries(PLATFORMS).find(([, p]) => p.domains.includes(h)))
|
||||
.filter(Boolean)
|
||||
.map(([key]) => key)
|
||||
)
|
||||
for (const key of Object.keys(PLATFORM_ARTIST_PATTERNS)) {
|
||||
expect(covered, `${key} has an artist pattern but no content-script match`).toContain(key)
|
||||
}
|
||||
})
|
||||
|
||||
it('requests no host permission for a domain no platform claims', () => {
|
||||
// '*://*/*' is the deliberate exception: FC is self-hosted at an arbitrary
|
||||
// operator-chosen URL, so the extension cannot enumerate its own backend.
|
||||
// Every OTHER entry is a platform domain and must still have an owner.
|
||||
for (const h of manifest.host_permissions) {
|
||||
if (h === '*://*/*') continue
|
||||
const host = hostOf(h)
|
||||
// pixiv's OAuth/API hosts are pixiv infrastructure, not creator pages,
|
||||
// so they are matched by suffix rather than by the domains list.
|
||||
const claimed = Object.values(PLATFORMS).some(
|
||||
(p) => p.domains.includes(host) || p.domains.some((d) => host.endsWith(d))
|
||||
)
|
||||
expect(claimed, `host permission "${h}" belongs to no platform`).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<v-card-text>
|
||||
<p class="fc-muted text-body-2">
|
||||
Pushes session cookies from supported platforms
|
||||
(patreon, subscribestar, hentaifoundry, discord, pixiv, deviantart)
|
||||
(patreon, subscribestar, hentaifoundry, discord, pixiv)
|
||||
into FabledCurator, and lets you add a creator as a source from
|
||||
their page in one click.
|
||||
</p>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Single source of truth for platform → color + icon mapping. Used by
|
||||
// PlatformChip and any other GS-style platform-tagged surface. The six
|
||||
// PlatformChip and any other GS-style platform-tagged surface. The five
|
||||
// platforms FC supports map 1:1 to the GS palette; unknown platforms fall
|
||||
// back to grey + mdi-web. Operator-confirmed scope 2026-05-27. The ICONS key
|
||||
// set is pinned against backend known_platform_keys() by
|
||||
// back to grey + mdi-web — which is deliberately what a retired platform
|
||||
// hits: a pre-#3069 deviantart source row still renders, as its raw key on
|
||||
// a grey chip. Operator-confirmed scope 2026-05-27. The ICONS key set is
|
||||
// pinned against backend known_platform_keys() by
|
||||
// tests/test_fe_be_contract.py.
|
||||
|
||||
const ICONS = {
|
||||
@@ -11,7 +13,6 @@ const ICONS = {
|
||||
hentaifoundry: 'mdi-palette',
|
||||
discord: 'mdi-discord',
|
||||
pixiv: 'mdi-alpha-p-box',
|
||||
deviantart: 'mdi-deviantart',
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
@@ -20,7 +21,6 @@ const COLORS = {
|
||||
hentaifoundry: 'purple',
|
||||
discord: 'indigo',
|
||||
pixiv: 'blue',
|
||||
deviantart: 'green',
|
||||
}
|
||||
|
||||
const LABELS = {
|
||||
@@ -29,7 +29,6 @@ const LABELS = {
|
||||
hentaifoundry: 'HentaiFoundry',
|
||||
discord: 'Discord',
|
||||
pixiv: 'Pixiv',
|
||||
deviantart: 'DeviantArt',
|
||||
}
|
||||
|
||||
export function platformIcon(platform) {
|
||||
|
||||
@@ -129,7 +129,6 @@ async def test_resolve_artist_name_dispatches_per_platform(db, monkeypatch):
|
||||
("https://www.subscribestar.com/foobar", "subscribestar", "foobar"),
|
||||
("https://subscribestar.adult/foobar", "subscribestar", "foobar"),
|
||||
("https://www.hentai-foundry.com/user/Foo/profile", "hentaifoundry", "Foo"),
|
||||
("https://www.deviantart.com/baz", "deviantart", "baz"),
|
||||
("https://www.pixiv.net/users/12345", "pixiv", "12345"),
|
||||
("https://www.pixiv.net/en/users/12345", "pixiv", "12345"),
|
||||
])
|
||||
@@ -160,6 +159,23 @@ async def test_quick_add_source_unknown_url_400(client, ext_key):
|
||||
assert "known" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_source_rejects_retired_deviantart(client, ext_key):
|
||||
"""#3069: a DeviantArt creator URL used to derive cleanly. Now that the
|
||||
platform is retired, the extension's own gate should never offer the
|
||||
button — but a stale content script on an un-updated browser still can,
|
||||
so the backend has to refuse it rather than create an unusable source."""
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": "https://www.deviantart.com/baz"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
body = await resp.get_json()
|
||||
assert body["error"] == "unknown_platform"
|
||||
assert "deviantart" not in body["known"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_source_invalid_url_400(client, ext_key):
|
||||
resp = await client.post(
|
||||
|
||||
@@ -6,16 +6,17 @@ pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_platforms_returns_gs_six(client):
|
||||
async def test_platforms_returns_gs_five(client):
|
||||
resp = await client.get("/api/platforms")
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
platforms = body["platforms"]
|
||||
assert set(platforms.keys()) == {
|
||||
"patreon", "subscribestar", "hentaifoundry",
|
||||
"discord", "pixiv", "deviantart",
|
||||
"discord", "pixiv",
|
||||
}
|
||||
assert "fanbox" not in platforms
|
||||
assert "deviantart" not in platforms # retired at #3069
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -154,7 +154,7 @@ async def test_list_platform_filter_excludes_no_source(db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_platform_filter_excludes_wrong_platform(db):
|
||||
a = await _seed_artist(db, "alice-wplat")
|
||||
await _seed_source(db, a.id, "deviantart", "https://d/alice-wp")
|
||||
await _seed_source(db, a.id, "discord", "https://d/alice-wp")
|
||||
await db.commit()
|
||||
|
||||
page = await ArtistDirectoryService(db).list_artists(
|
||||
|
||||
@@ -19,7 +19,7 @@ def test_native_platforms():
|
||||
def test_gallery_dl_platforms_are_not_native():
|
||||
# The platforms still served by gallery-dl must NOT route to the native
|
||||
# ingester — guards an accidental over-broad migration.
|
||||
for platform in ("hentaifoundry", "discord", "deviantart"):
|
||||
for platform in ("hentaifoundry", "discord"):
|
||||
assert uses_native_ingester(platform) is False
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ pytestmark = pytest.mark.integration
|
||||
|
||||
def test_non_serialized_platform_has_no_lock():
|
||||
# gallery-dl platforms aren't capped — they get no lock at all.
|
||||
assert platform_lock("deviantart", ttl_seconds=60) is None
|
||||
assert platform_lock("hentaifoundry", ttl_seconds=60) is None
|
||||
assert platform_lock("discord", ttl_seconds=60) is None
|
||||
|
||||
|
||||
def test_subscribestar_is_serialized():
|
||||
|
||||
@@ -11,10 +11,10 @@ from backend.app.services.platforms import (
|
||||
)
|
||||
|
||||
|
||||
def test_known_platform_keys_is_gs_six():
|
||||
def test_known_platform_keys_is_gs_five():
|
||||
assert known_platform_keys() == frozenset({
|
||||
"patreon", "subscribestar", "hentaifoundry",
|
||||
"discord", "pixiv", "deviantart",
|
||||
"discord", "pixiv",
|
||||
})
|
||||
|
||||
|
||||
@@ -23,6 +23,15 @@ def test_fanbox_not_in_registry():
|
||||
assert "fanbox" not in PLATFORMS
|
||||
|
||||
|
||||
def test_deviantart_is_retired():
|
||||
# #3069 executed the 2026-07-05 drop decision (FC downloaders = ART-
|
||||
# DEDICATED services only). The registry is what /api/platforms, the
|
||||
# source validator and the credential validator all read, so its absence
|
||||
# here is what actually retires the platform everywhere else.
|
||||
assert "deviantart" not in PLATFORMS
|
||||
assert auth_type_for("deviantart") is None
|
||||
|
||||
|
||||
def test_auth_type_for_known_and_unknown():
|
||||
assert auth_type_for("patreon") == "cookies"
|
||||
assert auth_type_for("discord") == "token"
|
||||
|
||||
@@ -169,7 +169,7 @@ async def test_scroll_filters_by_artist(db):
|
||||
async def test_scroll_filters_by_platform(db):
|
||||
artist = await _seed_artist(db, "alice-platf")
|
||||
src_p = await _seed_source(db, artist.id, "patreon", "https://p/alice-pp")
|
||||
src_d = await _seed_source(db, artist.id, "deviantart", "https://d/alice-dd")
|
||||
src_d = await _seed_source(db, artist.id, "discord", "https://d/alice-dd")
|
||||
now = datetime.now(UTC)
|
||||
pp = await _seed_post(db, src_p.id, external_id="PP", post_date=now)
|
||||
await _seed_post(db, src_d.id, external_id="PD", post_date=now)
|
||||
@@ -234,7 +234,7 @@ async def test_scroll_combined_artist_and_platform(db):
|
||||
alice = await _seed_artist(db, "alice-combo")
|
||||
bob = await _seed_artist(db, "bob-combo")
|
||||
src_alice_patreon = await _seed_source(db, alice.id, "patreon", "https://p/alice-c")
|
||||
src_alice_da = await _seed_source(db, alice.id, "deviantart", "https://d/alice-c")
|
||||
src_alice_da = await _seed_source(db, alice.id, "discord", "https://d/alice-c")
|
||||
src_bob_patreon = await _seed_source(db, bob.id, "patreon", "https://p/bob-c")
|
||||
now = datetime.now(UTC)
|
||||
target = await _seed_post(
|
||||
|
||||
@@ -23,12 +23,14 @@ async def _artist(db, name="Alice"):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_known_platforms_is_gs_six(db):
|
||||
async def test_known_platforms_is_gs_five(db):
|
||||
assert KNOWN_PLATFORMS == frozenset({
|
||||
"patreon", "subscribestar", "hentaifoundry",
|
||||
"discord", "pixiv", "deviantart",
|
||||
"discord", "pixiv",
|
||||
})
|
||||
assert "fanbox" not in KNOWN_PLATFORMS
|
||||
# Retired at #3069 — a source can no longer be created on it.
|
||||
assert "deviantart" not in KNOWN_PLATFORMS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user