batchmp 1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- batchmp/__init__.py +0 -0
- batchmp/cli/__init__.py +0 -0
- batchmp/cli/base/__init__.py +0 -0
- batchmp/cli/base/bmp_dispatch.py +60 -0
- batchmp/cli/base/bmp_options.py +349 -0
- batchmp/cli/base/vchk.py +47 -0
- batchmp/cli/bmfp/__init__.py +0 -0
- batchmp/cli/bmfp/bmfp_dispatch.py +120 -0
- batchmp/cli/bmfp/bmfp_options.py +442 -0
- batchmp/cli/renamer/__init__.py +0 -0
- batchmp/cli/renamer/renamer_dispatch.py +135 -0
- batchmp/cli/renamer/renamer_options.py +355 -0
- batchmp/cli/tagger/__init__.py +0 -0
- batchmp/cli/tagger/tagger_dispatch.py +143 -0
- batchmp/cli/tagger/tagger_options.py +338 -0
- batchmp/commons/__init__.py +0 -0
- batchmp/commons/chainedhandler.py +102 -0
- batchmp/commons/descriptors.py +173 -0
- batchmp/commons/progressbar.py +154 -0
- batchmp/commons/taskprocessor.py +149 -0
- batchmp/commons/utils.py +194 -0
- batchmp/ffmptools/__init__.py +0 -0
- batchmp/ffmptools/ffcommands/__init__.py +0 -0
- batchmp/ffmptools/ffcommands/cmdopt.py +115 -0
- batchmp/ffmptools/ffcommands/convert.py +130 -0
- batchmp/ffmptools/ffcommands/cuesplit.py +223 -0
- batchmp/ffmptools/ffcommands/denoise.py +173 -0
- batchmp/ffmptools/ffcommands/fragment.py +121 -0
- batchmp/ffmptools/ffcommands/normalize_peak.py +135 -0
- batchmp/ffmptools/ffcommands/segment.py +157 -0
- batchmp/ffmptools/ffcommands/silencesplit.py +159 -0
- batchmp/ffmptools/ffrunner.py +189 -0
- batchmp/ffmptools/ffutils.py +300 -0
- batchmp/ffmptools/processors/__init__.py +0 -0
- batchmp/ffmptools/processors/basefp.py +92 -0
- batchmp/ffmptools/processors/ffentry.py +81 -0
- batchmp/ffmptools/utils/__init__.py +0 -0
- batchmp/ffmptools/utils/cueparse.py +227 -0
- batchmp/ffmptools/utils/cuesheet.py +239 -0
- batchmp/fstools/__init__.py +0 -0
- batchmp/fstools/builders/__init__.py +0 -0
- batchmp/fstools/builders/fsb.py +221 -0
- batchmp/fstools/builders/fsentry.py +60 -0
- batchmp/fstools/builders/fsprms.py +372 -0
- batchmp/fstools/dirtools.py +549 -0
- batchmp/fstools/fsutils.py +272 -0
- batchmp/fstools/rename.py +390 -0
- batchmp/fstools/walker.py +79 -0
- batchmp/tags/__init__.py +0 -0
- batchmp/tags/handlers/__init__.py +0 -0
- batchmp/tags/handlers/basehandler.py +99 -0
- batchmp/tags/handlers/ffmphandler.py +75 -0
- batchmp/tags/handlers/ffmphandlers/__init__.py +0 -0
- batchmp/tags/handlers/ffmphandlers/base.py +243 -0
- batchmp/tags/handlers/mtghandler.py +56 -0
- batchmp/tags/handlers/pmhandler.py +36 -0
- batchmp/tags/handlers/tagsholder.py +264 -0
- batchmp/tags/output/__init__.py +0 -0
- batchmp/tags/output/formatters.py +218 -0
- batchmp/tags/processors/__init__.py +0 -0
- batchmp/tags/processors/basetp.py +266 -0
- batchmp-1.4.dist-info/METADATA +422 -0
- batchmp-1.4.dist-info/RECORD +68 -0
- batchmp-1.4.dist-info/WHEEL +5 -0
- batchmp-1.4.dist-info/entry_points.txt +5 -0
- batchmp-1.4.dist-info/licenses/LICENSE +11 -0
- batchmp-1.4.dist-info/top_level.txt +1 -0
- batchmp-1.4.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import sys, os, re, string
|
|
16
|
+
from batchmp.fstools.dirtools import DHandler
|
|
17
|
+
from batchmp.fstools.walker import DWalker
|
|
18
|
+
from batchmp.fstools.rename import DirCounters
|
|
19
|
+
from batchmp.tags.output.formatters import TagOutputFormatter, OutputFormatType
|
|
20
|
+
from batchmp.tags.handlers.mtghandler import MutagenTagHandler
|
|
21
|
+
from batchmp.tags.handlers.ffmphandler import FFmpegTagHandler
|
|
22
|
+
from batchmp.tags.handlers.tagsholder import TagHolder
|
|
23
|
+
from batchmp.commons.progressbar import progress_bar, CmdProgressBarRefreshRate
|
|
24
|
+
from functools import partial
|
|
25
|
+
|
|
26
|
+
class BaseTagProcessor:
|
|
27
|
+
''' Base Tag Processing
|
|
28
|
+
'''
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self._handler = MutagenTagHandler() + FFmpegTagHandler()
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def handler(self):
|
|
34
|
+
return self._handler
|
|
35
|
+
|
|
36
|
+
def print_dir(self, fs_entry_params, format = None, show_stats = False):
|
|
37
|
+
|
|
38
|
+
''' Prints tags in selected media files
|
|
39
|
+
'''
|
|
40
|
+
formatter = partial(TagOutputFormatter.tags_formatter,
|
|
41
|
+
format = format if format else OutputFormatType.COMPACT,
|
|
42
|
+
handler = self.handler,
|
|
43
|
+
show_stats = show_stats)
|
|
44
|
+
|
|
45
|
+
DHandler.print_dir(fs_entry_params,
|
|
46
|
+
formatter = formatter,
|
|
47
|
+
selected_files_description = 'media file')
|
|
48
|
+
|
|
49
|
+
def set_tags(self, fs_entry_params, total_files, total_dirs, tag_holder = None, tag_holder_builder = None):
|
|
50
|
+
|
|
51
|
+
''' Set tags from tag_holder attributes
|
|
52
|
+
'''
|
|
53
|
+
if not tag_holder and not tag_holder_builder:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
fcnt = 0
|
|
57
|
+
pass_filter = lambda fpath: self.handler.can_handle(fpath)
|
|
58
|
+
|
|
59
|
+
with progress_bar(refresh_rate = CmdProgressBarRefreshRate.FAST) as p_bar:
|
|
60
|
+
p_bar.info_msg = 'Setting tags in {} media files'.format(total_files)
|
|
61
|
+
for entry in DWalker.file_entries(fs_entry_params, pass_filter = pass_filter):
|
|
62
|
+
if tag_holder_builder:
|
|
63
|
+
tag_holder = tag_holder_builder(entry)
|
|
64
|
+
|
|
65
|
+
self.handler.copy_tags(tag_holder)
|
|
66
|
+
self.handler.save()
|
|
67
|
+
fcnt += 1
|
|
68
|
+
|
|
69
|
+
p_bar.progress += 100 / total_files
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# print summary
|
|
73
|
+
if not fs_entry_params.quiet:
|
|
74
|
+
print('Set tags in {0} media file{1}'.format(fcnt, '' if fcnt == 1 else 's'))
|
|
75
|
+
|
|
76
|
+
def set_tags_visual(self, fs_entry_params,
|
|
77
|
+
diff_tags_only = False,
|
|
78
|
+
tag_holder = None,
|
|
79
|
+
tag_holder_builder = None,
|
|
80
|
+
reset_tag_holder_builder = None):
|
|
81
|
+
|
|
82
|
+
''' Set tags from tag_holder attributes
|
|
83
|
+
Visualises changes before proceeding
|
|
84
|
+
'''
|
|
85
|
+
if not tag_holder and not tag_holder_builder:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if fs_entry_params.quiet:
|
|
89
|
+
proceed = True
|
|
90
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
91
|
+
else:
|
|
92
|
+
# visualise changes to tags and proceed if confirmed
|
|
93
|
+
preformatter = partial(TagOutputFormatter.tags_formatter,
|
|
94
|
+
handler = self.handler,
|
|
95
|
+
show_stats = False,
|
|
96
|
+
tag_holder = tag_holder,
|
|
97
|
+
tag_holder_builder = tag_holder_builder,
|
|
98
|
+
diff_tags_only = diff_tags_only)
|
|
99
|
+
|
|
100
|
+
formatter = partial(TagOutputFormatter.tags_formatter,
|
|
101
|
+
handler = self.handler,
|
|
102
|
+
show_stats = False,
|
|
103
|
+
tag_holder = tag_holder,
|
|
104
|
+
tag_holder_builder = tag_holder_builder,
|
|
105
|
+
show_tag_holder_values = True,
|
|
106
|
+
diff_tags_only = diff_tags_only)
|
|
107
|
+
|
|
108
|
+
proceed, total_files, total_dirs = DHandler.visualise_changes(fs_entry_params,
|
|
109
|
+
preformatter = preformatter, formatter = formatter,
|
|
110
|
+
reset_formatters = reset_tag_holder_builder,
|
|
111
|
+
selected_files_description = 'media file')
|
|
112
|
+
if proceed:
|
|
113
|
+
if reset_tag_holder_builder:
|
|
114
|
+
reset_tag_holder_builder()
|
|
115
|
+
|
|
116
|
+
self.set_tags(fs_entry_params, total_files, total_dirs,
|
|
117
|
+
tag_holder = tag_holder,
|
|
118
|
+
tag_holder_builder = tag_holder_builder)
|
|
119
|
+
|
|
120
|
+
def copy_tags(self, fs_entry_params, diff_tags_only = False, tag_holder_path = None):
|
|
121
|
+
|
|
122
|
+
''' Copies metadata (including artwork) from a tag_holder file
|
|
123
|
+
then applies to all selected media files
|
|
124
|
+
Visualises changes before proceeding
|
|
125
|
+
'''
|
|
126
|
+
if self.handler.can_handle(tag_holder_path):
|
|
127
|
+
tag_holder = TagHolder()
|
|
128
|
+
tag_holder.copy_tags(self.handler.tag_holder)
|
|
129
|
+
self.set_tags_visual(fs_entry_params,
|
|
130
|
+
diff_tags_only = diff_tags_only,
|
|
131
|
+
tag_holder = tag_holder)
|
|
132
|
+
else:
|
|
133
|
+
print('Can not handle tags holder: {}'.format(tag_holder_path))
|
|
134
|
+
|
|
135
|
+
def index(self, fs_entry_params,
|
|
136
|
+
diff_tags_only = False, start_from = 1):
|
|
137
|
+
|
|
138
|
+
''' Indexes the tracks / tracktotal tags, per media files' respective directories
|
|
139
|
+
Visualises changes before proceeding
|
|
140
|
+
'''
|
|
141
|
+
|
|
142
|
+
counters = {}
|
|
143
|
+
def reset_counts():
|
|
144
|
+
counters.clear()
|
|
145
|
+
|
|
146
|
+
def tag_holder_builder(entry):
|
|
147
|
+
nonlocal counters
|
|
148
|
+
|
|
149
|
+
parent_dir = os.path.dirname(entry.realpath)
|
|
150
|
+
if not counters.get(parent_dir):
|
|
151
|
+
counters[parent_dir] = DirCounters(start_from, start_from, 0, 0)
|
|
152
|
+
|
|
153
|
+
tag_holder = TagHolder()
|
|
154
|
+
tag_holder.tracktotal = len(fs_entry_params.fnames)
|
|
155
|
+
|
|
156
|
+
tag_holder.track = counters[parent_dir].files_cnt
|
|
157
|
+
counters[parent_dir].files_cnt += 1
|
|
158
|
+
|
|
159
|
+
return tag_holder
|
|
160
|
+
|
|
161
|
+
self.set_tags_visual(fs_entry_params,
|
|
162
|
+
diff_tags_only = diff_tags_only,
|
|
163
|
+
tag_holder_builder = tag_holder_builder,
|
|
164
|
+
reset_tag_holder_builder = reset_counts)
|
|
165
|
+
|
|
166
|
+
def remove_tags(self, fs_entry_params,
|
|
167
|
+
tag_fields = None,
|
|
168
|
+
diff_tags_only = False):
|
|
169
|
+
|
|
170
|
+
''' Removes metadata info from selected media files
|
|
171
|
+
Can remove all metadata, or metadata from specified fields
|
|
172
|
+
Visualises targeted changes before actual processing
|
|
173
|
+
'''
|
|
174
|
+
if tag_fields is None:
|
|
175
|
+
# remove all tags
|
|
176
|
+
tag_holder = TagHolder(copy_empty_vals = True)
|
|
177
|
+
else:
|
|
178
|
+
# remove specified tags
|
|
179
|
+
tag_holder = TagHolder(nullable_fields = tag_fields)
|
|
180
|
+
|
|
181
|
+
self.set_tags_visual(fs_entry_params,
|
|
182
|
+
diff_tags_only = diff_tags_only,
|
|
183
|
+
tag_holder = tag_holder)
|
|
184
|
+
|
|
185
|
+
def replace_tags(self, fs_entry_params,
|
|
186
|
+
diff_tags_only = False,
|
|
187
|
+
tag_fields = None,
|
|
188
|
+
ignore_case = False,
|
|
189
|
+
ind_str = None, replace_str = None):
|
|
190
|
+
|
|
191
|
+
''' RegExp-based replace in specified fields
|
|
192
|
+
Visualises changes before proceeding
|
|
193
|
+
'''
|
|
194
|
+
|
|
195
|
+
if not (tag_fields and find_str):
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
flags = re.UNICODE
|
|
199
|
+
if ignore_case:
|
|
200
|
+
flags = flags | re.IGNORECASE
|
|
201
|
+
p = re.compile(find_str, flags)
|
|
202
|
+
|
|
203
|
+
def replace_transform(value):
|
|
204
|
+
match = p.search(value)
|
|
205
|
+
if match:
|
|
206
|
+
if replace_str is not None:
|
|
207
|
+
value = p.sub(replace_str, value)
|
|
208
|
+
else:
|
|
209
|
+
value = match.group()
|
|
210
|
+
return value
|
|
211
|
+
|
|
212
|
+
tag_holder = TagHolder(process_templates = False)
|
|
213
|
+
for tag_field in tag_fields:
|
|
214
|
+
setattr(tag_holder, tag_field, '${}'.format(tag_field))
|
|
215
|
+
tag_holder.template_processor_method = replace_transform
|
|
216
|
+
|
|
217
|
+
self.set_tags_visual(fs_entry_params,
|
|
218
|
+
diff_tags_only = diff_tags_only,
|
|
219
|
+
tag_holder = tag_holder)
|
|
220
|
+
|
|
221
|
+
def capitalize_tags(self, fs_entry_params,
|
|
222
|
+
diff_tags_only = False,
|
|
223
|
+
tag_fields = None):
|
|
224
|
+
|
|
225
|
+
''' Capitalizes words in specified fields
|
|
226
|
+
Visualises changes before proceeding
|
|
227
|
+
'''
|
|
228
|
+
|
|
229
|
+
if not tag_fields:
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
def capitalize_transform(value):
|
|
233
|
+
return string.capwords(value)
|
|
234
|
+
|
|
235
|
+
tag_holder = TagHolder(process_templates = False)
|
|
236
|
+
for tag_field in tag_fields:
|
|
237
|
+
setattr(tag_holder, tag_field, '${}'.format(tag_field))
|
|
238
|
+
tag_holder.template_processor_method = capitalize_transform
|
|
239
|
+
|
|
240
|
+
self.set_tags_visual(fs_entry_params,
|
|
241
|
+
diff_tags_only = diff_tags_only,
|
|
242
|
+
tag_holder = tag_holder)
|
|
243
|
+
|
|
244
|
+
def detauch_art(self, fs_entry_params, target_dir = None):
|
|
245
|
+
|
|
246
|
+
''' Detauches art from selected media files
|
|
247
|
+
'''
|
|
248
|
+
|
|
249
|
+
fcnt = 0
|
|
250
|
+
pass_filter = lambda fpath: self.handler.can_handle(fpath)
|
|
251
|
+
for entry in DWalker.file_entries(fs_entry_params, pass_filter = pass_filter):
|
|
252
|
+
|
|
253
|
+
if not target_dir:
|
|
254
|
+
target_dir = src_dir
|
|
255
|
+
else:
|
|
256
|
+
os.makedirs(target_dir, exist_ok = True)
|
|
257
|
+
|
|
258
|
+
if self.handler.tag_holder.has_artwork:
|
|
259
|
+
self.handler.detauch_art(target_dir)
|
|
260
|
+
fcnt += 1
|
|
261
|
+
|
|
262
|
+
# print summary
|
|
263
|
+
if not fs_entry_params.quiet:
|
|
264
|
+
print('Detauched art from {0} media entries'.format(fcnt))
|
|
265
|
+
|
|
266
|
+
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: batchmp
|
|
3
|
+
Version: 1.4
|
|
4
|
+
Summary: Command-line tools for batch media processing
|
|
5
|
+
Home-page: https://github.com/akpw/batch-mp-tools
|
|
6
|
+
Author: Arseniy Kuznetsov
|
|
7
|
+
Author-email: k.arseniy@gmail.com
|
|
8
|
+
License: GPL-2.0-or-later
|
|
9
|
+
Keywords: batch processing media video audio CLI rename tags ID3
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
21
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
|
|
22
|
+
Classifier: Topic :: Multimedia :: Video
|
|
23
|
+
Classifier: Topic :: Multimedia :: Video :: Conversion
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: mutagen>=1.27
|
|
29
|
+
Requires-Dist: pygtrie>=2.3.2
|
|
30
|
+
Requires-Dist: filetype>=1.0.7
|
|
31
|
+
Requires-Dist: mediafile>=0.13.0
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: summary
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
**Status:**
|
|
46
|
+
A rainy weekends project under occasional development :)
|
|
47
|
+
|
|
48
|
+
#### Requirements:
|
|
49
|
+
- [Python 3.4.x](https://www.python.org/download/releases/3.4.1/) or later
|
|
50
|
+
|
|
51
|
+
#### Install:
|
|
52
|
+
- from [PyPI](https://pypi.python.org/pypi/batchmp): `$ pip install batchmp`
|
|
53
|
+
- latest from source repository: `$ pip install git+https://github.com/akpw/batch-mp-tools.git`
|
|
54
|
+
|
|
55
|
+
#### Blogs:
|
|
56
|
+
- [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
57
|
+
- [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html)
|
|
58
|
+
- [BatchMP Tools Tutorial](https://akpw.github.io//articles/2015/04/10/batchmp-tutorial-part-i.html)
|
|
59
|
+
- [The BatchMP Tools Project](https://akpw.github.io//articles/2015/03/21/the-batchmp-project.html)
|
|
60
|
+
- [Parallel batch media processing with FFmpeg and Python](https://akpw.github.io//articles/2014/11/24/batch-media-processing-ffmpeg-python.html)
|
|
61
|
+
|
|
62
|
+
## Description
|
|
63
|
+
|
|
64
|
+
Batch management of media files, from base properties such as file names through tags / artwork metadata to advanced operations on the media content.
|
|
65
|
+
|
|
66
|
+
The project is written in [Python 3.4](https://www.python.org/download/releases/3.4.1/) and currently consists of three main command-line utilities. All three share the core concept of various transformations applied to generated stream of file system entries, and consequently they also share the same set of global options. A quick way to check on that is to run:
|
|
67
|
+
```
|
|
68
|
+
$ renamer -h
|
|
69
|
+
$ tagger -h
|
|
70
|
+
$ bmfp -h
|
|
71
|
+
```
|
|
72
|
+
That will show the global options along with specific commands for each tool. Getting more info on the commands level can be done using a similar approach, e.g. to learn more about the `renamer replace` command:
|
|
73
|
+
```
|
|
74
|
+
$ renamer replace -h
|
|
75
|
+
```
|
|
76
|
+
By default the tools always visualize targeted changes (whenever possible) before actual processing.
|
|
77
|
+
|
|
78
|
+
A little bit more details on each utility:
|
|
79
|
+
|
|
80
|
+
[**Renamer**](https://github.com/akpw/batch-mp-tools#renamer) is a multi-platform batch rename tool. In addition to common operations such as regexp-based replace, adding text / dates, etc. it also supports advanced operations such as expandable template processing during replace, multi-level indexing across nested directories, flattening folders, organizing files by type or date, etc. The enhanced print command now supports virtual views to preview organization without moving files.
|
|
81
|
+
At its simplest, Renamer can be used to print out the content of current directory:
|
|
82
|
+
```
|
|
83
|
+
$ renamer
|
|
84
|
+
```
|
|
85
|
+
Without command arguments, renamer uses `print` as the default command. This is also the case for both `$ tagger` and `$ bmfp`, with each of the tool showing info which is relevant to its intended purpose.
|
|
86
|
+
A little bit more advanced, to see what's lurking at the 7th nested folder level:
|
|
87
|
+
```
|
|
88
|
+
$ renamer print -sl 7
|
|
89
|
+
```
|
|
90
|
+
For multi-level indexing of all M4A files in all sub-directories of the current folder:
|
|
91
|
+
```
|
|
92
|
+
$ renamer -r -in '*.m4a' index
|
|
93
|
+
/Desktop/_test/Gould
|
|
94
|
+
|-/1
|
|
95
|
+
|- 01 Glenn Gould French Suite 1 In D, BWV812 1 Allemande.m4a
|
|
96
|
+
|- 02 Glenn Gould French Suite 1 In D, BWV812 2 Courante.m4a
|
|
97
|
+
|- 03 Glenn Gould French Suite 1 In D, BWV812 3 Sarabande.m4a
|
|
98
|
+
|-/2
|
|
99
|
+
|- 01 Bach, Johann Sebastian French Suite 5 In G Major, BWV816 1 Allemande.m4a
|
|
100
|
+
|- 02 Bach, Johann Sebastian French Suite 5 In G Major, BWV816 2 Courante.m4a
|
|
101
|
+
|- 03 Bach, Johann Sebastian French Suite 5 In G Major, BWV816 3 Sarabande.m4a
|
|
102
|
+
|-/_Art
|
|
103
|
+
6 files, 3 folders
|
|
104
|
+
|
|
105
|
+
Proceed? [y/n]:
|
|
106
|
+
```
|
|
107
|
+
Sequential indexing is supported as well using the `-sq` switch. An important detail here, by default Renamer is visualizing the targeted changes and asking for permission to proceed before actually doing anything.
|
|
108
|
+
|
|
109
|
+
For organizing files by media type:
|
|
110
|
+
```
|
|
111
|
+
$ renamer organize -b type
|
|
112
|
+
~/Downloads
|
|
113
|
+
|- image/
|
|
114
|
+
|- photo1.jpg
|
|
115
|
+
|- screenshot.png
|
|
116
|
+
|- video/
|
|
117
|
+
|- movie.mp4
|
|
118
|
+
|- audio/
|
|
119
|
+
|- song.mp3
|
|
120
|
+
|
|
121
|
+
Proceed? [y/n]:
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or preview how files would look organized by date without moving them:
|
|
125
|
+
```
|
|
126
|
+
$ renamer print -b date --date-format "%Y/%m"
|
|
127
|
+
Virtual view by date:
|
|
128
|
+
~/Downloads
|
|
129
|
+
|- 2025/
|
|
130
|
+
|- 01/
|
|
131
|
+
|- document.pdf
|
|
132
|
+
|- photo.jpg
|
|
133
|
+
|- 02/
|
|
134
|
+
|- video.mp4
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
[**Tagger**](https://github.com/akpw/batch-mp-tools#tagger) manages media metadata, such as tags and artwork. Setting those in selected media file over multiple nested directories now becomes a breeze, with just a few simple commands working uniformly over almost any practically imaginable audio / video media formats. While easy to use, Tagger supports advanced metadata manipulation such as regexp-based replace, expandable template processing, etc. For example, to set the title tag to respective file names followed by the values of track and tracktotal tags:
|
|
140
|
+
```
|
|
141
|
+
$ tagger -r -in '*BWV816 1*' set --title '$filename, $track of $tracktotal'
|
|
142
|
+
Targeted after processing:
|
|
143
|
+
/Desktop/_test/Gould
|
|
144
|
+
|-/1
|
|
145
|
+
|-/2
|
|
146
|
+
|- Bach, Johann Sebastian French Suite 5 In G Major, BWV816 1 Allemande.m4a
|
|
147
|
+
Title: Bach, Johann Sebastian French Suite 5 In G Major, BWV816 1 Allemande, 1 of 26
|
|
148
|
+
Album: Bach French Suites BWV 812-817 Vol. II; Glenn Gould
|
|
149
|
+
Artist: Glenn Gould
|
|
150
|
+
Album Artist: Glenn Gould
|
|
151
|
+
Genre: Classical
|
|
152
|
+
Composer: Johann Sebastian Bach (1685-1750)
|
|
153
|
+
Year: 1994
|
|
154
|
+
Track: 1/26
|
|
155
|
+
Disk: 2/2
|
|
156
|
+
|-/Art
|
|
157
|
+
1 files, 3 folders
|
|
158
|
+
|
|
159
|
+
Proceed? [y/n]: n
|
|
160
|
+
```
|
|
161
|
+
The commands above show some of the available global options: `-r` for recursion into nested folders and `-in` to select media files. In the example above just one file was selected (for the sake of output brevity), which also could be achived via using `-f` for the file source mode.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
[**BMFP**](https://github.com/akpw/batch-mp-tools/blob/master/README.md#bmfp-requires-ffmpeg) is all about efficient media content processing, such as conversion between various formats, normalizing sound volume, segmenting / fragmenting media files, denoising audio, detaching individual audio / video streams, etc. As processing media files can typically be resource consuming, BMFP is designed to take advantage of multi-core processors. By default, it automatically breaks up jobs into individual tasks that are then run as separate processes on available CPU cores.
|
|
165
|
+
**BMFP is built on top of [FFmpeg](http://ffmpeg.org/download.html), which needs to be installed and available in the command line**. BMFP can be thought of as a batch FFmpeg runner, intended to make common uses of FFmpeg easy while not restricting its full power.
|
|
166
|
+
|
|
167
|
+
For example, to convert all files from the above example from M4A to FLAC:
|
|
168
|
+
```
|
|
169
|
+
$ bmfp -r convert -la -tf FLAC
|
|
170
|
+
```
|
|
171
|
+
The `-tf` switch specifies the target format, while `-la` explicitly tells BMFP to do a lossless conversion.
|
|
172
|
+
|
|
173
|
+
To check on the result, lets's just use the [tagger's](https://github.com/akpw/batch-mp-tools#tagger) ability to print media files info:
|
|
174
|
+
```
|
|
175
|
+
$ tagger -r -in '*BWV816 1*' print -st -ss
|
|
176
|
+
/Users/AKPower/Desktop/_test/Gould
|
|
177
|
+
|-/1
|
|
178
|
+
|-/2
|
|
179
|
+
|- 7.4MB Bach, Johann Sebastian French Suite 5 in G major, BWV816 1 Allemande.flac
|
|
180
|
+
Title: French Suite 5 in G major, BWV 816 - 1 Allemande
|
|
181
|
+
Album: Bach French Suites BWV 812-817 Vol. II; Glenn Gould
|
|
182
|
+
Artist: Glenn Gould
|
|
183
|
+
Album Artist: Glenn Gould
|
|
184
|
+
Genre: Classical
|
|
185
|
+
Composer: Johann Sebastian Bach (1685-1750)
|
|
186
|
+
Year: 1994
|
|
187
|
+
Track: 1/26
|
|
188
|
+
Disk: 2/2
|
|
189
|
+
Format: FLAC
|
|
190
|
+
Duration: 0:01:48, Bit rate: 548kb/s, Sample rate: 44100Hz, Bit depth: 16
|
|
191
|
+
|-/_backup_15Mar25_094341
|
|
192
|
+
|- 7.9MB Bach, Johann Sebastian French Suite 5 in G major, BWV816 1 Allemande.m4a
|
|
193
|
+
Title: French Suite 5 in G major, BWV 816 - 1 Allemande
|
|
194
|
+
Album: Bach French Suites BWV 812-817 Vol. II; Glenn Gould
|
|
195
|
+
Artist: Glenn Gould
|
|
196
|
+
Album Artist: Glenn Gould
|
|
197
|
+
Genre: Classical
|
|
198
|
+
Composer: Johann Sebastian Bach (1685-1750)
|
|
199
|
+
Year: 1994
|
|
200
|
+
Track: 1/26
|
|
201
|
+
Disk: 2/2
|
|
202
|
+
Format: ALAC
|
|
203
|
+
Duration: 0:01:48, Bit rate: 579kb/s, Sample rate: 44100Hz, Bit depth: 16
|
|
204
|
+
|-/Art
|
|
205
|
+
2 files, 4 folders
|
|
206
|
+
Total size: 15.2MB
|
|
207
|
+
```
|
|
208
|
+
From a brief glance, all looks OK. BMFP used FFmpeg to do the actual conversion, while taking care of all other things like preserving tags / artwork, etc.
|
|
209
|
+
|
|
210
|
+
I will follow up with more examples and common use-cases in future blogs.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
## Brief Description of CLI Commands (use -h to expand on details for individual commands)
|
|
214
|
+
### renamer
|
|
215
|
+
Batch renaming of files and directories
|
|
216
|
+
. source directory or source file modes
|
|
217
|
+
. visualises original / targeted folders structure before actual processing
|
|
218
|
+
. supports recursion, can optionally stop at specified end_level
|
|
219
|
+
. supports flattening folders beyond specified target_level
|
|
220
|
+
. supports include / exclude patterns (Unix style)
|
|
221
|
+
. display sorting:
|
|
222
|
+
.. by size/date, ascending/descending
|
|
223
|
+
. action commands:
|
|
224
|
+
.. print Prints source directory. Enhanced with virtual organization views:
|
|
225
|
+
$ renamer print -b type # Preview organize by media type
|
|
226
|
+
$ renamer print -b date -df "%Y/%m" # Preview organize by date
|
|
227
|
+
.. flatten Flatten all folders below target level, moving the
|
|
228
|
+
files up at the target level. By default, deletes all empty flattened folders
|
|
229
|
+
.. index Adds index to files and directories names
|
|
230
|
+
.. replace RegExp-based replace in files and directories names. Supports expandable templates,
|
|
231
|
+
such as $dirname, $pardirname, $atime, $ctime, etc. For media files, also can process
|
|
232
|
+
tag-based templates such as $title, $album, $artist, $albumartist, $genre, $year, $track,
|
|
233
|
+
etc.
|
|
234
|
+
.. add_date Adds date to files and directories names
|
|
235
|
+
.. add_text Adds text to files and directories names
|
|
236
|
+
.. remove Removes n characters from files and directories names
|
|
237
|
+
.. capitalize Capitalizes words in files / directories names
|
|
238
|
+
.. delete Delete selected files and directories
|
|
239
|
+
.. organize Organizes files into subdirectories based on their attributes:
|
|
240
|
+
$ renamer organize -b type # By media type (image/, video/, audio/)
|
|
241
|
+
$ renamer organize -b date -df "%Y-%m" # By date (2025-01/, 2025-02/)
|
|
242
|
+
$ renamer organize -b date -df "%Y/%m" -td ~/Sorted # To target directory
|
|
243
|
+
|
|
244
|
+
Usage: renamer [-h] [-d DIR] [-f FILE] [Global Options] {Commands} [Commands Options]
|
|
245
|
+
Global Options:
|
|
246
|
+
Input source mode:
|
|
247
|
+
[-d, --dir] Source directory (default is the current directory)
|
|
248
|
+
[-f, --file] File to process
|
|
249
|
+
|
|
250
|
+
Recursion mode:
|
|
251
|
+
[-r, --recursive] Recurse into nested folders
|
|
252
|
+
[-el, --end-level] End level for recursion into nested folders
|
|
253
|
+
|
|
254
|
+
Filter files or folders:
|
|
255
|
+
[-in, --include] Include: Unix-style name patterns separated by ';'
|
|
256
|
+
[-ex, --exclude] Exclude: Unix-style name patterns separated by ';'
|
|
257
|
+
(excludes hidden files by default)
|
|
258
|
+
[-fd, --filter-dirs] Enable Include/Exclude patterns on directories
|
|
259
|
+
[-af, --all-files] Disable Include/Exclude patterns on files
|
|
260
|
+
(shows hidden files excluded by default)
|
|
261
|
+
Miscellaneous:
|
|
262
|
+
[-s, --sort]{na|nd|sa|sd} Sort order for files / folders (name | date, asc | desc)
|
|
263
|
+
[-ni, nested-indent] Indent for printing nested directories
|
|
264
|
+
[-q, --quiet] Do not visualise changes / show messages during processing
|
|
265
|
+
|
|
266
|
+
Commands:
|
|
267
|
+
{print, index, add_date, add_text, remove, replace, capitalize, flatten, delete, organize, version, info}
|
|
268
|
+
$ renamer {command} -h #run this for detailed help on individual commands
|
|
269
|
+
|
|
270
|
+
### tagger
|
|
271
|
+
###### Supported formats:
|
|
272
|
+
'MP3', 'MP4', 'M4A', M4V', 'AIFF', 'ASF', 'QuickTime / MOV',
|
|
273
|
+
'FLAC', 'MonkeysAudio', 'Musepack',
|
|
274
|
+
'Ogg FLAC', 'Ogg Speex', 'Ogg Theora', 'Ogg Vorbis',
|
|
275
|
+
'True Audio', 'WavPack', 'OptimFROG' <p>
|
|
276
|
+
Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
|
|
277
|
+
|
|
278
|
+
Batch management of media files metadata (tags & artwork)
|
|
279
|
+
. source directory / source file modes
|
|
280
|
+
. visualises original / targeted files metadata structure
|
|
281
|
+
. supports recursion, can optionally stop at specified end_level
|
|
282
|
+
. include / exclude patterns (Unix style)
|
|
283
|
+
. display sorting:
|
|
284
|
+
.. by size/date, ascending/descending
|
|
285
|
+
. action commands:
|
|
286
|
+
.. print Prints media info
|
|
287
|
+
.. set Sets tags in media files, including artwork, e.g:
|
|
288
|
+
$ tagger set --album 'The Album' -art '~/Desktop/art.jpg'
|
|
289
|
+
Supports expandable templates. To specify a template value,
|
|
290
|
+
use the long tag field name preceded by $:
|
|
291
|
+
$ tagger set --title '$title, $track of $tracktotal'
|
|
292
|
+
In addition to tag fields templates, file system names are also supported:
|
|
293
|
+
$ tagger set --title '$filename' --album '$dirname' --artist '$pardirname'...
|
|
294
|
+
.. copy Copies tags from a specified media file
|
|
295
|
+
.. index Indexes Track / Track Total tags
|
|
296
|
+
.. remove Removes tags from media files
|
|
297
|
+
.. replace RegExp-based replace in specified tags
|
|
298
|
+
e.g., to remove the first three characters in title:
|
|
299
|
+
$ tagger replace -tf 'title' -fs '^[\s\S]{0,3}' -rs ''
|
|
300
|
+
.. capitalize Capitalizes words in specified tags
|
|
301
|
+
.. detauch Extracts artwork
|
|
302
|
+
|
|
303
|
+
Usage: tagger [-h] [-d DIR] [-f FILE] [Global Options] {Commands} [Commands Options]
|
|
304
|
+
Global Options:
|
|
305
|
+
Input source mode:
|
|
306
|
+
[-d, --dir] Source directory (default is the current directory)
|
|
307
|
+
[-f, --file] File to process
|
|
308
|
+
|
|
309
|
+
Recursion mode:
|
|
310
|
+
[-r, --recursive] Recurse into nested folders
|
|
311
|
+
[-el, --end-level] End level for recursion into nested folders
|
|
312
|
+
|
|
313
|
+
Filter files or folders:
|
|
314
|
+
[-in, --include] Include: Unix-style name patterns separated by ';'
|
|
315
|
+
[-ex, --exclude] Exclude: Unix-style name patterns separated by ';'
|
|
316
|
+
(excludes hidden files by default)
|
|
317
|
+
[-fd, --filter-dirs] Enable Include/Exclude patterns on directories
|
|
318
|
+
[-af, --all-files] Disable Include/Exclude patterns on files
|
|
319
|
+
(shows hidden files excluded by default)
|
|
320
|
+
Miscellaneous:
|
|
321
|
+
[-s, --sort]{na|nd|sa|sd} Sort order for files / folders (name | date, asc | desc)
|
|
322
|
+
[-ni, nested-indent] Indent for printing nested directories
|
|
323
|
+
[-q, --quiet] Do not visualise changes / show messages during processing
|
|
324
|
+
|
|
325
|
+
Commands:
|
|
326
|
+
{print, set, copy, index, remove, replace, capitalize, detauch, version, info}
|
|
327
|
+
$ tagger {command} -h #run this for detailed help on individual commands
|
|
328
|
+
|
|
329
|
+
### bmfp (requires [FFmpeg](http://ffmpeg.org/download.html))
|
|
330
|
+
Batch processing of media files
|
|
331
|
+
. Uses multiprocessing to utilize available CPU cores
|
|
332
|
+
. source directory / source file modes
|
|
333
|
+
. recursion to specified end_level
|
|
334
|
+
. include / exclude patterns (Unix style)
|
|
335
|
+
. action commands:
|
|
336
|
+
.. print Prints media files
|
|
337
|
+
.. convert Converts media to specified format
|
|
338
|
+
For example, to convert all files in current directory
|
|
339
|
+
$ bmfp convert -la -tf FLAC
|
|
340
|
+
.. normalize Nomalizes sound volume in media files
|
|
341
|
+
Peak normalization supported, RMS normalizations TBD
|
|
342
|
+
.. fragment Extract a media file fragment
|
|
343
|
+
.. segment Splits media files into segments
|
|
344
|
+
For example, to split media files in segments of 45 mins:
|
|
345
|
+
$ bmfp segment -d 45:00
|
|
346
|
+
.. silencesplit Splits media files into segments via detecting specified silence
|
|
347
|
+
$ bmfp silencesplit
|
|
348
|
+
.. cuesplit Splits media files into parts with specified output format,
|
|
349
|
+
according to their respective cue sheets
|
|
350
|
+
For example, to split all cue files in the current directory
|
|
351
|
+
$ bmfp cuesplit -tf mp3
|
|
352
|
+
.. denoise Reduces background audio noise in media files
|
|
353
|
+
|
|
354
|
+
.. adjust volume TDB: Adjust audio volume
|
|
355
|
+
.. speed up TDB: Uses Time Stretching to increase audio / video speed
|
|
356
|
+
.. slow down TDB: Uses Time Stretching to increase audio / video speed
|
|
357
|
+
|
|
358
|
+
Usage: bmfp [-h] [-d DIR] [-f FILE] [GLobal Options] {Commands}[Commands Options]
|
|
359
|
+
Input source mode:
|
|
360
|
+
[-d, --dir] Source directory (default is the current directory)
|
|
361
|
+
[-f, --file] File to process
|
|
362
|
+
|
|
363
|
+
Recursion mode:
|
|
364
|
+
[-r, --recursive] Recurse into nested folders
|
|
365
|
+
[-el, --end-level] End level for recursion into nested folders
|
|
366
|
+
|
|
367
|
+
Filter files or folders:
|
|
368
|
+
[-in, --include] Include: Unix-style name patterns separated by ';'
|
|
369
|
+
[-ex, --exclude] Exclude: Unix-style name patterns separated by ';'
|
|
370
|
+
(excludes hidden files by default)
|
|
371
|
+
[-fd, --filter-dirs] Enable Include/Exclude patterns on directories
|
|
372
|
+
[-af, --all-files] Disable Include/Exclude patterns on files
|
|
373
|
+
(shows hidden files excluded by default)
|
|
374
|
+
|
|
375
|
+
Target output Directory Target output directory. When omitted, will be
|
|
376
|
+
[-td, --target-dir] automatically created inside the parent level of
|
|
377
|
+
the input source. For recursive processing,
|
|
378
|
+
the processed files directory structure there
|
|
379
|
+
will be the same as for the original files.
|
|
380
|
+
FFmpeg General Output Options:
|
|
381
|
+
[-ma, --map-all] Force including all streams from the input file
|
|
382
|
+
[-cc, --copy-codecs] Copy streams codecs without re-encoding
|
|
383
|
+
[-vn, --no-video] Exclude video streams from the output
|
|
384
|
+
[-an, --no-audio] Exclude audio streams from the output
|
|
385
|
+
[-sn, --no-subs] Exclude subtitles streams from the output
|
|
386
|
+
[-fo, --ffmpeg-options] Additional FFmpeg options
|
|
387
|
+
|
|
388
|
+
FFmpeg Commands Execution:
|
|
389
|
+
[-q, --quiet] Do not visualise changes / show messages during processing
|
|
390
|
+
[-se, --serial-exec] Run all task's commands in a single process
|
|
391
|
+
|
|
392
|
+
Commands:
|
|
393
|
+
{print, convert, normalize, fragment, segment, silencesplit, cuesplit, denoise, version, info}
|
|
394
|
+
$ bmfp {command} -h #run this for detailed help on individual commands
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
## Installing Development version
|
|
398
|
+
- Clone the repo, then run: `$ python -m pip install .`
|
|
399
|
+
|
|
400
|
+
## Running Tests
|
|
401
|
+
|
|
402
|
+
**Using pytest (recommended):**
|
|
403
|
+
```bash
|
|
404
|
+
$ pytest -v --tb=short # Run all tests with verbose output
|
|
405
|
+
$ pytest tests/ # Run all tests
|
|
406
|
+
$ pytest tests/fs/test_fs_organize.py # Test organize functionality
|
|
407
|
+
$ pytest tests/fs/test_fsutils.py # Test core filesystem utilities
|
|
408
|
+
$ pytest tests/cli/test_renamer_cli.py # Test renamer CLI
|
|
409
|
+
$ pytest -k "test_organize" # Run tests matching pattern
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Using unittest (fallback):**
|
|
413
|
+
```bash
|
|
414
|
+
$ python -m unittest discover tests -v # Run all tests with verbose output
|
|
415
|
+
$ python -m unittest tests.fs.test_fs_organize # Run specific test module
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|