56970fb66d
Operator-flagged 2026-05-28, two asks.
**1. Credential Verify (was missing vs GS — and now actually verifies).**
GS's Verify was a stub (`TODO: implement actual verification` — just
stamped last_verified). FC does a real check, which matters given the
recent auth pain (subscribestar age cookie, HF host-only PHPSESSID):
- GalleryDLService.verify(url, platform, cookies_path, auth_token) runs
gallery-dl in `--simulate --range 1-1` mode (no download) against the
URL with the materialized credentials, then reuses _categorize_error:
returncode 0 / NO_NEW_CONTENT → valid; AUTH_ERROR → invalid; other →
inconclusive (reason surfaced). 45s timeout.
- POST /api/credentials/<platform>/verify picks an enabled Source for
the platform to probe, runs verify, and on success stamps
credential.last_verified (new CredentialService.mark_verified).
Returns {valid: bool|null, reason, last_verified?}. valid=null means
untestable (no credential, or no enabled source to point at).
- CredentialCard gains a Verify button (on credentialed cards) + a
result chip (Verified ✓ / Failed / Untestable) and a toast with the
reason. SettingsTab reloads on @verified so last_verified refreshes.
**2. Live download-activity feedback.** The Downloads tab was static —
no way to tell if downloads were succeeding without manually hitting
Refresh. It now auto-polls: stats every 4s, and the event list too
while anything is queued/running. Polling pauses when the tab is
backgrounded (document.hidden) and the list reload is skipped on idle
ticks to stay light. A pulsing "● live" indicator next to the stat
chips shows when auto-refresh is active (queued+running > 0); honors
prefers-reduced-motion.
Tests: verify endpoint — untestable with no credential, untestable with
no enabled source, valid+stamped on success (gallery-dl mocked), and
auth-failure reported without stamping.
198 lines
6.1 KiB
Vue
198 lines
6.1 KiB
Vue
<template>
|
|
<div>
|
|
<ExtensionKeyBar class="mb-4" />
|
|
|
|
<v-alert v-if="credentialsStore.error" type="error" variant="tonal" closable class="mb-4">
|
|
{{ String(credentialsStore.error) }}
|
|
</v-alert>
|
|
|
|
<h3 class="text-h6 mb-3">Platform credentials</h3>
|
|
<v-row>
|
|
<v-col
|
|
v-for="p in platformsStore.list"
|
|
:key="p.key"
|
|
cols="12" md="6"
|
|
>
|
|
<CredentialCard
|
|
:platform="p"
|
|
:credential="credentialsStore.byPlatform.get(p.key) || null"
|
|
@replace="openUpload"
|
|
@remove="confirmRemove"
|
|
@verified="onSaved"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<h3 class="text-h6 mb-3 mt-6">Downloader</h3>
|
|
<v-card variant="outlined">
|
|
<v-card-text v-if="importStore.settings">
|
|
<v-row>
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field
|
|
v-model.number="dl.download_rate_limit_seconds"
|
|
label="Rate limit (seconds between requests)"
|
|
type="number" step="0.5" min="0"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">gallery-dl extractor.sleep. Higher = slower but safer.</div>
|
|
</v-col>
|
|
<v-col cols="12" sm="6">
|
|
<v-switch
|
|
v-model="dl.download_validate_files"
|
|
label="Validate downloaded files (magic-byte check)"
|
|
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>
|
|
<v-card variant="outlined">
|
|
<v-card-text v-if="importStore.settings">
|
|
<v-row>
|
|
<v-col cols="12" sm="4">
|
|
<v-text-field
|
|
v-model.number="dl.download_schedule_default_seconds"
|
|
label="Default check interval (seconds)"
|
|
type="number" :min="60" :max="86400"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">
|
|
Used when a source has no per-source or per-artist override.
|
|
Default 28800 (8 hours).
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" sm="4">
|
|
<v-text-field
|
|
v-model.number="dl.download_event_retention_days"
|
|
label="Event retention (days)"
|
|
type="number" :min="1" :max="3650"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">
|
|
Completed download events older than this are deleted nightly.
|
|
Default 90.
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" sm="4">
|
|
<v-text-field
|
|
v-model.number="dl.download_failure_warning_threshold"
|
|
label="Failure warning threshold"
|
|
type="number" :min="1" :max="100"
|
|
density="compact" hide-details
|
|
@blur="saveDownloader"
|
|
/>
|
|
<div class="fc-help">
|
|
Source row badge turns red after this many consecutive
|
|
failures. Sources are never auto-disabled. Default 5.
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
<v-alert v-if="importStore.settingsError" type="error" variant="tonal" class="mt-2" closable>
|
|
{{ importStore.settingsError }}
|
|
</v-alert>
|
|
</v-card-text>
|
|
<v-card-text v-else>
|
|
<v-skeleton-loader type="paragraph" />
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<CredentialUploadDialog
|
|
v-model="showUpload"
|
|
:platform="uploadPlatform"
|
|
@saved="onSaved"
|
|
/>
|
|
|
|
<v-dialog v-model="removeConfirm.open" max-width="420">
|
|
<v-card>
|
|
<v-card-title>Delete {{ removeConfirm.platform?.name }} credential?</v-card-title>
|
|
<v-card-text>
|
|
The encrypted credential will be removed permanently. You'll need to
|
|
re-upload to use this platform again.
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn variant="text" @click="removeConfirm.open = false">Cancel</v-btn>
|
|
<v-btn color="error" variant="flat" @click="doRemove">Delete</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, reactive, ref, watch } from 'vue'
|
|
import { usePlatformsStore } from '../../stores/platforms.js'
|
|
import { useCredentialsStore } from '../../stores/credentials.js'
|
|
import { useImportStore } from '../../stores/import.js'
|
|
import ExtensionKeyBar from '../credentials/ExtensionKeyBar.vue'
|
|
import CredentialUploadDialog from '../credentials/CredentialUploadDialog.vue'
|
|
import CredentialCard from './CredentialCard.vue'
|
|
|
|
const platformsStore = usePlatformsStore()
|
|
const credentialsStore = useCredentialsStore()
|
|
const importStore = useImportStore()
|
|
|
|
const showUpload = ref(false)
|
|
const uploadPlatform = ref(null)
|
|
const removeConfirm = reactive({ open: false, platform: null })
|
|
|
|
const dl = reactive({
|
|
download_rate_limit_seconds: 3.0,
|
|
download_validate_files: true,
|
|
download_schedule_default_seconds: 28800,
|
|
download_event_retention_days: 90,
|
|
download_failure_warning_threshold: 5,
|
|
})
|
|
|
|
watch(() => importStore.settings, (s) => { if (s) Object.assign(dl, s) }, { immediate: true })
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
platformsStore.loadAll(),
|
|
credentialsStore.loadAll(),
|
|
importStore.loadSettings(),
|
|
])
|
|
})
|
|
|
|
function openUpload(platform) {
|
|
uploadPlatform.value = platform
|
|
showUpload.value = true
|
|
}
|
|
|
|
async function onSaved() {
|
|
await credentialsStore.loadAll()
|
|
}
|
|
|
|
function confirmRemove(platform) {
|
|
removeConfirm.platform = platform
|
|
removeConfirm.open = true
|
|
}
|
|
|
|
async function doRemove() {
|
|
await credentialsStore.remove(removeConfirm.platform.key)
|
|
removeConfirm.open = false
|
|
await credentialsStore.loadAll()
|
|
}
|
|
|
|
async function saveDownloader() {
|
|
await importStore.patchSettings({ ...dl })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-help {
|
|
font-size: 12px;
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
margin-top: 2px;
|
|
}
|
|
</style>
|