14668 Commits

Author SHA1 Message Date
Sebastian Mohr
1a03302a2d Proposal: Rewrote sync logic and split command again
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.
2026-06-15 21:48:55 +02:00
Alok Saboo
b85e5ec59d fix mypy: use explicit if-guard for album_result 2026-06-15 13:41:34 -04:00
Alok Saboo
4cd996cc0e apply ruff formatting 2026-06-15 13:18:15 -04:00
Alok Saboo
4a0b2beeef address reviewer comments: simplify _parse_popularity, fix flake8 E501 2026-06-15 13:00:59 -04:00
Alok Saboo
a6bf76b232 address reviewer comments: type _parse_popularity with attributes, add album popularity to sync 2026-06-15 11:38:12 -04:00
Alok Saboo
2f434b2b93 address reviewer comments 2026-06-15 09:45:35 -04:00
Alok Saboo
bed1f024b0 Fix tests and mypy error
- Use self.add_item() instead of self.lib.add_item() in tests
- Fix mypy: add isinstance(val, int) check in _popularity()
- Apply ruff formatting
2026-06-14 16:36:19 -04:00
Alok Saboo
71d09893ef fix tests 2026-06-14 15:59:25 -04:00
Alok Saboo
98681406af tidal: add flexattrs and tidalsync command
- 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
2026-06-14 13:44:16 -04:00
Šarūnas Nejus
60047dfabd convert: fix beets.util.pipeline import regression (#6740)
- 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'
```
2026-06-14 14:46:27 +01:00
Šarūnas Nejus
1b9701f1ff Import pipeline directly in convert plugin
- Use an explicit pipeline import for convert pipeline setup and decorators.
- This fixes AttributeError when beets.util does not automatically expose
  pipeline.
2026-06-14 13:21:28 +01:00
Šarūnas Nejus
2337cfb6c2 db: simplify LazyConvertDict (#6734)
- 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.
2026-06-14 10:28:53 +01:00
Šarūnas Nejus
8146d535af Refactor lazy model value storage
- 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.
2026-06-13 17:29:42 +01:00
Šarūnas Nejus
beff630d26 refactor(ftintitle): cache config options (#6732)
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.
2026-06-13 11:01:32 +01:00
Trey Turner
adc2056d87 refactor(ftintitle): cache config options 2026-06-13 10:56:02 +01:00
Šarūnas Nejus
ad03b35f86 Fix integration tests (#6724)
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.
2026-06-13 02:02:39 +01:00
Šarūnas Nejus
01792ec8ad Enable lyrics tests 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
0edda6df26 Fix links 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
1a25bbc12e Install dependencies in integration test 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
3b9034e8cf Only skip tests not in CI 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
af27ed2ee6 Centralise RUNNING_IN_CI definition 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
912ab791d9 Exclude tests that depend on ffprobe when unavailable 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
4f3ac5a40a Exclude artresizer tests when unavailable 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
949f987368 Exclude chroma tests when unimportable 2026-06-13 01:46:44 +01:00
Šarūnas Nejus
eaba5aa330 TimeoutAndRetrySession: fold in RateLimitAdapter and 429 retry (#6731)
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.
2026-06-13 01:17:44 +01:00
Alok Saboo
7c3566e073 fix mypy errors 2026-06-12 17:03:35 -04:00
Alok Saboo
68ed8924ef TimeoutAndRetrySession: fold in RateLimitAdapter and 429 retry 2026-06-12 09:43:23 -04:00
Šarūnas Nejus
ec729d318d lyrics: add rate limiting and exponential backoff to prevent 429 errors (#6729)
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
2026-06-12 02:28:31 +01:00
Alok Saboo
9da0c4264f fix create_session return type annotation to match parent class 2026-06-11 08:48:24 -04:00
Alok Saboo
a07d490f95 revert base class return type annotation change 2026-06-11 08:41:00 -04:00
Alok Saboo
7527bc1bc3 Merge branch 'master' into lyric 2026-06-11 08:22:43 -04:00
Alok Saboo
91ab049a22 refactor LyricsSession to extend TimeoutAndRetrySession
Simplifies by inheriting User-Agent, timeout, and raise_for_status
from the shared session base class, as suggested in review.
2026-06-11 08:15:53 -04:00
Sebastian Mohr
7de08ba63e refactor: Removed ImportTestCase in favor of ImportHelper (#6680)
## 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
2026-06-11 10:44:42 +02:00
Sebastian Mohr
93a53edf01 Use monkeypatch in logging instead of patch 2026-06-11 10:39:14 +02:00
Sebastian Mohr
8db903c8f8 Propagate rename to test_ui_importer 2026-06-11 10:39:14 +02:00
Sebastian Mohr
6327f1122a Remove now unused ImportTestCase 2026-06-11 10:39:14 +02:00
Sebastian Mohr
b745157bec test_logging: ImportTestCase -> ImporterHelper 2026-06-11 10:39:14 +02:00
Sebastian Mohr
7636022cb2 scrub: ImportTestCase -> ImporterHelper 2026-06-11 10:39:14 +02:00
Sebastian Mohr
fb2470dd0b Naming change in ui inheritance. 2026-06-11 10:39:14 +02:00
Sebastian Mohr
500bad329c permissions: ImportTestCase -> ImporterHelper 2026-06-11 10:39:14 +02:00
Sebastian Mohr
86de25a352 keyfinder: ImportTestCase -> ImporterHelper 2026-06-11 10:39:14 +02:00
Sebastian Mohr
69100c7946 test_importer: ImportTestCase -> ImporterHelper 2026-06-11 10:39:14 +02:00
Sebastian Mohr
a368803443 replaygain: ImportTestCase -> ImportHelper 2026-06-11 10:39:14 +02:00
Sebastian Mohr
c300dd24ce filefilter: ImportTestCase -> ImportHelper and fixture based setup 2026-06-11 10:39:14 +02:00
Sebastian Mohr
7a894fc532 chroma: ImportTestCase -> ImportHelper 2026-06-11 10:39:14 +02:00
Alok Saboo
740597213f fix mypy return type and address copilot review comments
- 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
2026-06-10 21:33:54 -04:00
Alok Saboo
49c415a302 lyrics: add rate limiting and exponential backoff to prevent 429 errors
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
2026-06-10 21:24:31 -04:00
Šarūnas Nejus
018089221f Cast keys to list to avoid exception in optparse (#6725)
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)
```
2026-06-11 01:10:02 +01:00
Aidan Epstein
db4f30b230 Cast keys to list to avoid exception in optparse
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)
2026-06-11 01:03:13 +01:00
Sebastian Mohr
6f8d55da41 Fix replace command callback signature (#6700)
## 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
```
2026-06-09 10:20:09 +02:00