mirror of
https://github.com/iluvcapra/mfbatch.git
synced 2025-12-31 08:50:51 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465fe64d93 | ||
|
|
4241d3a4e8 | ||
|
|
33fd0597cd | ||
|
|
cb10eec36a | ||
|
|
180adcbbe4 | ||
|
|
b315680bdd | ||
|
|
49055ef881 | ||
|
|
ecc93640c2 | ||
|
|
538da34a0c | ||
|
|
2d4ea5a8d8 | ||
|
|
532f67e3a8 | ||
|
|
4989832247 | ||
|
|
549f49da31 | ||
|
|
ba3b0dbf96 | ||
|
|
ef1377a616 | ||
|
|
b7535618e6 | ||
|
|
dfa2a2a5ad | ||
|
|
2c135d413e | ||
|
|
334fa56a2c | ||
|
|
66ac136270 | ||
|
|
aa64d5e183 | ||
|
|
6766e81b23 | ||
|
|
a2ce03a259 | ||
|
|
0ba40893df | ||
|
|
e2b93f5183 | ||
|
|
7015e80cf9 | ||
|
|
042f3116dd | ||
|
|
20518fa31c | ||
|
|
c4a2e380de |
2
.github/workflows/pylint.yml
vendored
2
.github/workflows/pylint.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
|||||||
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
@@ -35,4 +35,4 @@ jobs:
|
|||||||
- name: Build package
|
- name: Build package
|
||||||
run: python -m build
|
run: python -m build
|
||||||
- name: Publish package
|
- name: Publish package
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ from subprocess import CalledProcessError, run
|
|||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
import shlex
|
import shlex
|
||||||
from typing import Callable
|
from typing import Callable, List, Tuple
|
||||||
import inspect
|
import inspect
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
@@ -29,56 +30,83 @@ def execute_batch_list(batch_list_path: str, dry_run: bool, interactive: bool):
|
|||||||
parser.eval(line, line_no, interactive)
|
parser.eval(line, line_no, interactive)
|
||||||
|
|
||||||
|
|
||||||
def create_batch_list(command_file: str, recursive=True, sort_mode='path'):
|
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
|
Read all FLAC files in the cwd and create a batchfile that re-creates all
|
||||||
of their metadata.
|
of their metadata.
|
||||||
|
|
||||||
:param recursive: Recursively enter directories
|
: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',
|
:param sort_mode: Order of paths in the batch list. Either 'path',
|
||||||
'mtime', 'ctime', 'name'
|
'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:
|
with open(command_file, mode='w', encoding='utf-8') as f:
|
||||||
f.write("# mfbatch\n\n")
|
|
||||||
metadatums = {}
|
metadatums = {}
|
||||||
flac_files = glob('./**/*.flac', recursive=recursive)
|
|
||||||
|
|
||||||
if sort_mode == 'path':
|
f.write("# mfbatch\n\n")
|
||||||
flac_files = sorted(flac_files)
|
|
||||||
elif sort_mode == 'mtime':
|
|
||||||
flac_files = sorted(flac_files, key=os.path.getmtime)
|
|
||||||
elif sort_mode == 'ctime':
|
|
||||||
flac_files = sorted(flac_files, key=os.path.getctime)
|
|
||||||
elif sort_mode == 'name':
|
|
||||||
flac_files = sorted(flac_files, key=os.path.basename)
|
|
||||||
|
|
||||||
for path in tqdm(flac_files, unit='File', desc='Scanning FLAC files'):
|
for path in tqdm(flac_files, unit='File',
|
||||||
try:
|
desc='Scanning with metaflac...'):
|
||||||
this_file_metadata = metadata_funcs.read_metadata(path)
|
|
||||||
except CalledProcessError as e:
|
|
||||||
f.write(f"# !!! METAFLAC ERROR ({e.returncode}) while reading "
|
|
||||||
f"metadata from the file {path}\n\n")
|
|
||||||
continue
|
|
||||||
|
|
||||||
for this_key, this_value in this_file_metadata.items():
|
metadatums, buffer = write_batchfile_entries_for_file(path,
|
||||||
if this_key not in metadatums:
|
metadatums)
|
||||||
f.write(f":set {this_key} "
|
f.write(buffer)
|
||||||
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())
|
f.write("# mfbatch: create batchlist operation complete\n")
|
||||||
for key in keys:
|
|
||||||
if key not in this_file_metadata:
|
|
||||||
f.write(f":unset {key}\n")
|
|
||||||
del metadatums[key]
|
|
||||||
|
|
||||||
f.write(path + "\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -91,6 +119,10 @@ def main():
|
|||||||
op.add_argument('-c', '--create', default=False,
|
op.add_argument('-c', '--create', default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='create a new list')
|
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',
|
op.add_argument('-e', '--edit', action='store_true',
|
||||||
help="open batch file in the default editor",
|
help="open batch file in the default editor",
|
||||||
default=False)
|
default=False)
|
||||||
@@ -123,7 +155,7 @@ def main():
|
|||||||
if options.help_commands:
|
if options.help_commands:
|
||||||
print("Command Help\n------------")
|
print("Command Help\n------------")
|
||||||
commands = [command for command in dir(BatchfileParser) if
|
commands = [command for command in dir(BatchfileParser) if
|
||||||
not command.startswith('_') or command != "eval"]
|
not command.startswith('_') and command != "eval"]
|
||||||
print(f"{inspect.cleandoc(BatchfileParser.__doc__ or '')}\n\n")
|
print(f"{inspect.cleandoc(BatchfileParser.__doc__ or '')}\n\n")
|
||||||
for command in commands:
|
for command in commands:
|
||||||
meth = getattr(BatchfileParser, command)
|
meth = getattr(BatchfileParser, command)
|
||||||
@@ -138,7 +170,18 @@ def main():
|
|||||||
|
|
||||||
if options.create:
|
if options.create:
|
||||||
mode_given = True
|
mode_given = True
|
||||||
create_batch_list(options.batchfile, sort_mode=options.sort)
|
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:
|
if options.edit:
|
||||||
mode_given = True
|
mode_given = True
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ class CommandEnv:
|
|||||||
del self.metadatums['_FILENAME']
|
del self.metadatums['_FILENAME']
|
||||||
del self.metadatums['_FOLDER']
|
del self.metadatums['_FOLDER']
|
||||||
|
|
||||||
|
if '_NEW_BASENAME' in self.metadatums:
|
||||||
|
del self.metadatums['_NEW_BASENAME']
|
||||||
|
|
||||||
def revert_onces(self):
|
def revert_onces(self):
|
||||||
"""
|
"""
|
||||||
Revert all set-once keys.
|
Revert all set-once keys.
|
||||||
@@ -131,9 +134,9 @@ class CommandEnv:
|
|||||||
"""
|
"""
|
||||||
Increment all increment keys.
|
Increment all increment keys.
|
||||||
"""
|
"""
|
||||||
for k, v in self.incr.items():
|
for k, _ in self.incr.items():
|
||||||
v = int(v)
|
val = int(self.metadatums[k])
|
||||||
self.metadatums[k] = self.incr[k] % (v + 1)
|
self.metadatums[k] = self.incr[k] % (val + 1)
|
||||||
|
|
||||||
|
|
||||||
class BatchfileParser:
|
class BatchfileParser:
|
||||||
@@ -192,13 +195,29 @@ they appear in the batchfile.
|
|||||||
def _handle_comment(self, _):
|
def _handle_comment(self, _):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _write_metadata_impl(self, line):
|
def _write_metadata_and_rename_impl(self, line):
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
print("DRY RUN would write metadata here.", file=self.outstream)
|
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:
|
else:
|
||||||
self.outstream.write("Writing metadata... ")
|
self.outstream.write("Writing metadata... ")
|
||||||
self.write_metadata_f(line, self.env.metadatums)
|
self.write_metadata_f(line, self.env.metadatums)
|
||||||
self.outstream.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.increment_all()
|
||||||
self.env.revert_onces()
|
self.env.revert_onces()
|
||||||
@@ -234,10 +253,16 @@ they appear in the batchfile.
|
|||||||
|
|
||||||
self._print_kv_columnar(key, value)
|
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:
|
if interactive:
|
||||||
val = input('Write? [Y/n/a/:] > ')
|
val = input('Write? [Y/n/a/:] > ')
|
||||||
if val == '' or val[0].upper() == 'Y':
|
if val == '' or val[0].upper() == 'Y':
|
||||||
self._write_metadata_impl(line)
|
self._write_metadata_and_rename_impl(line)
|
||||||
break
|
break
|
||||||
if val.startswith(self.COMMAND_LEADER):
|
if val.startswith(self.COMMAND_LEADER):
|
||||||
self._handle_command(val.lstrip(self.COMMAND_LEADER),
|
self._handle_command(val.lstrip(self.COMMAND_LEADER),
|
||||||
@@ -246,7 +271,7 @@ they appear in the batchfile.
|
|||||||
print("Aborting write session...", file=sys.stdout)
|
print("Aborting write session...", file=sys.stdout)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self._write_metadata_impl(line)
|
self._write_metadata_and_rename_impl(line)
|
||||||
break
|
break
|
||||||
|
|
||||||
def set(self, args):
|
def set(self, args):
|
||||||
@@ -322,6 +347,17 @@ they appear in the batchfile.
|
|||||||
repl = args[3]
|
repl = args[3]
|
||||||
self.env.set_pattern(key, inp, pattern, repl)
|
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):
|
def d(self, args):
|
||||||
"""
|
"""
|
||||||
d VALUE
|
d VALUE
|
||||||
@@ -329,3 +365,18 @@ they appear in the batchfile.
|
|||||||
"""
|
"""
|
||||||
val = args[0]
|
val = args[0]
|
||||||
self.env.set_once('DESCRIPTION', val)
|
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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "mfbatch"
|
name = "mfbatch"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
description = "MetaFlac batch editor"
|
description = "MetaFlac batch editor"
|
||||||
authors = ["Jamie Hardt <jamiehardt@me.com>"]
|
authors = ["Jamie Hardt <jamiehardt@me.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -13,6 +13,7 @@ classifiers = [
|
|||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Topic :: Multimedia :: Sound/Audio :: Editors',
|
'Topic :: Multimedia :: Sound/Audio :: Editors',
|
||||||
|
'Programming Language :: Python :: 3.13',
|
||||||
'Programming Language :: Python :: 3.12',
|
'Programming Language :: Python :: 3.12',
|
||||||
'Programming Language :: Python :: 3.11',
|
'Programming Language :: Python :: 3.11',
|
||||||
'Programming Language :: Python :: 3.10',
|
'Programming Language :: Python :: 3.10',
|
||||||
|
|||||||
Reference in New Issue
Block a user