mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
feat(FtInTitle): support tracks by artists != album artist
This commit is contained in:
parent
159f43cf46
commit
f0a6059685
3 changed files with 89 additions and 30 deletions
|
|
@ -26,14 +26,16 @@ if TYPE_CHECKING:
|
|||
from beets.library import Item
|
||||
|
||||
|
||||
def split_on_feat(artist: str) -> tuple[str, str | None]:
|
||||
def split_on_feat(
|
||||
artist: str, for_artist: bool = True
|
||||
) -> tuple[str, str | None]:
|
||||
"""Given an artist string, split the "main" artist from any artist
|
||||
on the right-hand side of a string like "feat". Return the main
|
||||
artist, which is always a string, and the featuring artist, which
|
||||
may be a string or None if none is present.
|
||||
"""
|
||||
# split on the first "feat".
|
||||
regex = re.compile(plugins.feat_tokens(), re.IGNORECASE)
|
||||
regex = re.compile(plugins.feat_tokens(for_artist), re.IGNORECASE)
|
||||
parts = tuple(s.strip() for s in regex.split(artist, 1))
|
||||
if len(parts) == 1:
|
||||
return parts[0], None
|
||||
|
|
@ -53,32 +55,35 @@ def contains_feat(title: str) -> bool:
|
|||
)
|
||||
|
||||
|
||||
def find_feat_part(artist: str, albumartist: str) -> str | None:
|
||||
def find_feat_part(artist: str, albumartist: str | None) -> str | None:
|
||||
"""Attempt to find featured artists in the item's artist fields and
|
||||
return the results. Returns None if no featured artist found.
|
||||
"""
|
||||
# Look for the album artist in the artist field. If it's not
|
||||
# present, give up.
|
||||
albumartist_split = artist.split(albumartist, 1)
|
||||
if len(albumartist_split) <= 1:
|
||||
return None
|
||||
# Handle a wider variety of extraction cases if the album artist is
|
||||
# contained within the track artist.
|
||||
if albumartist and albumartist in artist:
|
||||
albumartist_split = artist.split(albumartist, 1)
|
||||
|
||||
# If the last element of the split (the right-hand side of the
|
||||
# album artist) is nonempty, then it probably contains the
|
||||
# featured artist.
|
||||
elif albumartist_split[1] != "":
|
||||
# Extract the featured artist from the right-hand side.
|
||||
_, feat_part = split_on_feat(albumartist_split[1])
|
||||
return feat_part
|
||||
# If the last element of the split (the right-hand side of the
|
||||
# album artist) is nonempty, then it probably contains the
|
||||
# featured artist.
|
||||
if albumartist_split[1] != "":
|
||||
# Extract the featured artist from the right-hand side.
|
||||
_, feat_part = split_on_feat(albumartist_split[1])
|
||||
return feat_part
|
||||
|
||||
# Otherwise, if there's nothing on the right-hand side, look for a
|
||||
# featuring artist on the left-hand side.
|
||||
else:
|
||||
lhs, rhs = split_on_feat(albumartist_split[0])
|
||||
if lhs:
|
||||
return lhs
|
||||
# Otherwise, if there's nothing on the right-hand side,
|
||||
# look for a featuring artist on the left-hand side.
|
||||
else:
|
||||
lhs, _ = split_on_feat(albumartist_split[0])
|
||||
if lhs:
|
||||
return lhs
|
||||
|
||||
return None
|
||||
# Fall back to conservative handling of the track artist without relying
|
||||
# on albumartist, which covers compilations using a 'Various Artists'
|
||||
# albumartist and album tracks by a guest artist featuring a third artist.
|
||||
_, feat_part = split_on_feat(artist, False)
|
||||
return feat_part
|
||||
|
||||
|
||||
class FtInTitlePlugin(plugins.BeetsPlugin):
|
||||
|
|
@ -153,8 +158,9 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
"artist: {.artist} (Not changing due to keep_in_artist)", item
|
||||
)
|
||||
else:
|
||||
self._log.info("artist: {0.artist} -> {0.albumartist}", item)
|
||||
item.artist = item.albumartist
|
||||
track_artist, _ = split_on_feat(item.artist)
|
||||
self._log.info("artist: {0.artist} -> {1}", item, track_artist)
|
||||
item.artist = track_artist
|
||||
|
||||
if item.artist_sort:
|
||||
# Just strip the featured artist from the sort name.
|
||||
|
|
@ -187,7 +193,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
# Check whether there is a featured artist on this track and the
|
||||
# artist field does not exactly match the album artist field. In
|
||||
# that case, we attempt to move the featured artist to the title.
|
||||
if not albumartist or albumartist == artist:
|
||||
if albumartist and artist == albumartist:
|
||||
return False
|
||||
|
||||
_, featured = split_on_feat(artist)
|
||||
|
|
|
|||
|
|
@ -161,6 +161,9 @@ Other changes:
|
|||
Autogenerated API references are now located in the ``docs/api`` subdirectory.
|
||||
- :doc:`/plugins/substitute`: Fix rST formatting for example cases so that each
|
||||
case is shown on separate lines.
|
||||
- :doc:`/plugins/ftintitle`: Process items whose albumartist is not contained in
|
||||
the artist field, including compilations using Various Artists as an
|
||||
albumartist and album tracks by guest artists featuring a third artist.
|
||||
- Refactored library.py file by splitting it into multiple modules within the
|
||||
beets/library directory.
|
||||
- Added a test to check that all plugins can be imported without errors.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,43 @@ class FtInTitlePluginFunctional(PluginTestCase):
|
|||
self.config["ftintitle"]["auto"] = auto
|
||||
self.config["ftintitle"]["keep_in_artist"] = keep_in_artist
|
||||
|
||||
def test_functional_no_featured_artist(self):
|
||||
item = self._ft_add_item("/", "Alice", "Song 1", "Alice")
|
||||
self.run_command("ftintitle")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_no_albumartist(self):
|
||||
self._ft_set_config("feat {0}")
|
||||
item = self._ft_add_item("/", "Alice ft. Bob", "Song 1", None)
|
||||
self.run_command("ftintitle")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1 feat Bob"
|
||||
|
||||
def test_functional_no_albumartist_no_feature(self):
|
||||
item = self._ft_add_item("/", "Alice", "Song 1", None)
|
||||
self.run_command("ftintitle")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_guest_artist(self):
|
||||
self._ft_set_config("featuring {0}")
|
||||
item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "George")
|
||||
self.run_command("ftintitle")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1 featuring Bob"
|
||||
|
||||
def test_functional_guest_artist_no_feature(self):
|
||||
item = self._ft_add_item("/", "Alice", "Song 1", "George")
|
||||
self.run_command("ftintitle")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_drop(self):
|
||||
item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice")
|
||||
self.run_command("ftintitle", "-d")
|
||||
|
|
@ -47,12 +84,25 @@ class FtInTitlePluginFunctional(PluginTestCase):
|
|||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_not_found(self):
|
||||
def test_functional_drop_no_featured_artist(self):
|
||||
item = self._ft_add_item("/", "Alice", "Song 1", "Alice")
|
||||
self.run_command("ftintitle", "-d")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_drop_guest_artist(self):
|
||||
item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "George")
|
||||
self.run_command("ftintitle", "-d")
|
||||
item.load()
|
||||
# item should be unchanged
|
||||
assert item["artist"] == "Alice ft Bob"
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_drop_guest_artist_no_feature(self):
|
||||
item = self._ft_add_item("/", "Alice", "Song 1", "George")
|
||||
self.run_command("ftintitle", "-d")
|
||||
item.load()
|
||||
assert item["artist"] == "Alice"
|
||||
assert item["title"] == "Song 1"
|
||||
|
||||
def test_functional_custom_format(self):
|
||||
|
|
@ -147,7 +197,7 @@ class FtInTitlePluginTest(unittest.TestCase):
|
|||
{
|
||||
"artist": "Alice ft. Carol",
|
||||
"album_artist": "Bob",
|
||||
"feat_part": None,
|
||||
"feat_part": "Carol",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -155,7 +205,7 @@ class FtInTitlePluginTest(unittest.TestCase):
|
|||
feat_part = ftintitle.find_feat_part(
|
||||
test_case["artist"], test_case["album_artist"]
|
||||
)
|
||||
assert feat_part == test_case["feat_part"]
|
||||
assert feat_part == test_case["feat_part"], f"failed: {test_case}"
|
||||
|
||||
def test_split_on_feat(self):
|
||||
parts = ftintitle.split_on_feat("Alice ft. Bob")
|
||||
|
|
|
|||
Loading…
Reference in a new issue