commit 34415a8062fc1d7d580a26de0a1a1142cfc2589d Author: ashastral Date: Thu Jul 28 21:15:00 2022 -0700 Initial commit diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c7cadb4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": ".venv/bin/python" +} \ No newline at end of file diff --git a/change_sync_bias.py b/change_sync_bias.py new file mode 100644 index 0000000..46bb089 --- /dev/null +++ b/change_sync_bias.py @@ -0,0 +1,86 @@ +import argparse +from decimal import Decimal +import sys +from typing import Union + +import simfile +from simfile.dir import SimfilePack + + +class ChangeSyncBiasArgs: + """Stores the command-line arguments for this script.""" + + pack: str + itg_to_null: bool + null_to_itg: bool + + +def argparser(): + """Get an ArgumentParser instance for this command-line script.""" + parser = argparse.ArgumentParser(prefix_chars="-+") + parser.add_argument("pack", type=str, help="path to the pack to modify") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "-9", "--itg-to-null", action="store_true", help="subtract 9ms from offsets" + ) + group.add_argument( + "+9", "--null-to-itg", action="store_true", help="add 9ms to offsets" + ) + return parser + + +def adjust_offset( + obj: Union[simfile.types.Simfile, simfile.ssc.SSCChart], + delta: Decimal, +): + """Add the delta to the simfile or SSC chart's offset, if present.""" + if obj.offset is not None: + obj.offset = str(Decimal(obj.offset) + delta) + + +def change_sync_bias(simfile_path: str, args: ChangeSyncBiasArgs): + """ + Add or subtract 9 milliseconds to the simfile's offset, + as well as any SSC charts with their own timing data. + + This saves the updated simfile to its original location + and writes a backup copy with a ~ appended to the filename. + """ + # Map the +9 or -9 arg to the actual offset delta. + # + # We don't have to check both itg_to_null and null_to_itg + # because the mutually exclusive & required argument group + # ensures that exactly one of them will be True. + delta = Decimal("-0.009" if args.itg_to_null else "+0.009") + + # You could specify output_filename here to write the updated file elsewhere + with simfile.mutate( + input_filename=f"{simfile_path}", + backup_filename=f"{simfile_path}~", + ) as sf: + # Always adjust the simfile's offset + adjust_offset(sf, delta) + + # Additionally try to adjust SSC charts' offsets. + # This won't do anything unless the chart has its own timing data. + if isinstance(sf, simfile.ssc.SSCSimfile): + for chart in sf.charts: + adjust_offset(chart, delta) + + +def main(argv): + # Parse command-line arguments + args = argparser().parse_args(argv[1:], namespace=ChangeSyncBiasArgs()) + + # Iterate over SimfileDirectory objects from the pack + # so that we can easily get the .sm and/or .ssc paths + for simfile_dir in SimfilePack(args.pack).simfile_dirs(): + + # Try to update whichever formats exist + for simfile_path in [simfile_dir.sm_path, simfile_dir.ssc_path]: + if simfile_path: + change_sync_bias(simfile_path, args) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1ba5450 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,229 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "black" +version = "22.6.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "fs" +version = "2.4.16" +description = "Python's filesystem abstraction layer" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +appdirs = ">=1.4.3,<1.5.0" +six = ">=1.10,<2.0" + +[package.extras] +scandir = ["scandir (>=1.5,<2.0)"] + +[[package]] +name = "importlib-metadata" +version = "4.12.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] + +[[package]] +name = "msdparser" +version = "2.0.0" +description = "Robust & lightning fast MSD parser (StepMania file format)" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mypy" +version = "0.971" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "simfile" +version = "2.1.0rc1" +description = "Modern simfile library for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +fs = ">=2.4.15,<2.5.0" +msdparser = ">=2.0.0b4,<2.1.0" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "zipp" +version = "3.8.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "a68c45fcf34f1ce3e8acd3562ff47bed859d19846aed1c332c4576574c9e5301" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +black = [] +click = [] +colorama = [] +fs = [ + {file = "fs-2.4.16-py2.py3-none-any.whl", hash = "sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c"}, + {file = "fs-2.4.16.tar.gz", hash = "sha256:ae97c7d51213f4b70b6a958292530289090de3a7e15841e108fbe144f069d313"}, +] +importlib-metadata = [] +msdparser = [] +mypy = [] +mypy-extensions = [] +pathspec = [] +platformdirs = [] +simfile = [] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +tomli = [] +typed-ast = [] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] +zipp = [] diff --git a/prefix_title_with_meter.py b/prefix_title_with_meter.py new file mode 100644 index 0000000..5b09153 --- /dev/null +++ b/prefix_title_with_meter.py @@ -0,0 +1,115 @@ +import argparse +import sys +from typing import Optional, Sequence + +import simfile +from simfile.dir import SimfilePack + + +class PrefixTitleWithMeterArgs: + """Stores the command-line arguments for this script.""" + + pack: str + stepstype: str + digits: int + remove: bool + + +def argparser(): + """Get an ArgumentParser instance for this command-line script.""" + parser = argparse.ArgumentParser() + parser.add_argument("pack", type=str, help="path to the pack to modify") + parser.add_argument("-s", "--stepstype", type=str, default="dance-single") + parser.add_argument( + "-d", + "--digits", + type=int, + default=2, + help="minimum digits (will add leading zeroes)", + ) + parser.add_argument( + "-r", + "--remove", + action=argparse.BooleanOptionalAction, + help="remove meter prefix", + ) + return parser + + +def hardest_chart( + charts: Sequence[simfile.types.Chart], stepstype: str +) -> Optional[simfile.types.Chart]: + """ + Find & return the hardest chart (numerically) of a given stepstype. + + Returns None if there are no charts matching the stepstype. + """ + return max( + [c for c in charts if c.stepstype == stepstype], + key=lambda c: int(c.meter or "1"), + default=None, + ) + + +def prefix_title_with_meter(simfile_path: str, args: PrefixTitleWithMeterArgs): + """ + Add (or remove) a prefix to the simfile's title. + + This saves the updated simfile to its original location + and writes a backup copy with a ~ appended to the filename. + """ + # You could specify output_filename here to write the updated file elsewhere + with simfile.mutate( + input_filename=f"{simfile_path}", + backup_filename=f"{simfile_path}~", + ) as sf: + # It's very unlikely for the title property to be blank or missing. + # This is mostly to satisfy type-checkers. + current_title = sf.title or "" + + if args.remove: + # Look for a number in brackets at the start of the title + if current_title.startswith("["): + open_bracket_index = current_title.find("[") + close_bracket_index = current_title.find("]") + bracketed_text = current_title[ + open_bracket_index + 1 : close_bracket_index + ] + if bracketed_text.isnumeric(): + # Remove the bracketed number from the title + sf.title = current_title[close_bracket_index + 1 :].lstrip(" ") + else: + # Find the hardest chart (numerically) within a stepstype + # and use it to prefix the title + chart = hardest_chart(sf.charts, args.stepstype) + + # Skip this simfile if there were no charts for the stepstype. + # Nothing will be written to disk in this case. + if not chart: + raise simfile.CancelMutation + + # It's very unlikely for the meter property to be blank or missing. + # This is mostly to satisfy type-checkers. + meter = chart.meter or "1" + + # Put the meter at the start of the title, + # filling in leading zeros per arguments + sf.title = f"[{meter.zfill(args.digits)}] {current_title}" + + +def main(argv): + # Parse command-line arguments + args = argparser().parse_args(argv[1:], namespace=PrefixTitleWithMeterArgs()) + + # Iterate over SimfileDirectory objects from the pack + # so that we can easily get the .sm and/or .ssc paths + for simfile_dir in SimfilePack(args.pack).simfile_dirs(): + + # Try to update whichever formats exist + for simfile_path in [simfile_dir.sm_path, simfile_dir.ssc_path]: + if simfile_path: + prefix_title_with_meter(simfile_path, args) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aa1cc67 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "simfile-scripts" +version = "0.1.0" +description = "" +authors = ["ash garcia "] + +[tool.poetry.dependencies] +python = "^3.7" +simfile = "^2.1.0rc1" +msdparser = "^2.0.0" + +[tool.poetry.dev-dependencies] +mypy = "^0.971" +black = "^22.6.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"