Unify query creation logic from
- queryparse.py:construct_query_part,
- Model.field_query,
- DefaultTemplateFunctions._tmpl_unique
to a single implementation under `LibModel.field_query` class method.
This method should be used for query resolution for model (flex)fields.
Allow filtering item attributes in album queries and vice versa by
merging `flex_attrs` from Album and Item together as `all_flex_attrs`.
This field is only used for filtering and is discarded after.
For a flexible attribute query, replace the `col_name` property with
a function call that extracts that attribute from the `field_attrs`
field introduced in the earlier commit.
Additionally, for boolean, numeric and date queries CAST the value to
NUMERIC SQLite affinity to ensure that our queries like 'flex:1..5' and
'flex:true' continue working fine.
This removes the concept of 'slow query', since every query for any
field now has an SQL clause.
Now we are able to type Results simply as a Sequence. This way, we can
also now use a more generic representation `Sequence[LibModel]` to refer
to *any* of the two models.
- Track active notification tasks by connection so repeated dispatches do not
send duplicate idle responses while a previous send is still draining.
- Add regression coverage for serialized notification delivery and idle command
disconnect handling.
Return an empty string rather than INSTRUMENTAL_LYRICS from the final
fallback in LRCLyrics.get_text. That branch is not about an instrumental
track, so the marker was misleading.
Strip the LRC timestamps when synced lyrics stand in for a null
plainLyrics. The value is being used as plain text there, so the
timestamps do not belong in it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LRCLib stores track metadata independently of the lyrics themselves, so an
entry can come back with both `plainLyrics` and `syncedLyrics` null while
`instrumental` is False. `LRCLyrics.is_valid` accepted such an entry as a
match on duration alone, and `get_text` then returned `self.plain`, i.e.
None, despite being annotated `-> str`.
The None propagated into `Lyrics`, and the first access of its text raised
AttributeError: 'NoneType' object has no attribute 'splitlines'
`beet lyrics` surfaced this per track, but during an import the exception
escaped the pipeline stage and aborted the entire run: one such track
stranded every file queued behind it.
Treat an entry with no lyrics text as not a match, so the search moves on to
other candidates and backends and ultimately reports that no lyrics were
found. That is deliberately distinct from an instrumental track, where "no
lyrics" is itself the answer and the existing `instrumental` handling still
applies. Marking these as instrumental would assert something the API
response does not tell us.
Also fall back to synced lyrics when only `plainLyrics` is null, rather than
discarding lyrics we do have, and correct the annotations: `plainLyrics` and
`LRCLyrics.plain` are both nullable.
_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.
Introduce Source class to encapsulate metadata extraction logic,
replacing scattered `get_most_common_tags` calls throughout codebase.
- Add Source class in importer.tasks with artist, name, and metadata
- Simplify `distance()` to accept Source.data instead of items list
- Update display functions to use Source objects
- Cache Source creation with @cached_property on ImportTask
This centralizes metadata handling and reduces coupling between
autotag and library modules.
- Handle formats as strings ONLY and evaluate them through a shared
cached template helper.
- This removes the need for conditional logic that acts on Template
objects or strings.
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.