add printer out-of-paper warning and track app.py
Show a yellow warning banner on page load if CUPS reports the printer is stopped with a paper-related reason (e.g. Media Empty).
This commit is contained in:
parent
01e6cf6300
commit
3cd6d20d13
2 changed files with 151 additions and 1 deletions
138
home/print/app.py
Normal file
138
home/print/app.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pypdf
|
||||||
|
from flask import Flask, flash, redirect, render_template, request, url_for
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
UPLOAD_FOLDER = str(Path(__file__).parent / "uploads")
|
||||||
|
ALLOWED_EXTENSIONS = {"pdf"}
|
||||||
|
PRINTER_NAME = "HL-2270DW_series"
|
||||||
|
MAX_PAGES = 10
|
||||||
|
MAX_COPIES = 10
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||||
|
app.config["SECRET_KEY"] = os.urandom(24)
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def get_printer_warning():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["lpstat", "-l", "-p", PRINTER_NAME],
|
||||||
|
capture_output=True, text=True, timeout=3
|
||||||
|
)
|
||||||
|
output = result.stdout.lower()
|
||||||
|
first_line = result.stdout.splitlines()[0] if result.stdout else ""
|
||||||
|
if "stopped" in first_line.lower() or "offline" in first_line.lower():
|
||||||
|
paper_keywords = ("media empty", "out of paper", "paper", "tray empty", "no media")
|
||||||
|
if any(kw in output for kw in paper_keywords):
|
||||||
|
return "Printer appears to be out of paper."
|
||||||
|
return "Printer is stopped or offline and may not accept jobs."
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_pdf_page_count(filepath):
|
||||||
|
try:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
reader = pypdf.PdfReader(f)
|
||||||
|
return len(reader.pages)
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Could not read PDF pages: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET", "POST"])
|
||||||
|
def upload_and_print():
|
||||||
|
if request.method == "POST":
|
||||||
|
if "file" not in request.files:
|
||||||
|
flash("No file part in the request.", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
|
||||||
|
if file.filename == "":
|
||||||
|
flash("No file selected. Please choose a PDF to print.", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
try:
|
||||||
|
copies = int(request.form.get("copies", 1))
|
||||||
|
except ValueError:
|
||||||
|
copies = 1
|
||||||
|
copies = max(1, min(copies, MAX_COPIES))
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
|
||||||
|
filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
try:
|
||||||
|
page_count = get_pdf_page_count(filepath)
|
||||||
|
if page_count is None:
|
||||||
|
flash(
|
||||||
|
"Could not process the PDF file. It might be corrupt.", "error"
|
||||||
|
)
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
if page_count > MAX_PAGES:
|
||||||
|
flash(
|
||||||
|
f"Error: File has {page_count} pages. The maximum allowed is {MAX_PAGES}.",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
app.logger.info(
|
||||||
|
f"Sending '{filename}' ({page_count} pages, {copies} copies) to printer '{PRINTER_NAME}'"
|
||||||
|
)
|
||||||
|
command = [
|
||||||
|
"lp",
|
||||||
|
"-d",
|
||||||
|
PRINTER_NAME,
|
||||||
|
"-n",
|
||||||
|
str(copies),
|
||||||
|
filepath,
|
||||||
|
]
|
||||||
|
result = subprocess.run(
|
||||||
|
command, capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
app.logger.info(f"lp command stdout: {result.stdout}")
|
||||||
|
copy_word = "copy" if copies == 1 else "copies"
|
||||||
|
flash(f"Success! '{filename}' ({copies} {copy_word}) has been sent to the printer.", "success")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
app.logger.error(f"Error printing file: {e}")
|
||||||
|
app.logger.error(f"lp command stderr: {e.stderr}")
|
||||||
|
flash(
|
||||||
|
"Error sending file to printer. The printer may be offline or misconfigured.",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"An unexpected error occurred: {e}")
|
||||||
|
flash("An unexpected server error occurred.", "error")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
app.logger.info(f"Cleaned up temporary file: {filepath}")
|
||||||
|
|
||||||
|
return redirect(url_for("upload_and_print"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
flash("Invalid file type. Please upload a PDF file.", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
return render_template("index.html", printer_name=PRINTER_NAME, max_pages=MAX_PAGES, max_copies=MAX_COPIES, printer_warning=get_printer_warning())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="127.0.0.1", port=57928, debug=False)
|
||||||
|
|
@ -26,6 +26,12 @@
|
||||||
<p class="text-sm text-gray-500 mt-1">(Max {{ max_pages }} pages)</p>
|
<p class="text-sm text-gray-500 mt-1">(Max {{ max_pages }} pages)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if printer_warning %}
|
||||||
|
<div class="mb-6 p-4 rounded-lg text-sm bg-yellow-100 text-yellow-800" role="alert">
|
||||||
|
<span class="font-medium">Warning:</span> {{ printer_warning }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="mb-6 space-y-3">
|
<div class="mb-6 space-y-3">
|
||||||
|
|
@ -52,7 +58,13 @@
|
||||||
<input id="file-upload" name="file" type="file" class="sr-only" accept=".pdf">
|
<input id="file-upload" name="file" type="file" class="sr-only" accept=".pdf">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-6">
|
||||||
|
<label for="copies" class="block text-sm font-medium text-gray-700 mb-1">Number of copies</label>
|
||||||
|
<input id="copies" name="copies" type="number" value="1" min="1" max="{{ max_copies }}"
|
||||||
|
class="w-24 border border-gray-300 rounded-lg px-3 py-2 text-center text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
<button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 transition-all duration-300 transform hover:scale-105">
|
<button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 transition-all duration-300 transform hover:scale-105">
|
||||||
Print File
|
Print File
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue