deezy 1.2.6__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.
- cli/__init__.py +1075 -0
- cli/payload_factory.py +228 -0
- cli/utils.py +170 -0
- deezy/__init__.py +0 -0
- deezy/audio_encoders/base.py +120 -0
- deezy/audio_encoders/dee/ac4.py +274 -0
- deezy/audio_encoders/dee/atmos.py +184 -0
- deezy/audio_encoders/dee/base.py +240 -0
- deezy/audio_encoders/dee/dd.py +318 -0
- deezy/audio_encoders/dee/ddp.py +324 -0
- deezy/audio_encoders/dee/json/ac4_base.py +74 -0
- deezy/audio_encoders/dee/json/atmos_base.py +73 -0
- deezy/audio_encoders/dee/json/dd_base.py +86 -0
- deezy/audio_encoders/dee/json/dee_json_generator.py +240 -0
- deezy/audio_encoders/delay/__init__.py +143 -0
- deezy/audio_processors/dee.py +214 -0
- deezy/audio_processors/ffmpeg.py +176 -0
- deezy/audio_processors/truehdd.py +267 -0
- deezy/config/__init__.py +8 -0
- deezy/config/defaults.py +90 -0
- deezy/config/manager.py +223 -0
- deezy/enums/__init__.py +86 -0
- deezy/enums/ac4.py +36 -0
- deezy/enums/atmos.py +50 -0
- deezy/enums/dd.py +82 -0
- deezy/enums/ddp.py +198 -0
- deezy/enums/ddp_bluray.py +26 -0
- deezy/enums/shared.py +150 -0
- deezy/exceptions/__init__.py +42 -0
- deezy/info/__init__.py +206 -0
- deezy/payloads/ac4.py +19 -0
- deezy/payloads/atmos.py +11 -0
- deezy/payloads/dd.py +9 -0
- deezy/payloads/ddp.py +10 -0
- deezy/payloads/shared.py +88 -0
- deezy/track_info/audio_track_info.py +19 -0
- deezy/track_info/mediainfo.py +248 -0
- deezy/track_info/track_index.py +104 -0
- deezy/utils/__init__.py +0 -0
- deezy/utils/_version.py +33 -0
- deezy/utils/dependencies.py +82 -0
- deezy/utils/exit.py +24 -0
- deezy/utils/file_parser.py +42 -0
- deezy/utils/logger.py +49 -0
- deezy/utils/progress.py +233 -0
- deezy/utils/utils.py +39 -0
- deezy-1.2.6.dist-info/METADATA +900 -0
- deezy-1.2.6.dist-info/RECORD +51 -0
- deezy-1.2.6.dist-info/WHEEL +5 -0
- deezy-1.2.6.dist-info/licenses/LICENSE +21 -0
- deezy-1.2.6.dist-info/top_level.txt +2 -0
cli/__init__.py
ADDED
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from cli.payload_factory import PayloadBuilder
|
|
7
|
+
from cli.utils import (
|
|
8
|
+
CustomHelpFormatter,
|
|
9
|
+
clean_deezy_temp_folders,
|
|
10
|
+
dialnorm_options,
|
|
11
|
+
get_deezy_temp_info,
|
|
12
|
+
int_0_100,
|
|
13
|
+
validate_track_index,
|
|
14
|
+
)
|
|
15
|
+
from deezy.audio_encoders.dee.ac4 import Ac4Encoder
|
|
16
|
+
from deezy.audio_encoders.dee.atmos import AtmosEncoder
|
|
17
|
+
from deezy.audio_encoders.dee.dd import DDEncoderDEE
|
|
18
|
+
from deezy.audio_encoders.dee.ddp import DDPEncoderDEE
|
|
19
|
+
from deezy.config.defaults import get_default_config_path
|
|
20
|
+
from deezy.config.manager import ConfigManager, get_config_manager
|
|
21
|
+
from deezy.enums import case_insensitive_enum, enum_choices
|
|
22
|
+
from deezy.enums.ac4 import Ac4EncodingProfile
|
|
23
|
+
from deezy.enums.atmos import AtmosMode, WarpMode
|
|
24
|
+
from deezy.enums.dd import DolbyDigitalChannels
|
|
25
|
+
from deezy.enums.ddp import DolbyDigitalPlusChannels
|
|
26
|
+
from deezy.enums.ddp_bluray import DolbyDigitalPlusBlurayChannels
|
|
27
|
+
from deezy.enums.shared import TrackType
|
|
28
|
+
from deezy.enums.shared import DeeDRC, LogLevel, MeteringMode, StereoDownmix
|
|
29
|
+
from deezy.info import parse_audio_streams
|
|
30
|
+
from deezy.track_info.track_index import TrackIndex
|
|
31
|
+
from deezy.utils._version import __version__, program_name
|
|
32
|
+
from deezy.utils.dependencies import DependencyNotFoundError, FindDependencies
|
|
33
|
+
from deezy.utils.exit import EXIT_FAIL, EXIT_SUCCESS, exit_application
|
|
34
|
+
from deezy.utils.file_parser import parse_input_s
|
|
35
|
+
from deezy.utils.logger import logger, logger_manager
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_main_parser() -> argparse.ArgumentParser:
|
|
39
|
+
"""Create the main argument parser with global options."""
|
|
40
|
+
parser = argparse.ArgumentParser(prog=program_name)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--config",
|
|
46
|
+
type=str,
|
|
47
|
+
help="Path to configuration file (default: deezy-conf.toml beside executable)",
|
|
48
|
+
metavar="CONFIG_FILE",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--log-level",
|
|
52
|
+
type=case_insensitive_enum(LogLevel),
|
|
53
|
+
default=LogLevel.INFO,
|
|
54
|
+
choices=LogLevel,
|
|
55
|
+
metavar=enum_choices(LogLevel),
|
|
56
|
+
help="Sets the log level (defaults to INFO).",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--log-to-file",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Write log to file (defaults to input path with suffix of .log).",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--no-progress-bars",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Disables progress bars on level INFO (disabled for DEBUG or higher).",
|
|
67
|
+
)
|
|
68
|
+
return parser
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_common_argument_groups() -> dict[str, argparse.ArgumentParser]:
|
|
72
|
+
"""Create reusable argument groups."""
|
|
73
|
+
# input files argument group
|
|
74
|
+
input_group = argparse.ArgumentParser(add_help=False)
|
|
75
|
+
input_group.add_argument(
|
|
76
|
+
"input", nargs="+", help="Input file paths or directories", metavar="INPUT"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# common Encode Args
|
|
80
|
+
encode_group = argparse.ArgumentParser(add_help=False)
|
|
81
|
+
encode_group.add_argument(
|
|
82
|
+
"--ffmpeg",
|
|
83
|
+
type=str,
|
|
84
|
+
help="Path to FFMPEG executable.",
|
|
85
|
+
)
|
|
86
|
+
encode_group.add_argument(
|
|
87
|
+
"--truehdd",
|
|
88
|
+
type=str,
|
|
89
|
+
help="Path to Truehdd executable.",
|
|
90
|
+
)
|
|
91
|
+
encode_group.add_argument(
|
|
92
|
+
"--dee",
|
|
93
|
+
type=str,
|
|
94
|
+
help="Path to Dolby Encoding Engine executable.",
|
|
95
|
+
)
|
|
96
|
+
encode_group.add_argument(
|
|
97
|
+
"--track-index",
|
|
98
|
+
type=validate_track_index,
|
|
99
|
+
default=TrackIndex(TrackType.AUDIO, 0),
|
|
100
|
+
help="Track to use for encoding. Supports: 'N' (audio track N), 'a:N' (audio track N), 's:N' (stream index N).",
|
|
101
|
+
)
|
|
102
|
+
encode_group.add_argument(
|
|
103
|
+
"--delay",
|
|
104
|
+
type=str,
|
|
105
|
+
help="The delay in milliseconds or seconds. Note '--delay=' is required! (--delay=-10ms / --delay=10s).",
|
|
106
|
+
)
|
|
107
|
+
encode_group.add_argument(
|
|
108
|
+
"--keep-temp",
|
|
109
|
+
action="store_true",
|
|
110
|
+
help="Keeps the temp files after finishing.",
|
|
111
|
+
)
|
|
112
|
+
encode_group.add_argument(
|
|
113
|
+
"--temp-dir",
|
|
114
|
+
type=str,
|
|
115
|
+
help=(
|
|
116
|
+
"Path to store temporary files to. If not specified this will "
|
|
117
|
+
"automatically happen in the temp dir of the os."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
encode_group.add_argument(
|
|
121
|
+
"--output",
|
|
122
|
+
type=str,
|
|
123
|
+
help=(
|
|
124
|
+
"The output file path. If not specified we will attempt to automatically add "
|
|
125
|
+
"Delay/Language string to output file name."
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# bitrate group
|
|
130
|
+
bitrate_group = argparse.ArgumentParser(add_help=False)
|
|
131
|
+
bitrate_group.add_argument(
|
|
132
|
+
"--bitrate",
|
|
133
|
+
type=int,
|
|
134
|
+
default=None,
|
|
135
|
+
help=(
|
|
136
|
+
"The bitrate in Kbps (If too high or low for you desired layout, "
|
|
137
|
+
"the bitrate will automatically be adjusted to the closest allowed bitrate)."
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# DD/DDP/Atmos DRC group (limited choices)
|
|
142
|
+
dd_ddp_atmos_drc_group = argparse.ArgumentParser(add_help=False)
|
|
143
|
+
dd_ddp_atmos_drc_choices = (
|
|
144
|
+
DeeDRC.FILM_STANDARD,
|
|
145
|
+
DeeDRC.FILM_LIGHT,
|
|
146
|
+
DeeDRC.MUSIC_STANDARD,
|
|
147
|
+
DeeDRC.MUSIC_LIGHT,
|
|
148
|
+
DeeDRC.SPEECH,
|
|
149
|
+
)
|
|
150
|
+
dd_ddp_atmos_drc_group.add_argument(
|
|
151
|
+
"--drc-line-mode",
|
|
152
|
+
type=case_insensitive_enum(DeeDRC),
|
|
153
|
+
choices=dd_ddp_atmos_drc_choices,
|
|
154
|
+
metavar=enum_choices(dd_ddp_atmos_drc_choices),
|
|
155
|
+
default=DeeDRC.FILM_LIGHT,
|
|
156
|
+
help="Dynamic range compression settings.",
|
|
157
|
+
)
|
|
158
|
+
dd_ddp_atmos_drc_group.add_argument(
|
|
159
|
+
"--drc-rf-mode",
|
|
160
|
+
type=case_insensitive_enum(DeeDRC),
|
|
161
|
+
choices=dd_ddp_atmos_drc_choices,
|
|
162
|
+
metavar=enum_choices(dd_ddp_atmos_drc_choices),
|
|
163
|
+
default=DeeDRC.FILM_LIGHT,
|
|
164
|
+
help="Dynamic range compression settings.",
|
|
165
|
+
)
|
|
166
|
+
dd_ddp_atmos_drc_group.add_argument(
|
|
167
|
+
"--custom-dialnorm",
|
|
168
|
+
type=dialnorm_options,
|
|
169
|
+
default=0,
|
|
170
|
+
help="Custom dialnorm (0 disables custom dialnorm).",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# shared loudness arguments (common to all formats)
|
|
174
|
+
loudness_group = argparse.ArgumentParser(add_help=False)
|
|
175
|
+
loudness_group.add_argument(
|
|
176
|
+
"--no-dialogue-intelligence",
|
|
177
|
+
action="store_false",
|
|
178
|
+
help="Dialogue Intelligence enabled. Option ignored for 1770-1 or LeqA metering mode.",
|
|
179
|
+
)
|
|
180
|
+
loudness_group.add_argument(
|
|
181
|
+
"--speech-threshold",
|
|
182
|
+
type=int_0_100,
|
|
183
|
+
default=15,
|
|
184
|
+
help=(
|
|
185
|
+
"[0-100] If the percentage of speech is higher than the threshold, the encoder uses speech "
|
|
186
|
+
"gating to set the dialnorm value. (Otherwise, the encoder uses level gating)."
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# DD/DDP limited metering mode (excludes MODE_1770_4)
|
|
191
|
+
dd_ddp_metering_group = argparse.ArgumentParser(add_help=False)
|
|
192
|
+
dd_ddp_metering_choices = (
|
|
193
|
+
MeteringMode.MODE_1770_1,
|
|
194
|
+
MeteringMode.MODE_1770_2,
|
|
195
|
+
MeteringMode.MODE_1770_3,
|
|
196
|
+
MeteringMode.MODE_LEQA,
|
|
197
|
+
)
|
|
198
|
+
dd_ddp_metering_group.add_argument(
|
|
199
|
+
"--metering-mode",
|
|
200
|
+
type=case_insensitive_enum(MeteringMode),
|
|
201
|
+
choices=dd_ddp_metering_choices,
|
|
202
|
+
metavar=enum_choices(dd_ddp_metering_choices),
|
|
203
|
+
default=MeteringMode.MODE_1770_3,
|
|
204
|
+
help="Loudness measuring mode according to one of the broadcast standards.",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Atmos/AC4 full metering mode (includes MODE_1770_4)
|
|
208
|
+
atmos_ac4_metering_group = argparse.ArgumentParser(add_help=False)
|
|
209
|
+
atmos_ac4_metering_group.add_argument(
|
|
210
|
+
"--metering-mode",
|
|
211
|
+
type=case_insensitive_enum(MeteringMode),
|
|
212
|
+
choices=MeteringMode,
|
|
213
|
+
metavar=enum_choices(MeteringMode),
|
|
214
|
+
default=MeteringMode.MODE_1770_4,
|
|
215
|
+
help="Loudness measuring mode according to one of the broadcast standards.",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# DD/DDP specific processing options
|
|
219
|
+
dd_ddp_processing_group = argparse.ArgumentParser(add_help=False)
|
|
220
|
+
dd_ddp_processing_group.add_argument(
|
|
221
|
+
"--no-low-pass-filter",
|
|
222
|
+
action="store_false",
|
|
223
|
+
help="Disables low pass filter.",
|
|
224
|
+
)
|
|
225
|
+
dd_ddp_processing_group.add_argument(
|
|
226
|
+
"--no-surround-3db",
|
|
227
|
+
action="store_false",
|
|
228
|
+
help="Disables surround 3db attenuation.",
|
|
229
|
+
)
|
|
230
|
+
dd_ddp_processing_group.add_argument(
|
|
231
|
+
"--no-surround-90-deg-phase-shift",
|
|
232
|
+
action="store_false",
|
|
233
|
+
help="Disables surround 90 degree phase shift.",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# stereo downmix mode
|
|
237
|
+
stereo_downmix_group = argparse.ArgumentParser(add_help=False)
|
|
238
|
+
stereo_downmix_group.add_argument(
|
|
239
|
+
"--stereo-down-mix",
|
|
240
|
+
type=case_insensitive_enum(StereoDownmix),
|
|
241
|
+
choices=StereoDownmix,
|
|
242
|
+
default=StereoDownmix.LORO,
|
|
243
|
+
metavar=enum_choices(StereoDownmix),
|
|
244
|
+
help="Down mix method for stereo.",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# downmix metadata levels
|
|
248
|
+
downmix_metadata_group = argparse.ArgumentParser(add_help=False)
|
|
249
|
+
downmix_metadata_group.add_argument(
|
|
250
|
+
"--lt-rt-center",
|
|
251
|
+
type=str,
|
|
252
|
+
choices=("+3", "+1.5", "0", "-1.5", "-3", "-4.5", "-6", "-inf"),
|
|
253
|
+
default="-3",
|
|
254
|
+
help="Lt/Rt center downmix level.",
|
|
255
|
+
)
|
|
256
|
+
downmix_metadata_group.add_argument(
|
|
257
|
+
"--lt-rt-surround",
|
|
258
|
+
type=str,
|
|
259
|
+
choices=("-1.5", "-3", "-4.5", "-6", "-inf"),
|
|
260
|
+
default="-3",
|
|
261
|
+
help="Lt/Rt surround downmix level.",
|
|
262
|
+
)
|
|
263
|
+
downmix_metadata_group.add_argument(
|
|
264
|
+
"--lo-ro-center",
|
|
265
|
+
type=str,
|
|
266
|
+
choices=("+3", "+1.5", "0", "-1.5", "-3", "-4.5", "-6", "-inf"),
|
|
267
|
+
default="-3",
|
|
268
|
+
help="Lo/Ro center downmix level.",
|
|
269
|
+
)
|
|
270
|
+
downmix_metadata_group.add_argument(
|
|
271
|
+
"--lo-ro-surround",
|
|
272
|
+
type=str,
|
|
273
|
+
choices=("-1.5", "-3", "-4.5", "-6", "-inf"),
|
|
274
|
+
default="-3",
|
|
275
|
+
help="Lo/Ro surround downmix level.",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# DD channels group
|
|
279
|
+
dd_channels_group = argparse.ArgumentParser(add_help=False)
|
|
280
|
+
dd_channels_group.add_argument(
|
|
281
|
+
"--channels",
|
|
282
|
+
type=case_insensitive_enum(DolbyDigitalChannels),
|
|
283
|
+
choices=DolbyDigitalChannels,
|
|
284
|
+
default=DolbyDigitalChannels.AUTO,
|
|
285
|
+
metavar=enum_choices(DolbyDigitalChannels),
|
|
286
|
+
help="The number of channels.",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# DDP channels group
|
|
290
|
+
ddp_channels_group = argparse.ArgumentParser(add_help=False)
|
|
291
|
+
ddp_channels_group.add_argument(
|
|
292
|
+
"--channels",
|
|
293
|
+
type=case_insensitive_enum(DolbyDigitalPlusChannels),
|
|
294
|
+
choices=DolbyDigitalPlusChannels,
|
|
295
|
+
default=DolbyDigitalPlusChannels.AUTO,
|
|
296
|
+
metavar=enum_choices(DolbyDigitalPlusChannels),
|
|
297
|
+
help="The number of channels.",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# DDP BluRay channels group
|
|
301
|
+
ddp_bluray_channels_group = argparse.ArgumentParser(add_help=False)
|
|
302
|
+
ddp_bluray_channels_group.add_argument(
|
|
303
|
+
"--channels",
|
|
304
|
+
type=case_insensitive_enum(DolbyDigitalPlusBlurayChannels),
|
|
305
|
+
choices=DolbyDigitalPlusBlurayChannels,
|
|
306
|
+
default=DolbyDigitalPlusBlurayChannels.SURROUNDEX,
|
|
307
|
+
metavar=enum_choices(DolbyDigitalPlusBlurayChannels),
|
|
308
|
+
help="The number of channels.",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# atmos specific options
|
|
312
|
+
atmos_options_group = argparse.ArgumentParser(add_help=False)
|
|
313
|
+
atmos_options_group.add_argument(
|
|
314
|
+
"--atmos-mode",
|
|
315
|
+
type=case_insensitive_enum(AtmosMode),
|
|
316
|
+
choices=AtmosMode,
|
|
317
|
+
default=AtmosMode.STREAMING,
|
|
318
|
+
metavar=enum_choices(AtmosMode),
|
|
319
|
+
help="Atmos encoding mode.",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# trueHDD options (shared by Atmos and AC4)
|
|
323
|
+
truehdd_options_group = argparse.ArgumentParser(add_help=False)
|
|
324
|
+
truehdd_options_group.add_argument(
|
|
325
|
+
"--thd-warp-mode",
|
|
326
|
+
type=case_insensitive_enum(WarpMode),
|
|
327
|
+
choices=WarpMode,
|
|
328
|
+
default=WarpMode.NORMAL,
|
|
329
|
+
metavar=enum_choices(WarpMode),
|
|
330
|
+
help="Specify warp mode when not present in metadata (truehdd).",
|
|
331
|
+
)
|
|
332
|
+
truehdd_options_group.add_argument(
|
|
333
|
+
"--bed-conform",
|
|
334
|
+
action="store_true",
|
|
335
|
+
help="Enables bed conformance for Atmos content (truehd).",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# AC4 basic options
|
|
339
|
+
ac4_basic_options_group = argparse.ArgumentParser(add_help=False)
|
|
340
|
+
ac4_basic_options_group.add_argument(
|
|
341
|
+
"--ims-legacy-presentation",
|
|
342
|
+
action="store_true",
|
|
343
|
+
help=(
|
|
344
|
+
"Determines whether the Dolby AC-4 encoder inserts an additional "
|
|
345
|
+
"presentation for backward compatibility."
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
ac4_basic_options_group.add_argument(
|
|
349
|
+
"--encoding-profile",
|
|
350
|
+
type=case_insensitive_enum(Ac4EncodingProfile),
|
|
351
|
+
choices=Ac4EncodingProfile,
|
|
352
|
+
metavar=enum_choices(Ac4EncodingProfile),
|
|
353
|
+
default=Ac4EncodingProfile.IMS,
|
|
354
|
+
help="Encoding profile. For encoding music content, select ims_music.",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# AC4 DRC options (full DeeDRC enum choices)
|
|
358
|
+
ac4_drc_group = argparse.ArgumentParser(add_help=False)
|
|
359
|
+
ac4_drc_args = (
|
|
360
|
+
"--ddp-drc",
|
|
361
|
+
"--flat-panel-drc",
|
|
362
|
+
"--home-theatre-drc",
|
|
363
|
+
"--portable-headphones-drc",
|
|
364
|
+
"--portable-speakers-drc",
|
|
365
|
+
)
|
|
366
|
+
for ac4_drc in ac4_drc_args:
|
|
367
|
+
ac4_drc_group.add_argument(
|
|
368
|
+
ac4_drc,
|
|
369
|
+
type=case_insensitive_enum(DeeDRC),
|
|
370
|
+
choices=DeeDRC,
|
|
371
|
+
metavar=enum_choices(DeeDRC),
|
|
372
|
+
default=DeeDRC.FILM_LIGHT,
|
|
373
|
+
help="Dynamic range compression settings for AC4.",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# preset metering mode (full choices for flexibility)
|
|
377
|
+
preset_metering_group = argparse.ArgumentParser(add_help=False)
|
|
378
|
+
preset_metering_group.add_argument(
|
|
379
|
+
"--metering-mode",
|
|
380
|
+
type=case_insensitive_enum(MeteringMode),
|
|
381
|
+
choices=MeteringMode,
|
|
382
|
+
metavar=enum_choices(MeteringMode),
|
|
383
|
+
help="Loudness measuring mode according to one of the broadcast standards.",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# preset override channels (flexible string type)
|
|
387
|
+
preset_channels_group = argparse.ArgumentParser(add_help=False)
|
|
388
|
+
preset_channels_group.add_argument(
|
|
389
|
+
"--channels",
|
|
390
|
+
type=str,
|
|
391
|
+
help="Override channels setting from preset (format depends on preset's format).",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
"input_group": input_group,
|
|
396
|
+
"encode_group": encode_group,
|
|
397
|
+
"bitrate_group": bitrate_group,
|
|
398
|
+
"dd_ddp_atmos_drc_group": dd_ddp_atmos_drc_group,
|
|
399
|
+
"loudness_group": loudness_group,
|
|
400
|
+
"dd_ddp_metering_group": dd_ddp_metering_group,
|
|
401
|
+
"atmos_ac4_metering_group": atmos_ac4_metering_group,
|
|
402
|
+
"preset_metering_group": preset_metering_group,
|
|
403
|
+
"dd_ddp_processing_group": dd_ddp_processing_group,
|
|
404
|
+
"stereo_downmix_group": stereo_downmix_group,
|
|
405
|
+
"downmix_metadata_group": downmix_metadata_group,
|
|
406
|
+
"dd_channels_group": dd_channels_group,
|
|
407
|
+
"ddp_channels_group": ddp_channels_group,
|
|
408
|
+
"ddp_bluray_channels_group": ddp_bluray_channels_group,
|
|
409
|
+
"atmos_options_group": atmos_options_group,
|
|
410
|
+
"truehdd_options_group": truehdd_options_group,
|
|
411
|
+
"ac4_basic_options_group": ac4_basic_options_group,
|
|
412
|
+
"ac4_drc_group": ac4_drc_group,
|
|
413
|
+
"preset_channels_group": preset_channels_group,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def create_encode_parsers(
|
|
418
|
+
subparsers: argparse._SubParsersAction,
|
|
419
|
+
argument_groups: dict[str, argparse.ArgumentParser],
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Create encode command parsers."""
|
|
422
|
+
# encode command parser
|
|
423
|
+
encode_parser = subparsers.add_parser("encode", help="Encode management")
|
|
424
|
+
encode_subparsers = encode_parser.add_subparsers(
|
|
425
|
+
dest="format_command", required=True
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
### Dolby Digital Command ###
|
|
429
|
+
_encode_dd_parser = encode_subparsers.add_parser(
|
|
430
|
+
"dd",
|
|
431
|
+
parents=(
|
|
432
|
+
argument_groups["input_group"],
|
|
433
|
+
argument_groups["encode_group"],
|
|
434
|
+
argument_groups["bitrate_group"],
|
|
435
|
+
argument_groups["dd_ddp_atmos_drc_group"],
|
|
436
|
+
argument_groups["loudness_group"],
|
|
437
|
+
argument_groups["dd_ddp_metering_group"],
|
|
438
|
+
argument_groups["dd_ddp_processing_group"],
|
|
439
|
+
argument_groups["stereo_downmix_group"],
|
|
440
|
+
argument_groups["downmix_metadata_group"],
|
|
441
|
+
argument_groups["dd_channels_group"],
|
|
442
|
+
),
|
|
443
|
+
formatter_class=lambda prog: CustomHelpFormatter(
|
|
444
|
+
prog,
|
|
445
|
+
width=78,
|
|
446
|
+
max_help_position=7,
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
### Dolby Digital Plus Command ###
|
|
451
|
+
_encode_ddp_parser = encode_subparsers.add_parser(
|
|
452
|
+
"ddp",
|
|
453
|
+
parents=(
|
|
454
|
+
argument_groups["input_group"],
|
|
455
|
+
argument_groups["encode_group"],
|
|
456
|
+
argument_groups["bitrate_group"],
|
|
457
|
+
argument_groups["dd_ddp_atmos_drc_group"],
|
|
458
|
+
argument_groups["loudness_group"],
|
|
459
|
+
argument_groups["dd_ddp_metering_group"],
|
|
460
|
+
argument_groups["dd_ddp_processing_group"],
|
|
461
|
+
argument_groups["stereo_downmix_group"],
|
|
462
|
+
argument_groups["downmix_metadata_group"],
|
|
463
|
+
argument_groups["ddp_channels_group"],
|
|
464
|
+
),
|
|
465
|
+
formatter_class=lambda prog: CustomHelpFormatter(
|
|
466
|
+
prog,
|
|
467
|
+
width=78,
|
|
468
|
+
max_help_position=7,
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
### Dolby Digital Plus BluRay Command ###
|
|
473
|
+
_encode_ddp_bluray_parser = encode_subparsers.add_parser(
|
|
474
|
+
"ddp-bluray",
|
|
475
|
+
parents=(
|
|
476
|
+
argument_groups["input_group"],
|
|
477
|
+
argument_groups["encode_group"],
|
|
478
|
+
argument_groups["bitrate_group"],
|
|
479
|
+
argument_groups["dd_ddp_atmos_drc_group"],
|
|
480
|
+
argument_groups["loudness_group"],
|
|
481
|
+
argument_groups["dd_ddp_metering_group"],
|
|
482
|
+
argument_groups["dd_ddp_processing_group"],
|
|
483
|
+
argument_groups["stereo_downmix_group"],
|
|
484
|
+
argument_groups["downmix_metadata_group"],
|
|
485
|
+
argument_groups["ddp_bluray_channels_group"],
|
|
486
|
+
),
|
|
487
|
+
formatter_class=lambda prog: CustomHelpFormatter(
|
|
488
|
+
prog,
|
|
489
|
+
width=78,
|
|
490
|
+
max_help_position=7,
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
### Atmos Command ###
|
|
495
|
+
_encode_atmos_parser = encode_subparsers.add_parser(
|
|
496
|
+
"atmos",
|
|
497
|
+
parents=(
|
|
498
|
+
argument_groups["input_group"],
|
|
499
|
+
argument_groups["encode_group"],
|
|
500
|
+
argument_groups["bitrate_group"],
|
|
501
|
+
argument_groups["dd_ddp_atmos_drc_group"],
|
|
502
|
+
argument_groups["loudness_group"],
|
|
503
|
+
argument_groups["atmos_ac4_metering_group"],
|
|
504
|
+
argument_groups["downmix_metadata_group"],
|
|
505
|
+
argument_groups["stereo_downmix_group"],
|
|
506
|
+
argument_groups["atmos_options_group"],
|
|
507
|
+
argument_groups["truehdd_options_group"],
|
|
508
|
+
),
|
|
509
|
+
formatter_class=lambda prog: CustomHelpFormatter(
|
|
510
|
+
prog,
|
|
511
|
+
width=78,
|
|
512
|
+
max_help_position=7,
|
|
513
|
+
),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
### AC4 Command ###
|
|
517
|
+
_encode_ac4_parser = encode_subparsers.add_parser(
|
|
518
|
+
"ac4",
|
|
519
|
+
parents=(
|
|
520
|
+
argument_groups["input_group"],
|
|
521
|
+
argument_groups["encode_group"],
|
|
522
|
+
argument_groups["bitrate_group"],
|
|
523
|
+
argument_groups["loudness_group"],
|
|
524
|
+
argument_groups["atmos_ac4_metering_group"],
|
|
525
|
+
argument_groups["ac4_basic_options_group"],
|
|
526
|
+
argument_groups["ac4_drc_group"],
|
|
527
|
+
argument_groups["truehdd_options_group"],
|
|
528
|
+
),
|
|
529
|
+
formatter_class=lambda prog: CustomHelpFormatter(
|
|
530
|
+
prog,
|
|
531
|
+
width=78,
|
|
532
|
+
max_help_position=7,
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
### Preset Command ###
|
|
537
|
+
encode_preset_parser = encode_subparsers.add_parser(
|
|
538
|
+
"preset",
|
|
539
|
+
parents=(
|
|
540
|
+
argument_groups["input_group"],
|
|
541
|
+
argument_groups["encode_group"],
|
|
542
|
+
argument_groups["bitrate_group"],
|
|
543
|
+
argument_groups["dd_ddp_atmos_drc_group"],
|
|
544
|
+
argument_groups["loudness_group"],
|
|
545
|
+
argument_groups["preset_metering_group"],
|
|
546
|
+
argument_groups["dd_ddp_processing_group"],
|
|
547
|
+
argument_groups["stereo_downmix_group"],
|
|
548
|
+
argument_groups["downmix_metadata_group"],
|
|
549
|
+
argument_groups["preset_channels_group"],
|
|
550
|
+
argument_groups["atmos_options_group"],
|
|
551
|
+
argument_groups["truehdd_options_group"],
|
|
552
|
+
argument_groups["ac4_basic_options_group"],
|
|
553
|
+
argument_groups["ac4_drc_group"],
|
|
554
|
+
),
|
|
555
|
+
formatter_class=lambda prog: CustomHelpFormatter(
|
|
556
|
+
prog,
|
|
557
|
+
width=78,
|
|
558
|
+
max_help_position=7,
|
|
559
|
+
),
|
|
560
|
+
help="Encode using a preset configuration (format determined from preset).",
|
|
561
|
+
)
|
|
562
|
+
encode_preset_parser.add_argument(
|
|
563
|
+
"--name",
|
|
564
|
+
dest="preset_name",
|
|
565
|
+
required=True,
|
|
566
|
+
help="Name of the preset to use for encoding.",
|
|
567
|
+
metavar="PRESET_NAME",
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def create_other_parsers(
|
|
572
|
+
subparsers: argparse._SubParsersAction,
|
|
573
|
+
argument_groups: dict[str, argparse.ArgumentParser],
|
|
574
|
+
) -> None:
|
|
575
|
+
"""Create find, info, and config command parsers."""
|
|
576
|
+
# find command parser
|
|
577
|
+
find_parser = subparsers.add_parser(
|
|
578
|
+
"find", parents=[argument_groups["input_group"]], help="Find management"
|
|
579
|
+
)
|
|
580
|
+
find_parser.add_argument(
|
|
581
|
+
"-n",
|
|
582
|
+
"--name",
|
|
583
|
+
action="store_true",
|
|
584
|
+
help="Only display names instead of full paths.",
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# info command parser
|
|
588
|
+
_info_parser = subparsers.add_parser(
|
|
589
|
+
"info", parents=[argument_groups["input_group"]], help="Info management"
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# config command parser
|
|
593
|
+
config_parser = subparsers.add_parser("config", help="Configuration management")
|
|
594
|
+
config_subparsers = config_parser.add_subparsers(
|
|
595
|
+
dest="config_command", required=True
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# generate config subcommand
|
|
599
|
+
generate_parser = config_subparsers.add_parser(
|
|
600
|
+
"generate", help="Generate configuration file"
|
|
601
|
+
)
|
|
602
|
+
generate_parser.add_argument(
|
|
603
|
+
"-o",
|
|
604
|
+
"--output",
|
|
605
|
+
type=str,
|
|
606
|
+
help="Output path for config file (default: auto-detect)",
|
|
607
|
+
)
|
|
608
|
+
generate_parser.add_argument(
|
|
609
|
+
"--overwrite", action="store_true", help="Overwrite existing config file"
|
|
610
|
+
)
|
|
611
|
+
generate_parser.add_argument(
|
|
612
|
+
"--from-args",
|
|
613
|
+
action="store_true",
|
|
614
|
+
help="Generate config from current CLI arguments (use with encode command)",
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# info config subcommand
|
|
618
|
+
info_parser = config_subparsers.add_parser(
|
|
619
|
+
"info", help="Show configuration information"
|
|
620
|
+
)
|
|
621
|
+
info_parser.add_argument("--path", type=str, help="Show specific config file path")
|
|
622
|
+
|
|
623
|
+
# validate config subcommand
|
|
624
|
+
validate_parser = config_subparsers.add_parser(
|
|
625
|
+
"validate", help="Validate configuration file"
|
|
626
|
+
)
|
|
627
|
+
validate_parser.add_argument("--preset", type=str, help="Validate specific preset")
|
|
628
|
+
|
|
629
|
+
# list presets subcommand
|
|
630
|
+
list_parser = config_subparsers.add_parser(
|
|
631
|
+
"list-presets", help="List available presets"
|
|
632
|
+
)
|
|
633
|
+
list_parser.add_argument(
|
|
634
|
+
"--detailed", action="store_true", help="Show detailed preset information"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# temp management command parser
|
|
638
|
+
temp_parser = subparsers.add_parser("temp", help="Temporary folder management")
|
|
639
|
+
temp_subparsers = temp_parser.add_subparsers(dest="temp_command", required=True)
|
|
640
|
+
|
|
641
|
+
# clean temp subcommand
|
|
642
|
+
clean_parser = temp_subparsers.add_parser("clean", help="Clean DeeZy temp folders")
|
|
643
|
+
clean_parser.add_argument(
|
|
644
|
+
"--max-age",
|
|
645
|
+
type=int,
|
|
646
|
+
default=24,
|
|
647
|
+
help="Remove folders older than N hours (default: 24)",
|
|
648
|
+
metavar="HOURS",
|
|
649
|
+
)
|
|
650
|
+
clean_parser.add_argument(
|
|
651
|
+
"--dry-run",
|
|
652
|
+
action="store_true",
|
|
653
|
+
help="Show what would be deleted without actually deleting",
|
|
654
|
+
)
|
|
655
|
+
# info temp subcommand
|
|
656
|
+
temp_subparsers.add_parser("info", help="Show temp folder information")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def apply_default_bitrate(
|
|
660
|
+
args: argparse.Namespace, config_manager: ConfigManager | None
|
|
661
|
+
) -> None:
|
|
662
|
+
"""Apply default bitrate based on format and channels/mode if none specified."""
|
|
663
|
+
if not hasattr(args, "bitrate") or args.bitrate is None:
|
|
664
|
+
# determine the channel/mode key based on format
|
|
665
|
+
channels_or_mode = None
|
|
666
|
+
if args.format_command == "atmos":
|
|
667
|
+
# for Atmos, use the mode (streaming/bluray)
|
|
668
|
+
if hasattr(args, "atmos_mode") and args.atmos_mode:
|
|
669
|
+
channels_or_mode = (
|
|
670
|
+
args.atmos_mode.value
|
|
671
|
+
if hasattr(args.atmos_mode, "value")
|
|
672
|
+
else str(args.atmos_mode)
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
# for DD/DDP, use channels
|
|
676
|
+
if hasattr(args, "channels") and args.channels:
|
|
677
|
+
channels_or_mode = args.channels
|
|
678
|
+
|
|
679
|
+
# get default bitrate from config
|
|
680
|
+
if config_manager is not None:
|
|
681
|
+
default_bitrate = config_manager.get_default_bitrate(
|
|
682
|
+
args.format_command, channels_or_mode
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
if default_bitrate:
|
|
686
|
+
args.bitrate = default_bitrate
|
|
687
|
+
logger.debug(
|
|
688
|
+
f"No bitrate specified, using default {default_bitrate}k for {args.format_command}"
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def handle_preset_injection() -> None:
|
|
693
|
+
"""Handle preset injection into sys.argv before argparse runs."""
|
|
694
|
+
|
|
695
|
+
# check if this is a preset command
|
|
696
|
+
if len(sys.argv) >= 4 and "preset" in sys.argv and "--name" in sys.argv:
|
|
697
|
+
try:
|
|
698
|
+
name_idx = sys.argv.index("--name")
|
|
699
|
+
|
|
700
|
+
# get preset name (should be right after --name)
|
|
701
|
+
if name_idx + 1 < len(sys.argv):
|
|
702
|
+
preset_name = sys.argv[name_idx + 1]
|
|
703
|
+
|
|
704
|
+
# load config and get preset
|
|
705
|
+
config_manager = get_config_manager()
|
|
706
|
+
config_manager.load_config()
|
|
707
|
+
config_manager.inject_preset_args(preset_name)
|
|
708
|
+
|
|
709
|
+
except (ValueError, IndexError):
|
|
710
|
+
# preset parsing failed, let argparse handle the error
|
|
711
|
+
pass
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def cli_parser(base_wd: Path) -> None:
|
|
715
|
+
"""Main CLI parser entry point."""
|
|
716
|
+
# handle presets by injecting args before parsing
|
|
717
|
+
handle_preset_injection()
|
|
718
|
+
|
|
719
|
+
# create parser and subcommands
|
|
720
|
+
parser = create_main_parser()
|
|
721
|
+
subparsers = parser.add_subparsers(dest="sub_command")
|
|
722
|
+
|
|
723
|
+
# create argument groups and parsers
|
|
724
|
+
argument_groups = create_common_argument_groups()
|
|
725
|
+
create_encode_parsers(subparsers, argument_groups)
|
|
726
|
+
create_other_parsers(subparsers, argument_groups)
|
|
727
|
+
|
|
728
|
+
# parse arguments and initialize
|
|
729
|
+
args = parser.parse_args()
|
|
730
|
+
setup_logging(args)
|
|
731
|
+
|
|
732
|
+
if not args.sub_command:
|
|
733
|
+
if not hasattr(args, "version"):
|
|
734
|
+
parser.print_usage()
|
|
735
|
+
exit_application("", EXIT_FAIL)
|
|
736
|
+
|
|
737
|
+
# handle configuration and dependencies
|
|
738
|
+
config_manager = handle_configuration(args)
|
|
739
|
+
|
|
740
|
+
# apply default bitrates for encoding commands
|
|
741
|
+
if args.sub_command == "encode" and hasattr(args, "format_command"):
|
|
742
|
+
apply_default_bitrate(args, config_manager)
|
|
743
|
+
dependencies = handle_dependencies(args, base_wd, config_manager)
|
|
744
|
+
file_inputs = handle_file_inputs(args)
|
|
745
|
+
|
|
746
|
+
# execute the appropriate command
|
|
747
|
+
execute_command(args, file_inputs, dependencies, config_manager)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def setup_logging(args: argparse.Namespace) -> None:
|
|
751
|
+
"""Initialize logging based on arguments."""
|
|
752
|
+
logger_manager.set_level(args.log_level.to_logging_level())
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def handle_configuration(args: argparse.Namespace) -> ConfigManager | None:
|
|
756
|
+
"""Load configuration manager."""
|
|
757
|
+
config_manager = None
|
|
758
|
+
if args.sub_command != "config":
|
|
759
|
+
config_manager = get_config_manager()
|
|
760
|
+
return config_manager
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def handle_dependencies(
|
|
764
|
+
args: argparse.Namespace, base_wd: Path, config_manager: ConfigManager | None
|
|
765
|
+
) -> dict[str, Path | None] | None:
|
|
766
|
+
"""
|
|
767
|
+
Handle tool dependencies detection.
|
|
768
|
+
CLI > Config.
|
|
769
|
+
"""
|
|
770
|
+
if args.sub_command in ("config", "temp"):
|
|
771
|
+
return None
|
|
772
|
+
|
|
773
|
+
deps = {}
|
|
774
|
+
config_deps = (
|
|
775
|
+
config_manager.config.get("dependencies", {}) if config_manager else {}
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
for key in ("ffmpeg", "truehdd", "dee"):
|
|
779
|
+
cli_value = getattr(args, key, None)
|
|
780
|
+
config_value = config_deps.get(key)
|
|
781
|
+
deps[key] = cli_value or config_value
|
|
782
|
+
|
|
783
|
+
atmos_required = getattr(args, "format_command", None) in ("atmos", "ac4")
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
tools = FindDependencies().get_dependencies(
|
|
787
|
+
base_wd,
|
|
788
|
+
deps["ffmpeg"],
|
|
789
|
+
deps["truehdd"],
|
|
790
|
+
deps["dee"],
|
|
791
|
+
require_truehdd=atmos_required,
|
|
792
|
+
)
|
|
793
|
+
except DependencyNotFoundError as e:
|
|
794
|
+
exit_application(str(e), EXIT_FAIL)
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
"ffmpeg_path": Path(tools.ffmpeg),
|
|
798
|
+
"truehdd_path": Path(tools.truehdd) if tools.truehdd else None,
|
|
799
|
+
"dee_path": Path(tools.dee),
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def handle_file_inputs(args: argparse.Namespace) -> list[Path]:
|
|
804
|
+
"""Parse and validate file inputs."""
|
|
805
|
+
if not hasattr(args, "input") or not args.input:
|
|
806
|
+
if args.sub_command not in {"config", "temp"}:
|
|
807
|
+
exit_application("", EXIT_FAIL)
|
|
808
|
+
return []
|
|
809
|
+
|
|
810
|
+
if args.sub_command not in ("find", "info", "encode"):
|
|
811
|
+
return []
|
|
812
|
+
|
|
813
|
+
# parse all possible file inputs
|
|
814
|
+
file_inputs = parse_input_s(args.input)
|
|
815
|
+
if not file_inputs and args.sub_command in ("find", "info", "encode"):
|
|
816
|
+
exit_application("No input files were found.", EXIT_FAIL)
|
|
817
|
+
|
|
818
|
+
return file_inputs
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def execute_command(
|
|
822
|
+
args: argparse.Namespace,
|
|
823
|
+
file_inputs: list[Path],
|
|
824
|
+
dependencies: dict[str, Path | None] | None,
|
|
825
|
+
config_manager: ConfigManager | None,
|
|
826
|
+
) -> None:
|
|
827
|
+
"""Execute the appropriate command based on parsed arguments."""
|
|
828
|
+
if args.sub_command == "encode":
|
|
829
|
+
if dependencies is None:
|
|
830
|
+
exit_application("Dependencies not found for encoding.", EXIT_FAIL)
|
|
831
|
+
execute_encode_command(args, file_inputs, dependencies)
|
|
832
|
+
elif args.sub_command == "find":
|
|
833
|
+
execute_find_command(args, file_inputs)
|
|
834
|
+
elif args.sub_command == "info":
|
|
835
|
+
execute_info_command(args, file_inputs)
|
|
836
|
+
elif args.sub_command == "config":
|
|
837
|
+
execute_config_command(args, config_manager)
|
|
838
|
+
elif args.sub_command == "temp":
|
|
839
|
+
execute_temp_command(args)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def execute_encode_command(
|
|
843
|
+
args: argparse.Namespace,
|
|
844
|
+
file_inputs: list[Path],
|
|
845
|
+
dependencies: dict[str, Path | None],
|
|
846
|
+
) -> None:
|
|
847
|
+
"""Execute encoding commands."""
|
|
848
|
+
ffmpeg_path = dependencies["ffmpeg_path"]
|
|
849
|
+
truehdd_path = dependencies["truehdd_path"]
|
|
850
|
+
dee_path = dependencies["dee_path"]
|
|
851
|
+
|
|
852
|
+
# assert required paths are not None
|
|
853
|
+
assert ffmpeg_path is not None, "ffmpeg_path is required for encoding"
|
|
854
|
+
assert dee_path is not None, "dee_path is required for encoding"
|
|
855
|
+
|
|
856
|
+
try:
|
|
857
|
+
for input_file in file_inputs:
|
|
858
|
+
# update logger to write to file if needed
|
|
859
|
+
if getattr(args, "log_to_file", False):
|
|
860
|
+
logger_manager.set_file(input_file.with_suffix(".log"))
|
|
861
|
+
|
|
862
|
+
# build payload based on format
|
|
863
|
+
if args.format_command == "dd":
|
|
864
|
+
payload = PayloadBuilder.build_dd_payload(
|
|
865
|
+
args, input_file, ffmpeg_path, truehdd_path, dee_path
|
|
866
|
+
)
|
|
867
|
+
result = DDEncoderDEE(payload).encode()
|
|
868
|
+
elif args.format_command in ("ddp", "ddp-bluray"):
|
|
869
|
+
payload = PayloadBuilder.build_ddp_payload(
|
|
870
|
+
args, input_file, ffmpeg_path, truehdd_path, dee_path
|
|
871
|
+
)
|
|
872
|
+
result = DDPEncoderDEE(payload).encode()
|
|
873
|
+
elif args.format_command == "atmos":
|
|
874
|
+
payload = PayloadBuilder.build_atmos_payload(
|
|
875
|
+
args, input_file, ffmpeg_path, truehdd_path, dee_path
|
|
876
|
+
)
|
|
877
|
+
result = AtmosEncoder(payload).encode()
|
|
878
|
+
elif args.format_command == "ac4":
|
|
879
|
+
payload = PayloadBuilder.build_ac4_payload(
|
|
880
|
+
args, input_file, ffmpeg_path, truehdd_path, dee_path
|
|
881
|
+
)
|
|
882
|
+
result = Ac4Encoder(payload).encode()
|
|
883
|
+
elif args.format_command == "preset":
|
|
884
|
+
# this should not happen since config manager converts "preset" to actual format
|
|
885
|
+
exit_application(
|
|
886
|
+
"Preset format conversion failed. This is a bug in the configuration system.",
|
|
887
|
+
EXIT_FAIL,
|
|
888
|
+
)
|
|
889
|
+
else:
|
|
890
|
+
exit_application(f"Unknown format: {args.format_command}", EXIT_FAIL)
|
|
891
|
+
|
|
892
|
+
logger.info(f"Job successful! Output file path:\n{result}")
|
|
893
|
+
|
|
894
|
+
except Exception as e:
|
|
895
|
+
logger.debug(traceback.format_exc())
|
|
896
|
+
exit_application(str(e), EXIT_FAIL)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def execute_find_command(args: argparse.Namespace, file_inputs: list[Path]) -> None:
|
|
900
|
+
"""Execute find command."""
|
|
901
|
+
file_names = []
|
|
902
|
+
for input_file in file_inputs:
|
|
903
|
+
# if name only is used, print only the name of the file.
|
|
904
|
+
if args.name:
|
|
905
|
+
input_file = input_file.name
|
|
906
|
+
file_names.append(str(input_file))
|
|
907
|
+
|
|
908
|
+
exit_application("\n".join(file_names), EXIT_SUCCESS)
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def execute_info_command(_args: argparse.Namespace, file_inputs: list[Path]) -> None:
|
|
912
|
+
"""Execute info command."""
|
|
913
|
+
track_s_info = ""
|
|
914
|
+
for input_file in file_inputs:
|
|
915
|
+
info = parse_audio_streams(input_file)
|
|
916
|
+
if info.media_info:
|
|
917
|
+
track_s_info = (
|
|
918
|
+
track_s_info
|
|
919
|
+
+ f"File: {input_file.name}\nAudio tracks: {info.track_list}\n"
|
|
920
|
+
+ info.media_info
|
|
921
|
+
+ "\n\n"
|
|
922
|
+
)
|
|
923
|
+
exit_application(track_s_info, EXIT_SUCCESS)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def execute_config_command(
|
|
927
|
+
args: argparse.Namespace, config_manager: ConfigManager | None
|
|
928
|
+
) -> None:
|
|
929
|
+
"""Execute config command."""
|
|
930
|
+
# config commands need their own manager instance
|
|
931
|
+
if config_manager is None:
|
|
932
|
+
config_manager = get_config_manager()
|
|
933
|
+
|
|
934
|
+
if args.config_command == "generate":
|
|
935
|
+
try:
|
|
936
|
+
output_path = Path(args.output) if args.output else None
|
|
937
|
+
|
|
938
|
+
# generate config (simplified - no from_args support yet)
|
|
939
|
+
config_path = config_manager.generate_config(
|
|
940
|
+
output_path=output_path, overwrite=args.overwrite
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
exit_application(
|
|
944
|
+
f"Configuration file generated: {config_path}", EXIT_SUCCESS
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
except FileExistsError:
|
|
948
|
+
exit_application(
|
|
949
|
+
"Configuration file already exists. Use --overwrite to replace it.",
|
|
950
|
+
EXIT_FAIL,
|
|
951
|
+
)
|
|
952
|
+
except Exception as e:
|
|
953
|
+
exit_application(f"Failed to generate config: {e}", EXIT_FAIL)
|
|
954
|
+
|
|
955
|
+
elif args.config_command == "info":
|
|
956
|
+
try:
|
|
957
|
+
if args.path:
|
|
958
|
+
config_path = Path(args.path)
|
|
959
|
+
if config_path.exists():
|
|
960
|
+
exit_application(f"Config file: {config_path}", EXIT_SUCCESS)
|
|
961
|
+
else:
|
|
962
|
+
exit_application(f"Config file not found: {config_path}", EXIT_FAIL)
|
|
963
|
+
else:
|
|
964
|
+
if config_manager.config_path:
|
|
965
|
+
info_text = f"Active config file: {config_manager.config_path}\n"
|
|
966
|
+
info_text += f"Presets available: {', '.join(config_manager.list_presets()) or 'None'}"
|
|
967
|
+
else:
|
|
968
|
+
info_text = (
|
|
969
|
+
"No configuration file found. Using built-in defaults.\n"
|
|
970
|
+
)
|
|
971
|
+
info_text += f"Default config location: {get_default_config_path()}"
|
|
972
|
+
|
|
973
|
+
exit_application(info_text, EXIT_SUCCESS)
|
|
974
|
+
except Exception as e:
|
|
975
|
+
exit_application(f"Failed to load config info: {e}", EXIT_FAIL)
|
|
976
|
+
|
|
977
|
+
elif args.config_command == "validate":
|
|
978
|
+
try:
|
|
979
|
+
if not config_manager.has_valid_config():
|
|
980
|
+
exit_application("No configuration file found to validate.", EXIT_FAIL)
|
|
981
|
+
|
|
982
|
+
if args.preset:
|
|
983
|
+
# validate specific preset
|
|
984
|
+
if args.preset not in config_manager.list_presets():
|
|
985
|
+
available = ", ".join(config_manager.list_presets()) or "None"
|
|
986
|
+
exit_application(
|
|
987
|
+
f"Preset '{args.preset}' not found. Available: {available}",
|
|
988
|
+
EXIT_FAIL,
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
if config_manager.validate_preset(args.preset):
|
|
992
|
+
exit_application(f"Preset '{args.preset}' is valid.", EXIT_SUCCESS)
|
|
993
|
+
else:
|
|
994
|
+
exit_application(f"Preset '{args.preset}' is invalid.", EXIT_FAIL)
|
|
995
|
+
else:
|
|
996
|
+
# validate all presets
|
|
997
|
+
presets = config_manager.list_presets()
|
|
998
|
+
if not presets:
|
|
999
|
+
exit_application("No presets found in configuration.", EXIT_SUCCESS)
|
|
1000
|
+
|
|
1001
|
+
invalid_presets = []
|
|
1002
|
+
for preset in presets:
|
|
1003
|
+
if not config_manager.validate_preset(preset):
|
|
1004
|
+
invalid_presets.append(preset)
|
|
1005
|
+
|
|
1006
|
+
if invalid_presets:
|
|
1007
|
+
exit_application(
|
|
1008
|
+
f"Invalid presets found: {', '.join(invalid_presets)}",
|
|
1009
|
+
EXIT_FAIL,
|
|
1010
|
+
)
|
|
1011
|
+
else:
|
|
1012
|
+
exit_application(
|
|
1013
|
+
f"All {len(presets)} presets are valid.", EXIT_SUCCESS
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
exit_application(f"Failed to validate config: {e}", EXIT_FAIL)
|
|
1018
|
+
|
|
1019
|
+
elif args.config_command == "list-presets":
|
|
1020
|
+
try:
|
|
1021
|
+
presets = config_manager.list_presets()
|
|
1022
|
+
if not presets:
|
|
1023
|
+
exit_application("No presets found in configuration.", EXIT_SUCCESS)
|
|
1024
|
+
|
|
1025
|
+
if args.detailed:
|
|
1026
|
+
info_lines = ["Available presets:"]
|
|
1027
|
+
for preset in presets:
|
|
1028
|
+
preset_info = config_manager.get_preset_info(preset)
|
|
1029
|
+
valid = "✓" if config_manager.validate_preset(preset) else "✗"
|
|
1030
|
+
info_lines.append(f" {valid} {preset}: {preset_info['command']}")
|
|
1031
|
+
exit_application("\n".join(info_lines), EXIT_SUCCESS)
|
|
1032
|
+
else:
|
|
1033
|
+
exit_application(
|
|
1034
|
+
f"Available presets: {', '.join(presets)}", EXIT_SUCCESS
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
except Exception as e:
|
|
1038
|
+
exit_application(f"Failed to list presets: {e}", EXIT_FAIL)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def execute_temp_command(args: argparse.Namespace) -> None:
|
|
1042
|
+
"""Execute temp management commands."""
|
|
1043
|
+
if args.temp_command == "clean":
|
|
1044
|
+
removed_count, size_mb = clean_deezy_temp_folders(
|
|
1045
|
+
max_age_hours=args.max_age, dry_run=args.dry_run
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
if args.dry_run:
|
|
1049
|
+
if removed_count == 0:
|
|
1050
|
+
exit_application("No temp folders to clean.", EXIT_SUCCESS)
|
|
1051
|
+
else:
|
|
1052
|
+
exit_application(
|
|
1053
|
+
f"Would remove {removed_count} folders ({size_mb:.1f} MB)",
|
|
1054
|
+
EXIT_SUCCESS,
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
if removed_count == 0:
|
|
1058
|
+
exit_application("No temp folders to clean.", EXIT_SUCCESS)
|
|
1059
|
+
else:
|
|
1060
|
+
exit_application(
|
|
1061
|
+
f"Removed {removed_count} temp folders ({size_mb:.1f} MB)",
|
|
1062
|
+
EXIT_SUCCESS,
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
elif args.temp_command == "info":
|
|
1066
|
+
deezy_temp_base, job_folders, total_size_mb = get_deezy_temp_info()
|
|
1067
|
+
|
|
1068
|
+
if not job_folders:
|
|
1069
|
+
exit_application("No DeeZy temp folders found.", EXIT_SUCCESS)
|
|
1070
|
+
|
|
1071
|
+
message = f"DeeZy temp folder: {deezy_temp_base}\n"
|
|
1072
|
+
message += f"Job folders: {len(job_folders)}\n"
|
|
1073
|
+
message += f"Total size: {total_size_mb:.1f} MB"
|
|
1074
|
+
|
|
1075
|
+
exit_application(message, EXIT_SUCCESS)
|