Commit Graph

14367 Commits

Author SHA1 Message Date
Konstantin
412c76afdd Refactor ID extraction to use enum and cached patterns
Use `UrlSource` enum and cached `pattern_by_source` function to replace
string-based source handling and regex dictionary
2026-05-14 02:21:00 +02:00
Konstantin
934e1e682d Add optional id extractor pattern parameter to metadata plugins 2026-05-14 00:14:35 +02:00
J0J0 Todos
7c50f94c60 lastgenre: Test empty last.fm result doesnt wipe (#6608)
Add a test case that proves that issue 5991 is fixed by now.

Closes #5991 .
2026-05-09 09:20:05 +02:00
J0J0 Todos
5f94ca79a3 lastgenre: Test empty last.fm result doesnt wipe
Add a test case that proves that issue 5991 is fixed by now.
2026-05-09 09:11:05 +02:00
Šarūnas Nejus
324877042a fix: mbpseudo issues when applying pseudorelease (#6512)
I have not created an issue for this but I tried to use mbpseudo to
apply this pseudorelease:
https://musicbrainz.org/release/6c100fef-6abf-41c4-bd21-6f9becaaab6c

When doing that I encountered two errors as seen below, this PR should
fix those issues. The second issue was only apparent once the first
issue was fixed.

```
Sending event: import_task_choice
Traceback (most recent call last):
  File "/lsiopy/bin/beet", line 6, in <module>
    sys.exit(main())
             ^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/ui/__init__.py", line 1013, in main
    _raw_main(args)
  File "/lsiopy/lib/python3.12/site-packages/beets/ui/__init__.py", line 992, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/lsiopy/lib/python3.12/site-packages/beets/ui/commands/import_/__init__.py", line 131, in import_func
    import_files(lib, byte_paths, query)
  File "/lsiopy/lib/python3.12/site-packages/beets/ui/commands/import_/__init__.py", line 75, in import_files
    session.run()
  File "/lsiopy/lib/python3.12/site-packages/beets/importer/session.py", line 237, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/lsiopy/lib/python3.12/site-packages/beets/util/pipeline.py", line 471, in run_parallel
    raise exc_info[1].with_traceback(exc_info[2])
  File "/lsiopy/lib/python3.12/site-packages/beets/util/pipeline.py", line 336, in run
    out = self.coro.send(msg)
          ^^^^^^^^^^^^^^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/util/pipeline.py", line 195, in coro
    task = func(*args, task)
           ^^^^^^^^^^^^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/importer/stages.py", line 217, in user_query
    _apply_choice(session, task)
  File "/lsiopy/lib/python3.12/site-packages/beets/importer/stages.py", line 323, in _apply_choice
    task.apply_metadata()
  File "/lsiopy/lib/python3.12/site-packages/beets/importer/tasks.py", line 263, in apply_metadata
    self.match.apply_metadata()
  File "/lsiopy/lib/python3.12/site-packages/beets/autotag/hooks.py", line 609, in apply_metadata
    for item, data in self.merged_pairs:
                      ^^^^^^^^^^^^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/autotag/hooks.py", line 603, in merged_pairs
    (i, ti.merge_with_album(self.info))
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/autotag/hooks.py", line 482, in merge_with_album
    album = album_info.raw_data
            ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/functools.py", line 998, in __get__
    val = self.func(instance)
          ^^^^^^^^^^^^^^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/autotag/hooks.py", line 301, in raw_data
    data = {**super().raw_data}
              ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/functools.py", line 998, in __get__
    val = self.func(instance)
          ^^^^^^^^^^^^^^^^^^^
  File "/lsiopy/lib/python3.12/site-packages/beets/autotag/hooks.py", line 176, in raw_data
    data = self.__class__(**self.copy())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: PseudoAlbumInfo.__init__() missing 2 required positional arguments: 'pseudo_release' and 'official_release'
```

```
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/martin/personal/src-ext/beets/beets/__main__.py", line 24, in <module>
    main(sys.argv[1:])
    ~~~~^^^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/ui/__init__.py", line 1013, in main
    _raw_main(args)
    ~~~~~~~~~^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/ui/__init__.py", line 992, in _raw_main
    subcommand.func(lib, suboptions, subargs)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/ui/commands/import_/__init__.py", line 131, in import_func
    import_files(lib, byte_paths, query)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/ui/commands/import_/__init__.py", line 75, in import_files
    session.run()
    ~~~~~~~~~~~^^
  File "/home/martin/personal/src-ext/beets/beets/importer/session.py", line 237, in run
    pl.run_parallel(QUEUE_SIZE)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/util/pipeline.py", line 471, in run_parallel
    raise exc_info[1].with_traceback(exc_info[2])
  File "/home/martin/personal/src-ext/beets/beets/util/pipeline.py", line 336, in run
    out = self.coro.send(msg)
  File "/home/martin/personal/src-ext/beets/beets/util/pipeline.py", line 195, in coro
    task = func(*args, task)
  File "/home/martin/personal/src-ext/beets/beets/importer/stages.py", line 217, in user_query
    _apply_choice(session, task)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/importer/stages.py", line 326, in _apply_choice
    task.add(session.lib)
    ~~~~~~~~^^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/importer/tasks.py", line 503, in add
    self.album = lib.add_album(self.imported_items())
                 ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/library/library.py", line 83, in add_album
    item.add(self)
    ~~~~~~~~^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/library/models.py", line 84, in add
    super().add(lib)
    ~~~~~~~~~~~^^^^^
  File "/home/martin/personal/src-ext/beets/beets/dbcore/db.py", line 717, in add
    self.store()
    ~~~~~~~~~~^^
  File "/home/martin/personal/src-ext/beets/beets/library/models.py", line 74, in store
    super().store(fields)
    ~~~~~~~~~~~~~^^^^^^^^
  File "/home/martin/personal/src-ext/beets/beets/dbcore/db.py", line 659, in store
    tx.mutate(
    ~~~~~~~~~^
        f"INSERT INTO {self._flex_table} "
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<2 lines>...
        (self.id, key, value),
        ^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/martin/personal/src-ext/beets/beets/dbcore/db.py", line 1039, in mutate
    return self.db._connection().execute(statement, subvals).lastrowid
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
sqlite3.ProgrammingError: Error binding parameter 3: type 'dict' is not supported
```
2026-05-08 13:53:32 +01:00
Šarūnas Nejus
cde2dd139e Merge branch 'master' into fix/mbpseudo-raw-data 2026-05-08 13:38:30 +01:00
Sebastian Mohr
e58d404222 Refactored test_hook to use pytest and removed capture_log (#6618)
This pull request refactors the `test/plugins/test_hook.py` test suite
to use `pytest` fixtures and utilities rather than the previous
unittest-based helpers. It also updates the way logs are captured in
tests to use `pytest`'s `caplog` fixture instead of the custom
`capture_log` context manager.

---

This is part of the multi-step efforts to improve logging in beets
https://github.com/beetbox/beets/issues/6553
2026-05-08 13:54:01 +02:00
Sebastian Mohr
2056cce74b test_hook: Removed capture_log in favor for pytest caplog
Also minor refactor to align with pytest
2026-05-08 13:47:37 +02:00
Martin Caspersen
610443ae9e doc: fix formatting 2026-05-08 12:35:59 +02:00
Martin Caspersen
5342d9bc76 Merge remote-tracking branch 'fork/master' into fix/mbpseudo-raw-data 2026-05-08 12:25:49 +02:00
Mathilde Gilles
48b5dbb0e2 feat(import): add --nomove / -M option (#6615)
## Description

Add `--nomove` / `-M` option to override the `move: yes` config option
during import.

This option is especially useful for reimporting when using players /
music servers like Navidrome that easily loose track of files when they
change path and tags at the same time.
Now one can retag their files without moving them `beet import -M
my-files/`, wait for a rescan, and then `beet move`.

original idea by @snejus 

## To Do

<!--
- If you believe one of below checkpoints is not required for the change
you
are submitting, cross it out and check the box nonetheless to let us
know.
  For example: - [x] ~Changelog~
- Regarding the changelog, often it makes sense to add your entry only
once
reviewing is finished. That way you might prevent conflicts from other
PR's in
that file, as well as keep the chance high your description fits with
the
  latest revision of your feature/fix.
- Regarding documentation, bugfixes often don't require additions to the
docs.
- Please remove the descriptive sentences in braces from the enumeration
below,
  which helps to unclutter your PR description.
-->

- [x] Documentation. (If you've added a new command-line flag, for
example, find the appropriate page under `docs/` to describe it.)
- [x] Changelog. (Add an entry to `docs/changelog.rst` to the bottom of
one of the lists near the top of the document.)
- [ ] Tests. (Very much encouraged but not strictly required.)
2026-05-08 08:53:40 +02:00
ShimmerGlass
45e78473b0 feat(import): add --nomove / -M option 2026-05-07 13:50:40 +02:00
Šarūnas Nejus
7dde183d7d convert: Add types and simplify args parsing (#6573)
## Refactor `ConvertPlugin` to use instance attributes instead of
parameter threading

This PR cleans up the `ConvertPlugin` class by eliminating the pattern
of passing config values as explicit parameters through the call chain.

### What changed

- **Removed `_get_opts_and_config`** — a method that returned a 9-tuple
of config values. All callers unpacked and re-passed these values
explicitly.
- **Config values are now `@cached_property` attributes** on
`ConvertPlugin`: `dest`, `fmt`, `force`, `pretend`, `link`, `hardlink`,
`threads`, `playlist`, `path_formats`.
- **CLI option defaults now set from config**, so
`self.config.set(vars(opts))` in `convert_func` is sufficient to merge
CLI overrides — no more `opts.x or config["x"]` branching in
`_get_opts_and_config`.
- **`get_format` → `command` cached property** returning a typed
`FormatCommand` `NamedTuple`, replacing a free function that accessed
the global `config`.
- **`should_transcode` and `encode`** signatures simplified — `fmt`,
`force`, and `pretend` no longer passed as arguments; methods read from
`self`.
- **`_parallel_convert` and `convert_item`** reduced to `(items,
keep_new)` signatures.
- **Free module-level functions** (`get_format`, `in_no_convert`,
`should_transcode`) removed; equivalent logic now lives as methods,
removing the dependency on the global `config` object.
- Types and `TYPE_CHECKING` imports added throughout; `after_convert`
event registered in `plugins.py`.

### Impact

No behaviour change. The refactor reduces argument surface area
significantly, makes the config read path explicit and testable
per-instance, and eliminates a source of subtle bugs where CLI flags
could silently fall back to wrong defaults.
2026-05-06 15:49:48 +01:00
Šarūnas Nejus
86813678d7 Use pipeline.mutator_stage instead of a Generator 2026-05-06 15:30:51 +01:00
Šarūnas Nejus
ec940e8b61 Remove redundant param provision to encode 2026-05-06 15:30:51 +01:00
Šarūnas Nejus
dd33fef52e Remove redundant param provision to should_transcode 2026-05-06 15:30:51 +01:00
Šarūnas Nejus
e574e86887 Replace get_format with command attribute 2026-05-06 15:30:51 +01:00
Šarūnas Nejus
34772d2684 Replace _get_opts_and_config with attributes 2026-05-06 15:30:50 +01:00
Šarūnas Nejus
ee895efd2d Handle opts using the config 2026-05-06 15:30:10 +01:00
Šarūnas Nejus
b15006ca11 Remove dependency on global config 2026-05-06 15:30:10 +01:00
Šarūnas Nejus
241eb6db09 Fix typing issues 2026-05-06 15:30:10 +01:00
Šarūnas Nejus
33efc2bd19 Add types 2026-05-06 15:30:10 +01:00
snejus
26ab6b2636 Increment version to 2.11.0 v2.11.0 2026-05-06 10:05:13 +00:00
Šarūnas Nejus
abf41b46f6 Update deps (#6611)
<img width="675" height="461" alt="image"
src="https://github.com/user-attachments/assets/a57c4930-8fdf-4982-a210-9ba785e9fb16"
/>
2026-05-06 11:02:59 +01:00
Šarūnas Nejus
4a0bfdc632 Update dependencies 2026-05-06 10:51:32 +01:00
Šarūnas Nejus
3d3f5e3ba5 badfiles: respect import.quiet during import hook (#6589)
The badfiles plugin's `on_import_task_before_choice` hook prompted for
input even when quiet mode was active. Non-interactive imports stalled
on the corrupt-file dialog. The hook now returns early when
`import.quiet` is set, so the importer falls back to its summary
judgment under both the `--quiet` flag and the `import.quiet: yes`
config key.

Fixes #4736.
2026-05-02 23:55:48 +01:00
Šarūnas Nejus
10d0f37793 Merge branch 'master' into fix/badfiles-quiet-mode 2026-05-02 22:35:41 +01:00
Šarūnas Nejus
250b3fdb8b Fix misplaced changelog note (#6602)
Fix misplaced changelog note.
2026-05-02 22:27:57 +01:00
Šarūnas Nejus
57b9b648b0 Fix changelog 2026-05-02 22:18:12 +01:00
Šarūnas Nejus
2a8380aa9d fix(MusicBrainz): date parsing fix (#6599)
Correctly handle release dates where leading or intermediate components
are missing, e.g. 2008-??-02

Without this fix parsing of these releases fail with:
```Error in 'MusicBrainz.albums_for_ids': invalid literal for int() with base 10: '??'```

While cases like that are rare in MusicBrainz they do exist and we should support them.
example: https://musicbrainz.org/release/18295b5f-3150-4086-9095-7497c261e6ce
2026-05-02 22:12:29 +01:00
ShimmerGlass
cc63a4e9b9 fix(MusicBrainz): date parsing fix
Correctly handle release dates where leading or
intermediate components are missing, e.g. 2008-??-02
2026-05-02 16:26:48 +02:00
Šarūnas Nejus
2aa7031bcc Add Beetnik in other plugins (#6597)
This just a documentation change adding BeetNik in the list of plugins.
2026-05-02 02:52:21 +01:00
ika
394ed738c4 Show how to install extra dependencies
I spend my day figuring how to do that. I hope it will help someone
else.
2026-05-02 02:47:02 +01:00
Ivan Kanis
4fdd8e2b96 Add Beetnik in other plugins 2026-05-02 02:47:02 +01:00
Eyüp Can Akman
382ec79fe0 badfiles: address review feedback 2026-05-01 17:01:47 +03:00
Eyüp Can Akman
811593a1ee badfiles: respect import.quiet during import hook
The import hook prompted for input even when quiet mode was active, so
non-interactive imports blocked on the corrupt-file dialog. Return early
from `on_import_task_before_choice` when `import.quiet` is set so the
importer falls back to its summary judgment.
2026-05-01 17:01:47 +03:00
Sebastian Mohr
2912805ca3 Added notes on AI usage to contribution guide. (#6594)
This pull request adds a new section to the `CONTRIBUTING.rst` file to
clarify the project's stance on AI-generated contributions.

@beetbox/maintainers Do any of you have another stance? Am very happy to
iterate on the exact wording if some of you see need for change.
2026-05-01 13:02:07 +02:00
Sebastian Mohr
f5b3aafadc Removed statement about disclosure of AI usage. 2026-05-01 12:53:56 +02:00
Sebastian Mohr
c5c2d6eb17 Added notes on AI usage to contribution guide. 2026-05-01 12:53:56 +02:00
Sebastian Mohr
347588568e Remove (some) capture_log occurences (#6595)
This pull request refactors several test files by replacing the custom
`capture_log` context manager with pytest’s built-in `caplog` fixture.
Using `caplog` improves clarity, aligns with standard pytest practices,
and makes the tests more reliable and easier to maintain.


Note: A few usages of `capture_log` still remain. These cases are more
complex and will be addressed separately in a follow-up PR.

---

This is part of the multi-step efforts to improve logging in beets
https://github.com/beetbox/beets/issues/6553
2026-05-01 12:53:02 +02:00
Sebastian Mohr
d6245835f2 test_discogs: Removed capture_log in favor for pytest caplog 2026-04-30 19:56:31 +02:00
Sebastian Mohr
a8db8931b7 test_playcount: Removed capture_log in favor for pytest caplog 2026-04-30 14:41:25 +02:00
Sebastian Mohr
9bae5b8fcb test_autobpm: Removed capture_log in favor for pytest caplog 2026-04-30 14:41:25 +02:00
Šarūnas Nejus
83034f7fcc Fix original date application (#6588)
## `autotag`: Move `original_date` override into `AlbumInfo.item_data`

Previously, the `original_date` year/month/day substitution was applied
in `merge_with_album` — meaning only track-level items got the corrected
date. Album-level metadata was never updated, causing a mismatch between
item and album fields.

This PR moves the logic into a `cached_property` override of `item_data`
on `AlbumInfo`, so both item and album metadata consistently reflect the
original release date when `original_date: yes` is configured.

**Key changes:**
- New `AlbumInfo.item_data` property applies the `original_date`
override before data is consumed by either items or albums.
- `merge_with_album` drops its now-redundant inline override block.
- Test renamed and extended to assert album-level fields
(`album.year/month/day`) rather than just item fields.

Fixes #6577.
2026-04-30 10:50:43 +01:00
Šarūnas Nejus
06ffb91022 autotag: move original_date override into AlbumInfo.item_data
Previously, the original_date year/month/day override was applied in
merge_with_album after building the merged dict. Move this logic into
a cached_property on AlbumInfo so album-level metadata also reflects
the original release date. Fixes 🐛`6577`.
2026-04-29 14:40:46 +01:00
Šarūnas Nejus
48582a357f Spotify: Batch Spotify spotifysync API calls (#6485)
This changes the Spotify plugin to batch `spotifysync` lookups instead
of making per-track API calls. It now:
- batches track metadata requests through /v1/tracks
- batches audio-features requests through /v1/audio-features
- deduplicates repeated Spotify track IDs within a run
- preserves the existing behavior that disables audio-features fetching
after a Spotify 403
The previous implementation made one metadata request and one
audio-features request per track, which was inefficient for larger
libraries and increased the chance of hitting rate limits. Batching
reduces request volume substantially while keeping the stored fields and
user-facing behavior the same.
2026-04-29 14:40:26 +01:00
Alok Saboo
e84ce4198a Revert unrelated docstring reformatting 2026-04-29 08:33:31 -04:00
Alok Saboo
f459edbbd4 Merge upstream master and fix changelog.rst
Sync with upstream beets/master and resolve changelog conflict,
keeping only the Spotify batch entry in the Unreleased section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:12:30 -04:00
Šarūnas Nejus
20b0dd33a9 lyrics: handle apostrophes in musixmatch slug (#6590)
`REPLACEMENTS` runs before `unidecode` in the `musixmatch` backend, so
curly apostrophes (U+2018, U+2019) survived as raw bytes in the
percent-encoded URL and titles like "If They're Shooting at You"
produced a lookup that returned nothing. A new entry in `REPLACEMENTS`
maps both codepoints to a dash. The shared `slug()` helper avoids the
bug because it calls `unidecode` first.

Fixes #4759.
2026-04-28 16:27:32 +01:00
Eyüp Can Akman
88be3023bb lyrics: handle apostrophes in musixmatch slug
The musixmatch URL slug builder did not replace curly apostrophes
(U+2018, U+2019) before applying the whitespace-to-dash rule, so titles
like "If They're Shooting at You" produced a slug with the raw quote
preserved and the lookup failed.

Add a REPLACEMENTS entry that maps both curly quote codepoints to a
dash before the rest of the substitutions run.
2026-04-28 17:27:28 +03:00