Commit graph

13430 commits

Author SHA1 Message Date
Jesse Pinkman
7002cbdda2
Merge branch 'master' into add-mp3rgain-support 2026-01-18 11:00:38 +01:00
m_igashi
545e7eb0b6 refactor: simplify CommandBackend and improve documentation
- Remove auto-detection of command tools, require explicit command config
- Simplify __init__ method by removing redundant else branch
- Reorganize docs with separate sections for mp3gain, aacgain, mp3rgain
- Fix CVE reference (CVE-2021-34085 is fixed in mp3gain 1.6.2)
- Update command option description per review feedback
2026-01-18 10:52:41 +01:00
Šarūnas Nejus
ef59cfa522
Handle potential OSError when unlinking temporary files in ArtResizer (#5615)
## Description

was getting permission error because after png is converted to jpg beets
want to delete the png but somehow it is still being used causing the
import to fail. this temporarily fixes the import but still needs a
proper way to know what is using the file and how to delete it.

```
  File "C:\Users\DELL\projects\_myForks\beets\beetsplug\fetchart.py", line 1321, in fetch_art
    candidate = self.art_for_album(task.album, task.paths, local)
  File "C:\Users\DELL\projects\_myForks\beets\beetsplug\fetchart.py", line 1413, in art_for_album
    out.resize(self)
    ~~~~~~~~~~^^^^^^
  File "C:\Users\DELL\projects\_myForks\beets\beetsplug\fetchart.py", line 218, in resize
    self._resize(plugin, current_check)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\DELL\projects\_myForks\beets\beetsplug\fetchart.py", line 246, in _resize
    self.path = ArtResizer.shared.reformat(
                ~~~~~~~~~~~~~~~~~~~~~~~~~~^
        self.path,
        ^^^^^^^^^^
        plugin.cover_format,
        ^^^^^^^^^^^^^^^^^^^^
        deinterlaced=plugin.deinterlace,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Users\DELL\projects\_myForks\beets\beets\util\artresizer.py", line 658, in reformat
    os.unlink(path_in)
    ~~~~~~~~~^^^^^^^^^
PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: b'C:\\Users\\DELL\\AppData\\Local\\Temp\\beets\\beetsplug_fetchart\\4kqx2um2.png'
```
when importing
https://musicbrainz.org/release/5744ddb7-e9b6-4b46-a55c-38e75aa95460

beet config

```yaml
fetchart:
    minwidth: 500
    maxwidth: 3000
    max_filesize: 3500000
    sources:
    -   coverart: release
    -   coverart: releasegroup
    - itunes
    - amazon
    - filesystem
    - albumart
    - '*'
    cautious: yes
    cover_names: cover front art artwork folder album
    store_source: yes
    cover_format: JPEG
    auto: yes
    quality: 0
    enforce_ratio: no
    high_resolution: no
    deinterlace: no
```
2026-01-17 14:35:44 +00:00
Dr.Blank
52284ff7ed
fix: changelog entry 2026-01-17 14:30:22 +05:30
m_igashi
29e1c283eb fix: sort imports alphabetically 2026-01-17 02:15:43 +01:00
m_igashi
5ea41b3fbb refactor: simplify CommandBackend with SUPPORTED_FORMATS_BY_TOOL
- Add Tool type alias and SUPPORTED_FORMATS_BY_TOOL class variable
- Refactor __init__ to use shutil.which() and set cmd_name early
- Simplify format_supported() to use dictionary lookup
2026-01-17 02:10:29 +01:00
m_igashi
683da049a0 style: format replaygain.rst with docstrfmt 2026-01-16 16:20:45 +01:00
m_igashi
179dc7d070 style: remove trailing whitespace from blank line 2026-01-16 16:06:17 +01:00
Jesse Pinkman
dc117046a9
Merge branch 'master' into add-mp3rgain-support 2026-01-16 15:55:23 +01:00
Dr.Blank
4ad5871ef0
fix: sort imports 2026-01-16 15:53:34 +05:30
Dr.Blank
b0bce80518
remove changelog not related to pr 2026-01-16 15:50:09 +05:30
Dr.Blank
8482733034
Merge branch 'master' into fix-permission-error 2026-01-16 15:48:19 +05:30
Dr.Blank
e85f67ac7b
refactor: suppress OSError when unlinking temporary files in ArtResizer 2026-01-16 15:43:01 +05:30
Šarūnas Nejus
6c2c46091b
Respect no_convert and never_convert_lossy_files in convert plugin (#6286)
Given that @frigginbrownie's #5556 PR received some thumb ups but they
haven't responded since a while ago, I'm creating this PR to merge their
fix (I could not commit in the PR branch since I have no permissions to
push to their fork).

Supersedes: #5556 

Copying @frigginbrownie description from #5556:

According to the docs, the auto_keep function will "Convert your files
automatically on import to dest but import the non transcoded version."
This is true but not 100% accurate. In cases where no conversion is
required (say, importing lossy files where there's no need to convert),
auto_keep will copy the files to dest.

This behavior results in duplicate files being created on import when
the auto_keep function is set to yes - a lossy file will be imported
into the default directory (say /music) and then copied to the dest
location (say /transcodes).

This is ideal if you wish to have all music formats in your default
directory (lossy and lossless) and all lossy files (original imports and
transcodes) in a secondary directory (say /lossy).

But what if you want a separate directory of all music you've
transcoded? auto_keep won't provide that, as it copies lossy files to
the dest location. In addition, if the dest is set to the same location
as default directory, auto_keep will copy lossy files into the same
directory that beets previously imported files into, resulting in the
directory having two files for each file in an album. If you use paths
(say to have singletons imported into /music/singles), auto_keep will
import the file into the path location, then copy the file to the dest,
creating directories to match the path.

Unlike with the auto option or using "beet convert", auto_keep does not
follow the never_convert_lossy_files or no_convert options and will not
validate whether files need to be converted or copied on import to dest
- it transcodes or it copies, no questions asked.

This change updates the auto_convert_keep function to filter items using
should_transcode. This way, if the user sets never_convert_lossy_files
to no or no_convert: 'format:mp3', lossy files will not be copied to the
dest, while lossless files will be converted to the dest (perfect for a
seperate /transcodes directory). If the user sets
never_convert_lossy_files to yes, lossy files will to be copied to the
dest and lossless files will be converted to the dest (perfect for a
/lossy directory). In turn, this change makes behavior consistent with
"beet convert" and the auto option.
2026-01-15 15:58:35 +00:00
Šarūnas Nejus
bfb24da51c
Add note to the changelog 2026-01-15 15:53:06 +00:00
frigginbrownie
1ff254215a
Update convert.py 2026-01-15 15:46:01 +00:00
Šarūnas Nejus
679cfc93ed
Fix 'from_scratch': delete all tags before writing new tags to file (#5828)
Fixes #3706.

### Issue
Comment tags are written to file even if option 'from_scratch' is used.
The same tags are not written to the file if imported together with
other files as album. Therefore 'from_scratch' is not working as
described in the documentation.

### Solution
1. Add test: Adapt the function from the 'regular' import class and
insert it in the class for the singleton import test.
2. Fix bug : Add check for 'from_scratch' option. If used, clear
metadata before applying 'new' metadata with autotag.
3. No documentation change needed. Option now works as described in the
documentation.
4. Add changelog.
2026-01-15 15:43:19 +00:00
rdy2go
fdfeb35076 add changelog for and to resolve PR #5828 2026-01-15 16:07:54 +01:00
rdy2go
445ad02399
Merge branch 'beetbox:master' into master 2026-01-15 16:04:46 +01:00
m_igashi
ebd0e70012 Add mp3rgain support to ReplayGain command backend
mp3rgain is a modern Rust rewrite of mp3gain that provides:
- CLI-compatible drop-in replacement for mp3gain
- Support for both MP3 and AAC/M4A formats (like aacgain)
- Fixes for CVE-2021-34085 (Critical, CVSS 9.8) and CVE-2019-18359 (Medium)
- Memory-safe implementation in Rust
- Works on modern systems (Windows 11, macOS Apple Silicon)

Changes:
- Add mp3rgain to the command search list (prioritized first)
- Update format_supported() with more robust command name detection
  using os.path.basename() and startswith() instead of substring matching
- Update documentation with installation instructions

See: https://github.com/M-Igashi/mp3rgain
2026-01-14 01:47:45 +01:00
Šarūnas Nejus
b3c42a3350
Enable ruff's future-annotations and RUF* rules (#6245)
## Summary

This PR updates typing and linting across the codebase and enables
stricter `ruff` checks for Python 3.10:

1. Enable `tool.ruff.lint.future-annotations`
Very handy feature released in `0.13.0`: if required, it _automatically_
adds `from __future__ import annotations` and moves relevant imports
under `if TYPE_CHECKING`:

   ```py
   # before (runtime import)
   from beets.library import Library

   # after
   from __future__ import annotations

   from typing import TYPE_CHECKING

   if TYPE_CHECKING:
       from beets.library import Library
   ```

2. Set `tool.ruff.target-version = "py310"`

   This enforced PEP 604 unions in the codebase:

   ```py
   # before
   SQLiteType = Union[str, bytes, float, int, memoryview, None]

   # after
   SQLiteType = str | bytes | float | int | memoryview | None
   ```

3. Enable `RUF*` family of checks

   - Remove unused `# noqa`s
   - Ignore unused unpacked variables

     ```py
     # before
     likelies, consensus = util.get_most_common_tags(self.items)

     # after
     likelies, _ = util.get_most_common_tags(self.items)
     ```
   - Avoid list materialization
     ```py
     # before
     for part in parts + [","]:

     # after
     for part in [*parts, ","]:
     ```

- And, most importantly, **RUF012**: use `ClassVar` for mutable class
attributes

- This underlined our messy `BeetsPlugin.template_*` attributes design,
where I have now defined `BeetsPluginMeta` to make a clear distinction
between class and instance attributes. @semohr and @asardaes I saw you
had a discussion regarding these earlier - we will need to revisit this
at some point to sort it out for good.
- It also revealed a legitimate issue in `metasync.MetaSource` where
`item_types` were initialised as an instance attribute (but luckily
never used).
2026-01-13 21:02:32 +00:00
Šarūnas Nejus
c9625f8fb3
Update git blame ignore revs 2026-01-13 20:55:40 +00:00
Šarūnas Nejus
1c20e4bd4e
Address RUF012 2026-01-13 20:55:40 +00:00
Šarūnas Nejus
c52656fb0a
Enable RUF rules 2026-01-13 20:55:40 +00:00
Šarūnas Nejus
078ffc1c57
Configure ruff for py310 2026-01-13 20:55:40 +00:00
Šarūnas Nejus
b964d8b7eb
Configure future-annotations 2026-01-13 20:55:40 +00:00
Šarūnas Nejus
bd319c2c43
db: disable DQS on Python >= 3.12 (#5235)
cf. https://github.com/beetbox/beets/issues/4709, let's see how badly
this breaks CI
2026-01-13 13:53:45 +00:00
wisp3rwind
7685e9439a db: disable DQS on Python >= 3.12 2026-01-13 13:48:23 +00:00
Šarūnas Nejus
265d513251
Change link for beets-usertag (#5756)
The beets-usertag plugin was originally created by igordetigor, but has
been unmaintained for some time now. Recently, I decided to add some
features to it and, with Ingo's blessing, publish on PyPI. He's also
okay with me replacing the link in the plugins list with my fork.
2026-01-13 13:44:42 +00:00
Šarūnas Nejus
0efce4a86b
Merge branch 'master' into usertag 2026-01-13 13:38:56 +00:00
Šarūnas Nejus
4ff6b39ecb
Fix bug in fetching preferred release event. (#6279)
With the changes to how data is fetched from MusicBrainz, empty releases
are now `None` instead of an empty dict.
2026-01-13 13:21:24 +00:00
David Logie
3ea4bb7941 Fix bug in fetching preferred release event.
With the changes to how data is fetched from MusicBrainz, empty releases
are now `None` instead of an empty dict.
2026-01-13 13:16:09 +00:00
Šarūnas Nejus
f63585fe31
Remove expired Spotify credentials and log error for missing configuration (#6271)
Looks like our hard-coded API credentials expired. This PR removes
expired credentials and makes it clear to the user that they must
provide their credentials.

Fixes #6270
2026-01-11 19:02:37 +00:00
Alok Saboo
cff631f9c9 updated credentials 2026-01-11 09:20:03 -05:00
Henry Oberholtzer
c04fc95e59 Merge #6213 - Fix ftintitle plugin to prioritize explicit featuring tokens 2026-01-08 12:21:00 -08:00
Henry Oberholtzer
f7b05cb7db ftintitle: fix changelog conflict 2026-01-08 12:20:25 -08:00
Šarūnas Nejus
a3c2e383f4
Merge branch 'master' into spotify 2026-01-08 00:23:15 +00:00
Alok Saboo
dd3ecec579 Updated Spotify API credentials 2026-01-07 18:54:39 -05:00
Šarūnas Nejus
ad2ff1f97e
Fix fetchart colors broken by 67e668d81f (#6273)
Fetchart colors were broken by 67e668d81f.
Since that commit fetchart displays this, with escape codes instead of
colorized output:
```
fetchart: Alasdair Fraser & Natalie Haas - Meridians: �[37mhas album art�[39;49;00m
```

This fixes it by using `ui.print_` instead of `Logger`. This seems to be
in line with other plugins such as play:

1a899cc92a/beetsplug/play.py (L135)
2026-01-07 22:41:44 +00:00
Aidan Epstein
b53aff9b15 Fix fetchart colors broken by 67e668d81f 2026-01-07 10:36:08 -08:00
Šarūnas Nejus
1a899cc92a
Drop dependency on python-musicbrainzngs (#6234)
## Replace `python-musicbrainzngs` with Custom Lightweight MusicBrainz
Client

### Core Problem Solved

**Before**: Beets depended on the external `python-musicbrainzngs`
library (v0.7.1) for all MusicBrainz API interactions. This dependency
required separate installation for multiple plugins and introduced an
abstraction layer that obscured direct HTTP semantics.

**After**: Custom **lightweight MusicBrainz client**
(`beetsplug.utils.musicbrainz`) built directly on `requests` and
`requests-ratelimiter`, eliminating the external dependency while
maintaining full API compatibility.

---

### Architecture Overview

#### **1. New MusicBrainz Client Foundation**

Created `beetsplug/utils/musicbrainz.py` with three core components:

```
MusicBrainzAPI (base client)
├─ Configuration-driven initialization (from config['musicbrainz'])
├─ Rate-limited session via LimiterTimeoutSession
├─ Generic entity fetching (get_entity, search_entity)
├─ Specialized methods (get_release, get_recording, get_work)
└─ Recursive relation grouping (group_relations)

MusicBrainzUserAPI (authenticated operations)
├─ Extends MusicBrainzAPI with HTTPDigestAuth
├─ Collection management (get_collections)
└─ User-specific API operations

MBCollection (collection manipulation)
├─ Paginated release fetching
├─ Chunked PUT operations (add_releases)
└─ Chunked DELETE operations (remove_releases)
```

#### **2. Config parsing now centralized in
`MusicBrainzAPI.__post_init__()`**.

And applies to all plugins.

#### **3. Mixin Pattern for Plugin Integration**

```python
# OLD: Plugins directly instantiated musicbrainzngs
import musicbrainzngs
musicbrainzngs.auth(user, pass)
resp = musicbrainzngs.get_recording_by_id(id, includes=['releases'])

# NEW: Plugins inherit MusicBrainzAPIMixin
class MyPlugin(MusicBrainzAPIMixin, BeetsPlugin):
    def some_method(self):
        recording = self.mbapi.get_recording(id, includes=['releases'])
```

**Affected plugins**:

- `musicbrainz` → `MusicBrainzAPIMixin`
- `listenbrainz` → `MusicBrainzAPIMixin`
- `mbcollection` → `MusicBrainzUserAPIMixin` (requires authentication)
- `missing` → `MusicBrainzAPIMixin`
- `parentwork` → `MusicBrainzAPIMixin`

---

### Plugin-Specific Refactoring

#### **`mbcollection`: From Procedural to Object-Oriented**

**Before**: Module-level functions (`submit_albums`, `mb_call`) with
error handling scattered across multiple try-except blocks.

**After**: `MBCollection` dataclass encapsulating collection operations:

```python
# OLD approach
collection_id = get_collection()
albums_in_collection = get_albums_in_collection(collection_id)
submit_albums(collection_id, album_ids)

# NEW approach
collection = self.collection  # cached property with validation
collection.add_releases(album_ids)
collection.remove_releases(removed_ids)
```

**Key improvements**:

- Eliminated `mb_call` error wrapper (errors propagate naturally from
`RequestHandler`)
- Consolidated pagination logic into `MBCollection.releases` property
- Type hints for `Library`, `ImportSession`, `ImportTask` parameters

#### **`listenbrainz`: Search Query Simplification**

**Before**: Called `musicbrainzngs.search_recordings()` with constructed
query strings.

**After**: Uses `self.mbapi.search_entity()` which handles query
formatting:

```python
# OLD
resp = musicbrainzngs.search_recordings(
    query=f'track:{track_name} AND release:{album_name}',
    strict=True
)
if resp.get('recording-count') > 1:
    return resp['recording-list'][0].get('id')

# NEW
for recording in self.mbapi.search_entity('recording', 
                                           {'track': track_name, 
                                            'release': album_name}):
    return recording['id']
return None
```

#### **`parentwork`: Inline Traversal Logic**

**Before**: Module-level functions (`direct_parent_id`,
`work_parent_id`, `find_parent_work_info`) that made sequential API
calls.

**After**: Single method `find_parent_work_info` that traverses work
hierarchy inline:

```python
def find_parent_work_info(self, mbworkid: str) -> tuple[dict, str | None]:
    workdate = None
    parent_id = mbworkid
    
    while parent_id := current_id:  # walrus operator
        workinfo = self.mbapi.get_work(current_id, includes=['work-rels', 'artist-rels'])
        workdate = workdate or extract_composer_date(workinfo)
        parent_id = find_parent_in_relations(workinfo)
    
    return workinfo, workdate
```

Eliminates three function calls per traversal level.

---

### Dependency & Installation Impact

#### **Package Dependencies**

```diff
# pyproject.toml
-musicbrainzngs = {version = ">=0.4", optional = true}

# Removed from extras groups
-listenbrainz = ["musicbrainzngs"]
-mbcollection = ["musicbrainzngs"]
-missing = ["musicbrainzngs"]
-parentwork = ["musicbrainzngs"]
```

**Result**: Four plugin extras no longer require external package
installation beyond `requests` (already a core dependency).

#### **CI Workflow**

```diff
# .github/workflows/ci.yaml
-poetry install --extras=parentwork
+poetry install  # parentwork now part of core
```

The `parentwork` extra is removed entirely since it had no other
dependencies.

#### **Documentation Updates**

Removed "Installation" sections from four plugin docs that previously
required:

```bash
pip install beets[listenbrainz]  # NO LONGER NEEDED
pip install beets[mbcollection]  # NO LONGER NEEDED
pip install beets[missing]       # NO LONGER NEEDED
pip install beets[parentwork]    # NO LONGER NEEDED
```

Plugins now work **out-of-the-box** with just `plugins: [listenbrainz,
...]` in config.

---

### Testing Improvements

#### **New Shared Fixture: `requests_mock`**

Created `test/plugins/conftest.py` with fixture that disables rate
limiting during tests:

```python
@pytest.fixture
def requests_mock(requests_mock, monkeypatch):
    """Use plain session wherever MB requests are mocked."""
    monkeypatch.setattr(
        'beetsplug.utils.musicbrainz.MusicBrainzAPI.session',
        requests.Session,
    )
    return requests_mock
```

This avoids artificial delays when mocking HTTP responses.

#### **New Plugin Test Suites**

Added comprehensive tests for previously untested plugins:

1. **`test_listenbrainz.py`**: Tests recording ID lookup and track info
fetching
2. **`test_mbcollection.py`**: Tests collection validation, pagination,
and sync operations
3. **`test_missing.py`**: Tests missing album detection logic
4. **`test/plugins/utils/test_musicbrainz.py`**: Tests `group_relations`
transformation

#### **Test Migration**

Moved `test_group_relations` from `test_musicbrainz.py` to
`test/plugins/utils/test_musicbrainz.py` (84 lines) since
`group_relations` is now a utility function.

---

### Migration Benefits

| **Aspect** | **Before** | **After** |
| ------------------------- | ------------------------------------ |
------------------------------------------- |
| **External dependencies** | `python-musicbrainzngs` (0.7.1) | None
(uses existing `requests`) |
| **Plugin installation** | `pip install beets[plugin]` required | Works
with base install |
| **API surface area** | ~50 functions in musicbrainzngs | ~10 methods
tailored to Beets |
| **Error messages** | Generic exceptions with status codes | Full HTTP
response text included |
| **Response structure** | Raw MusicBrainz JSON | Normalized with
grouped relations |
| **Code ownership** | External maintenance dependency | Direct control
over API client |
| **Test speed** | Rate-limited even with mocks | Fixture disables
limits for mocked requests |
| **Type safety** | Minimal type hints in musicbrainzngs | Full type
hints (`JSONDict`, `list[str]`) |

---

### Backward Compatibility

** Fully backward compatible**:

- All existing plugin APIs unchanged from user perspective
- Configuration keys remain identical (`musicbrainz.user`,
`musicbrainz.pass`, etc.)
- MusicBrainz API responses maintain same structure (with additional
normalization)
- Test suite passes without modification to integration tests

**Breaking changes**: None from end-user perspective.


closes #6265
2026-01-07 11:25:00 +00:00
Šarūnas Nejus
a801afd8b6
Update git blame ignore revs 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
a405821828
Fix changelog formatting 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
d4b00ab4f4
Add request handler utils to the docs 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
59b02bc49b
Type MusicBrainzAPI properly 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
55b9c1c145
Retry on server errors too 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
1447f49b72
Add some documentation to musicbrainz api mixins 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
34d993c043
Add a changelog note 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
b49d71cb69
mbcollection: slight refactor 2026-01-06 09:54:02 +00:00
Šarūnas Nejus
92352574aa
Migrate mbcollection to use MusicBrainzAPI 2026-01-06 09:54:02 +00:00