- 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
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.
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.
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
## 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).
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.
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
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)
## Summary
This PR updates the `ftintitle` plugin to insert featured artist tokens
before brackets containing remix/edit-related keywords (e.g., "Remix",
"Live", "Edit") instead of always appending them at the end of the
title.
## Motivation
Previously, the plugin would always append featured artists at the end
of titles, resulting in awkward formatting like:
- `Threshold (Myselor Remix) ft. Hallucinator`
With this change, featured artists are inserted before the first bracket
containing keywords, producing cleaner formatting:
- `Threshold ft. Hallucinator (Myselor Remix)`
## Changes
### Core Functionality
- Added `find_bracket_position()` function that:
- Searches for brackets containing remix/edit-related keywords
- Supports multiple bracket types: `()`, `[]`, `<>`, `{}`
- Only matches brackets with matching opening/closing pairs
- Uses case-insensitive word-boundary matching for keywords
- Returns the position of the earliest matching bracket
- Updated `update_metadata()` to insert featured artists before brackets
instead of appending
### Configuration
- Added new `bracket_keywords` configuration option:
- **Default**: List of keywords including: `abridged`, `acapella`,
`club`, `demo`, `edit`, `edition`, `extended`, `instrumental`, `live`,
`mix`, `radio`, `release`, `remaster`, `remastered`, `remix`, `rmx`,
`unabridged`, `unreleased`, `version`, and `vip`
- **Customizable**: Users can override with their own keyword list
- **Empty list**: Setting to `[]` matches any bracket content regardless
of keywords
### Example Configuration
```yaml
ftintitle:
bracket_keywords: ["remix", "live", "edit", "version", "extended"]
```
## Behavior
- **Titles with keyword brackets**: Featured artists are inserted before
the first bracket containing keywords
- `Song (Remix) ft. Artist` → `Song ft. Artist (Remix)`
- `Song (Live) [Remix] ft. Artist` → `Song ft. Artist (Live) [Remix]`
(picks first bracket with keyword)
- **Titles without keyword brackets**: Featured artists are appended at
the end (backward compatible)
- `Song (Arbitrary) ft. Artist` → `Song (Arbitrary) ft. Artist`
- **Nested brackets**: Correctly handles nested brackets of same and
different types
- `Song (Remix [Extended]) ft. Artist` → `Song ft. Artist (Remix
[Extended])`
- **Multiple brackets**: Picks the earliest bracket containing keywords
- `Song (Live) (Remix) ft. Artist` → `Song ft. Artist (Live) (Remix)`
(if both contain keywords, picks first)
## Testing
- Added comprehensive test coverage for:
- Different bracket types (`()`, `[]`, `<>`, `{}`)
- Nested brackets (same and different types)
- Multiple brackets
- Custom keywords
- Empty keyword list behavior
- Edge cases (unmatched brackets, no brackets, etc.)
All 112 tests pass.
## Backward Compatibility
This change is **backward compatible**:
- Titles without brackets continue to append featured artists at the end
- Titles with brackets that don't contain keywords also append at the
end
- Existing configuration files continue to work (uses sensible defaults)
## Documentation
- Updated changelog with detailed description of the new feature
- Configuration option is documented in the changelog entry