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