_get_track resolved the artist with track_data.get('contributors', [track_data['artist']]),
whose default is evaluated eagerly, so track_data['artist'] raises KeyError whenever the
artist key is absent, even when contributors is present. Guard the fallback the same way
album_for_id already does. Fixes#4339.
_parse_label() used to store Tidal's raw copyright info as the label
name. Add _normalize_label(), a helper that strips leading markers,
years, legal entities, and boilerplate clauses.
Fixes#6796
Items with no artist or title tags produce a search with an empty query
and no filters. The request was still sent to the metadata source API,
and MusicBrainz answers it with a 400 Bad Request, which was logged with
a traceback once per affected file during an import.
Return no candidates instead of issuing a request that cannot match
anything.
Singleton searches built the query as `<title> artist:"<artist>"`. Deezer
discards unquoted free text as soon as a query contains any field:"value"
filter, so that was evaluated as `artist:"<artist>"` alone - every track by
the artist, in Deezer's own relevance order, truncated to `search_limit`
(default 5). Substituting nonsense for the title returns a byte-identical
result set. For any artist with more releases than that window, the track
being imported was simply never among the candidates offered.
Filtering on the title as well (`track:"<title>" artist:"<artist>"`) is not
a fix: `artist:` matches loosely enough to return unrelated artists, so the
two filters can intersect to nothing even for a correctly tagged file.
`track:"Get Lucky" artist:"Daft Punk"` returns zero results, while the plain
free text `Get Lucky Daft Punk` returns the right track first.
Measured over 12 tracks at the default `search_limit`, counting the wanted
track appearing anywhere in the results:
`<title> artist:"..."` 6/12, mean rank 1.50
`track:"..." artist:"..."` 7/12, mean rank 1.00
free text 10/12, mean rank 1.00
Album searches are unchanged; `album:"<name>"` has no equivalent problem.
Adds test/plugins/test_deezer.py, which did not exist.
SD_PATTERNS' featuring/feat/ft regex has no word boundary before the
alternation, so it matches "ft" embedded in ordinary words like
"draft", "left", "gift", "craft" and treats everything after it as a
low-weight suffix. "Draft Beer" vs. "Draft Whiskey" scores 0.05
(near-identical) instead of correctly registering as very different.
unique_path parses a trailing counter so it can continue from it, but the
pattern is `\.(\d)+$` -- a single-digit group repeated, not a multi-digit
group. group(1) is therefore the last digit only, and the counter restarts
from it:
track.10.mp3 -> track.1.mp3 (expected track.11.mp3)
track.12.mp3 -> track.3.mp3
track.123.mp3 -> track.4.mp3
The returned path still does not exist, so nothing is overwritten, but the
new name sorts before the file it was derived from, and the scan restarts
from a low number when the neighbours are already taken.
Use `\.(\d+)$`. Existing coverage only exercised a single-digit counter
(`x.1.mp3`), which is why this held.
A date range query whose start lies after its end, such as
`beet ls added:2024..2020`, crashed with an uncaught ValueError
raised by DateInterval's endpoint-order check:
ValueError: start date 2024-01-01 00:00:00 is not before end date
2021-01-01 00:00:00
Only InvalidQueryArgumentValueError is converted into a user-facing
InvalidQueryError by Library._get_results, so the plain ValueError
escaped all the way up as a traceback.
Rather than reporting an error, accept the range and swap its two
endpoints, so `added:2024..2020` means the same as `added:2020..2024`.
The swap lives in `DateInterval.from_periods`, which is the only place
where the two user-supplied endpoints are still available as `Period`
objects. Swapping further down, in `DateInterval.__init__`, would swap
the already-derived datetimes, and those are derived asymmetrically:
the start contributes `Period.date` while the end contributes
`Period.open_right_endpoint()`. For `2024..2020` that would yield
[2021-01-01, 2024-01-01), which excludes both 2020 and 2024 -- not the
interval the user asked for. Swapping the periods first yields
[2020-01-01, 2025-01-01), which is exactly `2020..2024`.
`DateInterval.__init__` keeps raising ValueError, since it remains the
invariant guard for callers constructing an interval from datetimes
directly, where there is no notion of the order the user typed.
The swap only triggers when the interval would otherwise be empty, so
partially overlapping mixed-precision ranges that are valid today, such
as `2000-06..2000`, are unaffected. Genuinely malformed input such as
`added:notadate` still raises InvalidQueryArgumentValueError from
`Period.parse` as before.
Co-Authored-By: Claude <noreply@anthropic.com>
## Description
Fixes#2848.
The classic Windows console (`cmd.exe`) doesn't render beets' output
(special characters, colors) correctly. Added a note in the Windows
installation guide pointing people at a terminal emulator like Windows
Terminal or cmder, or a Unix-like shell such as Git Bash.