diff --git a/beets/dbcore/pathutils.py b/beets/dbcore/pathutils.py index 16071a3c8..49d76cf2f 100644 --- a/beets/dbcore/pathutils.py +++ b/beets/dbcore/pathutils.py @@ -8,6 +8,15 @@ from beets import context, util MaybeBytes = TypeVar("MaybeBytes", bytes, None) +def _is_same_path_or_child(path: bytes, music_dir: bytes) -> bool: + """Check if path is the music directory itself or resides within it.""" + path_cmp = os.path.normcase(os.fsdecode(path)) + music_dir_cmp = os.path.normcase(os.fsdecode(music_dir)) + return path_cmp == music_dir_cmp or path_cmp.startswith( + os.path.join(music_dir_cmp, "") + ) + + def normalize_path_for_db(path: MaybeBytes) -> MaybeBytes: """Convert an absolute library path to its database representation.""" if not path or not os.path.isabs(path): @@ -17,10 +26,7 @@ def normalize_path_for_db(path: MaybeBytes) -> MaybeBytes: if not music_dir: return path - if path == music_dir: - return os.path.relpath(path, music_dir) - - if path.startswith(os.path.join(music_dir, b"")): + if _is_same_path_or_child(path, music_dir): return os.path.relpath(path, music_dir) return path diff --git a/beets/library/models.py b/beets/library/models.py index 18a7f413c..0c0f88fc3 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -12,8 +12,9 @@ from typing import TYPE_CHECKING, ClassVar from mediafile import MediaFile, UnreadableFileError import beets -from beets import context, dbcore, logging, plugins, util +from beets import dbcore, logging, plugins, util from beets.dbcore import types +from beets.dbcore.pathutils import normalize_path_for_db from beets.util import ( MoveOperation, bytestring_path, @@ -104,19 +105,15 @@ class LibModel(dbcore.Model["Library"]): if ( cls._type(field).query is dbcore.query.PathQuery and query_cls is not dbcore.query.PathQuery - and (music_dir := context.get_music_dir()) ): # Regex, exact, and string queries operate on the raw DB value, so # strip the library prefix to match the stored relative path. if isinstance(pattern, bytes): - prefix = os.path.join(music_dir, b"") - if pattern.startswith(prefix): - pattern = os.path.relpath(pattern, music_dir) + pattern = normalize_path_for_db(pattern) else: - music_dir_str = os.fsdecode(music_dir) - prefix = music_dir_str + os.sep - if pattern.startswith(prefix): - pattern = pattern.removeprefix(prefix) + pattern = os.fsdecode( + normalize_path_for_db(util.bytestring_path(pattern)) + ) if field in cls.shared_db_fields: # This field exists in both tables, so SQLite will encounter # an OperationalError if we try to use it in a query. diff --git a/test/library/test_migrations.py b/test/library/test_migrations.py index 34710f67f..53c52fa74 100644 --- a/test/library/test_migrations.py +++ b/test/library/test_migrations.py @@ -161,7 +161,7 @@ class TestRelativePathMigration: helper.teardown_beets() def test_migrate(self, helper: TestHelper): - relative_path = "foo/bar/baz.mp3" + relative_path = os.path.join("foo", "bar", "baz.mp3") absolute_path = os.fsencode(helper.lib_path / relative_path) # need to insert the path directly into the database to bypass the path setter diff --git a/test/plugins/test_ipfs.py b/test/plugins/test_ipfs.py index 5481b8585..00d6f0512 100644 --- a/test/plugins/test_ipfs.py +++ b/test/plugins/test_ipfs.py @@ -15,9 +15,9 @@ import os from unittest.mock import Mock, patch +from beets import util from beets.test import _common from beets.test.helper import PluginTestCase -from beets.util import bytestring_path from beetsplug.ipfs import IPFSPlugin @@ -37,10 +37,9 @@ class IPFSPluginTest(PluginTestCase): try: if check_item.get("ipfs", with_album=False): ipfs_item = os.fsdecode(os.path.basename(want_item.path)) - want_path = os.path.normpath( - f"/ipfs/{test_album.ipfs}/{ipfs_item}" + want_path = util.normpath( + os.path.join("/ipfs", test_album.ipfs, ipfs_item) ) - want_path = bytestring_path(want_path) assert check_item.path == want_path assert ( check_item.get("ipfs", with_album=False) diff --git a/test/test_library.py b/test/test_library.py index 274d2dea6..3f08feaeb 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -1094,7 +1094,7 @@ class PathStringTest(BeetsTestCase): assert isinstance(dest, bytes) def test_artpath_stores_special_chars(self): - path = b"b\xe1r" + path = bytestring_path("b\xe1r") alb = self.lib.add_album([self.i]) alb.artpath = path alb.store() @@ -1131,7 +1131,7 @@ class PathStringTest(BeetsTestCase): assert isinstance(alb.artpath, bytes) def test_relative_path_is_stored(self): - relative_path = b"abc/foo.mp3" + relative_path = os.path.join(b"abc", b"foo.mp3") absolute_path = os.path.join(self.libdir, relative_path) self.i.path = absolute_path self.i.store()