diff --git a/API.md b/API.md index 9ed301c7..57d30de0 100644 --- a/API.md +++ b/API.md @@ -59,7 +59,6 @@ This document describes the available endpoints and their usage. All endpoints r - [GET /api/player/subtitle/{file:.\*}.vtt](#get-apiplayersubtitlefilevtt) - [GET /api/player/subtitles/manifest/{file:.\*}](#get-apiplayersubtitlesmanifestfile) - [GET /api/player/subtitles/{source\_format}/{file:.\*}](#get-apiplayersubtitlessource_formatfile) - - [GET /api/thumbnail](#get-apithumbnail) - [GET /api/file/ffprobe/{file:.\*}](#get-apifileffprobefile) - [GET /api/file/info/{file:.\*}](#get-apifileinfofile) - [GET /api/file/browser/{path:.\*}](#get-apifilebrowserpath) @@ -1655,17 +1654,6 @@ Binary TS data (`Content-Type: video/mpegts`). --- -### GET /api/thumbnail -**Purpose**: Proxy/fetch a remote thumbnail image. - -**Query Parameter**: -- `?url=` - -**Response**: -Binary image data with the appropriate `Content-Type`. - ---- - ### GET /api/file/ffprobe/{file:.*} **Purpose**: Return the `ffprobe` data for a local file. diff --git a/app/routes/api/images.py b/app/routes/api/images.py index 6abeca72..ebb55af8 100644 --- a/app/routes/api/images.py +++ b/app/routes/api/images.py @@ -1,8 +1,5 @@ -import asyncio import logging import random -import time -from datetime import UTC, datetime from typing import Any from urllib.parse import urlparse, urlsplit, urlunsplit @@ -15,7 +12,6 @@ from app.library.cache import Cache from app.library.config import Config from app.library.httpx_client import Globals, build_request_headers, get_async_client, resolve_curl_transport from app.library.router import route -from app.library.Utils import validate_url LOG: logging.Logger = logging.getLogger(__name__) @@ -43,72 +39,6 @@ def _safe_url(url: str | None) -> str: return urlunsplit((parsed.scheme, netloc, parsed.path, query, fragment)) -@route("GET", "api/thumbnail/", "get_thumbnail") -async def get_thumbnail(request: Request, config: Config) -> Response: - """ - Get the thumbnail. - - Args: - request (Request): The request object. - config (Config): The configuration object. - - Returns: - Response: The response object. - - """ - url: str | None = request.query.get("url") - if not url: - return web.json_response(data={"error": "URL is required."}, status=web.HTTPForbidden.status_code) - - try: - await asyncio.to_thread(validate_url, url, config.allow_internal_urls) - except ValueError as e: - return web.json_response(data={"error": str(e)}, status=web.HTTPForbidden.status_code) - - try: - ytdlp_args: dict = YTDLPOpts.get_instance().preset(name=config.default_preset).get_all() - use_curl = resolve_curl_transport() - request_headers = build_request_headers( - user_agent=request.headers.get("User-Agent", ytdlp_args.get("user_agent", Globals.get_random_agent())), - use_curl=use_curl, - ) - proxy = ytdlp_args.get("proxy") - safe_url = _safe_url(url) - - client = get_async_client(proxy=proxy, use_curl=use_curl) - LOG.debug(f"Fetching thumbnail from '{safe_url}'.") - response = await client.request( - method="GET", - url=url, - follow_redirects=True, - headers=request_headers, - timeout=10.0, - ) - - if response.status_code != web.HTTPOk.status_code: - LOG.error(f"Failed to fetch thumbnail from '{safe_url}'. Status code: {response.status_code}.") - return web.json_response(data={"error": "failed to retrieve the thumbnail."}, status=response.status_code) - - return web.Response( - body=response.content, - headers={ - "Content-Type": response.headers.get("Content-Type"), - "Pragma": "public", - "Access-Control-Allow-Origin": "*", - "Cache-Control": f"public, max-age={time.time() + 31536000}", - "Expires": time.strftime( - "%a, %d %b %Y %H:%M:%S GMT", - datetime.fromtimestamp(time.time() + 31536000, tz=UTC).timetuple(), - ), - }, - ) - except Exception as e: - LOG.error(f"Error fetching thumbnail from '{safe_url}'. '{e}'.") - return web.json_response( - data={"error": "failed to retrieve the thumbnail."}, status=web.HTTPInternalServerError.status_code - ) - - @route("GET", "api/random/background/", "get_background") async def get_background(request: Request, config: Config, cache: Cache) -> Response: """ diff --git a/app/tests/test_images_routes.py b/app/tests/test_images_routes.py index 417b5209..c8d4ea1d 100644 --- a/app/tests/test_images_routes.py +++ b/app/tests/test_images_routes.py @@ -26,78 +26,6 @@ class _Resp: self.headers = {"Content-Type": content_type} -@pytest.mark.asyncio -async def test_thumb_thread(monkeypatch: pytest.MonkeyPatch) -> None: - config = Config.get_instance() - req = make_mocked_request("GET", "/api/thumbnail?url=https://example.com/a.jpg") - req._rel_url = req._rel_url.with_query({"url": "https://example.com/a.jpg"}) - - seen = {"to_thread": False, "validate": False} - - def fake_validate_url(url: str, allow_internal: bool = False) -> bool: - seen["validate"] = True - assert url == "https://example.com/a.jpg" - assert allow_internal is config.allow_internal_urls - return True - - async def fake_to_thread(func, *args, **kwargs): - seen["to_thread"] = True - return func(*args, **kwargs) - - client = AsyncMock() - client.request.return_value = _Resp() - - monkeypatch.setattr(images, "validate_url", fake_validate_url) - monkeypatch.setattr(images.asyncio, "to_thread", fake_to_thread) - monkeypatch.setattr(images, "get_async_client", lambda **_kwargs: client) - monkeypatch.setattr(images, "resolve_curl_transport", lambda: False) - monkeypatch.setattr(images, "build_request_headers", lambda **_kwargs: {}) - monkeypatch.setattr(images.Globals, "get_random_agent", staticmethod(lambda: "agent")) - monkeypatch.setattr( - images.YTDLPOpts, - "get_instance", - staticmethod( - lambda: type( - "Opts", - (), - { - "preset": lambda self, name: self, - "get_all": lambda self: {}, - }, - )() - ), - ) - - response = await images.get_thumbnail(req, config) - - assert response.status == web.HTTPOk.status_code - assert seen["to_thread"] is True - assert seen["validate"] is True - client.request.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_thumb_reject(monkeypatch: pytest.MonkeyPatch) -> None: - config = Config.get_instance() - req = make_mocked_request("GET", "/api/thumbnail?url=https://bad.example/a.jpg") - req._rel_url = req._rel_url.with_query({"url": "https://bad.example/a.jpg"}) - - def fake_validate_url(_url: str, allow_internal: bool = False) -> bool: - assert allow_internal is config.allow_internal_urls - raise ValueError("Invalid hostname.") - - async def fake_to_thread(func, *args, **kwargs): - return func(*args, **kwargs) - - monkeypatch.setattr(images, "validate_url", fake_validate_url) - monkeypatch.setattr(images.asyncio, "to_thread", fake_to_thread) - - response = await images.get_thumbnail(req, config) - - assert response.status == web.HTTPForbidden.status_code - assert response.text == '{"error": "Invalid hostname."}' - - @pytest.mark.asyncio async def test_bg_log_redact(caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: config = Config.get_instance() diff --git a/ui/app/components/GetInfo.vue b/ui/app/components/GetInfo.vue index 8b2b6898..e0f1238e 100644 --- a/ui/app/components/GetInfo.vue +++ b/ui/app/components/GetInfo.vue @@ -1,201 +1,124 @@ diff --git a/ui/app/pages/browser/[...slug].vue b/ui/app/pages/browser/[...slug].vue index 4a129808..3fca59c5 100644 --- a/ui/app/pages/browser/[...slug].vue +++ b/ui/app/pages/browser/[...slug].vue @@ -491,7 +491,7 @@ - - + + diff --git a/ui/app/utils/index.ts b/ui/app/utils/index.ts index 8b846f47..49655989 100644 --- a/ui/app/utils/index.ts +++ b/ui/app/utils/index.ts @@ -873,7 +873,7 @@ const getPath = (basePath: string, item: StoreItem): string => { const getRemoteImage = (item: StoreItem, fallback: boolean = true): string => { if (item?.extras?.thumbnail) { - return uri('/api/thumbnail?id=' + item._id + '&url=' + encodePath(item.extras.thumbnail)); + return uri(item.extras.thumbnail); } return fallback ? uri('/images/placeholder.png') : '';