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

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:
2026-08-27 07:24:08 -04:00
co-authored by Claude Opus 5
parent 2e0f8f8c61
commit ddf896078c
23 changed files with 202 additions and 94 deletions
@@ -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},
)
+1 -1
View File
@@ -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)
+2 -3
View File
@@ -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,
+3 -11
View File
@@ -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__(
+3 -4
View File
@@ -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,
)
}
+1 -1
View File
@@ -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
View File
@@ -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)
-11
View File
@@ -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 -3
View File
@@ -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 -1
View File
@@ -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.",
+71 -8
View File
@@ -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>
+5 -6
View File
@@ -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) {
+17 -1
View File
@@ -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(
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -2
View File
@@ -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"
+2 -2
View File
@@ -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(
+4 -2
View File
@@ -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