Fix unique_path counter for names ending in two or more digits (#6875)

## Problem

`unique_path` parses a trailing counter so it can continue from it, but
the pattern is `\.(\d)+$` — a single-digit group repeated, rather than a
multi-digit group. `group(1)` is therefore only the final digit, and the
counter restarts from it:

```python
>>> # with only track.10.mp3 present
>>> unique_path("track.10.mp3")
'track.1.mp3'          # expected track.11.mp3
```

```
base           re.search(rb"\.(\d)+$", base).group(1)
song.9    ->   b'9'    -> 9    ✓
song.10   ->   b'0'    -> 0    ✗
song.12   ->   b'2'    -> 2    ✗
song.123  ->   b'3'    -> 3    ✗
```

The returned path still does not exist, so nothing is overwritten — but
the new name sorts *before* the file it was derived from, and when the
low numbers are already taken the loop rescans them from scratch instead
of continuing past the existing counter. `unique_path` is used for art
and item destinations in `beets/library/models.py`.

Existing coverage only exercised a single-digit counter (`x.1.mp3`),
which is why this held.

## Fix

`\.(\d)+$` → `\.(\d+)$`.

Changelog entry added under *Bug fixes*. I did not open an issue first,
so there is no `🐛` reference — happy to file one and add the
reference if you prefer that.
This commit is contained in:
Šarūnas Nejus
2026-07-30 11:58:05 +01:00
committed by GitHub
3 changed files with 10 additions and 1 deletions

View File

@@ -644,7 +644,7 @@ def unique_path(path: AnyStr) -> AnyStr:
byte_path = os.fsencode(path)
base, ext = os.path.splitext(byte_path)
match = re.search(rb"\.(\d)+$", base)
match = re.search(rb"\.(\d+)$", base)
if match:
num = int(match.group(1))
base = base[: match.start()]

View File

@@ -19,6 +19,10 @@ Bug fixes
- A date range query written back to front (for example ``added:2024..2020``) no
longer crashes with an uncaught ``ValueError``. The endpoints are now swapped,
so such a range means the same as ``added:2020..2024``.
- Deduplicating a file whose name already ends in a counter of two or more
digits no longer restarts the numbering: ``track.10.mp3`` now yields
``track.11.mp3`` instead of ``track.1.mp3``. The counter was matched with
``\.(\d)+$``, which captures only the final digit.
..
For plugin developers

View File

@@ -428,6 +428,11 @@ class UniquePathTest(BeetsTestCase):
path = util.unique_path(self.base / "x.1.mp3")
assert path == str(self.base / "x.3.mp3")
def test_conflicting_file_with_multi_digit_number_increases_number(self):
(self.base / "w.10.mp3").touch()
path = util.unique_path(self.base / "w.10.mp3")
assert path == str(self.base / "w.11.mp3")
class MkDirAllTest(BeetsTestCase):
def test_mkdirall(self):