beetbox / beetbox/beets

Inconsistent handling of reimporting, causing data loss, when the music dir is under a symlinked parent

Open
#6,930 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.