easyrip 4.11.3__py3-none-any.whl → 4.13.0__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.
easyrip/ripper/ripper.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import re
3
3
  import shutil
4
+ import textwrap
4
5
  from collections.abc import Callable, Iterable
5
6
  from dataclasses import dataclass
6
7
  from datetime import datetime
@@ -16,11 +17,8 @@ from ..easyrip_mlang import Global_lang_val, gettext, translate_subtitles
16
17
  from ..utils import get_base62_time
17
18
  from .media_info import Media_info, Stream_error
18
19
  from .param import (
19
- DEFAULT_PRESET_PARAMS,
20
20
  FONT_SUFFIX_SET,
21
21
  SUBTITLE_SUFFIX_SET,
22
- X264_PARAMS_NAME,
23
- X265_PARAMS_NAME,
24
22
  )
25
23
  from .sub_and_font import subset
26
24
 
@@ -53,13 +51,13 @@ class Ripper:
53
51
  @dataclass(slots=True)
54
52
  class Option:
55
53
  preset_name: "Ripper.Preset_name"
56
- encoder_format_str: str
54
+ encoder_format_str_list: list[str]
57
55
  audio_encoder: "Ripper.Audio_codec | None"
58
56
  muxer: "Ripper.Muxer | None"
59
- muxer_format_str: str
57
+ muxer_format_str_list: list[str]
60
58
 
61
59
  def __str__(self) -> str:
62
- return f" preset_name = {self.preset_name}\n option_format = {self.encoder_format_str}"
60
+ return f" preset_name = {self.preset_name}\n option_format = {self.encoder_format_str_list}"
63
61
 
64
62
  input_path_list: list[Path]
65
63
  output_prefix_list: list[str]
@@ -134,6 +132,13 @@ class Ripper:
134
132
  )
135
133
 
136
134
  def preset_name_to_option(self, preset_name: Preset_name) -> Option:
135
+ if os.name == "nt":
136
+ cmd_head_del = "del /Q"
137
+ cmd_head_copy = "copy"
138
+ else:
139
+ cmd_head_del = "rm /f"
140
+ cmd_head_copy = "cp"
141
+
137
142
  if (
138
143
  force_fps := self.option_map.get("r") or self.option_map.get("fps")
139
144
  ) == "auto":
@@ -238,28 +243,26 @@ class Ripper:
238
243
  audio_option = ""
239
244
 
240
245
  # Muxer
246
+ muxer_format_str_list: list[str]
241
247
  if muxer := self.option_map.get("muxer"):
242
248
  muxer = Ripper.Muxer(muxer)
243
249
 
244
250
  match muxer:
245
251
  case Ripper.Muxer.mp4:
246
- muxer_format_str = (
247
- ' && mp4box -add "{output}" -new "{output}" '
252
+ muxer_format_str_list = [
253
+ 'mp4box -add "{output}" -new "{output}" '
248
254
  + (
249
255
  f"-chap {chapters} "
250
256
  if (chapters := self.option_map.get("chapters"))
251
257
  else ""
252
258
  )
253
- + (
254
- ""
255
- if self.preset_name == Ripper.Preset_name.flac
256
- else (
257
- "&& mp4fpsmod "
258
- + (f"-r 0:{force_fps}" if force_fps else "")
259
- + ' -i "{output}"'
260
- )
259
+ ]
260
+ if self.preset_name != Ripper.Preset_name.flac:
261
+ muxer_format_str_list.append(
262
+ "mp4fpsmod "
263
+ + (f"-r 0:{force_fps}" if force_fps else "")
264
+ + ' -i "{output}"'
261
265
  )
262
- )
263
266
 
264
267
  case Ripper.Muxer.mkv:
265
268
  if (
@@ -270,46 +273,52 @@ class Ripper:
270
273
  log.error("It is not a dir: {}", only_mux_sub_path)
271
274
  only_mux_sub_path = None
272
275
 
273
- muxer_format_str = (
274
- ' && mkvpropedit "{output}" --add-track-statistics-tags && mkvmerge -o "{output}.temp.mkv" "{output}" && mkvmerge -o "{output}" '
275
- + (
276
- f"--default-duration 0:{force_fps}fps --fix-bitstream-timing-information 0:1 "
277
- if force_fps and only_mux_sub_path is None
278
- else ""
279
- )
280
- + (
281
- f"--chapters {chapters} "
282
- if (chapters := self.option_map.get("chapters"))
283
- else ""
284
- )
285
- + (
286
- " ".join(
287
- (
288
- ""
289
- if len(
290
- affixes := _file.stem.rsplit(".", maxsplit=1)
276
+ muxer_format_str_list = [
277
+ 'mkvpropedit "{output}" --add-track-statistics-tags',
278
+ 'mkvmerge -o "{output}.temp.mkv" "{output}"',
279
+ (
280
+ 'mkvmerge -o "{output}" '
281
+ + (
282
+ f"--default-duration 0:{force_fps}fps --fix-bitstream-timing-information 0:1 "
283
+ if force_fps and only_mux_sub_path is None
284
+ else ""
285
+ )
286
+ + (
287
+ f"--chapters {chapters} "
288
+ if (chapters := self.option_map.get("chapters"))
289
+ else ""
290
+ )
291
+ + (
292
+ " ".join(
293
+ (
294
+ ""
295
+ if len(
296
+ affixes := _file.stem.rsplit(
297
+ ".", maxsplit=1
298
+ )
299
+ )
300
+ == 1
301
+ else "--attach-file "
302
+ if _file.suffix in FONT_SUFFIX_SET
303
+ else f"--language 0:{affixes[1]} --track-name 0:{Global_lang_val.language_tag_to_local_str(affixes[1])} "
291
304
  )
292
- == 1
293
- else "--attach-file "
294
- if _file.suffix in FONT_SUFFIX_SET
295
- else f"--language 0:{affixes[1]} --track-name 0:{Global_lang_val.language_tag_to_local_str(affixes[1])} "
305
+ + f'"{_file.absolute()}"'
306
+ for _file in only_mux_sub_path.iterdir()
307
+ if _file.suffix
308
+ in (SUBTITLE_SUFFIX_SET | FONT_SUFFIX_SET)
296
309
  )
297
- + f'"{_file.absolute()}"'
298
- for _file in only_mux_sub_path.iterdir()
299
- if _file.suffix
300
- in (SUBTITLE_SUFFIX_SET | FONT_SUFFIX_SET)
310
+ if only_mux_sub_path
311
+ else ""
301
312
  )
302
- if only_mux_sub_path
303
- else ""
304
- )
305
- + ' --no-global-tags --no-track-tags --default-track-flag 0 "{output}.temp.mkv" && del /Q "{output}.temp.mkv"'
306
- )
313
+ + ' --no-global-tags --no-track-tags --default-track-flag 0 "{output}.temp.mkv"'
314
+ ),
315
+ cmd_head_del + ' "{output}.temp.mkv"',
316
+ ]
307
317
 
308
318
  else:
309
319
  muxer = None
310
- muxer_format_str = ""
320
+ muxer_format_str_list = []
311
321
 
312
- vspipe_input: str = ""
313
322
  pipe_gvar_list = [
314
323
  s for s in self.option_map.get("pipe:gvar", "").split(":") if s
315
324
  ]
@@ -319,15 +328,6 @@ class Ripper:
319
328
  if sub_pathname:
320
329
  pipe_gvar_dict["subtitle"] = sub_pathname
321
330
 
322
- if self.input_path_list[0].suffix == ".vpy":
323
- vspipe_input = f'vspipe -c y4m {" ".join(f'-a "{k}={v}"' for k, v in pipe_gvar_dict.items())} "{{input}}" - | '
324
- elif vpy_pathname:
325
- vspipe_input = f'vspipe -c y4m {" ".join(f'-a "{k}={v}"' for k, v in pipe_gvar_dict.items())} -a "input={{input}}" "{vpy_pathname}" - | '
326
-
327
- hwaccel = (
328
- f"-hwaccel {hwaccel}" if (hwaccel := self.option_map.get("hwaccel")) else ""
329
- )
330
-
331
331
  ffparams_ff = self.option_map.get("ff-params:ff") or self.option_map.get(
332
332
  "ff-params", ""
333
333
  )
@@ -342,10 +342,45 @@ class Ripper:
342
342
 
343
343
  FFMPEG_HEADER = f"ffmpeg {'-hide_banner ' if self.option_map.get('_sub_ripper_num') else ''}-progress {FF_PROGRESS_LOG_FILE} -report {ffparams_ff} {ffparams_in}"
344
344
 
345
+ def get_vs_ff_cmd(enc_opt: str) -> str:
346
+ vspipe_input: str = ""
347
+ if self.input_path_list[0].suffix == ".vpy":
348
+ vspipe_input = f'vspipe -c y4m {" ".join(f'-a "{k}={v}"' for k, v in pipe_gvar_dict.items())} "{{input}}" - |'
349
+ elif vpy_pathname:
350
+ vspipe_input = f'vspipe -c y4m {" ".join(f'-a "{k}={v}"' for k, v in pipe_gvar_dict.items())} -a "input={{input}}" "{vpy_pathname}" - |'
351
+
352
+ hwaccel = (
353
+ [f"-hwaccel {hwaccel}"]
354
+ if (hwaccel := self.option_map.get("hwaccel"))
355
+ else []
356
+ )
357
+
358
+ return " ".join(
359
+ (
360
+ vspipe_input,
361
+ FFMPEG_HEADER,
362
+ *hwaccel,
363
+ " ".join(f"-i {s}" for s in ff_input_option),
364
+ " ".join(f"-map {s}" for s in ff_stream_option),
365
+ audio_option,
366
+ enc_opt,
367
+ ffparams_out,
368
+ (f'-vf "{",".join(ff_vf_option)}" ' if len(ff_vf_option) else ""),
369
+ '"{output}"',
370
+ )
371
+ )
372
+
373
+ preset_param_getted = {
374
+ _param_name: self.option_map.get(_param_name)
375
+ for _param_name in preset_name.get_param_name_set(set())
376
+ }
377
+ preset_param_default_dict = preset_name.get_param_default_dict({})
378
+
379
+ encoder_format_str_list: list[str]
345
380
  match preset_name:
346
381
  case Ripper.Preset_name.custom:
347
382
  if not (
348
- encoder_format_str := self.option_map.get(
383
+ _encoder_format_str := self.option_map.get(
349
384
  "custom:format",
350
385
  self.option_map.get(
351
386
  "custom:template", self.option_map.get("custom")
@@ -355,67 +390,67 @@ class Ripper:
355
390
  log.warning(
356
391
  "The preset custom must have custom:format or custom:template"
357
392
  )
358
- encoder_format_str = ""
393
+ encoder_format_str_list = []
359
394
 
360
395
  else:
361
- if encoder_format_str.startswith("''''"):
362
- encoder_format_str = encoder_format_str[4:]
396
+ if _encoder_format_str.startswith("''''"):
397
+ _encoder_format_str = _encoder_format_str[4:]
363
398
  else:
364
- encoder_format_str = encoder_format_str.replace("''", '"')
365
- encoder_format_str = (
366
- encoder_format_str.replace("\\34/", '"')
399
+ _encoder_format_str = _encoder_format_str.replace("''", '"')
400
+ _encoder_format_str = (
401
+ _encoder_format_str.replace("\\34/", '"')
367
402
  .replace("\\39/", "'")
368
403
  .format_map(
369
404
  self.option_map | {"input": "{input}", "output": "{output}"}
370
405
  )
371
406
  )
407
+ encoder_format_str_list = [_encoder_format_str]
372
408
 
373
409
  case Ripper.Preset_name.copy:
374
- hwaccel = (
375
- f"-hwaccel {hwaccel}"
376
- if (hwaccel := self.option_map.get("hwaccel"))
377
- else ""
378
- )
379
-
380
- encoder_format_str = (
381
- f"{FFMPEG_HEADER} {hwaccel} "
410
+ encoder_format_str_list = [
411
+ f"{FFMPEG_HEADER} "
382
412
  '-i "{input}" -c copy '
383
413
  f"{' '.join(f'-map {s}' for s in ff_stream_option)} "
384
414
  + audio_option
385
415
  + ffparams_out
386
416
  + '"{output}"'
387
- )
417
+ ]
388
418
 
419
+ _encoder_format_str: str | None = None
389
420
  match self.option_map.get("c:a"):
390
421
  case None | "flac":
391
422
  if muxer == Ripper.Muxer.mp4:
392
- encoder_format_str = 'mp4box -add "{input}" -new "{output}"'
423
+ _encoder_format_str = (
424
+ 'mp4box -add "{input}" -new "{output}"'
425
+ )
393
426
  for _audio_info in self.media_info.audio_info:
394
- encoder_format_str += f" -rem {_audio_info.index + 1}"
427
+ _encoder_format_str += f" -rem {_audio_info.index + 1}"
395
428
  else:
396
- encoder_format_str = (
429
+ _encoder_format_str = (
397
430
  'mkvmerge -o "{output}" --no-audio "{input}"'
398
431
  )
399
432
  case "copy":
400
- encoder_format_str = (
433
+ _encoder_format_str = (
401
434
  'mp4box -add "{input}" -new "{output}"'
402
435
  if muxer == Ripper.Muxer.mp4
403
436
  else 'mkvmerge -o "{output}" "{input}"'
404
437
  )
438
+ if _encoder_format_str is not None:
439
+ encoder_format_str_list = [_encoder_format_str]
405
440
 
406
441
  case Ripper.Preset_name.flac:
407
442
  _ff_encode_str: str = ""
408
- _flac_encode_str: str = ""
443
+ _flac_encode_str_list: list[str] = []
409
444
  _mux_flac_input_list: list[str] = []
410
- # _mux_flac_map_str: str = ""
411
- _del_flac_str: str = ""
445
+ _del_flac_str_list: list[str] = []
412
446
 
413
447
  for _audio_info in self.media_info.audio_info:
414
448
  _encoder: str = (
415
449
  "pcm_s24le"
416
- if (
417
- _audio_info.bits_per_raw_sample == 24
418
- or _audio_info.bits_per_sample == 24
450
+ if 24
451
+ in (
452
+ _audio_info.bits_per_raw_sample,
453
+ _audio_info.bits_per_sample,
419
454
  )
420
455
  else {
421
456
  "u8": "pcm_u8",
@@ -433,47 +468,57 @@ class Ripper:
433
468
  }.get(_audio_info.sample_fmt, "pcm_s32le")
434
469
  )
435
470
 
436
- _new_output_str: str = f"{{output}}.{_audio_info.index}.temp"
471
+ _new_output_str: str = "{output}" + f".{_audio_info.index}.temp"
437
472
 
438
473
  _ff_encode_str += (
439
474
  f"-map 0:{_audio_info.index} -c:a {_encoder} {ffparams_out} "
440
475
  f'"{_new_output_str}.wav" '
441
476
  )
442
- _flac_encode_str += (
443
- f"&& flac -j 32 -8 -e -p -l {'19' if _audio_info.sample_rate > 48000 else '12'} "
444
- f'-o "{_new_output_str}.flac" "{_new_output_str}.wav" && del /Q "{_new_output_str}.wav" '
445
- )
477
+ _flac_encode_str_list = [
478
+ (
479
+ f"flac -j 32 -8 -e -p -l {'19' if _audio_info.sample_rate > 48000 else '12'} "
480
+ f'-o "{_new_output_str}.flac" "{_new_output_str}.wav"'
481
+ ),
482
+ f'{cmd_head_del} "{_new_output_str}.wav"',
483
+ ]
446
484
 
447
485
  _mux_flac_input_list.append(f'"{_new_output_str}.flac"')
448
486
 
449
- _del_flac_str += f'&& del /Q "{_new_output_str}.flac" '
487
+ _del_flac_str_list = [f'{cmd_head_del} "{_new_output_str}.flac" ']
450
488
 
451
489
  match len(_mux_flac_input_list):
452
490
  case 0:
453
491
  raise RuntimeError(f'No audio in "{self.input_path_list[0]}"')
454
492
 
455
493
  case 1 if muxer is None:
456
- encoder_format_str = (
457
- FFMPEG_HEADER + ' -i "{input}" '
458
- f"{_ff_encode_str} {_flac_encode_str} "
459
- f"&& {'copy' if os.name == 'nt' else 'cp'} {_mux_flac_input_list[0]} "
460
- + '"{output}" '
461
- + _del_flac_str
462
- )
494
+ encoder_format_str_list = [
495
+ FFMPEG_HEADER + f' -i "{{input}}" {_ff_encode_str}',
496
+ *_flac_encode_str_list,
497
+ (
498
+ f"{cmd_head_copy} {_mux_flac_input_list[0]} "
499
+ '"{output}"' # .
500
+ ),
501
+ *_del_flac_str_list,
502
+ ]
463
503
 
464
504
  case _:
465
505
  _mux_str = (
466
- f"mp4box -add {' -add '.join(_mux_flac_input_list)}"
467
- ' -new "{output}" '
506
+ (
507
+ f"mp4box {' '.join(f'-add {s}' for s in _mux_flac_input_list)} "
508
+ '-new "{output}"'
509
+ )
468
510
  if muxer == Ripper.Muxer.mp4
469
- else 'mkvmerge -o "{output}" '
470
- + " ".join(_mux_flac_input_list)
471
- )
472
- encoder_format_str = (
473
- FFMPEG_HEADER + ' -i "{input}" '
474
- f"{_ff_encode_str} {_flac_encode_str} "
475
- f"&& {_mux_str} " + _del_flac_str
511
+ else (
512
+ 'mkvmerge -o "{output}" '
513
+ + " ".join(_mux_flac_input_list)
514
+ )
476
515
  )
516
+ encoder_format_str_list = [
517
+ FFMPEG_HEADER + f' -i "{{input}}" {_ff_encode_str}',
518
+ *_flac_encode_str_list,
519
+ f"{_mux_str}",
520
+ *_del_flac_str_list,
521
+ ]
477
522
 
478
523
  case (
479
524
  Ripper.Preset_name.x264
@@ -483,10 +528,7 @@ class Ripper:
483
528
  _custom_option_map: dict[str, str] = {
484
529
  k: v
485
530
  for k, v in {
486
- **{
487
- _param_name: self.option_map.get(_param_name)
488
- for _param_name in X264_PARAMS_NAME
489
- },
531
+ **preset_param_getted,
490
532
  **dict(
491
533
  s.split("=", maxsplit=1)
492
534
  for s in str(self.option_map.get("x264-params", "")).split(
@@ -498,9 +540,7 @@ class Ripper:
498
540
  if v is not None
499
541
  }
500
542
 
501
- _option_map = (
502
- DEFAULT_PRESET_PARAMS.get(preset_name, {}) | _custom_option_map
503
- )
543
+ _option_map = preset_param_default_dict | _custom_option_map
504
544
 
505
545
  if (
506
546
  (_crf := _option_map.get("crf"))
@@ -512,14 +552,11 @@ class Ripper:
512
552
 
513
553
  _param = ":".join(f"{key}={val}" for key, val in _option_map.items())
514
554
 
515
- encoder_format_str = (
516
- f"{vspipe_input} {FFMPEG_HEADER} {hwaccel} {' '.join(f'-i {s}' for s in ff_input_option)} {' '.join(f'-map {s}' for s in ff_stream_option)} "
517
- + audio_option
518
- + f"-c:v libx264 {'' if is_pipe_input else '-pix_fmt yuv420p'} -x264-params "
519
- + f'"{_param}" {ffparams_out} '
520
- + (f'-vf "{",".join(ff_vf_option)}" ' if len(ff_vf_option) else "")
521
- + '"{output}"'
522
- )
555
+ encoder_format_str_list = [
556
+ get_vs_ff_cmd(
557
+ f'-c:v libx264 {"" if is_pipe_input else "-pix_fmt yuv420p"} -x264-params "{_param}" '
558
+ )
559
+ ]
523
560
 
524
561
  case (
525
562
  Ripper.Preset_name.x265
@@ -533,10 +570,7 @@ class Ripper:
533
570
  _custom_option_map: dict[str, str] = {
534
571
  k: v
535
572
  for k, v in {
536
- **{
537
- _param_name: self.option_map.get(_param_name)
538
- for _param_name in X265_PARAMS_NAME
539
- },
573
+ **preset_param_getted,
540
574
  **dict(
541
575
  s.split("=", maxsplit=1)
542
576
  for s in str(self.option_map.get("x265-params", "")).split(
@@ -548,9 +582,7 @@ class Ripper:
548
582
  if v is not None
549
583
  }
550
584
 
551
- _option_map = (
552
- DEFAULT_PRESET_PARAMS.get(preset_name, {}) | _custom_option_map
553
- )
585
+ _option_map = preset_param_default_dict | _custom_option_map
554
586
 
555
587
  # HEVC 规范
556
588
  if self.option_map.get(
@@ -593,14 +625,11 @@ class Ripper:
593
625
 
594
626
  _param = ":".join(f"{key}={val}" for key, val in _option_map.items())
595
627
 
596
- encoder_format_str = (
597
- f"{vspipe_input} {FFMPEG_HEADER} {hwaccel} {' '.join(f'-i {s}' for s in ff_input_option)} {' '.join(f'-map {s}' for s in ff_stream_option)} "
598
- + audio_option
599
- + f"-c:v libx265 {'' if is_pipe_input else '-pix_fmt yuv420p10le'} -x265-params "
600
- + f'"{_param}" {ffparams_out} '
601
- + (f'-vf "{",".join(ff_vf_option)}" ' if len(ff_vf_option) else "")
602
- + '"{output}"'
603
- )
628
+ encoder_format_str_list = [
629
+ get_vs_ff_cmd(
630
+ f'-c:v libx265 {"" if is_pipe_input else "-pix_fmt yuv420p10le"} -x265-params "{_param}"'
631
+ )
632
+ ]
604
633
 
605
634
  case (
606
635
  Ripper.Preset_name.h264_amf
@@ -630,14 +659,9 @@ class Ripper:
630
659
  (f"-{key} {val}" for key, val in _option_map.items() if val)
631
660
  )
632
661
 
633
- encoder_format_str = (
634
- f"{vspipe_input} {FFMPEG_HEADER} {hwaccel} {' '.join(f'-i {s}' for s in ff_input_option)} {' '.join(f'-map {s}' for s in ff_stream_option)} "
635
- + audio_option
636
- + f"-c:v {preset_name.value} "
637
- + f"{_param} {ffparams_out} "
638
- + (f' -vf "{",".join(ff_vf_option)}" ' if len(ff_vf_option) else "")
639
- + ' "{output}"'
640
- )
662
+ encoder_format_str_list = [
663
+ get_vs_ff_cmd(f'-c:v {preset_name.value} "{_param}"')
664
+ ]
641
665
 
642
666
  case Ripper.Preset_name.svtav1:
643
667
  _option_map = {
@@ -654,14 +678,7 @@ class Ripper:
654
678
  (f"-{key} {val}" for key, val in _option_map.items() if val)
655
679
  )
656
680
 
657
- encoder_format_str = (
658
- f"{vspipe_input} {FFMPEG_HEADER} {hwaccel} {' '.join(f'-i {s}' for s in ff_input_option)} {' '.join(f'-map {s}' for s in ff_stream_option)} "
659
- + audio_option
660
- + "-c:v libsvtav1 "
661
- + f"{_param} {ffparams_out} "
662
- + (f'-vf "{",".join(ff_vf_option)}" ' if len(ff_vf_option) else "")
663
- + ' "{output}"'
664
- )
681
+ encoder_format_str_list = [get_vs_ff_cmd(f'-c:v libsvtav1 "{_param}"')]
665
682
 
666
683
  case Ripper.Preset_name.vvenc:
667
684
  _option_map = {
@@ -677,20 +694,29 @@ class Ripper:
677
694
  (f"-{key} {val}" for key, val in _option_map.items() if val)
678
695
  )
679
696
 
680
- encoder_format_str = (
681
- f"{vspipe_input} {FFMPEG_HEADER} {hwaccel} {' '.join(f'-i {s}' for s in ff_input_option)} {' '.join(f'-map {s}' for s in ff_stream_option)} "
682
- + audio_option
683
- + "-c:v libvvenc "
684
- + f"{_param} {ffparams_out} "
685
- + (f'-vf "{",".join(ff_vf_option)}" ' if len(ff_vf_option) else "")
686
- + ' "{output}"'
697
+ encoder_format_str_list = [get_vs_ff_cmd(f'-c:v libvvenc "{_param}"')]
698
+
699
+ case Ripper.Preset_name.ffv1:
700
+ _option_map = {
701
+ "pix_fmt": self.option_map.get("pix_fmt"),
702
+ **preset_param_getted,
703
+ }
704
+
705
+ _param = " ".join(
706
+ (f"-{key} {val}" for key, val in _option_map.items() if val)
687
707
  )
688
708
 
709
+ encoder_format_str_list = [get_vs_ff_cmd(f'-c:v ffv1 "{_param}"')]
710
+
689
711
  case Ripper.Preset_name.subset:
690
- encoder_format_str = ""
712
+ encoder_format_str_list = []
691
713
 
692
714
  return Ripper.Option(
693
- preset_name, encoder_format_str, audio_encoder, muxer, muxer_format_str
715
+ preset_name,
716
+ encoder_format_str_list,
717
+ audio_encoder,
718
+ muxer,
719
+ muxer_format_str_list,
694
720
  )
695
721
 
696
722
  def _flush_progress(self, sleep_sec: float) -> None:
@@ -766,6 +792,7 @@ class Ripper:
766
792
  suffix: str
767
793
 
768
794
  # 根据格式判断
795
+ cmd_list: list[str]
769
796
  match self.option.preset_name:
770
797
  case Ripper.Preset_name.custom:
771
798
  suffix = (
@@ -774,32 +801,45 @@ class Ripper:
774
801
  else ""
775
802
  )
776
803
  temp_name = temp_name + suffix
777
- cmd = self.option.encoder_format_str.format_map(
778
- {
779
- "input": str(self.input_path_list[0]),
780
- "output": os.path.join(self.output_dir, temp_name),
781
- }
782
- )
783
-
784
- case Ripper.Preset_name.flac:
785
- if self.option.muxer is not None or len(self.media_info.audio_info) > 1:
786
- suffix = f".flac.{'mp4' if self.option.muxer == Ripper.Muxer.mp4 else 'mkv'}"
787
- temp_name = temp_name + suffix
788
- cmd = f"{self.option.encoder_format_str} {self.option.muxer_format_str}".format_map(
804
+ cmd_list = [
805
+ s.format_map(
789
806
  {
790
807
  "input": str(self.input_path_list[0]),
791
808
  "output": os.path.join(self.output_dir, temp_name),
792
809
  }
793
810
  )
811
+ for s in self.option.encoder_format_str_list
812
+ ]
813
+
814
+ case Ripper.Preset_name.flac:
815
+ if self.option.muxer is not None or len(self.media_info.audio_info) > 1:
816
+ suffix = f".flac.{'mp4' if self.option.muxer == Ripper.Muxer.mp4 else 'mkv'}"
817
+ temp_name = temp_name + suffix
818
+ cmd_list = [
819
+ s.format_map(
820
+ {
821
+ "input": str(self.input_path_list[0]),
822
+ "output": os.path.join(self.output_dir, temp_name),
823
+ }
824
+ )
825
+ for str_list in (
826
+ self.option.encoder_format_str_list,
827
+ self.option.muxer_format_str_list,
828
+ )
829
+ for s in str_list
830
+ ]
794
831
  else:
795
832
  suffix = ".flac"
796
833
  temp_name = temp_name + suffix
797
- cmd = self.option.encoder_format_str.format_map(
798
- {
799
- "input": str(self.input_path_list[0]),
800
- "output": os.path.join(self.output_dir, temp_name),
801
- }
802
- )
834
+ cmd_list = [
835
+ s.format_map(
836
+ {
837
+ "input": str(self.input_path_list[0]),
838
+ "output": os.path.join(self.output_dir, temp_name),
839
+ }
840
+ )
841
+ for s in self.option.encoder_format_str_list
842
+ ]
803
843
 
804
844
  case Ripper.Preset_name.subset:
805
845
  # 临时翻译
@@ -861,18 +901,18 @@ class Ripper:
861
901
 
862
902
  _font_in_sub = self.option_map.get("subset-font-in-sub", "0") == "1"
863
903
  _use_win_font = (
864
- self.option_map.get("subset-use-win-font", "0") == "1"
904
+ self.option_map.get("subset-use-win-font", "0") != "0"
865
905
  )
866
906
  _use_libass_spec = (
867
- self.option_map.get("subset-use-libass-spec", "0") == "1"
907
+ self.option_map.get("subset-use-libass-spec", "1") != "0"
868
908
  )
869
909
  _drop_non_render = (
870
- self.option_map.get("subset-drop-non-render", "1") == "1"
910
+ self.option_map.get("subset-drop-non-render", "1") != "0"
871
911
  )
872
912
  _drop_unkow_data = (
873
- self.option_map.get("subset-drop-unkow-data", "1") == "1"
913
+ self.option_map.get("subset-drop-unkow-data", "1") != "0"
874
914
  )
875
- _strict = self.option_map.get("subset-strict", "0") == "1"
915
+ _strict = self.option_map.get("subset-strict", "0") != "0"
876
916
 
877
917
  subset_res = subset(
878
918
  _ass_list,
@@ -913,12 +953,19 @@ class Ripper:
913
953
  ".va.mp4" if self.option.audio_encoder else ".v.mp4"
914
954
  )
915
955
  temp_name = temp_name + suffix
916
- cmd = f"{self.option.encoder_format_str} {self.option.muxer_format_str}".format_map(
917
- {
918
- "input": str(self.input_path_list[0]),
919
- "output": os.path.join(self.output_dir, temp_name),
920
- }
921
- )
956
+ cmd_list = [
957
+ s.format_map(
958
+ {
959
+ "input": str(self.input_path_list[0]),
960
+ "output": os.path.join(self.output_dir, temp_name),
961
+ }
962
+ )
963
+ for str_list in (
964
+ self.option.encoder_format_str_list,
965
+ self.option.muxer_format_str_list,
966
+ )
967
+ for s in str_list
968
+ ]
922
969
 
923
970
  case Ripper.Muxer.mkv:
924
971
  if self.option_map.get("auto-infix", "1") == "0":
@@ -928,12 +975,19 @@ class Ripper:
928
975
  ".va.mkv" if self.option.audio_encoder else ".v.mkv"
929
976
  )
930
977
  temp_name = temp_name + suffix
931
- cmd = f"{self.option.encoder_format_str} {self.option.muxer_format_str}".format_map(
932
- {
933
- "input": str(self.input_path_list[0]),
934
- "output": os.path.join(self.output_dir, temp_name),
935
- }
936
- )
978
+ cmd_list = [
979
+ s.format_map(
980
+ {
981
+ "input": str(self.input_path_list[0]),
982
+ "output": os.path.join(self.output_dir, temp_name),
983
+ }
984
+ )
985
+ for str_list in (
986
+ self.option.encoder_format_str_list,
987
+ self.option.muxer_format_str_list,
988
+ )
989
+ for s in str_list
990
+ ]
937
991
 
938
992
  case _:
939
993
  if self.option_map.get("auto-infix", "1") == "0":
@@ -943,15 +997,18 @@ class Ripper:
943
997
  ".va.mkv" if self.option.audio_encoder else ".v.mkv"
944
998
  )
945
999
  temp_name = temp_name + suffix
946
- cmd = self.option.encoder_format_str.format_map(
947
- {
948
- "input": str(self.input_path_list[0]),
949
- "output": os.path.join(
950
- self.output_dir,
951
- os.path.join(self.output_dir, temp_name),
952
- ),
953
- }
954
- )
1000
+ cmd_list = [
1001
+ s.format_map(
1002
+ {
1003
+ "input": str(self.input_path_list[0]),
1004
+ "output": os.path.join(
1005
+ self.output_dir,
1006
+ os.path.join(self.output_dir, temp_name),
1007
+ ),
1008
+ }
1009
+ )
1010
+ for s in self.option.encoder_format_str_list
1011
+ ]
955
1012
 
956
1013
  # 执行
957
1014
  output_filename = basename + suffix
@@ -992,8 +1049,27 @@ class Ripper:
992
1049
  if self.preset_name is not Ripper.Preset_name.custom:
993
1050
  os.environ["FFREPORT"] = f"file={FF_REPORT_LOG_FILE}:level=31"
994
1051
 
995
- log.info(cmd)
996
- is_cmd_run_failed = os.system(cmd)
1052
+ log.info(
1053
+ "Run the following commands in order:\n{}",
1054
+ textwrap.indent(
1055
+ "\n".join(f"{i}. {s}" for i, s in enumerate(cmd_list, 1)), prefix=" "
1056
+ ),
1057
+ )
1058
+ is_cmd_run_failed: bool = False
1059
+ for i, cmd in enumerate(cmd_list, 1):
1060
+ log.info("Run the command {}", i)
1061
+ log.debug(
1062
+ "Run the command {}",
1063
+ f"{i}:\n {cmd}",
1064
+ )
1065
+ if (_cmd_res := os.system(cmd)) != 0:
1066
+ is_cmd_run_failed = True
1067
+ log.error(
1068
+ "Command run failed: status code {}\n Failed command: {}",
1069
+ _cmd_res,
1070
+ f"{i}. {cmd}",
1071
+ )
1072
+ break
997
1073
 
998
1074
  # 读取编码速度
999
1075
  speed: str = "N/A"