Replace tidalsync() with sync_item_popularity() / sync_album_popularity()
backed by a shared _sync_popularity() helper.
Split the `tidal` command into `tidal` (auth-only) and `tidalsync`
(popularity sync with --item, --album, --force, --write flags). While
I liked the tidal [auth|sync] it is currently not well supported in beets
and e.g. help pages are lacking.
Accept int IDs throughout the search pipeline to avoid unnecessary
str conversions.
- Add item_types ClassVar with tidal_track_id, tidal_album_id,
tidal_artist_id, tidal_track_popularity, tidal_alb_popularity,
tidal_updated fields
- Populate flexattrs during album/track import via AlbumInfo/TrackInfo
kwargs
- Add beet tidalsync command to refresh popularity data post-import
- Add tidal fields to REIMPORT_FRESH_FIELDS_ITEM for reimport support
- Add tests for flexattr population and tidalsync behavior
- Update tidal plugin docs with attribute reference and tidalsync usage
- Use an explicit pipeline import for convert pipeline setup and
decorators.
- This fixes AttributeError when beets.util does not automatically
expose pipeline.
See:
```py
$ python -c 'import beetsplug.convert'
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/sarunas/repo/beets/beetsplug/convert.py", line 73, in <module>
class ConvertPlugin(BeetsPlugin):
File "/home/sarunas/repo/beets/beetsplug/convert.py", line 402, in ConvertPlugin
@util.pipeline.mutator_stage
AttributeError: module 'beets.util' has no attribute 'pipeline'
```
```py
$ python -c 'from beets import util; print(util.pipeline)'
Traceback (most recent call last):
File "<string>", line 1, in <module>
AttributeError: module 'beets.util' has no attribute 'pipeline'
```
- Use an explicit pipeline import for convert pipeline setup and decorators.
- This fixes AttributeError when beets.util does not automatically expose
pipeline.
- Replaces `LazyConvertDict` with `LazyDict`, a simpler `UserDict`-based
container for lazily converted model values.
- Removes the separate `_awaken` construction path and routes
database-loaded values through `Model.__init__`, so both normal and
database-backed objects follow the same creation flow.
- Centralizes SQL-to-Python conversion in `Model._convert`, which makes
value handling easier to reason about and reduces special-case model
initialization logic.
**High-level impact**
- Simplifies the model storage architecture by reducing custom dict
behavior.
- Makes object construction more consistent across the codebase.
- Lowers maintenance risk by keeping lazy conversion in one place while
preserving the existing lazy-loading behavior.
- Replace LazyConvertDict with a UserDict-backed LazyDict and route
database-awakened values through the model constructor.
- This keeps lazy conversion centralized while removing the separate _awaken
construction path.
Pre-factor for https://github.com/beetbox/beets/pull/6726.
Make `ftintitle`'s config `@cached_property` attributes instead of
passing them through the call stack. Modeled after similar recent
changes in `convert`.
## Changes
- Add cached plugin properties for `auto`, `drop`, `format`,
`keep_in_artist`, `preserve_album_artist`, and `custom_words`, keeping
the existing cached `bracket_keywords` property I forgot I added a while
back.
- `commands()`, `imported()`, `ft_in_title()`, and `update_metadata()`
are cleaned up to read these directly.
- Don't read `auto` during plugin init; the import stage remains
registered and `imported()` checks `auto` when invoked.
This PR tightens the integration test workflow so CI runs the
dependency-heavy checks that are usually skipped in local environments.
### What changed
- Install the system packages and Poetry extras needed by
integration-only coverage, including ffmpeg/ffprobe,
ImageMagick/Pillow-backed art resizing, chroma, docs, replaygain,
reflink, lyrics, and related plugin dependencies.
- Run `poe docs` before `poe test` in the integration workflow and set
`LYRICS_UPDATED=1` so lyrics tests are included in CI.
- Centralize CI detection in `beets.test.helper.RUNNING_IN_CI` and reuse
shared import/program checks across test collection and helpers.
- Keep dependency-heavy tests strict in the beetbox GitHub Actions
environment, while skipping them locally when optional tools/modules are
unavailable.
- Mark ffprobe-, artresizer-, chroma-, pandoc-, and completion-related
cases with focused availability gates so local test runs fail less often
for missing optional dependencies.
- Update docs link-check ignores and stale documentation URLs so `poe
check-docs-links` is less noisy in CI.
Follow up to https://github.com/beetbox/beets/pull/6729
Move shared rate-limiting and 429 retry logic from the lyrics-specific
`LyricsSession` into the base `TimeoutAndRetrySession` so all plugins
benefit. Simplify `lyrics.py` by removing the now-redundant
`LyricsSession` subclass.
Add a LyricsSession class that mounts RateLimitAdapter (4 req/sec) and
configures urllib3 Retry with exponential backoff on 429 responses. Each
backend instance gets its own session so rate limits apply independently
per API source.
Fixes#6728
## Description
This PR removes `ImportTestCase` and replaces all occurrences with
`ImportHelper` + `pytest`.
This touches:
- `test_scrub`
- `test_filefilter`
- `test_replaygain`
- `test_chroma`
- `test_import`
---
This is related to the multi-step efforts to improve logging in beets
https://github.com/beetbox/beets/issues/6553
- Change RequestHandler.create_session()/session return type to
requests.Session so subclasses can override with custom session types
- Use setdefault for User-Agent header to respect custom overrides
- Clarify rate_limit parameter units in docstring
Add a LyricsSession class that mounts RateLimitAdapter (4 req/sec)
and configures urllib3 Retry with exponential backoff on 429
responses. Each backend instance gets its own session so rate
limits apply independently per API source.
Fixes#6728
In #6695, the Item and Album all_keys methods return sets now. This
causes an error when used in optparse for choices as it doesn't accept
sets:
```
Traceback (most recent call last):
File "/home/user/.local/bin/beet", line 10, in <module>
sys.exit(main())
~~~~^^
File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 964, in main
_raw_main(args)
~~~~~~~~~^^^^^^
File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 940, in _raw_main
subcommands, lib = _setup(options)
~~~~~~^^^^^^^^^
File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 781, in _setup
subcommands.extend(plugins.commands())
~~~~~~~~~~~~~~~~^^
File "/home/user/.local/lib/beets/beets/plugins.py", line 508, in commands
out += plugin.commands()
~~~~~~~~~~~~~~~^^
File "/home/user/.local/lib/beets/beetsplug/fish.py", line 80, in commands
cmd.parser.add_option(
~~~~~~~~~~~~~~~~~~~~~^
"-e",
^^^^^
...<4 lines>...
help="include specified field *values* in completions",
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/lib/python3.14/optparse.py", line 990, in add_option
option = self.option_class(*args, **kwargs)
File "/usr/lib/python3.14/optparse.py", line 571, in __init__
checker(self)
~~~~~~~^^^^^^
File "/usr/lib/python3.14/optparse.py", line 658, in _check_choice
raise OptionError(
"choices must be a list of strings ('%s' supplied)"
% str(type(self.choices)).split("'")[1], self)
optparse.OptionError: option -e/--extravalues: choices must be a list of strings ('set' supplied)
```
In #6695, the Item and Album all_keys methods return sets now. This
causes an error when used in optparse for choices as it doesn't accept
sets:
Traceback (most recent call last):
File "/home/user/.local/bin/beet", line 10, in <module>
sys.exit(main())
~~~~^^
File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 964, in main
_raw_main(args)
~~~~~~~~~^^^^^^
File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 940, in _raw_main
subcommands, lib = _setup(options)
~~~~~~^^^^^^^^^
File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 781, in _setup
subcommands.extend(plugins.commands())
~~~~~~~~~~~~~~~~^^
File "/home/user/.local/lib/beets/beets/plugins.py", line 508, in commands
out += plugin.commands()
~~~~~~~~~~~~~~~^^
File "/home/user/.local/lib/beets/beetsplug/fish.py", line 80, in commands
cmd.parser.add_option(
~~~~~~~~~~~~~~~~~~~~~^
"-e",
^^^^^
...<4 lines>...
help="include specified field *values* in completions",
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/lib/python3.14/optparse.py", line 990, in add_option
option = self.option_class(*args, **kwargs)
File "/usr/lib/python3.14/optparse.py", line 571, in __init__
checker(self)
~~~~~~~^^^^^^
File "/usr/lib/python3.14/optparse.py", line 658, in _check_choice
raise OptionError(
"choices must be a list of strings ('%s' supplied)"
% str(type(self.choices)).split("'")[1], self)
optparse.OptionError: option -e/--extravalues: choices must be a list of strings ('set' supplied)
## Description
Fixes#6260.
The `replace` plugin assigns `ReplacePlugin.run` directly as the
`replace` command callback. Beets command callbacks are invoked with
`(lib, opts, args)`, but `ReplacePlugin.run` only accepted `(lib,
args)`, so invoking the command raised:
```text
TypeError: ReplacePlugin.run() takes 3 positional arguments but 4 were given
```
This change updates the callback signature to accept the options
argument and adds regression coverage for invoking `beet replace`
through the command runner.
## To Do
- [x] ~Documentation.~ This is a bug fix for an existing command.
- [x] Changelog. Added an entry to `docs/changelog.rst`.
- [x] Tests. Added regression coverage for the command callback path.
## Tests
```console
BEETSDIR=/private/tmp/beets-test-config UV_CACHE_DIR=/private/tmp/uv-cache uv run pytest test/plugins/test_replace.py
# 10 passed
```