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