mirror of
https://github.com/beetbox/beets.git
synced 2026-01-19 14:45:17 +01:00
fix(ftintitle): remaining opportunities for improvement
This commit is contained in:
parent
572645b94c
commit
b14755df88
3 changed files with 58 additions and 71 deletions
|
|
@ -146,32 +146,19 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
|
||||
# If we have keywords, require one of them to appear in the bracket text.
|
||||
# If kw == "", the lookahead becomes true and we match any bracket content.
|
||||
kw = rf"\b(?:{kw_inner})\b" if kw_inner else ""
|
||||
|
||||
kw = rf"\b(?={kw_inner})\b" if kw_inner else ""
|
||||
return re.compile(
|
||||
rf"""
|
||||
(?: # Match ONE bracketed segment of any supported type
|
||||
\( # "("
|
||||
(?=[^)]*{kw}) # Lookahead: keyword must appear before closing ")"
|
||||
# - if kw == "", this is always true
|
||||
[^)]* # Consume bracket content (no nested ")" handling)
|
||||
\) # ")"
|
||||
|
||||
| \[ # "["
|
||||
(?=[^\]]*{kw}) # Lookahead
|
||||
[^\]]* # Consume content up to first "]"
|
||||
\] # "]"
|
||||
|
||||
| < # "<"
|
||||
(?=[^>]*{kw}) # Lookahead
|
||||
[^>]* # Consume content up to first ">"
|
||||
> # ">"
|
||||
|
||||
| \x7B # Literal open brace
|
||||
(?=[^\x7D]*{kw}) # Lookahead
|
||||
[^\x7D]* # Consume content up to first close brace
|
||||
\x7D # Literal close brace
|
||||
) # End bracketed segment alternation
|
||||
(?: # non-capturing group for the split
|
||||
\s*? # optional whitespace before brackets
|
||||
(?= # any bracket containing a keyword
|
||||
\([^)]*{kw}.*?\)
|
||||
| \[[^]]*{kw}.*?\]
|
||||
| <[^>]*{kw}.*? >
|
||||
| \{{[^}}]*{kw}.*?\}}
|
||||
| $ # or the end of the string
|
||||
)
|
||||
)
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
|
@ -290,7 +277,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
if not drop_feat and not contains_feat(item.title, custom_words):
|
||||
feat_format = self.config["format"].as_str()
|
||||
formatted = feat_format.format(feat_part)
|
||||
new_title = FtInTitlePlugin.insert_ft_into_title(
|
||||
new_title = self.insert_ft_into_title(
|
||||
item.title, formatted, self.bracket_keywords
|
||||
)
|
||||
self._log.info("title: {.title} -> {}", item, new_title)
|
||||
|
|
@ -349,19 +336,16 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
|
|||
m: re.Match[str] | None = pattern.search(title)
|
||||
return m.start() if m else None
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
def insert_ft_into_title(
|
||||
title: str, feat_part: str, keywords: list[str] | None = None
|
||||
cls, title: str, feat_part: str, keywords: list[str] | None = None
|
||||
) -> str:
|
||||
"""Insert featured artist before the first bracket containing
|
||||
remix/edit keywords if present.
|
||||
"""
|
||||
if (
|
||||
bracket_pos := FtInTitlePlugin.find_bracket_position(
|
||||
title, keywords
|
||||
)
|
||||
) is not None:
|
||||
title_before = title[:bracket_pos].rstrip()
|
||||
title_after = title[bracket_pos:]
|
||||
return f"{title_before} {feat_part} {title_after}"
|
||||
return f"{title} {feat_part}"
|
||||
normalized = (
|
||||
DEFAULT_BRACKET_KEYWORDS if keywords is None else tuple(keywords)
|
||||
)
|
||||
pattern = cls._bracket_position_pattern(normalized)
|
||||
parts = pattern.split(title, maxsplit=1)
|
||||
return f" {feat_part} ".join(parts).strip()
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ ignore = [
|
|||
[tool.ruff.lint.per-file-ignores]
|
||||
"beets/**" = ["PT"]
|
||||
"test/test_util.py" = ["E501"]
|
||||
"test/plugins/test_ftintitle.py" = ["E501"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
split-on-trailing-comma = false
|
||||
|
|
|
|||
|
|
@ -335,55 +335,57 @@ def test_split_on_feat(
|
|||
[
|
||||
## default keywords
|
||||
# different braces and keywords
|
||||
("Song (Remix)", None, 5),
|
||||
("Song [Version]", None, 5),
|
||||
("Song {Extended Mix}", None, 5),
|
||||
("Song <Instrumental>", None, 5),
|
||||
("Song (Remix)", None, "Song ft. Bob (Remix)"),
|
||||
("Song [Version]", None, "Song ft. Bob [Version]"),
|
||||
("Song {Extended Mix}", None, "Song ft. Bob {Extended Mix}"),
|
||||
("Song <Instrumental>", None, "Song ft. Bob <Instrumental>"),
|
||||
# two keyword clauses
|
||||
("Song (Remix) (Live)", None, 5),
|
||||
("Song (Remix) (Live)", None, "Song ft. Bob (Remix) (Live)"),
|
||||
# brace insensitivity
|
||||
("Song (Live) [Remix]", None, 5),
|
||||
("Song [Edit] (Remastered)", None, 5),
|
||||
("Song (Live) [Remix]", None, "Song ft. Bob (Live) [Remix]"),
|
||||
("Song [Edit] (Remastered)", None, "Song ft. Bob [Edit] (Remastered)"),
|
||||
# negative cases
|
||||
("Song", None, None), # no clause
|
||||
("Song (Arbitrary)", None, None), # no keyword
|
||||
("Song (", None, None), # no matching brace or keyword
|
||||
("Song (Live", None, None), # no matching brace with keyword
|
||||
("Song", None, "Song ft. Bob"), # no clause
|
||||
("Song (Arbitrary)", None, "Song (Arbitrary) ft. Bob"), # no keyword
|
||||
("Song (", None, "Song ( ft. Bob"), # no matching brace or keyword
|
||||
("Song (Live", None, "Song (Live ft. Bob"), # no matching brace with keyword
|
||||
# one keyword clause, one non-keyword clause
|
||||
("Song (Live) (Arbitrary)", None, 5),
|
||||
("Song (Arbitrary) (Remix)", None, 17),
|
||||
("Song (Live) (Arbitrary)", None, "Song ft. Bob (Live) (Arbitrary)"),
|
||||
("Song (Arbitrary) (Remix)", None, "Song (Arbitrary) ft. Bob (Remix)"),
|
||||
# nested brackets - same type
|
||||
("Song (Remix (Extended))", None, 5),
|
||||
("Song [Arbitrary [Description]]", None, None),
|
||||
("Song (Remix (Extended))", None, "Song ft. Bob (Remix (Extended))"),
|
||||
("Song [Arbitrary [Description]]", None, "Song [Arbitrary [Description]] ft. Bob"),
|
||||
# nested brackets - different types
|
||||
("Song (Remix [Extended])", None, 5),
|
||||
("Song (Remix [Extended])", None, "Song ft. Bob (Remix [Extended])"),
|
||||
# nested - returns outer start position despite inner keyword
|
||||
("Song [Arbitrary {Extended}]", None, 5),
|
||||
("Song {Live <Arbitrary>}", None, 5),
|
||||
("Song <Remaster (Arbitrary)>", None, 5),
|
||||
("Song <Extended> [Live]", None, 5),
|
||||
("Song (Version) <Live>", None, 5),
|
||||
("Song (Arbitrary [Description])", None, None),
|
||||
("Song [Description (Arbitrary)]", None, None),
|
||||
("Song [Arbitrary {Extended}]", None, "Song ft. Bob [Arbitrary {Extended}]"),
|
||||
("Song {Live <Arbitrary>}", None, "Song ft. Bob {Live <Arbitrary>}"),
|
||||
("Song <Remaster (Arbitrary)>", None, "Song ft. Bob <Remaster (Arbitrary)>"),
|
||||
("Song <Extended> [Live]", None, "Song ft. Bob <Extended> [Live]"),
|
||||
("Song (Version) <Live>", None, "Song ft. Bob (Version) <Live>"),
|
||||
("Song (Arbitrary [Description])", None, "Song (Arbitrary [Description]) ft. Bob"),
|
||||
("Song [Description (Arbitrary)]", None, "Song [Description (Arbitrary)] ft. Bob"),
|
||||
## custom keywords
|
||||
("Song (Live)", ["live"], 5),
|
||||
("Song (Concert)", ["concert"], 5),
|
||||
("Song (Remix)", ["custom"], None),
|
||||
("Song (Custom)", ["custom"], 5),
|
||||
("Song", [], None),
|
||||
("Song (", [], None),
|
||||
("Song (Live)", ["live"], "Song ft. Bob (Live)"),
|
||||
("Song (Concert)", ["concert"], "Song ft. Bob (Concert)"),
|
||||
("Song (Remix)", ["custom"], "Song (Remix) ft. Bob"),
|
||||
("Song (Custom)", ["custom"], "Song ft. Bob (Custom)"),
|
||||
("Song", [], "Song ft. Bob"),
|
||||
("Song (", [], "Song ( ft. Bob"),
|
||||
# Multi-word keyword tests
|
||||
("Song (Club Mix)", ["club mix"], 5), # Positive: matches multi-word
|
||||
("Song (Club Remix)", ["club mix"], None), # Negative: no match
|
||||
("Song (Club Mix)", ["club mix"], "Song ft. Bob (Club Mix)"), # Positive: matches multi-word
|
||||
("Song (Club Remix)", ["club mix"], "Song (Club Remix) ft. Bob"), # Negative: no match
|
||||
],
|
||||
)
|
||||
def test_find_bracket_position(
|
||||
) # fmt: skip
|
||||
def test_insert_ft_into_title(
|
||||
given: str,
|
||||
keywords: list[str] | None,
|
||||
expected: int | None,
|
||||
expected: str,
|
||||
) -> None:
|
||||
assert (
|
||||
ftintitle.FtInTitlePlugin.find_bracket_position(given, keywords)
|
||||
ftintitle.FtInTitlePlugin.insert_ft_into_title(
|
||||
given, "ft. Bob", keywords
|
||||
)
|
||||
== expected
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue