From 053bf75e05b32a1bebe803630402701b0f3f4f40 Mon Sep 17 00:00:00 2001 From: FrederikBaerentsen Date: Wed, 22 Jan 2025 22:41:35 +0100 Subject: [PATCH] Added instructions downloader from Rebrickable. --- bricktracker/instructions.py | 29 ++++++++++- bricktracker/views/instructions.py | 73 +++++++++++++++++++++++++++- templates/instructions.html | 3 ++ templates/instructions/download.html | 49 +++++++++++++++++++ templates/set/card.html | 1 + 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 templates/instructions/download.html 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 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() %}

Upload an instructions file + Download instructions from Rebrickable Refresh the instructions cache

{% 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 @@ +
+
+
+
+
+
+
Download instructions from Rebrickable
+
+
+ + +
+ + +
+ +
+
+
+ {% if found_tags %} +
+
+
Select instructions to download
+
+
+
+
+ +
+ {% for alt_text, href in found_tags %} +
+ + + + +
+ {% endfor %} +
+
+ +
+
+
+ {% endif %} +
+
+
\ 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 @@ No instructions file found. {% if g.login.is_authenticated() %} Upload an instructions file + Download instruction from Rebrickable {% endif %} {% endif %}