auto-editor 27.1.1__py3-none-any.whl → 28.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. auto_editor/__init__.py +1 -1
  2. auto_editor/__main__.py +0 -8
  3. auto_editor/cmds/cache.py +1 -1
  4. auto_editor/cmds/desc.py +8 -9
  5. auto_editor/cmds/info.py +4 -17
  6. auto_editor/cmds/palet.py +1 -1
  7. auto_editor/cmds/subdump.py +2 -4
  8. auto_editor/cmds/test.py +50 -31
  9. auto_editor/edit.py +52 -52
  10. auto_editor/{formats → exports}/fcp11.py +11 -7
  11. auto_editor/{formats → exports}/fcp7.py +7 -239
  12. auto_editor/exports/json.py +32 -0
  13. auto_editor/{formats → exports}/shotcut.py +8 -17
  14. auto_editor/ffwrapper.py +1 -3
  15. auto_editor/help.py +6 -17
  16. auto_editor/imports/__init__.py +0 -0
  17. auto_editor/imports/fcp7.py +275 -0
  18. auto_editor/{formats → imports}/json.py +16 -43
  19. auto_editor/json.py +2 -2
  20. auto_editor/lang/palet.py +6 -43
  21. auto_editor/lang/stdenv.py +8 -20
  22. auto_editor/lib/contracts.py +4 -6
  23. auto_editor/lib/data_structs.py +0 -3
  24. auto_editor/make_layers.py +13 -13
  25. auto_editor/preview.py +1 -2
  26. auto_editor/render/audio.py +9 -6
  27. auto_editor/render/video.py +3 -2
  28. auto_editor/timeline.py +35 -52
  29. {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/METADATA +2 -2
  30. auto_editor-28.0.1.dist-info/RECORD +56 -0
  31. {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/WHEEL +1 -1
  32. auto_editor/formats/utils.py +0 -56
  33. auto_editor-27.1.1.dist-info/RECORD +0 -54
  34. /auto_editor/{formats → exports}/__init__.py +0 -0
  35. {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/entry_points.txt +0 -0
  36. {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/licenses/LICENSE +0 -0
  37. {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "27.1.1"
1
+ __version__ = "28.0.1"
auto_editor/__main__.py CHANGED
@@ -78,7 +78,6 @@ class Args:
78
78
  audio_layout: str | None = None
79
79
  audio_bitrate: str = "auto"
80
80
  mix_audio_streams: bool = False
81
- keep_tracks_separate: bool = False
82
81
  audio_normalize: str = "#f"
83
82
 
84
83
  # Misc.
@@ -353,11 +352,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
353
352
  parser.add_argument(
354
353
  "--mix-audio-streams", flag=True, help="Mix all audio streams together into one"
355
354
  )
356
- parser.add_argument(
357
- "--keep-tracks-separate",
358
- flag=True,
359
- help="Don't mix all audio streams into one when exporting (default)",
360
- )
361
355
  parser.add_argument(
362
356
  "--audio-normalize",
363
357
  metavar="NORM-TYPE",
@@ -447,9 +441,7 @@ def main() -> None:
447
441
  ({"--export-to-resolve", "-exr"}, ["--export", "resolve"]),
448
442
  ({"--export-to-final-cut-pro", "-exf"}, ["--export", "final-cut-pro"]),
449
443
  ({"--export-to-shotcut", "-exs"}, ["--export", "shotcut"]),
450
- ({"--export-as-json"}, ["--export", "json"]),
451
444
  ({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
452
- ({"--keep-tracks-seperate"}, ["--keep-tracks-separate"]),
453
445
  ({"--edit-based-on"}, ["--edit"]),
454
446
  ],
455
447
  )
auto_editor/cmds/cache.py CHANGED
@@ -26,7 +26,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
26
26
  return
27
27
 
28
28
  def format_bytes(size: float) -> str:
29
- for unit in {"B", "KiB", "MiB", "GiB", "TiB"}:
29
+ for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
30
30
  if size < 1024:
31
31
  return f"{size:.2f} {unit}"
32
32
  size /= 1024
auto_editor/cmds/desc.py CHANGED
@@ -1,10 +1,8 @@
1
- from __future__ import annotations
2
-
3
1
  import sys
4
2
  from dataclasses import dataclass, field
5
3
 
6
- from auto_editor.ffwrapper import FileInfo
7
- from auto_editor.utils.log import Log
4
+ import bv
5
+
8
6
  from auto_editor.vanparse import ArgumentParser
9
7
 
10
8
 
@@ -22,11 +20,12 @@ def desc_options(parser: ArgumentParser) -> ArgumentParser:
22
20
  def main(sys_args: list[str] = sys.argv[1:]) -> None:
23
21
  args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
24
22
  for path in args.input:
25
- src = FileInfo.init(path, Log())
26
- if src.description is not None:
27
- sys.stdout.write(f"\n{src.description}\n\n")
28
- else:
29
- sys.stdout.write("\nNo description.\n\n")
23
+ try:
24
+ container = bv.open(path)
25
+ desc = container.metadata.get("description", None)
26
+ except Exception:
27
+ desc = None
28
+ sys.stdout.write("\nNo description.\n\n" if desc is None else f"\n{desc}\n\n")
30
29
 
31
30
 
32
31
  if __name__ == "__main__":
auto_editor/cmds/info.py CHANGED
@@ -8,7 +8,6 @@ from typing import Any, Literal, TypedDict
8
8
  from auto_editor.ffwrapper import FileInfo
9
9
  from auto_editor.json import dump
10
10
  from auto_editor.make_layers import make_sane_timebase
11
- from auto_editor.timeline import v3
12
11
  from auto_editor.utils.func import aspect_ratio
13
12
  from auto_editor.utils.log import Log
14
13
  from auto_editor.vanparse import ArgumentParser
@@ -86,19 +85,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
86
85
  log.error(f"Could not find '{file}'")
87
86
 
88
87
  ext = os.path.splitext(file)[1]
89
- if ext == ".json":
90
- from auto_editor.formats.json import read_json
91
-
92
- tl = read_json(file, log)
93
- file_info[file] = {"type": "timeline"}
94
- file_info[file]["version"] = "v3" if isinstance(tl, v3) else "v1"
95
-
96
- clip_lens = [clip.dur / clip.speed for clip in tl.a[0]]
97
- file_info[file]["clips"] = len(clip_lens)
98
-
99
- continue
100
-
101
- if ext in {".xml", ".fcpxml", ".mlt"}:
88
+ if ext in {".v1", ".v3", ".json", ".xml", ".fcpxml", ".mlt"}:
102
89
  file_info[file] = {"type": "timeline"}
103
90
  continue
104
91
 
@@ -126,7 +113,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
126
113
  f"{recTb.numerator}/{recTb.denominator}"
127
114
  )
128
115
 
129
- for track, v in enumerate(src.videos):
116
+ for v in src.videos:
130
117
  w, h = v.width, v.height
131
118
 
132
119
  vid: VideoJson = {
@@ -147,7 +134,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
147
134
  }
148
135
  file_info[file]["video"].append(vid)
149
136
 
150
- for track, a in enumerate(src.audios):
137
+ for a in src.audios:
151
138
  aud: AudioJson = {
152
139
  "codec": a.codec,
153
140
  "layout": a.layout,
@@ -158,7 +145,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
158
145
  }
159
146
  file_info[file]["audio"].append(aud)
160
147
 
161
- for track, s_stream in enumerate(src.subtitles):
148
+ for s_stream in src.subtitles:
162
149
  sub: SubtitleJson = {"codec": s_stream.codec, "lang": s_stream.lang}
163
150
  file_info[file]["subtitle"].append(sub)
164
151
 
auto_editor/cmds/palet.py CHANGED
@@ -14,7 +14,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
14
14
 
15
15
  env.update(make_standard_env())
16
16
  try:
17
- interpret(env, Parser(Lexer(sys_args[0], program_text, True)))
17
+ interpret(env, Parser(Lexer(sys_args[0], program_text)))
18
18
  except (MyError, ZeroDivisionError) as e:
19
19
  sys.stderr.write(f"error: {e}\n")
20
20
  sys.exit(1)
@@ -21,8 +21,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
21
21
  parser.add_argument("--json", flag=True)
22
22
  args = parser.parse_args(SubdumpArgs, sys_args)
23
23
 
24
- do_filter = True
25
-
26
24
  if args.json:
27
25
  data = {}
28
26
  for input_file in args.input:
@@ -46,7 +44,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
46
44
  startf = round(float(start), 3)
47
45
  endf = round(float(end), 3)
48
46
 
49
- if do_filter and endf - startf <= 0.02:
47
+ if endf - startf <= 0.02:
50
48
  continue
51
49
 
52
50
  for sub in packet.decode():
@@ -60,7 +58,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
60
58
  dump(data, sys.stdout, indent=4)
61
59
  return
62
60
 
63
- for i, input_file in enumerate(args.input):
61
+ for input_file in args.input:
64
62
  with bv.open(input_file) as container:
65
63
  for s in range(len(container.streams.subtitles)):
66
64
  print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
auto_editor/cmds/test.py CHANGED
@@ -202,6 +202,19 @@ class Runner:
202
202
  assert audio.language == "eng"
203
203
  assert audio.layout.name == "stereo"
204
204
 
205
+ def test_video_to_mp3(self) -> None:
206
+ out = self.main(["example.mp4"], [], output="example_ALTERED.mp3")
207
+ with bv.open(out) as container:
208
+ assert container.duration is not None
209
+ assert container.duration > 17300000 and container.duration < 2 << 24
210
+
211
+ assert len(container.streams) == 1
212
+ audio = container.streams[0]
213
+ assert isinstance(audio, bv.AudioStream)
214
+ assert audio.codec.name in ("mp3", "mp3float")
215
+ assert audio.sample_rate == 48000
216
+ assert audio.layout.name == "stereo"
217
+
205
218
  def test_to_mono(self) -> None:
206
219
  out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
207
220
  with bv.open(out) as container:
@@ -290,6 +303,7 @@ class Runner:
290
303
 
291
304
  def test_silent_threshold(self):
292
305
  with bv.open("resources/new-commentary.mp3") as container:
306
+ assert container.duration is not None
293
307
  assert container.duration / bv.time_base == 6.732
294
308
 
295
309
  out = self.main(
@@ -298,6 +312,7 @@ class Runner:
298
312
  out += ".mp3"
299
313
 
300
314
  with bv.open(out) as container:
315
+ assert container.duration is not None
301
316
  assert container.duration / bv.time_base == 6.552
302
317
 
303
318
  def test_track(self):
@@ -305,7 +320,9 @@ class Runner:
305
320
  assert len(fileinfo(out).audios) == 2
306
321
 
307
322
  def test_export_json(self):
308
- out = self.main(["example.mp4"], ["--export_as_json"], "c77130d763d40e8.json")
323
+ out = self.main(["example.mp4"], ["--export", "v1"], "c77130d763d40e8.json")
324
+ self.main([out], [])
325
+ out = self.main(["example.mp4"], ["--export", "v1"], "c77130d763d40e8.v1")
309
326
  self.main([out], [])
310
327
 
311
328
  def test_import_v1(self):
@@ -317,10 +334,19 @@ class Runner:
317
334
 
318
335
  self.main([path], [])
319
336
 
320
- def test_premiere_named_export(self):
337
+ def test_res_with_v1(self):
338
+ v1 = self.main(["example.mp4"], ["--export", "v1"], "input.v1")
339
+ out = self.main([v1], ["-res", "720,720"], "output.mp4")
340
+
341
+ output = fileinfo(out)
342
+ assert output.videos[0].width == 720
343
+ assert output.videos[0].height == 720
344
+ assert len(output.audios) == 1
345
+
346
+ def test_premiere_named_export(self) -> None:
321
347
  self.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
322
348
 
323
- def test_export_subtitles(self):
349
+ def test_export_subtitles(self) -> None:
324
350
  # cn = fileinfo(self.main(["resources/mov_text.mp4"], [], "movtext_out.mp4"))
325
351
 
326
352
  # assert len(cn.videos) == 1
@@ -332,7 +358,7 @@ class Runner:
332
358
  assert len(cn.audios) == 1
333
359
  assert len(cn.subtitles) == 1
334
360
 
335
- def test_scale(self):
361
+ def test_scale(self) -> None:
336
362
  cn = fileinfo(self.main(["example.mp4"], ["--scale", "1.5"], "scale.mp4"))
337
363
  assert cn.videos[0].fps == 30
338
364
  assert cn.videos[0].width == 1920
@@ -363,7 +389,7 @@ class Runner:
363
389
  # assert len(cn.videos) == 1
364
390
  # assert len(cn.audios) == 2
365
391
 
366
- def test_premiere(self):
392
+ def test_premiere(self) -> None:
367
393
  for test_name in all_files:
368
394
  if test_name == "multi-track.mov":
369
395
  continue
@@ -379,14 +405,14 @@ class Runner:
379
405
  self.main([test_file], ["-exs"])
380
406
  self.main([test_file], ["--stats"])
381
407
 
382
- def test_clip_sequence(self):
408
+ def test_clip_sequence(self) -> None:
383
409
  for test_name in all_files:
384
410
  test_file = f"resources/{test_name}"
385
- self.main([test_file], ["--export_as_clip_sequence"])
411
+ self.main([test_file], ["--export", "clip-sequence"])
386
412
 
387
- def test_codecs(self):
388
- self.main(["example.mp4"], ["--video_codec", "h264"])
389
- self.main(["example.mp4"], ["--audio_codec", "ac3"])
413
+ def test_codecs(self) -> None:
414
+ self.main(["example.mp4"], ["--video-codec", "h264"])
415
+ self.main(["example.mp4"], ["--audio-codec", "ac3"])
390
416
 
391
417
  # Issue #241
392
418
  def test_multi_track_edit(self):
@@ -503,24 +529,21 @@ class Runner:
503
529
  assert output.videos[0].pix_fmt == "yuv420p"
504
530
 
505
531
  def test_encode_hevc(self):
506
- if os.environ.get("GITHUB_ACTIONS") == "true":
507
- raise SkipTest()
508
-
509
532
  out = self.main(["resources/testsrc.mp4"], ["-c:v", "hevc"], "out.mkv")
510
533
  output = fileinfo(out)
511
534
  assert output.videos[0].codec == "hevc"
512
535
  assert output.videos[0].pix_fmt == "yuv420p"
513
536
 
514
537
  # Issue 280
515
- def test_SAR(self):
538
+ def test_SAR(self) -> None:
516
539
  out = self.main(["resources/SAR-2by3.mp4"], [], "2by3_out.mp4")
517
540
  assert fileinfo(out).videos[0].sar == Fraction(2, 3)
518
541
 
519
- def test_audio_norm_f(self):
520
- return self.main(["example.mp4"], ["--audio-normalize", "#f"])
542
+ def test_audio_norm_f(self) -> None:
543
+ self.main(["example.mp4"], ["--audio-normalize", "#f"])
521
544
 
522
- def test_audio_norm_ebu(self):
523
- return self.main(
545
+ def test_audio_norm_ebu(self) -> None:
546
+ self.main(
524
547
  ["example.mp4"], ["--audio-normalize", "ebu:i=-5,lra=20,gain=5,tp=-1"]
525
548
  )
526
549
 
@@ -536,26 +559,26 @@ class Runner:
536
559
  except MyError as e:
537
560
  raise ValueError(f"{text}\nMyError: {e}")
538
561
 
562
+ result_val = results[-1]
539
563
  if isinstance(expected, np.ndarray):
540
- if not np.array_equal(expected, results[-1]):
564
+ if not isinstance(result_val, np.ndarray):
565
+ raise ValueError(f"{text}: Result is not an ndarray")
566
+ if not np.array_equal(expected, result_val):
541
567
  raise ValueError(f"{text}: Numpy arrays don't match")
542
- elif expected != results[-1]:
543
- raise ValueError(f"{text}: Expected: {expected}, got {results[-1]}")
568
+ elif expected != result_val:
569
+ raise ValueError(f"{text}: Expected: {expected}, got {result_val}")
544
570
 
545
571
  cases(
546
572
  ("345", 345),
547
573
  ("238.5", 238.5),
548
574
  ("-34", -34),
549
575
  ("-98.3", -98.3),
550
- ("+3i", 3j),
551
576
  ("3sec", 90),
552
577
  ("-3sec", -90),
553
578
  ("0.2sec", 6),
554
579
  ("(+ 4 3)", 7),
555
580
  ("(+ 4 3 2)", 9),
556
581
  ("(+ 10.5 3)", 13.5),
557
- ("(+ 3+4i -2-2i)", 1 + 2j),
558
- ("(+ 3+4i -2-2i 5)", 6 + 2j),
559
582
  ("(- 4 3)", 1),
560
583
  ("(- 3)", -3),
561
584
  ("(- 10.5 3)", 7.5),
@@ -564,7 +587,6 @@ class Runner:
564
587
  ("(/ 5)", 0.2),
565
588
  ("(/ 6 1)", 6.0),
566
589
  ("30/1", Fraction(30)),
567
- ("(sqrt -4)", 2j),
568
590
  ("(pow 2 3)", 8),
569
591
  ("(pow 4 0.5)", 2.0),
570
592
  ("(abs 1.0)", 1.0),
@@ -579,7 +601,6 @@ class Runner:
579
601
  ("(int? #t)", False),
580
602
  ("(int? #f)", False),
581
603
  ("(int? 4/5)", False),
582
- ("(int? 0+2i)", False),
583
604
  ('(int? "hello")', False),
584
605
  ('(int? "3")', False),
585
606
  ("(float? -23.4)", True),
@@ -593,7 +614,6 @@ class Runner:
593
614
  ('(define apple "Red Wood") apple', "Red Wood"),
594
615
  ("(= 1 1.0)", True),
595
616
  ("(= 1 2)", False),
596
- ("(= 2+3i 2+3i 2+3i)", True),
597
617
  ("(= 1)", True),
598
618
  ("(+)", 0),
599
619
  ("(*)", 1),
@@ -602,7 +622,6 @@ class Runner:
602
622
  ('(if #f mango "Hi")', "Hi"),
603
623
  ('{if (= [+ 3 4] 7) "yes" "no"}', "yes"),
604
624
  ("((if #t + -) 3 4)", 7),
605
- ("((if #t + oops) 3+3i 4-2i)", 7 + 1j),
606
625
  ("((if #f + -) 3 4)", -1),
607
626
  ("(when (positive? 3) 17)", 17),
608
627
  ("(string)", ""),
@@ -675,14 +694,14 @@ class Runner:
675
694
  ('#(#("sym" "symbol?") "bool?")', [["sym", "symbol?"], "bool?"]),
676
695
  )
677
696
 
678
- def palet_scripts(self):
697
+ def palet_scripts(self) -> None:
679
698
  self.raw(["palet", "resources/scripts/scope.pal"])
680
699
  self.raw(["palet", "resources/scripts/maxcut.pal"])
681
700
  self.raw(["palet", "resources/scripts/case.pal"])
682
701
  self.raw(["palet", "resources/scripts/testmath.pal"])
683
702
 
684
703
 
685
- def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
704
+ def run_tests(tests: list[Callable], args: TestArgs) -> None:
686
705
  if args.only != []:
687
706
  tests = list(filter(lambda t: t.__name__ in args.only, tests))
688
707
 
@@ -781,7 +800,7 @@ def main(sys_args: list[str] | None = None) -> None:
781
800
  ]
782
801
  )
783
802
  try:
784
- run_tests(run, tests, args)
803
+ run_tests(tests, args)
785
804
  except KeyboardInterrupt:
786
805
  print("Testing Interrupted by User.")
787
806
  shutil.rmtree(run.temp_dir)
auto_editor/edit.py CHANGED
@@ -17,7 +17,7 @@ from auto_editor.make_layers import clipify, make_av, make_timeline
17
17
  from auto_editor.render.audio import make_new_audio
18
18
  from auto_editor.render.subtitle import make_new_subtitles
19
19
  from auto_editor.render.video import render_av
20
- from auto_editor.timeline import v1, v3
20
+ from auto_editor.timeline import set_stream_to_0, v1, v3
21
21
  from auto_editor.utils.bar import initBar
22
22
  from auto_editor.utils.chunks import Chunk, Chunks
23
23
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
  def set_output(
32
32
  out: str | None, _export: str | None, path: Path | None, log: Log
33
33
  ) -> tuple[str, dict[str, Any]]:
34
- if out is None:
34
+ if out is None or out == "-":
35
35
  if path is None:
36
36
  log.error("`--output` must be set.") # When a timeline file is the input.
37
37
  root, ext = splitext(path)
@@ -43,16 +43,19 @@ def set_output(
43
43
  ext = ".mp4" if path is None else path.suffix
44
44
 
45
45
  if _export is None:
46
- if ext == ".xml":
47
- export = {"export": "premiere"}
48
- elif ext == ".fcpxml":
49
- export = {"export": "final-cut-pro"}
50
- elif ext == ".mlt":
51
- export = {"export": "shotcut"}
52
- elif ext == ".json":
53
- export = {"export": "json"}
54
- else:
55
- export = {"export": "default"}
46
+ match ext:
47
+ case ".xml":
48
+ export: dict[str, Any] = {"export": "premiere"}
49
+ case ".fcpxml":
50
+ export = {"export": "final-cut-pro"}
51
+ case ".mlt":
52
+ export = {"export": "shotcut"}
53
+ case ".json" | ".v1":
54
+ export = {"export": "v1"}
55
+ case ".v3":
56
+ export = {"export": "v3"}
57
+ case _:
58
+ export = {"export": "default"}
56
59
  else:
57
60
  export = parse_export(_export, log)
58
61
 
@@ -63,11 +66,13 @@ def set_output(
63
66
  "resolve": ".fcpxml",
64
67
  "shotcut": ".mlt",
65
68
  "json": ".json",
66
- "audio": ".wav",
67
69
  }
68
70
  if export["export"] in ext_map:
69
71
  ext = ext_map[export["export"]]
70
72
 
73
+ if out == "-":
74
+ return "-", export
75
+
71
76
  if out is None:
72
77
  return f"{root}_ALTERED{ext}", export
73
78
 
@@ -134,53 +139,44 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
134
139
  name, text = exploded
135
140
 
136
141
  name_attr = pAttr("name", "Auto-Editor Media Group", is_str)
137
-
138
- parsing: dict[str, pAttrs] = {
142
+ parsing = {
139
143
  "default": pAttrs("default"),
140
144
  "premiere": pAttrs("premiere", name_attr),
141
- "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
142
145
  "final-cut-pro": pAttrs(
143
146
  "final-cut-pro", name_attr, pAttr("version", 11, is_int)
144
147
  ),
145
148
  "resolve": pAttrs("resolve", name_attr),
149
+ "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
146
150
  "shotcut": pAttrs("shotcut"),
147
- "json": pAttrs("json", pAttr("api", 3, is_int)),
148
- "timeline": pAttrs("json", pAttr("api", 3, is_int)),
149
- "audio": pAttrs("audio"),
151
+ "v1": pAttrs("v1"),
152
+ "v3": pAttrs("v3"),
150
153
  "clip-sequence": pAttrs("clip-sequence"),
151
154
  }
152
155
 
153
156
  if name in parsing:
154
157
  try:
155
- _tmp = parse_with_palet(text, parsing[name], {})
156
- _tmp["export"] = name
157
- return _tmp
158
+ return {"export": name} | parse_with_palet(text, parsing[name], {})
158
159
  except ParserError as e:
159
160
  log.error(e)
160
161
 
161
- log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
162
+ valid_choices = " ".join(f'"{s}"' for s in parsing.keys())
163
+ log.error(f'Invalid export format: "{name}"\nValid choices: {valid_choices}')
162
164
 
163
165
 
164
166
  def edit_media(paths: list[str], args: Args, log: Log) -> None:
165
167
  bar = initBar(args.progress)
166
168
  tl = src = use_path = None
167
169
 
168
- if args.keep_tracks_separate:
169
- log.deprecated("--keep-tracks-separate is deprecated.")
170
- args.keep_tracks_separate = False
171
-
172
170
  if paths:
173
171
  path_ext = splitext(paths[0])[1].lower()
174
172
  if path_ext == ".xml":
175
- from auto_editor.formats.fcp7 import fcp7_read_xml
173
+ from auto_editor.imports.fcp7 import fcp7_read_xml
176
174
 
177
175
  tl = fcp7_read_xml(paths[0], log)
178
176
  elif path_ext == ".mlt":
179
- from auto_editor.formats.shotcut import shotcut_read_mlt
180
-
181
- tl = shotcut_read_mlt(paths[0], log)
182
- elif path_ext == ".json":
183
- from auto_editor.formats.json import read_json
177
+ log.error("Reading mlt files not implemented")
178
+ elif path_ext in {".v1", ".v3", ".json"}:
179
+ from auto_editor.imports.json import read_json
184
180
 
185
181
  tl = read_json(paths[0], log)
186
182
  else:
@@ -192,7 +188,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
192
188
  assert "export" in export_ops
193
189
  export = export_ops["export"]
194
190
 
195
- if export == "timeline":
191
+ if output == "-":
192
+ # When printing to stdout, silence all logs.
196
193
  log.quiet = True
197
194
 
198
195
  if not args.preview:
@@ -211,12 +208,15 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
211
208
 
212
209
  if tl is None:
213
210
  tl = make_timeline(sources, args, samplerate, bar, log)
214
-
215
- if export == "timeline":
216
- from auto_editor.formats.json import make_json_timeline
217
-
218
- make_json_timeline(export_ops["api"], 0, tl, log)
219
- return
211
+ else:
212
+ if args.resolution is not None:
213
+ tl.T.res = args.resolution
214
+ if args.background is not None:
215
+ tl.background = args.background
216
+ if args.frame_rate is not None:
217
+ log.warning(
218
+ "Setting timebase/framerate is not supported when importing timelines"
219
+ )
220
220
 
221
221
  if args.preview:
222
222
  from auto_editor.preview import preview
@@ -224,41 +224,41 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
224
224
  preview(tl, log)
225
225
  return
226
226
 
227
- if export == "json":
228
- from auto_editor.formats.json import make_json_timeline
227
+ if export in {"v1", "v3"}:
228
+ from auto_editor.exports.json import make_json_timeline
229
229
 
230
- make_json_timeline(export_ops["api"], output, tl, log)
230
+ make_json_timeline(export, output, tl, log)
231
231
  return
232
232
 
233
233
  if export in {"premiere", "resolve-fcp7"}:
234
- from auto_editor.formats.fcp7 import fcp7_write_xml
234
+ from auto_editor.exports.fcp7 import fcp7_write_xml
235
235
 
236
236
  is_resolve = export.startswith("resolve")
237
237
  fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
238
238
  return
239
239
 
240
240
  if export == "final-cut-pro":
241
- from auto_editor.formats.fcp11 import fcp11_write_xml
241
+ from auto_editor.exports.fcp11 import fcp11_write_xml
242
242
 
243
243
  ver = export_ops["version"]
244
244
  fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
245
245
  return
246
246
 
247
247
  if export == "resolve":
248
- from auto_editor.formats.fcp11 import fcp11_write_xml
249
- from auto_editor.timeline import set_stream_to_0
248
+ from auto_editor.exports.fcp11 import fcp11_write_xml
250
249
 
251
- assert src is not None
252
- set_stream_to_0(src, tl, log)
250
+ set_stream_to_0(tl, log)
253
251
  fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
254
252
  return
255
253
 
256
254
  if export == "shotcut":
257
- from auto_editor.formats.shotcut import shotcut_write_mlt
255
+ from auto_editor.exports.shotcut import shotcut_write_mlt
258
256
 
259
257
  shotcut_write_mlt(output, tl)
260
258
  return
261
259
 
260
+ if output == "-":
261
+ log.error("Exporting media files to stdout is not supported.")
262
262
  out_ext = splitext(output)[1].replace(".", "")
263
263
 
264
264
  # Check if export options make sense.
@@ -286,7 +286,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
286
286
  output = bv.open(output_path, "w", container_options=options)
287
287
 
288
288
  # Setup video
289
- if ctr.default_vid != "none" and tl.v:
289
+ if ctr.default_vid not in ("none", "png") and tl.v:
290
290
  vframes = render_av(output, tl, args, log)
291
291
  output_stream: bv.VideoStream | None
292
292
  output_stream = next(vframes) # type: ignore
@@ -522,7 +522,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
522
522
 
523
523
  log.stop_timer()
524
524
 
525
- if not args.no_open and export in {"default", "audio"}:
525
+ if not args.no_open and export == "default":
526
526
  if args.player is None:
527
527
  if sys.platform == "win32":
528
528
  try:
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, cast
3
+ import xml.etree.ElementTree as ET
4
+ from typing import TYPE_CHECKING, cast
4
5
  from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
5
6
 
7
+ from auto_editor.timeline import Clip, v3
8
+
6
9
  if TYPE_CHECKING:
7
- from collections.abc import Sequence
8
10
  from fractions import Fraction
9
11
 
10
12
  from auto_editor.ffwrapper import FileInfo
11
- from auto_editor.timeline import TlAudio, TlVideo, v3
12
13
  from auto_editor.utils.log import Log
13
14
 
14
15
 
@@ -70,7 +71,7 @@ def fcp11_write_xml(
70
71
  resources = SubElement(fcpxml, "resources")
71
72
 
72
73
  src_dur = 0
73
- tl_dur = 0 if resolve else tl.out_len()
74
+ tl_dur = 0 if resolve else len(tl)
74
75
 
75
76
  for i, one_src in enumerate(tl.unique_sources()):
76
77
  if i == 0:
@@ -120,7 +121,7 @@ def fcp11_write_xml(
120
121
  )
121
122
  spine = SubElement(sequence, "spine")
122
123
 
123
- def make_clip(ref: str, clip: TlVideo | TlAudio) -> None:
124
+ def make_clip(ref: str, clip: Clip) -> None:
124
125
  clip_properties = {
125
126
  "name": proj_name,
126
127
  "ref": ref,
@@ -145,7 +146,7 @@ def fcp11_write_xml(
145
146
  )
146
147
 
147
148
  if tl.v and tl.v[0]:
148
- clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
149
+ clips = cast(list[Clip], tl.v[0])
149
150
  elif tl.a and tl.a[0]:
150
151
  clips = tl.a[0]
151
152
  else:
@@ -162,4 +163,7 @@ def fcp11_write_xml(
162
163
 
163
164
  tree = ElementTree(fcpxml)
164
165
  indent(tree, space="\t", level=0)
165
- tree.write(output, xml_declaration=True, encoding="utf-8")
166
+ if output == "-":
167
+ print(ET.tostring(fcpxml, encoding="unicode"))
168
+ else:
169
+ tree.write(output, xml_declaration=True, encoding="utf-8")