Repository: beetbox/beets

Issue:
ftintitle: False positives in "feat. X" detection
### Problem

The `ftintitle` plugin has the `contains_feat` function to check whether a given title already contains a "feat. X" part. The regex used for pattern matching is defined in the `feat_tokens` function in `beets/plugins.py`.

In the default call of `feat_tokens()`, this adds possible `feat_words` like "and" and "with". This can yield to false positives in the feature detection as shown in the following example:

```python
>>> from beetsplug import ftintitle
>>> ftintitle.contains_feat("Live and Let Go")
True
```

However, this is simply the title of the song and does not contain a "feat. X" part. (The "and" in the title is detected and matched in the regex.)

The consequence of this is that the `ftintitle` plugin will actually not add the actual "feat. X" part to the title as it thinks it already contains one.

### Fix

I believe the issue is that in `ftintitle.contains_feat`, the regex is constructed using `plugins.feat_tokens()`, which sets `for_artist=True`. A fix would be to change this to `plugins.feat_tokens(for_artist=False)` as we are matching song titles and not artist.


### Setup

* OS: Linux
* Python version: 3.12
* beets version: `master` branch @ 1a59368dbfea5bf4bcab5fdb95e4201fe072d4d5


Discussion:

Patch:
diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py
index 5bc018fe10..e4f51fd10b 100644
--- a/beetsplug/ftintitle.py
+++ b/beetsplug/ftintitle.py
@@ -37,7 +37,13 @@ def split_on_feat(artist):
 
 def contains_feat(title):
     """Determine whether the title contains a "featured" marker."""
-    return bool(re.search(plugins.feat_tokens(), title, flags=re.IGNORECASE))
+    return bool(
+        re.search(
+            plugins.feat_tokens(for_artist=False),
+            title,
+            flags=re.IGNORECASE,
+        )
+    )
 
 
 def find_feat_part(artist, albumartist):
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 63ce2e0c41..a9ed03e422 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -26,7 +26,9 @@ New features:
 
 Bug fixes:
 
-* The detection of a "feat. X" part now also matches such parts if they are in
+* :doc:`plugins/ftintitle`: The detection of a "feat. X" part in a song title does not produce any false
+  positives caused by words like "and" or "with" anymore. :bug:`5441`
+* :doc:`plugins/ftintitle`: The detection of a "feat. X" part now also matches such parts if they are in
   parentheses or brackets. :bug:`5436`
 * Improve naming of temporary files by separating the random part with the file extension.
 * Fix the ``auto`` value for the :ref:`reflink` config option.


Test patch:
diff --git a/test/plugins/test_ftintitle.py b/test/plugins/test_ftintitle.py
index 9e8f14fe1a..1dbe4a7274 100644
--- a/test/plugins/test_ftintitle.py
+++ b/test/plugins/test_ftintitle.py
@@ -180,9 +180,6 @@ def test_contains_feat(self):
         assert ftintitle.contains_feat("Alice feat. Bob")
         assert ftintitle.contains_feat("Alice feat Bob")
         assert ftintitle.contains_feat("Alice featuring Bob")
-        assert ftintitle.contains_feat("Alice & Bob")
-        assert ftintitle.contains_feat("Alice and Bob")
-        assert ftintitle.contains_feat("Alice With Bob")
         assert ftintitle.contains_feat("Alice (ft. Bob)")
         assert ftintitle.contains_feat("Alice (feat. Bob)")
         assert ftintitle.contains_feat("Alice [ft. Bob]")
@@ -190,3 +187,5 @@ def test_contains_feat(self):
         assert not ftintitle.contains_feat("Alice defeat Bob")
         assert not ftintitle.contains_feat("Aliceft.Bob")
         assert not ftintitle.contains_feat("Alice (defeat Bob)")
+        assert not ftintitle.contains_feat("Live and Let Go")
+        assert not ftintitle.contains_feat("Come With Me")