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,130 @@
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
+ """ Batch Conversion of media files
16
+ """
17
+ import shutil, sys, os, shlex
18
+ from batchmp.commons.utils import temp_dir
19
+ from batchmp.ffmptools.ffrunner import FFMPRunner, FFMPRunnerTask, LogLevel
20
+ from batchmp.commons.taskprocessor import TaskResult
21
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
22
+ from batchmp.commons.utils import (
23
+ timed,
24
+ run_cmd,
25
+ CmdProcessingError
26
+ )
27
+
28
+ class ConvertorTask(FFMPRunnerTask):
29
+ ''' Conversion TasksProcessor task
30
+ '''
31
+ def __init__(self, fpath, target_dir, log_level,
32
+ ff_general_options, ff_other_options, preserve_metadata,
33
+ target_format):
34
+ self.target_format = target_format
35
+
36
+ super().__init__(fpath, target_dir, log_level,
37
+ ff_general_options, ff_other_options, preserve_metadata)
38
+
39
+ def _check_defaults(self):
40
+ if not self.ff_other_options:
41
+ self.ff_other_options = FFmpegCommands.CONVERT_COPY_VBR_QUALITY
42
+ elif self.ff_other_options == FFmpegCommands.CONVERT_LOSSLESS:
43
+ # see if lossless is appropriate
44
+ # TBD: video formats
45
+ if self.target_format == '.flac':
46
+ self.ff_other_options = FFmpegCommands.CONVERT_LOSSLESS_FLAC
47
+ elif self.target_format == '.m4a':
48
+ self.ff_other_options = FFmpegCommands.CONVERT_LOSSLESS_ALAC
49
+ else:
50
+ self.ff_other_options = FFmpegCommands.CONVERT_COPY_VBR_QUALITY
51
+
52
+ if not self.ff_general_options:
53
+ self.ff_general_options = FFmpegBitMaskOptions.ff_general_options(
54
+ FFmpegBitMaskOptions.MAP_ALL_STREAMS)
55
+
56
+ if self.ff_other_options in (FFmpegCommands.CONVERT_COPY_VBR_QUALITY,
57
+ FFmpegCommands.CONVERT_LOSSLESS_FLAC,
58
+ FFmpegCommands.CONVERT_LOSSLESS_ALAC,
59
+ FFmpegCommands.CONVERT_CHANGE_CONTAINER):
60
+ self.ff_other_options += self._ff_cmd_exclude_artwork_streams()
61
+
62
+ def execute(self):
63
+ ''' builds and runs FFmpeg Conversion command in a subprocess
64
+ '''
65
+ # store tags if needed
66
+ self._store_tags()
67
+
68
+ task_result = TaskResult()
69
+
70
+ with temp_dir() as tmp_dir:
71
+ # prepare the tmp output path
72
+ conv_fname = ''.join((os.path.splitext(os.path.basename(self.fpath))[0], self.target_format))
73
+ conv_fpath = os.path.join(tmp_dir, conv_fname)
74
+
75
+ # build ffmpeg cmd string
76
+ p_in = ''.join((self.ff_cmd, ' {}'.format(shlex.quote(conv_fpath))))
77
+ self._log(p_in, LogLevel.FFMPEG)
78
+
79
+ # run ffmpeg command as a subprocess
80
+ try:
81
+ _, task_elapsed = run_cmd(p_in)
82
+ task_result.add_task_step_duration(task_elapsed)
83
+ except CmdProcessingError as e:
84
+ task_result.add_task_step_info_msg('A problem while processing media file:\n\t{0}' \
85
+ '\nOriginal error message:\n\t{1}' \
86
+ .format(self.fpath, e.args[0]))
87
+ else:
88
+ # restore tags if needed
89
+ self._restore_tags(conv_fpath)
90
+
91
+ # move converted file to target dir
92
+ shutil.move(conv_fpath, self.target_dir)
93
+
94
+ # all well
95
+ task_result.succeeded = True
96
+
97
+ task_result.add_report_msg(self.fpath)
98
+ return task_result
99
+
100
+
101
+ class Convertor(FFMPRunner):
102
+ def convert(self, ff_entry_params):
103
+
104
+ ''' Converts media to specified format
105
+ '''
106
+ tasks = []
107
+ if ff_entry_params.target_format:
108
+ if ff_entry_params.target_format.startswith('.'):
109
+ target_dir_prefix = '{}'.format(ff_entry_params.target_format[1:])
110
+ else:
111
+ target_dir_prefix = '{}'.format(ff_entry_params.target_format)
112
+ target_format = '.{}'.format(ff_entry_params.target_format)
113
+ ff_entry_params.target_dir_prefix = target_dir_prefix
114
+
115
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
116
+ # build tasks
117
+ tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
118
+ ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata,
119
+ ff_entry_params.target_format)
120
+ for media_file, target_dir_path in zip(media_files, target_dirs)]
121
+ for task_param in tasks_params:
122
+ task = ConvertorTask(*task_param)
123
+ tasks.append(task)
124
+
125
+ # run tasks
126
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
127
+
128
+
129
+
130
+
@@ -0,0 +1,223 @@
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
+ """ Batch cue splitter
16
+ """
17
+ import shutil, sys, os, shlex, re
18
+ from datetime import timedelta
19
+ from batchmp.commons.utils import temp_dir
20
+ from batchmp.ffmptools.ffrunner import FFMPRunner, LogLevel
21
+ from batchmp.commons.taskprocessor import TaskResult
22
+ from batchmp.fstools.walker import DWalker
23
+ from batchmp.ffmptools.utils.cueparse import CueParser, CueParseReadDataEncodingError
24
+ from batchmp.tags.handlers.tagsholder import TagHolder
25
+ from batchmp.ffmptools.ffcommands.convert import ConvertorTask
26
+ from batchmp.commons.descriptors import PropertyDescriptor
27
+ from batchmp.commons.utils import (
28
+ run_cmd,
29
+ CmdProcessingError
30
+ )
31
+
32
+ class CueSplitTagHolder(TagHolder):
33
+ ''' Need a few extra attributes for cue splitting
34
+ '''
35
+ cue_virt_fpath = PropertyDescriptor()
36
+ time_offset = PropertyDescriptor()
37
+
38
+
39
+ class CueSplitterTask(ConvertorTask):
40
+ ''' Cue Slit TasksProcessor task
41
+ '''
42
+ def __init__(self, cue_tag_holder, target_dir, log_level,
43
+ ff_general_options, ff_other_options, preserve_metadata,
44
+ target_format):
45
+
46
+ # unpack relevant properties due to pickle / multiprocessing
47
+ self.time_offset = cue_tag_holder.time_offset
48
+ self.duration = cue_tag_holder.length
49
+
50
+ self.track_number = cue_tag_holder.track
51
+ self.year = cue_tag_holder.year
52
+ self.genre = cue_tag_holder.genre
53
+ self.track_title = cue_tag_holder.title
54
+ self.albumartist = cue_tag_holder.albumartist
55
+ self.album = cue_tag_holder.album
56
+ self.composer = cue_tag_holder.composer
57
+ self.comments = cue_tag_holder.comments
58
+
59
+ super().__init__(cue_tag_holder.filepath, target_dir, log_level,
60
+ ff_general_options, ff_other_options, preserve_metadata, target_format)
61
+
62
+ @property
63
+ def ff_cmd(self):
64
+ ''' Cue Split command builder
65
+ '''
66
+ return ''.join((super().ff_cmd,
67
+ ' -ss {}'.format(self.time_offset),
68
+ ' -t {}'.format(self.duration)
69
+ ))
70
+
71
+ def _store_tags(self):
72
+ if self.tag_holder:
73
+ if self.track_title:
74
+ self.tag_holder.title = self.track_title
75
+ if self.album:
76
+ self.tag_holder.album = self.album
77
+ if self.albumartist:
78
+ self.tag_holder.albumartist = self.albumartist
79
+ if self.composer:
80
+ self.tag_holder.composer = self.composer
81
+ if self.track_number:
82
+ self.tag_holder.track = self.track_number
83
+ if self.comments:
84
+ self.tag_holder.comments = self.comments
85
+ if self.year:
86
+ self.tag_holder.year = self.year
87
+ if self.genre:
88
+ self.tag_holder.genre = self.genre
89
+
90
+
91
+ def execute(self):
92
+ ''' builds and runs FFmpeg Conversion command in a subprocess
93
+ '''
94
+ # store tags
95
+ self._store_tags()
96
+
97
+ task_result = TaskResult()
98
+
99
+ with temp_dir() as tmp_dir:
100
+ # prepare the tmp output path
101
+ conv_fname = '{0:02d} {1}'.format(self.track_number, self.track_title)
102
+ conv_fname = re.sub(r'[^\w\-_\. ]', '_', conv_fname)
103
+ conv_fname = ''.join((conv_fname, self.target_format))
104
+ conv_fpath = os.path.join(tmp_dir, conv_fname)
105
+
106
+ # build ffmpeg cmd string
107
+ p_in = ''.join((self.ff_cmd, ' {}'.format(shlex.quote(conv_fpath))))
108
+ self._log(p_in, LogLevel.FFMPEG)
109
+
110
+ # run ffmpeg command as a subprocess
111
+ try:
112
+ _, task_elapsed = run_cmd(p_in)
113
+ task_result.add_task_step_duration(task_elapsed)
114
+ except CmdProcessingError as e:
115
+ task_result.add_task_step_info_msg('A problem while processing media file:\n\t{0}' \
116
+ '\nOriginal error message:\n\t{1}' \
117
+ .format(self.fpath, e.args[0]))
118
+ else:
119
+ # restore tags if needed
120
+ self._restore_tags(conv_fpath)
121
+
122
+ # move converted file to target dir
123
+ shutil.move(conv_fpath, self.target_dir)
124
+
125
+ # all well
126
+ task_result.succeeded = True
127
+
128
+ task_result.add_report_msg(self.fpath)
129
+ return task_result
130
+
131
+
132
+ class CueSplitter(FFMPRunner):
133
+ def cue_split(self, ff_entry_params, encoding = 'utf-8'):
134
+
135
+ ''' Converts media to specified format
136
+ '''
137
+ tasks = []
138
+ if ff_entry_params.target_format:
139
+ if not ff_entry_params.target_format.startswith('.'):
140
+ ff_entry_params.target_format = '.{}'.format(ff_entry_params.target_format)
141
+ ff_entry_params.target_dir_prefix = '{}'.format(ff_entry_params.target_format[1:])
142
+
143
+ cue_tagholders, target_dirs = self._prepare_cue_data(ff_entry_params, encoding = encoding)
144
+ # build tasks
145
+ tasks_params = [(cue_tag_holder, target_dir_path, ff_entry_params.log_level,
146
+ ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata,
147
+ ff_entry_params.target_format)
148
+ for cue_tag_holder, target_dir_path in zip(cue_tagholders, target_dirs)]
149
+ for task_param in tasks_params:
150
+ task = CueSplitterTask(*task_param)
151
+ tasks.append(task)
152
+
153
+ # run tasks
154
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
155
+
156
+
157
+ ## Internal helpers
158
+ @staticmethod
159
+ def _prepare_cue_data(ff_entry_params,
160
+ pass_filter = None,
161
+ encoding = 'utf-8'):
162
+ ''' Builds a list of target media files and their tagholder attributes corresponding to found .cue files.
163
+ Prepares their respective target output dirs
164
+ '''
165
+ pass_filter = lambda fpath: fpath.endswith('.cue')
166
+
167
+ cue_fpaths = [entry.realpath for entry in DWalker.file_entries(ff_entry_params, pass_filter = pass_filter)]
168
+
169
+ cue_tagholders = CueSplitter._prepare_tagholders(cue_fpaths, encoding = encoding)
170
+
171
+ target_dirs = FFMPRunner._setup_target_dirs(ff_entry_params,
172
+ fpathes = [cue_tagholder.cue_virt_fpath for cue_tagholder in cue_tagholders])
173
+
174
+ return cue_tagholders, target_dirs
175
+
176
+
177
+ @staticmethod
178
+ def _prepare_tagholders(cue_fpaths, encoding):
179
+ cue_tagholders = []
180
+ for cue_fpath in cue_fpaths:
181
+ cue_parser = CueParser()
182
+ try:
183
+ cue_sheet = cue_parser.parse(cue_fpath, encoding = encoding)
184
+ except CueParseReadDataEncodingError:
185
+ print('\nUnable to read data from the "{0}" file using encoding: {1}'.format(cue_fpath, encoding))
186
+ print('Use the \'-en\' encoding option to specify correct encoding, e.g.: -en \'latin-1\'\n')
187
+ exit(1)
188
+
189
+ for file in cue_sheet.files:
190
+ for track in file.tracks:
191
+ tag_holder = CueSplitTagHolder()
192
+
193
+ cue_virt_dir = os.path.dirname(cue_fpath) + os.path.sep + \
194
+ os.path.splitext(os.path.basename(cue_fpath))[0]
195
+ tag_holder.cue_virt_fpath = os.path.join(cue_virt_dir, file.name)
196
+ tag_holder.filepath = os.path.join(os.path.dirname(cue_fpath), file.name)
197
+
198
+ if cue_sheet.rem:
199
+ for rem_item in cue_sheet.rem:
200
+ if not tag_holder.year:
201
+ match = re.match(r'DATE.+(\d{4})', rem_item)
202
+ if match:
203
+ tag_holder.year = match.group(1)
204
+ continue
205
+ if not tag_holder.genre:
206
+ match = re.match(r'GENRE\s+(.+)$', rem_item)
207
+ if match:
208
+ tag_holder.genre = match.group(1)
209
+ tag_holder.comments = ', '.join(cue_sheet.rem)
210
+
211
+ tag_holder.title = track.title or cue_sheet.title
212
+ tag_holder.album = cue_sheet.title
213
+ tag_holder.albumartist = track.performer or cue_sheet.performer
214
+ tag_holder.composer = track.songwriter or cue_sheet.songwriter
215
+
216
+ tag_holder.track = track.number
217
+ tag_holder.time_offset = track.offset_in_seconds
218
+ tag_holder.length = track.duration_in_seconds or timedelta(days = 30).total_seconds()
219
+
220
+ cue_tagholders.append(tag_holder)
221
+
222
+ return cue_tagholders
223
+
@@ -0,0 +1,173 @@
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
+ """ Batch Reduce of background audio noise in media files,
16
+ via filtering out highpass / low-pass frequencies
17
+ Supports multi-passes processing, e.g. 3 times for each media file
18
+ """
19
+ import shutil, sys, os, datetime, math, shlex
20
+ from batchmp.commons.utils import temp_dir
21
+ from batchmp.ffmptools.ffrunner import FFMPRunner, FFMPRunnerTask, LogLevel
22
+ from batchmp.commons.taskprocessor import TaskResult
23
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
24
+ from batchmp.commons.utils import (
25
+ timed,
26
+ run_cmd,
27
+ CmdProcessingError
28
+ )
29
+
30
+ class DenoiserTask(FFMPRunnerTask):
31
+ ''' Denoise TasksProcessor task
32
+ '''
33
+ def __init__(self, fpath, target_dir, log_level,
34
+ ff_general_options, ff_other_options, preserve_metadata,
35
+ highpass, lowpass, num_passes):
36
+ # build ffmpeg '-af' parameter
37
+ if highpass and lowpass:
38
+ af_str = 'highpass=f={0}, lowpass=f={1}'.format(highpass, lowpass)
39
+ elif lowpass:
40
+ af_str = 'lowpass=f={}'.format(lowpass)
41
+ elif highpass:
42
+ af_str = 'highpass=f={}'.format(highpass)
43
+ else:
44
+ raise ValueError('At least one of the highpass / lowpass values must be specified')
45
+
46
+ self.af_str = af_str
47
+ self.num_passes = num_passes
48
+ self.excluded_artwork_streams = False
49
+
50
+ super().__init__(fpath, target_dir, log_level,
51
+ ff_general_options, ff_other_options, preserve_metadata)
52
+
53
+ def _check_defaults(self):
54
+ if not self.ff_other_options:
55
+ self.ff_other_options = FFmpegCommands.CONVERT_COPY_VBR_QUALITY
56
+
57
+ if not self.ff_general_options:
58
+ self.ff_general_options = FFmpegBitMaskOptions.ff_general_options(FFmpegBitMaskOptions.MAP_ALL_STREAMS)
59
+
60
+ if self.ff_other_options == FFmpegCommands.CONVERT_COPY_VBR_QUALITY:
61
+ self.ff_other_options += self._ff_cmd_exclude_artwork_streams()
62
+ self.excluded_artwork_streams = True
63
+
64
+
65
+ def ff_denoise_cmd(self, fpath, pass_cnt):
66
+ ''' Denoise command builder
67
+ '''
68
+ # when implicitly excluding artwork streams, need to do this only for the first pass
69
+ apply_ff_other_options = (not self.excluded_artwork_streams) or (pass_cnt == 0)
70
+
71
+ return ''.join(('ffmpeg',
72
+ FFmpegCommands.LOG_LEVEL_ERROR,
73
+ ' -i {}'.format(shlex.quote(fpath)),
74
+ self.ff_general_options,
75
+ self.ff_other_options if apply_ff_other_options else '',
76
+ ' -af {}'.format(shlex.quote(self.af_str))))
77
+
78
+ def execute(self):
79
+ ''' builds and runs Denoise command in a subprocess
80
+ '''
81
+ fname = os.path.basename(self.fpath)
82
+ fname_ext = os.path.splitext(fname)[1].strip().lower()
83
+
84
+ # ffmpeg initial input path
85
+ fpath_input = self.fpath
86
+
87
+ # store tags if needed
88
+ self._store_tags()
89
+ task_result = TaskResult()
90
+
91
+ with temp_dir() as tmp_dir:
92
+ # process the file in given number of passes
93
+ for pass_cnt in range(self.num_passes):
94
+
95
+ # compile intermediary output path
96
+ fpath_output = ''.join((os.path.splitext(fname)[0],
97
+ '_{}'.format(datetime.datetime.now().strftime("%H%M%S%f")),
98
+ fname_ext))
99
+ fpath_output = os.path.join(tmp_dir, fpath_output)
100
+
101
+ p_in = '{0} {1}'.format(self.ff_denoise_cmd(fpath_input, pass_cnt), shlex.quote(fpath_output))
102
+ self._log(p_in, LogLevel.FFMPEG)
103
+
104
+ # run ffmpeg command as a subprocess
105
+ try:
106
+ _, pass_elapsed = run_cmd(p_in)
107
+ except CmdProcessingError as e:
108
+ task_result.add_task_step_info_msg('\nA problem while processing media file:\n\t{0}' \
109
+ '\nSkipping further processing at pass {1} ...' \
110
+ '\nOriginal error message:\n\t{2}' \
111
+ .format(fpath_input, pass_cnt + 1, e.args[0]))
112
+ break
113
+ else:
114
+ task_result.add_task_step_duration(pass_elapsed)
115
+
116
+ if pass_cnt == self.num_passes - 1:
117
+ # the last pass, rounding up
118
+
119
+ # restore tags if needed
120
+ self._restore_tags(fpath_output)
121
+
122
+ # move denoised file to target dir
123
+ shutil.move(fpath_output, os.path.join(self.target_dir, fname))
124
+
125
+ # all well
126
+ task_result.succeeded = True
127
+ else:
128
+ # for the next pass, just make the intermediary output new input
129
+ fpath_input = fpath_output
130
+
131
+ task_result.add_report_msg(self.fpath)
132
+ return task_result
133
+
134
+
135
+ class Denoiser(FFMPRunner):
136
+ DEFAULT_HIGHPASS = 200
137
+ DEFAULT_LOWPASS = 3000
138
+ DEFAULT_NUM_PASSES = 1
139
+
140
+ def apply_af_filters(self, ff_entry_params,
141
+ num_passes = None, highpass = None, lowpass = None):
142
+
143
+ ''' Reduce of background audio noise in media files
144
+ via filtering out highpass / low-pass frequencies
145
+ '''
146
+ if not highpass:
147
+ highpass = self.DEFAULT_HIGHPASS
148
+ if not lowpass:
149
+ lowpass = self.DEFAULT_LOWPASS
150
+ if not num_passes:
151
+ num_passes = self.DEFAULT_NUM_PASSES
152
+
153
+ ff_entry_params.target_dir_prefix = 'denoised'
154
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
155
+
156
+ msg = None if len(media_files) == 0 else \
157
+ '{0} media files to process, ({1} {2} each)'.format(
158
+ len(media_files), num_passes,
159
+ 'passes' if num_passes > 1 else 'pass')
160
+ # build tasks
161
+ tasks = []
162
+ tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
163
+ ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata,
164
+ highpass, lowpass, num_passes)
165
+ for media_file, target_dir_path in zip(media_files, target_dirs)]
166
+ for task_param in tasks_params:
167
+ task = DenoiserTask(*task_param)
168
+ tasks.append(task)
169
+
170
+ # run tasks
171
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
172
+
173
+
@@ -0,0 +1,121 @@
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
+ """ Batch Fragmentation of media files
16
+ """
17
+ import shutil, sys, os, shlex
18
+ from batchmp.commons.utils import temp_dir
19
+ from batchmp.ffmptools.ffrunner import FFMPRunner, FFMPRunnerTask, LogLevel
20
+ from batchmp.commons.taskprocessor import TaskResult
21
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
22
+ from batchmp.ffmptools.ffcommands.segment import Segmenter
23
+ from batchmp.commons.utils import (
24
+ timed,
25
+ run_cmd,
26
+ CmdProcessingError
27
+ )
28
+
29
+ class FragmenterTask(FFMPRunnerTask):
30
+ ''' Fragment TasksProcessor task
31
+ '''
32
+ def __init__(self, fpath, target_dir, log_level,
33
+ ff_general_options, ff_other_options, preserve_metadata,
34
+ fragment_starttime, fragment_duration, fragment_trim):
35
+
36
+ self.fragment_starttime = fragment_starttime
37
+ self.fragment_duration = fragment_duration
38
+ self.fragment_trim = fragment_trim
39
+
40
+ super().__init__(fpath, target_dir, log_level,
41
+ ff_general_options, ff_other_options, preserve_metadata)
42
+
43
+ @property
44
+ def ff_cmd(self):
45
+ ''' Fragment command builder
46
+ '''
47
+ if self.fragment_trim:
48
+ media_duration = Segmenter._media_duration(self.fpath)
49
+ self.fragment_duration = media_duration - self.fragment_trim - self.fragment_starttime
50
+ return ''.join((super().ff_cmd,
51
+ ' -ss {}'.format(self.fragment_starttime),
52
+ ' -t {}'.format(self.fragment_duration)
53
+ ))
54
+
55
+ def execute(self):
56
+ ''' builds and runs Fragment FFmpeg command in a subprocess
57
+ '''
58
+ # store tags if needed
59
+ self._store_tags()
60
+
61
+ task_result = TaskResult()
62
+
63
+ with temp_dir() as tmp_dir:
64
+ # prepare the tmp output path
65
+ fragmented_fpath = os.path.join(tmp_dir, os.path.basename(self.fpath))
66
+
67
+ # build ffmpeg cmd string
68
+ p_in = '{0} {1}'.format(self.ff_cmd, shlex.quote(fragmented_fpath))
69
+ self._log(p_in, LogLevel.FFMPEG)
70
+
71
+ # run ffmpeg command as a subprocess
72
+ try:
73
+ if self.fragment_duration < 0:
74
+ raise CmdProcessingError('A problem while processing media file {0}: \
75
+ Negative media duration {1}s, check your input parameters to add up correctly'\
76
+ .format(self.fpath, int(self.fragment_duration)))
77
+
78
+ _, task_elapsed = run_cmd(p_in)
79
+ task_result.add_task_step_duration(task_elapsed)
80
+ except CmdProcessingError as e:
81
+ task_result.add_task_step_info_msg('A problem while processing media file:\n\t{0}' \
82
+ '\nOriginal error message:\n\t{1}' \
83
+ .format(self.fpath, e.args[0]))
84
+ else:
85
+ # restore tags if needed
86
+ self._restore_tags(fragmented_fpath)
87
+
88
+ # move fragmented file to target dir
89
+ shutil.move(fragmented_fpath, self.target_dir)
90
+
91
+ # all well
92
+ task_result.succeeded = True
93
+
94
+ task_result.add_report_msg(self.fpath)
95
+ return task_result
96
+
97
+
98
+ class Fragmenter(FFMPRunner):
99
+ def fragment(self, ff_entry_params,
100
+ fragment_starttime = None, fragment_duration = None, fragment_trim = None):
101
+
102
+ ''' Fragment media file by specified starttime & duration
103
+ '''
104
+ tasks = []
105
+ if (fragment_starttime is not None) and (fragment_duration is not None):
106
+ ff_entry_params.target_dir_prefix = 'fragmented'
107
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
108
+
109
+ # build tasks
110
+ tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
111
+ ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata,
112
+ fragment_starttime, fragment_duration, fragment_trim)
113
+ for media_file, target_dir_path in zip(media_files, target_dirs)]
114
+ for task_param in tasks_params:
115
+ task = FragmenterTask(*task_param)
116
+ tasks.append(task)
117
+
118
+ # run tasks
119
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
120
+
121
+