from typing import Optional, Union, Literal import os import aiohttp import cups from pathlib import Path from pdf2image import convert_from_path from brother_ql.conversion import convert from brother_ql.raster import BrotherQLRaster import logging import asyncio import time from datetime import datetime, timedelta logger = logging.getLogger(__name__) class PrintService: def __init__(self, printer_name: Optional[str] = None, printer_api_url: str = "http://localhost:8000/print", min_print_interval: int = 30): """Initialize the print service. Args: printer_name: Name of the printer to use. If None, will use default printer. printer_api_url: URL of the printer API endpoint min_print_interval: Minimum time in seconds between print requests for label printer """ self.printer_name = printer_name self.printer_api_url = printer_api_url self.status_url = printer_api_url.replace('/print', '/status') self.conn = cups.Connection() self.cache_dir = Path("app/data/cache/prints") self.cache_dir.mkdir(parents=True, exist_ok=True) # Rate limiting and coordination self.min_print_interval = min_print_interval self._last_print_time = None self._print_lock = asyncio.Lock() async def _wait_for_printer_ready(self, max_wait: int = 300) -> bool: """Wait for the printer to be ready. Args: max_wait: Maximum time to wait in seconds Returns: bool: True if printer is ready, False if timeout """ start_time = time.time() while time.time() - start_time < max_wait: try: async with aiohttp.ClientSession() as session: async with session.get(self.status_url) as response: if response.status == 200: data = await response.json() if data.get('status') == 'ready': return True except Exception as e: logger.warning(f"Error checking printer status: {e}") await asyncio.sleep(1) logger.error("Timeout waiting for printer to be ready") return False async def print_file(self, file_path: Union[str, Path], printer_type: Literal["regular", "label"] = "regular", label_type: Optional[Literal["dk1201", "dk1241"]] = None) -> bool: """Print a PDF or PNG file. Args: file_path: Path to the PDF or PNG file printer_type: Type of printer ("regular" or "label") label_type: Type of label to use ("dk1201" or "dk1241"). Only used when printer_type is "label". Returns: bool: True if print was successful, False otherwise """ try: file_path = Path(file_path) if not file_path.exists(): logger.error(f"File not found: {file_path}") return False if printer_type == "regular": # For regular printers, use CUPS printer = self.printer_name or self.conn.getDefault() if not printer: logger.error("No default printer found") return False job_id = self.conn.printFile( printer, str(file_path), f"{file_path.suffix.upper()} Print", {} ) logger.info(f"Print job {job_id} submitted to printer {printer}") return True else: # For label printers, we need to coordinate requests if label_type is None: logger.error("label_type must be specified when printing to label printer") return False # Wait for printer to be ready if not await self._wait_for_printer_ready(): logger.error("Printer not ready after waiting") return False if file_path.suffix.lower() == '.pdf': # Convert PDF to image first images = convert_from_path(file_path) if not images: logger.error(f"No images could be extracted from {file_path}") return False image = images[0] # Only use first page else: # For PNG files, we can use them directly from PIL import Image image = Image.open(file_path) # Convert to label format qlr = BrotherQLRaster("QL-1100") qlr.exception_on_warning = True # Get label size based on type label_size = "29x90" if label_type == "dk1201" else "102x152" converted_image = convert( qlr=qlr, images=[image], label=label_size, rotate="0", threshold=70.0, dither=False, compress=False, red=False, dpi_600=False, hq=True, cut=True ) # Cache the converted binary data cache_path = self.cache_dir / f"{file_path.stem}_{label_type}_converted.bin" with open(cache_path, "wb") as f: f.write(converted_image) # Send to API return await self._send_print_request(cache_path) except Exception as e: logger.error(f"Error printing file {file_path}: {str(e)}") return False async def _send_print_request(self, file_path: Union[str, Path]) -> bool: """Send print data to printer API. Args: file_path: Path to the binary data file to send to the printer Returns: bool: True if request was successful, False otherwise """ try: # Read the binary data from the cache file with open(file_path, "rb") as f: print_data = f.read() # Send the request to the printer API using aiohttp async with aiohttp.ClientSession() as session: async with session.post( self.printer_api_url, data=print_data, timeout=30 ) as response: if response.status == 200: return True else: response_text = await response.text() logger.error(f"Print request failed with status {response.status}: {response_text}") return False except Exception as e: logger.error(f"Error sending print request: {str(e)}") return False