32 Commits

Author SHA1 Message Date
Jamie Hardt
49055ef881 lint 2025-05-25 11:25:54 -07:00
Jamie Hardt
ecc93640c2 Completed the feature. 2025-05-25 11:23:55 -07:00
Jamie Hardt
538da34a0c lint 2025-05-25 10:55:09 -07:00
Jamie Hardt
2d4ea5a8d8 /John/George 2025-05-25 10:52:59 -07:00
Jamie Hardt
532f67e3a8 Refactored some code 2025-05-25 09:32:59 -07:00
Jamie Hardt
4989832247 autopep (added explicit encoding for from-file) 2025-05-25 08:29:57 -07:00
Jamie Hardt
549f49da31 autopep 2025-05-25 08:28:32 -07:00
Jamie Hardt
ba3b0dbf96 Added implementation 2025-05-25 08:27:11 -07:00
Jamie Hardt
2c135d413e twiddle text 2025-05-24 21:30:14 -07:00
Jamie Hardt
334fa56a2c Update __main__.py 2025-05-24 20:59:53 -07:00
Jamie Hardt
66ac136270 autopep 2025-05-20 15:23:14 -07:00
Jamie Hardt
aa64d5e183 Fixed a bug in the increment code
Not sure how that happened I thought this used to work!
2025-05-20 15:19:46 -07:00
Jamie Hardt
6766e81b23 Commenting this out for now 2024-10-17 21:32:34 -07:00
Jamie Hardt
a2ce03a259 Added default lines to the top of a new list 2024-10-17 12:19:08 -07:00
Jamie Hardt
0ba40893df Adding some blank picture methods...
...to be implemented later if ever a need.
2024-10-17 09:50:27 -07:00
Jamie Hardt
e2b93f5183 Fixed obvious bug in --help-commands 2024-10-16 14:39:41 -07:00
Jamie Hardt
7015e80cf9 Merge pull request #1 from iluvcapra/iluvcapra-patch-py313
Add support for Python 3.13
2024-10-16 14:33:43 -07:00
Jamie Hardt
042f3116dd Update pyproject.toml 2024-10-16 14:33:02 -07:00
Jamie Hardt
20518fa31c Update pyproject.toml 2024-10-16 14:32:31 -07:00
Jamie Hardt
c4a2e380de Update pylint.yml
Adding 3.13 to test matrix
2024-10-16 14:30:09 -07:00
Jamie Hardt
4ec028b51b Pylints 2024-10-16 14:19:15 -07:00
Jamie Hardt
ac00f93a8d Update pyproject.toml
Nudged version to 0.5.0
2024-10-16 14:10:49 -07:00
Jamie Hardt
8bcfd2ee54 Added .venv to gitignore 2024-10-16 14:09:57 -07:00
Jamie Hardt
63ec226be1 Implemented sorting on creation 2024-10-16 14:09:18 -07:00
Jamie Hardt
69cdb6dec1 Amended error line 2024-10-16 13:37:59 -07:00
Jamie Hardt
720eacc2a4 Added possible sort modes for --create 2024-10-15 22:09:55 -07:00
Jamie Hardt
afa02e4c96 Twiddle 2024-10-15 20:59:13 -07:00
Jamie Hardt
b986a36281 lint 2024-10-15 11:34:32 -07:00
Jamie Hardt
fb90b5db3c Online doc changes 2024-10-15 11:33:21 -07:00
Jamie Hardt
e69573b2a4 Made some online doc tweaks 2024-10-15 11:09:31 -07:00
Jamie Hardt
55cf591690 Added some commentary 2024-10-15 11:04:13 -07:00
Jamie Hardt
f0e05a9609 Paramaterized output stream
for reporting messages from BatchfileParser.
2024-07-07 22:44:52 -07:00
5 changed files with 128 additions and 43 deletions

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
dist/
*.pyc
poetry.lock
.venv/

View File

@@ -4,12 +4,13 @@ mfbatch main - Command entrypoint for mfbatch
import os
from glob import glob
from subprocess import run
from subprocess import CalledProcessError, run
import sys
from argparse import ArgumentParser
import shlex
from typing import Callable
from typing import Callable, List, Tuple
import inspect
from io import StringIO
from tqdm import tqdm
@@ -29,37 +30,83 @@ def execute_batch_list(batch_list_path: str, dry_run: bool, interactive: bool):
parser.eval(line, line_no, interactive)
def create_batch_list(command_file: str, recursive=True):
def sort_flac_files(file_list, mode):
"Sort flac files"
if mode == 'path':
return sorted(file_list)
if mode == 'mtime':
return sorted(file_list, key=os.path.getmtime)
if mode == 'ctime':
return sorted(file_list, key=os.path.getctime)
if mode == 'name':
return sorted(file_list, key=os.path.basename)
return file_list
def write_batchfile_entries_for_file(path, metadatums) -> Tuple[dict, str]:
"Create batchfile entries for `path`"
buffer = StringIO()
try:
this_file_metadata = metadata_funcs.read_metadata(path)
except CalledProcessError as e:
buffer.write(f"# !!! METAFLAC ERROR ({e.returncode}) while reading "
f"metadata from the file {path}\n\n")
return metadatums, buffer.getvalue()
for this_key, this_value in this_file_metadata.items():
if this_key not in metadatums:
buffer.write(f":set {this_key} "
f"{shlex.quote(this_value)}\n")
metadatums[this_key] = this_value
else:
if this_value != metadatums[this_key]:
buffer.write(f":set {this_key} "
f"{shlex.quote(this_value)}"
"\n")
metadatums[this_key] = this_value
keys = list(metadatums.keys())
for key in keys:
if key not in this_file_metadata:
buffer.write(f":unset {key}\n")
del metadatums[key]
buffer.write(path + "\n\n")
return metadatums, buffer.getvalue()
def create_batch_list(flac_files: List[str], command_file: str,
sort_mode='path'):
"""
Read all FLAC files in the cwd and create a batchfile that re-creates all
of their metadata.
:param flac_files: Paths of files to create batchfile from
:param command_file: Name of new batchfile
:param sort_mode: Order of paths in the batch list. Either 'path',
'mtime', 'ctime', 'name'
:param input_files: FLAC files to scan
"""
flac_files = sort_flac_files(flac_files, sort_mode)
with open(command_file, mode='w', encoding='utf-8') as f:
f.write("# mfbatch\n\n")
metadatums = {}
flac_files = glob('./**/*.flac', recursive=recursive)
flac_files = sorted(flac_files)
for path in tqdm(flac_files, unit='File', desc='Scanning FLAC files'):
this_file_metadata = metadata_funcs.read_metadata(path)
for this_key, this_value in this_file_metadata.items():
if this_key not in metadatums:
f.write(f":set {this_key} "
f"{shlex.quote(this_value)}\n")
metadatums[this_key] = this_value
else:
if this_value != metadatums[this_key]:
f.write(f":set {this_key} "
f"{shlex.quote(this_value)}"
"\n")
metadatums[this_key] = this_value
keys = list(metadatums.keys())
for key in keys:
if key not in this_file_metadata:
f.write(f":unset {key}\n")
del metadatums[key]
f.write("# mfbatch\n\n")
f.write(path + "\n\n")
for path in tqdm(flac_files, unit='File',
desc='Scanning with metaflac...'):
metadatums, buffer = write_batchfile_entries_for_file(path,
metadatums)
f.write(buffer)
f.write("# mfbatch: create batchlist operation complete\n")
def main():
@@ -72,16 +119,23 @@ def main():
op.add_argument('-c', '--create', default=False,
action='store_true',
help='create a new list')
op.add_argument('-F', '--from-file', metavar='FILE_LIST', action='store',
default=None, help="get file paths from FILE_LIST when "
"creating, instead of scanning directory"
"a new list")
op.add_argument('-e', '--edit', action='store_true',
help="open batch file in the default editor",
default=False)
op.add_argument('-W', '--write', default=False,
action='store_true',
help="execute batch list, write to files")
op.add_argument('-p', '--path', metavar='DIR',
help='chdir to DIR before running',
default=None)
op.add_argument('-s', '--sort', metavar='MODE', action='store',
default='path', help="when creating, Set mode to sort "
"files by. Default is 'path'. 'ctime, 'mtime' and 'name' "
"are also options.")
op.add_argument('-n', '--dry-run', action='store_true',
help="dry-run -W.")
op.add_argument('-f', '--batchfile', metavar='FILE',
@@ -101,12 +155,12 @@ def main():
if options.help_commands:
print("Command Help\n------------")
commands = [command for command in dir(BatchfileParser) if
not command.startswith('_')]
not command.startswith('_') and command != "eval"]
print(f"{inspect.cleandoc(BatchfileParser.__doc__ or '')}\n\n")
for command in commands:
meth = getattr(BatchfileParser, command)
if isinstance(meth, Callable):
print(f"{inspect.cleandoc(meth.__doc__ or '')}\n")
print(f"- {inspect.cleandoc(meth.__doc__ or '')}\n")
sys.exit(0)
@@ -116,7 +170,18 @@ def main():
if options.create:
mode_given = True
create_batch_list(options.batchfile)
flac_files: List[str] = []
if options.from_file:
with open(options.from_file, mode='r',
encoding='utf-8') as from_file:
flac_files = [line.strip() for line in from_file.readlines()]
else:
flac_files = glob('./**/*.flac', recursive=True)
# print(flac_files)
create_batch_list(flac_files, options.batchfile,
sort_mode=options.sort)
if options.edit:
mode_given = True

View File

@@ -9,7 +9,7 @@ import shutil
import re
import os.path
from typing import Dict, Tuple, Optional
from typing import Callable, Dict, Tuple, Optional
from mfbatch.metaflac import write_metadata as flac
@@ -131,16 +131,17 @@ class CommandEnv:
"""
Increment all increment keys.
"""
for k, v in self.incr.items():
v = int(v)
self.metadatums[k] = self.incr[k] % (v + 1)
for k, _ in self.incr.items():
val = int(self.metadatums[k])
self.metadatums[k] = self.incr[k] % (val + 1)
class BatchfileParser:
"""
A batchfile is a text file of lines. Lines either begin with a '#' to denote a
comment, a ':' to denote a Command, and if neither of these are present, the
line is interpreted as a file path to act upon. Empty lines are ignored.
line is interpreted as a file path to act upon. Empty lines are ignored. Lines
are split into arguments using `shlex.split`.
If a line ends with a backslash '\\', the backslash is deleted and the contents
of the following line are appended to the end of the present line.
@@ -152,6 +153,7 @@ they appear in the batchfile.
dry_run: bool
env: CommandEnv
write_metadata_f: Callable
COMMAND_LEADER = ':'
COMMENT_LEADER = '#'
@@ -160,6 +162,7 @@ they appear in the batchfile.
self.dry_run = True
self.env = CommandEnv()
self.write_metadata_f = flac
self.outstream = sys.stdout
def eval(self, line: str, lineno: int, interactive: bool):
"""
@@ -191,11 +194,11 @@ they appear in the batchfile.
def _write_metadata_impl(self, line):
if self.dry_run:
print("DRY RUN would write metadata here.")
print("DRY RUN would write metadata here.", file=self.outstream)
else:
sys.stdout.write("Writing metadata... ")
self.outstream.write("Writing metadata... ")
self.write_metadata_f(line, self.env.metadatums)
sys.stdout.write("Complete!")
self.outstream.write("Complete!")
self.env.increment_all()
self.env.revert_onces()
@@ -208,10 +211,10 @@ they appear in the batchfile.
for l in value_lines:
if key:
sys.stdout.write(f"{key:.<30} \033[4m{l}\033[0m\n")
self.outstream.write(f"{key:.<30} \033[4m{l}\033[0m\n")
key = None
else:
sys.stdout.write(f"{' ' * 30} \033[4m{l}\033[0m\n")
self.outstream.write(f"{' ' * 30} \033[4m{l}\033[0m\n")
def _handle_file(self, line, interactive):
while True:
@@ -220,9 +223,9 @@ they appear in the batchfile.
self.env.evaluate_patterns()
if self.dry_run:
sys.stdout.write(f"\nDRY RUN File: \033[1m{line}\033[0m\n")
self.outstream.write(f"\nDRY RUN File: \033[1m{line}\033[0m\n")
else:
sys.stdout.write(f"\nFile: \033[1m{line}\033[0m\n")
self.outstream.write(f"\nFile: \033[1m{line}\033[0m\n")
for key, value in self.env.metadatums.items():
@@ -326,3 +329,18 @@ they appear in the batchfile.
"""
val = args[0]
self.env.set_once('DESCRIPTION', val)
# def picture(self, args):
# """
# picture PATH
# Add PATH as a picture (flac picture type 0) to this and every
# subsequent file.
# """
# pass
#
# def nopicture(self, args):
# """
# unpicture
# Remove all p
# """
# pass

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "mfbatch"
version = "0.4.1"
version = "0.5.1"
description = "MetaFlac batch editor"
authors = ["Jamie Hardt <jamiehardt@me.com>"]
readme = "README.md"
@@ -13,6 +13,7 @@ classifiers = [
'Environment :: Console',
'License :: OSI Approved :: MIT License',
'Topic :: Multimedia :: Sound/Audio :: Editors',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.10',