batchmp 1.4__tar.gz → 1.4.2__tar.gz
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-1.4 → batchmp-1.4.2}/PKG-INFO +32 -4
- {batchmp-1.4 → batchmp-1.4.2}/README.md +27 -3
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/base/bmp_options.py +1 -1
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/commons/utils.py +10 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/dirtools.py +25 -185
- batchmp-1.4.2/batchmp/fstools/virtual_organizer.py +301 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/PKG-INFO +32 -4
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/SOURCES.txt +2 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/requires.txt +4 -0
- batchmp-1.4.2/pyproject.toml +3 -0
- {batchmp-1.4 → batchmp-1.4.2}/setup.py +8 -1
- {batchmp-1.4 → batchmp-1.4.2}/LICENSE +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/base/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/base/bmp_dispatch.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/base/vchk.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/bmfp/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/bmfp/bmfp_dispatch.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/bmfp/bmfp_options.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/renamer/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/renamer/renamer_dispatch.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/renamer/renamer_options.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/tagger/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/tagger/tagger_dispatch.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/cli/tagger/tagger_options.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/commons/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/commons/chainedhandler.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/commons/descriptors.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/commons/progressbar.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/commons/taskprocessor.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/cmdopt.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/convert.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/cuesplit.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/denoise.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/fragment.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/normalize_peak.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/segment.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffcommands/silencesplit.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffrunner.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/ffutils.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/processors/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/processors/basefp.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/processors/ffentry.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/utils/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/utils/cueparse.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/ffmptools/utils/cuesheet.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/builders/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/builders/fsb.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/builders/fsentry.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/builders/fsprms.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/fsutils.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/rename.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/fstools/walker.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/basehandler.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/ffmphandler.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/ffmphandlers/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/ffmphandlers/base.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/mtghandler.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/pmhandler.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/handlers/tagsholder.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/output/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/output/formatters.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/processors/__init__.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp/tags/processors/basetp.py +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/dependency_links.txt +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/entry_points.txt +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/top_level.txt +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/batchmp.egg-info/zip-safe +0 -0
- {batchmp-1.4 → batchmp-1.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchmp
|
|
3
|
-
Version: 1.4
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Command-line tools for batch media processing
|
|
5
5
|
Home-page: https://github.com/akpw/batch-mp-tools
|
|
6
6
|
Author: Arseniy Kuznetsov
|
|
@@ -29,6 +29,9 @@ Requires-Dist: mutagen>=1.27
|
|
|
29
29
|
Requires-Dist: pygtrie>=2.3.2
|
|
30
30
|
Requires-Dist: filetype>=1.0.7
|
|
31
31
|
Requires-Dist: mediafile>=0.13.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
32
35
|
Dynamic: author
|
|
33
36
|
Dynamic: author-email
|
|
34
37
|
Dynamic: classifier
|
|
@@ -38,6 +41,7 @@ Dynamic: home-page
|
|
|
38
41
|
Dynamic: keywords
|
|
39
42
|
Dynamic: license
|
|
40
43
|
Dynamic: license-file
|
|
44
|
+
Dynamic: provides-extra
|
|
41
45
|
Dynamic: requires-dist
|
|
42
46
|
Dynamic: summary
|
|
43
47
|
|
|
@@ -133,7 +137,10 @@ Or preview how files would look organized by date without moving them:
|
|
|
133
137
|
|- 02/
|
|
134
138
|
|- video.mp4
|
|
135
139
|
```
|
|
136
|
-
|
|
140
|
+
For more detailed examples and advanced organize / virtual views functionality, see:
|
|
141
|
+
- [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html)
|
|
142
|
+
- [BatchMP Tools Tutorial, Part II: renaming files with renamer](https://akpw.github.io/articles/2015/04/11/batchmp-tutorial-part-ii.html)
|
|
143
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
137
144
|
|
|
138
145
|
|
|
139
146
|
[**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:
|
|
@@ -160,6 +167,11 @@ Or preview how files would look organized by date without moving them:
|
|
|
160
167
|
```
|
|
161
168
|
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
169
|
|
|
170
|
+
For more practical examples, see:
|
|
171
|
+
- [BatchMP Tools Tutorial Part III: setting tags and artwork with tagger](https://akpw.github.io/articles/2015/04/12/batchmp-tutorial-part-iii.html)
|
|
172
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
173
|
+
|
|
174
|
+
|
|
163
175
|
|
|
164
176
|
[**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
177
|
**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.
|
|
@@ -207,7 +219,10 @@ To check on the result, lets's just use the [tagger's](https://github.com/akpw/b
|
|
|
207
219
|
```
|
|
208
220
|
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
221
|
|
|
210
|
-
|
|
222
|
+
For more practical examples, see:
|
|
223
|
+
- [BatchMP Tools Tutorial, splitting a long media file with bmfp](https://akpw.github.io/articles/2015/04/10/batchmp-tutorial-part-i.html)
|
|
224
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
225
|
+
|
|
211
226
|
|
|
212
227
|
|
|
213
228
|
## Brief Description of CLI Commands (use -h to expand on details for individual commands)
|
|
@@ -395,10 +410,23 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
|
|
|
395
410
|
|
|
396
411
|
|
|
397
412
|
## Installing Development version
|
|
398
|
-
- Clone the repo,
|
|
413
|
+
- Clone the repo, create a virtual environment, and activate it:
|
|
414
|
+
```bash
|
|
415
|
+
$ python3 -m venv .venv
|
|
416
|
+
$ source .venv/bin/activate
|
|
417
|
+
```
|
|
418
|
+
- Install the project in editable mode:
|
|
419
|
+
```bash
|
|
420
|
+
$ pip install -e .
|
|
421
|
+
```
|
|
399
422
|
|
|
400
423
|
## Running Tests
|
|
401
424
|
|
|
425
|
+
To run the test suite, first ensure you have installed the development dependencies:
|
|
426
|
+
```bash
|
|
427
|
+
$ pip install -e ".[test]"
|
|
428
|
+
```
|
|
429
|
+
|
|
402
430
|
**Using pytest (recommended):**
|
|
403
431
|
```bash
|
|
404
432
|
$ pytest -v --tb=short # Run all tests with verbose output
|
|
@@ -90,7 +90,10 @@ Or preview how files would look organized by date without moving them:
|
|
|
90
90
|
|- 02/
|
|
91
91
|
|- video.mp4
|
|
92
92
|
```
|
|
93
|
-
|
|
93
|
+
For more detailed examples and advanced organize / virtual views functionality, see:
|
|
94
|
+
- [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html)
|
|
95
|
+
- [BatchMP Tools Tutorial, Part II: renaming files with renamer](https://akpw.github.io/articles/2015/04/11/batchmp-tutorial-part-ii.html)
|
|
96
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
94
97
|
|
|
95
98
|
|
|
96
99
|
[**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:
|
|
@@ -117,6 +120,11 @@ Or preview how files would look organized by date without moving them:
|
|
|
117
120
|
```
|
|
118
121
|
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.
|
|
119
122
|
|
|
123
|
+
For more practical examples, see:
|
|
124
|
+
- [BatchMP Tools Tutorial Part III: setting tags and artwork with tagger](https://akpw.github.io/articles/2015/04/12/batchmp-tutorial-part-iii.html)
|
|
125
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
126
|
+
|
|
127
|
+
|
|
120
128
|
|
|
121
129
|
[**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.
|
|
122
130
|
**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.
|
|
@@ -164,7 +172,10 @@ To check on the result, lets's just use the [tagger's](https://github.com/akpw/b
|
|
|
164
172
|
```
|
|
165
173
|
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.
|
|
166
174
|
|
|
167
|
-
|
|
175
|
+
For more practical examples, see:
|
|
176
|
+
- [BatchMP Tools Tutorial, splitting a long media file with bmfp](https://akpw.github.io/articles/2015/04/10/batchmp-tutorial-part-i.html)
|
|
177
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
178
|
+
|
|
168
179
|
|
|
169
180
|
|
|
170
181
|
## Brief Description of CLI Commands (use -h to expand on details for individual commands)
|
|
@@ -352,10 +363,23 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
|
|
|
352
363
|
|
|
353
364
|
|
|
354
365
|
## Installing Development version
|
|
355
|
-
- Clone the repo,
|
|
366
|
+
- Clone the repo, create a virtual environment, and activate it:
|
|
367
|
+
```bash
|
|
368
|
+
$ python3 -m venv .venv
|
|
369
|
+
$ source .venv/bin/activate
|
|
370
|
+
```
|
|
371
|
+
- Install the project in editable mode:
|
|
372
|
+
```bash
|
|
373
|
+
$ pip install -e .
|
|
374
|
+
```
|
|
356
375
|
|
|
357
376
|
## Running Tests
|
|
358
377
|
|
|
378
|
+
To run the test suite, first ensure you have installed the development dependencies:
|
|
379
|
+
```bash
|
|
380
|
+
$ pip install -e ".[test]"
|
|
381
|
+
```
|
|
382
|
+
|
|
359
383
|
**Using pytest (recommended):**
|
|
360
384
|
```bash
|
|
361
385
|
$ pytest -v --tb=short # Run all tests with verbose output
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import os, sys, string
|
|
33
33
|
from argparse import ArgumentParser, HelpFormatter
|
|
34
|
-
from
|
|
34
|
+
from batchmp.commons.utils import strtobool
|
|
35
35
|
from urllib.parse import urlparse
|
|
36
36
|
from batchmp.commons.utils import MiscHelpers
|
|
37
37
|
from batchmp.fstools.fsutils import FSH
|
|
@@ -61,6 +61,16 @@ def run_cmd(cmd, shell = False):
|
|
|
61
61
|
return output
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def strtobool(val):
|
|
65
|
+
val = str(val).lower()
|
|
66
|
+
if val in ('y', 'yes', 't', 'true', 'on', '1'):
|
|
67
|
+
return 1
|
|
68
|
+
elif val in ('n', 'no', 'f', 'false', 'off', '0'):
|
|
69
|
+
return 0
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError("invalid truth value %r" % (val,))
|
|
72
|
+
|
|
73
|
+
|
|
64
74
|
class MiscHelpers:
|
|
65
75
|
@staticmethod
|
|
66
76
|
def int_num_digits(num):
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
import os, sys
|
|
16
16
|
from collections import namedtuple
|
|
17
17
|
from collections.abc import Iterable
|
|
18
|
-
from
|
|
18
|
+
from batchmp.commons.utils import strtobool
|
|
19
19
|
import pygtrie
|
|
20
20
|
from batchmp.fstools.walker import DWalker
|
|
21
21
|
from batchmp.fstools.fsutils import FSH
|
|
22
22
|
from batchmp.fstools.builders.fsentry import FSEntry, FSEntryType, FSEntryDefaults
|
|
23
23
|
from batchmp.fstools.builders.fsprms import FSEntryParamsExt, FSEntryParamsOrganize
|
|
24
24
|
from batchmp.commons.progressbar import progress_bar, CmdProgressBarRefreshRate
|
|
25
|
+
from batchmp.fstools.virtual_organizer import VirtualOrganizer
|
|
25
26
|
# from profilehooks import profile
|
|
26
27
|
|
|
27
28
|
|
|
@@ -238,7 +239,6 @@ class DHandler:
|
|
|
238
239
|
def rename_entries(fs_entry_params,
|
|
239
240
|
num_entries = 0,
|
|
240
241
|
formatter = None, check_unique = True):
|
|
241
|
-
|
|
242
242
|
""" Renames directory entries via applying formatter function supplied by the caller
|
|
243
243
|
"""
|
|
244
244
|
if not formatter or num_entries <= 0:
|
|
@@ -283,7 +283,6 @@ class DHandler:
|
|
|
283
283
|
|
|
284
284
|
@staticmethod
|
|
285
285
|
def remove_entries(fs_entry_params, formatter = None):
|
|
286
|
-
|
|
287
286
|
""" Removes entries with formatter function supplied by the caller
|
|
288
287
|
"""
|
|
289
288
|
if not formatter:
|
|
@@ -321,76 +320,26 @@ class DHandler:
|
|
|
321
320
|
def organize(fs_entry_params):
|
|
322
321
|
""" Organizes files into subdirectories based on specified attributes
|
|
323
322
|
"""
|
|
324
|
-
# Build a trie of the target directory structure
|
|
325
|
-
dir_trie = pygtrie.StringTrie(separator=os.path.sep)
|
|
326
|
-
entries_to_process = list(DWalker.entries(fs_entry_params))
|
|
327
|
-
fcnt = 0
|
|
328
|
-
for entry in entries_to_process:
|
|
329
|
-
if entry.type == FSEntryType.FILE and hasattr(entry, 'target_path'):
|
|
330
|
-
fcnt += 1
|
|
331
|
-
target_dir = os.path.dirname(entry.target_path)
|
|
332
|
-
if not dir_trie.has_key(target_dir):
|
|
333
|
-
dir_trie[target_dir] = []
|
|
334
|
-
dir_trie[target_dir].append(entry)
|
|
335
323
|
|
|
336
|
-
|
|
324
|
+
# Create and configure the virtual organizer
|
|
325
|
+
organizer = VirtualOrganizer(fs_entry_params)
|
|
326
|
+
|
|
327
|
+
# Build the virtual structure
|
|
328
|
+
if not organizer.build_virtual_structure():
|
|
337
329
|
print("Nothing to process")
|
|
338
330
|
return
|
|
339
|
-
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if rel_path == '.': continue # Skip files that stay in root
|
|
347
|
-
|
|
348
|
-
# Build nested tree structure
|
|
349
|
-
parts = rel_path.split(os.path.sep)
|
|
350
|
-
node = tree
|
|
351
|
-
for part in parts:
|
|
352
|
-
node = node.setdefault(part, {})
|
|
353
|
-
node['__files__'] = [f.basename for f in files]
|
|
354
|
-
|
|
355
|
-
# Recursive function to yield all levels of the tree
|
|
356
|
-
def walk_tree(current_path, subtree):
|
|
357
|
-
# Get directories and files at current level
|
|
358
|
-
subdirs = sorted([k for k in subtree.keys() if k != '__files__'])
|
|
359
|
-
files = sorted(subtree.get('__files__', []))
|
|
360
|
-
|
|
361
|
-
# Yield current directory
|
|
362
|
-
yield current_path, subdirs, files
|
|
363
|
-
|
|
364
|
-
# Recursively yield subdirectories
|
|
365
|
-
for subdir in subdirs:
|
|
366
|
-
subdir_path = os.path.join(current_path, subdir)
|
|
367
|
-
yield from walk_tree(subdir_path, subtree[subdir])
|
|
368
|
-
|
|
369
|
-
# Start walking from root
|
|
370
|
-
yield from walk_tree(root_dir, tree)
|
|
331
|
+
|
|
332
|
+
# Get components for organize preview
|
|
333
|
+
virtual_walker = organizer.organize_virtual_walker()
|
|
334
|
+
max_depth = organizer.max_directory_depth()
|
|
335
|
+
preview_params = organizer.organize_preview_params(max_depth)
|
|
336
|
+
entries_to_process = list(DWalker.entries(fs_entry_params))
|
|
337
|
+
fcnt = sum(1 for entry in entries_to_process if entry.type == FSEntryType.FILE and hasattr(entry, 'target_path'))
|
|
371
338
|
|
|
372
339
|
# Visualize the changes
|
|
373
340
|
if fs_entry_params.quiet:
|
|
374
341
|
proceed = True
|
|
375
342
|
else:
|
|
376
|
-
# Calculate required depth based on organization structure
|
|
377
|
-
max_depth = 0
|
|
378
|
-
for target_path in dir_trie.keys():
|
|
379
|
-
rel_path = os.path.relpath(target_path, fs_entry_params.src_dir)
|
|
380
|
-
if rel_path != '.':
|
|
381
|
-
depth = len(rel_path.split(os.path.sep))
|
|
382
|
-
max_depth = max(max_depth, depth)
|
|
383
|
-
|
|
384
|
-
# Create preview parameters directly
|
|
385
|
-
preview_params = FSEntryParamsOrganize({
|
|
386
|
-
'all_files': True,
|
|
387
|
-
'all_dirs': True,
|
|
388
|
-
'end_level': max_depth
|
|
389
|
-
})
|
|
390
|
-
preview_params.src_dir = fs_entry_params.src_dir # Explicitly set src_dir
|
|
391
|
-
# Override builder for preview
|
|
392
|
-
from batchmp.fstools.builders.fsb import FSEntryBuilderOrganize
|
|
393
|
-
preview_params.__dict__['fs_entry_builder'] = FSEntryBuilderOrganize()
|
|
394
343
|
proceed, _, _ = DHandler.visualise_changes(preview_params, virtual_walker)
|
|
395
344
|
|
|
396
345
|
if proceed and fcnt > 0:
|
|
@@ -417,133 +366,24 @@ class DHandler:
|
|
|
417
366
|
def print_organized_view(fs_entry_params):
|
|
418
367
|
""" Print hierarchical organized-like virtual view
|
|
419
368
|
"""
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
fcnt = 0
|
|
424
|
-
|
|
425
|
-
for entry in entries_to_process:
|
|
426
|
-
if entry.type == FSEntryType.FILE and hasattr(entry, 'target_path'):
|
|
427
|
-
fcnt += 1
|
|
428
|
-
target_dir = os.path.dirname(entry.target_path)
|
|
429
|
-
if not dir_trie.has_key(target_dir):
|
|
430
|
-
dir_trie[target_dir] = []
|
|
431
|
-
dir_trie[target_dir].append(entry)
|
|
369
|
+
|
|
370
|
+
# Create and configure the virtual organizer
|
|
371
|
+
organizer = VirtualOrganizer(fs_entry_params)
|
|
432
372
|
|
|
433
|
-
|
|
373
|
+
# Build the virtual structure
|
|
374
|
+
if not organizer.build_virtual_structure():
|
|
434
375
|
print("No files to organize view")
|
|
435
376
|
return
|
|
436
377
|
|
|
437
|
-
#
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
rel_path = os.path.relpath(target_path, root_dir)
|
|
443
|
-
if rel_path == '.':
|
|
444
|
-
# Files that stay in root
|
|
445
|
-
tree.setdefault('__files__', []).extend([f.basename for f in files])
|
|
446
|
-
continue
|
|
447
|
-
|
|
448
|
-
# Build nested tree structure
|
|
449
|
-
parts = rel_path.split(os.path.sep)
|
|
450
|
-
node = tree
|
|
451
|
-
for part in parts:
|
|
452
|
-
node = node.setdefault(part, {})
|
|
453
|
-
node['__files__'] = [f.basename for f in files]
|
|
454
|
-
|
|
455
|
-
# Recursive function to yield all levels of the tree
|
|
456
|
-
def walk_tree(current_path, subtree):
|
|
457
|
-
# Get directories and files at current level
|
|
458
|
-
subdirs = sorted([k for k in subtree.keys() if k != '__files__'])
|
|
459
|
-
files = sorted(subtree.get('__files__', []))
|
|
460
|
-
|
|
461
|
-
# Yield current directory
|
|
462
|
-
yield current_path, subdirs, files
|
|
463
|
-
|
|
464
|
-
# Recursively yield subdirectories
|
|
465
|
-
for subdir in subdirs:
|
|
466
|
-
subdir_path = os.path.join(current_path, subdir)
|
|
467
|
-
yield from walk_tree(subdir_path, subtree[subdir])
|
|
468
|
-
|
|
469
|
-
# Start walking from root
|
|
470
|
-
yield from walk_tree(root_dir, tree)
|
|
471
|
-
|
|
472
|
-
# Calculate required depth based on organization structure
|
|
473
|
-
max_depth = 0
|
|
474
|
-
for target_path in dir_trie.keys():
|
|
475
|
-
rel_path = os.path.relpath(target_path, fs_entry_params.src_dir)
|
|
476
|
-
if rel_path != '.':
|
|
477
|
-
depth = len(rel_path.split(os.path.sep))
|
|
478
|
-
max_depth = max(max_depth, depth)
|
|
479
|
-
|
|
480
|
-
# Pre-calculate directory sizes by aggregating file sizes
|
|
481
|
-
dir_sizes = {}
|
|
482
|
-
if fs_entry_params.show_size:
|
|
483
|
-
for target_path, files in dir_trie.items():
|
|
484
|
-
total_size = 0
|
|
485
|
-
for file_entry in files:
|
|
486
|
-
try:
|
|
487
|
-
fsize = os.path.getsize(file_entry.realpath)
|
|
488
|
-
total_size += fsize
|
|
489
|
-
except (OSError, IOError):
|
|
490
|
-
pass
|
|
491
|
-
dir_sizes[target_path] = total_size
|
|
492
|
-
|
|
493
|
-
# Create a custom formatter that shows sizes for both files and virtual dirs
|
|
494
|
-
def size_aware_formatter(entry):
|
|
495
|
-
if fs_entry_params.show_size:
|
|
496
|
-
if entry.type == FSEntryType.FILE:
|
|
497
|
-
# For files, show size from their real path (original file location)
|
|
498
|
-
# Find the original file in our file mapping
|
|
499
|
-
original_file = None
|
|
500
|
-
for target_path, files in dir_trie.items():
|
|
501
|
-
for file_entry in files:
|
|
502
|
-
if file_entry.basename == entry.basename:
|
|
503
|
-
original_file = file_entry
|
|
504
|
-
break
|
|
505
|
-
if original_file:
|
|
506
|
-
break
|
|
507
|
-
|
|
508
|
-
if original_file:
|
|
509
|
-
try:
|
|
510
|
-
fsize = os.path.getsize(original_file.realpath)
|
|
511
|
-
size_str = FSH.fs_size(fsize)
|
|
512
|
-
return f" {size_str} {entry.basename}"
|
|
513
|
-
except (OSError, IOError):
|
|
514
|
-
pass
|
|
515
|
-
|
|
516
|
-
elif entry.type == FSEntryType.DIR:
|
|
517
|
-
# For virtual directories, show aggregated size of contained files
|
|
518
|
-
# Find the corresponding target path for this virtual directory
|
|
519
|
-
virtual_path = entry.realpath
|
|
520
|
-
if virtual_path in dir_sizes:
|
|
521
|
-
total_size = dir_sizes[virtual_path]
|
|
522
|
-
if total_size > 0:
|
|
523
|
-
size_str = FSH.fs_size(total_size)
|
|
524
|
-
return f" {size_str} {entry.basename}"
|
|
525
|
-
|
|
526
|
-
# For directories without size info or when size not requested, just return basename
|
|
527
|
-
return entry.basename
|
|
528
|
-
|
|
529
|
-
# Create preview parameters
|
|
530
|
-
from batchmp.fstools.builders.fsprms import FSEntryParamsOrganize
|
|
531
|
-
from batchmp.fstools.builders.fsb import FSEntryBuilderOrganize
|
|
532
|
-
|
|
533
|
-
preview_params = FSEntryParamsOrganize({
|
|
534
|
-
'all_files': True,
|
|
535
|
-
'all_dirs': True,
|
|
536
|
-
'end_level': max_depth,
|
|
537
|
-
'show_size': False # We handle sizes with custom formatter
|
|
538
|
-
})
|
|
539
|
-
preview_params.src_dir = fs_entry_params.src_dir
|
|
540
|
-
|
|
541
|
-
# Override builder for preview
|
|
542
|
-
preview_params.__dict__['fs_entry_builder'] = FSEntryBuilderOrganize()
|
|
378
|
+
# Get components for virtual view
|
|
379
|
+
virtual_walker = organizer.print_virtual_walker()
|
|
380
|
+
size_formatter = organizer.print_formatter_with_sizes()
|
|
381
|
+
max_depth = organizer.max_directory_depth()
|
|
382
|
+
preview_params = organizer.print_preview_params(max_depth)
|
|
543
383
|
|
|
544
384
|
# Print the virtual view
|
|
545
385
|
print(f"Virtual view by {fs_entry_params.by}:")
|
|
546
|
-
DHandler.print_dir(preview_params, virtual_walker, formatter=
|
|
386
|
+
DHandler.print_dir(preview_params, virtual_walker, formatter=size_formatter)
|
|
547
387
|
|
|
548
388
|
|
|
549
389
|
|
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
import os
|
|
15
|
+
import pygtrie
|
|
16
|
+
from collections import namedtuple
|
|
17
|
+
from batchmp.fstools.walker import DWalker
|
|
18
|
+
from batchmp.fstools.builders.fsentry import FSEntryType
|
|
19
|
+
from batchmp.fstools.fsutils import FSH
|
|
20
|
+
from batchmp.fstools.builders.fsprms import FSEntryParamsOrganize
|
|
21
|
+
from batchmp.fstools.builders.fsb import FSEntryBuilderOrganize
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VirtualOrganizer:
|
|
25
|
+
""" Helper class for creating virtual organized views without moving files.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, fs_entry_params):
|
|
29
|
+
self.fs_entry_params = fs_entry_params
|
|
30
|
+
self.dir_trie = pygtrie.StringTrie(separator=os.path.sep)
|
|
31
|
+
self.dir_sizes = {}
|
|
32
|
+
|
|
33
|
+
def build_virtual_structure(self):
|
|
34
|
+
""" Build the virtual directory structure from FSEntry data.
|
|
35
|
+
"""
|
|
36
|
+
entries_to_process = list(DWalker.entries(self.fs_entry_params))
|
|
37
|
+
fcnt = 0
|
|
38
|
+
|
|
39
|
+
for entry in entries_to_process:
|
|
40
|
+
if entry.type == FSEntryType.FILE and hasattr(entry, 'target_path'):
|
|
41
|
+
fcnt += 1
|
|
42
|
+
target_dir = os.path.dirname(entry.target_path)
|
|
43
|
+
if not self.dir_trie.has_key(target_dir):
|
|
44
|
+
self.dir_trie[target_dir] = []
|
|
45
|
+
self.dir_trie[target_dir].append(entry)
|
|
46
|
+
|
|
47
|
+
if fcnt == 0:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
self._calculate_directory_sizes()
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def max_directory_depth(self):
|
|
54
|
+
""" Calculate required depth based on organization structure.
|
|
55
|
+
"""
|
|
56
|
+
max_depth = 0
|
|
57
|
+
for target_path in self.dir_trie.keys():
|
|
58
|
+
rel_path = os.path.relpath(target_path, self.fs_entry_params.src_dir)
|
|
59
|
+
if rel_path != '.':
|
|
60
|
+
depth = len(rel_path.split(os.path.sep))
|
|
61
|
+
max_depth = max(max_depth, depth)
|
|
62
|
+
return max_depth
|
|
63
|
+
|
|
64
|
+
def organize_virtual_walker(self):
|
|
65
|
+
""" Create walker for organize method with simple alphabetical sorting.
|
|
66
|
+
"""
|
|
67
|
+
root_dir = self.fs_entry_params.src_dir
|
|
68
|
+
|
|
69
|
+
def virtual_walker(root_dir):
|
|
70
|
+
# Build hierarchical tree structure from trie
|
|
71
|
+
tree = {}
|
|
72
|
+
for target_path, files in self.dir_trie.items():
|
|
73
|
+
rel_path = os.path.relpath(target_path, root_dir)
|
|
74
|
+
if rel_path == '.':
|
|
75
|
+
continue # Skip files that stay in root
|
|
76
|
+
|
|
77
|
+
# Build nested tree structure
|
|
78
|
+
parts = rel_path.split(os.path.sep)
|
|
79
|
+
node = tree
|
|
80
|
+
for part in parts:
|
|
81
|
+
node = node.setdefault(part, {})
|
|
82
|
+
node['__files__'] = [f.basename for f in files]
|
|
83
|
+
|
|
84
|
+
# Recursive function to yield all levels of the tree
|
|
85
|
+
def walk_tree(current_path, subtree):
|
|
86
|
+
# Get directories and files at current level (sorted alphabetically)
|
|
87
|
+
subdirs = sorted([k for k in subtree.keys() if k != '__files__'])
|
|
88
|
+
files = sorted(subtree.get('__files__', []))
|
|
89
|
+
|
|
90
|
+
# Yield current directory
|
|
91
|
+
yield current_path, subdirs, files
|
|
92
|
+
|
|
93
|
+
# Recursively yield subdirectories
|
|
94
|
+
for subdir in subdirs:
|
|
95
|
+
subdir_path = os.path.join(current_path, subdir)
|
|
96
|
+
yield from walk_tree(subdir_path, subtree[subdir])
|
|
97
|
+
|
|
98
|
+
# Start walking from root
|
|
99
|
+
yield from walk_tree(root_dir, tree)
|
|
100
|
+
|
|
101
|
+
return virtual_walker
|
|
102
|
+
|
|
103
|
+
def organize_preview_params(self, max_depth):
|
|
104
|
+
""" Create FSEntry parameters for organize preview with standard sorting.
|
|
105
|
+
"""
|
|
106
|
+
preview_params = FSEntryParamsOrganize({
|
|
107
|
+
'all_files': True,
|
|
108
|
+
'all_dirs': True,
|
|
109
|
+
'end_level': max_depth
|
|
110
|
+
})
|
|
111
|
+
preview_params.src_dir = self.fs_entry_params.src_dir
|
|
112
|
+
# Override builder for preview
|
|
113
|
+
preview_params.__dict__['fs_entry_builder'] = FSEntryBuilderOrganize()
|
|
114
|
+
return preview_params
|
|
115
|
+
|
|
116
|
+
def print_virtual_walker(self):
|
|
117
|
+
""" Create walker for print_organized_view with full sorting support.
|
|
118
|
+
"""
|
|
119
|
+
root_dir = self.fs_entry_params.src_dir
|
|
120
|
+
|
|
121
|
+
def virtual_walker(root_dir):
|
|
122
|
+
# Build hierarchical tree structure from trie
|
|
123
|
+
tree = {}
|
|
124
|
+
for target_path, files in self.dir_trie.items():
|
|
125
|
+
rel_path = os.path.relpath(target_path, root_dir)
|
|
126
|
+
if rel_path == '.':
|
|
127
|
+
# Files that stay in root
|
|
128
|
+
tree.setdefault('__files__', []).extend([f.basename for f in files])
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Build nested tree structure
|
|
132
|
+
parts = rel_path.split(os.path.sep)
|
|
133
|
+
node = tree
|
|
134
|
+
for part in parts:
|
|
135
|
+
node = node.setdefault(part, {})
|
|
136
|
+
node['__files__'] = [f.basename for f in files]
|
|
137
|
+
|
|
138
|
+
# Recursive function to yield all levels of the tree
|
|
139
|
+
def walk_tree(current_path, subtree):
|
|
140
|
+
# Get directories and files at current level
|
|
141
|
+
subdirs = [k for k in subtree.keys() if k != '__files__']
|
|
142
|
+
files = subtree.get('__files__', [])
|
|
143
|
+
|
|
144
|
+
# Apply custom sorting for size-based sorts (FSEntry can't handle virtual paths)
|
|
145
|
+
if self.fs_entry_params.by_size:
|
|
146
|
+
subdirs = self._sort_entries_by_size(subdirs, is_dir=True)
|
|
147
|
+
files = self._sort_entries_by_size(files, is_dir=False)
|
|
148
|
+
|
|
149
|
+
# Yield current directory (use copies to prevent DWalker from modifying our sorted lists)
|
|
150
|
+
yield current_path, subdirs.copy(), files.copy()
|
|
151
|
+
|
|
152
|
+
# Recursively yield subdirectories
|
|
153
|
+
for subdir in subdirs:
|
|
154
|
+
subdir_path = os.path.join(current_path, subdir)
|
|
155
|
+
yield from walk_tree(subdir_path, subtree[subdir])
|
|
156
|
+
|
|
157
|
+
# Start walking from root
|
|
158
|
+
yield from walk_tree(root_dir, tree)
|
|
159
|
+
|
|
160
|
+
return virtual_walker
|
|
161
|
+
|
|
162
|
+
def print_formatter_with_sizes(self):
|
|
163
|
+
""" Create formatter for print_organized_view that shows sizes for both files and virtual dirs.
|
|
164
|
+
"""
|
|
165
|
+
def size_aware_formatter(entry):
|
|
166
|
+
if self.fs_entry_params.show_size:
|
|
167
|
+
if entry.type == FSEntryType.FILE:
|
|
168
|
+
# For files, show size from their real path (original file location)
|
|
169
|
+
# Find the original file in our file mapping
|
|
170
|
+
original_file = None
|
|
171
|
+
for target_path, files in self.dir_trie.items():
|
|
172
|
+
for file_entry in files:
|
|
173
|
+
if file_entry.basename == entry.basename:
|
|
174
|
+
original_file = file_entry
|
|
175
|
+
break
|
|
176
|
+
if original_file:
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if original_file:
|
|
180
|
+
try:
|
|
181
|
+
fsize = os.path.getsize(original_file.realpath)
|
|
182
|
+
size_str = FSH.fs_size(fsize)
|
|
183
|
+
return f" {size_str} {entry.basename}"
|
|
184
|
+
except (OSError, IOError):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
elif entry.type == FSEntryType.DIR:
|
|
188
|
+
# For virtual directories, show aggregated size of contained files
|
|
189
|
+
# Find the corresponding target path for this virtual directory
|
|
190
|
+
virtual_path = entry.realpath
|
|
191
|
+
if virtual_path in self.dir_sizes:
|
|
192
|
+
total_size = self.dir_sizes[virtual_path]
|
|
193
|
+
if total_size > 0:
|
|
194
|
+
size_str = FSH.fs_size(total_size)
|
|
195
|
+
return f" {size_str} {entry.basename}"
|
|
196
|
+
|
|
197
|
+
# For directories without size info or when size not requested, just return basename
|
|
198
|
+
return entry.basename
|
|
199
|
+
|
|
200
|
+
return size_aware_formatter
|
|
201
|
+
|
|
202
|
+
def print_preview_params(self, max_depth):
|
|
203
|
+
""" Create FSEntry parameters for print_organized_view with advanced sorting.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
# For size-based sorting, we need to completely bypass FSEntry sorting
|
|
207
|
+
# since it tries to access virtual file paths that don't exist
|
|
208
|
+
if self.fs_entry_params.by_size:
|
|
209
|
+
# Create a custom FSEntry class that bypasses sorting
|
|
210
|
+
class NoSortFSEntryParamsOrganize(FSEntryParamsOrganize):
|
|
211
|
+
class NoSortFilesDescriptor:
|
|
212
|
+
def __set__(self, instance, value):
|
|
213
|
+
# Just store the files without any sorting or filtering
|
|
214
|
+
# The virtual walker already sorted them correctly
|
|
215
|
+
instance._fnames = value
|
|
216
|
+
def __get__(self, instance, owner):
|
|
217
|
+
return getattr(instance, '_fnames', [])
|
|
218
|
+
|
|
219
|
+
class NoSortDirsDescriptor:
|
|
220
|
+
def __set__(self, instance, value):
|
|
221
|
+
# Just store the dirs without any sorting or filtering
|
|
222
|
+
# The virtual walker already sorted them correctly
|
|
223
|
+
DNames = namedtuple('DNames', ['passed', 'enclosing'])
|
|
224
|
+
instance._dnames = DNames(value, [])
|
|
225
|
+
def __get__(self, instance, owner):
|
|
226
|
+
DNames = namedtuple('DNames', ['passed', 'enclosing'])
|
|
227
|
+
return getattr(instance, '_dnames', DNames([], []))
|
|
228
|
+
|
|
229
|
+
# Override the descriptors to bypass sorting
|
|
230
|
+
fnames = NoSortFilesDescriptor()
|
|
231
|
+
dnames = NoSortDirsDescriptor()
|
|
232
|
+
|
|
233
|
+
preview_params = NoSortFSEntryParamsOrganize({
|
|
234
|
+
'all_files': True,
|
|
235
|
+
'all_dirs': True,
|
|
236
|
+
'end_level': max_depth,
|
|
237
|
+
'show_size': False # We handle sizes with custom formatter
|
|
238
|
+
})
|
|
239
|
+
preview_params.src_dir = self.fs_entry_params.src_dir
|
|
240
|
+
preview_params.sort = 'na' # Won't be used due to custom descriptors
|
|
241
|
+
else:
|
|
242
|
+
# For name-based sorting, let FSEntry handle it normally
|
|
243
|
+
preview_params = FSEntryParamsOrganize({
|
|
244
|
+
'all_files': True,
|
|
245
|
+
'all_dirs': True,
|
|
246
|
+
'end_level': max_depth,
|
|
247
|
+
'show_size': False # We handle sizes with custom formatter
|
|
248
|
+
})
|
|
249
|
+
preview_params.src_dir = self.fs_entry_params.src_dir
|
|
250
|
+
preview_params.sort = self.fs_entry_params.sort
|
|
251
|
+
|
|
252
|
+
# Override builder for preview
|
|
253
|
+
preview_params.__dict__['fs_entry_builder'] = FSEntryBuilderOrganize()
|
|
254
|
+
return preview_params
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
## Helpers
|
|
258
|
+
def _calculate_directory_sizes(self):
|
|
259
|
+
""" Pre-calculate directory sizes by aggregating file sizes.
|
|
260
|
+
"""
|
|
261
|
+
if self.fs_entry_params.show_size:
|
|
262
|
+
for target_path, files in self.dir_trie.items():
|
|
263
|
+
total_size = 0
|
|
264
|
+
for file_entry in files:
|
|
265
|
+
try:
|
|
266
|
+
fsize = os.path.getsize(file_entry.realpath)
|
|
267
|
+
total_size += fsize
|
|
268
|
+
except (OSError, IOError):
|
|
269
|
+
pass
|
|
270
|
+
self.dir_sizes[target_path] = total_size
|
|
271
|
+
|
|
272
|
+
def _sort_entries_by_size(self, items, is_dir):
|
|
273
|
+
""" Sort items by size when FSEntry sorting would fail due to virtual paths.
|
|
274
|
+
"""
|
|
275
|
+
if not items:
|
|
276
|
+
return items
|
|
277
|
+
|
|
278
|
+
if is_dir:
|
|
279
|
+
# For directories, use aggregated sizes from dir_sizes
|
|
280
|
+
def size_key(dirname):
|
|
281
|
+
# Find the virtual directory path that corresponds to this dirname
|
|
282
|
+
for target_path in self.dir_sizes.keys():
|
|
283
|
+
if target_path.endswith(os.sep + dirname) or target_path.endswith(dirname):
|
|
284
|
+
return self.dir_sizes.get(target_path, 0)
|
|
285
|
+
return 0
|
|
286
|
+
sort_key = size_key
|
|
287
|
+
else:
|
|
288
|
+
# For files, look up original file size from dir_trie
|
|
289
|
+
def file_size_key(filename):
|
|
290
|
+
# Find the file entry in dir_trie to get its real path
|
|
291
|
+
for target_path, files in self.dir_trie.items():
|
|
292
|
+
for file_entry in files:
|
|
293
|
+
if file_entry.basename == filename:
|
|
294
|
+
try:
|
|
295
|
+
return os.path.getsize(file_entry.realpath)
|
|
296
|
+
except (OSError, IOError):
|
|
297
|
+
return 0
|
|
298
|
+
return 0
|
|
299
|
+
sort_key = file_size_key
|
|
300
|
+
|
|
301
|
+
return sorted(items, key=sort_key, reverse=self.fs_entry_params.descending)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchmp
|
|
3
|
-
Version: 1.4
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Command-line tools for batch media processing
|
|
5
5
|
Home-page: https://github.com/akpw/batch-mp-tools
|
|
6
6
|
Author: Arseniy Kuznetsov
|
|
@@ -29,6 +29,9 @@ Requires-Dist: mutagen>=1.27
|
|
|
29
29
|
Requires-Dist: pygtrie>=2.3.2
|
|
30
30
|
Requires-Dist: filetype>=1.0.7
|
|
31
31
|
Requires-Dist: mediafile>=0.13.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
32
35
|
Dynamic: author
|
|
33
36
|
Dynamic: author-email
|
|
34
37
|
Dynamic: classifier
|
|
@@ -38,6 +41,7 @@ Dynamic: home-page
|
|
|
38
41
|
Dynamic: keywords
|
|
39
42
|
Dynamic: license
|
|
40
43
|
Dynamic: license-file
|
|
44
|
+
Dynamic: provides-extra
|
|
41
45
|
Dynamic: requires-dist
|
|
42
46
|
Dynamic: summary
|
|
43
47
|
|
|
@@ -133,7 +137,10 @@ Or preview how files would look organized by date without moving them:
|
|
|
133
137
|
|- 02/
|
|
134
138
|
|- video.mp4
|
|
135
139
|
```
|
|
136
|
-
|
|
140
|
+
For more detailed examples and advanced organize / virtual views functionality, see:
|
|
141
|
+
- [Renamer Organize & Virtual Views](https://akpw.github.io/articles/2025/09/22/Print-and-Organize.html)
|
|
142
|
+
- [BatchMP Tools Tutorial, Part II: renaming files with renamer](https://akpw.github.io/articles/2015/04/11/batchmp-tutorial-part-ii.html)
|
|
143
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
137
144
|
|
|
138
145
|
|
|
139
146
|
[**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:
|
|
@@ -160,6 +167,11 @@ Or preview how files would look organized by date without moving them:
|
|
|
160
167
|
```
|
|
161
168
|
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
169
|
|
|
170
|
+
For more practical examples, see:
|
|
171
|
+
- [BatchMP Tools Tutorial Part III: setting tags and artwork with tagger](https://akpw.github.io/articles/2015/04/12/batchmp-tutorial-part-iii.html)
|
|
172
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
173
|
+
|
|
174
|
+
|
|
163
175
|
|
|
164
176
|
[**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
177
|
**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.
|
|
@@ -207,7 +219,10 @@ To check on the result, lets's just use the [tagger's](https://github.com/akpw/b
|
|
|
207
219
|
```
|
|
208
220
|
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
221
|
|
|
210
|
-
|
|
222
|
+
For more practical examples, see:
|
|
223
|
+
- [BatchMP Tools Tutorial, splitting a long media file with bmfp](https://akpw.github.io/articles/2015/04/10/batchmp-tutorial-part-i.html)
|
|
224
|
+
- Other related posts in [Practical BatchMP](https://akpw.github.io//tags.html#BatchMP+Tools)
|
|
225
|
+
|
|
211
226
|
|
|
212
227
|
|
|
213
228
|
## Brief Description of CLI Commands (use -h to expand on details for individual commands)
|
|
@@ -395,10 +410,23 @@ Support via FFmpeg: 'AVI', 'FLV', 'MKV', 'MKA'
|
|
|
395
410
|
|
|
396
411
|
|
|
397
412
|
## Installing Development version
|
|
398
|
-
- Clone the repo,
|
|
413
|
+
- Clone the repo, create a virtual environment, and activate it:
|
|
414
|
+
```bash
|
|
415
|
+
$ python3 -m venv .venv
|
|
416
|
+
$ source .venv/bin/activate
|
|
417
|
+
```
|
|
418
|
+
- Install the project in editable mode:
|
|
419
|
+
```bash
|
|
420
|
+
$ pip install -e .
|
|
421
|
+
```
|
|
399
422
|
|
|
400
423
|
## Running Tests
|
|
401
424
|
|
|
425
|
+
To run the test suite, first ensure you have installed the development dependencies:
|
|
426
|
+
```bash
|
|
427
|
+
$ pip install -e ".[test]"
|
|
428
|
+
```
|
|
429
|
+
|
|
402
430
|
**Using pytest (recommended):**
|
|
403
431
|
```bash
|
|
404
432
|
$ pytest -v --tb=short # Run all tests with verbose output
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
|
+
pyproject.toml
|
|
3
4
|
setup.cfg
|
|
4
5
|
setup.py
|
|
5
6
|
batchmp/__init__.py
|
|
@@ -52,6 +53,7 @@ batchmp/fstools/__init__.py
|
|
|
52
53
|
batchmp/fstools/dirtools.py
|
|
53
54
|
batchmp/fstools/fsutils.py
|
|
54
55
|
batchmp/fstools/rename.py
|
|
56
|
+
batchmp/fstools/virtual_organizer.py
|
|
55
57
|
batchmp/fstools/walker.py
|
|
56
58
|
batchmp/fstools/builders/__init__.py
|
|
57
59
|
batchmp/fstools/builders/fsb.py
|
|
@@ -20,7 +20,7 @@ with open(path.join(pkg_dir, 'README.md'), encoding='utf-8') as f:
|
|
|
20
20
|
|
|
21
21
|
setup(
|
|
22
22
|
name='batchmp',
|
|
23
|
-
version='1.4',
|
|
23
|
+
version='1.4.2',
|
|
24
24
|
|
|
25
25
|
url='https://github.com/akpw/batch-mp-tools',
|
|
26
26
|
|
|
@@ -38,6 +38,13 @@ setup(
|
|
|
38
38
|
|
|
39
39
|
install_requires = ['mutagen>=1.27', 'pygtrie>=2.3.2', 'filetype>=1.0.7', 'mediafile>=0.13.0'], ##, 'profilehooks>=1.11.0'],
|
|
40
40
|
|
|
41
|
+
extras_require={
|
|
42
|
+
'test': [
|
|
43
|
+
'pytest',
|
|
44
|
+
'pytest-mock',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
|
|
41
48
|
entry_points={'console_scripts': [
|
|
42
49
|
'batchmp = batchmp.cli.base.bmp_dispatch:main',
|
|
43
50
|
'renamer = batchmp.cli.renamer.renamer_dispatch:main',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|