forked from FrederikBaerentsen/BrickTracker
Compare commits
4 Commits
add-rebric
...
master
Author | SHA1 | Date | |
---|---|---|---|
9aa5bd43ec | |||
053bf75e05 | |||
ace4a06b6a | |||
631df49cd3 |
@ -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:
|
||||||
|
@ -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'))
|
||||||
|
@ -7,4 +7,5 @@ humanize
|
|||||||
jinja2
|
jinja2
|
||||||
rebrick
|
rebrick
|
||||||
requests
|
requests
|
||||||
tzdata
|
tzdata
|
||||||
|
bs4
|
||||||
|
@ -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 %}
|
||||||
|
49
templates/instructions/download.html
Normal file
49
templates/instructions/download.html
Normal 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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user