auto-editor 25.2.0__py3-none-any.whl → 25.3.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.
auto_editor/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "25.2.0"
1
+ __version__ = "25.3.1"
auto_editor/__main__.py CHANGED
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ import re
3
4
  import sys
4
5
  from os import environ
6
+ from os.path import exists, isdir, isfile, lexists, splitext
7
+ from subprocess import run
5
8
 
6
9
  import auto_editor
7
10
  from auto_editor.edit import edit_media
8
- from auto_editor.ffwrapper import FFmpeg
11
+ from auto_editor.ffwrapper import FFmpeg, initFFmpeg
12
+ from auto_editor.utils.func import get_stdout
9
13
  from auto_editor.utils.log import Log
10
14
  from auto_editor.utils.types import (
11
15
  Args,
@@ -20,7 +24,6 @@ from auto_editor.utils.types import (
20
24
  speed_range,
21
25
  time_range,
22
26
  )
23
- from auto_editor.validate_input import valid_input
24
27
  from auto_editor.vanparse import ArgumentParser
25
28
 
26
29
 
@@ -254,6 +257,11 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
254
257
  flag=True,
255
258
  help="Disable the inclusion of subtitle streams in the output file",
256
259
  )
260
+ parser.add_argument(
261
+ "-dn",
262
+ flag=True,
263
+ help="Disable the inclusion of data streams in the output file",
264
+ )
257
265
  parser.add_argument(
258
266
  "--extras",
259
267
  metavar="CMD",
@@ -269,6 +277,50 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
269
277
  return parser
270
278
 
271
279
 
280
+ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
281
+ log.conwrite("Downloading video...")
282
+
283
+ def get_domain(url: str) -> str:
284
+ t = __import__("urllib").parse.urlparse(url).netloc
285
+ return ".".join(t.split(".")[-2:])
286
+
287
+ download_format = args.download_format
288
+ if download_format is None and get_domain(my_input) == "youtube.com":
289
+ download_format = "bestvideo[ext=mp4]+bestaudio[ext=m4a]"
290
+
291
+ if args.output_format is None:
292
+ output_format = re.sub(r"\W+", "-", splitext(my_input)[0]) + ".%(ext)s"
293
+ else:
294
+ output_format = args.output_format
295
+
296
+ yt_dlp_path = args.yt_dlp_location
297
+
298
+ cmd = ["--ffmpeg-location", ffmpeg.path]
299
+
300
+ if download_format is not None:
301
+ cmd.extend(["-f", download_format])
302
+
303
+ cmd.extend(["-o", output_format, my_input])
304
+
305
+ if args.yt_dlp_extras is not None:
306
+ cmd.extend(args.yt_dlp_extras.split(" "))
307
+
308
+ try:
309
+ location = get_stdout(
310
+ [yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
311
+ ).strip()
312
+ except FileNotFoundError:
313
+ log.error("Program `yt-dlp` must be installed and on PATH.")
314
+
315
+ if not isfile(location):
316
+ run([yt_dlp_path] + cmd)
317
+
318
+ if not isfile(location):
319
+ log.error(f"Download file wasn't created: {location}")
320
+
321
+ return location
322
+
323
+
272
324
  def main() -> None:
273
325
  subcommands = ("test", "info", "levels", "subdump", "desc", "repl", "palet")
274
326
 
@@ -279,8 +331,7 @@ def main() -> None:
279
331
  obj.main(sys.argv[2:])
280
332
  return
281
333
 
282
- ff_color = "AV_LOG_FORCE_NOCOLOR"
283
- no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
334
+ no_color = bool(environ.get("NO_COLOR") or environ.get("AV_LOG_FORCE_NOCOLOR"))
284
335
  log = Log(no_color=no_color)
285
336
 
286
337
  args = main_options(ArgumentParser("Auto-Editor")).parse_args(
@@ -322,13 +373,28 @@ def main() -> None:
322
373
  is_machine = args.progress == "machine"
323
374
  log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
324
375
 
325
- ffmpeg = FFmpeg(
376
+ ffmpeg = initFFmpeg(
377
+ log,
326
378
  args.ffmpeg_location,
327
379
  args.my_ffmpeg,
328
380
  args.show_ffmpeg_commands,
329
381
  args.show_ffmpeg_output,
330
382
  )
331
- paths = valid_input(args.input, ffmpeg, args, log)
383
+ paths = []
384
+ for my_input in args.input:
385
+ if my_input.startswith("http://") or my_input.startswith("https://"):
386
+ paths.append(download_video(my_input, args, ffmpeg, log))
387
+ else:
388
+ if not splitext(my_input)[1]:
389
+ if isdir(my_input):
390
+ log.error("Input must be a file or a URL, not a directory.")
391
+ if exists(my_input):
392
+ log.error(f"Input file must have an extension: {my_input}")
393
+ if lexists(my_input):
394
+ log.error(f"Input file is a broken symbolic link: {my_input}")
395
+ if my_input.startswith("-"):
396
+ log.error(f"Option/Input file doesn't exist: {my_input}")
397
+ paths.append(my_input)
332
398
 
333
399
  try:
334
400
  edit_media(paths, ffmpeg, args, log)
auto_editor/analyze.py CHANGED
@@ -14,12 +14,11 @@ from av.audio.fifo import AudioFifo
14
14
  from av.subtitles.subtitle import AssSubtitle
15
15
 
16
16
  from auto_editor import __version__
17
- from auto_editor.utils.subtitle_tools import convert_ass_to_text
18
17
 
19
18
  if TYPE_CHECKING:
20
- from collections.abc import Iterator
19
+ from collections.abc import Iterator, Sequence
21
20
  from fractions import Fraction
22
- from typing import Any
21
+ from pathlib import Path
23
22
 
24
23
  from numpy.typing import NDArray
25
24
 
@@ -70,15 +69,6 @@ def mut_remove_large(
70
69
  active = False
71
70
 
72
71
 
73
- def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
74
- key = f"{tag}:{tb}:"
75
- for k, v in obj.items():
76
- key += f"{k}={v},"
77
-
78
- key = key[:-1] # remove unnecessary char
79
- return key
80
-
81
-
82
72
  def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
83
73
  fifo = AudioFifo()
84
74
  try:
@@ -122,7 +112,7 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
122
112
 
123
113
  prev_frame = None
124
114
  current_frame = None
125
- total_pixels = src.videos[0].width * src.videos[0].height
115
+ total_pixels = None
126
116
  index = 0
127
117
  prev_index = -1
128
118
 
@@ -140,10 +130,13 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
140
130
  continue
141
131
 
142
132
  graph.push(unframe)
143
- frame = graph.pull()
133
+ frame = graph.vpull()
144
134
  assert frame.time is not None
145
135
  index = round(frame.time * tb)
146
136
 
137
+ if total_pixels is None:
138
+ total_pixels = frame.width * frame.height
139
+
147
140
  current_frame = frame.to_ndarray()
148
141
  if prev_frame is None:
149
142
  value = np.float32(0.0)
@@ -161,6 +154,12 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
161
154
  container.close()
162
155
 
163
156
 
157
+ def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
158
+ mod_time = int(path.stat().st_mtime)
159
+ key = f"{path.name}:{mod_time:x}:{kind}:{tb}:"
160
+ return key + ",".join(f"{v}" for v in obj)
161
+
162
+
164
163
  @dataclass(slots=True)
165
164
  class Levels:
166
165
  src: FileInfo
@@ -173,7 +172,7 @@ class Levels:
173
172
  @property
174
173
  def media_length(self) -> int:
175
174
  if self.src.audios:
176
- if (arr := self.read_cache("audio", {"stream": 0})) is not None:
175
+ if (arr := self.read_cache("audio", (0,))) is not None:
177
176
  return len(arr)
178
177
 
179
178
  result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
@@ -201,7 +200,7 @@ class Levels:
201
200
  def all(self) -> NDArray[np.bool_]:
202
201
  return np.zeros(self.media_length, dtype=np.bool_)
203
202
 
204
- def read_cache(self, tag: str, obj: dict[str, Any]) -> None | np.ndarray:
203
+ def read_cache(self, kind: str, obj: Sequence[object]) -> None | np.ndarray:
205
204
  if self.no_cache:
206
205
  return None
207
206
 
@@ -213,14 +212,14 @@ class Levels:
213
212
  self.log.debug(e)
214
213
  return None
215
214
 
216
- key = f"{self.src.path}:{obj_tag(tag, self.tb, obj)}"
215
+ key = obj_tag(self.src.path, kind, self.tb, obj)
217
216
  if key not in npzfile.files:
218
217
  return None
219
218
 
220
219
  self.log.debug("Using cache")
221
220
  return npzfile[key]
222
221
 
223
- def cache(self, arr: np.ndarray, tag: str, obj: dict[str, Any]) -> np.ndarray:
222
+ def cache(self, arr: np.ndarray, kind: str, obj: Sequence[object]) -> np.ndarray:
224
223
  if self.no_cache:
225
224
  return arr
226
225
 
@@ -228,8 +227,8 @@ class Levels:
228
227
  if not os.path.exists(workdur):
229
228
  os.mkdir(workdur)
230
229
 
231
- tag = obj_tag(tag, self.tb, obj)
232
- np.savez(os.path.join(workdur, "cache.npz"), **{f"{self.src.path}:{tag}": arr})
230
+ key = obj_tag(self.src.path, kind, self.tb, obj)
231
+ np.savez(os.path.join(workdur, "cache.npz"), **{key: arr})
233
232
 
234
233
  return arr
235
234
 
@@ -237,7 +236,7 @@ class Levels:
237
236
  if stream >= len(self.src.audios):
238
237
  raise LevelError(f"audio: audio stream '{stream}' does not exist.")
239
238
 
240
- if (arr := self.read_cache("audio", {"stream": stream})) is not None:
239
+ if (arr := self.read_cache("audio", (stream,))) is not None:
241
240
  return arr
242
241
 
243
242
  with av.open(self.src.path, "r") as container:
@@ -264,13 +263,13 @@ class Levels:
264
263
  index += 1
265
264
 
266
265
  bar.end()
267
- return self.cache(result[:index], "audio", {"stream": stream})
266
+ return self.cache(result[:index], "audio", (stream,))
268
267
 
269
268
  def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
270
269
  if stream >= len(self.src.videos):
271
270
  raise LevelError(f"motion: video stream '{stream}' does not exist.")
272
271
 
273
- mobj = {"stream": stream, "width": width, "blur": blur}
272
+ mobj = (stream, width, blur)
274
273
  if (arr := self.read_cache("motion", mobj)) is not None:
275
274
  return arr
276
275
 
@@ -359,11 +358,10 @@ class Levels:
359
358
  san_end = round((start + dur) * self.tb)
360
359
 
361
360
  for sub in subset:
362
- if isinstance(sub, AssSubtitle):
363
- line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
364
- else:
361
+ if not isinstance(sub, AssSubtitle):
365
362
  continue
366
363
 
364
+ line = sub.dialogue.decode(errors="ignore")
367
365
  if line and re.search(re_pattern, line):
368
366
  result[san_start:san_end] = 1
369
367
  count += 1
auto_editor/edit.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import sys
5
+ from subprocess import run
4
6
  from typing import Any
5
7
 
6
8
  from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
@@ -11,11 +13,10 @@ from auto_editor.render.audio import make_new_audio
11
13
  from auto_editor.render.subtitle import make_new_subtitles
12
14
  from auto_editor.render.video import render_av
13
15
  from auto_editor.timeline import v1, v3
14
- from auto_editor.utils.bar import Bar
16
+ from auto_editor.utils.bar import initBar
15
17
  from auto_editor.utils.chunks import Chunk, Chunks
16
18
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
17
19
  from auto_editor.utils.container import Container, container_constructor
18
- from auto_editor.utils.func import open_with_system_default
19
20
  from auto_editor.utils.log import Log
20
21
  from auto_editor.utils.types import Args
21
22
 
@@ -125,7 +126,9 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
125
126
  "default": pAttrs("default"),
126
127
  "premiere": pAttrs("premiere", name_attr),
127
128
  "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
128
- "final-cut-pro": pAttrs("final-cut-pro", name_attr),
129
+ "final-cut-pro": pAttrs(
130
+ "final-cut-pro", name_attr, pAttr("version", 11, is_int)
131
+ ),
129
132
  "resolve": pAttrs("resolve", name_attr),
130
133
  "shotcut": pAttrs("shotcut"),
131
134
  "json": pAttrs("json", pAttr("api", 3, is_int)),
@@ -146,7 +149,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
146
149
 
147
150
 
148
151
  def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
149
- bar = Bar(args.progress)
152
+ bar = initBar(args.progress)
150
153
  tl = None
151
154
 
152
155
  if paths:
@@ -232,11 +235,19 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
232
235
  fcp7_write_xml(export_ops["name"], output, is_resolve, tl, log)
233
236
  return
234
237
 
235
- if export in ("final-cut-pro", "resolve"):
238
+ if export == "final-cut-pro":
236
239
  from auto_editor.formats.fcp11 import fcp11_write_xml
237
240
 
238
- is_resolve = export.startswith("resolve")
239
- fcp11_write_xml(export_ops["name"], ffmpeg, output, is_resolve, tl, log)
241
+ ver = export_ops["version"]
242
+ fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
243
+ return
244
+
245
+ if export == "resolve":
246
+ from auto_editor.formats.fcp11 import fcp11_write_xml
247
+ from auto_editor.timeline import set_stream_to_0
248
+
249
+ set_stream_to_0(tl, log)
250
+ fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
240
251
  return
241
252
 
242
253
  if export == "shotcut":
@@ -318,7 +329,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
318
329
  total_frames = tl.v1.chunks[-1][1] - 1
319
330
  clip_num = 0
320
331
  for chunk in tl.v1.chunks:
321
- if chunk[2] == 99999:
332
+ if chunk[2] == 0 or chunk[2] >= 99999:
322
333
  continue
323
334
 
324
335
  padded_chunks = pad_chunk(chunk, total_frames)
@@ -344,11 +355,23 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
344
355
 
345
356
  log.stop_timer()
346
357
 
347
- if not args.no_open and export in ("default", "audio", "clip-sequence"):
358
+ if not args.no_open and export in ("default", "audio"):
348
359
  if args.player is None:
349
- open_with_system_default(output, log)
360
+ if sys.platform == "win32":
361
+ try:
362
+ os.startfile(output)
363
+ except OSError:
364
+ log.warning(f"Could not find application to open file: {output}")
365
+ else:
366
+ try: # MacOS case
367
+ run(["open", output])
368
+ except Exception:
369
+ try: # WSL2 case
370
+ run(["cmd.exe", "/C", "start", output])
371
+ except Exception:
372
+ try: # Linux case
373
+ run(["xdg-open", output])
374
+ except Exception:
375
+ log.warning(f"Could not open output file: {output}")
350
376
  else:
351
- import subprocess
352
- from shlex import split
353
-
354
- subprocess.run(split(args.player) + [output])
377
+ run(__import__("shlex").split(args.player) + [output])
auto_editor/ffwrapper.py CHANGED
@@ -1,83 +1,59 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os.path
4
- import subprocess
5
4
  import sys
6
5
  from dataclasses import dataclass
7
6
  from fractions import Fraction
8
7
  from pathlib import Path
9
8
  from re import search
10
9
  from shutil import which
11
- from subprocess import PIPE, Popen
10
+ from subprocess import PIPE, Popen, run
12
11
  from typing import Any
13
12
 
14
13
  import av
15
14
 
16
- from auto_editor.utils.func import get_stdout
17
15
  from auto_editor.utils.log import Log
18
16
 
19
17
 
20
- class FFmpeg:
21
- __slots__ = ("debug", "show_cmd", "path", "version")
22
-
23
- def __init__(
24
- self,
25
- ff_location: str | None = None,
26
- my_ffmpeg: bool = False,
27
- show_cmd: bool = False,
28
- debug: bool = False,
29
- ):
30
- def _set_ff_path(ff_location: str | None, my_ffmpeg: bool) -> str:
31
- if ff_location is not None:
32
- return ff_location
33
- if my_ffmpeg:
34
- return "ffmpeg"
35
-
36
- try:
37
- import ae_ffmpeg
38
-
39
- return ae_ffmpeg.get_path()
40
- except ImportError:
41
- return "ffmpeg"
42
-
43
- self.debug = debug
44
- self.show_cmd = show_cmd
45
- _path: str | None = _set_ff_path(ff_location, my_ffmpeg)
46
-
47
- if _path == "ffmpeg":
48
- _path = which("ffmpeg")
49
-
50
- if _path is None:
51
- Log().error("Did not find ffmpeg on PATH.")
52
- self.path = _path
53
-
18
+ def initFFmpeg(
19
+ log: Log, ff_location: str | None, my_ffmpeg: bool, show_cmd: bool, debug: bool
20
+ ) -> FFmpeg:
21
+ if ff_location is not None:
22
+ program = ff_location
23
+ elif my_ffmpeg:
24
+ program = "ffmpeg"
25
+ else:
54
26
  try:
55
- _version = get_stdout([self.path, "-version"]).split("\n")[0]
56
- self.version = _version.replace("ffmpeg version", "").strip().split(" ")[0]
57
- except FileNotFoundError:
58
- Log().error("ffmpeg must be installed and on PATH.")
27
+ import ae_ffmpeg
59
28
 
60
- def print(self, message: str) -> None:
61
- if self.debug:
62
- sys.stderr.write(f"FFmpeg: {message}\n")
29
+ program = ae_ffmpeg.get_path()
30
+ except ImportError:
31
+ program = "ffmpeg"
63
32
 
64
- def print_cmd(self, cmd: list[str]) -> None:
65
- if self.show_cmd:
66
- sys.stderr.write(f"{' '.join(cmd)}\n\n")
33
+ path: str | None = which(program)
34
+ if path is None:
35
+ log.error("Did not find ffmpeg on PATH.")
36
+
37
+ return FFmpeg(log, path, show_cmd, debug)
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class FFmpeg:
42
+ log: Log
43
+ path: str
44
+ show_cmd: bool
45
+ debug: bool
67
46
 
68
47
  def run(self, cmd: list[str]) -> None:
69
48
  cmd = [self.path, "-hide_banner", "-y"] + cmd
70
49
  if not self.debug:
71
50
  cmd.extend(["-nostats", "-loglevel", "error"])
72
- self.print_cmd(cmd)
73
- subprocess.run(cmd)
51
+ if self.show_cmd:
52
+ sys.stderr.write(f"{' '.join(cmd)}\n\n")
53
+ run(cmd)
74
54
 
75
55
  def run_check_errors(
76
- self,
77
- cmd: list[str],
78
- log: Log,
79
- show_out: bool = False,
80
- path: str | None = None,
56
+ self, cmd: list[str], show_out: bool = False, path: str | None = None
81
57
  ) -> None:
82
58
  process = self.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
83
59
  _, stderr = process.communicate()
@@ -104,27 +80,39 @@ class FFmpeg:
104
80
 
105
81
  for item in error_list:
106
82
  if check := search(item, output):
107
- log.error(check.group())
83
+ self.log.error(check.group())
108
84
 
109
85
  if path is not None and not os.path.isfile(path):
110
- log.error(f"The file {path} was not created.")
111
- elif show_out and not self.debug:
86
+ self.log.error(f"The file {path} was not created.")
87
+ if show_out and not self.debug:
112
88
  print(f"stderr: {output}")
113
89
 
114
90
  def Popen(
115
91
  self, cmd: list[str], stdin: Any = None, stdout: Any = PIPE, stderr: Any = None
116
92
  ) -> Popen:
117
- cmd = [self.path] + cmd
118
- self.print_cmd(cmd)
119
- return Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr)
93
+ if self.show_cmd:
94
+ sys.stderr.write(f"{self.path} {' '.join(cmd)}\n\n")
95
+ return Popen([self.path] + cmd, stdin=stdin, stdout=stdout, stderr=stderr)
96
+
97
+
98
+ def mux(input: Path, output: Path, stream: int) -> None:
99
+ input_container = av.open(input, "r")
100
+ output_container = av.open(output, "w")
101
+
102
+ input_audio_stream = input_container.streams.audio[stream]
103
+ output_audio_stream = output_container.add_stream("pcm_s16le")
104
+
105
+ for frame in input_container.decode(input_audio_stream):
106
+ packet = output_audio_stream.encode(frame)
107
+ if packet:
108
+ output_container.mux(packet)
120
109
 
121
- def pipe(self, cmd: list[str]) -> str:
122
- cmd = [self.path, "-y"] + cmd
110
+ packet = output_audio_stream.encode(None)
111
+ if packet:
112
+ output_container.mux(packet)
123
113
 
124
- self.print_cmd(cmd)
125
- output = get_stdout(cmd)
126
- self.print(output)
127
- return output
114
+ output_container.close()
115
+ input_container.close()
128
116
 
129
117
 
130
118
  @dataclass(slots=True, frozen=True)
@@ -269,7 +257,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
269
257
 
270
258
  desc = cont.metadata.get("description", None)
271
259
  bitrate = 0 if cont.bit_rate is None else cont.bit_rate
272
- dur = 0 if cont.duration is None else cont.duration / 1_000_000
260
+ dur = 0 if cont.duration is None else cont.duration / av.time_base
273
261
 
274
262
  cont.close()
275
263
 
@@ -3,17 +3,15 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, cast
4
4
  from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
5
5
 
6
- from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
7
-
8
- from .utils import make_tracks_dir
9
-
10
6
  if TYPE_CHECKING:
11
7
  from collections.abc import Sequence
12
8
  from fractions import Fraction
13
9
 
10
+ from auto_editor.ffwrapper import FileInfo
14
11
  from auto_editor.timeline import TlAudio, TlVideo, v3
15
12
  from auto_editor.utils.log import Log
16
13
 
14
+
17
15
  """
18
16
  Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later.
19
17
 
@@ -54,7 +52,7 @@ def make_name(src: FileInfo, tb: Fraction) -> str:
54
52
 
55
53
 
56
54
  def fcp11_write_xml(
57
- group_name: str, ffmpeg: FFmpeg, output: str, resolve: bool, tl: v3, log: Log
55
+ group_name: str, version: int, output: str, resolve: bool, tl: v3, log: Log
58
56
  ) -> None:
59
57
  def fraction(val: int) -> str:
60
58
  if val == 0:
@@ -68,23 +66,17 @@ def fcp11_write_xml(
68
66
  src_dur = int(src.duration * tl.tb)
69
67
  tl_dur = src_dur if resolve else tl.out_len()
70
68
 
71
- all_srcs: list[FileInfo] = [src]
72
- all_refs: list[str] = ["r2"]
73
- if resolve and len(src.audios) > 1:
74
- fold = make_tracks_dir(src)
75
-
76
- for i in range(1, len(src.audios)):
77
- newtrack = fold / f"{i}.wav"
78
- ffmpeg.run(
79
- ["-i", f"{src.path.resolve()}", "-map", f"0:a:{i}", f"{newtrack}"]
80
- )
81
- all_srcs.append(initFileInfo(f"{newtrack}", log))
82
- all_refs.append(f"r{(i + 1) * 2}")
69
+ if version == 11:
70
+ ver_str = "1.11"
71
+ elif version == 10:
72
+ ver_str = "1.10"
73
+ else:
74
+ log.error(f"Unknown final cut pro version: {version}")
83
75
 
84
- fcpxml = Element("fcpxml", version="1.10" if resolve else "1.11")
76
+ fcpxml = Element("fcpxml", version=ver_str)
85
77
  resources = SubElement(fcpxml, "resources")
86
78
 
87
- for i, one_src in enumerate(all_srcs):
79
+ for i, one_src in enumerate(tl.unique_sources()):
88
80
  SubElement(
89
81
  resources,
90
82
  "format",
@@ -126,13 +118,6 @@ def fcp11_write_xml(
126
118
  )
127
119
  spine = SubElement(sequence, "spine")
128
120
 
129
- if tl.v and tl.v[0]:
130
- clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
131
- elif tl.a and tl.a[0]:
132
- clips = tl.a[0]
133
- else:
134
- clips = []
135
-
136
121
  def make_clip(ref: str, clip: TlVideo | TlAudio) -> None:
137
122
  clip_properties = {
138
123
  "name": proj_name,
@@ -157,7 +142,19 @@ def fcp11_write_xml(
157
142
  interp="smooth2",
158
143
  )
159
144
 
160
- for my_ref in all_refs:
145
+ if tl.v and tl.v[0]:
146
+ clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
147
+ elif tl.a and tl.a[0]:
148
+ clips = tl.a[0]
149
+ else:
150
+ clips = []
151
+
152
+ all_refs: list[str] = ["r2"]
153
+ if resolve:
154
+ for i in range(1, len(tl.a)):
155
+ all_refs.append(f"r{(i + 1) * 2}")
156
+
157
+ for my_ref in reversed(all_refs):
161
158
  for clip in clips:
162
159
  make_clip(my_ref, clip)
163
160
 
@@ -4,9 +4,6 @@ from typing import TYPE_CHECKING
4
4
  from xml.etree.ElementTree import Element
5
5
 
6
6
  if TYPE_CHECKING:
7
- from pathlib import Path
8
-
9
- from auto_editor.ffwrapper import FileInfo
10
7
  from auto_editor.utils.log import Log
11
8
 
12
9
 
@@ -19,21 +16,6 @@ def show(ele: Element, limit: int, depth: int = 0) -> None:
19
16
  show(child, limit, depth + 1)
20
17
 
21
18
 
22
- def make_tracks_dir(src: FileInfo) -> Path:
23
- from os import mkdir
24
- from shutil import rmtree
25
-
26
- fold = src.path.parent / f"{src.path.stem}_tracks"
27
-
28
- try:
29
- mkdir(fold)
30
- except OSError:
31
- rmtree(fold)
32
- mkdir(fold)
33
-
34
- return fold
35
-
36
-
37
19
  class Validator:
38
20
  def __init__(self, log: Log):
39
21
  self.log = log