diff --git a/formtool/__init__.py b/formtool/__init__.py index a91c542..1b0341b 100644 --- a/formtool/__init__.py +++ b/formtool/__init__.py @@ -1,9 +1,10 @@ import glob import traceback from pathlib import Path -from subprocess import check_call +from subprocess import check_call, DEVNULL from hypy_utils import printc +from rich.progress import track defaults = { 'av1': { @@ -34,6 +35,9 @@ defaults = { '-c:a': 'flac', '-compression_level': '7', }, + 'wav': { + '-c:a': 'pcm_s16le', + } } suffixes = { 'av1': '.av1-{-crf}.mp4', @@ -41,10 +45,13 @@ suffixes = { 'mp3': '.v{-q:a}.mp3', 'opus': '.v{-b:a}.opus', 'flac': '.flac', + 'wav': '.wav', } -def main(fmt: str, files: list[str], keep: bool, passthrough: list[str]): +def main(fmt: str, files: list[str], keep: bool, passthrough: list[str], quiet: bool = False, silent: bool = False): + printq = printc if not silent else lambda *a, **k: None + quiet = quiet or silent # Process each file provided on the command line files = [ Path(p) @@ -52,16 +59,19 @@ def main(fmt: str, files: list[str], keep: bool, passthrough: list[str]): for p in glob.glob(str(Path(pattern).expanduser())) if Path(p).is_file() ] + total_orig_size, total_new_size = 0, 0 printc(f"&e> Using format: {fmt}") printc(f"&e> Found {len(files)} files to process.") printc(f"&e> Keep original files: {'Yes' if keep else 'No'}") printc(f"&e> Passthrough parameters: {passthrough if passthrough else 'None'}") print() - for inf in files: - printc("&e-----------------------------------------") + for inf in track(files): + printq("&e-----------------------------------------") try: params: dict[str, str | None] = defaults[fmt].copy() old_size = inf.stat().st_size + if quiet: + params['-y'] = None # Overwrite output files without asking # Check for any passthrough arguments and add them to params (overrides defaults) i = 0 @@ -70,11 +80,11 @@ def main(fmt: str, files: list[str], keep: bool, passthrough: list[str]): # Check if next item exists and is not a flag (i.e., it's a value) if i + 1 < len(passthrough) and not passthrough[i+1].startswith('-'): v = passthrough[i+1] - printc(f"&a> Overriding parameter: {k} {v} (was {params.get(k, 'not set')})") + printq(f"&a> Overriding parameter: {k} {v} (was {params.get(k, 'not set')})") params[k] = v i += 2 else: # It's a standalone flag - printc(f"&a> Overriding parameter: {k} (was {params.get(k, 'not set')})") + printq(f"&a> Overriding parameter: {k} (was {params.get(k, 'not set')})") params[k] = None # Use None to signify a flag without a value i += 1 @@ -84,35 +94,48 @@ def main(fmt: str, files: list[str], keep: bool, passthrough: list[str]): end = ''.join(c for c in end if c.isalnum() or c in ' ._-+').rstrip() if inf.name.endswith(end): - printc(f"&c> Error: File already has target suffix '{end}', skipping: {inf.name}") + printq(f"&c> Error: File already has target suffix '{end}', skipping: {inf.name}") continue ouf = inf.with_name(f'{inf.stem}{end}') - printc(f"&e+ Compressing '{inf.name}' > '{ouf.name}'") + printq(f"&e+ Compressing '{inf.name}' > '{ouf.name}'") # Construct and run the ffmpeg command cmd = ['ffmpeg', '-hide_banner', '-i', str(inf), *sum(([k] if v is None else [k, str(v)] for k, v in params.items()), []), str(ouf)] - printc(f"&e> Running command: {' '.join(cmd)}") + printq(f"&e> Running command: {' '.join(cmd)}") - check_call(cmd) - printc(f"&a> Compression successful :)") + check_call(cmd) if not quiet else check_call(cmd, stdout=DEVNULL, stderr=DEVNULL) + printq(f"&a> Compression successful :)") new_size = ouf.stat().st_size ratio = new_size / old_size - printc(f"&a> Size: {old_size / 1_000_000:.2f} MB -> {new_size / 1_000_000:.2f} MB ({ratio:.2%})") + printq(f"&a> Size: {old_size / 1_000_000:.2f} MB -> {new_size / 1_000_000:.2f} MB ({ratio:.2%})") + + total_orig_size += old_size + total_new_size += new_size if not keep: if new_size >= old_size: printc(f"&c! Warning: Compressed file is not smaller than original. Keeping original file :(") else: - printc(f"&e- Removing original file: '{inf.name}'") + printq(f"&e- Removing original file: '{inf.name}'") inf.unlink() - printc(f"&a> Original file removed.") + printq(f"&a> Original file removed.") - print() + printq('') except Exception as e: printc(f"&c! An error occurred while processing {inf.name}: {e}") printc("&c! Leaving original file intact.\n") traceback.print_exc() + + # Print summary + if total_orig_size > 0: + total_ratio = total_new_size / total_orig_size + printc("&a=========================================") + printc(f"&a> Processed {len(files)} files.") + printc(f"&a> Total size: {total_orig_size / 1_000_000:.2f} MB -> {total_new_size / 1_000_000:.2f} MB ({total_ratio:.2%})") + printc("&a=========================================") + else: + printc("&c! Nothing to do") diff --git a/formtool/__main__.py b/formtool/__main__.py index 6b36004..8c030cc 100644 --- a/formtool/__main__.py +++ b/formtool/__main__.py @@ -9,9 +9,11 @@ def cli(fmt: str | None = None): agupa.add_argument('format', choices=defaults.keys(), help="Compression format to use.") agupa.add_argument('files', nargs='+', help="One or more files to compress.") agupa.add_argument('--keep', action='store_true', help="Keep original files after compression.") + agupa.add_argument('--quiet', action='store_true', help="Suppress ffmpeg output.") + agupa.add_argument('--silent', action='store_true', help="Suppress all output except errors.") args, passthrough = agupa.parse_known_args() - main(fmt or args.format, args.files, args.keep, passthrough) + main(fmt or args.format, args.files, args.keep, passthrough, args.quiet, args.silent) def av1(): @@ -34,5 +36,9 @@ def flac(): cli('flac') +def wav(): + cli('wav') + + if __name__ == '__main__': cli() diff --git a/pyproject.toml b/pyproject.toml index cd9d6a1..ce235b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "hypy-utils>=1.0.29", + "rich>=14.1.0", ] [project.scripts] @@ -15,3 +16,4 @@ fx264 = "formtool.__main__:x264" fmp3 = "formtool.__main__:mp3" fopus = "formtool.__main__:opus" fflac = "formtool.__main__:flac" +fwav = "formtool.__main__:wav" diff --git a/uv.lock b/uv.lock index aec7638..b1dfd32 100644 --- a/uv.lock +++ b/uv.lock @@ -8,10 +8,14 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "hypy-utils" }, + { name = "rich" }, ] [package.metadata] -requires-dist = [{ name = "hypy-utils", specifier = ">=1.0.29" }] +requires-dist = [ + { name = "hypy-utils", specifier = ">=1.0.29" }, + { name = "rich", specifier = ">=14.1.0" }, +] [[package]] name = "hypy-utils" @@ -21,3 +25,46 @@ sdist = { url = "https://files.pythonhosted.org/packages/e1/02/6c93a5f7be972d55b wheels = [ { url = "https://files.pythonhosted.org/packages/e8/34/7e4f6c09da586d317a74958e6b853da8a4c220898c5a8adf98030abb1492/hypy_utils-1.0.29-py3-none-any.whl", hash = "sha256:146a5aa85d3b4bc175e64d3f9e26befd0d87d221c4366eb70e137e46933eb65d", size = 18099, upload-time = "2024-12-10T11:51:12.019Z" }, ] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +]