diff --git a/backend/beets_flask/server/routes/library/artwork.py b/backend/beets_flask/server/routes/library/artwork.py index 6ef315bb..5c449b2f 100644 --- a/backend/beets_flask/server/routes/library/artwork.py +++ b/backend/beets_flask/server/routes/library/artwork.py @@ -2,7 +2,6 @@ from io import BytesIO from typing import TYPE_CHECKING, cast -from beets import util as beets_util from mediafile import Image, MediaFile # comes with the beets install from PIL import Image as PILImage from quart import ( @@ -22,6 +21,7 @@ InvalidUsageException, NotFoundException, ) +from beets_flask.server.routes.library.path_utils import resolve_library_path if TYPE_CHECKING: # For type hinting the global g object @@ -112,7 +112,7 @@ async def item_art_idx(item_id: int): f"Item with beets_id:'{item_id}' not found in beets db." ) - item_path = beets_util.syspath(item.path) + item_path = resolve_library_path(item.path) count = get_image_count_from_file(item_path) return jsonify({"count": count}), 200 @@ -129,7 +129,7 @@ async def item_art(item_id: int): f"Item with beets_id:'{item_id}' not found in beets db." ) - item_path = beets_util.syspath(item.path) + item_path = resolve_library_path(item.path) img_data = get_image_data_from_file(item_path, idx) return await send_image(img_data, size) @@ -151,7 +151,7 @@ async def album_art(album_id: int): # Has art set on album level if album.artpath and idx == 0: - art_path = beets_util.syspath(album.artpath) + art_path = resolve_library_path(album.artpath) if not os.path.exists(art_path): raise IntegrityException( f"Album art file '{art_path}' does not exist for album beets_id:'{album_id}'." diff --git a/backend/beets_flask/server/routes/library/audio.py b/backend/beets_flask/server/routes/library/audio.py index 2aec5978..c401be28 100644 --- a/backend/beets_flask/server/routes/library/audio.py +++ b/backend/beets_flask/server/routes/library/audio.py @@ -12,13 +12,13 @@ import aiofiles import numpy as np -from beets import util as beets_util from cachetools import Cache, TTLCache from cachetools.keys import hashkey from quart import Blueprint, Response, g from beets_flask.logger import log from beets_flask.server.exceptions import IntegrityException, NotFoundException +from beets_flask.server.routes.library.path_utils import resolve_library_path audio_bp = Blueprint("audio", __name__) @@ -45,7 +45,7 @@ async def item_audio(item_id: int): f"Item with beets_id:'{item_id}' not found in beets db." ) - item_path = beets_util.syspath(item.path) + item_path = resolve_library_path(item.path) if not os.path.exists(item_path): raise IntegrityException( f"Item file '{item_path}' does not exist for item beets_id:'{item_id}'." @@ -70,7 +70,7 @@ async def item_audio_peaks(item_id: int): f"Item with beets_id:'{item_id}' not found in beets db." ) - item_path = beets_util.syspath(item.path) + item_path = resolve_library_path(item.path) if not os.path.exists(item_path): raise IntegrityException( f"Item file '{item_path}' does not exist for item beets_id:'{item_id}'." diff --git a/backend/beets_flask/server/routes/library/metadata.py b/backend/beets_flask/server/routes/library/metadata.py index fdf721f9..bf165355 100644 --- a/backend/beets_flask/server/routes/library/metadata.py +++ b/backend/beets_flask/server/routes/library/metadata.py @@ -7,11 +7,11 @@ from pathlib import Path from typing import TYPE_CHECKING -from beets import util as beets_util from quart import Blueprint, g from tinytag import TinyTag from beets_flask.server.exceptions import IntegrityException, NotFoundException +from beets_flask.server.routes.library.path_utils import resolve_library_path if TYPE_CHECKING: # For type hinting the global g object @@ -36,7 +36,7 @@ async def item_metadata(item_id: int): ) # File path - item_path = beets_util.syspath(item.path) + item_path = resolve_library_path(item.path) if not os.path.exists(item_path): raise IntegrityException( f"Item file '{item_path}' does not exist for item beets_id:'{item_id}'." diff --git a/backend/beets_flask/server/routes/library/path_utils.py b/backend/beets_flask/server/routes/library/path_utils.py new file mode 100644 index 00000000..6225ef1f --- /dev/null +++ b/backend/beets_flask/server/routes/library/path_utils.py @@ -0,0 +1,45 @@ +"""Helpers for resolving paths stored in the beets library. + +Beets can store item and artwork paths relative to the configured library +``directory`` when the files live inside that directory. Beets itself expands +those paths through its library abstractions, but beets-flask sometimes needs a +plain filesystem path for libraries such as mutagen/mediafile, TinyTag, ffmpeg, +or ``os.path.getsize``. + +This module provides a single helper that mirrors Beets' expected behavior for +those call sites: absolute paths are returned unchanged; relative paths are +resolved against the currently-open Beets library directory. +""" + +import os +from os import PathLike +from typing import TYPE_CHECKING + +from beets import util as beets_util +from quart import g + +if TYPE_CHECKING: + from . import g + + +def resolve_library_path(path: str | bytes | PathLike[str] | PathLike[bytes]) -> str: + """Return an absolute filesystem path for a Beets item/artwork path. + + Beets commonly stores paths relative to ``Library.directory``. Directly + passing such a path to file APIs makes it resolve relative to the current + process working directory, which is not necessarily the music library root + inside the beets-flask container. Resolve relative paths against the + active Beets library directory so existing Beets databases with relative + paths work correctly. + """ + + filesystem_path = beets_util.syspath(path) + if os.path.isabs(filesystem_path): + return filesystem_path + + lib = getattr(g, "lib", None) + library_directory = getattr(lib, "directory", None) + if library_directory: + return os.path.join(beets_util.syspath(library_directory), filesystem_path) + + return filesystem_path diff --git a/backend/beets_flask/server/routes/library/resources.py b/backend/beets_flask/server/routes/library/resources.py index c385d096..94a4cb2d 100644 --- a/backend/beets_flask/server/routes/library/resources.py +++ b/backend/beets_flask/server/routes/library/resources.py @@ -30,6 +30,7 @@ from beets_flask.config import get_config from beets_flask.logger import log +from beets_flask.server.routes.library.path_utils import resolve_library_path from beets_flask.server.exceptions import NotFoundException from beets_flask.server.routes.exception import InvalidUsageException from beets_flask.server.utility import pop_query_param @@ -636,7 +637,7 @@ def _repr_Item(item: Item | None, minimal=False) -> ItemResponse | ItemResponseM ] else: # Use all keys - keys = item.keys(True) + ["name"] + keys = [k for k in item.keys(True) if k != "filesize"] + ["name"] # Check data source prefixes: # plugins such as spotify, tidal, discogs add a prefix to the id, @@ -722,7 +723,7 @@ def _repr_Item(item: Item | None, minimal=False) -> ItemResponse | ItemResponseM # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. try: - out["size"] = os.path.getsize(beets_util.syspath(path=item.path)) + out["size"] = os.path.getsize(resolve_library_path(item.path)) except OSError: out["size"] = 0 diff --git a/frontend/src/components/library/coverArt.tsx b/frontend/src/components/library/coverArt.tsx index 89046004..6416d4e3 100644 --- a/frontend/src/components/library/coverArt.tsx +++ b/frontend/src/components/library/coverArt.tsx @@ -182,18 +182,19 @@ function CoverArtFromQuery({ } if (isError) { - if (error instanceof HTTPError) { - return ( - - ); - } else { - throw error; - } + const coverArtError = + error instanceof HTTPError + ? error + : new HTTPError(error?.message || String(error)); + + return ( + + ); } if (art) {