diff --git a/bricktracker/instructions.py b/bricktracker/instructions.py index 6aaa050..9813501 100644 --- a/bricktracker/instructions.py +++ b/bricktracker/instructions.py @@ -3,11 +3,13 @@ import logging import os from typing import TYPE_CHECKING -from flask import current_app, g, url_for +from flask import current_app, g, url_for, flash import humanize from werkzeug.datastructures import FileStorage from werkzeug.utils import secure_filename +import requests + from .exceptions import ErrorException if TYPE_CHECKING: from .set import BrickSet @@ -112,6 +114,31 @@ class BrickInstructions(object): logger.info('The instruction file {file} has been imported'.format( 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 def url(self, /) -> str: diff --git a/bricktracker/views/instructions.py b/bricktracker/views/instructions.py index 6145914..db3491a 100644 --- a/bricktracker/views/instructions.py +++ b/bricktracker/views/instructions.py @@ -4,7 +4,8 @@ from flask import ( redirect, render_template, request, - url_for + url_for, + flash ) from flask_login import login_required from werkzeug.wrappers.response import Response @@ -15,6 +16,9 @@ from ..instructions import BrickInstructions from ..instructions_list import BrickInstructionsList from .upload import upload_helper +import requests +from bs4 import BeautifulSoup + instructions_page = Blueprint( 'instructions', __name__, @@ -126,3 +130,70 @@ def do_upload() -> Response: BrickInstructionsList(force=True) 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')) diff --git a/templates/instructions.html b/templates/instructions.html index 54bb4c0..28734f3 100644 --- a/templates/instructions.html +++ b/templates/instructions.html @@ -5,6 +5,8 @@ {% block main %} {% if upload %} {% include 'instructions/upload.html' %} + {% elif download %} + {% include 'instructions/download.html' %} {% elif rename %} {% include 'instructions/rename.html' %} {% elif delete %} @@ -14,6 +16,7 @@ {% if g.login.is_authenticated() %} <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.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> </p> {% endif %} diff --git a/templates/instructions/download.html b/templates/instructions/download.html new file mode 100644 index 0000000..79b3ccc --- /dev/null +++ b/templates/instructions/download.html @@ -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> \ No newline at end of file diff --git a/templates/set/card.html b/templates/set/card.html index d639516..31900d1 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -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> {% 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.download', set_num=item.fields.set_num) }}"><i class="ri-download-line"></i> Download instruction from Rebrickable</a> {% endif %} {% endif %} </div>