Commit graph

13371 commits

Author SHA1 Message Date
Šarūnas Nejus
143cd70e2f
mbcollection: Add tests 2026-01-06 00:51:51 +00:00
Šarūnas Nejus
9349ad7551
Migrate missing to use MusicBrainzAPI 2026-01-06 00:51:50 +00:00
Šarūnas Nejus
d346daf48e
missing: add tests for --album flag 2026-01-06 00:51:24 +00:00
Šarūnas Nejus
a33371b6ef
Migrate parentwork to use MusicBrainzAPI 2026-01-06 00:27:36 +00:00
Šarūnas Nejus
741f5c4be1
parentwork: simplify work retrieval and tests 2026-01-06 00:27:36 +00:00
Šarūnas Nejus
36964e433e
Migrate listenbrainz plugin to use our MusicBrainzAPI implementation 2026-01-06 00:27:36 +00:00
Šarūnas Nejus
af96c3244e
Add a minimal test for listenbrainz 2026-01-06 00:26:42 +00:00
Šarūnas Nejus
523fa6ceaf
Move MusicBrainzAPI to a shared util 2026-01-06 00:26:41 +00:00
Šarūnas Nejus
ea2e7bf997
feat(ftintitle): Insert featured artist before track variant clause (#6159)
## 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
2026-01-04 02:57:04 +00:00
Trey Turner
714c9705c3 Merge branch 'master' into feat/ftintitle/insert_featured_artist_before_variant_clauses 2026-01-01 15:40:37 -06:00
Trey Turner
b14755df88 fix(ftintitle): remaining opportunities for improvement 2026-01-01 15:39:17 -06:00
Jack Wilsdon
afc26fa58f Add packaging note about mock dependency removal 2026-01-01 15:50:37 +00:00
Jack Wilsdon
d6da6cda7e Update poetry.lock after removing mock 2026-01-01 15:46:06 +00:00
Alexandre Detiste
c1e36e52a8 drop extraneous dependency on old external "mock" 2026-01-01 01:49:17 +01:00
J0J0 Todos
e0489097e0
lastgenre: Finalize type hints in plugin (#6239)
## Description

Add type hints to the few remaining methods and helpers that didn't have
them already.
2025-12-30 21:39:52 +01:00
J0J0 Todos
b8c7c87b41 lastgenre: Add typehints to remaining methods,
to finally reach full type hint coverage in the plugin!
2025-12-30 21:33:07 +01:00
J0J0 Todos
8dd6988077
lastgenre: Use albumartists field to improve last.fm results (#5981)
Often last.fm does not find artist genres for delimiter-separated artist
names (eg. "Artist One, Artist Two") or where multiple artists are
combined with "concatenation words" like "and" , "+", "featuring" and so
on.

This fix gathers each artist's last.fm genre separately by using Beets'
mutli-valued `albumartists` field to improve the likeliness of finding
genres in the artist genre fetching stage.

Refactoring was done along the existing genre fetching helper functions
(`fetch_album_genre`, `fetch_track_genre`, ...):

- last.fm can be asked for genre for these combinations of metadata:
   - albumartist/album
   - artist/track
   - artist
- Instead of passing `Album` or `Item` objects directly to these
helpers., generalize them and pass the (string) metadata directly.
- Passing "what's to fetch" in the callers instead of hiding it in the
methods also helps readability in `_get_genre()`
- And reduces the requirement at hand for another additional method (or
adaptation) to support "multi-albumartist genre fetching"
2025-12-30 21:03:38 +01:00
j0j0
28dc78be95 lastgenre: Changelog for #5981 lastgenre 2025-12-30 20:57:10 +01:00
J0J0 Todos
f19d672016 lastgenre: Type hints for genre fetch methods 2025-12-30 20:57:10 +01:00
J0J0 Todos
d72307a16f lastgenre: Adapt test_get_genre function signatures 2025-12-30 20:57:10 +01:00
J0J0 Todos
a046f60c51 lastgenre: Hint mypy to Album.items()
instead of obj.items()
2025-12-30 20:57:10 +01:00
j0j0
355c9cc1b6 lastgenre: Use multi-valued albumartists field
In case the albumartist genre can't be found (often due to variations of
artist-combination wording issues, eg "featuring", "+", "&" and so on)
use the albumartists list field, fetch a genre for each artist
separately and concatenate them.
2025-12-30 20:57:10 +01:00
j0j0
40a212a2c4 lastgenre: Simplify genre fetchers
Reduce fetcher methods to 3: last.fm can be asked for
for a genre for these combinations of metadata:

- albumartist/album
- artist/track
- artist

Passing them in the callers instead of hiding it in the
methods also helps readability in _get_genre().
2025-12-30 20:57:10 +01:00
Šarūnas Nejus
1b701c866c
fetchart: Add support for configurable fallback cover art (#6253)
Fixes discussion #6252.
2025-12-30 16:49:52 +00:00
Danny Trunk
9ddddf4c39 fetchart: Add support for configurable fallback cover art 2025-12-30 13:45:03 +01:00
Šarūnas Nejus
ed566eb14e
Fix original release id access for a pseudo release (#6250)
Fixes #6248
2025-12-29 17:11:40 +00:00
Šarūnas Nejus
f9c3aae4ed
Musicbrainz: fix original release id access for a pseudo releae 2025-12-29 17:05:32 +00:00
Šarūnas Nejus
21e6a1f757
Improve model changes colour display and db / field diff typing (#6240)
## 1. **Refactored UI diffs**

Using the following command:

```sh
beet modify turn page data_source= 'play_count!' hello=hi comp=1 mb_albumid=https://bandcamp
```

### Before
<img width="613" height="260" alt="before"
src="https://github.com/user-attachments/assets/785c4b73-69e4-4c60-b4dd-d114ee3170a1"
/>

* New field additions have been shown in red
* No difference in formatting between
  - field _removal_ (`field!`)
  - and it being reset to an empty string (`field=`)
 
### After
<img width="640" height="256" alt="after"
src="https://github.com/user-attachments/assets/89b036d7-a074-494b-a5e1-b1bf5100d454"
/>


* Now, the field name is colored in red or green whenever it's added or
removed

## 2. Small improvements in `Model` types:
* Added `NotFoundError` and `Model.get_fresh_from_db` for those cases
where `Database._get` must return a non-optional instance of `Model`
   * Added cached `Model.db` property to dedupe `Model._check_db` calls.

## 3. Added a global autouse model-level `config` fixture.
2025-12-27 14:35:43 +00:00
Šarūnas Nejus
c807effeda
Define a shared fixture for config 2025-12-27 14:30:35 +00:00
Šarūnas Nejus
75baec611a
Improve and simplify show_model_changes 2025-12-27 14:30:35 +00:00
Šarūnas Nejus
e1e0d945f8
Add NotFoundError and Model.get_fresh_from_db; tidy DB getters
Introduce NotFoundError and a Model.get_fresh_from_db helper that reloads
an object from the database and raises when missing. Use it to simplify
Model.load and UI change detection.
2025-12-27 14:26:29 +00:00
Šarūnas Nejus
8ccb33e4bc
dbcore: add Model.db cached attribute 2025-12-27 14:26:15 +00:00
Šarūnas Nejus
a62f4fb817
Introduce Info.name property and add types to match details functions (#6142)
# Generalize some of common tagging functionality to `Info` and `Match`
base classes

This PR centralises some common tagging functionality between singletons
and albums allowing to simplify `ChangeRepresentation` and importing
functionality. This is prep work for a larger PR which refactors and
simplifies the entire tagging workflow.

## Changes

- **Core type changes**: Changed `mapping` parameter from `Mapping[Item,
TrackInfo]` to `list[tuple[Item, TrackInfo]]` in `apply_metadata()`,
`distance()`, and `assign_items()` functions
- **Match dataclasses**: Converted `AlbumMatch` and `TrackMatch` from
`NamedTuple` to `@dataclass`, introducing base `Match` class with common
functionality
- **New properties**: Added `name` cached property to `Info` class for
unified name access
- **ChangeRepresentation refactor**: Converted to `@dataclass` with lazy
property evaluation, replacing `cur_album`/`cur_title` with unified
`cur_name` field
- **UI improvements**: Simplified display logic by using
`match.info.name` instead of type-specific field checks
- **Parameter renaming**: Renamed `search_album`/`search_title`
parameters to `search_name` for consistency across singleton and album
workflows

The changes maintain backward compatibility in behavior while improving
type safety and code clarity.
2025-12-24 11:06:47 +00:00
Šarūnas Nejus
60b4a38c09
Add missing type defs in import_/display.py 2025-12-24 11:01:27 +00:00
Šarūnas Nejus
7873ae56f0
hooks: introduce Info.name property 2025-12-24 11:01:26 +00:00
Šarūnas Nejus
84f6ada739
hooks: Generalise AlbumMatch and TrackMatch into Match 2025-12-24 11:01:26 +00:00
Šarūnas Nejus
acc7c2aeac
matching: replace search_title, search_album with search_name 2025-12-24 11:01:26 +00:00
Šarūnas Nejus
ea157832fe
hooks: make AlbumMatch.mapping a tuple 2025-12-24 11:01:26 +00:00
Sebastian Mohr
b05821865f
Fix edit plugin cancel flow restoring in-memory tags (#6104) (#6200)
fixes #6104

## Description

When using the `fromfilename` and `edit` plugins together during import,
aborted edit sessions could silently discard the temporary tags injected
by `fromfilename` (e.g., track number and title derived from the
filename). This happened when using `eDit` or `edit Candidates` and then
cancelling: the edit plugin reverted objects by re-reading from disk,
which does not contain the `fromfilename`-generated metadata.

This PR changes the `edit` plugin so that cancel and “continue Editing”
both roll back objects to the original in-memory snapshot captured
before opening the editor, instead of reloading from the files. This
preserves temporary tags provided by other plugins (like `fromfilename`)
across aborted edit sessions, while still only writing to disk when the
user chooses Apply.

`importer_edit` is also updated to rely on this rollback behavior when
edits are cancelled, rather than re-reading from disk, so interactive
imports resume with the same in-memory metadata they started with.
2025-12-23 15:40:23 +01:00
Sebastian Mohr
53a42bf6f6
Merge branch 'master' into gabepush-test-fix 2025-12-23 15:34:47 +01:00
Trey Turner
572645b94c Merge branch 'master' into feat/ftintitle/insert_featured_artist_before_variant_clauses 2025-12-21 21:06:31 -06:00
J0J0 Todos
5d1210ada5
importsource: Catch importer crash when skipping; Fix original changelog entry; Add new tests (#6203)
Prevents a crash when "skip" is selected in the importer and
`task.imported_items()` runs into a condition branch that supposedly
should never be reached:

```
  File "beets/beets/importer/tasks.py", line 254, in imported_items
    assert False
           ^^^^^
AssertionError
```

- Since for items/albums that should be skipped, looping through
`task.imported_items()` is not required anyway, the fix here is to exit
early from the function that calls it.
- Additionally this PR fixes the original changelog entry which was
located at an older releases "new features list". Also now it briefly
explains to changelog readers what the plugin actually does.
- Two new tests were added that proof that "skip doesn't crash" and
reimports never "suggest removal of source files"

---------

Co-authored-by: Doron Behar <doron.behar@gmail.com>
2025-12-21 20:35:27 +01:00
J0J0 Todos
9ffae4bef1 importsource: Test skip, Test reimport-skip 2025-12-21 13:07:02 +01:00
J0J0 Todos
be3485b066 Fix initial importsource plugin #4748 changelog
- Fix position (wrong release)
- Elaborate wording
2025-12-21 13:07:02 +01:00
Doron Behar
0230352da1 importsource: fix potential prevent_suggest_removal crash 2025-12-21 13:07:02 +01:00
Šarūnas Nejus
c1904b1f69
Make musicbrainz plugin talk to musicbrainz directly (#6052)
This PR refactors the MusicBrainz plugin implementation by replacing the
`musicbrainzngs` library with direct HTTP API calls using `requests` and
`requests-ratelimiter`.

**Key Changes:**

- **New utilities module**: Added `beetsplug/_utils/requests.py` with
`TimeoutSession` class and HTTP error handling (`HTTPNotFoundError`,
`CaptchaError`)
- **MusicBrainz API rewrite**: Replaced `musicbrainzngs` dependency with
custom `MusicBrainzAPI` class using direct HTTP requests
- **Rate limiting**: Integrated `requests-ratelimiter` for API rate
limiting instead of `musicbrainzngs.set_rate_limit()`
- **Data structure updates**: Updated field names to match MusicBrainz
JSON API v2 format (e.g., `medium-list` → `media`, `track-list` →
`tracks`)
- **Dependency management**: 
- Made `musicbrainzngs` optional and added it to plugin-specific extras
(`listenbrainz`, `mbcollection`, `missing`, `parentwork`). Updated
plugin docs accordingly.
- Made `requests` a required dependency to ensure backwards
compatibility (ideally, we would make it an optional dependency under
`musicbrainz` extra).
- **Error handling**: Simplified error handling by removing
`MusicBrainzAPIError` wrapper class

**Benefits:**
- Direct control over HTTP requests
- Consistent rate limiting across all network requests
- Better alignment with modern MusicBrainz API responses

The changes maintain backward compatibility while modernizing the
underlying implementation.

Fixes #5553
Fixes #5095
2025-12-21 01:08:10 +00:00
Šarūnas Nejus
5785ce3a84
Ensure that inc are joined with a plus
See this line in https://musicbrainz.org/doc/MusicBrainz_API#Lookups

> To include more than one subquery in a single request, separate the arguments to inc= with a + (plus sign), like inc=recordings+labels.
2025-12-21 01:03:20 +00:00
Šarūnas Nejus
d1aa45a008
Add retries for connection errors 2025-12-21 01:03:20 +00:00
Šarūnas Nejus
9dad040977
Add Usage block to RequestHandler 2025-12-21 00:40:40 +00:00
Šarūnas Nejus
72f7d6ebe3
Refactor HTTP request handling with RequestHandler base class
Introduce a new RequestHandler base class to introduce a shared session,
centralize HTTP request management and error handling across plugins.

Key changes:
- Add RequestHandler base class with a shared/cached session
- Convert TimeoutSession to use SingletonMeta for proper resource
  management
- Create LyricsRequestHandler subclass with lyrics-specific error
  handling
- Update MusicBrainzAPI to inherit from RequestHandler
2025-12-21 00:40:40 +00:00