Files
lego-instructions-manager/app/config.py
T
jessikitty 89f65e2d74 Add automatic instruction fetcher for official sets
Downloads official instruction PDFs from LEGO, located via the Brickset API, one set per interval (10 minutes by default) so neither API gets hammered. Official sets only; MOCs are excluded throughout.

- New admin page at /admin/instruction-fetch: queue builder, pacing controls, live countdown and per-set job history

- Database-backed queue (instruction_fetch_jobs) and last-run timestamp, so pacing survives restarts

- Background worker thread, plus 'flask instructions queue/run/status' for Task Scheduler or cron

- SHA-256 dedupe (instructions.sha256), PDF header check, size cap and streaming downloads via temp files

- brickset_api: getInstructions2 by set number and getInstructions by setID (the old code sent a set number to the setID method); getSets params now proper JSON; getKeyUsageStats added

- Schema top-ups applied automatically at boot: new table plus instructions.sha256/source

- Docker: fetcher env vars passed through compose, no worker thread in the entrypoint bootstrap
2026-09-26 13:43:17 +10:00

112 lines
4.4 KiB
Python

import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
projectdir = os.path.dirname(basedir)
# Look for .env beside app/ first (project root), then inside app/.
load_dotenv(os.path.join(projectdir, '.env'))
load_dotenv(os.path.join(basedir, '.env'))
def _resolve_path(value, default):
"""Absolute value as-is; relative resolved against the project root."""
if not value:
return default
return value if os.path.isabs(value) else os.path.join(projectdir, value)
class Config:
"""Base configuration."""
# Flask
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
# Database
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'lego_instructions.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# File Upload
# Defaults to <app>/static/uploads (E:\LIM\app\static\uploads on the
# Windows install). Set UPLOAD_FOLDER to override — the Docker image
# points it at a mounted volume, e.g. /data/uploads.
# A relative value is resolved against the project root, NOT the working
# directory, so `UPLOAD_FOLDER=app/static/uploads` in .env means the same
# thing no matter where you run flask from.
UPLOAD_FOLDER = _resolve_path(os.environ.get('UPLOAD_FOLDER'),
os.path.join(basedir, 'static', 'uploads'))
MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH', 52428800)) # 50MB
ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp'}
# Brickset API
BRICKSET_API_KEY = os.environ.get('BRICKSET_API_KEY')
BRICKSET_USERNAME = os.environ.get('BRICKSET_USERNAME')
BRICKSET_PASSWORD = os.environ.get('BRICKSET_PASSWORD')
BRICKSET_API_URL = 'https://brickset.com/api/v3.asmx'
# Backups
# Defaults to <project root>/backups so archives never land inside the
# uploads tree (which would make each backup include the previous one).
# Relative values resolve against the project root, as with UPLOAD_FOLDER.
BACKUP_FOLDER = _resolve_path(os.environ.get('BACKUP_FOLDER'),
os.path.join(projectdir, 'backups'))
# Keep only the newest N archives after a backup made from the web UI.
# Set to 0 to keep everything.
BACKUP_KEEP = int(os.environ.get('BACKUP_KEEP', 5))
# Ceiling for archives uploaded through the admin page, in bytes.
# 0 = no limit. This is separate from MAX_CONTENT_LENGTH: backup uploads
# stream straight to disk and never go through Flask's form parser.
BACKUP_MAX_UPLOAD = int(os.environ.get('BACKUP_MAX_UPLOAD', 0))
# Automatic instruction fetcher
# Pacing (interval, delays, on/off) lives in site_settings so it can be
# changed from the admin page. These are the hard limits and the switch
# for the in-process worker thread.
#
# Turn the worker off (0) if you drive the queue from Task Scheduler or
# cron with `flask instructions run` instead, or if you ever run more
# than one LIM process against the same database.
INSTRUCTION_FETCH_WORKER = os.environ.get('INSTRUCTION_FETCH_WORKER', '1') != '0'
# How often the worker wakes to check whether the interval has elapsed.
INSTRUCTION_FETCH_TICK_SECONDS = int(
os.environ.get('INSTRUCTION_FETCH_TICK_SECONDS', 20))
# Refuse anything larger than this, in megabytes. Some Technic manuals
# are genuinely enormous, hence the generous default.
FETCH_MAX_FILE_MB = int(os.environ.get('FETCH_MAX_FILE_MB', 150))
FETCH_TIMEOUT_SECONDS = int(os.environ.get('FETCH_TIMEOUT_SECONDS', 60))
# Add missing columns automatically at startup. Turn off if you'd
# rather manage schema changes with Flask-Migrate yourself.
AUTO_SCHEMA_UPGRADE = os.environ.get('AUTO_SCHEMA_UPGRADE', '1') != '0'
# Pagination
SETS_PER_PAGE = int(os.environ.get('SETS_PER_PAGE', 20))
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
TESTING = False
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
TESTING = False
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}