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) {