mirror of
https://github.com/beetbox/beets.git
synced 2026-07-22 02:06:50 -04:00
Merge branch 'master' into edit
This commit is contained in:
@@ -37,6 +37,7 @@ class Library(dbcore.Database):
|
||||
(migrations.MultiArrangerFieldMigration, (Item,)),
|
||||
(migrations.RelativePathMigration, (Item, Album)),
|
||||
(migrations.RemoveInheritedArtpathMigration, (Item,)),
|
||||
(migrations.InstrumentalLyricsInFlexFieldMigration, (Item,)),
|
||||
)
|
||||
replacements: Replacements
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from beets.dbcore.db import Migration
|
||||
from beets.dbcore.pathutils import normalize_path_for_db
|
||||
from beets.dbcore.types import MULTI_VALUE_DELIMITER
|
||||
from beets.util import chunks, unique_list
|
||||
from beets.util.lyrics import Lyrics
|
||||
from beets.util.lyrics import INSTRUMENTAL_LYRICS, Lyrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beets.dbcore.db import Model
|
||||
@@ -284,3 +284,46 @@ class RemoveInheritedArtpathMigration(Migration):
|
||||
tx.mutate(f"DELETE FROM {flex_table} WHERE key == 'artpath'")
|
||||
|
||||
ui.print_(f"Migration complete: {total} {table} updated")
|
||||
|
||||
|
||||
class InstrumentalLyricsInFlexFieldMigration(Migration):
|
||||
"""Move legacy instrumental lyrics markers into a flexible field."""
|
||||
|
||||
def _migrate_data(self, model_cls: type[Model], _: set[str]) -> None:
|
||||
"""Migrate legacy instrumental markers out of the lyrics text."""
|
||||
table = model_cls._table
|
||||
flex_table = model_cls._flex_table
|
||||
|
||||
with self.db.transaction() as tx:
|
||||
rows = tx.query(
|
||||
f"""
|
||||
SELECT id
|
||||
FROM {table}
|
||||
WHERE lyrics == ?
|
||||
""",
|
||||
(INSTRUMENTAL_LYRICS,),
|
||||
)
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
total = len(rows)
|
||||
ui.print_(f"Migrating instrumental lyrics for {total} {table}...")
|
||||
with self.db.transaction() as tx:
|
||||
tx.mutate(
|
||||
f"""
|
||||
UPDATE {table}
|
||||
SET lyrics = ''
|
||||
WHERE lyrics == ?
|
||||
""",
|
||||
(INSTRUMENTAL_LYRICS,),
|
||||
)
|
||||
tx.mutate_many(
|
||||
f"""
|
||||
INSERT INTO {flex_table} (entity_id, key, value)
|
||||
VALUES (?, 'lyrics_instrumental', '1')
|
||||
""",
|
||||
[(r["id"],) for r in rows],
|
||||
)
|
||||
|
||||
ui.print_(f"Migration complete: {total} of {total} {table} updated")
|
||||
|
||||
@@ -21,8 +21,8 @@ class Lyrics:
|
||||
"""Represent lyrics text together with structured source metadata.
|
||||
|
||||
This value object keeps the canonical lyrics body, optional provenance, and
|
||||
optional translation metadata synchronized across fetching, translation, and
|
||||
persistence.
|
||||
optional instrumental and translation metadata synchronized across fetching,
|
||||
translation, and persistence.
|
||||
"""
|
||||
|
||||
ORIGINAL_PAT = re.compile(r"[^\n]+ / ")
|
||||
@@ -33,12 +33,21 @@ class Lyrics:
|
||||
text: str
|
||||
backend: str | None = None
|
||||
url: str | None = None
|
||||
instrumental: bool = False
|
||||
language: str | None = None
|
||||
translation_language: str | None = None
|
||||
translations: list[str] = field(default_factory=list)
|
||||
|
||||
def handle_instrumental(self) -> None:
|
||||
"""Track instrumental matches without storing marker text as lyrics."""
|
||||
if self.text == INSTRUMENTAL_LYRICS:
|
||||
self.instrumental = True
|
||||
self.text = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Populate missing language metadata from the current text."""
|
||||
"""Normalize lyrics metadata and infer missing language information."""
|
||||
self.handle_instrumental()
|
||||
|
||||
try:
|
||||
import langdetect
|
||||
except ImportError:
|
||||
@@ -47,7 +56,7 @@ class Lyrics:
|
||||
# Set seed to 0 for deterministic results
|
||||
langdetect.DetectorFactory.seed = 0
|
||||
|
||||
if not self.text or self.text == INSTRUMENTAL_LYRICS:
|
||||
if not self.text:
|
||||
return
|
||||
|
||||
if not self.language:
|
||||
|
||||
@@ -993,6 +993,7 @@ BACKEND_BY_NAME = {
|
||||
class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):
|
||||
item_types: ClassVar[dict[str, types.Type]] = {
|
||||
"lyrics_url": types.STRING,
|
||||
"lyrics_instrumental": types.BOOLEAN,
|
||||
"lyrics_backend": types.STRING,
|
||||
"lyrics_language": types.STRING,
|
||||
"lyrics_translation_language": types.STRING,
|
||||
@@ -1166,9 +1167,15 @@ class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):
|
||||
)
|
||||
return
|
||||
|
||||
for key in ("backend", "url", "language", "translation_language"):
|
||||
for key in (
|
||||
"backend",
|
||||
"url",
|
||||
"instrumental",
|
||||
"language",
|
||||
"translation_language",
|
||||
):
|
||||
item_key = f"lyrics_{key}"
|
||||
if value := getattr(new_lyrics, key):
|
||||
if (value := getattr(new_lyrics, key)) is not None:
|
||||
item[item_key] = value
|
||||
elif item_key in item:
|
||||
del item[item_key]
|
||||
|
||||
@@ -53,6 +53,10 @@ Bug fixes
|
||||
example a multi-disc album whose cover lives in the album root rather than a
|
||||
per-disc directory); the missing art is skipped instead. :bug:`4692`
|
||||
- :doc:`plugins/tidal`: Normalize Tidal album types to lowercase.
|
||||
- :doc:`plugins/lyrics`: Leave lyrics empty when a source reports an
|
||||
instrumental track, and store that state in ``lyrics_instrumental`` flexible
|
||||
attribute. Existing ``[Instrumental]`` lyrics are migrated automatically.
|
||||
:bug:`6719`
|
||||
|
||||
..
|
||||
For plugin developers
|
||||
|
||||
@@ -30,10 +30,14 @@ also sets a few useful flexible attributes:
|
||||
|
||||
- ``lyrics_backend``: name of the backend that provided the lyrics
|
||||
- ``lyrics_url``: URL of the page where the lyrics were found
|
||||
- ``lyrics_instrumental``: whether the backend marked the track as instrumental
|
||||
- ``lyrics_language``: original language of the lyrics
|
||||
- ``lyrics_translation_language``: language of the lyrics translation (if
|
||||
translation is enabled)
|
||||
|
||||
When a backend reports an instrumental track, beets leaves the lyrics text empty
|
||||
and sets ``lyrics_instrumental`` instead.
|
||||
|
||||
If the ``import.write`` config option is on, then the lyrics will also be
|
||||
written to the files' tags.
|
||||
|
||||
|
||||
@@ -346,3 +346,32 @@ class TestMigrationBackup(MigrationTestHelper):
|
||||
if os.fsdecode(f).endswith(".bak")
|
||||
]
|
||||
assert len(backups) == expected_count
|
||||
|
||||
|
||||
class TestInstrumentalLyricsInFlexFieldMigration(MigrationTestHelper):
|
||||
"""Verify legacy instrumental markers move out of canonical lyrics."""
|
||||
|
||||
migration = (migrations.InstrumentalLyricsInFlexFieldMigration, (Item,))
|
||||
|
||||
def test_migrate(self):
|
||||
"""Ensure exact instrumental markers become metadata, not lyric text."""
|
||||
instrumental_item = self.add_item(lyrics="[Instrumental]")
|
||||
regular_item = self.add_item(lyrics="Regular lyrics")
|
||||
|
||||
self.lib._migrate()
|
||||
|
||||
instrumental_item.load()
|
||||
regular_item.load()
|
||||
|
||||
assert instrumental_item.lyrics == ""
|
||||
assert instrumental_item.lyrics_instrumental == "1"
|
||||
assert regular_item.lyrics == "Regular lyrics"
|
||||
|
||||
with pytest.raises(AttributeError, match="lyrics_instrumental"):
|
||||
regular_item.lyrics_instrumental
|
||||
|
||||
# remove cached initial db tables data
|
||||
del self.lib.db_tables
|
||||
assert self.lib.migration_exists(
|
||||
"instrumental_lyrics_in_flex_field", "items"
|
||||
)
|
||||
|
||||
@@ -350,6 +350,7 @@ class TestLyricsPlugin(LyricsPluginMixin):
|
||||
item = helper.lib.get_item(item.id)
|
||||
|
||||
assert item.lyrics_url == lyrics.url
|
||||
assert item.lyrics_instrumental == "0"
|
||||
assert item.lyrics_backend == lyrics.backend
|
||||
if is_importable("langdetect"):
|
||||
assert item.lyrics_language == "EN"
|
||||
@@ -621,9 +622,7 @@ class TestLRCLibLyrics(LyricsBackendTest):
|
||||
[lyrics_match(duration=1)], None, id="none: duration too short"
|
||||
),
|
||||
pytest.param(
|
||||
[lyrics_match(instrumental=True)],
|
||||
"[Instrumental]",
|
||||
id="instrumental track",
|
||||
[lyrics_match(instrumental=True)], "", id="instrumental track"
|
||||
),
|
||||
pytest.param(
|
||||
[lyrics_match(syncedLyrics=None)],
|
||||
|
||||
@@ -9,7 +9,8 @@ class TestLyrics:
|
||||
"[Instrumental]", "lrclib", url="https://lrclib.net/api/1"
|
||||
)
|
||||
|
||||
assert lyrics.full_text == "[Instrumental]"
|
||||
assert lyrics.full_text == ""
|
||||
assert lyrics.instrumental
|
||||
assert lyrics.backend == "lrclib"
|
||||
assert lyrics.url == "https://lrclib.net/api/1"
|
||||
assert lyrics.language is None
|
||||
@@ -38,3 +39,4 @@ class TestLyrics:
|
||||
assert lyrics.translation_language == (
|
||||
"FR" if langdetect_available else None
|
||||
)
|
||||
assert not lyrics.instrumental
|
||||
|
||||
Reference in New Issue
Block a user