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
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
from datetime import datetime
|
|
from app import db
|
|
|
|
|
|
class Instruction(db.Model):
|
|
"""Model for instruction files (PDFs or images)."""
|
|
|
|
__tablename__ = 'instructions'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
set_id = db.Column(db.Integer, db.ForeignKey('sets.id'), nullable=False, index=True)
|
|
|
|
# File information
|
|
file_type = db.Column(db.String(10), nullable=False) # 'PDF' or 'IMAGE'
|
|
file_path = db.Column(db.String(500), nullable=False)
|
|
file_name = db.Column(db.String(200), nullable=False)
|
|
file_size = db.Column(db.Integer) # Size in bytes
|
|
thumbnail_path = db.Column(db.String(500)) # Thumbnail preview (especially for PDFs)
|
|
|
|
# SHA-256 of the stored file. Set by the automatic fetcher so the same
|
|
# PDF is never downloaded twice; null for older manual uploads.
|
|
sha256 = db.Column(db.String(64), nullable=True, index=True)
|
|
# Where the file came from: None/'upload' for manual, 'brickset' for the
|
|
# automatic fetcher.
|
|
source = db.Column(db.String(20), nullable=True)
|
|
|
|
# For image sequences
|
|
page_number = db.Column(db.Integer, default=1)
|
|
|
|
# Metadata
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
uploaded_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
|
|
|
def __repr__(self):
|
|
return f'<Instruction {self.file_name} for Set {self.set_id}>'
|
|
|
|
def to_dict(self):
|
|
"""Convert instruction to dictionary."""
|
|
# Ensure forward slashes for web URLs
|
|
clean_path = self.file_path.replace('\\', '/')
|
|
thumbnail_clean = self.thumbnail_path.replace('\\', '/') if self.thumbnail_path else None
|
|
|
|
return {
|
|
'id': self.id,
|
|
'set_id': self.set_id,
|
|
'file_type': self.file_type,
|
|
'file_name': self.file_name,
|
|
'file_size': self.file_size,
|
|
'page_number': self.page_number,
|
|
'source': self.source,
|
|
'file_url': f'/static/uploads/{clean_path}',
|
|
'thumbnail_url': f'/static/uploads/{thumbnail_clean}' if thumbnail_clean else None,
|
|
'uploaded_at': self.uploaded_at.isoformat()
|
|
}
|
|
|
|
@property
|
|
def file_size_mb(self):
|
|
"""Return file size in MB."""
|
|
if self.file_size:
|
|
return round(self.file_size / (1024 * 1024), 2)
|
|
return 0
|
|
|
|
@staticmethod
|
|
def allowed_file(filename):
|
|
"""Check if file extension is allowed."""
|
|
from flask import current_app
|
|
return '.' in filename and \
|
|
filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
|