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,135 @@
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 Peak Normalization of media files
16
+ """
17
+ import shutil, sys, os, shlex
18
+ from batchmp.commons.utils import temp_dir
19
+ from batchmp.ffmptools.ffutils import FFH
20
+ from batchmp.ffmptools.ffrunner import FFMPRunner, FFMPRunnerTask, LogLevel
21
+ from batchmp.commons.taskprocessor import TaskResult
22
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
23
+ from batchmp.commons.utils import (
24
+ timed,
25
+ run_cmd,
26
+ CmdProcessingError
27
+ )
28
+
29
+ class PeakNormalizerTask(FFMPRunnerTask):
30
+ ''' Peak Normalizer TasksProcessor task
31
+ '''
32
+ def __init__(self, fpath, target_dir, log_level,
33
+ ff_general_options, ff_other_options, preserve_metadata):
34
+
35
+ super().__init__(fpath, target_dir, log_level,
36
+ ff_general_options, ff_other_options, preserve_metadata)
37
+
38
+ def _check_defaults(self):
39
+ if not self.ff_other_options:
40
+ self.ff_other_options = FFmpegCommands.CONVERT_COPY_VBR_QUALITY
41
+
42
+ if not self.ff_general_options:
43
+ self.ff_general_options = FFmpegBitMaskOptions.ff_general_options(
44
+ FFmpegBitMaskOptions.MAP_ALL_STREAMS)
45
+
46
+ if self.ff_other_options == FFmpegCommands.CONVERT_COPY_VBR_QUALITY:
47
+ self.ff_other_options += self._ff_cmd_exclude_artwork_streams()
48
+
49
+ def ff_normalize_cmd(self, volume_gain):
50
+ ''' Peak Normalize command builder
51
+ '''
52
+ return ''.join((super().ff_cmd,
53
+ ' -af "volume=volume={}dB"'.format(volume_gain)))
54
+ #'' if True else ' -c:a pcm_s16le'))
55
+
56
+ def execute(self):
57
+ ''' builds and runs Peak Normalization command in a subprocess
58
+ '''
59
+ task_result = TaskResult()
60
+
61
+ volume_entry, task_elapsed = self._detect_volumes()
62
+ task_result.add_task_step_duration(task_elapsed)
63
+
64
+ if not volume_entry:
65
+ task_result.add_task_step_info_msg('A problem analyzing volume in media file:\n\t{}' \
66
+ .format(self.fpath))
67
+ elif not volume_entry.max_volume:
68
+ task_result.add_task_step_info_msg( \
69
+ 'Already normalized:\n\t{0}'.format(self.fpath))
70
+ # copy source file to target dir
71
+ shutil.copy(self.fpath, self.target_dir)
72
+
73
+ # all well
74
+ task_result.succeeded = True
75
+ else:
76
+ # store tags if needed
77
+ self._store_tags()
78
+
79
+ with temp_dir() as tmp_dir:
80
+ # prepare the tmp output path
81
+ norm_fname = os.path.basename(self.fpath)
82
+ norm_fpath = os.path.join(tmp_dir, norm_fname)
83
+
84
+ # build ffmpeg cmd string
85
+ p_in = ''.join((self.ff_normalize_cmd(volume_entry.max_volume), \
86
+ ' {}'.format(shlex.quote(norm_fpath))))
87
+ self._log(p_in, LogLevel.FFMPEG)
88
+
89
+ # run ffmpeg command as a subprocess
90
+ try:
91
+ _, task_elapsed = run_cmd(p_in)
92
+ task_result.add_task_step_duration(task_elapsed)
93
+ except CmdProcessingError as e:
94
+ task_result.add_task_step_info_msg('A problem while processing media file:\n\t{0}' \
95
+ '\nOriginal error message:\n\t{1}' \
96
+ .format(self.fpath, e.args[0]))
97
+ else:
98
+ # restore tags if needed
99
+ self._restore_tags(norm_fpath)
100
+
101
+ # move converted file to target dir
102
+ shutil.move(norm_fpath, self.target_dir)
103
+
104
+ # all well
105
+ task_result.succeeded = True
106
+
107
+ task_result.add_report_msg(self.fpath)
108
+ return task_result
109
+
110
+ @timed
111
+ def _detect_volumes(self):
112
+ return FFH.volume_detector(self.fpath)
113
+
114
+
115
+ class PeakNormalizer(FFMPRunner):
116
+ def peak_normalize(self, ff_entry_params):
117
+
118
+ ''' Peak Normalization of media files
119
+ '''
120
+ ff_entry_params.target_dir_prefix = 'peak_normalized'
121
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
122
+
123
+ # build tasks
124
+ tasks = []
125
+ tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
126
+ ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata)
127
+ for media_file, target_dir_path in zip(media_files, target_dirs)]
128
+ for task_param in tasks_params:
129
+ task = PeakNormalizerTask(*task_param)
130
+ tasks.append(task)
131
+
132
+ # run tasks
133
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
134
+
135
+
@@ -0,0 +1,157 @@
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 splitting of media files
16
+ """
17
+ import shutil, sys, os, math, fnmatch, 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.tags.handlers.ffmphandler import FFmpegTagHandler
22
+ from batchmp.tags.handlers.mtghandler import MutagenTagHandler
23
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
24
+ from batchmp.ffmptools.ffutils import FFH
25
+ from batchmp.commons.utils import (
26
+ timed,
27
+ run_cmd,
28
+ CmdProcessingError,
29
+ MiscHelpers
30
+ )
31
+
32
+ class SegmenterTask(FFMPRunnerTask):
33
+ ''' Segment TasksProcessor task
34
+ '''
35
+ def __init__(self, fpath, target_dir, log_level,
36
+ ff_general_options, ff_other_options, preserve_metadata,
37
+ reset_timestamps, segment_size_MB, segment_length_secs):
38
+
39
+ super().__init__(fpath, target_dir, log_level,
40
+ ff_general_options, ff_other_options, preserve_metadata)
41
+
42
+ # calculate number of segments and, if needed, the segment length in secs
43
+ if segment_length_secs:
44
+ self.segment_length_secs = segment_length_secs
45
+ self.num_segments = math.ceil(Segmenter._media_duration(self.fpath) / segment_length_secs)
46
+ elif segment_size_MB:
47
+ num_segments = Segmenter._media_size_MB(self.fpath) / segment_size_MB
48
+ self.segment_length_secs = Segmenter._media_duration(self.fpath) / num_segments
49
+ self.num_segments = math.ceil(num_segments)
50
+ else:
51
+ # should not really get there, but just in case
52
+ raise ValueError('One of the command parameters needs to be specified: '\
53
+ '<segment_size_MB | segment_length_secs>')
54
+ self.reset_timestamps = reset_timestamps
55
+
56
+ @property
57
+ def ff_cmd(self):
58
+ ''' Fragment command builder
59
+ '''
60
+ return ''.join((super().ff_cmd,
61
+ FFmpegCommands.SEGMENT,
62
+ ' {0} {1}'.format(FFmpegCommands.SEGMENT_TIME, self.segment_length_secs),
63
+ FFmpegCommands.SEGMENT_RESET_TIMESTAMPS if self.reset_timestamps else ''))
64
+
65
+ def execute(self):
66
+ ''' builds and runs Segment FFmpeg command in a subprocess
67
+ '''
68
+ # store tags if needed
69
+ self._store_tags()
70
+
71
+ task_result = TaskResult()
72
+
73
+ with temp_dir() as tmp_dir:
74
+ # compile intermediary output path
75
+ fn_parts = os.path.splitext(os.path.basename(self.fpath))
76
+ fname_ext = fn_parts[1].strip().lower()
77
+ fpath_output = ''.join((fn_parts[0],
78
+ '_%{}d'.format(MiscHelpers.int_num_digits(self.num_segments)),
79
+ fname_ext))
80
+ fpath_output = os.path.join(tmp_dir, fpath_output)
81
+
82
+ # build ffmpeg cmd string
83
+ p_in = ''.join((self.ff_cmd, ' {}'.format(shlex.quote(fpath_output))))
84
+ self._log(p_in, LogLevel.FFMPEG)
85
+
86
+ # run ffmpeg command as a subprocess
87
+ try:
88
+ _, task_elapsed = run_cmd(p_in)
89
+ task_result.add_task_step_duration(task_elapsed)
90
+ except CmdProcessingError as e:
91
+ task_result.add_task_step_info_msg('A problem while processing media file:\n\t{0}' \
92
+ '\nOriginal error message:\n\t{1}' \
93
+ .format(self.fpath, e.args[0]))
94
+ else:
95
+ # move split files to target directory
96
+ for segmented_fname in os.listdir(tmp_dir):
97
+ if fnmatch.fnmatch(segmented_fname, '*{}'.format(fname_ext)):
98
+ segmented_fpath = os.path.join(tmp_dir, segmented_fname)
99
+
100
+ # restore tags if needed
101
+ self._restore_tags(segmented_fpath)
102
+
103
+ # move fragmented file to target dir
104
+ shutil.move(segmented_fpath, self.target_dir)
105
+
106
+ # all well
107
+ task_result.succeeded = True
108
+
109
+ task_result.add_report_msg(self.fpath)
110
+ return task_result
111
+
112
+
113
+ class Segmenter(FFMPRunner):
114
+ def segment(self, ff_entry_params,
115
+ segment_size_MB = 0.0,
116
+ segment_length_secs = 0.0,
117
+ reset_timestamps = False):
118
+
119
+ ''' Segment media file by specified size | duration
120
+ '''
121
+ tasks = []
122
+ if segment_size_MB or segment_length_secs:
123
+ # if segment_length_secs:
124
+ # # here need to determine media length
125
+ # pass_filter = lambda fpath: self._media_duration(fpath) > segment_length_secs
126
+ # elif segment_size_MB:
127
+ # # simple media selection by size
128
+ # pass_filter = lambda fpath: FFH.ffmpeg_supported_media(fpath) and (self._media_size_MB(fpath) > segment_size_MB)
129
+
130
+ ff_entry_params.target_dir_prefix = 'segmented'
131
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
132
+
133
+ # build tasks
134
+ tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level,
135
+ ff_entry_params.ff_general_options, ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata,
136
+ reset_timestamps, segment_size_MB, segment_length_secs)
137
+ for media_file, target_dir_path in zip(media_files, target_dirs)]
138
+ for task_param in tasks_params:
139
+ task = SegmenterTask(*task_param)
140
+ tasks.append(task)
141
+
142
+ # run tasks
143
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
144
+
145
+
146
+ # Internal Helpers
147
+ @staticmethod
148
+ def _media_duration(fpath):
149
+ handler = MutagenTagHandler() + FFmpegTagHandler()
150
+ if handler.can_handle(fpath):
151
+ return handler.tag_holder.length
152
+ else:
153
+ return 0.0
154
+
155
+ @staticmethod
156
+ def _media_size_MB(fpath):
157
+ return os.path.getsize(fpath) / 1000**2
@@ -0,0 +1,159 @@
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 split on silence
16
+ """
17
+ import shutil, sys, os, fnmatch, 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.tags.handlers.ffmphandler import FFmpegTagHandler
22
+ from batchmp.tags.handlers.mtghandler import MutagenTagHandler
23
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
24
+ from batchmp.ffmptools.ffutils import FFH
25
+ from batchmp.commons.utils import (
26
+ timed,
27
+ run_cmd,
28
+ CmdProcessingError,
29
+ MiscHelpers
30
+ )
31
+
32
+ class SilenceSplitterTask(FFMPRunnerTask):
33
+ ''' Segment TasksProcessor task
34
+ '''
35
+ def __init__(self, fpath, target_dir, log_level,
36
+ ff_general_options, ff_other_options, preserve_metadata,
37
+ reset_timestamps, silence_min_duration, silence_noise_tolerance_amplitude_ratio,
38
+ silence_auto_duration, silence_target_trimmed_duration):
39
+
40
+ super().__init__(fpath, target_dir, log_level,
41
+ ff_general_options, ff_other_options, preserve_metadata)
42
+
43
+ self.reset_timestamps = reset_timestamps
44
+ self.silence_min_duration = silence_min_duration
45
+ self.silence_noise_tolerance_amplitude_ratio = silence_noise_tolerance_amplitude_ratio
46
+ self.silence_auto_duration = silence_auto_duration
47
+ self.silence_target_trimmed_duration = silence_target_trimmed_duration
48
+
49
+ def ff_cmd(self, segment_start_times):
50
+ ''' Silence Splitter command builder
51
+ '''
52
+ return ''.join((super().ff_cmd,
53
+ FFmpegCommands.SEGMENT,
54
+ ' {0} {1}'.format(FFmpegCommands.SEGMENT_TIMES, ','.join(segment_start_times)),
55
+ FFmpegCommands.SEGMENT_RESET_TIMESTAMPS if self.reset_timestamps else ''))
56
+
57
+ def execute(self):
58
+ ''' builds and runs Segment FFmpeg command in a subprocess
59
+ '''
60
+
61
+ task_result = TaskResult()
62
+
63
+ segment_start_times, task_elapsed = self._segment_start_times()
64
+ task_result.add_task_step_duration(task_elapsed)
65
+ if not segment_start_times:
66
+ task_result.add_task_step_info_msg( \
67
+ 'No silence detected in media file:\n\t{0}'.format(self.fpath))
68
+ else:
69
+ # store tags if needed
70
+ self._store_tags()
71
+
72
+ with temp_dir() as tmp_dir:
73
+ # compile intermediary output path
74
+ fn_parts = os.path.splitext(os.path.basename(self.fpath))
75
+ fname_ext = fn_parts[1].strip().lower()
76
+ fpath_output = ''.join((fn_parts[0],
77
+ '_%{}d'.format(MiscHelpers.int_num_digits(len(segment_start_times))),
78
+ fname_ext))
79
+ fpath_output = os.path.join(tmp_dir, fpath_output)
80
+
81
+ # build ffmpeg cmd string
82
+ p_in = ''.join((self.ff_cmd(segment_start_times), ' {}'.format(shlex.quote(fpath_output))))
83
+ self._log(p_in, LogLevel.FFMPEG)
84
+
85
+ # run ffmpeg command as a subprocess
86
+ try:
87
+ _, task_elapsed = run_cmd(p_in)
88
+ task_result.add_task_step_duration(task_elapsed)
89
+ except CmdProcessingError as e:
90
+ task_result.add_task_step_info_msg('A problem while processing media file:\n\t{0}' \
91
+ '\nOriginal error message:\n\t{1}' \
92
+ .format(self.fpath, e.args[0]))
93
+ else:
94
+ # move split files to target directory
95
+ for segmented_fname in os.listdir(tmp_dir):
96
+ if fnmatch.fnmatch(segmented_fname, '*{}'.format(fname_ext)):
97
+ segmented_fpath = os.path.join(tmp_dir, segmented_fname)
98
+
99
+ # restore tags if needed
100
+ self._restore_tags(segmented_fpath)
101
+
102
+ # move fragmented file to target dir
103
+ shutil.move(segmented_fpath, self.target_dir)
104
+
105
+ # all well
106
+ task_result.succeeded = True
107
+
108
+ task_result.add_report_msg(self.fpath)
109
+ return task_result
110
+
111
+ @timed
112
+ def _segment_start_times(self):
113
+ silence_entries = FFH.silence_detector(self.fpath,
114
+ min_duration = self.silence_min_duration,
115
+ noise_tolerance_amplitude_ratio = self.silence_noise_tolerance_amplitude_ratio)
116
+
117
+ # silence entry duration
118
+ duration = lambda silence_entry: silence_entry.silence_end - silence_entry.silence_start
119
+
120
+ # auto-duration filter
121
+ if self.silence_auto_duration:
122
+ durations = [duration(silence_entry) for silence_entry in silence_entries]
123
+ min_duration = MiscHelpers.percentile(durations, 25)
124
+ silence_entries = [silence_entry for silence_entry in silence_entries if duration(silence_entry) > min_duration]
125
+
126
+ segment_start_times = []
127
+ for silence_entry in silence_entries:
128
+ # trim silences start duration
129
+ silence_start = lambda silence_entry : silence_entry.silence_start \
130
+ if duration(silence_entry) < self.silence_target_trimmed_duration \
131
+ else silence_entry.silence_end - self.silence_target_trimmed_duration
132
+ segment_start_times.append(str(silence_start(silence_entry)))
133
+
134
+ return segment_start_times
135
+
136
+
137
+ class SilenceSplitter(FFMPRunner):
138
+ def silence_split(self, ff_entry_params):
139
+ ''' Segment media file by specified silence
140
+ '''
141
+
142
+ ff_entry_params.target_dir_prefix = 'silence_split'
143
+ media_files, target_dirs = self._prepare_files(ff_entry_params)
144
+
145
+ # build tasks
146
+ tasks = []
147
+ tasks_params = [(media_file, target_dir_path, ff_entry_params.log_level, ff_entry_params.ff_general_options,
148
+ ff_entry_params.ff_other_options, ff_entry_params.preserve_metadata, ff_entry_params.reset_timestamps,
149
+ ff_entry_params.silence_min_duration, ff_entry_params.silence_noise_tolerance_amplitude_ratio,
150
+ ff_entry_params.silence_auto_duration, ff_entry_params.silence_target_trimmed_duration)
151
+ for media_file, target_dir_path in zip(media_files, target_dirs)]
152
+
153
+ for task_param in tasks_params:
154
+ task = SilenceSplitterTask(*task_param)
155
+ tasks.append(task)
156
+
157
+ # run tasks
158
+ self.run_tasks(tasks, serial_exec = ff_entry_params.serial_exec, quiet = ff_entry_params.quiet)
159
+
@@ -0,0 +1,189 @@
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, shlex
16
+ from enum import IntEnum
17
+ from batchmp.fstools.walker import DWalker
18
+ from batchmp.commons.utils import MiscHelpers
19
+ from batchmp.commons.taskprocessor import Task, TasksProcessor
20
+ from batchmp.ffmptools.ffutils import FFH, FFmpegNotInstalled
21
+ from batchmp.tags.handlers.mtghandler import MutagenTagHandler
22
+ from batchmp.tags.handlers.ffmphandler import FFmpegTagHandler
23
+ from batchmp.tags.handlers.tagsholder import TagHolder
24
+ from batchmp.fstools.fsutils import UniqueDirNamesChecker
25
+ from batchmp.ffmptools.ffcommands.cmdopt import FFmpegCommands, FFmpegBitMaskOptions
26
+
27
+
28
+ class FFMPRunnerTask(Task):
29
+ ''' Represents an abstract FFMP Runner task
30
+ '''
31
+ def __init__(self, fpath, target_dir, log_level,
32
+ ff_general_options, ff_other_options, preserve_metadata):
33
+ self.fpath = fpath
34
+ self.target_dir = target_dir
35
+ self.log_level = log_level
36
+
37
+ self.ff_general_options = FFmpegBitMaskOptions.ff_general_options(ff_general_options)
38
+ self.ff_other_options = ff_other_options
39
+
40
+ self.tag_holder = TagHolder() if preserve_metadata else None
41
+
42
+ self._check_defaults()
43
+
44
+ @property
45
+ def ff_cmd(self):
46
+ ''' Base FFmpeg command builder
47
+ '''
48
+ return ''.join(('ffmpeg',
49
+ FFmpegCommands.LOG_LEVEL_ERROR,
50
+ ' -i {}'.format(shlex.quote(self.fpath)),
51
+ self.ff_general_options,
52
+ self.ff_other_options))
53
+
54
+ # Helpers
55
+ def _check_defaults(self):
56
+ if not self.ff_other_options:
57
+ self.ff_other_options = FFmpegCommands.CONVERT_COPY_VBR_QUALITY
58
+
59
+ if not self.ff_general_options:
60
+ self.ff_general_options = FFmpegBitMaskOptions.ff_general_options(
61
+ FFmpegBitMaskOptions.COPY_CODECS | FFmpegBitMaskOptions.MAP_ALL_STREAMS)
62
+
63
+ if self.ff_other_options == FFmpegCommands.CONVERT_COPY_VBR_QUALITY:
64
+ self.ff_other_options += self._ff_cmd_exclude_artwork_streams()
65
+
66
+ def _store_tags(self):
67
+ if self.tag_holder:
68
+ handler = MutagenTagHandler() + FFmpegTagHandler()
69
+ if handler.can_handle(self.fpath):
70
+ self.tag_holder.copy_tags(handler.tag_holder)
71
+
72
+ def _restore_tags(self, fpath):
73
+ if self.tag_holder:
74
+ handler = MutagenTagHandler() + FFmpegTagHandler()
75
+ if handler.can_handle(fpath):
76
+ handler.tag_holder.copy_tags(self.tag_holder)
77
+ handler.save()
78
+
79
+ def _log(self, msg, type):
80
+ if self.log_level and self.log_level >= type:
81
+ # quick log
82
+ print(msg)
83
+
84
+ # FFmpeg command parts builders
85
+ def _ff_cmd_exclude_artwork_streams(self):
86
+ media_entry = FFH.media_file_info_full(self.fpath)
87
+ exclude_artworks_cmd = ''
88
+ if media_entry:
89
+ for artwork_stream in media_entry.artwork_streams:
90
+ idx = artwork_stream.get('index')
91
+ if idx is not None:
92
+ exclude_artworks_cmd = '{0} {1}'.format(exclude_artworks_cmd,
93
+ FFmpegCommands.exclude_input_stream(idx))
94
+ return exclude_artworks_cmd
95
+
96
+
97
+ class LogLevel(IntEnum):
98
+ QUIET = 0
99
+ FFMPEG = 1
100
+ VERBOSE = 2
101
+
102
+
103
+ class FFMPRunner:
104
+ ''' Base FFMPRunner
105
+ '''
106
+ def __init__(self):
107
+ if not FFH.ffmpeg_installed():
108
+ print(FFmpegNotInstalled().default_message)
109
+ sys.exit(0)
110
+
111
+ def run_tasks(self, tasks, msg = None, serial_exec = False, quiet = False):
112
+ if tasks and len(tasks) > 0:
113
+ print('{0} media files to process'.format(len(tasks)) if msg is None else msg)
114
+
115
+ (tasks_results, cpu_core_time), total_elapsed = TasksProcessor().process_tasks(tasks,
116
+ serial_exec = serial_exec,
117
+ quiet = quiet)
118
+ # print run report
119
+ if not quiet:
120
+ self.run_report(tasks_results, cpu_core_time, total_elapsed)
121
+ else:
122
+ print('No media files to process')
123
+
124
+
125
+ def run_report(self, tasks_results, cpu_core_time, total_elapsed):
126
+ ''' Info summary on executed FFMP commands
127
+ '''
128
+ succeeded = sum(1 for result in tasks_results if result.succeeded)
129
+ failed = sum(1 for result in tasks_results if not result.succeeded)
130
+
131
+ total_elapsed_str = MiscHelpers.time_delta_str(total_elapsed)
132
+ cpu_core_time_str = MiscHelpers.time_delta_str(cpu_core_time)
133
+
134
+ num_tasks = len(tasks_results)
135
+ print('Finished running {0} task{1} '\
136
+ '(Succeeded: {2}, Failed: {3})'.format(num_tasks,
137
+ '' if num_tasks == 1 else 's',
138
+ succeeded, failed))
139
+ print('Cumulative FFmpeg CPU Cores time: {}'.format(cpu_core_time_str))
140
+ print('Total running time: {}'.format(total_elapsed_str))
141
+
142
+
143
+ ## Internal helpers
144
+ @staticmethod
145
+ def _prepare_files(ff_entry_params, pass_filter = None):
146
+ ''' Builds a list of matching media files to process,
147
+ along with their respective target out dirs
148
+ '''
149
+ if not pass_filter:
150
+ pass_filter = lambda fpath: FFH.ffmpeg_supported_media(fpath)
151
+
152
+ media_files = [entry.realpath for entry in DWalker.file_entries(ff_entry_params, pass_filter = pass_filter)]
153
+
154
+ target_dirs = FFMPRunner._setup_target_dirs(ff_entry_params, fpathes = media_files)
155
+
156
+ return media_files, target_dirs
157
+
158
+ @staticmethod
159
+ def _setup_target_dirs(ff_entry_params, fpathes = None):
160
+ # check inputs
161
+ # target dir prefix
162
+ DEFAULT_TARGET_DIR_PREFIX = 'processed'
163
+ if ff_entry_params.target_dir_prefix is None:
164
+ ff_entry_params.target_dir_prefix = DEFAULT_TARGET_DIR_PREFIX
165
+ # target dir
166
+ if ff_entry_params.target_dir is None:
167
+ ff_entry_params.target_dir = os.path.dirname(ff_entry_params.src_dir)
168
+
169
+ # target path (within the target dir)
170
+ target_dir_name = '{0}_{1}'.format(os.path.basename(ff_entry_params.src_dir), ff_entry_params.target_dir_prefix)
171
+ target_dir_name = UniqueDirNamesChecker(ff_entry_params.target_dir).unique_name(target_dir_name)
172
+ target_path_dir = os.path.join(ff_entry_params.target_dir, target_dir_name)
173
+
174
+ # target dirs
175
+ target_dirs = []
176
+ for fpath in fpathes:
177
+ relpath = os.path.relpath(os.path.dirname(fpath), ff_entry_params.src_dir)
178
+ if relpath.startswith(os.pardir):
179
+ raise ValueError('File not in specified source directory or its subfolders')
180
+ elif relpath.endswith('{}'.format(os.path.curdir)):
181
+ relpath = relpath[:-1]
182
+
183
+ target_path = os.path.join(target_path_dir, relpath)
184
+ if not os.path.exists(target_path):
185
+ os.makedirs(target_path)
186
+ target_dirs.append(target_path)
187
+
188
+ return target_dirs
189
+