Ftintitle: Continue even if albumartist and artist is the same (#6102)

## Description

This small PR allows ftintitle to process even if the artist/s in the
artist and albumartist fields are the same.

This fixes the problem with a lot of singles like [Porter Robinsons song
Shelter](https://musicbrainz.org/release/ccc261b9-e4cc-4965-81b8-7c92a5d28601)
and even [Rihanas's album
Umbrella](https://musicbrainz.org/release/60f8f1f5-485b-4637-8574-23f2bb98531f)

Without this fix the songs would end up with the feat. artist in the
artists folder-name and not the feat. in the songs filename.
Without:
`Rihanna feat. JAY‐Z\(2007) Umbrella\01 - Umbrella (radio edit).flac`
`Porter Robinson feat. Madeon\(2016) Shelter\01 - Shelter.flac`
With:
`Rihanna\(2007) Umbrella\01 - Umbrella (radio edit) feat. JAY‐Z.flac`
`Porter Robinson\(2016) Shelter\01 - Shelter feat. Madeon.flac`

I left the current way ftintitle works as the default so stuff doesn't
randomly change for users, but maybe it should is changed as the PR that
changed the ftintitle's behavour is only ~2 month old
https://github.com/beetbox/beets/pull/5943
Thoughts?

I'm also not super happy with the args name
`skip_if_artist_and_album_artists_is_the_same` so any suggestion what it
could be instead is more than welcome 😅


## To Do

<!--
- If you believe one of below checkpoints is not required for the change
you
are submitting, cross it out and check the box nonetheless to let us
know.
  For example: - [x] ~Changelog~
- Regarding the changelog, often it makes sense to add your entry only
once
reviewing is finished. That way you might prevent conflicts from other
PR's in
that file, as well as keep the chance high your description fits with
the
  latest revision of your feature/fix.
- Regarding documentation, bugfixes often don't require additions to the
docs.
- Please remove the descriptive sentences in braces from the enumeration
below,
  which helps to unclutter your PR description.
-->

- [x] Documentation. (If you've added a new command-line flag, for
example, find the appropriate page under `docs/` to describe it.)
- [x] Changelog. (Add an entry to `docs/changelog.rst` to the bottom of
one of the lists near the top of the document.)
- [x] Tests. (Very much encouraged but not strictly required.)
This commit is contained in:
henry 2025-10-20 17:52:36 -07:00 committed by GitHub
commit 043581e0c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 63 additions and 4 deletions

View file

@ -108,6 +108,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
"drop": False, "drop": False,
"format": "feat. {}", "format": "feat. {}",
"keep_in_artist": False, "keep_in_artist": False,
"preserve_album_artist": True,
"custom_words": [], "custom_words": [],
} }
) )
@ -133,12 +134,19 @@ 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)
preserve_album_artist = self.config["preserve_album_artist"].get(
bool
)
custom_words = self.config["custom_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_words item,
drop_feat,
keep_in_artist_field,
preserve_album_artist,
custom_words,
): ):
item.store() item.store()
if write: if write:
@ -151,11 +159,16 @@ 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)
preserve_album_artist = self.config["preserve_album_artist"].get(bool)
custom_words = self.config["custom_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_words item,
drop_feat,
keep_in_artist_field,
preserve_album_artist,
custom_words,
): ):
item.store() item.store()
@ -204,6 +217,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,
preserve_album_artist: bool,
custom_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
@ -218,7 +232,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
# Check whether there is a featured artist on this track and the # Check whether there is a featured artist on this track and the
# artist field does not exactly match the album artist field. In # artist field does not exactly match the album artist field. In
# that case, we attempt to move the featured artist to the title. # that case, we attempt to move the featured artist to the title.
if albumartist and artist == albumartist: if preserve_album_artist and albumartist and artist == albumartist:
return False return False
_, featured = split_on_feat(artist, custom_words=custom_words) _, featured = split_on_feat(artist, custom_words=custom_words)

View file

@ -10,7 +10,9 @@ Unreleased
New features: New features:
- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle. - :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.
- :doc: `/plugins/play`: Added `$playlist` marker to precisely edit the playlist - :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. filepath into the command calling the player program.
Bug fixes: Bug fixes:

View file

@ -28,6 +28,8 @@ file. The available options are:
- **keep_in_artist**: Keep the featuring X part in the artist field. This can be - **keep_in_artist**: Keep the featuring X part in the artist field. This can be
useful if you still want to be able to search for features in the artist useful if you still want to be able to search for features in the artist
field. Default: ``no``. field. Default: ``no``.
- **preserve_album_artist**: If the artist and the album artist are the same,
skip the ftintitle processing. Default: ``yes``.
- **custom_words**: List of additional words that will be treated as a marker - **custom_words**: List of additional words that will be treated as a marker
for artist features. Default: ``[]``. for artist features. Default: ``[]``.

View file

@ -205,6 +205,47 @@ def add_item(
("Alice med Bob", "Song 1"), ("Alice med Bob", "Song 1"),
id="custom-feat-words-keep-in-artists-drop-from-title", id="custom-feat-words-keep-in-artists-drop-from-title",
), ),
# ---- preserve_album_artist variants ----
pytest.param(
{
"format": "feat. {}",
"preserve_album_artist": True,
},
("ftintitle",),
("Alice feat. Bob", "Song 1", "Alice"),
("Alice", "Song 1 feat. Bob"),
id="skip-if-artist-and-album-artists-is-the-same-different-match",
),
pytest.param(
{
"format": "feat. {}",
"preserve_album_artist": False,
},
("ftintitle",),
("Alice feat. Bob", "Song 1", "Alice"),
("Alice", "Song 1 feat. Bob"),
id="skip-if-artist-and-album-artists-is-the-same-different-match-b",
),
pytest.param(
{
"format": "feat. {}",
"preserve_album_artist": True,
},
("ftintitle",),
("Alice feat. Bob", "Song 1", "Alice feat. Bob"),
("Alice feat. Bob", "Song 1"),
id="skip-if-artist-and-album-artists-is-the-same-matching-match",
),
pytest.param(
{
"format": "feat. {}",
"preserve_album_artist": False,
},
("ftintitle",),
("Alice feat. Bob", "Song 1", "Alice feat. Bob"),
("Alice", "Song 1 feat. Bob"),
id="skip-if-artist-and-album-artists-is-the-same-matching-match-b",
),
], ],
) )
def test_ftintitle_functional( def test_ftintitle_functional(