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,272 @@
|
|
|
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, fnmatch, shutil
|
|
16
|
+
import hashlib
|
|
17
|
+
|
|
18
|
+
class UniqueDirNamesChecker:
|
|
19
|
+
''' Unique file names Helper
|
|
20
|
+
'''
|
|
21
|
+
def __init__(self, src_dir, *, unique_fnames = None):
|
|
22
|
+
self._uname_gen = unique_fnames() if unique_fnames else FSH.unique_fnames()
|
|
23
|
+
|
|
24
|
+
# init the generator function with file names from given source directory
|
|
25
|
+
fnames = [fname for fname in os.listdir(src_dir)]
|
|
26
|
+
#if os.path.isfile(os.path.join(src_dir, fname))]
|
|
27
|
+
for fname in fnames:
|
|
28
|
+
next(self._uname_gen)
|
|
29
|
+
self._uname_gen.send(fname)
|
|
30
|
+
|
|
31
|
+
def unique_name(self, fname):
|
|
32
|
+
''' Returns unique file name
|
|
33
|
+
'''
|
|
34
|
+
next(self._uname_gen)
|
|
35
|
+
return self._uname_gen.send(fname)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FSH:
|
|
39
|
+
''' FS helper utilities
|
|
40
|
+
'''
|
|
41
|
+
@staticmethod
|
|
42
|
+
def full_path(path, check_parent_path = False):
|
|
43
|
+
if path:
|
|
44
|
+
path = os.path.expanduser(path)
|
|
45
|
+
path = os.path.expandvars(path)
|
|
46
|
+
path = os.path.abspath(path)
|
|
47
|
+
path = os.path.realpath(path)
|
|
48
|
+
|
|
49
|
+
# for files, check that the parent dir exists
|
|
50
|
+
if check_parent_path:
|
|
51
|
+
if not os.access(os.path.dirname(path), os.W_OK):
|
|
52
|
+
print('Non-valid path:\n\t "{}"'.format(path))
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
return path if path else None
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def path_components(path):
|
|
59
|
+
path = FSH.full_path(path)
|
|
60
|
+
return path.split(os.path.sep) if path else None
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def path_extension(path):
|
|
64
|
+
components = FSH.path_components(path)
|
|
65
|
+
return os.path.splitext(components[-1])[1][1:] if components else None
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def is_subdir(subdir_path, parent_path):
|
|
69
|
+
subdir_path = FSH.full_path(subdir_path)
|
|
70
|
+
parent_path = FSH.full_path(parent_path)
|
|
71
|
+
|
|
72
|
+
relative = os.path.relpath(subdir_path, start=parent_path)
|
|
73
|
+
|
|
74
|
+
return not relative.startswith(os.pardir)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def level_from_root(root_path, nested_path):
|
|
78
|
+
''' determines the level from root_path folder
|
|
79
|
+
'''
|
|
80
|
+
return FSH.full_path(nested_path).count(os.path.sep) - \
|
|
81
|
+
FSH.full_path(root_path).count(os.path.sep)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def folders_at_level(src_dir, target_level):
|
|
85
|
+
''' generates a sequence of folders at given level from src_dir
|
|
86
|
+
'''
|
|
87
|
+
for r,d,f in os.walk(src_dir):
|
|
88
|
+
if FSH.level_from_root(src_dir, r) == target_level:
|
|
89
|
+
yield FSH.full_path(r)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def remove_folders_below_target_level(src_dir, target_level=sys.maxsize, empty_only=True, non_empty_msg = None):
|
|
93
|
+
''' removes folders below target level
|
|
94
|
+
'''
|
|
95
|
+
folders_removed = 0
|
|
96
|
+
for tpath in FSH.folders_at_level(src_dir, target_level):
|
|
97
|
+
for crpath, dnames, _ in os.walk(tpath, topdown = False):
|
|
98
|
+
for dname in dnames:
|
|
99
|
+
dpath = os.path.join(crpath, dname)
|
|
100
|
+
if not (empty_only and os.listdir(dpath)):
|
|
101
|
+
folders_removed +=1
|
|
102
|
+
shutil.rmtree(dpath)
|
|
103
|
+
else:
|
|
104
|
+
print('not empty: {}'.format(dpath))
|
|
105
|
+
if non_empty_msg:
|
|
106
|
+
print(non_empty_msg)
|
|
107
|
+
return folders_removed
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def unique_fnames():
|
|
111
|
+
''' default unique file names generator method,
|
|
112
|
+
via appending a simple numbering pattern
|
|
113
|
+
'''
|
|
114
|
+
unique_names = {}
|
|
115
|
+
while True:
|
|
116
|
+
fname = yield
|
|
117
|
+
while True:
|
|
118
|
+
if fname in unique_names:
|
|
119
|
+
unique_names[fname] += 1
|
|
120
|
+
name_base, name_ext = os.path.splitext(fname)
|
|
121
|
+
fname = '{0}_{1}{2}'.format(name_base, unique_names[fname], name_ext)
|
|
122
|
+
else:
|
|
123
|
+
unique_names[fname] = 0
|
|
124
|
+
yield fname
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def fs_size(size, kb_1024=False):
|
|
129
|
+
''' human readable file system entry size
|
|
130
|
+
'''
|
|
131
|
+
if size < 0:
|
|
132
|
+
raise ValueError('File size can not be negative')
|
|
133
|
+
unit_sfx = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
|
134
|
+
1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
|
|
135
|
+
div = 1024 if kb_1024 else 1000
|
|
136
|
+
for suffix in unit_sfx[div]:
|
|
137
|
+
size /= div
|
|
138
|
+
if size < div:
|
|
139
|
+
if suffix in ('KB', 'KiB'):
|
|
140
|
+
return '{0:.0f}{1}'.format(size, suffix)
|
|
141
|
+
else:
|
|
142
|
+
return '{0:.1f}{1}'.format(size, suffix)
|
|
143
|
+
raise ValueError('File is way too large')
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def dir_size(dir_path, shared_cache = None):
|
|
147
|
+
''' Calculates directory size in bytes
|
|
148
|
+
'''
|
|
149
|
+
total_size = 0
|
|
150
|
+
use_shared_cache = isinstance(shared_cache, dict)
|
|
151
|
+
|
|
152
|
+
for r, _, fnames in os.walk(dir_path):
|
|
153
|
+
# for repetitive calls, look to get from provided shared cache
|
|
154
|
+
if use_shared_cache:
|
|
155
|
+
r_size_from_cache = shared_cache.get(r)
|
|
156
|
+
if r_size_from_cache:
|
|
157
|
+
total_size += r_size_from_cache
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# caculate current root size
|
|
161
|
+
r_size = 0
|
|
162
|
+
r_size += os.path.getsize(r)
|
|
163
|
+
for fname in fnames:
|
|
164
|
+
fpath = os.path.join(r, fname)
|
|
165
|
+
try:
|
|
166
|
+
stat = os.stat(fpath)
|
|
167
|
+
except OSError:
|
|
168
|
+
continue
|
|
169
|
+
r_size += stat.st_size
|
|
170
|
+
|
|
171
|
+
total_size += r_size
|
|
172
|
+
if use_shared_cache:
|
|
173
|
+
shared_cache[r] = r_size
|
|
174
|
+
|
|
175
|
+
return total_size
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def file_md5(fpath, block_size=0, hex=False):
|
|
179
|
+
''' Calculates MD5 hash for a file at fpath
|
|
180
|
+
'''
|
|
181
|
+
md5 = hashlib.md5()
|
|
182
|
+
if block_size == 0:
|
|
183
|
+
block_size = 128 * md5.block_size
|
|
184
|
+
with open(fpath,'rb') as f:
|
|
185
|
+
for chunk in iter(lambda: f.read(block_size), b''):
|
|
186
|
+
md5.update(chunk)
|
|
187
|
+
return md5.hexdigest() if hex else md5.digest()
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def files(src_dir, *, recursive = False, pass_filter = None):
|
|
191
|
+
''' list of files passing specified filter
|
|
192
|
+
'''
|
|
193
|
+
if not pass_filter:
|
|
194
|
+
pass_filter = lambda f: True
|
|
195
|
+
if recursive:
|
|
196
|
+
fpathes = [os.path.join(r,f) for r,d,files in os.walk(src_dir)
|
|
197
|
+
for f in files if pass_filter(f)]
|
|
198
|
+
else:
|
|
199
|
+
fpathes = (os.path.join(src_dir, fname) for fname in os.listdir(src_dir)
|
|
200
|
+
if pass_filter(fname))
|
|
201
|
+
fpathes = [f for f in fpathes if os.path.isfile(f)]
|
|
202
|
+
|
|
203
|
+
return fpathes
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def move_FS_entry(orig_path, target_path,
|
|
207
|
+
check_unique = True,
|
|
208
|
+
quiet = False, stop = False):
|
|
209
|
+
succeeded = False
|
|
210
|
+
try:
|
|
211
|
+
if check_unique and os.path.exists(target_path):
|
|
212
|
+
raise OSError('\nTarget path entry already exists')
|
|
213
|
+
shutil.move(orig_path, target_path)
|
|
214
|
+
succeeded = True
|
|
215
|
+
except OSError as e:
|
|
216
|
+
if not quiet:
|
|
217
|
+
print(str(e))
|
|
218
|
+
print('Failed to move entry:\n\t{0}\n\t{1}'.format(orig_path, target_path))
|
|
219
|
+
print('Exiting...') if stop else print('Skipping...')
|
|
220
|
+
if stop:
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
return succeeded
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def remove_FS_entry(entry_path, include_read_only = False):
|
|
226
|
+
''' Remove files / dirs,
|
|
227
|
+
can deal with with read-only files
|
|
228
|
+
'''
|
|
229
|
+
def check_writable(fpath):
|
|
230
|
+
if include_read_only and (not os.access(fpath, os.W_OK)):
|
|
231
|
+
os.chmod(fpath, stat.S_IWUSR)
|
|
232
|
+
return True
|
|
233
|
+
else:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def onerror(func, fpath, exc_info):
|
|
237
|
+
if check_writable(fpath):
|
|
238
|
+
func(fpath)
|
|
239
|
+
else:
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
entry_path = FSH.full_path(entry_path)
|
|
243
|
+
if os.path.isfile(entry_path):
|
|
244
|
+
check_writable(entry_path)
|
|
245
|
+
os.remove(entry_path)
|
|
246
|
+
|
|
247
|
+
elif os.path.isdir(entry_path):
|
|
248
|
+
shutil.rmtree(entry_path, onerror = onerror)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def fs_entry(fpath):
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# Quick dev test
|
|
256
|
+
if __name__ == '__main__':
|
|
257
|
+
from batchmp.commons.utils import timed
|
|
258
|
+
path = os.path.realpath(os.path.dirname(__file__))
|
|
259
|
+
|
|
260
|
+
@timed
|
|
261
|
+
def dir_size_test(n):
|
|
262
|
+
for i in range(n):
|
|
263
|
+
size = FSH.dir_size(path, shared_cache)
|
|
264
|
+
return size
|
|
265
|
+
|
|
266
|
+
repeat_cnt = 5000
|
|
267
|
+
shared_cache = {}
|
|
268
|
+
print(dir_size_test(repeat_cnt))
|
|
269
|
+
|
|
270
|
+
shared_cache = None
|
|
271
|
+
print(dir_size_test(repeat_cnt))
|
|
272
|
+
|
|
@@ -0,0 +1,390 @@
|
|
|
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, re, datetime, string
|
|
16
|
+
from collections import namedtuple
|
|
17
|
+
from string import Template
|
|
18
|
+
from batchmp.fstools.dirtools import DHandler
|
|
19
|
+
from batchmp.fstools.fsutils import FSH
|
|
20
|
+
from batchmp.fstools.builders.fsentry import FSEntry, FSEntryType, FSEntryDefaults
|
|
21
|
+
from batchmp.fstools.builders.fsprms import FSEntryParamsBase
|
|
22
|
+
from batchmp.commons.utils import MiscHelpers
|
|
23
|
+
from batchmp.tags.handlers.ffmphandler import FFmpegTagHandler
|
|
24
|
+
from batchmp.tags.handlers.mtghandler import MutagenTagHandler
|
|
25
|
+
from batchmp.tags.output.formatters import TagOutputFormatter
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# DirEntry Counters helper
|
|
29
|
+
class DirCounters:
|
|
30
|
+
def __init__(self, dirs_cnt = 0, files_cnt = 0, num_files = 0, num_dirs = 0):
|
|
31
|
+
self.dirs_cnt = dirs_cnt
|
|
32
|
+
self.files_cnt = files_cnt
|
|
33
|
+
self.num_files = num_files
|
|
34
|
+
self.num_dirs = num_dirs
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def num_digits(number, min_digits):
|
|
38
|
+
return max(MiscHelpers.int_num_digits(number), min_digits)
|
|
39
|
+
|
|
40
|
+
class Renamer:
|
|
41
|
+
''' Renames FS entries
|
|
42
|
+
'''
|
|
43
|
+
@classmethod
|
|
44
|
+
def add_index(cls, fs_entry_params,
|
|
45
|
+
as_prefix = False, join_str = '_',
|
|
46
|
+
start_from = 1, min_digits = 1,
|
|
47
|
+
sequential = False, by_directory = False):
|
|
48
|
+
''' adds indexing
|
|
49
|
+
automatically figures out right number of min_digits
|
|
50
|
+
'''
|
|
51
|
+
try:
|
|
52
|
+
start_from = abs(int(start_from))
|
|
53
|
+
except ValueError:
|
|
54
|
+
start_from = 1
|
|
55
|
+
|
|
56
|
+
counters = {fs_entry_params.src_dir : DirCounters(start_from, start_from, 0, 0)}
|
|
57
|
+
total_files = total_dirs = 0
|
|
58
|
+
|
|
59
|
+
if (sequential or by_directory):
|
|
60
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
61
|
+
cnt_key = fs_entry_params.src_dir # one key to rule them all here
|
|
62
|
+
|
|
63
|
+
def index_sequential(entry):
|
|
64
|
+
nonlocal counters
|
|
65
|
+
addition = None
|
|
66
|
+
|
|
67
|
+
if entry.type == FSEntryType.DIR:
|
|
68
|
+
if fs_entry_params.include_dirs:
|
|
69
|
+
addition = str(counters[cnt_key].dirs_cnt).zfill(DirCounters.num_digits(total_dirs, min_digits))
|
|
70
|
+
# update the dirs counter
|
|
71
|
+
if fs_entry_params.include_dirs or by_directory:
|
|
72
|
+
counters[cnt_key].dirs_cnt += 1
|
|
73
|
+
|
|
74
|
+
elif entry.type == FSEntryType.FILE:
|
|
75
|
+
if by_directory:
|
|
76
|
+
# indexing via adding respective directory counter
|
|
77
|
+
fcnt = counters[cnt_key].dirs_cnt - 1
|
|
78
|
+
# do nothing for root files
|
|
79
|
+
if fcnt >= 0:
|
|
80
|
+
addition = str(fcnt).zfill(Counters.num_digits(total_files, min_digits))
|
|
81
|
+
else:
|
|
82
|
+
addition = str(counters[cnt_key].files_cnt).zfill(DirCounters.num_digits(total_files, min_digits))
|
|
83
|
+
# need to update the files counter
|
|
84
|
+
counters[cnt_key].files_cnt += 1
|
|
85
|
+
|
|
86
|
+
return addition
|
|
87
|
+
else:
|
|
88
|
+
# multilevel indexing
|
|
89
|
+
def index_multilevel(entry):
|
|
90
|
+
nonlocal counters, total_files, total_dirs
|
|
91
|
+
addition = None
|
|
92
|
+
|
|
93
|
+
if entry.scopeSwitchingEntry:
|
|
94
|
+
counters[entry.realpath] = DirCounters(start_from, start_from,
|
|
95
|
+
len(fs_entry_params.fnames), len(fs_entry_params.dnames.passed))
|
|
96
|
+
|
|
97
|
+
cnt = counters[os.path.dirname(entry.realpath)]
|
|
98
|
+
if entry.type == FSEntryType.DIR and fs_entry_params.include_dirs:
|
|
99
|
+
addition = str(cnt.dirs_cnt).zfill(DirCounters.num_digits(cnt.num_dirs, min_digits))
|
|
100
|
+
cnt.dirs_cnt += 1
|
|
101
|
+
total_dirs += 1
|
|
102
|
+
|
|
103
|
+
elif entry.type == FSEntryType.FILE:
|
|
104
|
+
addition = str(cnt.files_cnt).zfill(DirCounters.num_digits(cnt.num_files, min_digits))
|
|
105
|
+
cnt.files_cnt += 1
|
|
106
|
+
total_files += 1
|
|
107
|
+
|
|
108
|
+
return addition
|
|
109
|
+
|
|
110
|
+
# set the index function
|
|
111
|
+
index_function = index_sequential if (sequential or by_directory) else index_multilevel
|
|
112
|
+
def add_index_transform(entry):
|
|
113
|
+
addition = None
|
|
114
|
+
|
|
115
|
+
# src dir
|
|
116
|
+
if entry.type == FSEntryType.ROOT:
|
|
117
|
+
pass
|
|
118
|
+
# dirs
|
|
119
|
+
elif entry.type == FSEntryType.DIR:
|
|
120
|
+
addition = index_function(entry)
|
|
121
|
+
# files
|
|
122
|
+
elif entry.type == FSEntryType.FILE:
|
|
123
|
+
if fs_entry_params.include_files:
|
|
124
|
+
addition = index_function(entry)
|
|
125
|
+
|
|
126
|
+
if addition is None:
|
|
127
|
+
return entry.basename
|
|
128
|
+
if as_prefix:
|
|
129
|
+
return join_str.join((addition, entry.basename))
|
|
130
|
+
else:
|
|
131
|
+
name_base, name_ext = os.path.splitext(entry.basename)
|
|
132
|
+
return '{0}{1}{2}{3}'.format(name_base, join_str, addition, name_ext)
|
|
133
|
+
|
|
134
|
+
# visualise changes and proceed if confirmed
|
|
135
|
+
if fs_entry_params.quiet:
|
|
136
|
+
proceed = True
|
|
137
|
+
else:
|
|
138
|
+
proceed, _, _ = DHandler.visualise_changes(fs_entry_params, formatter = add_index_transform)
|
|
139
|
+
|
|
140
|
+
if proceed:
|
|
141
|
+
counters = {fs_entry_params.src_dir : DirCounters(start_from, start_from, 0, 0)}
|
|
142
|
+
if (total_dirs + total_files) == 0:
|
|
143
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
144
|
+
DHandler.rename_entries(fs_entry_params, total_files + total_dirs, formatter = add_index_transform)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def capitalize(cls, fs_entry_params):
|
|
148
|
+
''' capitalizes names of FS entries
|
|
149
|
+
'''
|
|
150
|
+
|
|
151
|
+
def capitalize_transform(entry):
|
|
152
|
+
if entry.type == FSEntryType.ROOT:
|
|
153
|
+
return entry.basename
|
|
154
|
+
if entry.type == FSEntryType.DIR and not fs_entry_params.include_dirs:
|
|
155
|
+
return entry.basename
|
|
156
|
+
if entry.type == FSEntryType.FILE and not fs_entry_params.include_files:
|
|
157
|
+
return entry.basename
|
|
158
|
+
return string.capwords(entry.basename)
|
|
159
|
+
|
|
160
|
+
# visualise changes and proceed if confirmed
|
|
161
|
+
if fs_entry_params.quiet:
|
|
162
|
+
proceed = True
|
|
163
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
164
|
+
else:
|
|
165
|
+
proceed, total_files, total_dirs = DHandler.visualise_changes(fs_entry_params, formatter = capitalize_transform)
|
|
166
|
+
|
|
167
|
+
if proceed:
|
|
168
|
+
DHandler.rename_entries(fs_entry_params, total_files + total_dirs, formatter = capitalize_transform, check_unique = False)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def add_date(cls, fs_entry_params, as_prefix = False, join_str = '_', format = '%Y-%m-%d'):
|
|
173
|
+
''' adds current date
|
|
174
|
+
'''
|
|
175
|
+
addition = datetime.datetime.now().strftime(format)
|
|
176
|
+
join_str = str(join_str)
|
|
177
|
+
|
|
178
|
+
def add_date_transform(entry):
|
|
179
|
+
if entry.type == FSEntryType.ROOT:
|
|
180
|
+
return entry.basename
|
|
181
|
+
if entry.type == FSEntryType.DIR and not fs_entry_params.include_dirs:
|
|
182
|
+
return entry.basename
|
|
183
|
+
if entry.type == FSEntryType.FILE and not fs_entry_params.include_files:
|
|
184
|
+
return entry.basename
|
|
185
|
+
|
|
186
|
+
if as_prefix:
|
|
187
|
+
return join_str.join((addition, entry.basename))
|
|
188
|
+
else:
|
|
189
|
+
name_base, name_ext = os.path.splitext(entry.basename)
|
|
190
|
+
return '{0}{1}{2}{3}'.format(name_base, join_str, addition, name_ext)
|
|
191
|
+
|
|
192
|
+
# visualise changes and proceed if confirmed
|
|
193
|
+
if fs_entry_params.quiet:
|
|
194
|
+
proceed = True
|
|
195
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
196
|
+
else:
|
|
197
|
+
proceed, total_files, total_dirs = DHandler.visualise_changes(fs_entry_params, formatter = add_date_transform)
|
|
198
|
+
|
|
199
|
+
if proceed:
|
|
200
|
+
DHandler.rename_entries(fs_entry_params, total_files + total_dirs, formatter = add_date_transform)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def add_text(cls, fs_entry_params, text, as_prefix = False, join_str = ' '):
|
|
204
|
+
''' adds text
|
|
205
|
+
'''
|
|
206
|
+
addition = text
|
|
207
|
+
join_str = str(join_str)
|
|
208
|
+
|
|
209
|
+
def add_text_transform(entry):
|
|
210
|
+
if entry.type == FSEntryType.ROOT:
|
|
211
|
+
return entry.basename
|
|
212
|
+
if entry.type == FSEntryType.DIR and not fs_entry_params.include_dirs:
|
|
213
|
+
return entry.basename
|
|
214
|
+
if entry.type == FSEntryType.FILE and not fs_entry_params.include_files:
|
|
215
|
+
return entry.basename
|
|
216
|
+
|
|
217
|
+
if as_prefix:
|
|
218
|
+
return join_str.join((addition, entry.basename))
|
|
219
|
+
else:
|
|
220
|
+
name_base, name_ext = os.path.splitext(entry.basename)
|
|
221
|
+
return '{0}{1}{2}{3}'.format(name_base, join_str, addition, name_ext)
|
|
222
|
+
|
|
223
|
+
if fs_entry_params.quiet:
|
|
224
|
+
proceed = True
|
|
225
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
226
|
+
else:
|
|
227
|
+
proceed, total_files, total_dirs = DHandler.visualise_changes(fs_entry_params, formatter = add_text_transform)
|
|
228
|
+
|
|
229
|
+
if proceed:
|
|
230
|
+
DHandler.rename_entries(fs_entry_params, total_files + total_dirs, formatter = add_text_transform)
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def remove_n_characters(cls, fs_entry_params, num_chars = 0, from_head = True):
|
|
234
|
+
''' removes n first characters
|
|
235
|
+
'''
|
|
236
|
+
num_chars = abs(num_chars)
|
|
237
|
+
|
|
238
|
+
def remove_n_chars_transform(entry):
|
|
239
|
+
if entry.type == FSEntryType.ROOT:
|
|
240
|
+
return entry.basename
|
|
241
|
+
if entry.type == FSEntryType.DIR and not fs_entry_params.include_dirs:
|
|
242
|
+
return entry.basename
|
|
243
|
+
if entry.type == FSEntryType.FILE and not fs_entry_params.include_files:
|
|
244
|
+
return entry.basename
|
|
245
|
+
|
|
246
|
+
name_base, name_ext = os.path.splitext(entry.basename)
|
|
247
|
+
if from_head:
|
|
248
|
+
name_base = name_base[num_chars:]
|
|
249
|
+
else:
|
|
250
|
+
name_base = name_base[:-num_chars]
|
|
251
|
+
return ''.join((name_base, name_ext))
|
|
252
|
+
|
|
253
|
+
# visualise changes and proceed if confirmed
|
|
254
|
+
if fs_entry_params.quiet:
|
|
255
|
+
proceed = True
|
|
256
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
257
|
+
else:
|
|
258
|
+
proceed, total_files, total_dirs = DHandler.visualise_changes(fs_entry_params, formatter = remove_n_chars_transform)
|
|
259
|
+
|
|
260
|
+
if proceed:
|
|
261
|
+
DHandler.rename_entries(fs_entry_params, total_files + total_dirs, formatter = remove_n_chars_transform)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def replace(cls, fs_entry_params, find_str, replace_str, case_insensitive=False, include_extension = False):
|
|
265
|
+
''' Regexp-base replace
|
|
266
|
+
'''
|
|
267
|
+
flags = re.UNICODE
|
|
268
|
+
if case_insensitive:
|
|
269
|
+
flags = flags | re.IGNORECASE
|
|
270
|
+
p = re.compile(find_str, flags)
|
|
271
|
+
|
|
272
|
+
def replace_transform(entry):
|
|
273
|
+
if entry.type == FSEntryType.ROOT:
|
|
274
|
+
return entry.basename
|
|
275
|
+
if entry.type == FSEntryType.DIR and not fs_entry_params.include_dirs:
|
|
276
|
+
return entry.basename
|
|
277
|
+
if entry.type == FSEntryType.FILE and not fs_entry_params.include_files:
|
|
278
|
+
return entry.basename
|
|
279
|
+
|
|
280
|
+
name_base, name_ext = os.path.splitext(entry.basename)
|
|
281
|
+
match = p.search(entry.basename if include_extension else name_base)
|
|
282
|
+
if match:
|
|
283
|
+
if replace_str is not None:
|
|
284
|
+
# expand templates
|
|
285
|
+
replace_str_expanded = cls._expand_templates(entry, replace_str)
|
|
286
|
+
res = p.sub(replace_str_expanded, entry.basename if include_extension else name_base)
|
|
287
|
+
else:
|
|
288
|
+
res = match.group()
|
|
289
|
+
return '{0}{1}'.format(res, '' if include_extension else name_ext)
|
|
290
|
+
else:
|
|
291
|
+
return entry.basename
|
|
292
|
+
|
|
293
|
+
# visualise changes and proceed if confirmed
|
|
294
|
+
if fs_entry_params.quiet:
|
|
295
|
+
proceed = True
|
|
296
|
+
total_files, total_dirs, _ = DHandler.dir_stats(fs_entry_params)
|
|
297
|
+
else:
|
|
298
|
+
proceed, total_files, total_dirs = DHandler.visualise_changes(fs_entry_params, formatter = replace_transform)
|
|
299
|
+
|
|
300
|
+
if proceed:
|
|
301
|
+
DHandler.rename_entries(fs_entry_params, total_files + total_dirs, formatter = replace_transform)
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
def delete(cls, fs_entry_params):
|
|
305
|
+
|
|
306
|
+
''' Deletes selected files
|
|
307
|
+
Support detection of non-media files
|
|
308
|
+
'''
|
|
309
|
+
|
|
310
|
+
if fs_entry_params.filter_dirs & fs_entry_params.include_dirs:
|
|
311
|
+
fs_entry_params.filter_files = False
|
|
312
|
+
fs_entry_params.include_files = True
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def delete_transform(entry):
|
|
316
|
+
if entry.type == FSEntryType.ROOT:
|
|
317
|
+
return entry.basename
|
|
318
|
+
if entry.type == FSEntryType.DIR and not fs_entry_params.include_dirs:
|
|
319
|
+
return None
|
|
320
|
+
if entry.type == FSEntryType.FILE and not fs_entry_params.include_files:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
# these are to be gone soon...
|
|
324
|
+
return entry.basename
|
|
325
|
+
|
|
326
|
+
if fs_entry_params.quiet:
|
|
327
|
+
proceed = True
|
|
328
|
+
else:
|
|
329
|
+
proceed, _, _ = DHandler.visualise_changes(fs_entry_params,
|
|
330
|
+
formatter = delete_transform,
|
|
331
|
+
after_msg = 'The following files / folders will be deleted')
|
|
332
|
+
|
|
333
|
+
if proceed:
|
|
334
|
+
DHandler.remove_entries(fs_entry_params, formatter = delete_transform)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def organize(cls, fs_entry_params):
|
|
339
|
+
|
|
340
|
+
''' Organizes files by selected attributes
|
|
341
|
+
Support detection of non-media files
|
|
342
|
+
'''
|
|
343
|
+
print('to be organized')
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def _expand_templates(cls, entry, value):
|
|
349
|
+
''' expands template values
|
|
350
|
+
'''
|
|
351
|
+
template = Template(value)
|
|
352
|
+
return template.safe_substitute(cls._substitute_dictionary(entry))
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def _substitute_dictionary(cls, entry):
|
|
356
|
+
''' internal template value substitution
|
|
357
|
+
'''
|
|
358
|
+
sd = {}
|
|
359
|
+
full_dir_name = os.path.dirname(entry.realpath)
|
|
360
|
+
sd['dirname'] = os.path.basename(full_dir_name)
|
|
361
|
+
sd['pardirname'] = os.path.basename(os.path.dirname(full_dir_name))
|
|
362
|
+
|
|
363
|
+
sd['adtime'] = datetime.datetime.fromtimestamp(os.path.getatime(entry.realpath))
|
|
364
|
+
sd['cdtime'] = datetime.datetime.fromtimestamp(os.path.getctime(entry.realpath))
|
|
365
|
+
sd['mdtime'] = datetime.datetime.fromtimestamp(os.path.getmtime(entry.realpath))
|
|
366
|
+
|
|
367
|
+
sd['atime'] = datetime.datetime.fromtimestamp(os.path.getatime(entry.realpath)).time()
|
|
368
|
+
sd['ctime'] = datetime.datetime.fromtimestamp(os.path.getctime(entry.realpath)).time()
|
|
369
|
+
sd['mtime'] = datetime.datetime.fromtimestamp(os.path.getmtime(entry.realpath)).time()
|
|
370
|
+
|
|
371
|
+
sd['adate'] = datetime.datetime.fromtimestamp(os.path.getatime(entry.realpath)).date()
|
|
372
|
+
sd['cdate'] = datetime.datetime.fromtimestamp(os.path.getctime(entry.realpath)).date()
|
|
373
|
+
sd['mdate'] = datetime.datetime.fromtimestamp(os.path.getmtime(entry.realpath)).date()
|
|
374
|
+
|
|
375
|
+
# for media files, update with the base tags values
|
|
376
|
+
sd.update(cls._get_tags(entry))
|
|
377
|
+
|
|
378
|
+
return sd
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def _get_tags(cls, entry):
|
|
382
|
+
''' media tags values for template value substitution
|
|
383
|
+
'''
|
|
384
|
+
tags = {}
|
|
385
|
+
handler = MutagenTagHandler() + FFmpegTagHandler()
|
|
386
|
+
if handler.can_handle(entry.realpath):
|
|
387
|
+
for field in TagOutputFormatter.COMPACT_FIELDS:
|
|
388
|
+
tags[field] = getattr(handler.tag_holder, field, '')
|
|
389
|
+
return tags
|
|
390
|
+
|