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,227 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf8
3
+ ## Copyright (c) 2014 Arseniy Kuznetsov
4
+ ##
5
+ ## This program is free software; you can redistribute it and/or
6
+ ## modify it under the terms of the GNU General Public License
7
+ ## as published by the Free Software Foundation; either version 2
8
+ ## of the License, or (at your option) any later version.
9
+ ##
10
+ ## This program is distributed in the hope that it will be useful,
11
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ ## GNU General Public License for more details.
14
+
15
+
16
+ import os, re
17
+ from batchmp.ffmptools.utils.cuesheet import CueSheet, CueTrack
18
+
19
+ class CueParseReadDataEncodingError(Exception):
20
+ def __init__(self, message = None):
21
+ super().__init__(message if message is not None else self.default_message)
22
+
23
+ @property
24
+ def default_message(self):
25
+ return '\n\tUnable to read data from the .cue file' \
26
+ '\n\tProvide correct encoding argument when parsing files, e.g.:' \
27
+ '\n\t\tcue_parser = CueParser()' \
28
+ '\n\t\tcue_parser.parse(cue_filepath, encoding = "latin-1")'
29
+
30
+ class CueLineParser:
31
+ ''' Cue Lines Parser
32
+ '''
33
+ def __init__(self):
34
+ self._line_matcher = re.compile(r'^([A-Z]+)\s+(.*)$')
35
+
36
+ def parse_line(self, line):
37
+ ''' Parses a line read from a *.cue file
38
+ '''
39
+ line = line.strip()
40
+
41
+ command = params = None
42
+ match = self._line_matcher.match(line)
43
+ if match and len(match.groups()) >= 2:
44
+ command = match.group(1)
45
+ params = self._parse_params(match.group(2))
46
+
47
+ return command, params
48
+
49
+ def _parse_params(self, params):
50
+ res = []
51
+ params = params.strip()
52
+ if params:
53
+ quote_idx = params.find('"')
54
+ if quote_idx < 0:
55
+ for param in params.split():
56
+ res.append(param)
57
+ elif quote_idx == 0:
58
+ if len(params) > 1:
59
+ res = self._parse_params(params[quote_idx + 1:])
60
+ elif quote_idx > 0:
61
+ res.append(params[:quote_idx])
62
+ if len(params) > 1:
63
+ res = res + self._parse_params(params[quote_idx + 1:])
64
+ return res
65
+
66
+ class CueParser:
67
+ ''' Cue files parser
68
+ '''
69
+ def __init__(self):
70
+ self._commands_map = {
71
+ 'CATALOG': self._parse_catalog,
72
+ 'CDTEXTFILE': self._parse_cdtextfile,
73
+ 'FILE': self._parse_file,
74
+ 'FLAGS': self._parse_flags,
75
+ 'INDEX': self._parse_index,
76
+ 'ISRC': self._parse_isrc,
77
+ 'PERFORMER': self._parse_performer,
78
+ 'POSTGAP': self._parse_postgap,
79
+ 'PREGAP': self._parse_pregap,
80
+ 'REM': self._parse_rem,
81
+ 'SONGWRITER': self._parse_songwriter,
82
+ 'TITLE': self._parse_title,
83
+ 'TRACK': self._parse_track
84
+ }
85
+
86
+ self._time_offset_matcher = re.compile(r'^(\d{1,3}):(\d{1,2}):(\d{1,2})$')
87
+ self._line_parser = CueLineParser()
88
+ self._lines = None
89
+ self._cuesheet = None
90
+
91
+ def parse(self, filepath, encoding = 'utf-8'):
92
+ try:
93
+ self._read_data(filepath, encoding = encoding)
94
+ except CueParseReadDataEncodingError:
95
+ raise
96
+ self._cuesheet = CueSheet()
97
+
98
+ for line in self._lines:
99
+ command, params = self._line_parser.parse_line(line)
100
+ if command:
101
+ self._commands_map[command](params)
102
+
103
+ return self._cuesheet
104
+
105
+
106
+ # Internal processing
107
+ #####################
108
+ def _read_data(self, filepath, encoding):
109
+ if not os.path.isfile(filepath):
110
+ print('Cannot open file: {}'.format(filepath))
111
+ exit(2)
112
+ try:
113
+ with open(filepath, encoding = encoding) as f:
114
+ lines = f.read()
115
+ except UnicodeDecodeError:
116
+ raise CueParseReadDataEncodingError
117
+ self._lines = lines.splitlines()
118
+
119
+ def _parse_catalog(self, params):
120
+ self._cuesheet.catalog = params[0];
121
+
122
+ def _parse_cdtextfile(self, params):
123
+ self._cuesheet.cdtextfile = params[0];
124
+
125
+ def _parse_file(self, params):
126
+ self._cuesheet.add_file()
127
+ self._cuesheet.last_file.name = params[0];
128
+ self._cuesheet.last_file.type = params[1];
129
+
130
+ def _parse_flags(self, params):
131
+ track = _cuesheet.last_track()
132
+ if track:
133
+ track.flags = params[0]
134
+
135
+ def _parse_index(self, params):
136
+ number = int(params[0])
137
+ time_offset = self._parse_time_offset(params[1])
138
+
139
+ track = self._cuesheet.last_track
140
+ if track:
141
+ index = CueTrack.Index(number, time_offset)
142
+ track.indexes.append(index)
143
+
144
+ # for the first index, look to calculate previous track duration
145
+ if len(track.indexes) == 1:
146
+ penultimate_track = self._cuesheet.penultimate_track
147
+ if penultimate_track and penultimate_track.indexes:
148
+ time_offset = track.indexes[0].time_offset_timedelta
149
+ previosOffset = penultimate_track.indexes[0].time_offset_timedelta
150
+ penultimate_track.duration = time_offset - previosOffset
151
+
152
+ def _parse_isrc(self, params):
153
+ track = self._cuesheet.last_track
154
+ if track:
155
+ track.isrc = params[0]
156
+
157
+ def _parse_performer(self, params):
158
+ track = self._cuesheet.last_track
159
+ if not track:
160
+ self._cuesheet.performer = params[0]
161
+ else:
162
+ track.performer = params[0]
163
+
164
+ def _parse_postgap(self, params):
165
+ track = self._cuesheet.last_track
166
+ if track:
167
+ track.postgap = self._parse_time_offset(params[0])
168
+
169
+ def _parse_pregap(self, params):
170
+ track = self._cuesheet.last_track
171
+ if track:
172
+ track.pregap = self._parse_time_offset(params[0])
173
+
174
+ def _parse_rem(self, params):
175
+ self._cuesheet.rem.append(' '.join(params))
176
+
177
+ def _parse_songwriter(self, params):
178
+ track = self._cuesheet.last_track
179
+ if not track:
180
+ self._cuesheet.songWriter = params[0]
181
+ else:
182
+ track.songWriter = params[0]
183
+
184
+ def _parse_title(self, params):
185
+ track = self._cuesheet.last_track
186
+ if not track:
187
+ self._cuesheet.title = params[0]
188
+ else:
189
+ track.title = params[0]
190
+
191
+ def _parse_track(self, params):
192
+ number = int(params[0])
193
+ type = params[1]
194
+ self._cuesheet.add_track(number, type)
195
+
196
+ def _parse_time_offset(self, time_offset_str):
197
+ mins = secs = frames = 0
198
+ match = self._time_offset_matcher.match(time_offset_str)
199
+ if match:
200
+ mins = int(match.group(1))
201
+ secs = int(match.group(2))
202
+ frames = int(match.group(3))
203
+ time_offset = CueTrack.TimeOffset(mins, secs, frames)
204
+ return time_offset
205
+
206
+ from datetime import timedelta
207
+ if __name__ == '__main__':
208
+ cue_filepath = os.path.expanduser('~/_Dev/GitHub/batch-mp-tools/tests/ffmp/.data/bmfp_a/noise.cue')
209
+
210
+ line_parser = CueLineParser()
211
+ param_dict = ['TRACK 01 AUDIO',
212
+ 'INDEX 01 00:00:00',
213
+ 'TITLE "BMFP Noisy Classical"',
214
+ 'FILE "01 background noise.aiff" AIFF',
215
+ 'REM GENRE "NOISY CLASSICAL"','REM DATE "2016"']
216
+
217
+ for params in param_dict:
218
+ param = line_parser.parse_line(params)
219
+ print(param)
220
+ print()
221
+
222
+ parser = CueParser()
223
+ cuesheet = parser.parse(cue_filepath)
224
+ print(cuesheet)
225
+
226
+
227
+
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf8
3
+ ## Copyright (c) 2014 Arseniy Kuznetsov
4
+ ##
5
+ ## This program is free software; you can redistribute it and/or
6
+ ## modify it under the terms of the GNU General Public License
7
+ ## as published by the Free Software Foundation; either version 2
8
+ ## of the License, or (at your option) any later version.
9
+ ##
10
+ ## This program is distributed in the hope that it will be useful,
11
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ ## GNU General Public License for more details.
14
+
15
+
16
+ import sys, os, re, math
17
+ from collections import namedtuple
18
+ from enum import Enum
19
+ from datetime import timedelta
20
+ from string import Template
21
+ from batchmp.commons.descriptors import PropertyDescriptor
22
+
23
+
24
+ class CueDefaultOutputDescriptor(PropertyDescriptor):
25
+ ''' Output format property descriptor, with support for default values
26
+ '''
27
+ def __init__(self, func_name):
28
+ self._func_name = func_name
29
+
30
+ def __get__(self, instance, type=None):
31
+ value = super().__get__(instance, type = type)
32
+ if not value:
33
+ value = getattr(instance, self._func_name)()
34
+ return value
35
+
36
+ class TimeOffsetPropertyDescriptor(PropertyDescriptor):
37
+ ''' Named tuple type property descriptor
38
+ or, "typing like a duck rhymes with ..."
39
+ '''
40
+ def __set__(self, instance, value):
41
+ if isinstance(value, tuple): #and hasattr ...
42
+ super().__set__(instance, value)
43
+ else:
44
+ raise TypeError("Not a Tuple Type: {}".format(value))
45
+
46
+ class CueBase:
47
+ ''' Base cue sheet / track stuff
48
+ '''
49
+ performer = PropertyDescriptor()
50
+ songwriter = PropertyDescriptor()
51
+ title = PropertyDescriptor()
52
+ outputformat = CueDefaultOutputDescriptor("default_output_format")
53
+ substitute_dictionary = CueDefaultOutputDescriptor("default_substitute_dictionary")
54
+
55
+ # Internal methods
56
+ ###################
57
+ def __repr__(self):
58
+ template = Template(self.default_output_format)
59
+ return template.safe_substitute(self.default_substitute_dictionary)
60
+
61
+ @property
62
+ def default_output_format(self):
63
+ return None
64
+
65
+ @property
66
+ def default_substitute_dictionary(self):
67
+ ''' common default output-related template processing
68
+ subclasses can extend this via adding ther specific stuff to process
69
+ '''
70
+ sd = {}
71
+ sd['performer'] = self.performer if self.performer else ''
72
+ sd['songwriter'] = self.songwriter if self.songwriter else ''
73
+ sd['title'] = self.title if self.title else ''
74
+
75
+ return sd
76
+
77
+ class CueTrack(CueBase):
78
+ ''' Cue track attributes / processing
79
+ '''
80
+ TimeOffset = namedtuple('TimeOffset', ['mins', 'secs', 'frames'])
81
+ class Index:
82
+ number = PropertyDescriptor()
83
+ time_offset = TimeOffsetPropertyDescriptor()
84
+
85
+ def __init__(self, number, time_offset):
86
+ self.number = number
87
+ self.time_offset = time_offset
88
+
89
+ @property
90
+ def time_offset_str(self):
91
+ return '{0:02d}:{1:02d}:{2:02d}'.format(self.time_offset.mins, self.time_offset.secs, self.time_offset.frames)
92
+
93
+ @property
94
+ def time_offset_timedelta(self):
95
+ ''' time_offset is specified as mm:ss:ff (minute-second-frame) format,
96
+ where there are 75 such frames per second of audio
97
+ '''
98
+ seconds = self.time_offset.secs + self.time_offset.frames / 75
99
+ return timedelta(minutes = self.time_offset.mins, seconds = seconds)
100
+
101
+ @property
102
+ def time_offset_in_seconds(self):
103
+ return self.time_offset_timedelta.total_seconds()
104
+
105
+ number = PropertyDescriptor()
106
+ type = PropertyDescriptor()
107
+ flags = PropertyDescriptor()
108
+ isrc = PropertyDescriptor()
109
+ pregap = TimeOffsetPropertyDescriptor()
110
+ postgap = TimeOffsetPropertyDescriptor()
111
+ duration = PropertyDescriptor()
112
+
113
+ def __init__(self, number, type):
114
+ self.number = number
115
+ self.type = type
116
+ self.indexes = []
117
+
118
+ @property
119
+ def offset_in_seconds(self):
120
+ offset_in_seconds = 0
121
+ if len(self.indexes) > 0:
122
+ offset_in_seconds = self.indexes[0].time_offset_in_seconds
123
+ return offset_in_seconds
124
+
125
+ @property
126
+ def duration_in_seconds(self):
127
+ return self.duration.total_seconds() if self.duration else 0
128
+
129
+ # Internal methods
130
+ ###################
131
+ @property
132
+ def default_output_format(self):
133
+ return ' $number\t$time_offset\t$duration\t$title'
134
+
135
+ @property
136
+ def default_substitute_dictionary(self):
137
+ ''' cue track default output template processing
138
+ '''
139
+ sd = super().default_substitute_dictionary
140
+ if len(self.indexes) > 0:
141
+ sd['time_offset'] = self.indexes[0].time_offset_str
142
+ else:
143
+ sd['time_offset'] = ''
144
+
145
+ sd['number'] = '{0:02d}'.format(self.number) if self.number else ''
146
+ if self.duration:
147
+ minutes = math.floor(self.duration.total_seconds() / 60)
148
+ sd['duration'] = '{0:02d}:{1:02d}'.format(minutes, self.duration.seconds - 60 * minutes)
149
+ else:
150
+ sd['duration'] = ' : '
151
+ return sd
152
+
153
+ class CueSheet(CueBase):
154
+ ''' Cue sheet processing
155
+ '''
156
+ class File:
157
+ name = PropertyDescriptor()
158
+ format = PropertyDescriptor()
159
+
160
+ def __init__(self):
161
+ self.tracks = []
162
+
163
+ catalog = PropertyDescriptor()
164
+ cdtextfile = PropertyDescriptor()
165
+
166
+ def __init__(self):
167
+ self.files = []
168
+ self.rem = []
169
+
170
+ @property
171
+ def last_file(self):
172
+ files_cnt = len(self.files)
173
+ if files_cnt > 0:
174
+ return self.files[files_cnt - 1]
175
+ return None
176
+
177
+ @property
178
+ def last_track(self):
179
+ file = self.last_file
180
+ if file and file.tracks:
181
+ tracks_cnt = len(file.tracks)
182
+ if tracks_cnt > 0:
183
+ return file.tracks[tracks_cnt - 1]
184
+ return None
185
+
186
+ @property
187
+ def penultimate_track(self):
188
+ file = self.last_file
189
+ if file and file.tracks:
190
+ tracks_cnt = len(file.tracks)
191
+ if tracks_cnt > 1:
192
+ return file.tracks[tracks_cnt - 2]
193
+ return None
194
+
195
+ def add_file(self):
196
+ file = CueSheet.File()
197
+ self.files.append(file)
198
+
199
+ def add_track(self, number, type):
200
+ file = self.last_file
201
+ if file:
202
+ file.tracks.append(CueTrack(number, type))
203
+
204
+ # Internal methods
205
+ ###################
206
+ @property
207
+ def default_output_format(self):
208
+ return 'Performer: $performer\nTitle: $title\nRem: $rem\nFiles: $files\nTracks: $tracks'
209
+
210
+ @property
211
+ def default_substitute_dictionary(self):
212
+ ''' cue sheet default output template processing
213
+ '''
214
+ sd = super().default_substitute_dictionary
215
+
216
+ if self.rem:
217
+ rem_output = '\n'
218
+ for rem in self.rem:
219
+ if rem == self.rem[-1]:
220
+ rem_output += ' {}'.format(rem)
221
+ else:
222
+ rem_output += ' {}\n'.format(rem)
223
+ sd['rem'] = rem_output
224
+
225
+ files_output = '\n'
226
+ tracks_output = '\n'
227
+ for file in self.files:
228
+ if file == self.files[-1]:
229
+ files_output += ' {0} ({1})'.format(file.name, file.type)
230
+ else:
231
+ files_output += ' {0} ({1})\n'.format(file.name, file.type)
232
+ for track in file.tracks:
233
+ tracks_output += '{}\n'.format(str(track))
234
+
235
+ sd['files'] = files_output
236
+ sd['tracks'] = tracks_output
237
+
238
+ return sd
239
+
File without changes
File without changes
@@ -0,0 +1,221 @@
1
+ # coding=utf8
2
+ ## Copyright (c) 2014 Arseniy Kuznetsov
3
+ ##
4
+ ## This program is free software; you can redistribute it and/or
5
+ ## modify it under the terms of the GNU General Public License
6
+ ## as published by the Free Software Foundation; either version 2
7
+ ## of the License, or (at your option) any later version.
8
+ ##
9
+ ## This program is distributed in the hope that it will be useful,
10
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ ## GNU General Public License for more details.
13
+
14
+ import os, sys
15
+ from abc import ABCMeta, abstractmethod
16
+ from batchmp.fstools.fsutils import FSH
17
+ from batchmp.fstools.builders.fsentry import FSEntry, FSEntryType, FSMediaEntryGroupType
18
+
19
+
20
+ class FSEntryBuilder(metaclass = ABCMeta):
21
+ ''' root entry builder
22
+ '''
23
+ @staticmethod
24
+ def build_root_entry(fs_entry_params):
25
+ # yield the current folder
26
+ if fs_entry_params.current_level == 0:
27
+ # src dir goes in full and without indent
28
+ entry = FSEntry(type = FSEntryType.ROOT,
29
+ basename = os.path.basename(fs_entry_params.rpath),
30
+ realpath = fs_entry_params.rpath,
31
+ indent = os.path.dirname(fs_entry_params.rpath) + os.path.sep,
32
+ isEnclosingEntry = True,
33
+ isEnclosingFilesContainterEntry = True)
34
+ else:
35
+ entry = FSEntry(FSEntryType.DIR,
36
+ basename = os.path.basename(fs_entry_params.rpath),
37
+ realpath = fs_entry_params.rpath,
38
+ indent = fs_entry_params.current_indent[:-1] + os.path.sep,
39
+ isEnclosingEntry = fs_entry_params.isEnclosingEntry,
40
+ isEnclosingFilesContainterEntry = fs_entry_params.isEnclosingFilesContainterEntry,
41
+ isScopeSwitchingEntry = True)
42
+ # debug
43
+ # print("{}cur level:{}".format(fs_entry_params.current_indent, fs_entry_params.current_level))
44
+ # print("{}end level:{}".format(fs_entry_params.current_indent, fs_entry_params.end_level))
45
+ yield entry
46
+
47
+ ''' Abstract builder method
48
+ '''
49
+ @staticmethod
50
+ @abstractmethod
51
+ def build_entry(fs_entry_params):
52
+ yield None
53
+
54
+
55
+ class FSEntryBuilderBase(FSEntryBuilder):
56
+ ''' File System Processing
57
+ '''
58
+ @staticmethod
59
+ def build_entry(fs_entry_params):
60
+ ## not much there for enclosing entries
61
+ if fs_entry_params.isEnclosingEntry and not fs_entry_params.isEnclosingFilesContainterEntry:
62
+ return
63
+
64
+ ## Files processing ##
65
+ for fname in fs_entry_params.fnames:
66
+ fpath = os.path.join(fs_entry_params.rpath, fname)
67
+ entry = FSEntry(type = FSEntryType.FILE,
68
+ basename = fname,
69
+ realpath = fpath,
70
+ indent = fs_entry_params.siblings_indent)
71
+ yield entry
72
+
73
+ ## Directories processing ##
74
+ for dname in fs_entry_params.dnames.passed:
75
+ dpath = os.path.join(fs_entry_params.rpath, dname)
76
+
77
+ # check the current_level from root
78
+ if fs_entry_params.current_level == fs_entry_params.end_level:
79
+ # not going any deeper
80
+ # yield the dir
81
+ entry = FSEntry(type = FSEntryType.DIR,
82
+ basename = dname,
83
+ realpath = dpath,
84
+ indent = fs_entry_params.siblings_indent[:-1] + os.path.sep)
85
+ #print('from build_entry!\n')
86
+ yield entry
87
+
88
+
89
+
90
+ class FSEntryBuilderFlatten(FSEntryBuilder):
91
+ @staticmethod
92
+ def build_root_entry(fs_entry_params):
93
+ if fs_entry_params.current_level <= fs_entry_params.target_level:
94
+ yield from FSEntryBuilderBase.build_root_entry(fs_entry_params)
95
+
96
+
97
+ @staticmethod
98
+ def build_entry(fs_entry_params):
99
+ if fs_entry_params.current_level < fs_entry_params.target_level:
100
+ yield from FSEntryBuilderBase.build_entry(fs_entry_params)
101
+ else:
102
+ if fs_entry_params.current_level > fs_entry_params.target_level:
103
+ return
104
+
105
+ flattens = []
106
+ unique_fname = fs_entry_params.unique_fnames()
107
+
108
+ ## Files processing ##
109
+ for fname in fs_entry_params.fnames:
110
+ fpath = os.path.join(fs_entry_params.rpath, fname)
111
+ entry = FSEntry(type = FSEntryType.FILE,
112
+ basename = fname,
113
+ realpath = fpath,
114
+ indent = fs_entry_params.siblings_indent)
115
+ flattens.append(entry)
116
+
117
+ # store the name generator init values
118
+ next(unique_fname)
119
+ unique_fname.send(fname)
120
+
121
+ ## Directories processing ##
122
+ # remove non-matching
123
+ for dname in fs_entry_params.merged_dnames:
124
+ dpath = os.path.join(fs_entry_params.rpath, dname)
125
+
126
+ # flattening, yield the underlying files
127
+ for dr, _, dfnames in os.walk(dpath):
128
+ dr_path = FSH.full_path(dr)
129
+ df_path = lambda fname: os.path.join(dr_path, fname)
130
+
131
+ # filter non-matching files
132
+ if fs_entry_params.filter_files:
133
+ dfnames = (fname for fname in dfnames if fs_entry_params.passed_filters(fname))
134
+
135
+ # file types
136
+ if fs_entry_params.file_type != FSMediaEntryGroupType.ANY:
137
+ dfnames = [fname for fname in dfnames if fs_entry_params.is_of_required_type(df_path(fname))]
138
+
139
+
140
+ for fname in dfnames:
141
+ fpath = df_path(fname)
142
+ next(unique_fname)
143
+ fname = unique_fname.send(fname)
144
+
145
+ entry = FSEntry(FSEntryType.FILE, fname, fpath, fs_entry_params.siblings_indent)
146
+ flattens.append(entry)
147
+
148
+ # OK to sort now
149
+ if fs_entry_params.by_size:
150
+ sort_key = lambda entry: os.path.getsize(entry.realpath)
151
+ else:
152
+ # for sorting need to still derive basename from realpath
153
+ # as for flattened it might be different from entry.basename
154
+ sort_key = lambda entry: os.path.basename(entry.realpath).lower()
155
+
156
+ for entry in sorted(flattens, key = sort_key, reverse = fs_entry_params.descending):
157
+ yield entry
158
+
159
+
160
+
161
+ from batchmp.ffmptools.ffutils import FFH
162
+ import datetime
163
+
164
+ class FSEntryBuilderOrganize(FSEntryBuilderBase):
165
+ @staticmethod
166
+ def build_entry(fs_entry_params):
167
+ """ Build entries for virtual directory preview """
168
+ # not much there for enclosing entries
169
+ if fs_entry_params.isEnclosingEntry and not fs_entry_params.isEnclosingFilesContainterEntry:
170
+ return
171
+
172
+ ## Files processing - for virtual preview, just show the files ##
173
+ for fname in fs_entry_params.fnames:
174
+ entry = FSEntry(type = FSEntryType.FILE,
175
+ basename = fname,
176
+ realpath = os.path.join(fs_entry_params.rpath, fname), # Virtual path for display
177
+ indent = fs_entry_params.siblings_indent)
178
+ yield entry
179
+
180
+ ## Directories processing ##
181
+ for dname in fs_entry_params.dnames.passed:
182
+ dpath = os.path.join(fs_entry_params.rpath, dname)
183
+
184
+ # check the current_level from root
185
+ if fs_entry_params.current_level == fs_entry_params.end_level:
186
+ # not going any deeper
187
+ # yield the dir
188
+ entry = FSEntry(type = FSEntryType.DIR,
189
+ basename = dname,
190
+ realpath = dpath,
191
+ indent = fs_entry_params.siblings_indent[:-1] + os.path.sep)
192
+ yield entry
193
+
194
+
195
+ class FSEntryBuilderOrganizeWorker(FSEntryBuilder):
196
+ @staticmethod
197
+ def build_entry(fs_entry_params):
198
+ base_target_dir = fs_entry_params.target_dir if fs_entry_params.target_dir else fs_entry_params.src_dir
199
+
200
+ for fname in fs_entry_params.fnames:
201
+ fpath = os.path.join(fs_entry_params.rpath, fname)
202
+ entry = FSEntry(type=FSEntryType.FILE,
203
+ basename=fname,
204
+ realpath=fpath,
205
+ indent=fs_entry_params.siblings_indent)
206
+
207
+ if fs_entry_params.by == 'type':
208
+ media_type = FFH.media_type(fpath=fpath, fast_scan=fs_entry_params.fast_scan)
209
+ subdir = str(media_type.name).lower()
210
+ target_dir = os.path.join(base_target_dir, subdir)
211
+ elif fs_entry_params.by == 'date':
212
+ mtime = os.path.getmtime(fpath)
213
+ date = datetime.datetime.fromtimestamp(mtime)
214
+ subdir = date.strftime(fs_entry_params.date_format)
215
+ target_dir = os.path.join(base_target_dir, subdir)
216
+ else:
217
+ # Should not happen if arguments are parsed correctly
218
+ target_dir = base_target_dir
219
+
220
+ entry.target_path = os.path.join(target_dir, fname)
221
+ yield entry