I humbly present a solution our lack of releases: a workflow that can be
triggered to automatically create one. This workflow builds the project,
creates a GitHub release, and publishes beets to PyPi, for a one-stop
solution.
@sampsyo this would make it much easier to create releases, as it
requires only one little interaction: going to the actions tab and
entering a version number. Once that's done, the workflow should take
care of the rest.
I have only tested the `build` job so far, since I can't do anything
about the pypi or do a release just to test, but the code is lifted from
other similar actions and should work fine.
It also requires one piece of setup. This is that PyPi must be set up
with a [trusted publisher](https://docs.pypi.org/trusted-publishers/) to
receive the new package. Once that's done, the process should go off
automatically.
Add `pytest-cov` which automatically measures coverage while tests are
executing and update tox and ci build configuration accordingly.
Add configuration for coverage:
1. Enable analysing logical branches
2. Save coverage details as HTML for local development use
3. Configure the HTML output to include coverage context: on the
right-hand side of each covered code line there is an expandable
section which lists every test that ran that line.
Enforce the same interface across all `...Query` implementations
### Make `PlaylistQuery` a `FieldQuery`
While working on the DB optimization and looking at updates upstream I discovered one query which does not follow the `FieldQuery` interface —`PlaylistQuery`, so I looked into it in more detail and ended up integrating it as a `FieldQuery`.
One special thing about it is that it uses **IN** SQL operator, so I added implementation for this sort of query outside the playlist context, see `InQuery`.
Otherwise, it seems like `PlaylistQuery` is a field query with a special way of resolving values it wants to query. In the future, we may want to consider moving this kind of custom _initialization_ logic away from `init` methods to factory/@classmethod: this should make it more clear that the purpose of such logic is to resolve the data that is required to define a particular `FieldQuery` class fully.
### Remove `NamedQuery` since it is unused
This simplifies query parsing logic in `queryparse.py`. We know that this logic can only receive `FieldQuery` classes thus I adjusted types and removed the logic that handles other cases.
Effectively, this means that the query parsing logic does not need to care whether the query is named by the corresponding DB field. Instead, queries like `SingletonQuery` and `PlaylistQuery` are initialized with the same data as others and take things from there themselves: in this case they translate `singleton` and `playlist` queries to the underlying DB filters.
Remove 'NamedQuery' since it is not being used by any queries any more.
This simplifies query parsing logic in 'queryparse.py'. We know that
this logic can only receive 'FieldQuery' thus I adjusted types and
removed the logic that handles other cases.
Effectively, this means that the query parsing logic does not any more
care whether the query is named by the corresponding DB field. Instead,
queries like 'SingletonQuery' and 'PlaylistQuery' are responsible for
translating 'singleton' and 'playlist' to the underlying DB filters.
While working on the DB optimisation I discovered one query which does
not follow the 'FieldQuery' interface - 'PlaylistQuery', so I looked
into it in more detail.
One special thing about it is that it uses 'IN' SQL operator, so
I defined 'InQuery' query class to have this logic outside of the
playlist context.
Otherwise, it seems like 'PlaylistQuery' is a field query, even if it
has a very special way of resolving values it wants to query. In the
future, we may want to consider moving this kind of custom
_initialisation_ logic away from '__init__' methods to
factory/@classmethod: this should make it more clear that the purpose of
such logic is to resolve the data that is required to define
a particular FieldQuery class fully.
As I was devving, I did something wrong and had 'beet mv' command fail on me.
Later, having spent an hour investigating why beets kept throwing me
'User-defined function raised exception' I discovered that it was
failing because that previous 'beet mv' command ended up writing value
'NULL' in one of the items 'path' column. This was not handled well by
the 'BYTELOWER' implementation.
Since we do not have a NOT NULL constraint for the 'path' column, it's
best to insure ourselves against this kind of stuff anyways.
When unix tools make use of an external editor, they typically check the
environment variable VISUAL and fall back to EDITOR. This commit adds the
additional check for VISUAL to the existing EDITOR check (where VISUAL is
preferred over EDITOR).
Allow generating extm3u playlists so that they contain additional item fields such as the `id`.
The feature is required by the mgoltzsche/beets-webm3u plugin (M3U server) to transform playlists using a request based item URI template which may require additional fields such as the `id`, e.g. `beets:library:track;$id`.