Inconsistent handling of reimporting, causing data loss, when the music dir is under a symlinked parent
- Dominant language
- Python
- Stars
- 15.7k
- Forks
- 2.1k
- Avg merge
- 4d 21h
- Merged PRs (30d)
- 31
Description
When the lib music dir is under a symlinked parent and reimported using not the realpath, the duplicate prompt will wrongly appear. And if `import.move` is on and choosing remove old, this will cause the music file be deleted.
FYI: on mac the `/tmp` is just a symlink to `/private/tmp`. This is stably reproducable as long as the music dir is under any symlinked dir.
Possible reason: we don't resolve symlinks here.
https://github.com/beetbox/beets/blob/b7993d19acca7aaf19b9bb4a7c6558cc38016cfd/beets/importer/tasks.py#L582-L593
### Problem
Running this command in verbose (`-vv`) mode:
```sh
/tmp/beets_test $ beet -vv imp -s ./music/Non-Album/Aimer/蝶々結び.mp3
```
Led to this problem:
```
configuration sources (highest → lowest priority):
YamlSource /tmp/beets_test/config.yaml
YamlSource /Users/proitheus/.cache/uv/archive-v0/glF2ru6xwIPcyNHl/lib/python3.14/site-packages/beets/config_default.yaml
data directory: /tmp/beets_test
plugin paths: []
Loading plugins: musicbrainz
Sending event: pluginload
library database: /tmp/beets_test/library.db
library directory: /tmp/beets_test/music
Sending event: library_opened
Sending event: import_begin
Sending event: import_task_created
Sending event: import_task_start
Looking up: /private/tmp/beets_test/music/Non-Album/Aimer/蝶々結び.mp3
Searching for track IDs: c13ab0f2-dd84-4d18-8ec4-fc6c53232b22
Sending event: mb_track_extract
Sending event: trackinfo_received
Track ID match.
/private/tmp/beets_test/music/Non-Album/Aimer/蝶々結び.mp3
Sending event: before_choose_candidate
Match (100.0%):
Aimer - 蝶々結び
MusicBrainz, Index None, Track None,
https://musicbrainz.org/recording/c13ab0f2-dd84-4d18-8ec4-fc6c53232b22
* Artist: Aimer
* Track: 蝶々結び
Sending event: import_task_choice
found duplicates: [1]
default action for duplicates: a
This item is already in the library!
Old: MP3, 175kbps, 5:05, 6.5 MiB
New: MP3, 175kbps, 5:07, 6.5 MiB
➜ [S]kip new, Merge all, Remove old, Keep all? r
Sending event: import_task_apply
0 of 1 items replaced
Sending event: database_change
removing 1 old duplicated items
Sending event: database_change
Sending event: item_removed
deleting duplicate /tmp/beets_test/music/Non-Album/Aimer/蝶々結び.mp3
Parsed query: AndQuery([NoneQuery('album_id', True)])
Parsed sort: NullSort()
Moving: file not found at /private/tmp/beets_test/music/Non-Album/Aimer/蝶々結び.mp3, skipping
Sending event: write
error reading /private/tmp/beets_test/music/Non-Album/Aimer/蝶々結び.mp3: [Errno 2] No such file or directory: '/private/tmp/beets_test/music/Non-Album/Aimer/蝶々結び.mp3'
Sending event: database_change
Sending event: import_task_files
Sending event: item_imported
Sending event: import
Sending event: cli_exit
```
Now the file has been DELETED, maybe causing real data loss.
Here's a link to the music files that trigger the bug (if relevant):
not relevant
#### Full Repro
This is done via `BEETSDIR` envvar isolation. The music file is pre-imported into the lib. From which path the music is initially imported is not much relevant, since it doesn't change the bug behaviour.
```
$ cd /tmp/beets_test
$ export BEETSDIR=$(pwd)
$ beet imp -s music/Non-Album/Aimer/蝶々結び.mp3 <- the -vv cmd above, cause trouble
```
```
$ cd /private/tmp/beets_test <- note the difference, here is the realpath
$ export BEETSDIR=$(pwd)
$ beet imp -s music/Non-Album/Aimer/蝶々結び.mp3 <- No prompts appear, beets correctly detects this is a reimport, nothing strange happens
```
Using absolute path under `/tmp` will also trigger this bug, while under `/private/tmp`, the realpath, will not.
### Setup
* OS: macOS Tahoe 26.5.2
* Python version: 3.14.3
* beets version: 2.13.1
* Turning off plugins made problem go away (yes/no): no
My configuration (output of `beet config`) is:
```yaml
import:
move: yes
directory: music
disabled_plugins: []
musicbrainz:
search_limit: 5
data_source_mismatch_penalty: 0.5
search_query_ascii: no
genres: no
genres_tag: genre
external_ids:
discogs: no
bandcamp: no
spotify: no
deezer: no
tidal: no
extra_tags: []
aliases_as_credits: no
```
Contributor guide
Research direction
Start with beets/importer/tasks.py around lines 582-593 and reproduce the import under /tmp versus /private/tmp using the commands in the issue. Trace how paths are compared during duplicate detection and removal. Done means reimporting through a symlinked parent does not show a false duplicate or delete the music file when import.move is enabled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100