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