Skip to content

Conversation

@mpksolutions
Copy link

Description

Fixes an issue where the ftintitle plugin would incorrectly split artist names on generic separators (&, and) when explicit featuring tokens (feat, ft, featuring) were also present.

Example

  • Before: Alice & Bob feat Charlie could split on &("Alice", "Bob feat Charlie")
  • After: Alice & Bob feat Charlie splits on feat("Alice & Bob", "Charlie")

Changes

  • Modified split_on_feat() to try explicit featuring tokens first
  • Falls back to generic separators only if no explicit tokens are found
  • Added test cases to verify correct behavior

To Do

  • Documentation. (Not applicable - internal behavior fix)
  • Changelog. (Added entry to docs/changelog.rst)
  • Tests. (Added 4 new test cases, all 76 tests passing)

- Prioritize explicit featuring tokens (feat, ft, featuring) over generic
  separators (&, and) when splitting artist names
- Prevents incorrect splits like 'Alice & Bob feat Charlie' from splitting
  on '&' instead of 'feat'
- Add test cases to verify the fix
@mpksolutions mpksolutions requested a review from a team as a code owner December 10, 2025 21:00
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `beetsplug/ftintitle.py:45-46` </location>
<code_context>
+        re.IGNORECASE,
     )
-    parts = tuple(s.strip() for s in regex.split(artist, 1))
+    parts = tuple(s.strip() for s in regex_explicit.split(artist, 1))
+    if len(parts) == 2:
+        return parts
+
</code_context>

<issue_to_address>
**issue (bug_risk):** When `for_artist` is False we no longer fall back to generic tokens, which changes previous behavior.

Previously, both artist and non-artist paths used `feat_tokens(for_artist, custom_words)`, so generic separators were still applied when no explicit featuring token was present. Now, when `for_artist` is False and there’s no explicit `feat`/`ft` token, the generic-token regex is never used and we always return `(artist, None)`. If you only want to avoid over-splitting the artist field, consider preserving the generic fallback for the non-artist case by moving that logic outside `if for_artist:` or adding an `else` that calls `feat_tokens(False, custom_words)`.
</issue_to_address>

### Comment 2
<location> `test/plugins/test_ftintitle.py:306-309` </location>
<code_context>
         ("Alice and Bob", ("Alice", "Bob")),
         ("Alice With Bob", ("Alice", "Bob")),
         ("Alice defeat Bob", ("Alice defeat Bob", None)),
+        ("Alice & Bob feat Charlie", ("Alice & Bob", "Charlie")),
+        ("Alice & Bob ft. Charlie", ("Alice & Bob", "Charlie")),
+        ("Alice & Bob featuring Charlie", ("Alice & Bob", "Charlie")),
+        ("Alice and Bob feat Charlie", ("Alice and Bob", "Charlie")),
     ],
 )
</code_context>

<issue_to_address>
**suggestion (testing):** Add a case where both explicit featuring tokens and generic separators appear on the right-hand side of the split

The added tests cover precedence between explicit featuring tokens and `&`/`and` between primary and featuring artists. To fully cover the behavior, also add a case like `"Alice & Bob feat Charlie & Dave" -> ("Alice & Bob", "Charlie & Dave")` (and possibly `and`/`with` variants) to confirm that, once an explicit token is found, any generic separators to its right remain part of the featuring artist rather than causing another split.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +45 to +46
parts = tuple(s.strip() for s in regex_explicit.split(artist, 1))
if len(parts) == 2:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): When for_artist is False we no longer fall back to generic tokens, which changes previous behavior.

Previously, both artist and non-artist paths used feat_tokens(for_artist, custom_words), so generic separators were still applied when no explicit featuring token was present. Now, when for_artist is False and there’s no explicit feat/ft token, the generic-token regex is never used and we always return (artist, None). If you only want to avoid over-splitting the artist field, consider preserving the generic fallback for the non-artist case by moving that logic outside if for_artist: or adding an else that calls feat_tokens(False, custom_words).

@codecov
Copy link

codecov bot commented Dec 10, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.90%. Comparing base (ffede9d) to head (9ba3e12).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #6213   +/-   ##
=======================================
  Coverage   67.90%   67.90%           
=======================================
  Files         137      137           
  Lines       18689    18694    +5     
  Branches     3160     3162    +2     
=======================================
+ Hits        12690    12695    +5     
  Misses       5332     5332           
  Partials      667      667           
Files with missing lines Coverage Δ
beetsplug/ftintitle.py 86.02% <100.00%> (+0.79%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@semohr
Copy link
Contributor

semohr commented Jan 5, 2026

The changes make sense from my perspective.

@henry-oberholtzer @EmberLightVFX Do you mind reviewing this too? I have the feeling that there might be something I'm missing since I'm not too familiar with the ftintitle plugin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants