42 Commits
v0.4.1 ... main

Author SHA1 Message Date
Jamie Hardt
5e4db46328 Update python-publish.yml
Nudged pypa/gh-action-pypi-publish@v1.13.0
2025-09-08 12:53:49 -07:00
Jamie Hardt
465fe64d93 Merge pull request #4 from iluvcapra/feat-rename
Feature: Rename
2025-05-26 12:12:40 -07:00
Jamie Hardt
4241d3a4e8 autopep 2025-05-26 12:11:24 -07:00
Jamie Hardt
33fd0597cd Rename tested and addressed bugs 2025-05-26 12:10:22 -07:00
Jamie Hardt
cb10eec36a implemented rename 2025-05-25 12:18:15 -07:00
Jamie Hardt
180adcbbe4 Nudge version number 2025-05-25 11:29:34 -07:00
Jamie Hardt
b315680bdd Merge pull request #2 from iluvcapra/iluvcapra-patch-1
Feature: Get file list from file
2025-05-25 11:28:32 -07:00
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
ef1377a616 Update pyproject.toml
Nudge version to 0.5.2
2025-05-24 21:46:43 -07:00
Jamie Hardt
b7535618e6 Merge pull request #3 from iluvcapra/iluvcapra-patch-2
Update python-publish.yml
2025-05-24 21:46:06 -07:00
Jamie Hardt
dfa2a2a5ad Update python-publish.yml
Updated publish action
2025-05-24 21:41:21 -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
6 changed files with 168 additions and 47 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 }}

View File

@@ -35,4 +35,4 @@ jobs:
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@v1.8.6
uses: pypa/gh-action-pypi-publish@v1.13.0

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
@@ -115,6 +115,9 @@ class CommandEnv:
del self.metadatums['_FILENAME']
del self.metadatums['_FOLDER']
if '_NEW_BASENAME' in self.metadatums:
del self.metadatums['_NEW_BASENAME']
def revert_onces(self):
"""
Revert all set-once keys.
@@ -131,16 +134,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 +156,7 @@ they appear in the batchfile.
dry_run: bool
env: CommandEnv
write_metadata_f: Callable
COMMAND_LEADER = ':'
COMMENT_LEADER = '#'
@@ -160,6 +165,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):
"""
@@ -189,13 +195,29 @@ they appear in the batchfile.
def _handle_comment(self, _):
pass
def _write_metadata_impl(self, line):
def _write_metadata_and_rename_impl(self, line):
if self.dry_run:
print("DRY RUN would write metadata here.")
print("DRY RUN would write metadata here.", file=self.outstream)
if '_NEW_BASENAME' in self.env.metadatums:
self.outstream.write('DRY RUN would rename file here.\n')
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!\n")
if '_NEW_BASENAME' in self.env.metadatums:
self.outstream.write("Attempting to rename... ")
full_old_path = os.path.abspath(line)
new_name = os.path.join(os.path.dirname(full_old_path),
self.env.metadatums['_NEW_BASENAME'])
if not os.path.exists(new_name):
os.rename(line, new_name)
self.outstream.write('File renamed!\n')
else:
self.outstream.write('File by new name already exists, '
'rename was not performed.\n')
self.env.increment_all()
self.env.revert_onces()
@@ -208,10 +230,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 +242,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():
@@ -231,10 +253,16 @@ they appear in the batchfile.
self._print_kv_columnar(key, value)
if '_NEW_BASENAME' in self.env.metadatums:
print("")
msg = "File will be renamed:"
print(
f"{msg:.<30} \033[4m{self.env.metadatums['_NEW_BASENAME']}\033[0m\n")
if interactive:
val = input('Write? [Y/n/a/:] > ')
if val == '' or val[0].upper() == 'Y':
self._write_metadata_impl(line)
self._write_metadata_and_rename_impl(line)
break
if val.startswith(self.COMMAND_LEADER):
self._handle_command(val.lstrip(self.COMMAND_LEADER),
@@ -243,7 +271,7 @@ they appear in the batchfile.
print("Aborting write session...", file=sys.stdout)
break
else:
self._write_metadata_impl(line)
self._write_metadata_and_rename_impl(line)
break
def set(self, args):
@@ -319,6 +347,17 @@ they appear in the batchfile.
repl = args[3]
self.env.set_pattern(key, inp, pattern, repl)
def rename(self, args):
"""
rename NEW-BASENAME
Renames the next file to NEW-BASENAME. The existing file is renamed
while keeping it in the same directory by appending the dirname and
NEW-BASENAME and performing an mv(1). Renaming occurs after metadata
writing. If a file with NEW-BASENAME already exists in the directory,
the action will not be performed.
"""
self.env.set_once('_NEW_BASENAME', args[0])
def d(self, args):
"""
d VALUE
@@ -326,3 +365,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.6.0"
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',