Files
FabledCurator/backend/app/services/platforms/__init__.py
T
bvandeusenandClaude Opus 5 24b10d0ffa
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
CI / frontend-build (push) Successful in 25s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 34s
Build images / build-web (push) Successful in 1m5s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m54s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m19s
feat: retire pixiv entirely — delete its code, its ledgers, its credential (3977, 3978, 3979)
Milestone #406 phase 2, with issue #3980 folded in. Phase 1 (2026-09-13)
unregistered pixiv so nothing could reach it; the code has sat in the tree
uncalled since. DeviantArt is why the second half is not left for later —
#3069 retired it in code on 2026-08-27 and its stored session was still in
the database seven weeks on.

Step 5 — the code. Deletes pixiv_client, pixiv_downloader, pixiv_ingester,
platforms/pixiv and their three test modules and fixture, then edits out
every remaining reference: the dispatch entry, the campaign-id and verify
branches in download_backends, the display-name branch in extension_service,
and the comments that still described pixiv as live.

The consolidation check the step asked for comes back negative:
native_ingest_common has seven non-pixiv callers (patreon, subscribestar,
membership_reconcile, membership_roster, ingest_core), so nothing there
drops to a single user.

Step 6 — the data, alembic 0102. Drops pixiv_seen_media and
pixiv_failed_media, and deletes credential rows whose platform is not
registered. Written as "not registered" rather than "pixiv" at the step's
explicit ask, which is what makes one migration cover two retirements: the
pixiv OAuth refresh token and DeviantArt's leftover session (#3980). It is
also the only way either row can go — the credentials UI renders one card
per platform from /api/platforms and looks the credential up by key, so an
unregistered platform's row has no card and no Remove button.

Pixiv's Source rows are KEPT, changing the milestone's original data table
on the operator's call. `platform` is stored only on Source; neither Post
nor ImageRecord carries it. Both FKs are ON DELETE SET NULL, so a delete
would not lose the art — but it would drop every pixiv image into the
gallery's __unsourced__ bucket and strip the platform chip off every pixiv
post. The rows stay disabled (0097) and unregistered, so nothing schedules
or downloads through them. Keeping them costs nothing and keeps the
attribution that "the art already downloaded from pixiv stays" is about.

Step 7 — the guard. test_pixiv_code_and_tables_are_gone asserts absence from
the module table and from Base.metadata, not from prose (snippet #3352's
trap). The extension and registry negative assertions were already in place
from phase 1. The final sweep found one real residue step 4 missed:
extension/README.md still advertised pixiv support and carried a "Pixiv
OAuth" manual-test item.

Also replaces the two deleted dispatch tests with one over the whole
NATIVE_INGESTER_PLATFORMS set, so adding a platform and forgetting its
ingester class now fails at unit level rather than as a mid-download
KeyError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
2026-09-21 20:46:15 -04:00

96 lines
3.1 KiB
Python

"""FC-3b platforms registry — single source of truth for what
FabledCurator supports + where each platform's quirks live.
Adding a new platform: drop a new module `<platform>.py` next to this
one, declare an `INFO = PlatformInfo(...)`, add the import + entry in
PLATFORMS below. Sidecar parsing, cookie materialization, and
`/api/platforms` pick it up automatically.
Lifted from GallerySubscriber's
~/Nextcloud/Projects/GallerySubscriber/backend/app/api/platforms.py
and ~/.../extension/lib/platforms.js. Five platforms; auth_type and
URL patterns match GS exactly so the existing browser extension
hits FC unmodified. deviantart was dropped at #3069 (2026-08-27) —
FC downloaders are art-dedicated services only. pixiv was retired at
milestone #406 (2026-09-13, rule #171): unregistered here first, which
switched it off everywhere this registry is consulted, then removed from
the tree entirely in the milestone's phase 2 (2026-09-21).
"""
from .base import (
DEFAULT_DESCRIPTION_KEYS,
DEFAULT_EXTERNAL_POST_ID_KEYS,
PlatformInfo,
)
from .discord import INFO as _DISCORD
from .hentaifoundry import INFO as _HENTAIFOUNDRY
from .patreon import INFO as _PATREON
from .subscribestar import INFO as _SUBSCRIBESTAR
PLATFORMS: dict[str, PlatformInfo] = {
info.key: info
for info in (
_PATREON,
_SUBSCRIBESTAR,
_HENTAIFOUNDRY,
_DISCORD,
)
}
def known_platform_keys() -> frozenset[str]:
return frozenset(PLATFORMS.keys())
def auth_type_for(platform: str) -> str | None:
info = PLATFORMS.get(platform)
return info.auth_type if info else None
def to_dict(info: PlatformInfo) -> dict:
"""Serialize a PlatformInfo to a JSON-safe dict for /api/platforms.
Behavioral fields (callables, sidecar-chain overrides) are
intentionally omitted — they aren't useful to API consumers.
"""
return {
"key": info.key,
"name": info.name,
"description": info.description,
"auth_type": info.auth_type,
"requires_auth": info.requires_auth,
"url_pattern": info.url_pattern,
"url_examples": info.url_examples,
"default_config": info.default_config,
"notes": info.notes,
}
def external_post_id_keys_for(platform: str | None) -> tuple[str, ...]:
"""Resolve the external_post_id lookup chain for a given platform,
falling back to the module default when the platform isn't
registered or hasn't overridden the chain."""
info = PLATFORMS.get(platform) if platform else None
if info is not None and info.external_post_id_keys is not None:
return info.external_post_id_keys
return DEFAULT_EXTERNAL_POST_ID_KEYS
def description_keys_for(platform: str | None) -> tuple[str, ...]:
"""Resolve the description body lookup chain for a given platform."""
info = PLATFORMS.get(platform) if platform else None
if info is not None and info.description_keys is not None:
return info.description_keys
return DEFAULT_DESCRIPTION_KEYS
__all__ = [
"PLATFORMS",
"PlatformInfo",
"auth_type_for",
"description_keys_for",
"external_post_id_keys_for",
"known_platform_keys",
"to_dict",
]