Skip to content

feat(ftintitle): hook metadata events, fixing mbsync#6726

Open
treyturner wants to merge 5 commits into
beetbox:masterfrom
treyturner:feat/ftintitle-hook-metadata-events
Open

feat(ftintitle): hook metadata events, fixing mbsync#6726
treyturner wants to merge 5 commits into
beetbox:masterfrom
treyturner:feat/ftintitle-hook-metadata-events

Conversation

@treyturner

@treyturner treyturner commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Depends on #6732.

Summary

This PR integrates ftintitle with metadata fetched through beets’ metadata-received events, so commands like mbsync apply already-normalized metadata when ftintitle is enabled.

Addresses #1153.

Changes

  • Register ftintitle listeners for trackinfo_received and albuminfo_received when ftintitle.auto is enabled.
  • Rewrite fetched TrackInfo metadata before mbsync applies it, avoiding unnecessary re-sync churn.
  • Preserve existing manual command and import behavior while sharing the same rewrite logic.
  • Handle artist_credit, cached Info properties, empty titles, and no-op configurations consistently.
  • Update ftintitle docs for the expanded auto behavior.

To Do

  • Documentation. (If you've added a new command-line flag, for example, find the appropriate page under docs/ to describe it.)
  • Changelog. (Add an entry to docs/changelog.rst to the bottom of one of the lists near the top of the document.)
  • Tests. (Very much encouraged but not strictly required.)

@github-actions github-actions Bot added ftintitle ftintitle plugin mbsync mbsync plugin labels Jun 10, 2026
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from 38e0446 to 8839b95 Compare June 10, 2026 10:16
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.52239% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.91%. Comparing base (37e2a8b) to head (109c61b).
⚠️ Report is 40 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/ftintitle.py 95.52% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6726      +/-   ##
==========================================
+ Coverage   74.85%   74.91%   +0.05%     
==========================================
  Files         163      163              
  Lines       20947    20998      +51     
  Branches     3299     3307       +8     
==========================================
+ Hits        15680    15730      +50     
+ Misses       4510     4508       -2     
- Partials      757      760       +3     
Files with missing lines Coverage Δ
beetsplug/ftintitle.py 92.74% <95.52%> (+3.30%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch 4 times, most recently from ff4a62f to 06da57e Compare June 10, 2026 11:20
@treyturner treyturner marked this pull request as ready for review June 10, 2026 11:32
@treyturner treyturner requested a review from a team as a code owner June 10, 2026 11:32
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from 06da57e to 9e8f19c Compare June 11, 2026 21:55

@snejus snejus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's split the following into a separate PR:

Define each configuration option as a @cached_property to remove the need to send them through each function. See 34772d2 where I did the same for convert plugin.

This will remove a big chunk of the current PR diff and make it easier to review the functionality that you added.

Comment thread beetsplug/ftintitle.py Outdated
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch 3 times, most recently from 3719e23 to ff085e4 Compare June 12, 2026 17:11
@treyturner

Copy link
Copy Markdown
Contributor Author

The cached properties are now in #6732 on which this PR is based. The remaining changes can be seen in the single subsequent commit:

ff085e4 (this PR)

@treyturner treyturner requested a review from snejus June 12, 2026 17:35
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from ff085e4 to 7bfb1a7 Compare June 13, 2026 01:18
snejus added a commit that referenced this pull request Jun 13, 2026
Pre-factor for #6726.

Make `ftintitle`'s config `@cached_property` attributes instead of
passing them through the call stack. Modeled after similar recent
changes in `convert`.

## Changes

- Add cached plugin properties for `auto`, `drop`, `format`,
`keep_in_artist`, `preserve_album_artist`, and `custom_words`, keeping
the existing cached `bracket_keywords` property I forgot I added a while
back.
- `commands()`, `imported()`, `ft_in_title()`, and `update_metadata()`
are cleaned up to read these directly.
- Don't read `auto` during plugin init; the import stage remains
registered and `imported()` checks `auto` when invoked.
@snejus

snejus commented Jun 13, 2026

Copy link
Copy Markdown
Member

Now rebase it on master and I'll review it again :)

@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch 2 times, most recently from 7b4d06a to 146715c Compare June 19, 2026 02:01
@snejus

snejus commented Jun 19, 2026

Copy link
Copy Markdown
Member

Now just rebase it again since your fix has been merged!

@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch 2 times, most recently from 2202df0 to ca2dcce Compare June 20, 2026 03:44

@snejus snejus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Had the first review - will have another look once these comments are addressed!

Comment thread beets/autotag/hooks.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
@treyturner

treyturner commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

I /think/ these are all addressed, but yes please have another look as updates were quite scattered. I really appreciate the reviews and help. Every few months when I get time to pick up more beets work I have to remember everything I learned about the data model and then forgot since the last hiatus 🤤

@treyturner treyturner requested a review from snejus June 20, 2026 20:22
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from 0074e86 to e94329a Compare June 20, 2026 20:23
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from e94329a to a42a3a3 Compare June 20, 2026 20:27
@snejus

snejus commented Jun 23, 2026

Copy link
Copy Markdown
Member

I have to remember everything I learned about the data model and then forgot since the last hiatus 🤤

It's also been regularly changing as we've been refactoring it... 😆

@snejus snejus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another quick review round: as I mentioned in the comments, inclusion of artist_credit will have unintended side-effects and we'd rather this separately.

Will have a final review once it's updated!

Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
Comment thread beetsplug/ftintitle.py Outdated
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from a42a3a3 to baeeff0 Compare June 27, 2026 17:38
@treyturner treyturner force-pushed the feat/ftintitle-hook-metadata-events branch from 03551ef to 109c61b Compare June 27, 2026 18:29
@treyturner treyturner requested a review from snejus June 27, 2026 18:54
Comment thread beetsplug/ftintitle.py
Comment on lines +281 to +291
def _strip_featured_from_field(
self,
metadata: Info | Item,
field: FeaturedField,
for_artist: bool = True,
) -> None:
if value := metadata.get(field):
stripped, _ = split_on_feat(
value, for_artist=for_artist, custom_words=self.custom_words
)
metadata[field] = stripped

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this method introduced? This is applied within update_info_metadata, but isn't applied within update_item_metadata.

Furthermore, this method edits the structure that was passed into it - not a good idea. Best to remove it

Comment thread beetsplug/ftintitle.py

return changed

def update_info_metadata(self, info: Info, feat_part: str) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are Item and Info updated using separate methods? Is there any difference in the logic?

It seems like we should be able to use a single update_metadata method instead.

Comment thread beetsplug/ftintitle.py
for track_info in info.tracks:
self.ft_in_info(track_info, albumartist)

def ft_in_title(self, item: Item) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see you've taken the right direction and defined ft_in_title and ft_in_info to be very similar to each other. We don't need a second method here - just define

    def ft_in_title(self, item: Item | Info, albumartist: str) -> bool:

and remove ft_in_info.

Note you will probably need to guard this line in ft_in_title implementation with hasattr("filepath") or something, since filepath is not defined on Info.

        self._log.info("{.filepath}", item)

Comment on lines +277 to +278
assert info.item_data["artist"] == "Alice"
assert info.item_data["title"] == "Song feat. Bob"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to assert these two since they don't seem to be functionally related to your changes.

Comment thread beetsplug/ftintitle.py
"""
if not albumartist:
_, feat_part = split_on_feat(
artist, for_artist=True, custom_words=custom_words

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this method adjusted?

Comment on lines +362 to +378
def test_trackinfo_received_preserves_collaborative_artist_credit(
self,
) -> None:
self.config["artist_credit"] = False
info = TrackInfo(
artist="Alice feat. Bob",
artist_credit="Alice & Bobby",
title="Song",
)

with self.configure_plugin({"auto": True}):
plugins.send("trackinfo_received", info=info)

assert info.artist == "Alice"
assert info.artist_credit == "Alice & Bobby"
assert info.title == "Song feat. Bob"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is essentially a copy of test_trackinfo_received_preserves_artist_credit_when_disabled.

Suggested change
def test_trackinfo_received_preserves_collaborative_artist_credit(
self,
) -> None:
self.config["artist_credit"] = False
info = TrackInfo(
artist="Alice feat. Bob",
artist_credit="Alice & Bobby",
title="Song",
)
with self.configure_plugin({"auto": True}):
plugins.send("trackinfo_received", info=info)
assert info.artist == "Alice"
assert info.artist_credit == "Alice & Bobby"
assert info.title == "Song feat. Bob"

Comment on lines +379 to +414
def test_command_preserves_artist_credit_when_enabled(self) -> None:
self.config["artist_credit"] = True
item = self.add_item(
path="/",
artist="Alice feat. Bob",
artist_credit="Alice feat. Bobby",
title="Song",
albumartist="Alice",
)

with self.configure_plugin({"auto": True}):
self.run_command("ftintitle")

item.load()
assert item.artist == "Alice"
assert item.artist_credit == "Alice feat. Bobby"
assert item.title == "Song feat. Bob"

def test_command_preserves_artist_credit_when_disabled(self) -> None:
self.config["artist_credit"] = False
item = self.add_item(
path="/",
artist="Alice feat. Bob",
artist_credit="Alice feat. Bobby",
title="Song",
albumartist="Alice",
)

with self.configure_plugin({"auto": True}):
self.run_command("ftintitle")

item.load()
assert item.artist == "Alice"
assert item.artist_credit == "Alice feat. Bobby"
assert item.title == "Song feat. Bob"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We removed artist_credit handling.

Suggested change
def test_command_preserves_artist_credit_when_enabled(self) -> None:
self.config["artist_credit"] = True
item = self.add_item(
path="/",
artist="Alice feat. Bob",
artist_credit="Alice feat. Bobby",
title="Song",
albumartist="Alice",
)
with self.configure_plugin({"auto": True}):
self.run_command("ftintitle")
item.load()
assert item.artist == "Alice"
assert item.artist_credit == "Alice feat. Bobby"
assert item.title == "Song feat. Bob"
def test_command_preserves_artist_credit_when_disabled(self) -> None:
self.config["artist_credit"] = False
item = self.add_item(
path="/",
artist="Alice feat. Bob",
artist_credit="Alice feat. Bobby",
title="Song",
albumartist="Alice",
)
with self.configure_plugin({"auto": True}):
self.run_command("ftintitle")
item.load()
assert item.artist == "Alice"
assert item.artist_credit == "Alice feat. Bobby"
assert item.title == "Song feat. Bob"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The tests aimed to make the current behavior explicit and support subsequent discussion or possible upcoming changes you hinted around mutating artist_credit, but I'll remove them 👍

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

Labels

ftintitle ftintitle plugin mbsync mbsync plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants