Store relative paths in the DB to make beets library portable#6460
Store relative paths in the DB to make beets library portable#6460
Conversation
|
Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry. |
7fb98e0 to
68d0d21
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #6460 +/- ##
==========================================
+ Coverage 70.56% 70.67% +0.11%
==========================================
Files 148 150 +2
Lines 18812 18916 +104
Branches 3065 3078 +13
==========================================
+ Hits 13274 13369 +95
- Misses 4891 4896 +5
- Partials 647 651 +4
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
PR make beets store item/album path in DB as library-relative, then expand back to absolute on read. Goal: portable DB when music folder move.
Changes:
- Add context var for active music dir, used by DB path encode/decode.
- Move path normalize/expand into
dbcoretypes + query layer, plus add migration to rewrite old rows. - Propagate context into thread pools/pipeline; update tests to expect absolute paths on public API.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
beets/context.py |
Add context var for music dir. |
beets/library/library.py |
Set music dir context early; register new migration. |
beets/dbcore/pathutils.py |
New helpers to normalize/expand paths for DB. |
beets/dbcore/types.py |
PathType encode/decode moved into DB type layer. |
beets/dbcore/query.py |
PathQuery now compares using DB-relative representation. |
beets/library/models.py |
Adjust non-PathQuery path field queries to match stored relative value. |
beets/library/migrations.py |
Add RelativePathMigration; add chunk size constant usage. |
beets/dbcore/db.py |
Add base migration chunk size constant. |
beets/util/pipeline.py |
Copy context into pipeline worker threads. |
beets/util/__init__.py |
Propagate context into par_map thread pool. |
beetsplug/replaygain.py |
Wrap pool work + callbacks in copied context. |
beetsplug/ipfs.py |
Create temp lib with /ipfs/ dir; store item paths relative to that root. |
beets/ui/__init__.py |
Override library DB path from BEETS_LIBRARY env var. |
test/test_library.py |
Update path expectations; assert raw stored relative paths. |
test/test_query.py |
Add query test for absolute input path. |
test/library/test_migrations.py |
Add migration test for absolute->relative rewrite. |
test/ui/test_ui.py |
Update ls -p output expectations to absolute paths. |
test/ui/commands/test_list.py |
Update $path output expectations to absolute paths. |
test/plugins/test_ipfs.py |
Normalize expected /ipfs/... path string. |
test/test_files.py |
Remove one remove/prune test (no replacement in this PR). |
0e2a815 to
65f87ba
Compare
4f52af6 to
c1b11aa
Compare
4473908 to
8637db7
Compare
318f2fd to
9a66310
Compare
|
I would love to find some time in the following days to look into this in detail! Have you considered moving a library between operating systems yet? I could see that as one absolutly killer side effects for relative paths. Think portable external drive. If you havent thought about it yet, we might want to test that paths are normalized correctly between Windows and Posix. |
I hope you mean it's a positive side effect! Well, this new logic consistently uses Any ideas how could we possibly test it? |
|
Yeah was meant in a positive way 😅 No idea how to tests it other than writting an example db in either operating system and trying to load it in the other. Im pretty sure slashes will not be converted/normalized correctly by using os.path. I guess this is one of the reason Pathlib was born. This is by no means a blocker tho but maybe a good followup enhancement. Just a thing I was wondering as it might be a personal usecase for me which required manually migrating/rewriting paths in the database beforehand. |
|
Someone already responded that they will give it a try because they have access to both file systems. I do want to get it right here before merging - if that means replacing |
|
Now that I think about it, I think we should actually be able to test it - it's just a matter of patching |
7f48736 to
d7ccf01
Compare
d7ccf01 to
2e39c0e
Compare
|
@semohr the library should now be portable between different file systems. |
1b42ca6 to
1750eab
Compare
|
@semohr would you mind to approve it if you're happy with it? |
Convert item paths to relative on write and back to absolute on read, keeping the database free of hardcoded library directory. Fix tests to account for absolute path return values.
Move path relativization/expansion logic from Item._setitem/__getitem__ into dbcore layer (PathType.to_sql/from_sql and PathQuery), so all models benefit without per-model overrides. Propagate contextvars to pipeline and replaygain pool threads so the library root context variable is available during background processing.
Store paths relative to the music directory in the database instead of absolute paths. Add RelativePathMigration to handle existing absolute paths in `path` and `artpath` fields on startup. Also move `self.directory` assignment before `super().__init__()` so the migration can access it.
1750eab to
a135d1d
Compare
a135d1d to
a461ccb
Compare
Migrate Item & Album Paths to Library-Relative Storage
Fixes: #133
Core Problem Solved
Before: Beets stored absolute file system paths in SQLite (e.g.
/home/user/Music/Artist/album/track.mp3). This made library databases non-portable — moving the music directory or sharing a database across machines broke all path references.After: Paths are stored relative to the music directory (e.g.
Artist/album/track.mp3), and expanded back to absolute paths transparently on read. The database is now portable.Architecture Changes
1. Context Variable for Music Directory (
beets/context.py)A new
contextvars.ContextVar(_music_dir_var) holds the active music directory, set once duringLibrary.__init__viacontext.set_music_dir(). This avoids passinglibrary.directorythrough every call stack.2. Path Relativization Moved to the DB Layer (
beets/dbcore/)Previously, path conversion lived in
Item._setitem/Item.__getitem__— model-specific overrides. It is now pushed down intoPathType.to_sql/PathType.from_sqlindbcore/types.py, through two helpers in the newbeets/dbcore/pathutils.py:normalize_path_for_db(path)expand_path_from_db(path)All models using
PathType(currentlyItemandAlbum) benefit automatically — no per-model overrides required.3.
PathQueryUpdated for Relative Storage (beets/dbcore/query.py)Queries like
path:/home/user/Music/Artistnow normalize the search term to its relative form before hitting the database, so SQL comparisons match stored values correctly. Bothcol_clause(SQL path) andmatch(in-memory path) usenormalize_path_for_db.4. One-Time Database Migration (
RelativePathMigration)Existing absolute paths in
pathandartpathcolumns are migrated on startup:self.directoryassignment was moved beforesuper().__init__()inLibrary.__init__so the migration can access the music dir when it runs.5. Context Propagation to Background Threads
The music dir context variable must be available in worker threads (pipeline stages, replaygain pool). Two propagation points were added:
beets/util/pipeline.py:Pipeline.run_parallel()snapshots the calling context withcontextvars.copy_context()and passes a per-thread copy to eachPipelineThread. Each stage coroutine is invoked viactx.run(...).beetsplug/replaygain.py: Pool workers and their callbacks are wrapped inctx.run(...)soexpand_path_from_dbworks correctly inside the process pool.beets/util/__init__.py:par_mapsimilarly propagates context into its thread pool workers.Data Flow: Read & Write Path
Key Invariants
item.pathalways returns an absolutebytespath.beetsplug/ipfs.pyfix)./) are skipped.