Compare commits

..

4 Commits

7 changed files with 156 additions and 5 deletions

View File

@ -3,11 +3,13 @@ import logging
import os import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from flask import current_app, g, url_for from flask import current_app, g, url_for, flash
import humanize import humanize
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import requests
from .exceptions import ErrorException from .exceptions import ErrorException
if TYPE_CHECKING: if TYPE_CHECKING:
from .set import BrickSet from .set import BrickSet
@ -112,6 +114,31 @@ class BrickInstructions(object):
logger.info('The instruction file {file} has been imported'.format( logger.info('The instruction file {file} has been imported'.format(
file=self.filename file=self.filename
)) ))
def download(self, href: str, /) -> None:
target = self.path(secure_filename(self.filename))
if os.path.isfile(target):
raise ErrorException('Cannot upload {target} as it already exists'.format( # noqa: E501
target=self.filename
))
url = f"https://rebrickable.com/{href}"
response = requests.get(url)
if response.status_code == 200:
# Save the file
with open(target, 'wb') as file:
file.write(response.content)
print(f"Downloaded {self.filename} to {target}")
else:
print(f"Failed to download {self.filename}. Status code: {response.status_code}", 'danger')
# Info
logger.info('The instruction file {file} has been imported'.format(
file=self.filename
))
# Compute the url for a set instructions file # Compute the url for a set instructions file
def url(self, /) -> str: def url(self, /) -> str:

View File

@ -4,7 +4,8 @@ from flask import (
redirect, redirect,
render_template, render_template,
request, request,
url_for url_for,
flash
) )
from flask_login import login_required from flask_login import login_required
from werkzeug.wrappers.response import Response from werkzeug.wrappers.response import Response
@ -15,6 +16,9 @@ from ..instructions import BrickInstructions
from ..instructions_list import BrickInstructionsList from ..instructions_list import BrickInstructionsList
from .upload import upload_helper from .upload import upload_helper
import requests
from bs4 import BeautifulSoup
instructions_page = Blueprint( instructions_page = Blueprint(
'instructions', 'instructions',
__name__, __name__,
@ -126,3 +130,70 @@ def do_upload() -> Response:
BrickInstructionsList(force=True) BrickInstructionsList(force=True)
return redirect(url_for('instructions.list')) return redirect(url_for('instructions.list'))
# Download instructions from Rebrickable
@instructions_page.route('/download/', methods=['GET'])
@login_required
@exception_handler(__file__)
def download() -> str:
return render_template(
'instructions.html',
download=True,
error=request.args.get('error')
)
# Actually download an instructions file
@instructions_page.route('/download/', methods=['POST'])
@login_required
@exception_handler(__file__, post_redirect='instructions.download')
def do_download() -> Response:
set_id: str = request.form.get('add-set', '')
url = f"https://rebrickable.com/instructions/{set_id}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
flash(f"Failed to load page. Status code: {response.status_code}", 'danger')
return redirect(url_for('instructions.download'))
# Parse the HTML content
soup = BeautifulSoup(response.content, 'html.parser')
# Collect all <img> tags with "LEGO Building Instructions" in the alt attribute
found_tags = []
for a_tag in soup.find_all('a', href=True):
img_tag = a_tag.find('img', alt=True)
if img_tag and "LEGO Building Instructions" in img_tag['alt']:
found_tags.append((img_tag['alt'].replace('LEGO Building Instructions for ', ''), a_tag['href'])) # Save alt and href
return render_template('instructions.html', download=True, found_tags=found_tags)
@instructions_page.route('/confirm_download', methods=['POST'])
@login_required
@exception_handler(__file__, post_redirect='instructions.download')
def confirm_download() -> Response:
selected_instructions = []
for key in request.form:
if key.startswith('instruction-') and request.form.get(key) == 'on': # Checkbox is checked
index = key.split('-')[-1]
alt_text = request.form.get(f'instruction-alt-text-{index}')
href_text = request.form.get(f'instruction-href-text-{index}')
selected_instructions.append((href_text,alt_text))
if not selected_instructions:
flash("No instructions selected", 'danger')
return redirect(url_for('instructions.download'))
for href, filename in selected_instructions:
print(f"Downloading {filename} from {href}")
BrickInstructions(f"{filename}.pdf").download(href)
BrickInstructionsList(force=True)
#flash("Selected instructions have been downloaded", 'success')
return redirect(url_for('instructions.list'))

View File

@ -7,4 +7,5 @@ humanize
jinja2 jinja2
rebrick rebrick
requests requests
tzdata tzdata
bs4

View File

@ -5,6 +5,8 @@
{% block main %} {% block main %}
{% if upload %} {% if upload %}
{% include 'instructions/upload.html' %} {% include 'instructions/upload.html' %}
{% elif download %}
{% include 'instructions/download.html' %}
{% elif rename %} {% elif rename %}
{% include 'instructions/rename.html' %} {% include 'instructions/rename.html' %}
{% elif delete %} {% elif delete %}
@ -14,6 +16,7 @@
{% if g.login.is_authenticated() %} {% if g.login.is_authenticated() %}
<p class="border-bottom pb-2 px-2 text-center"> <p class="border-bottom pb-2 px-2 text-center">
<a class="btn btn-primary" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a> <a class="btn btn-primary" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a>
<a class="btn btn-primary" href="{{ url_for('instructions.download') }}"><i class="ri-download-line"></i> Download instructions from Rebrickable</a>
<a href="{{ url_for('admin.admin', open_instructions=true) }}" class="btn btn-light border" role="button"><i class="ri-refresh-line"></i> Refresh the instructions cache</a> <a href="{{ url_for('admin.admin', open_instructions=true) }}" class="btn btn-light border" role="button"><i class="ri-refresh-line"></i> Refresh the instructions cache</a>
</p> </p>
{% endif %} {% endif %}

View File

@ -0,0 +1,49 @@
<div class="container">
<div class="row">
<div class="col-12">
<form method="POST" action="{{ url_for('instructions.do_download') }}">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="ri-add-circle-line"></i> Download instructions from Rebrickable</h5>
</div>
<div class="card-body">
<div id="add-fail" class="alert alert-danger d-none" role="alert"></div>
<div id="add-complete" class="alert alert-success d-none" role="alert"></div>
<div class="mb-3">
<label for="add-set" class="form-label">Set number (only one)</label>
<input type="text" class="form-control" id="add-set" name="add-set" placeholder="107-1 or 1642-1 or ..." value="{{ request.args.get('set_num', '') }}">
</div>
<button type="submit" class="btn btn-primary">Search</button>
</div>
</div>
</form>
{% if found_tags %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="ri-add-circle-line"></i> Select instructions to download</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('instructions.confirm_download') }}">
<div class="mb-3">
<label class="form-label">Available Instructions</label>
<div class="form-check">
{% for alt_text, href in found_tags %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="instruction-{{ loop.index }}" id="instruction-{{ loop.index }}">
<label class="form-check-label" for="instruction-{{ loop.index }}">
{{ alt_text }}
</label>
<input type="hidden" name="instruction-alt-text-{{ loop.index }}" value="{{ alt_text }}">
<input type="hidden" name="instruction-href-text-{{ loop.index }}" value="{{ href }}">
</div>
{% endfor %}
</div>
</div>
<button type="submit" class="btn btn-primary">Download Selected</button>
</form>
</div>
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -50,6 +50,7 @@
<span class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No instructions file found.</span> <span class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No instructions file found.</span>
{% if g.login.is_authenticated() %} {% if g.login.is_authenticated() %}
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a> <a class="list-group-item list-group-item-action" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a>
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set_num=item.fields.set_num) }}"><i class="ri-download-line"></i> Download instruction from Rebrickable</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -22,8 +22,7 @@
{% set retirement_date = retired.get(item.fields.set_num) %} {% set retirement_date = retired.get(item.fields.set_num) %}
<tr> <tr>
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set_num) }} {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set_num) }}
<td>{{ item.fields.set_num }}<br /> <td>{{ item.fields.set_num }}<br>{{ badge.rebrickable(item) }}</td>
{{ badge.rebrickable(item) }}</td>
<td>{{ item.fields.name }}</td> <td>{{ item.fields.name }}</td>
<td>{{ item.theme_name }}</td> <td>{{ item.theme_name }}</td>
<td>{{ item.fields.year }}</td> <td>{{ item.fields.year }}</td>