From 8d11ed51d298588ce9c9908dd4c5e7fd66ea14cb Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 4 Oct 2025 21:29:02 -0700 Subject: [PATCH] Initial title case plugin written and working, needs to apply to tags --- beetsplug/titlecase.py | 51 +++++++++++++++++++++++++++++++ poetry.lock | 18 ++++++++++- pyproject.toml | 2 ++ test/plugins/test_titlecase.py | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 beetsplug/titlecase.py create mode 100644 test/plugins/test_titlecase.py diff --git a/beetsplug/titlecase.py b/beetsplug/titlecase.py new file mode 100644 index 000000000..af7a88f38 --- /dev/null +++ b/beetsplug/titlecase.py @@ -0,0 +1,51 @@ +# This file is part of beets. +# Copyright 2025, Henry Oberholtzer +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Apply NYT manual of style title case rules, to paths and tag text. + Title case logic is derived from the python-titlecase library.""" + +from beets.plugins import BeetsPlugin +from titlecase import titlecase + +__author__ = "henryoberholtzer@gmail.com" +__version__ = "1.0" + +class TitlecasePlugin(BeetsPlugin): + preserve: dict[str, str] = {} + def __init__(self) -> None: + super().__init__() + # Register template function + self.template_funcs["titlecase"] = self.titlecase + + self.config.add( + { + "preserve": [], + "small_first_last": True + } + ) + print(self.config) + for word in self.config["preserve"].as_str_seq(): + self.preserve[word.upper()] = word + + def __preserved__(self, word, **kwargs) -> str | None: + """ Callback function for words to preserve case of.""" + if (preserved_word := self.preserve.get(word.upper(), "")): + return preserved_word + return None + + def titlecase(self, text: str) -> str: + """ Titlecase the given text """ + return titlecase(text, + small_first_last=self.config["small_first_last"], + callback=self.__preserved__) diff --git a/poetry.lock b/poetry.lock index 8c109f930..36811b448 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2182,6 +2182,8 @@ files = [ {file = "pycairo-1.28.0-cp313-cp313-win32.whl", hash = "sha256:d13352429d8a08a1cb3607767d23d2fb32e4c4f9faa642155383980ec1478c24"}, {file = "pycairo-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:082aef6b3a9dcc328fa648d38ed6b0a31c863e903ead57dd184b2e5f86790140"}, {file = "pycairo-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:026afd53b75291917a7412d9fe46dcfbaa0c028febd46ff1132d44a53ac2c8b6"}, + {file = "pycairo-1.28.0-cp314-cp314-win32.whl", hash = "sha256:d0ab30585f536101ad6f09052fc3895e2a437ba57531ea07223d0e076248025d"}, + {file = "pycairo-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:94f2ed204999ab95a0671a0fa948ffbb9f3d6fb8731fe787917f6d022d9c1c0f"}, {file = "pycairo-1.28.0-cp39-cp39-win32.whl", hash = "sha256:3ed16d48b8a79cc584cb1cb0ad62dfb265f2dda6d6a19ef5aab181693e19c83c"}, {file = "pycairo-1.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:da0d1e6d4842eed4d52779222c6e43d254244a486ca9fdab14e30042fd5bdf28"}, {file = "pycairo-1.28.0-cp39-cp39-win_arm64.whl", hash = "sha256:458877513eb2125513122e8aa9c938630e94bb0574f94f4fb5ab55eb23d6e9ac"}, @@ -3364,6 +3366,19 @@ files = [ {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, ] +[[package]] +name = "titlecase" +version = "2.4.1" +description = "Python Port of John Gruber's titlecase.pl" +optional = true +python-versions = ">=3.7" +files = [ + {file = "titlecase-2.4.1.tar.gz", hash = "sha256:7d83a277ccbbda11a2944e78a63e5ccaf3d32f828c594312e4862f9a07f635f5"}, +] + +[package.extras] +regex = ["regex (>=2020.4.4)"] + [[package]] name = "toml" version = "0.10.2" @@ -3624,9 +3639,10 @@ replaygain = ["PyGObject"] scrub = ["mutagen"] sonosupdate = ["soco"] thumbnails = ["Pillow", "pyxdg"] +titlecase = ["titlecase"] web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "faea27878ce1ca3f1335fd83e027b289351c51c73550bda72bf501a9c82166f7" +content-hash = "c5a6a4710beb5bf1e4bbd97f2a590ee020a567aea2341aad5f846eda13ebb94e" diff --git a/pyproject.toml b/pyproject.toml index 8338ce1c6..0407e8b22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ requests = { version = "*", optional = true } resampy = { version = ">=0.4.3", optional = true } requests-oauthlib = { version = ">=0.6.1", optional = true } soco = { version = "*", optional = true } +titlecase = { version = ">=2.4.1", optional = true } pydata-sphinx-theme = { version = "*", optional = true } sphinx = { version = "*", optional = true } @@ -151,6 +152,7 @@ replaygain = [ scrub = ["mutagen"] sonosupdate = ["soco"] thumbnails = ["Pillow", "pyxdg"] +titlecase = ["titlecase"] web = ["flask", "flask-cors"] [tool.poetry.scripts] diff --git a/test/plugins/test_titlecase.py b/test/plugins/test_titlecase.py new file mode 100644 index 000000000..12961d4d8 --- /dev/null +++ b/test/plugins/test_titlecase.py @@ -0,0 +1,55 @@ +# This file is part of beets. +# Copyright 2025, Henry Oberholtzer +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +""" Tests for the 'titlecase' plugin""" + +import pytest + +from beets import config +from beets.test.helper import BeetsTestCase +from beetsplug.titlecase import TitlecasePlugin + +@pytest.mark.parametrize("given, expected", + [("PENDULUM", "Pendulum"), + ("Aaron-carl", "Aaron-Carl"), + ("LTJ bukem", "LTJ Bukem"), + ("Freaky chakra Vs. Single Cell Orchestra", + "Freaky Chakra vs. Single Cell Orchestra") + ]) +def test_basic_titlecase(given, expected): + """ Assert that general behavior is as expected. """ + assert TitlecasePlugin().titlecase(given) == expected + + +class TitlecasePluginTest(BeetsTestCase): + + def test_preserved_case(self): + """ Test using given strings to preserve case """ + names_to_preserve = ["easyFun", "A.D.O.R.", + "D.R.", "ABBA", "LaTeX"] + config["titlecase"]["preserve"] = names_to_preserve + for name in names_to_preserve: + assert TitlecasePlugin().titlecase( + name.lower()) == name + + def test_small_first_last(self): + config["titlecase"]["small_first_last"] = False + assert TitlecasePlugin().titlecase( + "A Simple Trial") == "a Simple Trial" + config["titlecase"]["small_first_last"] = True + assert TitlecasePlugin().titlecase( + "A simple Trial") == "A Simple Trial" + + +