feat(maintenance): add library-validation sweep task and API

Walks the downloads tree and reports files that fail magic-byte
validation — the "how many pre-existing files in my library are
silently truncated?" question, answered. Skips the _quarantine
subtree (already known-bad) and caps suspect_paths at 500 with a
truncated flag so the persisted report can't grow unbounded.

On-demand only: a full filesystem walk on a NAS-mounted library is
expensive and the user should choose when to pay for it. Pre-existing
files are reported, not quarantined — they may be the only copy and
the user decides what to do.

Adds GET /api/settings/library-validation (most recent report) and
POST /api/settings/library-validation/run (queue a fresh sweep).
Cached under cache.library_validation_report.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 22:56:01 -04:00
parent 2765c464bd
commit 6d875662fa
3 changed files with 240 additions and 2 deletions
+34 -1
View File
@@ -6,7 +6,14 @@ from quart import Blueprint, request, jsonify, current_app
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.setting import Setting, DEFAULT_SETTINGS, EXTENSION_API_KEY_SETTING, STORAGE_STATS_SETTING, generate_api_key
from app.models.setting import (
Setting,
DEFAULT_SETTINGS,
EXTENSION_API_KEY_SETTING,
STORAGE_STATS_SETTING,
LIBRARY_VALIDATION_REPORT_SETTING,
generate_api_key,
)
from app.config import get_settings
from app.services.gallery_dl import GalleryDLService
@@ -270,6 +277,32 @@ async def refresh_storage_stats():
return jsonify({"error": str(e)}), 500
@bp.route("/library-validation", methods=["GET"])
async def get_library_validation_report():
"""Return the most recent library-validation sweep report (or null)."""
async with current_app.db_engine.connect() as conn:
async with AsyncSession(bind=conn) as session:
result = await session.execute(
select(Setting).where(Setting.key == LIBRARY_VALIDATION_REPORT_SETTING)
)
setting = result.scalar_one_or_none()
return jsonify(setting.value if setting and setting.value else None)
@bp.route("/library-validation/run", methods=["POST"])
async def run_library_validation():
"""Queue a library-validation sweep (walks the library, reports truncated files)."""
from app.tasks.maintenance import validate_library
try:
validate_library.delay()
current_app.logger.info("Queued library validation sweep")
return jsonify({"message": "Library validation queued"})
except Exception as e:
current_app.logger.error(f"Failed to queue library validation: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/logs", methods=["GET"])
async def get_logs():
"""Get recent download logs from download metadata.