mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Merge branch 'master' into ftintitle-continue-even-if-albumartist-and-artist-is-the-same
This commit is contained in:
commit
00e3da1a92
28 changed files with 627 additions and 358 deletions
2
.github/workflows/changelog_reminder.yaml
vendored
2
.github/workflows/changelog_reminder.yaml
vendored
|
|
@ -10,7 +10,7 @@ jobs:
|
|||
check_changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get all updated Python files
|
||||
id: changed-python-files
|
||||
|
|
|
|||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
|
|
@ -25,12 +25,12 @@ jobs:
|
|||
env:
|
||||
IS_MAIN_PYTHON: ${{ matrix.python-version == '3.9' && matrix.platform == 'ubuntu-latest' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- name: Setup Python with poetry caching
|
||||
# poetry cache requires poetry to already be installed, weirdly
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
|
|
@ -90,10 +90,10 @@ jobs:
|
|||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get the coverage report
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: coverage-report
|
||||
|
||||
|
|
|
|||
4
.github/workflows/integration_test.yaml
vendored
4
.github/workflows/integration_test.yaml
vendored
|
|
@ -7,10 +7,10 @@ jobs:
|
|||
test_integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: poetry
|
||||
|
|
|
|||
18
.github/workflows/lint.yml
vendored
18
.github/workflows/lint.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }}
|
||||
changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get changed docs files
|
||||
id: changed-doc-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
|
|
@ -56,10 +56,10 @@ jobs:
|
|||
name: Check formatting
|
||||
needs: changed-files
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: poetry
|
||||
|
|
@ -77,10 +77,10 @@ jobs:
|
|||
name: Check linting
|
||||
needs: changed-files
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: poetry
|
||||
|
|
@ -97,10 +97,10 @@ jobs:
|
|||
name: Check types with mypy
|
||||
needs: changed-files
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: poetry
|
||||
|
|
@ -120,10 +120,10 @@ jobs:
|
|||
name: Check docs
|
||||
needs: changed-files
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: poetry
|
||||
|
|
|
|||
12
.github/workflows/make_release.yaml
vendored
12
.github/workflows/make_release.yaml
vendored
|
|
@ -17,10 +17,10 @@ jobs:
|
|||
name: Bump version, commit and create tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: poetry
|
||||
|
|
@ -45,13 +45,13 @@ jobs:
|
|||
outputs:
|
||||
changelog: ${{ steps.generate_changelog.outputs.changelog }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ env.NEW_TAG }}
|
||||
|
||||
- name: Install Python tools
|
||||
uses: BrandonLWhite/pipx-install-action@v1.0.3
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: poetry
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
id-token: write
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
|
@ -107,7 +107,7 @@ jobs:
|
|||
CHANGELOG: ${{ needs.build.outputs.changelog }}
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -94,3 +94,6 @@ ENV/
|
|||
|
||||
# pyright
|
||||
pyrightconfig.json
|
||||
|
||||
# Pyrefly
|
||||
pyrefly.toml
|
||||
|
|
|
|||
|
|
@ -940,10 +940,10 @@ class Transaction:
|
|||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[Exception],
|
||||
exc_value: Exception,
|
||||
traceback: TracebackType,
|
||||
):
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool | None:
|
||||
"""Complete a transaction. This must be the most recently
|
||||
entered but not yet exited transaction. If it is the last active
|
||||
transaction, the database updates are committed.
|
||||
|
|
@ -965,6 +965,8 @@ class Transaction:
|
|||
):
|
||||
raise DBCustomFunctionError()
|
||||
|
||||
return None
|
||||
|
||||
def query(
|
||||
self, statement: str, subvals: Sequence[SQLiteType] = ()
|
||||
) -> list[sqlite3.Row]:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from typing import (
|
|||
NamedTuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from unidecode import unidecode
|
||||
|
|
@ -1052,7 +1053,7 @@ def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
|
|||
pool.join()
|
||||
|
||||
|
||||
class cached_classproperty:
|
||||
class cached_classproperty(Generic[T]):
|
||||
"""Descriptor implementing cached class properties.
|
||||
|
||||
Provides class-level dynamic property behavior where the getter function is
|
||||
|
|
@ -1060,9 +1061,9 @@ class cached_classproperty:
|
|||
instance properties, this operates on the class rather than instances.
|
||||
"""
|
||||
|
||||
cache: ClassVar[dict[tuple[Any, str], Any]] = {}
|
||||
cache: ClassVar[dict[tuple[type[object], str], object]] = {}
|
||||
|
||||
name: str
|
||||
name: str = ""
|
||||
|
||||
# Ideally, we would like to use `Callable[[type[T]], Any]` here,
|
||||
# however, `mypy` is unable to see this as a **class** property, and thinks
|
||||
|
|
@ -1078,21 +1079,21 @@ class cached_classproperty:
|
|||
# "Callable[[Album], ...]"; expected "Callable[[type[Album]], ...]"
|
||||
#
|
||||
# Therefore, we just use `Any` here, which is not ideal, but works.
|
||||
def __init__(self, getter: Callable[[Any], Any]) -> None:
|
||||
def __init__(self, getter: Callable[..., T]) -> None:
|
||||
"""Initialize the descriptor with the property getter function."""
|
||||
self.getter = getter
|
||||
self.getter: Callable[..., T] = getter
|
||||
|
||||
def __set_name__(self, owner: Any, name: str) -> None:
|
||||
def __set_name__(self, owner: object, name: str) -> None:
|
||||
"""Capture the attribute name this descriptor is assigned to."""
|
||||
self.name = name
|
||||
|
||||
def __get__(self, instance: Any, owner: type[Any]) -> Any:
|
||||
def __get__(self, instance: object, owner: type[object]) -> T:
|
||||
"""Compute and cache if needed, and return the property value."""
|
||||
key = owner, self.name
|
||||
key: tuple[type[object], str] = owner, self.name
|
||||
if key not in self.cache:
|
||||
self.cache[key] = self.getter(owner)
|
||||
|
||||
return self.cache[key]
|
||||
return cast(T, self.cache[key])
|
||||
|
||||
|
||||
class LazySharedInstance(Generic[T]):
|
||||
|
|
|
|||
|
|
@ -132,9 +132,9 @@ class DiscogsPlugin(MetadataSourcePlugin):
|
|||
"user_token": "",
|
||||
"separator": ", ",
|
||||
"index_tracks": False,
|
||||
"featured_string": "Feat.",
|
||||
"append_style_genre": False,
|
||||
"strip_disambiguation": True,
|
||||
"featured_string": "Feat.",
|
||||
"anv": {
|
||||
"artist_credit": True,
|
||||
"artist": False,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ from beets.util import get_temp_filename
|
|||
# If this is missing, they're placed at the end.
|
||||
ARGS_MARKER = "$args"
|
||||
|
||||
# Indicate where the playlist file (with absolute path) should be inserted into
|
||||
# the command string. If this is missing, its placed at the end, but before
|
||||
# arguments.
|
||||
PLS_MARKER = "$playlist"
|
||||
|
||||
|
||||
def play(
|
||||
command_str,
|
||||
|
|
@ -132,8 +137,23 @@ class PlayPlugin(BeetsPlugin):
|
|||
return
|
||||
|
||||
open_args = self._playlist_or_paths(paths)
|
||||
open_args_str = [
|
||||
p.decode("utf-8") for p in self._playlist_or_paths(paths)
|
||||
]
|
||||
command_str = self._command_str(opts.args)
|
||||
|
||||
if PLS_MARKER in command_str:
|
||||
if not config["play"]["raw"]:
|
||||
command_str = command_str.replace(
|
||||
PLS_MARKER, "".join(open_args_str)
|
||||
)
|
||||
self._log.debug(
|
||||
"command altered by PLS_MARKER to: {}", command_str
|
||||
)
|
||||
open_args = []
|
||||
else:
|
||||
command_str = command_str.replace(PLS_MARKER, " ")
|
||||
|
||||
# Check if the selection exceeds configured threshold. If True,
|
||||
# cancel, otherwise proceed with play command.
|
||||
if opts.yes or not self._exceeds_threshold(
|
||||
|
|
@ -162,6 +182,7 @@ class PlayPlugin(BeetsPlugin):
|
|||
return paths
|
||||
else:
|
||||
return [self._create_tmp_playlist(paths)]
|
||||
return [shlex.quote(self._create_tmp_playlist(paths))]
|
||||
|
||||
def _exceeds_threshold(
|
||||
self, selection, command_str, open_args, item_type="track"
|
||||
|
|
|
|||
|
|
@ -241,6 +241,11 @@ var AppView = Backbone.View.extend({
|
|||
'pause': _.bind(this.audioPause, this),
|
||||
'ended': _.bind(this.audioEnded, this)
|
||||
});
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.setActionHandler("nexttrack", () => {
|
||||
this.playNext();
|
||||
});
|
||||
}
|
||||
},
|
||||
showItems: function(items) {
|
||||
this.shownItems = items;
|
||||
|
|
@ -306,7 +311,9 @@ var AppView = Backbone.View.extend({
|
|||
},
|
||||
audioEnded: function() {
|
||||
this.playingItem.entryView.setPlaying(false);
|
||||
|
||||
this.playNext();
|
||||
},
|
||||
playNext: function(){
|
||||
// Try to play the next track.
|
||||
var idx = this.shownItems.indexOf(this.playingItem);
|
||||
if (idx == -1) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ New features:
|
|||
- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.
|
||||
- :doc:`plugins/ftintitle`: Added argument to skip the processing of artist and
|
||||
album artist are the same in ftintitle.
|
||||
- :doc:`plugins/play`: Added `$playlist` marker to precisely edit the playlist
|
||||
filepath into the command calling the player program.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
|
@ -48,6 +50,10 @@ Other changes:
|
|||
- :doc:`guides/main`: Modernized the *Getting Started* guide with tabbed
|
||||
sections and dropdown menus. Installation instructions have been streamlined,
|
||||
and a new subpage now provides additional setup details.
|
||||
- Documentation: introduced a new role ``conf`` for documenting configuration
|
||||
options. This role provides consistent formatting and creates references
|
||||
automatically. Applied it to :doc:`plugins/deezer`, :doc:`plugins/discogs`,
|
||||
:doc:`plugins/musicbrainz` and :doc:`plugins/spotify` plugins documentation.
|
||||
|
||||
2.5.0 (October 11, 2025)
|
||||
------------------------
|
||||
|
|
@ -58,16 +64,18 @@ New features:
|
|||
without storing or writing them.
|
||||
- :doc:`plugins/convert`: Add a config option to disable writing metadata to
|
||||
converted files.
|
||||
- :doc:`plugins/discogs`: New config option `strip_disambiguation` to toggle
|
||||
stripping discogs numeric disambiguation on artist and label fields.
|
||||
- :doc:`plugins/discogs`: New config option
|
||||
:conf:`plugins.discogs:strip_disambiguation` to toggle stripping discogs
|
||||
numeric disambiguation on artist and label fields.
|
||||
- :doc:`plugins/discogs` Added support for featured artists. :bug:`6038`
|
||||
- :doc:`plugins/discogs` New configuration option `featured_string` to change
|
||||
the default string used to join featured artists. The default string is
|
||||
`Feat.`.
|
||||
- :doc:`plugins/discogs` New configuration option
|
||||
:conf:`plugins.discogs:featured_string` to change the default string used to
|
||||
join featured artists. The default string is `Feat.`.
|
||||
- :doc:`plugins/discogs` Support for `artist_credit` in Discogs tags.
|
||||
:bug:`3354`
|
||||
- :doc:`plugins/discogs` Support for name variations and config options to
|
||||
specify where the variations are written. :bug:`3354`
|
||||
- :doc:`plugins/web` Support for `nexttrack` keyboard press
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
|
@ -91,9 +99,10 @@ Bug fixes:
|
|||
- :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor
|
||||
regexps, allow for more cases, add some logging), add tests.
|
||||
- Metadata source plugins: Fixed data source penalty calculation that was
|
||||
incorrectly applied during import matching. The ``source_weight``
|
||||
configuration option has been renamed to ``data_source_mismatch_penalty`` to
|
||||
better reflect its purpose. :bug:`6066`
|
||||
incorrectly applied during import matching. The
|
||||
:conf:`plugins.index:source_weight` configuration option has been renamed to
|
||||
:conf:`plugins.index:data_source_mismatch_penalty` to better reflect its
|
||||
purpose. :bug:`6066`
|
||||
|
||||
Other changes:
|
||||
|
||||
|
|
@ -139,12 +148,13 @@ New features:
|
|||
separate plugin. The default :ref:`plugins-config` includes ``musicbrainz``,
|
||||
but if you've customized your ``plugins`` list in your configuration, you'll
|
||||
need to explicitly add ``musicbrainz`` to continue using this functionality.
|
||||
Configuration option ``musicbrainz.enabled`` has thus been deprecated.
|
||||
:bug:`2686` :bug:`4605`
|
||||
Configuration option :conf:`plugins.musicbrainz:enabled` has thus been
|
||||
deprecated. :bug:`2686` :bug:`4605`
|
||||
- :doc:`plugins/web`: Show notifications when a track plays. This uses the Media
|
||||
Session API to customize media notifications.
|
||||
- :doc:`plugins/discogs`: Add configurable ``search_limit`` option to limit the
|
||||
number of results returned by the Discogs metadata search queries.
|
||||
- :doc:`plugins/discogs`: Add configurable :conf:`plugins.discogs:search_limit`
|
||||
option to limit the number of results returned by the Discogs metadata search
|
||||
queries.
|
||||
- :doc:`plugins/discogs`: Implement ``track_for_id`` method to allow retrieving
|
||||
singletons by their Discogs ID. :bug:`4661`
|
||||
- :doc:`plugins/replace`: Add new plugin.
|
||||
|
|
@ -159,12 +169,13 @@ New features:
|
|||
be played for it to be counted as played instead of skipped.
|
||||
- :doc:`plugins/web`: Display artist and album as part of the search results.
|
||||
- :doc:`plugins/spotify` :doc:`plugins/deezer`: Add new configuration option
|
||||
``search_limit`` to limit the number of results returned by search queries.
|
||||
:conf:`plugins.index:search_limit` to limit the number of results returned by
|
||||
search queries.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- :doc:`plugins/musicbrainz`: fix regression where user configured
|
||||
``extra_tags`` have been read incorrectly. :bug:`5788`
|
||||
:conf:`plugins.musicbrainz:extra_tags` have been read incorrectly. :bug:`5788`
|
||||
- tests: Fix library tests failing on Windows when run from outside ``D:/``.
|
||||
:bug:`5802`
|
||||
- Fix an issue where calling ``Library.add`` would cause the ``database_change``
|
||||
|
|
@ -196,9 +207,10 @@ Bug fixes:
|
|||
|
||||
For packagers:
|
||||
|
||||
- Optional ``extra_tags`` parameter has been removed from
|
||||
``BeetsPlugin.candidates`` method signature since it is never passed in. If
|
||||
you override this method in your plugin, feel free to remove this parameter.
|
||||
- Optional :conf:`plugins.musicbrainz:extra_tags` parameter has been removed
|
||||
from ``BeetsPlugin.candidates`` method signature since it is never passed in.
|
||||
If you override this method in your plugin, feel free to remove this
|
||||
parameter.
|
||||
- Loosened ``typing_extensions`` dependency in pyproject.toml to apply to every
|
||||
python version.
|
||||
|
||||
|
|
@ -554,8 +566,9 @@ New features:
|
|||
:bug:`4348`
|
||||
- Create the parental directories for database if they do not exist. :bug:`3808`
|
||||
:bug:`4327`
|
||||
- :ref:`musicbrainz-config`: a new :ref:`musicbrainz.enabled` option allows
|
||||
disabling the MusicBrainz metadata source during the autotagging process
|
||||
- :ref:`musicbrainz-config`: a new :conf:`plugins.musicbrainz:enabled` option
|
||||
allows disabling the MusicBrainz metadata source during the autotagging
|
||||
process
|
||||
- :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances :bug:`4101`
|
||||
- Add the item fields ``bitrate_mode``, ``encoder_info`` and
|
||||
``encoder_settings``.
|
||||
|
|
@ -588,8 +601,8 @@ New features:
|
|||
:bug:`4561` :bug:`4600`
|
||||
- :ref:`musicbrainz-config`: MusicBrainz release pages often link to related
|
||||
metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When
|
||||
enabled via the :ref:`musicbrainz.external_ids` options, release ID's will be
|
||||
extracted from those URL's and imported to the library. :bug:`4220`
|
||||
enabled via the :conf:`plugins.musicbrainz:external_ids` options, release ID's
|
||||
will be extracted from those URL's and imported to the library. :bug:`4220`
|
||||
- :doc:`/plugins/convert`: Add support for generating m3u8 playlists together
|
||||
with converted media files. :bug:`4373`
|
||||
- Fetch the ``release_group_title`` field from MusicBrainz. :bug:`4809`
|
||||
|
|
@ -943,8 +956,9 @@ Other new things:
|
|||
|
||||
- ``beet remove`` now also allows interactive selection of items from the query,
|
||||
similar to ``beet modify``.
|
||||
- Enable HTTPS for MusicBrainz by default and add configuration option ``https``
|
||||
for custom servers. See :ref:`musicbrainz-config` for more details.
|
||||
- Enable HTTPS for MusicBrainz by default and add configuration option
|
||||
:conf:`plugins.musicbrainz:https` for custom servers. See
|
||||
:ref:`musicbrainz-config` for more details.
|
||||
- :doc:`/plugins/mpdstats`: Add a new ``strip_path`` option to help build the
|
||||
right local path from MPD information.
|
||||
- :doc:`/plugins/convert`: Conversion can now parallelize conversion jobs on
|
||||
|
|
@ -964,8 +978,8 @@ Other new things:
|
|||
server.
|
||||
- :doc:`/plugins/subsonicupdate`: The plugin now automatically chooses between
|
||||
token- and password-based authentication based on the server version.
|
||||
- A new :ref:`extra_tags` configuration option lets you use more metadata in
|
||||
MusicBrainz queries to further narrow the search.
|
||||
- A new :conf:`plugins.musicbrainz:extra_tags` configuration option lets you use
|
||||
more metadata in MusicBrainz queries to further narrow the search.
|
||||
- A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets.
|
||||
- :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality``
|
||||
option that controls the quality of the image output when the image is
|
||||
|
|
@ -1019,9 +1033,9 @@ Other new things:
|
|||
(and now deprecated) separate ``host``, ``port``, and ``contextpath`` config
|
||||
options. As a consequence, the plugin can now talk to Subsonic over HTTPS.
|
||||
Thanks to :user:`jef`. :bug:`3449`
|
||||
- :doc:`/plugins/discogs`: The new ``index_tracks`` option enables incorporation
|
||||
of work names and intra-work divisions into imported track titles. Thanks to
|
||||
:user:`cole-miller`. :bug:`3459`
|
||||
- :doc:`/plugins/discogs`: The new :conf:`plugins.discogs:index_tracks` option
|
||||
enables incorporation of work names and intra-work divisions into imported
|
||||
track titles. Thanks to :user:`cole-miller`. :bug:`3459`
|
||||
- :doc:`/plugins/web`: The query API now interprets backslashes as path
|
||||
separators to support path queries. Thanks to :user:`nmeum`. :bug:`3567`
|
||||
- ``beet import`` now handles tar archives with bzip2 or gzip compression.
|
||||
|
|
@ -1035,9 +1049,9 @@ Other new things:
|
|||
:user:`logan-arens`. :bug:`2947`
|
||||
- There is a new ``--plugins`` (or ``-p``) CLI flag to specify a list of plugins
|
||||
to load.
|
||||
- A new :ref:`genres` option fetches genre information from MusicBrainz. This
|
||||
functionality depends on functionality that is currently unreleased in the
|
||||
python-musicbrainzngs_ library: see PR `#266
|
||||
- A new :conf:`plugins.musicbrainz:genres` option fetches genre information from
|
||||
MusicBrainz. This functionality depends on functionality that is currently
|
||||
unreleased in the python-musicbrainzngs_ library: see PR `#266
|
||||
<https://github.com/alastair/python-musicbrainzngs/pull/266>`_. Thanks to
|
||||
:user:`aereaux`.
|
||||
- :doc:`/plugins/replaygain`: Analysis now happens in parallel using the
|
||||
|
|
@ -1077,9 +1091,10 @@ Fixes:
|
|||
:bug:`3867`
|
||||
- :doc:`/plugins/web`: Fixed a small bug that caused the album art path to be
|
||||
redacted even when ``include_paths`` option is set. :bug:`3866`
|
||||
- :doc:`/plugins/discogs`: Fixed a bug with the ``index_tracks`` option that
|
||||
sometimes caused the index to be discarded. Also, remove the extra semicolon
|
||||
that was added when there is no index track.
|
||||
- :doc:`/plugins/discogs`: Fixed a bug with the
|
||||
:conf:`plugins.discogs:index_tracks` option that sometimes caused the index to
|
||||
be discarded. Also, remove the extra semicolon that was added when there is no
|
||||
index track.
|
||||
- :doc:`/plugins/subsonicupdate`: The API client was using the ``POST`` method
|
||||
rather the ``GET`` method. Also includes better exception handling, response
|
||||
parsing, and tests.
|
||||
|
|
@ -2695,9 +2710,9 @@ Major new features and bigger changes:
|
|||
analysis tool. Thanks to :user:`jmwatte`. :bug:`1343`
|
||||
- A new ``filesize`` field on items indicates the number of bytes in the file.
|
||||
:bug:`1291`
|
||||
- A new :ref:`search_limit` configuration option allows you to specify how many
|
||||
search results you wish to see when looking up releases at MusicBrainz during
|
||||
import. :bug:`1245`
|
||||
- A new :conf:`plugins.index:search_limit` configuration option allows you to
|
||||
specify how many search results you wish to see when looking up releases at
|
||||
MusicBrainz during import. :bug:`1245`
|
||||
- The importer now records the data source for a match in a new flexible
|
||||
attribute ``data_source`` on items and albums. :bug:`1311`
|
||||
- The colors used in the terminal interface are now configurable via the new
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add custom extensions directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent / "extensions"))
|
||||
|
||||
project = "beets"
|
||||
AUTHOR = "Adrian Sampson"
|
||||
|
|
@ -26,6 +31,7 @@ extensions = [
|
|||
"sphinx.ext.viewcode",
|
||||
"sphinx_design",
|
||||
"sphinx_copybutton",
|
||||
"conf",
|
||||
]
|
||||
|
||||
autosummary_generate = True
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ str.format-style string formatting. So you can write logging calls like this:
|
|||
|
||||
.. _pep 3101: https://www.python.org/dev/peps/pep-3101/
|
||||
|
||||
.. _standard python logging module: https://docs.python.org/2/library/logging.html
|
||||
.. _standard python logging module: https://docs.python.org/3/library/logging.html
|
||||
|
||||
When beets is in verbose mode, plugin messages are prefixed with the plugin name
|
||||
to make them easier to see.
|
||||
|
|
|
|||
142
docs/extensions/conf.py
Normal file
142
docs/extensions/conf.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""Sphinx extension for simple configuration value documentation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import directives
|
||||
from sphinx import addnodes
|
||||
from sphinx.directives import ObjectDescription
|
||||
from sphinx.domains import Domain, ObjType
|
||||
from sphinx.roles import XRefRole
|
||||
from sphinx.util.nodes import make_refnode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
from docutils.nodes import Element
|
||||
from docutils.parsers.rst.states import Inliner
|
||||
from sphinx.addnodes import desc_signature, pending_xref
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.util.typing import ExtensionMetadata, OptionSpec
|
||||
|
||||
|
||||
class Conf(ObjectDescription[str]):
|
||||
"""Directive for documenting a single configuration value."""
|
||||
|
||||
option_spec: ClassVar[OptionSpec] = {
|
||||
"default": directives.unchanged,
|
||||
}
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
"""Process the directive signature (the config name)."""
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
|
||||
# Add default value if provided
|
||||
if "default" in self.options:
|
||||
signode += nodes.Text(" ")
|
||||
default_container = nodes.inline("", "")
|
||||
default_container += nodes.Text("(default: ")
|
||||
default_container += nodes.literal("", self.options["default"])
|
||||
default_container += nodes.Text(")")
|
||||
signode += default_container
|
||||
|
||||
return sig
|
||||
|
||||
def add_target_and_index(
|
||||
self, name: str, sig: str, signode: desc_signature
|
||||
) -> None:
|
||||
"""Add cross-reference target and index entry."""
|
||||
target = f"conf-{name}"
|
||||
if target not in self.state.document.ids:
|
||||
signode["ids"].append(target)
|
||||
self.state.document.note_explicit_target(signode)
|
||||
|
||||
# A unique full name which includes the document name
|
||||
index_name = f"{self.env.docname.replace('/', '.')}:{name}"
|
||||
# Register with the conf domain
|
||||
domain = self.env.get_domain("conf")
|
||||
domain.data["objects"][index_name] = (self.env.docname, target)
|
||||
|
||||
# Add to index
|
||||
self.indexnode["entries"].append(
|
||||
("single", f"{name} (configuration value)", target, "", None)
|
||||
)
|
||||
|
||||
|
||||
class ConfDomain(Domain):
|
||||
"""Domain for simple configuration values."""
|
||||
|
||||
name = "conf"
|
||||
label = "Simple Configuration"
|
||||
object_types = {"conf": ObjType("conf", "conf")}
|
||||
directives = {"conf": Conf}
|
||||
roles = {"conf": XRefRole()}
|
||||
initial_data: dict[str, Any] = {"objects": {}}
|
||||
|
||||
def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:
|
||||
"""Return an iterable of object tuples for the inventory."""
|
||||
for name, (docname, targetname) in self.data["objects"].items():
|
||||
# Remove the document name prefix for display
|
||||
display_name = name.split(":")[-1]
|
||||
yield (name, display_name, "conf", docname, targetname, 1)
|
||||
|
||||
def resolve_xref(
|
||||
self,
|
||||
env: BuildEnvironment,
|
||||
fromdocname: str,
|
||||
builder: Builder,
|
||||
typ: str,
|
||||
target: str,
|
||||
node: pending_xref,
|
||||
contnode: Element,
|
||||
) -> Element | None:
|
||||
if entry := self.data["objects"].get(target):
|
||||
docname, targetid = entry
|
||||
return make_refnode(
|
||||
builder, fromdocname, docname, targetid, contnode
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# sphinx.util.typing.RoleFunction
|
||||
def conf_role(
|
||||
name: str,
|
||||
rawtext: str,
|
||||
text: str,
|
||||
lineno: int,
|
||||
inliner: Inliner,
|
||||
/,
|
||||
options: dict[str, Any] | None = None,
|
||||
content: Sequence[str] = (),
|
||||
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
|
||||
"""Role for referencing configuration values."""
|
||||
node = addnodes.pending_xref(
|
||||
"",
|
||||
refdomain="conf",
|
||||
reftype="conf",
|
||||
reftarget=text,
|
||||
refwarn=True,
|
||||
**(options or {}),
|
||||
)
|
||||
node += nodes.literal(text, text.split(":")[-1])
|
||||
return [node], []
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
app.add_domain(ConfDomain)
|
||||
|
||||
# register a top-level directive so users can use ".. conf:: ..."
|
||||
app.add_directive("conf", Conf)
|
||||
|
||||
# Register role with short name
|
||||
app.add_role("conf", conf_role)
|
||||
return {
|
||||
"version": "0.1",
|
||||
"parallel_read_safe": True,
|
||||
"parallel_write_safe": True,
|
||||
}
|
||||
|
|
@ -35,15 +35,23 @@ Default
|
|||
.. code-block:: yaml
|
||||
|
||||
deezer:
|
||||
search_query_ascii: no
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
search_query_ascii: no
|
||||
|
||||
- **search_query_ascii**: If set to ``yes``, the search query will be converted
|
||||
to ASCII before being sent to Deezer. Converting searches to ASCII can enhance
|
||||
search results in some cases, but in general, it is not recommended. For
|
||||
instance ``artist:deadmau5 album:4×4`` will be converted to ``artist:deadmau5
|
||||
album:4x4`` (notice ``×!=x``). Default: ``no``.
|
||||
.. conf:: search_query_ascii
|
||||
:default: no
|
||||
|
||||
If enabled, the search query will be converted to ASCII before being sent to
|
||||
Deezer. Converting searches to ASCII can enhance search results in some cases,
|
||||
but in general, it is not recommended. For instance, ``artist:deadmau5
|
||||
album:4×4`` will be converted to ``artist:deadmau5 album:4x4`` (notice
|
||||
``×!=x``).
|
||||
|
||||
.. include:: ./shared_metadata_source_config.rst
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
The ``deezer`` plugin provides an additional command ``deezerupdate`` to update
|
||||
the ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a
|
||||
|
|
|
|||
|
|
@ -71,21 +71,29 @@ Default
|
|||
.. code-block:: yaml
|
||||
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
apikey: REDACTED
|
||||
apisecret: REDACTED
|
||||
tokenfile: discogs_token.json
|
||||
user_token: REDACTED
|
||||
user_token:
|
||||
index_tracks: no
|
||||
append_style_genre: no
|
||||
separator: ', '
|
||||
strip_disambiguation: yes
|
||||
featured_string: Feat.
|
||||
anv:
|
||||
artist_credit: yes
|
||||
artist: no
|
||||
album_artist: no
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
|
||||
- **index_tracks**: Index tracks (see the `Discogs guidelines`_) along with
|
||||
headers, mark divisions between distinct works on the same release or within
|
||||
works. When enabled, beets will incorporate the names of the divisions
|
||||
containing each track into the imported track's title. Default: ``no``.
|
||||
.. conf:: index_tracks
|
||||
:default: no
|
||||
|
||||
Index tracks (see the `Discogs guidelines`_) along with headers, mark divisions
|
||||
between distinct works on the same release or within works. When enabled,
|
||||
beets will incorporate the names of the divisions containing each track into the
|
||||
imported track's title.
|
||||
|
||||
For example, importing `divisions album`_ would result in track names like:
|
||||
|
||||
|
|
@ -105,20 +113,36 @@ Default
|
|||
|
||||
This option is useful when importing classical music.
|
||||
|
||||
- **append_style_genre**: Appends the Discogs style (if found) to the genre tag.
|
||||
This can be useful if you want more granular genres to categorize your music.
|
||||
For example, a release in Discogs might have a genre of "Electronic" and a
|
||||
style of "Techno": enabling this setting would set the genre to be
|
||||
"Electronic, Techno" (assuming default separator of ``", "``) instead of just
|
||||
"Electronic". Default: ``False``
|
||||
- **separator**: How to join multiple genre and style values from Discogs into a
|
||||
string. Default: ``", "``
|
||||
- **strip_disambiguation**: Discogs uses strings like ``"(4)"`` to mark distinct
|
||||
artists and labels with the same name. If you'd like to use the discogs
|
||||
disambiguation in your tags, you can disable it. Default: ``True``
|
||||
- **featured_string**: Configure the string used for noting featured artists.
|
||||
Useful if you prefer ``Featuring`` or ``ft.``. Default: ``Feat.``
|
||||
- **anv**: These configuration option are dedicated to handling Artist Name
|
||||
.. conf:: append_style_genre
|
||||
:default: no
|
||||
|
||||
Appends the Discogs style (if found) to the genre tag. This can be useful if
|
||||
you want more granular genres to categorize your music. For example,
|
||||
a release in Discogs might have a genre of "Electronic" and a style of
|
||||
"Techno": enabling this setting would set the genre to be "Electronic,
|
||||
Techno" (assuming default separator of ``", "``) instead of just
|
||||
"Electronic".
|
||||
|
||||
.. conf:: separator
|
||||
:default: ", "
|
||||
|
||||
How to join multiple genre and style values from Discogs into a string.
|
||||
|
||||
.. conf:: strip_disambiguation
|
||||
:default: yes
|
||||
|
||||
Discogs uses strings like ``"(4)"`` to mark distinct artists and labels with
|
||||
the same name. If you'd like to use the Discogs disambiguation in your tags,
|
||||
you can disable this option.
|
||||
|
||||
.. conf:: featured_string
|
||||
:default: Feat.
|
||||
|
||||
Configure the string used for noting featured artists. Useful if you prefer ``Featuring`` or ``ft.``.
|
||||
|
||||
.. conf:: anv
|
||||
|
||||
This configuration option is dedicated to handling Artist Name
|
||||
Variations (ANVs). Sometimes a release credits artists differently compared to
|
||||
the majority of their work. For example, "Basement Jaxx" may be credited as
|
||||
"Tha Jaxx" or "The Basement Jaxx". You can select any combination of these
|
||||
|
|
@ -129,9 +153,11 @@ Default
|
|||
|
||||
discogs:
|
||||
anv:
|
||||
artist_credit: True
|
||||
artist: False
|
||||
album_artist: False
|
||||
artist_credit: yes
|
||||
artist: no
|
||||
album_artist: no
|
||||
|
||||
.. include:: ./shared_metadata_source_config.rst
|
||||
|
||||
.. _discogs guidelines: https://support.discogs.com/hc/en-us/articles/360005055373-Database-Guidelines-12-Tracklisting#Index_Tracks_And_Headings
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ These options match the options from the `Python csv module`_.
|
|||
|
||||
.. _python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params
|
||||
|
||||
.. _python json module: https://docs.python.org/2/library/json.html#basic-usage
|
||||
.. _python json module: https://docs.python.org/3/library/json.html#basic-usage
|
||||
|
||||
The default options look like this:
|
||||
|
||||
|
|
|
|||
|
|
@ -50,65 +50,7 @@ Using Metadata Source Plugins
|
|||
We provide several :ref:`autotagger_extensions` that fetch metadata from online
|
||||
databases. They share the following configuration options:
|
||||
|
||||
.. _data_source_mismatch_penalty:
|
||||
|
||||
- **data_source_mismatch_penalty**: Penalty applied when the data source of a
|
||||
match candidate differs from the original source of your existing tracks. Any
|
||||
decimal number between 0.0 and 1.0. Default: ``0.5``.
|
||||
|
||||
This setting controls how much to penalize matches from different metadata
|
||||
sources during import. The penalty is applied when beets detects that a match
|
||||
candidate comes from a different data source than what appears to be the
|
||||
original source of your music collection.
|
||||
|
||||
**Example configurations:**
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Prefer MusicBrainz over Discogs when sources don't match
|
||||
plugins: musicbrainz discogs
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.3 # Lower penalty = preferred
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.8 # Higher penalty = less preferred
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Do not penalise candidates from Discogs at all
|
||||
plugins: musicbrainz discogs
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.5
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.0
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Disable cross-source penalties entirely
|
||||
plugins: musicbrainz discogs
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.0
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.0
|
||||
|
||||
.. tip::
|
||||
|
||||
The last configuration is equivalent to setting:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
match:
|
||||
distance_weights:
|
||||
data_source: 0.0 # Disable data source matching
|
||||
|
||||
- **source_weight**
|
||||
|
||||
.. deprecated:: 2.5 Use `data_source_mismatch_penalty`_ instead.
|
||||
|
||||
- **search_limit**: Maximum number of search results to consider. Default:
|
||||
``5``.
|
||||
.. include:: ./shared_metadata_source_config.rst
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ Default
|
|||
.. code-block:: yaml
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
host: musicbrainz.org
|
||||
https: no
|
||||
ratelimit: 1
|
||||
|
|
@ -41,122 +39,107 @@ Default
|
|||
deezer: no
|
||||
beatport: no
|
||||
tidal: no
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
|
||||
You can instruct beets to use `your own MusicBrainz database
|
||||
.. conf:: host
|
||||
:default: musicbrainz.org
|
||||
|
||||
The Web server hostname (and port, optionally) that will be contacted by beets.
|
||||
You can use this to configure beets to use `your own MusicBrainz database
|
||||
<https://musicbrainz.org/doc/MusicBrainz_Server/Setup>`__ instead of the
|
||||
`main server`_.
|
||||
|
||||
`main server`_. Use the ``host``, ``https`` and ``ratelimit`` options under a
|
||||
``musicbrainz:`` header, like so
|
||||
The server must have search indices enabled (see `Building search indexes`_).
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
musicbrainz:
|
||||
host: localhost:5000
|
||||
https: no
|
||||
ratelimit: 100
|
||||
|
||||
The ``host`` key, of course, controls the Web server hostname (and port,
|
||||
optionally) that will be contacted by beets (default: musicbrainz.org). The
|
||||
``https`` key makes the client use HTTPS instead of HTTP. This setting applies
|
||||
only to custom servers. The official MusicBrainz server always uses HTTPS.
|
||||
(Default: no.) The server must have search indices enabled (see `Building search
|
||||
indexes`_).
|
||||
.. conf:: https
|
||||
:default: no
|
||||
|
||||
The ``ratelimit`` option, an integer, controls the number of Web service
|
||||
requests per second (default: 1). **Do not change the rate limit setting** if
|
||||
you're using the main MusicBrainz server---on this public server, you're
|
||||
limited_ to one request per second.
|
||||
Makes the client use HTTPS instead of HTTP. This setting applies only to custom
|
||||
servers. The official MusicBrainz server always uses HTTPS.
|
||||
|
||||
.. _building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup
|
||||
.. conf:: ratelimit
|
||||
:default: 1
|
||||
|
||||
.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
|
||||
Controls the number of Web service requests per second.
|
||||
|
||||
.. _main server: https://musicbrainz.org/
|
||||
**Do not change the rate limit setting** if you're using the main MusicBrainz
|
||||
server---on this public server, you're limited_ to one request per second.
|
||||
|
||||
.. _musicbrainz.enabled:
|
||||
.. conf:: ratelimit_interval
|
||||
:default: 1.0
|
||||
|
||||
enabled
|
||||
+++++++
|
||||
The time interval (in seconds) for the rate limit.
|
||||
|
||||
.. conf:: enabled
|
||||
:default: yes
|
||||
|
||||
.. deprecated:: 2.4 Add ``musicbrainz`` to the ``plugins`` list instead.
|
||||
|
||||
This option allows you to disable using MusicBrainz as a metadata source. This
|
||||
applies if you use plugins that fetch data from alternative sources and should
|
||||
make the import process quicker.
|
||||
|
||||
Default: ``yes``.
|
||||
|
||||
.. _search_limit:
|
||||
|
||||
search_limit
|
||||
++++++++++++
|
||||
|
||||
The number of matches returned when sending search queries to the MusicBrainz
|
||||
server.
|
||||
|
||||
Default: ``5``.
|
||||
|
||||
searchlimit
|
||||
+++++++++++
|
||||
|
||||
.. deprecated:: 2.4 Use `search_limit`_.
|
||||
|
||||
.. _extra_tags:
|
||||
|
||||
extra_tags
|
||||
++++++++++
|
||||
.. conf:: extra_tags
|
||||
:default: []
|
||||
|
||||
By default, beets will use only the artist, album, and track count to query
|
||||
MusicBrainz. Additional tags to be queried can be supplied with the
|
||||
``extra_tags`` setting. For example
|
||||
``extra_tags`` setting.
|
||||
|
||||
This setting should improve the autotagger results if the metadata with the
|
||||
given tags match the metadata returned by MusicBrainz.
|
||||
|
||||
Note that the only tags supported by this setting are: ``barcode``,
|
||||
``catalognum``, ``country``, ``label``, ``media``, and ``year``.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
musicbrainz:
|
||||
extra_tags: [barcode, catalognum, country, label, media, year]
|
||||
|
||||
This setting should improve the autotagger results if the metadata with the
|
||||
given tags match the metadata returned by MusicBrainz.
|
||||
|
||||
Note that the only tags supported by this setting are the ones listed in the
|
||||
above example.
|
||||
|
||||
Default: ``[]``
|
||||
|
||||
.. _genres:
|
||||
|
||||
genres
|
||||
++++++
|
||||
.. conf:: genres
|
||||
:default: no
|
||||
|
||||
Use MusicBrainz genre tags to populate (and replace if it's already set) the
|
||||
``genre`` tag. This will make it a list of all the genres tagged for the release
|
||||
and the release-group on MusicBrainz, separated by "; " and sorted by the total
|
||||
number of votes. Default: ``no``
|
||||
number of votes.
|
||||
|
||||
.. _musicbrainz.external_ids:
|
||||
.. conf:: external_ids
|
||||
|
||||
external_ids
|
||||
++++++++++++
|
||||
|
||||
Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz
|
||||
importer to look for links to related metadata sources. If such a link is
|
||||
available the release ID will be extracted from the URL provided and imported to
|
||||
the beets library
|
||||
**Default**
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
musicbrainz:
|
||||
external_ids:
|
||||
discogs: yes
|
||||
spotify: yes
|
||||
bandcamp: yes
|
||||
beatport: yes
|
||||
deezer: yes
|
||||
tidal: yes
|
||||
discogs: no
|
||||
spotify: no
|
||||
bandcamp: no
|
||||
beatport: no
|
||||
deezer: no
|
||||
tidal: no
|
||||
|
||||
Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz
|
||||
importer to look for links to related metadata sources. If such a link is
|
||||
available the release ID will be extracted from the URL provided and imported to
|
||||
the beets library.
|
||||
|
||||
The library fields of the corresponding :ref:`autotagger_extensions` are used to
|
||||
save the data (``discogs_albumid``, ``bandcamp_album_id``, ``spotify_album_id``,
|
||||
save the data as flexible attributes (``discogs_album_id``, ``bandcamp_album_id``, ``spotify_album_id``,
|
||||
``beatport_album_id``, ``deezer_album_id``, ``tidal_album_id``). On re-imports
|
||||
existing data will be overwritten.
|
||||
|
||||
The default of all options is ``no``.
|
||||
.. include:: ./shared_metadata_source_config.rst
|
||||
|
||||
.. _building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup
|
||||
|
||||
.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
|
||||
|
||||
.. _main server: https://musicbrainz.org/
|
||||
|
|
|
|||
|
|
@ -107,6 +107,15 @@ string, use ``$args`` to indicate where to insert them. For example:
|
|||
indicates that you need to insert extra arguments before specifying the
|
||||
playlist.
|
||||
|
||||
Some players require a different syntax. For example, with ``mpv`` the optional
|
||||
``$playlist`` variable can be used to match the syntax of the ``--playlist``
|
||||
option:
|
||||
|
||||
::
|
||||
|
||||
play:
|
||||
command: mpv $args --playlist=$playlist
|
||||
|
||||
The ``--yes`` (or ``-y``) flag to the ``play`` command will skip the warning
|
||||
message if you choose to play more items than the **warning_threshold** value
|
||||
usually allows.
|
||||
|
|
@ -123,4 +132,4 @@ until they are externally wiped could be an issue for privacy or storage
|
|||
reasons. If this is the case for you, you might want to use the ``raw`` config
|
||||
option described above.
|
||||
|
||||
.. _tempfile.tempdir: https://docs.python.org/2/library/tempfile.html#tempfile.tempdir
|
||||
.. _tempfile.tempdir: https://docs.python.org/3/library/tempfile.html#tempfile.tempdir
|
||||
|
|
|
|||
65
docs/plugins/shared_metadata_source_config.rst
Normal file
65
docs/plugins/shared_metadata_source_config.rst
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
.. _data_source_mismatch_penalty:
|
||||
|
||||
.. conf:: data_source_mismatch_penalty
|
||||
:default: 0.5
|
||||
|
||||
Penalty applied when the data source of a
|
||||
match candidate differs from the original source of your existing tracks. Any
|
||||
decimal number between 0.0 and 1.0
|
||||
|
||||
This setting controls how much to penalize matches from different metadata
|
||||
sources during import. The penalty is applied when beets detects that a match
|
||||
candidate comes from a different data source than what appears to be the
|
||||
original source of your music collection.
|
||||
|
||||
**Example configurations:**
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Prefer MusicBrainz over Discogs when sources don't match
|
||||
plugins: musicbrainz discogs
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.3 # Lower penalty = preferred
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.8 # Higher penalty = less preferred
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Do not penalise candidates from Discogs at all
|
||||
plugins: musicbrainz discogs
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.5
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.0
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Disable cross-source penalties entirely
|
||||
plugins: musicbrainz discogs
|
||||
|
||||
musicbrainz:
|
||||
data_source_mismatch_penalty: 0.0
|
||||
discogs:
|
||||
data_source_mismatch_penalty: 0.0
|
||||
|
||||
.. tip::
|
||||
|
||||
The last configuration is equivalent to setting:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
match:
|
||||
distance_weights:
|
||||
data_source: 0.0 # Disable data source matching
|
||||
|
||||
.. conf:: source_weight
|
||||
:default: 0.5
|
||||
|
||||
.. deprecated:: 2.5 Use `data_source_mismatch_penalty`_ instead.
|
||||
|
||||
.. conf:: search_limit
|
||||
:default: 5
|
||||
|
||||
Maximum number of search results to return.
|
||||
|
|
@ -73,8 +73,6 @@ Default
|
|||
.. code-block:: yaml
|
||||
|
||||
spotify:
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
mode: list
|
||||
region_filter:
|
||||
show_failures: no
|
||||
|
|
@ -84,8 +82,13 @@ Default
|
|||
client_id: REDACTED
|
||||
client_secret: REDACTED
|
||||
tokenfile: spotify_token.json
|
||||
data_source_mismatch_penalty: 0.5
|
||||
search_limit: 5
|
||||
|
||||
- **mode**: One of the following:
|
||||
.. conf:: mode
|
||||
:default: list
|
||||
|
||||
Controls how the playlist is output:
|
||||
|
||||
- ``list``: Print out the playlist as a list of links. This list can then
|
||||
be pasted in to a new or existing Spotify playlist.
|
||||
|
|
@ -93,50 +96,53 @@ Default
|
|||
instructions to open Spotify with the playlist you created. Until this
|
||||
has been tested on all platforms, it will remain optional.
|
||||
|
||||
Default: ``list``.
|
||||
.. conf:: region_filter
|
||||
:default:
|
||||
|
||||
- **region_filter**: A two-character country abbreviation, to limit results to
|
||||
that market. Default: None.
|
||||
- **show_failures**: List each lookup that does not return a Spotify ID (and
|
||||
therefore cannot be added to a playlist). Default: ``no``.
|
||||
- **tiebreak**: How to choose the track if there is more than one identical
|
||||
result. For example, there might be multiple releases of the same album. The
|
||||
options are ``popularity`` and ``first`` (to just choose the first match
|
||||
returned). Default: ``popularity``.
|
||||
- **regex**: An array of regex transformations to perform on the
|
||||
track/album/artist fields before sending them to Spotify. Can be useful for
|
||||
changing certain abbreviations, like ft. -> feat. See the examples below.
|
||||
Default: None.
|
||||
- **search_query_ascii**: If set to ``yes``, the search query will be converted
|
||||
to ASCII before being sent to Spotify. Converting searches to ASCII can
|
||||
enhance search results in some cases, but in general, it is not recommended.
|
||||
For instance ``artist:deadmau5 album:4×4`` will be converted to
|
||||
``artist:deadmau5 album:4x4`` (notice ``×!=x``). Default: ``no``.
|
||||
A two-character country abbreviation, to limit results to that market.
|
||||
|
||||
Here's an example:
|
||||
.. conf:: show_failures
|
||||
:default: no
|
||||
|
||||
::
|
||||
List each lookup that does not return a Spotify ID (and therefore cannot be
|
||||
added to a playlist).
|
||||
|
||||
spotify:
|
||||
data_source_mismatch_penalty: 0.7
|
||||
mode: open
|
||||
region_filter: US
|
||||
show_failures: on
|
||||
tiebreak: first
|
||||
search_query_ascii: no
|
||||
.. conf:: tiebreak
|
||||
:default: popularity
|
||||
|
||||
regex: [
|
||||
{
|
||||
field: "albumartist", # Field in the item object to regex.
|
||||
search: "Something", # String to look for.
|
||||
replace: "Replaced" # Replacement value.
|
||||
},
|
||||
{
|
||||
field: "title",
|
||||
search: "Something Else",
|
||||
replace: "AlsoReplaced"
|
||||
}
|
||||
]
|
||||
How to choose the candidate if there is more than one identical result. For
|
||||
example, there might be multiple releases of the same album.
|
||||
|
||||
- ``popularity``: pick the more popular candidate
|
||||
- ``first``: pick the first candidate
|
||||
|
||||
.. conf:: regex
|
||||
:default: []
|
||||
|
||||
An array of regex transformations to perform on the track/album/artist fields
|
||||
before sending them to Spotify. Can be useful for changing certain
|
||||
abbreviations, like ft. -> feat. For example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
regex:
|
||||
- field: albumartist
|
||||
search: Something
|
||||
replace: Replaced
|
||||
- field: title
|
||||
search: Something Else
|
||||
replace: AlsoReplaced
|
||||
|
||||
.. conf:: search_query_ascii
|
||||
:default: no
|
||||
|
||||
If enabled, the search query will be converted to ASCII before being sent to
|
||||
Spotify. Converting searches to ASCII can enhance search results in some
|
||||
cases, but in general, it is not recommended. For instance,
|
||||
``artist:deadmau5 album:4×4`` will be converted to ``artist:deadmau5
|
||||
album:4x4`` (notice ``×!=x``).
|
||||
|
||||
.. include:: ./shared_metadata_source_config.rst
|
||||
|
||||
Obtaining Track Popularity and Audio Features from Spotify
|
||||
----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ terminal_encoding
|
|||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The text encoding, as `known to Python
|
||||
<https://docs.python.org/2/library/codecs.html#standard-encodings>`__, to use
|
||||
<https://docs.python.org/3/library/codecs.html#standard-encodings>`__, to use
|
||||
for messages printed to the standard output. It's also used to read messages
|
||||
from the standard input. By default, this is determined automatically from the
|
||||
locale environment variables.
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def create_rst_replacements() -> list[Replacement]:
|
|||
# Replace Sphinx directives by documentation URLs, e.g.,
|
||||
# :ref:`/plugins/autobpm` -> [AutoBPM Plugin](DOCS/plugins/autobpm.html)
|
||||
(
|
||||
r":(?:ref|doc|class):`+(?:([^`<]+)<)?/?([\w./_-]+)>?`+",
|
||||
r":(?:ref|doc|class|conf):`+(?:([^`<]+)<)?/?([\w.:/_-]+)>?`+",
|
||||
lambda m: make_ref_link(m[2], m[1]),
|
||||
),
|
||||
# Convert command references to documentation URLs
|
||||
|
|
|
|||
15
poetry.lock
generated
15
poetry.lock
generated
|
|
@ -3473,6 +3473,17 @@ files = [
|
|||
[package.dependencies]
|
||||
types-html5lib = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-docutils"
|
||||
version = "0.22.2.20251006"
|
||||
description = "Typing stubs for docutils"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "types_docutils-0.22.2.20251006-py3-none-any.whl", hash = "sha256:1e61afdeb4fab4ae802034deea3e853ced5c9b5e1d156179000cb68c85daf384"},
|
||||
{file = "types_docutils-0.22.2.20251006.tar.gz", hash = "sha256:c36c0459106eda39e908e9147bcff9dbd88535975cde399433c428a517b9e3b2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-flask-cors"
|
||||
version = "6.0.0.20250520"
|
||||
|
|
@ -3650,7 +3661,7 @@ beatport = ["requests-oauthlib"]
|
|||
bpd = ["PyGObject"]
|
||||
chroma = ["pyacoustid"]
|
||||
discogs = ["python3-discogs-client"]
|
||||
docs = ["pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design"]
|
||||
docs = ["docutils", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design"]
|
||||
embedart = ["Pillow"]
|
||||
embyupdate = ["requests"]
|
||||
fetchart = ["Pillow", "beautifulsoup4", "langdetect", "requests"]
|
||||
|
|
@ -3672,4 +3683,4 @@ web = ["flask", "flask-cors"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<4"
|
||||
content-hash = "1db39186aca430ef6f1fd9e51b9dcc3ed91880a458bc21b22d950ed8589fdf5a"
|
||||
content-hash = "aedfeb1ac78ae0120855c6a7d6f35963c63cc50a8750142c95dd07ffd213683f"
|
||||
|
|
|
|||
|
|
@ -77,10 +77,11 @@ resampy = { version = ">=0.4.3", optional = true }
|
|||
requests-oauthlib = { version = ">=0.6.1", optional = true }
|
||||
soco = { version = "*", optional = true }
|
||||
|
||||
docutils = { version = ">=0.20.1", optional = true }
|
||||
pydata-sphinx-theme = { version = "*", optional = true }
|
||||
sphinx = { version = "*", optional = true }
|
||||
sphinx-design = { version = "^0.6.1", optional = true }
|
||||
sphinx-copybutton = { version = "^0.5.2", optional = true }
|
||||
sphinx-design = { version = ">=0.6.1", optional = true }
|
||||
sphinx-copybutton = { version = ">=0.5.2", optional = true }
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
beautifulsoup4 = "*"
|
||||
|
|
@ -109,6 +110,7 @@ sphinx-lint = ">=1.0.0"
|
|||
[tool.poetry.group.typing.dependencies]
|
||||
mypy = "*"
|
||||
types-beautifulsoup4 = "*"
|
||||
types-docutils = ">=0.22.2.20251006"
|
||||
types-mock = "*"
|
||||
types-Flask-Cors = "*"
|
||||
types-Pillow = "*"
|
||||
|
|
@ -131,7 +133,14 @@ beatport = ["requests-oauthlib"]
|
|||
bpd = ["PyGObject"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0
|
||||
chroma = ["pyacoustid"] # chromaprint or fpcalc
|
||||
# convert # ffmpeg
|
||||
docs = ["pydata-sphinx-theme", "sphinx", "sphinx-lint", "sphinx-design", "sphinx-copybutton"]
|
||||
docs = [
|
||||
"docutils",
|
||||
"pydata-sphinx-theme",
|
||||
"sphinx",
|
||||
"sphinx-lint",
|
||||
"sphinx-design",
|
||||
"sphinx-copybutton",
|
||||
]
|
||||
discogs = ["python3-discogs-client"]
|
||||
embedart = ["Pillow"] # ImageMagick
|
||||
embyupdate = ["requests"]
|
||||
|
|
|
|||
|
|
@ -105,6 +105,19 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
|
|||
|
||||
open_mock.assert_called_once_with([self.item.path], "echo")
|
||||
|
||||
def test_pls_marker(self, open_mock):
|
||||
self.config["play"]["command"] = (
|
||||
"echo --some params --playlist=$playlist --some-more params"
|
||||
)
|
||||
|
||||
self.run_command("play", "nice")
|
||||
|
||||
open_mock.assert_called_once
|
||||
|
||||
commandstr = open_mock.call_args_list[0][0][1]
|
||||
assert commandstr.startswith("echo --some params --playlist=")
|
||||
assert commandstr.endswith(" --some-more params")
|
||||
|
||||
def test_not_found(self, open_mock):
|
||||
self.run_command("play", "not found")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue