remove feat from custom_feat_words

This commit is contained in:
Ember Light 2025-10-12 22:40:27 +02:00
parent af09e58fb0
commit b95a17d8d3
3 changed files with 35 additions and 39 deletions

View file

@ -633,7 +633,7 @@ def send(event: EventType, **arguments: Any) -> list[Any]:
def feat_tokens( def feat_tokens(
for_artist: bool = True, custom_feat_words: list[str] | None = None for_artist: bool = True, custom_words: list[str] | None = None
) -> str: ) -> str:
"""Return a regular expression that matches phrases like "featuring" """Return a regular expression that matches phrases like "featuring"
that separate a main artist or a song title from secondary artists. that separate a main artist or a song title from secondary artists.
@ -641,8 +641,8 @@ def feat_tokens(
suitable for matching artist fields (the default) or title fields. suitable for matching artist fields (the default) or title fields.
""" """
feat_words = ["ft", "featuring", "feat", "feat.", "ft."] feat_words = ["ft", "featuring", "feat", "feat.", "ft."]
if isinstance(custom_feat_words, list): if isinstance(custom_words, list):
feat_words += custom_feat_words feat_words += custom_words
if for_artist: if for_artist:
feat_words += ["with", "vs", "and", "con", "&"] feat_words += ["with", "vs", "and", "con", "&"]
return ( return (

View file

@ -29,7 +29,7 @@ if TYPE_CHECKING:
def split_on_feat( def split_on_feat(
artist: str, artist: str,
for_artist: bool = True, for_artist: bool = True,
custom_feat_words: list[str] | None = None, custom_words: list[str] | None = None,
) -> tuple[str, str | None]: ) -> tuple[str, str | None]:
"""Given an artist string, split the "main" artist from any artist """Given an artist string, split the "main" artist from any artist
on the right-hand side of a string like "feat". Return the main on the right-hand side of a string like "feat". Return the main
@ -38,7 +38,7 @@ def split_on_feat(
""" """
# split on the first "feat". # split on the first "feat".
regex = re.compile( regex = re.compile(
plugins.feat_tokens(for_artist, custom_feat_words), re.IGNORECASE plugins.feat_tokens(for_artist, custom_words), re.IGNORECASE
) )
parts = tuple(s.strip() for s in regex.split(artist, 1)) parts = tuple(s.strip() for s in regex.split(artist, 1))
if len(parts) == 1: if len(parts) == 1:
@ -48,15 +48,11 @@ def split_on_feat(
return parts return parts
def contains_feat( def contains_feat(title: str, custom_words: list[str] | None = None) -> bool:
title: str, custom_feat_words: list[str] | None = None
) -> bool:
"""Determine whether the title contains a "featured" marker.""" """Determine whether the title contains a "featured" marker."""
return bool( return bool(
re.search( re.search(
plugins.feat_tokens( plugins.feat_tokens(for_artist=False, custom_words=custom_words),
for_artist=False, custom_feat_words=custom_feat_words
),
title, title,
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
@ -66,7 +62,7 @@ def contains_feat(
def find_feat_part( def find_feat_part(
artist: str, artist: str,
albumartist: str | None, albumartist: str | None,
custom_feat_words: list[str] | None = None, custom_words: list[str] | None = None,
) -> str | None: ) -> str | None:
"""Attempt to find featured artists in the item's artist fields and """Attempt to find featured artists in the item's artist fields and
return the results. Returns None if no featured artist found. return the results. Returns None if no featured artist found.
@ -82,7 +78,7 @@ def find_feat_part(
if albumartist_split[1] != "": if albumartist_split[1] != "":
# Extract the featured artist from the right-hand side. # Extract the featured artist from the right-hand side.
_, feat_part = split_on_feat( _, feat_part = split_on_feat(
albumartist_split[1], custom_feat_words=custom_feat_words albumartist_split[1], custom_words=custom_words
) )
return feat_part return feat_part
@ -90,7 +86,7 @@ def find_feat_part(
# look for a featuring artist on the left-hand side. # look for a featuring artist on the left-hand side.
else: else:
lhs, _ = split_on_feat( lhs, _ = split_on_feat(
albumartist_split[0], custom_feat_words=custom_feat_words albumartist_split[0], custom_words=custom_words
) )
if lhs: if lhs:
return lhs return lhs
@ -98,7 +94,7 @@ def find_feat_part(
# Fall back to conservative handling of the track artist without relying # Fall back to conservative handling of the track artist without relying
# on albumartist, which covers compilations using a 'Various Artists' # on albumartist, which covers compilations using a 'Various Artists'
# albumartist and album tracks by a guest artist featuring a third artist. # albumartist and album tracks by a guest artist featuring a third artist.
_, feat_part = split_on_feat(artist, False, custom_feat_words) _, feat_part = split_on_feat(artist, False, custom_words)
return feat_part return feat_part
@ -112,7 +108,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
"drop": False, "drop": False,
"format": "feat. {}", "format": "feat. {}",
"keep_in_artist": False, "keep_in_artist": False,
"custom_feat_words": [], "custom_words": [],
} }
) )
@ -137,12 +133,12 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
self.config.set_args(opts) self.config.set_args(opts)
drop_feat = self.config["drop"].get(bool) drop_feat = self.config["drop"].get(bool)
keep_in_artist_field = self.config["keep_in_artist"].get(bool) keep_in_artist_field = self.config["keep_in_artist"].get(bool)
custom_feat_words = self.config["custom_feat_words"].get(list) custom_words = self.config["custom_words"].get(list)
write = ui.should_write() write = ui.should_write()
for item in lib.items(args): for item in lib.items(args):
if self.ft_in_title( if self.ft_in_title(
item, drop_feat, keep_in_artist_field, custom_feat_words item, drop_feat, keep_in_artist_field, custom_words
): ):
item.store() item.store()
if write: if write:
@ -155,11 +151,11 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
"""Import hook for moving featuring artist automatically.""" """Import hook for moving featuring artist automatically."""
drop_feat = self.config["drop"].get(bool) drop_feat = self.config["drop"].get(bool)
keep_in_artist_field = self.config["keep_in_artist"].get(bool) keep_in_artist_field = self.config["keep_in_artist"].get(bool)
custom_feat_words = self.config["custom_feat_words"].get(list) custom_words = self.config["custom_words"].get(list)
for item in task.imported_items(): for item in task.imported_items():
if self.ft_in_title( if self.ft_in_title(
item, drop_feat, keep_in_artist_field, custom_feat_words item, drop_feat, keep_in_artist_field, custom_words
): ):
item.store() item.store()
@ -169,7 +165,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
feat_part: str, feat_part: str,
drop_feat: bool, drop_feat: bool,
keep_in_artist_field: bool, keep_in_artist_field: bool,
custom_feat_words: list[str], custom_words: list[str],
) -> None: ) -> None:
"""Choose how to add new artists to the title and set the new """Choose how to add new artists to the title and set the new
metadata. Also, print out messages about any changes that are made. metadata. Also, print out messages about any changes that are made.
@ -183,7 +179,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
) )
else: else:
track_artist, _ = split_on_feat( track_artist, _ = split_on_feat(
item.artist, custom_feat_words=custom_feat_words item.artist, custom_words=custom_words
) )
self._log.info("artist: {0.artist} -> {1}", item, track_artist) self._log.info("artist: {0.artist} -> {1}", item, track_artist)
item.artist = track_artist item.artist = track_artist
@ -191,12 +187,12 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
if item.artist_sort: if item.artist_sort:
# Just strip the featured artist from the sort name. # Just strip the featured artist from the sort name.
item.artist_sort, _ = split_on_feat( item.artist_sort, _ = split_on_feat(
item.artist_sort, custom_feat_words=custom_feat_words item.artist_sort, custom_words=custom_words
) )
# Only update the title if it does not already contain a featured # Only update the title if it does not already contain a featured
# artist and if we do not drop featuring information. # artist and if we do not drop featuring information.
if not drop_feat and not contains_feat(item.title, custom_feat_words): if not drop_feat and not contains_feat(item.title, custom_words):
feat_format = self.config["format"].as_str() feat_format = self.config["format"].as_str()
new_format = feat_format.format(feat_part) new_format = feat_format.format(feat_part)
new_title = f"{item.title} {new_format}" new_title = f"{item.title} {new_format}"
@ -208,7 +204,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
item: Item, item: Item,
drop_feat: bool, drop_feat: bool,
keep_in_artist_field: bool, keep_in_artist_field: bool,
custom_feat_words: list[str], custom_words: list[str],
) -> bool: ) -> bool:
"""Look for featured artists in the item's artist fields and move """Look for featured artists in the item's artist fields and move
them to the title. them to the title.
@ -225,14 +221,14 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
if albumartist and artist == albumartist: if albumartist and artist == albumartist:
return False return False
_, featured = split_on_feat(artist, custom_feat_words=custom_feat_words) _, featured = split_on_feat(artist, custom_words=custom_words)
if not featured: if not featured:
return False return False
self._log.info("{.filepath}", item) self._log.info("{.filepath}", item)
# Attempt to find the featured artist. # Attempt to find the featured artist.
feat_part = find_feat_part(artist, albumartist, custom_feat_words) feat_part = find_feat_part(artist, albumartist, custom_words)
if not feat_part: if not feat_part:
self._log.info("no featuring artists found") self._log.info("no featuring artists found")
@ -240,6 +236,6 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
# If we have a featuring artist, move it to the title. # If we have a featuring artist, move it to the title.
self.update_metadata( self.update_metadata(
item, feat_part, drop_feat, keep_in_artist_field, custom_feat_words item, feat_part, drop_feat, keep_in_artist_field, custom_words
) )
return True return True

View file

@ -46,7 +46,7 @@ def set_config(
"drop": False, "drop": False,
"auto": True, "auto": True,
"keep_in_artist": False, "keep_in_artist": False,
"custom_feat_words": [], "custom_words": [],
} }
env.config["ftintitle"].set(defaults) env.config["ftintitle"].set(defaults)
env.config["ftintitle"].set(cfg) env.config["ftintitle"].set(cfg)
@ -172,9 +172,9 @@ def add_item(
("Alice ft Bob", "Song 1"), ("Alice ft Bob", "Song 1"),
id="keep-in-artist-drop-from-title", id="keep-in-artist-drop-from-title",
), ),
# ---- custom_feat_words variants ---- # ---- custom_words variants ----
pytest.param( pytest.param(
{"format": "featuring {}", "custom_feat_words": ["med"]}, {"format": "featuring {}", "custom_words": ["med"]},
("ftintitle",), ("ftintitle",),
("Alice med Bob", "Song 1", "Alice"), ("Alice med Bob", "Song 1", "Alice"),
("Alice", "Song 1 featuring Bob"), ("Alice", "Song 1 featuring Bob"),
@ -184,7 +184,7 @@ def add_item(
{ {
"format": "featuring {}", "format": "featuring {}",
"keep_in_artist": True, "keep_in_artist": True,
"custom_feat_words": ["med"], "custom_words": ["med"],
}, },
("ftintitle",), ("ftintitle",),
("Alice med Bob", "Song 1", "Alice"), ("Alice med Bob", "Song 1", "Alice"),
@ -195,7 +195,7 @@ def add_item(
{ {
"format": "featuring {}", "format": "featuring {}",
"keep_in_artist": True, "keep_in_artist": True,
"custom_feat_words": ["med"], "custom_words": ["med"],
}, },
( (
"ftintitle", "ftintitle",
@ -294,7 +294,7 @@ def test_contains_feat(given: str, expected: bool) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"given,custom_feat_words,expected", "given,custom_words,expected",
[ [
("Alice ft. Bob", [], True), ("Alice ft. Bob", [], True),
("Alice feat. Bob", [], True), ("Alice feat. Bob", [], True),
@ -317,9 +317,9 @@ def test_contains_feat(given: str, expected: bool) -> None:
("Alice med Carol", [], False), ("Alice med Carol", [], False),
], ],
) )
def test_custom_feat_words( def test_custom_words(
given: str, custom_feat_words: Optional[list[str]], expected: bool given: str, custom_words: Optional[list[str]], expected: bool
) -> None: ) -> None:
if custom_feat_words is None: if custom_words is None:
custom_feat_words = [] custom_words = []
assert ftintitle.contains_feat(given, custom_feat_words) is expected assert ftintitle.contains_feat(given, custom_words) is expected