| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- import logging
- from fastapi import APIRouter, Depends, HTTPException
- from sqlalchemy.ext.asyncio import AsyncSession
- from sqlalchemy import select
- from pydantic import BaseModel
- from backend.app.core.database import get_db
- from backend.app.core.auth import get_api_key, check_permission, check_printer_access
- from backend.app.models.api_key import APIKey
- from backend.app.models.archive import PrintArchive
- from backend.app.models.print_queue import PrintQueueItem
- from backend.app.models.printer import Printer
- from backend.app.services.printer_manager import printer_manager
- logger = logging.getLogger(__name__)
- router = APIRouter(prefix="/webhook", tags=["webhook"])
- # Request schemas
- class QueueAddRequest(BaseModel):
- archive_id: int
- printer_id: int
- project_id: int | None = None
- scheduled_time: str | None = None # ISO format datetime
- require_previous_success: bool = False
- auto_off_after: bool = False
- class QueueAddResponse(BaseModel):
- id: int
- archive_id: int
- printer_id: int
- position: int
- status: str
- message: str
- class PrinterStatusResponse(BaseModel):
- id: int
- name: str
- connected: bool
- state: str | None
- current_print: str | None
- progress: float | None
- remaining_time: int | None
- class QueueStatusResponse(BaseModel):
- printer_id: int
- printer_name: str
- pending: int
- printing: int
- items: list[dict]
- # Webhook endpoints
- @router.post("/queue/add", response_model=QueueAddResponse)
- async def webhook_add_to_queue(
- data: QueueAddRequest,
- api_key: APIKey = Depends(get_api_key),
- db: AsyncSession = Depends(get_db),
- ):
- """Add a print to the queue via webhook.
- Requires 'can_queue' permission.
- """
- check_permission(api_key, 'queue')
- check_printer_access(api_key, data.printer_id)
- # Verify archive exists
- result = await db.execute(
- select(PrintArchive).where(PrintArchive.id == data.archive_id)
- )
- archive = result.scalar_one_or_none()
- if not archive:
- raise HTTPException(status_code=404, detail="Archive not found")
- # Verify printer exists
- result = await db.execute(
- select(Printer).where(Printer.id == data.printer_id)
- )
- printer = result.scalar_one_or_none()
- if not printer:
- raise HTTPException(status_code=404, detail="Printer not found")
- # Get next position
- result = await db.execute(
- select(PrintQueueItem.position)
- .where(
- PrintQueueItem.printer_id == data.printer_id,
- PrintQueueItem.status == "pending",
- )
- .order_by(PrintQueueItem.position.desc())
- .limit(1)
- )
- max_position = result.scalar()
- next_position = (max_position or 0) + 1
- # Parse scheduled time if provided
- scheduled_time = None
- if data.scheduled_time:
- from datetime import datetime
- try:
- scheduled_time = datetime.fromisoformat(data.scheduled_time.replace('Z', '+00:00'))
- except ValueError:
- raise HTTPException(status_code=400, detail="Invalid scheduled_time format")
- # Create queue item
- queue_item = PrintQueueItem(
- printer_id=data.printer_id,
- archive_id=data.archive_id,
- project_id=data.project_id,
- position=next_position,
- scheduled_time=scheduled_time,
- require_previous_success=data.require_previous_success,
- auto_off_after=data.auto_off_after,
- )
- db.add(queue_item)
- await db.flush()
- await db.refresh(queue_item)
- return QueueAddResponse(
- id=queue_item.id,
- archive_id=queue_item.archive_id,
- printer_id=queue_item.printer_id,
- position=queue_item.position,
- status=queue_item.status,
- message=f"Added to queue at position {queue_item.position}",
- )
- @router.post("/printer/{printer_id}/start")
- async def webhook_start_print(
- printer_id: int,
- api_key: APIKey = Depends(get_api_key),
- db: AsyncSession = Depends(get_db),
- ):
- """Start the next queued print on a printer.
- Requires 'can_control_printer' permission.
- """
- check_permission(api_key, 'control_printer')
- check_printer_access(api_key, printer_id)
- # Get printer
- result = await db.execute(select(Printer).where(Printer.id == printer_id))
- printer = result.scalar_one_or_none()
- if not printer:
- raise HTTPException(status_code=404, detail="Printer not found")
- # Get next pending queue item
- result = await db.execute(
- select(PrintQueueItem)
- .where(
- PrintQueueItem.printer_id == printer_id,
- PrintQueueItem.status == "pending",
- )
- .order_by(PrintQueueItem.position)
- .limit(1)
- )
- queue_item = result.scalar_one_or_none()
- if not queue_item:
- raise HTTPException(status_code=404, detail="No pending prints in queue")
- # Check if printer is ready
- status = printer_manager.get_status(printer_id)
- if not status or not status.get("connected"):
- raise HTTPException(status_code=503, detail="Printer not connected")
- if status.get("state") not in ["IDLE", "FINISH", "FAILED"]:
- raise HTTPException(
- status_code=409,
- detail=f"Printer is busy (state: {status.get('state')})"
- )
- # Start the print
- try:
- await printer_manager.start_print(printer_id, queue_item.archive_id)
- except Exception as e:
- logger.error(f"Failed to start print: {e}")
- raise HTTPException(status_code=500, detail=str(e))
- return {"message": "Print started", "queue_item_id": queue_item.id}
- @router.post("/printer/{printer_id}/stop")
- async def webhook_stop_print(
- printer_id: int,
- api_key: APIKey = Depends(get_api_key),
- ):
- """Stop the current print on a printer.
- Requires 'can_control_printer' permission.
- """
- check_permission(api_key, 'control_printer')
- check_printer_access(api_key, printer_id)
- status = printer_manager.get_status(printer_id)
- if not status or not status.get("connected"):
- raise HTTPException(status_code=503, detail="Printer not connected")
- if status.get("state") != "RUNNING":
- raise HTTPException(status_code=409, detail="No print in progress")
- try:
- await printer_manager.stop_print(printer_id)
- except Exception as e:
- logger.error(f"Failed to stop print: {e}")
- raise HTTPException(status_code=500, detail=str(e))
- return {"message": "Print stopped"}
- @router.post("/printer/{printer_id}/cancel")
- async def webhook_cancel_print(
- printer_id: int,
- api_key: APIKey = Depends(get_api_key),
- ):
- """Cancel the current print on a printer.
- Requires 'can_control_printer' permission.
- """
- check_permission(api_key, 'control_printer')
- check_printer_access(api_key, printer_id)
- status = printer_manager.get_status(printer_id)
- if not status or not status.get("connected"):
- raise HTTPException(status_code=503, detail="Printer not connected")
- if status.get("state") not in ["RUNNING", "PAUSE"]:
- raise HTTPException(status_code=409, detail="No print to cancel")
- try:
- await printer_manager.cancel_print(printer_id)
- except Exception as e:
- logger.error(f"Failed to cancel print: {e}")
- raise HTTPException(status_code=500, detail=str(e))
- return {"message": "Print cancelled"}
- @router.get("/printer/{printer_id}/status", response_model=PrinterStatusResponse)
- async def webhook_get_printer_status(
- printer_id: int,
- api_key: APIKey = Depends(get_api_key),
- db: AsyncSession = Depends(get_db),
- ):
- """Get status of a printer.
- Requires 'can_read_status' permission.
- """
- check_permission(api_key, 'read_status')
- check_printer_access(api_key, printer_id)
- # Get printer
- result = await db.execute(select(Printer).where(Printer.id == printer_id))
- printer = result.scalar_one_or_none()
- if not printer:
- raise HTTPException(status_code=404, detail="Printer not found")
- status = printer_manager.get_status(printer_id)
- return PrinterStatusResponse(
- id=printer.id,
- name=printer.name,
- connected=status.get("connected", False) if status else False,
- state=status.get("state") if status else None,
- current_print=status.get("current_print") if status else None,
- progress=status.get("progress") if status else None,
- remaining_time=status.get("remaining_time") if status else None,
- )
- @router.get("/queue", response_model=list[QueueStatusResponse])
- async def webhook_get_queue_status(
- printer_id: int | None = None,
- api_key: APIKey = Depends(get_api_key),
- db: AsyncSession = Depends(get_db),
- ):
- """Get queue status for all printers or a specific printer.
- Requires 'can_read_status' permission.
- """
- check_permission(api_key, 'read_status')
- # Get printers
- if printer_id:
- check_printer_access(api_key, printer_id)
- result = await db.execute(select(Printer).where(Printer.id == printer_id))
- printers = result.scalars().all()
- else:
- result = await db.execute(select(Printer))
- printers = result.scalars().all()
- # Filter by allowed printers if limited
- if api_key.printer_ids:
- printers = [p for p in printers if p.id in api_key.printer_ids]
- response = []
- for printer in printers:
- # Get queue items
- result = await db.execute(
- select(PrintQueueItem)
- .where(
- PrintQueueItem.printer_id == printer.id,
- PrintQueueItem.status.in_(["pending", "printing"]),
- )
- .order_by(PrintQueueItem.position)
- )
- items = result.scalars().all()
- pending_count = sum(1 for i in items if i.status == "pending")
- printing_count = sum(1 for i in items if i.status == "printing")
- response.append(QueueStatusResponse(
- printer_id=printer.id,
- printer_name=printer.name,
- pending=pending_count,
- printing=printing_count,
- items=[
- {
- "id": item.id,
- "archive_id": item.archive_id,
- "position": item.position,
- "status": item.status,
- }
- for item in items
- ],
- ))
- return response
|