## Skip `langdetect`-dependent tests when package is unavailable
Fixes#6421.
### What changed
- Adds a `requires_import(module)` pytest marker to `conftest.py` that
skips tests when an optional dependency is not installed
- Adds an `is_importable` fixture for conditional assertions within a
test body
- Marks `TestTranslation` and other `langdetect`-dependent
tests/assertions with the new marker or fixture
- Extends `requires_import` with a `force_ci=True` kwarg — tests marked
this way are **never** skipped in `beetbox/beets` CI, ensuring coverage
where all deps are installed
- Moves marker declarations from `setup.cfg` into `pytest_configure` in
`conftest.py`
Add force_ci kwarg to requires_import pytest marker to allow tests
to run unconditionally in CI (GitHub Actions), even if the module
is not detected locally. Refactor autobpm test to use this instead
of manual env-checking at module level.
Add a `requires_import` pytest marker and `is_importable` fixture to
conditionally skip or adjust assertions based on whether optional
dependencies are available. Apply this to `langdetect`-dependent
language detection tests in lyrics and migration test suites.
## Documentation Link Maintenance
This PR audits and fixes all hyperlinks across the beets documentation.
No functional code changes — purely doc hygiene.
### What changed
- **HTTP → HTTPS**: Upgrades all `http://` links to `https://` where
supported.
- **Broken/outdated URLs**: Replaces dead or redirected links with their
current canonical targets. Key examples:
- `codecov.io` → `app.codecov.io`
- Python docs from `docs.python.org/library/` →
`docs.python.org/3/library/`
- PEP links from `python.org/dev/peps/` → `peps.python.org/`
- SourceForge project links updated to current project page URLs
- Spotify, Plex, Sonos, Discogs, IPFS, and other service URLs updated to
their current domains/paths
- Archived URLs wrapped in `web.archive.org` where the original is gone
(e.g. `echonest`, `albumart.org`, `phash`)
- **`docs/conf.py`**: Adds link-check exclusions for domains known to
block automated requests (SourceForge, fanart.tv, Imgur, Discogs
settings).
### Impact
No user-facing behaviour changes. Fixes broken links that would
frustrate contributors and users reading the docs, and unblocks the
Sphinx `linkcheck` builder from false positives.
Fixes: #6370
This PR completes the lyrics pipeline refactor around a structured
`Lyrics` value object and aligns storage, migration, and docs with that
model.
At a high level, lyrics handling is now end-to-end structured instead of
ad-hoc string/tuple flows: fetchers return `Lyrics`, translation
operates on `Lyrics`, and persistence writes both canonical text and
structured metadata.
High-level impact:
- Backends now return `Lyrics` instead of `(text, url)` tuples.
- Lyrics source metadata is no longer embedded in `item.lyrics` as a
`Source: ...` suffix.
- Lyrics metadata is stored in flexible fields:
`lyrics_backend`, `lyrics_url`, `lyrics_language`,
`lyrics_translation_language`.
- Existing libraries are automatically migrated on first run by a
one-time data migration that:
normalizes legacy mixed-content lyrics text and moves auxiliary metadata
into flex fields.
- Sync safety is improved:
with `synced` enabled, existing synced lyrics are not replaced by newly
fetched plain lyrics, even with `force`.
- LRCLib synced lyrics validation is stricter:
synced results are accepted only when the final synced timestamp is
consistent with track duration.
Docs and tests:
- Lyrics plugin docs now describe the new flexible metadata fields and
synced replacement behavior.
- Developer docs now document migration lifecycle, class-name-based
migration identity, and migration use cases.
- Changelog updated for all user-visible behavior changes.
- Tests were expanded/updated for migration behavior, backend return
types, translation behavior, synced-lyrics safety, and LRCLib duration
validation.
- Add `LyricsMetadataInFlexFieldsMigration` to extract legacy source
URLs and language metadata from lyrics text into flex attributes
- Add `Lyrics.from_legacy_text` to parse legacy lyrics format
- Move `with_row_factory` context manager up to base `Migration` class
- Rename `migrate_table` to `migrate_model` and pass model class
instead of table name string. This is so that the migration can access
both `_table` and `_flex_table` attributes.
- Make `langdetect` import optional in `Lyrics.__post_init__`: users may
not have have the dependency installed, and we do not want the
migration to fail because of that.
- Move `BACKEND_BY_NAME` to module level for use outside plugin class
* Introduce a `Lyrics` dataclass to carry text, source URL, and language
metadata through fetch, translation, and storage paths.
* Return `Lyrics` from backends and plugin lookup methods instead of raw
tuples/strings.
* Store backend name in `lyrics_source` derived from fetched URL root
domain.
* Simplify translator flow to operate on `Lyrics`, reuse line splitting,
append translations in-place, and record translation language
metadata.
When lyrics.synced is enabled, avoid replacing existing synced lyrics with
newly fetched unsynced lyrics, even with force enabled.
Allow replacement when the new lyrics are also synced, or when synced mode
is disabled.
Introduce a new lastgenre `cleanup_existing` flag.
It handles the case where canonicalization is desired on existing tags.
The new logic triggers if:
- `force`: False
- `cleanup_existing: True
Depending on whether `whitelist: True` or `canonical: True`, the genres
are then canonicalized and/or whitelisting is applied
Fix extension substitution inside path of the exported playlist.
Before this, the exported playlist contained relative paths pointing to
the
converted files BUT the extension were not substituted comparing to
before and
the after the conversion. Therefore, running the playlist will fail for
files
which have been converted and where extension have changed.
Example:
1. Convert `/path/to/library/artist.flac` to
`/path/to/converted/artist.mp3` using the `-m playlist.m3u` command-line
flag.
2. Open the generated playlist, and find the incorrect path
`/path/to/converted/artist.flac` inside.
Build playlist paths using the selected format (`--format`/config), and only
replace extensions when the destination file is actually transcoded.
Precompute playlist entries before conversion runs so `--keep-new` does not
pick up mutated item paths and produce mismatched extensions.
Add/expand convert CLI tests to cover:
- config format playlist extension
- `--format` override playlist extension
- no-transcode (`no_convert`) playlist extension
- `--keep-new` destination playlist path behavior
> **Note**: This fix was developed with assistance from Claude Code
(AI). The problem was identified by me, and Claude helped investigate
the codebase, trace the git history to find the original FIXME,
implement the fix, and update the tests. All changes have been reviewed
and tested.
When importing with `autotag: no`, duplicate detection is completely
bypassed. The `import_asis` stage calls `_apply_choice()` directly
without first calling `_resolve_duplicates()`, meaning any configured
`duplicate_keys` and `duplicate_action` settings are ignored.
This was a known limitation. Commit 79d1203541 (Sep 2014) added a FIXME
comment:
```python
# FIXME We should also resolve duplicates when not
# autotagging. This is currently handled in `user_query`
```
The FIXME was removed during a comment cleanup in f145e3b18 (Jan 2015),
but the underlying issue was never fixed. A test
`test_no_autotag_keeps_duplicate_album` was added to document the
existing behavior at the time.
### The Fix
Add `_resolve_duplicates(session, task)` to the `import_asis` stage
before `_apply_choice()`, matching the behavior of the `user_query`
stage used when autotagging.
### Test Changes
- Renamed `test_no_autotag_keeps_duplicate_album` →
`test_no_autotag_removes_duplicate_album`
- Fixed the test to use album metadata instead of item metadata for
duplicate matching
- Added missing `import_file.save()` call
When importing with autotag=no, duplicate detection was skipped entirely
because the import_asis stage called _apply_choice() directly without
first calling _resolve_duplicates(). This meant the duplicate_keys and
duplicate_action config options were ignored for as-is imports.
This was a known limitation documented by a FIXME comment added in
commit 79d1203541 (Sep 2014): "We should also resolve duplicates when
not autotagging." The FIXME was later removed during a comment cleanup
(f145e3b18) but the issue was never addressed.
This commit adds the _resolve_duplicates() call to import_asis, ensuring
duplicate detection works consistently regardless of the autotag setting.
This applies to both album imports and singleton imports.
Test changes:
- Renamed test_no_autotag_keeps_duplicate_album to
test_no_autotag_removes_duplicate_album to verify the corrected behavior
- Added test_no_autotag_removes_duplicate_singleton to verify singleton
duplicate detection also works with autotag=no
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes#2043Fixes#5638
Improve the `fuzzy` plugin in two ways:
1. Make short fuzzy queries behave more like substring matching.
2. Force fuzzy-prefixed queries to use slow evaluation so fuzzy logic is
always applied.
## Problem
Fuzzy prefix queries (for example `~foo` or custom prefixes like
`%%foo`) could take a fast DB query path on normal fields, which
bypassed fuzzy semantics and produced broad/
unrelated matches [#5638](https://github.com/beetbox/beets/issues/5638).
Also, when the query pattern was shorter than the field value, fuzzy
matching was too strict for substring-style use cases
[#2043](https://github.com/beetbox/beets/issues/2043).
Thanks to @carreter for this in #5140. Opened a new PR since I did not
have permissions to push to their fork.
Supersedes #5140.
Fixes#6176
Changed one line to ensure expected behavior as mentioned in the issue,
now `beet config -e` will have info message when no text editor is
available.