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.
Files changed (68) hide show
  1. batchmp/__init__.py +0 -0
  2. batchmp/cli/__init__.py +0 -0
  3. batchmp/cli/base/__init__.py +0 -0
  4. batchmp/cli/base/bmp_dispatch.py +60 -0
  5. batchmp/cli/base/bmp_options.py +349 -0
  6. batchmp/cli/base/vchk.py +47 -0
  7. batchmp/cli/bmfp/__init__.py +0 -0
  8. batchmp/cli/bmfp/bmfp_dispatch.py +120 -0
  9. batchmp/cli/bmfp/bmfp_options.py +442 -0
  10. batchmp/cli/renamer/__init__.py +0 -0
  11. batchmp/cli/renamer/renamer_dispatch.py +135 -0
  12. batchmp/cli/renamer/renamer_options.py +355 -0
  13. batchmp/cli/tagger/__init__.py +0 -0
  14. batchmp/cli/tagger/tagger_dispatch.py +143 -0
  15. batchmp/cli/tagger/tagger_options.py +338 -0
  16. batchmp/commons/__init__.py +0 -0
  17. batchmp/commons/chainedhandler.py +102 -0
  18. batchmp/commons/descriptors.py +173 -0
  19. batchmp/commons/progressbar.py +154 -0
  20. batchmp/commons/taskprocessor.py +149 -0
  21. batchmp/commons/utils.py +194 -0
  22. batchmp/ffmptools/__init__.py +0 -0
  23. batchmp/ffmptools/ffcommands/__init__.py +0 -0
  24. batchmp/ffmptools/ffcommands/cmdopt.py +115 -0
  25. batchmp/ffmptools/ffcommands/convert.py +130 -0
  26. batchmp/ffmptools/ffcommands/cuesplit.py +223 -0
  27. batchmp/ffmptools/ffcommands/denoise.py +173 -0
  28. batchmp/ffmptools/ffcommands/fragment.py +121 -0
  29. batchmp/ffmptools/ffcommands/normalize_peak.py +135 -0
  30. batchmp/ffmptools/ffcommands/segment.py +157 -0
  31. batchmp/ffmptools/ffcommands/silencesplit.py +159 -0
  32. batchmp/ffmptools/ffrunner.py +189 -0
  33. batchmp/ffmptools/ffutils.py +300 -0
  34. batchmp/ffmptools/processors/__init__.py +0 -0
  35. batchmp/ffmptools/processors/basefp.py +92 -0
  36. batchmp/ffmptools/processors/ffentry.py +81 -0
  37. batchmp/ffmptools/utils/__init__.py +0 -0
  38. batchmp/ffmptools/utils/cueparse.py +227 -0
  39. batchmp/ffmptools/utils/cuesheet.py +239 -0
  40. batchmp/fstools/__init__.py +0 -0
  41. batchmp/fstools/builders/__init__.py +0 -0
  42. batchmp/fstools/builders/fsb.py +221 -0
  43. batchmp/fstools/builders/fsentry.py +60 -0
  44. batchmp/fstools/builders/fsprms.py +372 -0
  45. batchmp/fstools/dirtools.py +549 -0
  46. batchmp/fstools/fsutils.py +272 -0
  47. batchmp/fstools/rename.py +390 -0
  48. batchmp/fstools/walker.py +79 -0
  49. batchmp/tags/__init__.py +0 -0
  50. batchmp/tags/handlers/__init__.py +0 -0
  51. batchmp/tags/handlers/basehandler.py +99 -0
  52. batchmp/tags/handlers/ffmphandler.py +75 -0
  53. batchmp/tags/handlers/ffmphandlers/__init__.py +0 -0
  54. batchmp/tags/handlers/ffmphandlers/base.py +243 -0
  55. batchmp/tags/handlers/mtghandler.py +56 -0
  56. batchmp/tags/handlers/pmhandler.py +36 -0
  57. batchmp/tags/handlers/tagsholder.py +264 -0
  58. batchmp/tags/output/__init__.py +0 -0
  59. batchmp/tags/output/formatters.py +218 -0
  60. batchmp/tags/processors/__init__.py +0 -0
  61. batchmp/tags/processors/basetp.py +266 -0
  62. batchmp-1.4.dist-info/METADATA +422 -0
  63. batchmp-1.4.dist-info/RECORD +68 -0
  64. batchmp-1.4.dist-info/WHEEL +5 -0
  65. batchmp-1.4.dist-info/entry_points.txt +5 -0
  66. batchmp-1.4.dist-info/licenses/LICENSE +11 -0
  67. batchmp-1.4.dist-info/top_level.txt +1 -0
  68. batchmp-1.4.dist-info/zip-safe +1 -0
@@ -0,0 +1,549 @@
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 os, sys
16
+ from collections import namedtuple
17
+ from collections.abc import Iterable
18
+ from distutils.util import strtobool
19
+ import pygtrie
20
+ from batchmp.fstools.walker import DWalker
21
+ from batchmp.fstools.fsutils import FSH
22
+ from batchmp.fstools.builders.fsentry import FSEntry, FSEntryType, FSEntryDefaults
23
+ from batchmp.fstools.builders.fsprms import FSEntryParamsExt, FSEntryParamsOrganize
24
+ from batchmp.commons.progressbar import progress_bar, CmdProgressBarRefreshRate
25
+ # from profilehooks import profile
26
+
27
+
28
+ class DHandler:
29
+ ''' FS Directory level utilities
30
+ '''
31
+ @staticmethod
32
+ # @profile
33
+ def print_dir(fs_entry_params, walker=os.walk, formatter = None, selected_files_description = None):
34
+ """ Prints content of given directory
35
+ Supports additional display name processing via formatter supplied by the caller
36
+ """
37
+ if not os.path.exists(fs_entry_params.src_dir):
38
+ raise ValueError('Not a valid path')
39
+
40
+ if formatter is None:
41
+ formatter = lambda entry: entry.basename
42
+
43
+ if selected_files_description is None:
44
+ selected_files_description = 'file'
45
+
46
+ # print the dir tree
47
+ fcnt = dcnt = 0
48
+ total_size = 0
49
+ shared_cache = {}
50
+
51
+ for entry in DWalker.entries(fs_entry_params, walker):
52
+ # get formatted output
53
+ formatted_output = ''
54
+ if isinstance(formatter, Iterable):
55
+ for chained_formatter in formatter:
56
+ chained_formatter_output = chained_formatter(entry)
57
+ formatted_output = '{0}{1}'.format(
58
+ formatted_output if formatted_output else '',
59
+ chained_formatter_output if chained_formatter_output else '')
60
+ else:
61
+ formatted_output = formatter(entry)
62
+
63
+ if formatted_output:
64
+ size = ''
65
+ if entry.type == FSEntryType.FILE:
66
+ fcnt += 1
67
+ if fs_entry_params.show_size:
68
+ fsize = os.path.getsize(entry.realpath)
69
+ size = ' {} '.format(FSH.fs_size(fsize))
70
+ total_size += fsize
71
+ elif entry.type == FSEntryType.DIR and not entry.isEnclosingEntry:
72
+ dcnt += 1
73
+ if fs_entry_params.show_size:
74
+ display_size = FSH.dir_size(entry.realpath, shared_cache = shared_cache)
75
+ size = ' {} '.format(FSH.fs_size(display_size))
76
+
77
+ if FSH.level_from_root(fs_entry_params.src_dir, entry.realpath) <= fs_entry_params.end_level:
78
+ dsize = os.path.getsize(entry.realpath)
79
+ else:
80
+ dsize = display_size
81
+
82
+ total_size += dsize
83
+
84
+ print('{0}{1}{2}'.format(entry.indent, size, formatted_output))
85
+
86
+ # print summary
87
+ print('{0} {1}{2}, {3} folder{4}'.format(fcnt,
88
+ selected_files_description, '' if fcnt == 1 else 's',
89
+ dcnt, '' if dcnt == 1 else 's'))
90
+
91
+ if fs_entry_params.show_size and total_size > 0:
92
+ print('Total selected entries size: {}'.format(FSH.fs_size(total_size)))
93
+
94
+ return fcnt, dcnt
95
+
96
+ @staticmethod
97
+ def stats(fs_entry_params):
98
+ print('Overall directory statistics might take a while...')
99
+ print(fs_entry_params.src_dir)
100
+
101
+ total_files, total_dirs, total_size = DHandler.dir_stats(fs_entry_params)
102
+
103
+ print('{0}Total files: {1}'.format(FSEntryDefaults.DEFAULT_NESTED_INDENT, total_files))
104
+ print('{0}Total directores: {1}'.format(FSEntryDefaults.DEFAULT_NESTED_INDENT, total_dirs))
105
+ if fs_entry_params.show_size:
106
+ print('{0}Total size: {1}'.format(FSEntryDefaults.DEFAULT_NESTED_INDENT, FSH.fs_size(total_size)))
107
+
108
+ @staticmethod
109
+ def dir_stats(fs_entry_params,
110
+ file_pass_filter = None, dir_pass_filter = None, break_on_filter = False):
111
+ """ Returns base stats for given directory
112
+ """
113
+ if not os.path.exists(fs_entry_params.src_dir):
114
+ raise ValueError('Not a valid path')
115
+
116
+ # count number of files, folders, and their total size
117
+ shared_cache = {}
118
+ fcnt = dcnt = total_size = 0
119
+ for entry in DWalker.entries(fs_entry_params):
120
+ if entry.type == FSEntryType.FILE:
121
+ if file_pass_filter and (not file_pass_filter(entry)):
122
+ if break_on_filter:
123
+ break
124
+ continue
125
+ fcnt += 1
126
+ if fs_entry_params.show_size:
127
+ total_size += os.path.getsize(entry.realpath)
128
+
129
+ elif entry.type == FSEntryType.DIR:
130
+ if entry.isEnclosingEntry:
131
+ continue
132
+ if dir_pass_filter and (not dir_pass_filter(entry)):
133
+ if break_on_filter:
134
+ break
135
+ continue
136
+
137
+ dcnt += 1
138
+ if fs_entry_params.show_size:
139
+ if FSH.level_from_root(fs_entry_params.src_dir, entry.realpath) <= fs_entry_params.end_level:
140
+ total_size += os.path.getsize(entry.realpath)
141
+ else:
142
+ total_size += FSH.dir_size(entry.realpath, shared_cache = shared_cache)
143
+
144
+ return fcnt, dcnt, total_size
145
+
146
+ @staticmethod
147
+ def get_user_input(quiet = False):
148
+ ''' Displays confirmation promt and gathers users' input
149
+ '''
150
+ answer = input('\nProceed? [y/n]: ')
151
+ try:
152
+ answer = True if strtobool(answer) else False
153
+ except ValueError:
154
+ print('Not confirmative, exiting')
155
+ return False
156
+
157
+ if not quiet:
158
+ if answer:
159
+ print('Confirmed, processing...')
160
+ else:
161
+ print('Not confirmed, exiting')
162
+
163
+ return answer
164
+
165
+ @staticmethod
166
+ def visualise_changes(fs_entry_params, walker=os.walk,
167
+ before_msg = 'Current source directory:',
168
+ after_msg = 'Targeted after processing:',
169
+ preformatter = None, formatter = None, reset_formatters = None,
170
+ selected_files_description = None, fs_preprocess_entry_params = None):
171
+
172
+ ''' Displays targeted changes and gets users' confirmation on futher processing
173
+ '''
174
+ if not fs_preprocess_entry_params:
175
+ fs_preprocess_entry_params = fs_entry_params
176
+
177
+ if fs_preprocess_entry_params.display_current:
178
+ print(before_msg)
179
+ DHandler.print_dir(fs_preprocess_entry_params, os.walk,
180
+ formatter = preformatter,
181
+ selected_files_description = selected_files_description)
182
+ if reset_formatters:
183
+ reset_formatters()
184
+ print()
185
+
186
+ print(after_msg)
187
+ fcnt, dcnt = DHandler.print_dir(fs_entry_params, walker,
188
+ formatter = formatter,
189
+ selected_files_description = selected_files_description)
190
+ if fcnt == dcnt == 0:
191
+ print ('Nothing to process')
192
+ return False, fcnt, dcnt
193
+ else:
194
+ return DHandler.get_user_input(), fcnt, dcnt
195
+
196
+ @staticmethod
197
+ def flatten_folders(ff_entry_params,
198
+ remove_folders = True, remove_non_empty_folders = False):
199
+ ''' Flattens all folders below target level, moving the files up at the target level
200
+ '''
201
+ fs_preprocess_entry_params = FSEntryParamsExt()
202
+ fs_preprocess_entry_params.copy_params(ff_entry_params)
203
+
204
+ if ff_entry_params.quiet:
205
+ proceed = True
206
+ else:
207
+ proceed, _, _ = DHandler.visualise_changes(ff_entry_params, fs_preprocess_entry_params = fs_preprocess_entry_params)
208
+
209
+ if proceed:
210
+ # OK to go
211
+ flattened_dirs_cnt = flattened_files_cnt = 0
212
+ target_dir_path = ''
213
+ for entry in DWalker.entries(ff_entry_params):
214
+ if entry.type in (FSEntryType.DIR, FSEntryType.ROOT):
215
+ if FSH.level_from_root(ff_entry_params.src_dir, entry.realpath) == ff_entry_params.target_level:
216
+ target_dir_path = entry.realpath
217
+ else:
218
+ # files
219
+ if target_dir_path and (FSH.level_from_root(ff_entry_params.src_dir, entry.realpath) - 1 > ff_entry_params.target_level):
220
+ target_fpath = os.path.join(target_dir_path, entry.basename)
221
+ if FSH.move_FS_entry(entry.realpath, target_fpath):
222
+ flattened_files_cnt += 1
223
+
224
+ # remove excessive folders
225
+ if ff_entry_params.remove_folders:
226
+ flattened_dirs_cnt = FSH.remove_folders_below_target_level(ff_entry_params.src_dir,
227
+ target_level = ff_entry_params.target_level,
228
+ empty_only = not ff_entry_params.remove_non_empty_folders,
229
+ non_empty_msg = ff_entry_params.non_empty_folders_mgs)
230
+ # print summary
231
+ if not ff_entry_params.quiet:
232
+ print('Flattened: {0} files, {1} folders'.format(flattened_files_cnt, flattened_dirs_cnt))
233
+
234
+ if not ff_entry_params.quiet:
235
+ print('\nDone')
236
+
237
+ @staticmethod
238
+ def rename_entries(fs_entry_params,
239
+ num_entries = 0,
240
+ formatter = None, check_unique = True):
241
+
242
+ """ Renames directory entries via applying formatter function supplied by the caller
243
+ """
244
+ if not formatter or num_entries <= 0:
245
+ return
246
+
247
+ fcnt = dcnt = 0
248
+ DirEntry = namedtuple('DirEntry', ['orig_path', 'target_path'])
249
+ dir_entries = []
250
+
251
+ with progress_bar(refresh_rate = CmdProgressBarRefreshRate.FAST) as p_bar:
252
+ p_bar.info_msg = 'Renaming {} entries'.format(num_entries)
253
+
254
+ for entry in DWalker.entries(fs_entry_params):
255
+ if entry.type == FSEntryType.ROOT:
256
+ continue
257
+
258
+ target_name = formatter(entry)
259
+ if target_name == entry.basename:
260
+ continue
261
+
262
+ target_path = os.path.join(os.path.dirname(entry.realpath), target_name)
263
+
264
+ if entry.type == FSEntryType.DIR:
265
+ # for dirs, need to postpone
266
+ dir_entries.append(DirEntry(entry.realpath, target_path))
267
+
268
+ elif entry.type == FSEntryType.FILE:
269
+ # for files, just rename
270
+ if FSH.move_FS_entry(entry.realpath, target_path, check_unique = check_unique):
271
+ fcnt += 1
272
+
273
+ p_bar.progress += 100 / num_entries
274
+
275
+ #rename the dirs
276
+ for dir_entry in reversed(dir_entries):
277
+ if FSH.move_FS_entry(dir_entry.orig_path, dir_entry.target_path, check_unique = check_unique):
278
+ dcnt += 1
279
+
280
+ # print summary
281
+ if not fs_entry_params.quiet:
282
+ print('Renamed: {0} files, {1} folders'.format(fcnt, dcnt))
283
+
284
+ @staticmethod
285
+ def remove_entries(fs_entry_params, formatter = None):
286
+
287
+ """ Removes entries with formatter function supplied by the caller
288
+ """
289
+ if not formatter:
290
+ return
291
+
292
+ fcnt = dcnt = 0
293
+ dir_entries = []
294
+ for entry in DWalker.entries(fs_entry_params):
295
+
296
+ if entry.type == FSEntryType.ROOT or (entry.type == FSEntryType.DIR and entry.isEnclosingEntry):
297
+ continue
298
+
299
+ if formatter(entry) is None:
300
+ continue
301
+
302
+ if entry.type == FSEntryType.DIR:
303
+ # for dirs, need to postpone
304
+ dir_entries.append(entry.realpath)
305
+
306
+ elif entry.type == FSEntryType.FILE:
307
+ # for files, OK to remove now
308
+ FSH.remove_FS_entry(entry.realpath)
309
+ fcnt += 1
310
+
311
+ #rename the dirs
312
+ for dir_entry in reversed(dir_entries):
313
+ FSH.remove_FS_entry(dir_entry)
314
+ dcnt += 1
315
+
316
+ # print summary
317
+ if not fs_entry_params.quiet:
318
+ print('Removed: {0} files, {1} folders'.format(fcnt, dcnt))
319
+
320
+ @staticmethod
321
+ def organize(fs_entry_params):
322
+ """ Organizes files into subdirectories based on specified attributes
323
+ """
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
+
336
+ if fcnt == 0:
337
+ print("Nothing to process")
338
+ return
339
+
340
+ # Create a custom walker for the virtual tree preview
341
+ def virtual_walker(root_dir):
342
+ # Build hierarchical tree structure from trie
343
+ tree = {}
344
+ for target_path, files in dir_trie.items():
345
+ rel_path = os.path.relpath(target_path, root_dir)
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)
371
+
372
+ # Visualize the changes
373
+ if fs_entry_params.quiet:
374
+ proceed = True
375
+ 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
+ proceed, _, _ = DHandler.visualise_changes(preview_params, virtual_walker)
395
+
396
+ if proceed and fcnt > 0:
397
+ moved_files_cnt = 0
398
+ with progress_bar(refresh_rate=CmdProgressBarRefreshRate.FAST) as p_bar:
399
+ p_bar.info_msg = f'Organizing {fcnt} files'
400
+ for entry in entries_to_process:
401
+ if entry.type == FSEntryType.FILE and hasattr(entry, 'target_path'):
402
+ target_dir = os.path.dirname(entry.target_path)
403
+ if not os.path.exists(target_dir):
404
+ os.makedirs(target_dir)
405
+ if FSH.move_FS_entry(entry.realpath, entry.target_path):
406
+ moved_files_cnt += 1
407
+ if fcnt > 0:
408
+ p_bar.progress += 100 / fcnt
409
+
410
+ if not fs_entry_params.quiet:
411
+ print(f'Organized: {moved_files_cnt} files')
412
+
413
+ if not fs_entry_params.quiet:
414
+ print('\\nDone')
415
+
416
+ @staticmethod
417
+ def print_organized_view(fs_entry_params):
418
+ """ Print hierarchical organized-like virtual view
419
+ """
420
+ # Build a trie of the virtual directory structure
421
+ dir_trie = pygtrie.StringTrie(separator=os.path.sep)
422
+ entries_to_process = list(DWalker.entries(fs_entry_params))
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)
432
+
433
+ if fcnt == 0:
434
+ print("No files to organize view")
435
+ return
436
+
437
+ # Create a custom walker for the virtual tree preview
438
+ def virtual_walker(root_dir):
439
+ # Build hierarchical tree structure from trie
440
+ tree = {}
441
+ for target_path, files in dir_trie.items():
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()
543
+
544
+ # Print the virtual view
545
+ print(f"Virtual view by {fs_entry_params.by}:")
546
+ DHandler.print_dir(preview_params, virtual_walker, formatter=size_aware_formatter)
547
+
548
+
549
+