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