Last active
May 18, 2025 20:29
-
-
Save mtfurlan/8420e9834e979e9eca81692922747c61 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# StupidifyQuotes: turn “typographical quotes” into "typewriter quote" | |
# The Unicode Consortium prefers "smart quotes" and musicbrainz and beets are | |
# following this. | |
# I Disagree | |
# also endash(U+2013) to hyphen(U+2D) | |
# also double prime to " | |
# this will modify incoming metadata, and also provide a stupidify-quotes | |
# command to edit existing items | |
# https://www.unicode.org/charts/PDF/U0000.pdf | |
# for double quote " U+0022, unicode 16.0 says | |
# > preferred characters in English for paired quotation marks are | |
# > 201C “ & 201D ” | |
# for single quote ' U+0027, unicode 16.0 says | |
# > 2019 ’ is preferred for apostrophe | |
# > preferred characters in English for paired quotation marks are | |
# > 2018 ‘ & 2019 ’ | |
# https://beets.readthedocs.io/en/stable/dev/plugins.html | |
# WTFPL | |
import re | |
import collections.abc | |
replacementMap = { | |
# left/right single quotation ‘’ | |
"\u2018": "'", | |
"\u2019": "'", | |
# left/right double quotation “” | |
"\u201C": "\"", | |
"\u201D": "\"", | |
# en dash – | |
"\u2013": "-", | |
# double prime ″ | |
"\u2033": "\"", | |
} | |
regexStr = f"[{''.join(list(replacementMap.keys()))}]" | |
quoteRegex = re.compile(regexStr) | |
def doReplace(inputString: str) -> str: | |
return quoteRegex.sub(lambda match: replacementMap[match.group(0)], inputString) | |
# if run as main, do some unit testing | |
# do all the beets stuff below so we don't import that if testing | |
if __name__ == "__main__": | |
import sys | |
print(f"regexStr is {regexStr}") | |
examples = { | |
"9–5ers Anthem": "9-5ers Anthem", | |
"The Truth 7″": "The Truth 7\"", | |
"I’ll Believe in Anything": "I'll Believe in Anything", | |
} | |
failed = False | |
for input, expected in examples.items(): | |
actual = doReplace(input) | |
if(actual != expected): | |
print(f"{input} => {expected} returned {actual}") | |
failed=True | |
if failed: | |
sys.exit(1) | |
print("looks good") | |
sys.exit(0) | |
from beets import plugins, ui | |
from beets.autotag.hooks import AlbumInfo, TrackInfo, AttrDict | |
from beets.library import Album, Item, LibModel | |
def isList(item): | |
return isinstance(item, collections.abc.Sequence) and not isinstance(item, (str)) | |
def replaceInMetadataObject(item: AttrDict | list) -> None: | |
""" | |
recursively modify fields inside AttrDict which contains strings and lists of string and lists of AttrDict | |
""" | |
indicies = [] | |
if isinstance(item, AttrDict): | |
indicies = item.keys() | |
elif isList(item): # TODO: I think isinstance(item, list) works here safely try that | |
indicies = range(len(item)) | |
else: | |
print(f"unknown type {type(item)}") | |
print(item) | |
import pdb; pdb.set_trace() | |
raise Exception(f"StupidifyQuotes: unknown type {type(item)} on item") | |
for k in indicies: | |
if isinstance(item[k], str): | |
item[k] = doReplace(item[k]) | |
elif isList(item[k]): | |
replaceInMetadataObject(item[k]); | |
elif isinstance(item[k], AttrDict): | |
replaceInMetadataObject(item[k]); | |
elif isinstance(item[k], float | int | None): | |
pass | |
else: | |
print(f"unknown type {item[k]}") | |
print(item[k]) | |
import pdb; pdb.set_trace() | |
raise Exception(f"StupidifyQuotes: unknown type {type(item[k])} on subitem") | |
class StupidifyQuotes(plugins.BeetsPlugin): | |
def __init__(self): | |
super().__init__() | |
self.register_listener('albuminfo_received', lambda info: replaceInMetadataObject(info)) | |
def commands(self): | |
stupidify_command = ui.Subcommand("stupidify-quotes", help="fix quotes in db") | |
stupidify_command.func = self._stupidfy_command | |
return [stupidify_command] | |
def _stupidfy_command(self, lib, opts, args): | |
"""The CLI command function for the `beet stupidify-quotes` command.""" | |
# modify albums then modify tracks | |
if len(args) > 0: | |
query = args[0] | |
else: | |
# TODO this doesn't work | |
query = '|'.join(list(replacementMap.keys())) | |
items = list(lib.albums(query)) + list(lib.items(query)) | |
if not items: | |
ui.print_("Nothing to edit :)") | |
return | |
self._thingy(items) | |
print("TODO: actually edit these steal edit code for the confirm stuff") | |
def _thingy(self, items: list[LibModel]) -> None: | |
for item in items: | |
# TODO: steal edit code till we edit what we need | |
ui.print_(format(item)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment