feat(external): per-host enable toggles in Settings (Phase 4d)
Operator lever: disable a single file host (e.g. mega.nz when it's banning) without touching the others. Five booleans on import_settings (extdl_<host>_enabled, default true — works out of the box, rule #26); the worker already reads them via getattr so no worker change. Migration 0050 + model fields + settings GET/PATCH (uniform boolean validation) + a 'External file-host downloads' card in the subscriptions Settings tab. Completes Phase 4. Refs FC #830. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||||
@@ -27,6 +27,20 @@ _EDITABLE_FIELDS = (
|
|||||||
"download_failure_warning_threshold",
|
"download_failure_warning_threshold",
|
||||||
"series_suggest_enabled",
|
"series_suggest_enabled",
|
||||||
"series_suggest_threshold",
|
"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,
|
"download_failure_warning_threshold": row.download_failure_warning_threshold,
|
||||||
"series_suggest_enabled": row.series_suggest_enabled,
|
"series_suggest_enabled": row.series_suggest_enabled,
|
||||||
"series_suggest_threshold": row.series_suggest_threshold,
|
"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(
|
return jsonify(
|
||||||
{"error": "series_suggest_enabled must be a boolean"}
|
{"error": "series_suggest_enabled must be a boolean"}
|
||||||
), 400
|
), 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:
|
if "series_suggest_threshold" in body:
|
||||||
v = body["series_suggest_threshold"]
|
v = body["series_suggest_threshold"]
|
||||||
if not isinstance(v, (int, float)) or isinstance(v, bool) or v < 0 or v > 1:
|
if not isinstance(v, (int, float)) or isinstance(v, bool) or v < 0 or v > 1:
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ class ImportSettings(Base):
|
|||||||
Float, nullable=False, default=0.5,
|
Float, nullable=False, default=0.5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# #830 off-platform file-host downloads — per-host enable lever (default on,
|
||||||
|
# rule #26). Column names are extdl_<host>_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
|
@classmethod
|
||||||
async def load(cls, session) -> ImportSettings:
|
async def load(cls, session) -> ImportSettings:
|
||||||
"""The singleton settings row (id=1), via an async session."""
|
"""The singleton settings row (id=1), via an async session."""
|
||||||
|
|||||||
@@ -52,6 +52,52 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<h3 class="text-h6 mb-3 mt-6">External file-host downloads</h3>
|
||||||
|
<v-card variant="outlined">
|
||||||
|
<v-card-text v-if="importStore.settings">
|
||||||
|
<div class="fc-help mb-2">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="dl.extdl_mega_enabled" label="mega.nz"
|
||||||
|
density="compact" hide-details color="accent" @change="saveDownloader"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="dl.extdl_gdrive_enabled" label="Google Drive"
|
||||||
|
density="compact" hide-details color="accent" @change="saveDownloader"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="dl.extdl_pixeldrain_enabled" label="Pixeldrain"
|
||||||
|
density="compact" hide-details color="accent" @change="saveDownloader"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="dl.extdl_mediafire_enabled" label="MediaFire"
|
||||||
|
density="compact" hide-details color="accent" @change="saveDownloader"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="dl.extdl_dropbox_enabled" label="Dropbox"
|
||||||
|
density="compact" hide-details color="accent" @change="saveDownloader"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-text v-else>
|
||||||
|
<v-skeleton-loader type="paragraph" />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
<h3 class="text-h6 mb-3 mt-6">Schedule defaults</h3>
|
<h3 class="text-h6 mb-3 mt-6">Schedule defaults</h3>
|
||||||
<v-card variant="outlined">
|
<v-card variant="outlined">
|
||||||
<v-card-text v-if="importStore.settings">
|
<v-card-text v-if="importStore.settings">
|
||||||
@@ -151,6 +197,11 @@ const dl = reactive({
|
|||||||
download_schedule_default_seconds: 28800,
|
download_schedule_default_seconds: 28800,
|
||||||
download_event_retention_days: 90,
|
download_event_retention_days: 90,
|
||||||
download_failure_warning_threshold: 5,
|
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 })
|
watch(() => importStore.settings, (s) => { if (s) Object.assign(dl, s) }, { immediate: true })
|
||||||
|
|||||||
@@ -44,3 +44,24 @@ async def test_patch_rejects_non_bool_validate(client):
|
|||||||
"/api/settings/import", json={"download_validate_files": "yes"}
|
"/api/settings/import", json={"download_validate_files": "yes"}
|
||||||
)
|
)
|
||||||
assert resp.status_code == 400
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user