typed-ffmpeg-compatible 3.2.1__py3-none-any.whl → 3.2.3__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.
typed_ffmpeg/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '3.2.1'
21
- __version_tuple__ = version_tuple = (3, 2, 1)
20
+ __version__ = version = '3.2.3'
21
+ __version_tuple__ = version_tuple = (3, 2, 3)
@@ -47,11 +47,23 @@ from .validate import validate
47
47
 
48
48
 
49
49
  def get_options_dict() -> dict[str, FFMpegOption]:
50
+ """
51
+ Load and index FFmpeg options from the cache.
52
+
53
+ Returns:
54
+ Dictionary mapping option names to their FFMpegOption definitions
55
+ """
50
56
  options = load(list[FFMpegOption], "options")
51
57
  return {option.name: option for option in options}
52
58
 
53
59
 
54
60
  def get_filter_dict() -> dict[str, FFMpegFilter]:
61
+ """
62
+ Load and index FFmpeg filters from the cache.
63
+
64
+ Returns:
65
+ Dictionary mapping filter names to their FFMpegFilter definitions
66
+ """
55
67
  filters = load(list[FFMpegFilter], "filters")
56
68
  return {filter.name: filter for filter in filters}
57
69
 
@@ -99,6 +111,24 @@ def parse_options(tokens: list[str]) -> dict[str, list[str | None | bool]]:
99
111
  def parse_stream_selector(
100
112
  selector: str, mapping: Mapping[str, FilterableStream]
101
113
  ) -> FilterableStream:
114
+ """
115
+ Parse a stream selector string and return the corresponding stream.
116
+
117
+ This function handles FFmpeg's stream selector syntax:
118
+ - Simple selectors: "[0]" (first input)
119
+ - Type selectors: "[0:v]" (first input, video stream)
120
+ - Index selectors: "[0:v:0]" (first input, first video stream)
121
+
122
+ Args:
123
+ selector: Stream selector string to parse
124
+ mapping: Dictionary of available streams
125
+
126
+ Returns:
127
+ The selected FilterableStream
128
+
129
+ Raises:
130
+ AssertionError: If the stream label is not found in the mapping
131
+ """
102
132
  selector = selector.strip("[]")
103
133
 
104
134
  if ":" in selector:
@@ -126,19 +156,48 @@ def parse_stream_selector(
126
156
  return stream
127
157
 
128
158
 
159
+ def _is_filename(token: str) -> bool:
160
+ """
161
+ Check if a token is a filename.
162
+
163
+ Args:
164
+ token: The token to check
165
+
166
+ Returns:
167
+ True if the token is a filename, False otherwise
168
+ """
169
+ # not start with - and has ext
170
+ return not token.startswith("-") and len(token.split(".")) > 1
171
+
172
+
129
173
  def parse_output(
130
174
  source: list[str],
131
175
  in_streams: Mapping[str, FilterableStream],
132
176
  ffmpeg_options: dict[str, FFMpegOption],
133
177
  ) -> list[OutputStream]:
178
+ """
179
+ Parse output file specifications and their options.
180
+
181
+ This function processes the output portion of an FFmpeg command line,
182
+ handling output file paths, stream mapping, and output-specific options.
183
+
184
+ Args:
185
+ source: List of command-line tokens for output specifications
186
+ in_streams: Dictionary of available input streams
187
+ ffmpeg_options: Dictionary of valid FFmpeg options
188
+
189
+ Returns:
190
+ List of OutputStream objects representing the output specifications
191
+ """
134
192
  tokens = source.copy()
135
193
 
136
194
  export: list[OutputStream] = []
137
-
138
195
  buffer: list[str] = []
196
+
139
197
  while tokens:
140
198
  token = tokens.pop(0)
141
- if token.startswith("-") or len(buffer) % 2 == 1:
199
+
200
+ if not _is_filename(token):
142
201
  buffer.append(token)
143
202
  continue
144
203
 
@@ -161,8 +220,21 @@ def parse_output(
161
220
  if isinstance(in_streams[k], AVStream)
162
221
  ]
163
222
 
164
- assert inputs, f"No inputs found for output {filename}"
165
- export.append(output(*inputs, filename=filename, extra_options=options))
223
+ parameters: dict[str, str | bool] = {}
224
+
225
+ for key, value in options.items():
226
+ key_base = key.split(":")[0]
227
+ if key_base in ffmpeg_options:
228
+ option = ffmpeg_options[key_base]
229
+
230
+ if option.is_output_option:
231
+ # just ignore not input options
232
+ if value[-1] is None:
233
+ parameters[key] = True
234
+ else:
235
+ parameters[key] = value[-1]
236
+
237
+ export.append(output(*inputs, filename=filename, extra_options=parameters))
166
238
  buffer = []
167
239
 
168
240
  return export
@@ -171,6 +243,19 @@ def parse_output(
171
243
  def parse_input(
172
244
  tokens: list[str], ffmpeg_options: dict[str, FFMpegOption]
173
245
  ) -> dict[str, FilterableStream]:
246
+ """
247
+ Parse input file specifications and their options.
248
+
249
+ This function processes the input portion of an FFmpeg command line,
250
+ handling input file paths and input-specific options.
251
+
252
+ Args:
253
+ tokens: List of command-line tokens for input specifications
254
+ ffmpeg_options: Dictionary of valid FFmpeg options
255
+
256
+ Returns:
257
+ Dictionary mapping input indices to their FilterableStream objects
258
+ """
174
259
  output: list[AVStream] = []
175
260
 
176
261
  while "-i" in tokens:
@@ -184,15 +269,15 @@ def parse_input(
184
269
  parameters: dict[str, str | bool] = {}
185
270
 
186
271
  for key, value in options.items():
187
- assert key in ffmpeg_options, f"Unknown option: {key}"
188
- option = ffmpeg_options[key]
272
+ if key in ffmpeg_options:
273
+ option = ffmpeg_options[key]
189
274
 
190
- if option.is_input_option:
191
- # just ignore not input options
192
- if value[-1] is None:
193
- parameters[key] = True
194
- else:
195
- parameters[key] = value[-1]
275
+ if option.is_input_option:
276
+ # just ignore not input options
277
+ if value[-1] is None:
278
+ parameters[key] = True
279
+ else:
280
+ parameters[key] = value[-1]
196
281
 
197
282
  output.append(input(filename=filename, extra_options=parameters))
198
283
 
@@ -317,46 +402,53 @@ def parse_global(
317
402
  For tokens like ['-y', '-loglevel', 'quiet', '-i', 'input.mp4']:
318
403
  Returns ({'y': True, 'loglevel': 'quiet'}, ['-i', 'input.mp4'])
319
404
  """
320
- global_params: dict[str, str | bool] = {}
321
- remaining_tokens = tokens.copy()
322
-
323
- # Process tokens until we hit an input file marker (-i)
324
- while remaining_tokens and remaining_tokens[0] != "-i":
325
- if remaining_tokens[0].startswith("-"):
326
- option_name = remaining_tokens[0][1:]
327
- assert option_name in ffmpeg_options, (
328
- f"Unknown global option: {option_name}"
329
- )
330
- option = ffmpeg_options[option_name]
405
+ options = parse_options(tokens[: tokens.index("-i")])
406
+ remaining_tokens = tokens[tokens.index("-i") :]
407
+ parameters: dict[str, str | bool] = {}
331
408
 
332
- if not option.is_global_option:
333
- continue
409
+ for key, value in options.items():
410
+ if key in ffmpeg_options:
411
+ option = ffmpeg_options[key]
334
412
 
335
- if len(remaining_tokens) > 1 and not remaining_tokens[1].startswith("-"):
336
- # Option with value
337
- global_params[option_name] = remaining_tokens[1]
338
- remaining_tokens = remaining_tokens[2:]
339
- else:
340
- # Boolean option
341
- if option_name.startswith("no"):
342
- global_params[option_name[2:]] = False
413
+ if option.is_global_option:
414
+ # Process only recognized global options
415
+ if value[-1] is None:
416
+ parameters[key] = True
343
417
  else:
344
- global_params[option_name] = True
345
- remaining_tokens = remaining_tokens[1:]
346
- else:
347
- # Skip non-option tokens
348
- remaining_tokens = remaining_tokens[1:]
349
-
350
- return global_params, remaining_tokens
418
+ parameters[key] = value[-1]
419
+ return parameters, remaining_tokens
351
420
 
352
421
 
353
422
  def parse(cli: str) -> Stream:
423
+ """
424
+ Parse a complete FFmpeg command line into a Stream object.
425
+
426
+ This function takes a full FFmpeg command line string and converts it into
427
+ a Stream object representing the filter graph. It handles all components:
428
+ - Global options
429
+ - Input files and their options
430
+ - Filter complex
431
+ - Output files and their options
432
+
433
+ Args:
434
+ cli: Complete FFmpeg command line string
435
+
436
+ Returns:
437
+ Stream object representing the parsed command line
438
+
439
+ Example:
440
+ ```python
441
+ stream = parse(
442
+ "ffmpeg -i input.mp4 -filter_complex '[0:v]scale=1280:720[v]' -map '[v]' output.mp4"
443
+ )
444
+ ```
445
+ """
354
446
  # ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
355
447
  ffmpeg_options = get_options_dict()
356
448
  ffmpeg_filters = get_filter_dict()
357
449
 
358
450
  tokens = shlex.split(cli)
359
- assert tokens[0] == "ffmpeg"
451
+ assert tokens[0].lower().split(".")[0] == "ffmpeg"
360
452
  tokens = tokens[1:]
361
453
 
362
454
  # Parse global options first
@@ -72,7 +72,6 @@ def _parse_obj_from_dict(data: Any, cls: type[T]) -> T | None:
72
72
  Returns:
73
73
  The parsed dataclass instance
74
74
  """
75
-
76
75
  if data is None:
77
76
  return None
78
77
 
@@ -104,6 +103,7 @@ def _parse_obj_from_dict(data: Any, cls: type[T]) -> T | None:
104
103
  continue
105
104
  item_type = tuple_args[0]
106
105
  value = data.get(field_name, [])
106
+ # NOTE: if the value is a single item, convert it to a list (xml's issue)
107
107
  if not isinstance(value, list):
108
108
  value = [value]
109
109
  kwargs[field_name] = tuple(
@@ -26,15 +26,15 @@ class packetsType:
26
26
 
27
27
  @dataclass(kw_only=True, frozen=True)
28
28
  class framesType:
29
- frame: Optional["frameType"] = None
30
- subtitle: Optional["subtitleType"] = None
29
+ frame: tuple["frameType", ...] | None = None
30
+ subtitle: tuple["subtitleType", ...] | None = None
31
31
 
32
32
 
33
33
  @dataclass(kw_only=True, frozen=True)
34
34
  class packetsAndFramesType:
35
- packet: Optional["packetType"] = None
36
- frame: Optional["frameType"] = None
37
- subtitle: Optional["subtitleType"] = None
35
+ packet: tuple["packetType", ...] | None = None
36
+ frame: tuple["frameType", ...] | None = None
37
+ subtitle: tuple["subtitleType", ...] | None = None
38
38
 
39
39
 
40
40
  @dataclass(kw_only=True, frozen=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typed-ffmpeg-compatible
3
- Version: 3.2.1
3
+ Version: 3.2.3
4
4
  Summary: Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation.
5
5
  Author-email: lucemia <lucemia@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  typed_ffmpeg/__init__.py,sha256=n1SZfENc9xhZ0eA3ZzkBNPuIY-Pt3-rOwB8-uUj5olU,1592
2
- typed_ffmpeg/_version.py,sha256=XRi79FWoZnvkU9YpUCVq-_6vnTUrxKmzOwOlvrJYqm4,511
2
+ typed_ffmpeg/_version.py,sha256=P9n86yqDQj4uxDqvcIKGlJWsA9YZ7G1BSuEXNqVACeI,511
3
3
  typed_ffmpeg/base.py,sha256=C5Tqbx2I0c-09D7aXKZoGkspu-lAAeAhuOns5zr3PXQ,6304
4
4
  typed_ffmpeg/exceptions.py,sha256=D4SID6WOwkjVV8O8mAjrEDHWn-8BRDnK_jteaDof1SY,2474
5
5
  typed_ffmpeg/filters.py,sha256=_lBpGZgPHK3KgGVrw-TCdQEsBRuEXVIgwggYNGd80MU,110076
@@ -13,7 +13,7 @@ typed_ffmpeg/common/cache.py,sha256=j0JvfX7jewLpdJWxgo7Pwze0BkUJdYGHX2uGR8BZ-9M,
13
13
  typed_ffmpeg/common/schema.py,sha256=qM8yfMX9UU3EAQSNsTrr-SAmyqKx8eQCXTtu3RJWkEk,19673
14
14
  typed_ffmpeg/common/serialize.py,sha256=dLim0DBP5CdJ1JiMV9xEmmh1XMSIhBOWs61EopAL15s,7719
15
15
  typed_ffmpeg/compile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- typed_ffmpeg/compile/compile_cli.py,sha256=wzLW0_JYS6vth-8YGfkzVaGJg1er-_-ekX-X0KxDC_I,29042
16
+ typed_ffmpeg/compile/compile_cli.py,sha256=--2j1QPPOB2pEQzqFM4VpINokbJilcegtRqTRF2_Jfg,31588
17
17
  typed_ffmpeg/compile/compile_json.py,sha256=YCiTyfAnUVSbFr7BiQpmJYs13K5sa-xo77Iih33mb6I,992
18
18
  typed_ffmpeg/compile/compile_python.py,sha256=oo4e8Ldwk0OkrZtHucfuGR5JDFF8xY8omNKPMDyUpQ8,11506
19
19
  typed_ffmpeg/compile/context.py,sha256=macQ3HhEJ73j_WbWYtU9GCQCzcB_KQGAPimcuU-WOac,10946
@@ -31,9 +31,9 @@ typed_ffmpeg/dag/io/_input.py,sha256=KRLTSQPEfmgPcPEAJdeWRHZhNsClaJCB9Ac6czMOrmE
31
31
  typed_ffmpeg/dag/io/_output.py,sha256=_no6ffAOABznbLNTki8CYr7pvr4Sa0LweRfn38-cszs,12470
32
32
  typed_ffmpeg/dag/io/output_args.py,sha256=SThIhZh9PXs2m6Fz5JsSy8oS-Te7GM_oz7HRuZo0-eI,13901
33
33
  typed_ffmpeg/ffprobe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- typed_ffmpeg/ffprobe/parse.py,sha256=9Lp7VgFOjAlyPOgtyW1iomRrt-O8Hbsm71gqtolpPjs,3706
34
+ typed_ffmpeg/ffprobe/parse.py,sha256=fq1cGFkUpUdvyVi3S-TAQ7qn-tXUdae6ADhymZt7-t4,3791
35
35
  typed_ffmpeg/ffprobe/probe.py,sha256=D_opsKp3OgT2HSBzhxgY1b9SIqXpDhz3xh_k9HUGux0,10367
36
- typed_ffmpeg/ffprobe/schema.py,sha256=QmFN3ZxGvQmE7gRWbRUmgznPMe9HZ28nGc5srxwwUhY,13586
36
+ typed_ffmpeg/ffprobe/schema.py,sha256=PBgBWYGO8wKnI0Lae9oCJ1Nprhv2ciPkdLrumzPVllY,13631
37
37
  typed_ffmpeg/ffprobe/xml2json.py,sha256=1TSuxR7SYc-M_-JmE-1khHGbXCapgW0Oap9kzL0nwNg,2455
38
38
  typed_ffmpeg/streams/__init__.py,sha256=Nt9uWpcVI1sQLl5Qt_kBCBcWOGZA1vczCQ0qvFbSko0,141
39
39
  typed_ffmpeg/streams/audio.py,sha256=2oNRhb5UetWtlPG3NW73AjUZoFPl303yMv-6W1sGWt0,259054
@@ -50,8 +50,9 @@ typed_ffmpeg/utils/view.py,sha256=QCSlQoQkRBI-T0sWjiywGgM9DlKd8Te3CB2ZYX-pEVU,34
50
50
  typed_ffmpeg/utils/lazy_eval/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  typed_ffmpeg/utils/lazy_eval/operator.py,sha256=QWybd-UH3VdDa8kgWkqAMi3WV0b0WF1d1JixQr6is2E,4136
52
52
  typed_ffmpeg/utils/lazy_eval/schema.py,sha256=WSg-E3MS3itN1AT6Dq4Z9btnRHEReuN3o6zruXou7h4,9623
53
- typed_ffmpeg_compatible-3.2.1.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
54
- typed_ffmpeg_compatible-3.2.1.dist-info/METADATA,sha256=bqpqg08AUkgGm22Htg9TrOjhUrvOJncpspmOCmIzgMk,8385
55
- typed_ffmpeg_compatible-3.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
- typed_ffmpeg_compatible-3.2.1.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
57
- typed_ffmpeg_compatible-3.2.1.dist-info/RECORD,,
53
+ typed_ffmpeg_compatible-3.2.3.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
54
+ typed_ffmpeg_compatible-3.2.3.dist-info/METADATA,sha256=HwRj91ZONWRkUUACimtXw8TNwhZveMe358-cYoBYaUU,8385
55
+ typed_ffmpeg_compatible-3.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ typed_ffmpeg_compatible-3.2.3.dist-info/entry_points.txt,sha256=kUQvZ27paV-07qtkIFV-emKsYtjFOTw9kknBRSXPs04,45
57
+ typed_ffmpeg_compatible-3.2.3.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
58
+ typed_ffmpeg_compatible-3.2.3.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scripts = scripts.main:app