diff --git a/backend/app/services/platforms.py b/backend/app/services/platforms.py new file mode 100644 index 0000000..bd3723a --- /dev/null +++ b/backend/app/services/platforms.py @@ -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, + } diff --git a/tests/test_platforms_registry.py b/tests/test_platforms_registry.py new file mode 100644 index 0000000..c100792 --- /dev/null +++ b/tests/test_platforms_registry.py @@ -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