feat(fc3b): platforms.py — GS-6 registry (single source of truth)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
|||||||
|
"""FC-3b platforms registry — the single source of truth for what
|
||||||
|
FabledCurator supports.
|
||||||
|
|
||||||
|
Lifted from GallerySubscriber's
|
||||||
|
~/Nextcloud/Projects/GallerySubscriber/backend/app/api/platforms.py
|
||||||
|
and ~/.../extension/lib/platforms.js. Six platforms; auth_type and
|
||||||
|
URL patterns match GS exactly so the existing browser extension
|
||||||
|
hits FC unmodified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlatformInfo:
|
||||||
|
key: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
auth_type: Literal["cookies", "token"]
|
||||||
|
requires_auth: bool
|
||||||
|
url_pattern: str
|
||||||
|
url_examples: list[str]
|
||||||
|
default_config: dict
|
||||||
|
notes: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# Common defaults used across most platforms; embedded per-platform
|
||||||
|
# below so per-platform overrides remain explicit.
|
||||||
|
_DEFAULTS = {
|
||||||
|
"sleep": 3.0,
|
||||||
|
"sleep_request": 1.5,
|
||||||
|
"skip_existing": True,
|
||||||
|
"save_metadata": True,
|
||||||
|
"timeout": 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PLATFORMS: dict[str, PlatformInfo] = {
|
||||||
|
"patreon": PlatformInfo(
|
||||||
|
key="patreon",
|
||||||
|
name="Patreon",
|
||||||
|
description="Download posts from Patreon creators",
|
||||||
|
auth_type="cookies",
|
||||||
|
requires_auth=True,
|
||||||
|
url_pattern=r"^https?://(www\.)?patreon\.com/",
|
||||||
|
url_examples=[
|
||||||
|
"https://www.patreon.com/example_artist",
|
||||||
|
"https://www.patreon.com/user?u=12345678",
|
||||||
|
],
|
||||||
|
default_config={**_DEFAULTS, "content_types": ["images", "attachments"]},
|
||||||
|
),
|
||||||
|
"subscribestar": PlatformInfo(
|
||||||
|
key="subscribestar",
|
||||||
|
name="SubscribeStar",
|
||||||
|
description="Download posts from SubscribeStar creators",
|
||||||
|
auth_type="cookies",
|
||||||
|
requires_auth=True,
|
||||||
|
url_pattern=r"^https?://(www\.)?subscribestar\.(com|adult)/",
|
||||||
|
url_examples=[
|
||||||
|
"https://subscribestar.adult/example_artist",
|
||||||
|
"https://www.subscribestar.com/example_artist",
|
||||||
|
],
|
||||||
|
default_config={**_DEFAULTS, "content_types": ["all"]},
|
||||||
|
),
|
||||||
|
"hentaifoundry": PlatformInfo(
|
||||||
|
key="hentaifoundry",
|
||||||
|
name="Hentai Foundry",
|
||||||
|
description="Download artwork from Hentai Foundry artists",
|
||||||
|
auth_type="cookies",
|
||||||
|
requires_auth=False,
|
||||||
|
url_pattern=r"^https?://(www\.)?hentai-foundry\.com/",
|
||||||
|
url_examples=[
|
||||||
|
"https://www.hentai-foundry.com/user/example_artist",
|
||||||
|
"https://www.hentai-foundry.com/pictures/user/example_artist",
|
||||||
|
],
|
||||||
|
default_config={**_DEFAULTS, "content_types": ["pictures"]},
|
||||||
|
),
|
||||||
|
"discord": PlatformInfo(
|
||||||
|
key="discord",
|
||||||
|
name="Discord",
|
||||||
|
description="Download attachments from Discord channels",
|
||||||
|
auth_type="token",
|
||||||
|
requires_auth=True,
|
||||||
|
url_pattern=r"^https?://(www\.)?discord\.com/channels/",
|
||||||
|
url_examples=["https://discord.com/channels/123456789/987654321"],
|
||||||
|
default_config={**_DEFAULTS, "content_types": ["all"]},
|
||||||
|
notes="Requires Discord user token (not bot token).",
|
||||||
|
),
|
||||||
|
"pixiv": PlatformInfo(
|
||||||
|
key="pixiv",
|
||||||
|
name="Pixiv",
|
||||||
|
description="Download artwork from Pixiv artists",
|
||||||
|
auth_type="token",
|
||||||
|
requires_auth=True,
|
||||||
|
url_pattern=r"^https?://(www\.)?pixiv\.net/",
|
||||||
|
url_examples=[
|
||||||
|
"https://www.pixiv.net/users/12345678",
|
||||||
|
"https://www.pixiv.net/en/users/12345678",
|
||||||
|
],
|
||||||
|
default_config={**_DEFAULTS, "content_types": ["all"]},
|
||||||
|
notes="Requires OAuth refresh token. Run `gallery-dl oauth:pixiv` to obtain one.",
|
||||||
|
),
|
||||||
|
"deviantart": 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={**_DEFAULTS, "content_types": ["gallery"]},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backend.app.services.platforms import (
|
||||||
|
PLATFORMS,
|
||||||
|
PlatformInfo,
|
||||||
|
auth_type_for,
|
||||||
|
known_platform_keys,
|
||||||
|
to_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_platform_keys_is_gs_six():
|
||||||
|
assert known_platform_keys() == frozenset({
|
||||||
|
"patreon", "subscribestar", "hentaifoundry",
|
||||||
|
"discord", "pixiv", "deviantart",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_fanbox_not_in_registry():
|
||||||
|
# Sanity check — FC-3a added 'fanbox' by mistake; it's not a GS platform.
|
||||||
|
assert "fanbox" not in PLATFORMS
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_type_for_known_and_unknown():
|
||||||
|
assert auth_type_for("patreon") == "cookies"
|
||||||
|
assert auth_type_for("discord") == "token"
|
||||||
|
assert auth_type_for("nope") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_each_platform_has_required_fields():
|
||||||
|
for key, info in PLATFORMS.items():
|
||||||
|
assert isinstance(info, PlatformInfo)
|
||||||
|
assert info.key == key
|
||||||
|
assert info.name
|
||||||
|
assert info.description
|
||||||
|
assert info.auth_type in ("cookies", "token")
|
||||||
|
assert isinstance(info.requires_auth, bool)
|
||||||
|
re.compile(info.url_pattern) # valid regex
|
||||||
|
assert info.url_examples
|
||||||
|
assert isinstance(info.default_config, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_dict_round_trip():
|
||||||
|
d = to_dict(PLATFORMS["patreon"])
|
||||||
|
assert d["key"] == "patreon"
|
||||||
|
assert d["auth_type"] == "cookies"
|
||||||
|
assert d["default_config"]["sleep"] == 3.0
|
||||||
|
assert "url_examples" in d
|
||||||
Reference in New Issue
Block a user