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,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
|
+
|