diff --git a/alembic/versions/0050_external_link_host_toggles.py b/alembic/versions/0050_external_link_host_toggles.py new file mode 100644 index 0000000..ac78e75 --- /dev/null +++ b/alembic/versions/0050_external_link_host_toggles.py @@ -0,0 +1,38 @@ +"""import_settings: per-host enable toggles for external file-host downloads + +Operator levers (#830): disable a single host (e.g. mega.nz when it's +rate-limiting/banning) without touching the others. The worker reads these via +getattr and defaults to enabled, so the toggles default TRUE (works out of the +box, rule #26). + +Revision ID: 0050 +Revises: 0049 +Create Date: 2026-06-14 +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0050" +down_revision: Union[str, None] = "0049" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +_HOSTS = ("mega", "gdrive", "mediafire", "dropbox", "pixeldrain") + + +def upgrade() -> None: + for host in _HOSTS: + op.add_column( + "import_settings", + sa.Column( + f"extdl_{host}_enabled", sa.Boolean(), nullable=False, + server_default=sa.true(), + ), + ) + + +def downgrade() -> None: + for host in _HOSTS: + op.drop_column("import_settings", f"extdl_{host}_enabled") diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 7d69fbf..a8af431 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -27,6 +27,20 @@ _EDITABLE_FIELDS = ( "download_failure_warning_threshold", "series_suggest_enabled", "series_suggest_threshold", + "extdl_mega_enabled", + "extdl_gdrive_enabled", + "extdl_mediafire_enabled", + "extdl_dropbox_enabled", + "extdl_pixeldrain_enabled", +) + +# Per-host external-download toggles — all plain booleans, validated uniformly. +_EXTDL_TOGGLE_FIELDS = ( + "extdl_mega_enabled", + "extdl_gdrive_enabled", + "extdl_mediafire_enabled", + "extdl_dropbox_enabled", + "extdl_pixeldrain_enabled", ) @@ -50,6 +64,11 @@ async def get_import_settings(): "download_failure_warning_threshold": row.download_failure_warning_threshold, "series_suggest_enabled": row.series_suggest_enabled, "series_suggest_threshold": row.series_suggest_threshold, + "extdl_mega_enabled": row.extdl_mega_enabled, + "extdl_gdrive_enabled": row.extdl_gdrive_enabled, + "extdl_mediafire_enabled": row.extdl_mediafire_enabled, + "extdl_dropbox_enabled": row.extdl_dropbox_enabled, + "extdl_pixeldrain_enabled": row.extdl_pixeldrain_enabled, }) @@ -106,6 +125,9 @@ async def update_import_settings(): return jsonify( {"error": "series_suggest_enabled must be a boolean"} ), 400 + for tog in _EXTDL_TOGGLE_FIELDS: + if tog in body and not isinstance(body[tog], bool): + return jsonify({"error": f"{tog} must be a boolean"}), 400 if "series_suggest_threshold" in body: v = body["series_suggest_threshold"] if not isinstance(v, (int, float)) or isinstance(v, bool) or v < 0 or v > 1: diff --git a/backend/app/models/import_settings.py b/backend/app/models/import_settings.py index 8c59fa8..5b533fc 100644 --- a/backend/app/models/import_settings.py +++ b/backend/app/models/import_settings.py @@ -73,6 +73,25 @@ class ImportSettings(Base): Float, nullable=False, default=0.5, ) + # #830 off-platform file-host downloads — per-host enable lever (default on, + # rule #26). Column names are extdl__enabled so the worker reads them + # via getattr(settings, f"extdl_{host}_enabled", True). + extdl_mega_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, server_default="true", + ) + extdl_gdrive_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, server_default="true", + ) + extdl_mediafire_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, server_default="true", + ) + extdl_dropbox_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, server_default="true", + ) + extdl_pixeldrain_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, server_default="true", + ) + @classmethod async def load(cls, session) -> ImportSettings: """The singleton settings row (id=1), via an async session.""" diff --git a/frontend/src/components/subscriptions/SettingsTab.vue b/frontend/src/components/subscriptions/SettingsTab.vue index 5b6c3b2..3d86e13 100644 --- a/frontend/src/components/subscriptions/SettingsTab.vue +++ b/frontend/src/components/subscriptions/SettingsTab.vue @@ -52,6 +52,52 @@ +

External file-host downloads

+ + +
+ When a creator links files on these hosts in a post, Curator downloads + them into the post automatically (tagged + provenance-linked like any + other media). Turn a host off if it starts rate-limiting or banning. +
+ + + + + + + + + + + + + + + + + +
+ + + +
+

Schedule defaults

@@ -151,6 +197,11 @@ const dl = reactive({ download_schedule_default_seconds: 28800, download_event_retention_days: 90, download_failure_warning_threshold: 5, + extdl_mega_enabled: true, + extdl_gdrive_enabled: true, + extdl_mediafire_enabled: true, + extdl_dropbox_enabled: true, + extdl_pixeldrain_enabled: true, }) watch(() => importStore.settings, (s) => { if (s) Object.assign(dl, s) }, { immediate: true }) diff --git a/tests/test_api_settings_downloader.py b/tests/test_api_settings_downloader.py index 4a1235c..b784263 100644 --- a/tests/test_api_settings_downloader.py +++ b/tests/test_api_settings_downloader.py @@ -44,3 +44,24 @@ async def test_patch_rejects_non_bool_validate(client): "/api/settings/import", json={"download_validate_files": "yes"} ) assert resp.status_code == 400 + + +@pytest.mark.asyncio +async def test_extdl_host_toggles_default_on_and_patch(client): + body = await (await client.get("/api/settings/import")).get_json() + assert body["extdl_mega_enabled"] is True + assert body["extdl_pixeldrain_enabled"] is True + + resp = await client.patch( + "/api/settings/import", json={"extdl_mega_enabled": False} + ) + assert resp.status_code == 200 + assert (await resp.get_json())["extdl_mega_enabled"] is False + + +@pytest.mark.asyncio +async def test_extdl_toggle_rejects_non_bool(client): + resp = await client.patch( + "/api/settings/import", json={"extdl_gdrive_enabled": "nope"} + ) + assert resp.status_code == 400