diff --git a/mediapreview/backends/__init__.py b/mediapreview/backends/__init__.py index 9b72834..756cd98 100644 --- a/mediapreview/backends/__init__.py +++ b/mediapreview/backends/__init__.py @@ -34,7 +34,7 @@ def dispatch(path, quality, maxsize, maxzoom, data=None): backend = "unknown" try: if data: - backend = "pyvips" + backend = "vips" return process_image_buffer( data, quality=quality, maxsize=maxsize, maxzoom=maxzoom ) @@ -47,11 +47,11 @@ def dispatch(path, quality, maxsize, maxzoom, data=None): backend = "video" return process_video(path, quality=quality, maxsize=maxsize) if mime_type and mime_type.startswith("image/"): - backend = "pyvips" + backend = "vips" return process_image(path, quality=quality, maxsize=maxsize) except PreviewError: - # Already structured (e.g. a stage of a combined pipeline like - # pdf+pyvips) — keep the original backend/stage identity. + # Already structured (e.g. a failing stage of a combined pipeline + # like pdf+vips) — keep the original backend identity. raise except ValueError as e: raise backend_error(backend, str(e)) from e diff --git a/mediapreview/backends/image.py b/mediapreview/backends/image.py index e693823..37218d2 100644 --- a/mediapreview/backends/image.py +++ b/mediapreview/backends/image.py @@ -148,7 +148,7 @@ def process_image_pyvips(path, *, maxsize, quality): ) except pyvips.error.Error as e: raise ValueError(f"cannot decode image: {e}") from e - backend = "pyvips" + backend = "vips" t_end = perf_counter() return ret, PreviewResponse( @@ -181,7 +181,7 @@ def process_image_buffer(data: bytes, *, quality, maxsize, maxzoom): return ret, PreviewResponse( ok=True, mime="image/avif", - backend="pyvips", + backend="vips", timings=[round((t_end - t_start) * 1000, 1)], width=orig_w, height=orig_h, diff --git a/mediapreview/backends/pdf.py b/mediapreview/backends/pdf.py index 87f037b..d99289f 100644 --- a/mediapreview/backends/pdf.py +++ b/mediapreview/backends/pdf.py @@ -1,4 +1,4 @@ -"""PDF/XPS/EPUB preview conversion via PyMuPDF + pyvips.""" +"""PDF/XPS/EPUB preview conversion via PyMuPDF + vips.""" from time import perf_counter @@ -13,7 +13,7 @@ try: except ImportError: # pragma: no cover - optional pdf extra pymupdf = None -BACKEND = "pdf+pyvips" +BACKEND = "pdf+vips" def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0): @@ -31,7 +31,8 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0): pix = page.get_pixmap(matrix=mat) samples, width, height, n = pix.samples_mv, pix.width, pix.height, pix.n except Exception as e: - raise backend_error(BACKEND, str(e), stage="pdf") from e + # vips was never reached — this is a plain pdf error. + raise backend_error("pdf", str(e)) from e t_load_end = perf_counter() t_save_start = perf_counter() @@ -39,7 +40,7 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0): img = pyvips.Image.new_from_memory(samples, width, height, n, "uchar") ret = img.write_to_buffer(".avif", Q=quality, effort=AVIF_FAST_EFFORT, keep="none") except Exception as e: - raise backend_error(BACKEND, str(e), stage="pyvips") from e + raise backend_error(BACKEND, str(e)) from e t_save_end = perf_counter() return ret, PreviewResponse( diff --git a/mediapreview/exceptions.py b/mediapreview/exceptions.py index e343733..6fa5821 100644 --- a/mediapreview/exceptions.py +++ b/mediapreview/exceptions.py @@ -17,9 +17,10 @@ The hierarchy is intentionally small: - ``OnlyOfficeError`` covers all OnlyOffice failures; optional fields (``code``, ``status``, ``url``, ``snippet``) describe the specific failure. -- ``PreviewBackendError`` covers backend conversion failures (ffmpeg, pyvips, - pdf, etc.); ``stage`` identifies the failing step of a combined pipeline - (e.g. "pdf" vs "pyvips" in the "pdf+pyvips" backend). +- ``PreviewBackendError`` covers backend conversion failures (ffmpeg, vips, + pdf, etc.). Combined pipelines report the failing step in ``backend`` + (e.g. "pdf" if pdf reading failed before vips was reached, "pdf+vips" + for a vips write failure). - ``PreviewTimeoutError`` covers timeouts for any backend. - ``PreviewCancelledError`` covers cancellations (e.g. pool shutdown). @@ -80,17 +81,6 @@ class OnlyOfficeError(PreviewError): class PreviewBackendError(PreviewError): """Backend conversion failure (image/video/pdf/etc).""" - def __init__( - self, - message: str = "preview failed", - short: str = "error", - *, - stage: str | None = None, - backend: str | None = None, - ): - super().__init__(message, short, backend=backend) - self.stage = stage - class PreviewTimeoutError(PreviewError): """Preview conversion exceeded its timeout for a given backend.""" @@ -163,13 +153,12 @@ def onlyoffice_no_fileurl_error(snippet: str | None = None) -> OnlyOfficeError: return OnlyOfficeError(log, "no-fileurl error", snippet=snippet) -def backend_error(backend: str, message: str, *, stage: str | None = None) -> PreviewBackendError: +def backend_error(backend: str, message: str) -> PreviewBackendError: short = message.splitlines()[0][:60] return PreviewBackendError( f"[{backend}] preview failed: {message}", short, backend=backend, - stage=stage, ) diff --git a/mediapreview/formats.py b/mediapreview/formats.py index b188158..2b8eccf 100644 --- a/mediapreview/formats.py +++ b/mediapreview/formats.py @@ -90,5 +90,5 @@ def expected_backend(path: Path) -> str: if mime_type and mime_type.startswith("video/"): return "video" if mime_type and mime_type.startswith("image/"): - return "pyvips" + return "vips" return "preview" diff --git a/mediapreview/pool.py b/mediapreview/pool.py index 0bcfe70..f095251 100644 --- a/mediapreview/pool.py +++ b/mediapreview/pool.py @@ -477,7 +477,7 @@ async def generate_office_preview( img, resp = await run_preview(filepath, quality, maxsize, maxzoom, data=png_bytes) if resp is not None: - resp.backend = "onlyoffice+" + (resp.backend or "pyvips") + resp.backend = "onlyoffice+" + (resp.backend or "vips") if resp.timings: resp.timings = [round((t_oo_end - t_oo_start) * 1000, 1), *resp.timings] return img, resp diff --git a/mediapreview/worker.py b/mediapreview/worker.py index 9462e32..280a9ff 100644 --- a/mediapreview/worker.py +++ b/mediapreview/worker.py @@ -38,6 +38,7 @@ except ImportError: # pragma: no cover - optional worker extra sys.exit(1) from mediapreview.backends import dispatch +from mediapreview.exceptions import PreviewError from mediapreview.protocol import PreviewRequest, PreviewResponse from mediapreview.util.logformat import format_level_prefix @@ -140,7 +141,13 @@ def _run_loop() -> None: ) _write_response(resp, result or b"") except Exception as e: - logger.exception("Preview worker error for %s", req.path) + # PreviewError is an expected failure (broken input, missing + # extra, backend error) — a warning suffices. Tracebacks are + # reserved for internal errors we did not anticipate. + if isinstance(e, PreviewError): + logger.warning("Preview failed for %s: %s", req.path, e) + else: + logger.exception("Preview worker error for %s", req.path) captured = stderr_capture.getvalue().strip() _write_response( PreviewResponse( diff --git a/tests/test_office.py b/tests/test_office.py index 27bde4a..de0bee0 100644 --- a/tests/test_office.py +++ b/tests/test_office.py @@ -64,11 +64,10 @@ def test_onlyoffice_no_fileurl_error(): assert err.short == "no-fileurl error" -def test_backend_error_stage(): - """Combined pipelines tag the failing stage.""" - err = backend_error("pdf+pyvips", "cannot read document", stage="pdf") - assert err.backend == "pdf+pyvips" - assert err.stage == "pdf" +def test_backend_error_pipeline_backend(): + """Combined pipelines report the failing step in the backend name.""" + err = backend_error("pdf", "cannot read document") + assert err.backend == "pdf" assert err.short == "cannot read document" assert isinstance(err, PreviewBackendError) diff --git a/tests/test_previews.py b/tests/test_previews.py index e9f35b4..c420958 100644 --- a/tests/test_previews.py +++ b/tests/test_previews.py @@ -56,7 +56,7 @@ def _assert_ok(data, resp, backend: str | None = None) -> None: def test_process_image_exif_orientations(path: Path) -> None: """Every EXIF orientation fixture must produce a valid preview.""" data, resp = process_image(path, maxsize=512, quality=60) - _assert_ok(data, resp, backend="pyvips") + _assert_ok(data, resp, backend="vips") assert resp.width in (1200, 1800) assert resp.height in (1200, 1800) @@ -74,7 +74,7 @@ def test_process_image_pyvips() -> None: """The pyvips-only image backend works on a plain JPEG.""" path = FILES / "Landscape_1.jpg" data, resp = process_image_pyvips(path, maxsize=512, quality=60) - _assert_ok(data, resp, backend="pyvips") + _assert_ok(data, resp, backend="vips") def test_process_image_buffer() -> None: @@ -83,7 +83,7 @@ def test_process_image_buffer() -> None: data, resp = process_image_buffer( path.read_bytes(), maxsize=512, quality=60, maxzoom=2.0 ) - _assert_ok(data, resp, backend="pyvips") + _assert_ok(data, resp, backend="vips") # --------------------------------------------------------------------------- @@ -127,7 +127,7 @@ def test_process_pdf() -> None: maxzoom=2.0, quality=60, ) - _assert_ok(data, resp, backend="pdf+pyvips") + _assert_ok(data, resp, backend="pdf+vips") assert resp.width == 595 assert resp.height == 842 @@ -138,9 +138,9 @@ def test_process_pdf() -> None: DISPATCH_FIXTURES = [ - ("Landscape_1.jpg", "pyvips", 1800, 1200), + ("Landscape_1.jpg", "vips", 1800, 1200), ("sample-1mb.mp4", "video", 854, 480), - ("sample.pdf", "pdf+pyvips", 595, 842), + ("sample.pdf", "pdf+vips", 595, 842), ]