parse-avidemux-project 0.1.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.
@@ -0,0 +1,11 @@
1
+ from .models import AudioTrack, AvidemuxProject, Segment
2
+ from .parser import parse_project, parse_project_file, us_to_iso_time
3
+
4
+ __all__ = [
5
+ "AudioTrack",
6
+ "AvidemuxProject",
7
+ "Segment",
8
+ "parse_project",
9
+ "parse_project_file",
10
+ "us_to_iso_time",
11
+ ]
@@ -0,0 +1,38 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+
5
+ from . import parse_project_file
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(
10
+ description="Parse an Avidemux TinyPY project file to JSON."
11
+ )
12
+ parser.add_argument("input", help="Path to the Avidemux project file (.py)")
13
+ parser.add_argument(
14
+ "-o", "--output",
15
+ help="Path to write JSON output (default: stdout)",
16
+ )
17
+ args = parser.parse_args()
18
+
19
+ try:
20
+ project = parse_project_file(args.input)
21
+ except FileNotFoundError as e:
22
+ print(f"Error: {e}", file=sys.stderr)
23
+ sys.exit(1)
24
+ except ValueError as e:
25
+ print(f"Parse error: {e}", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+ output = json.dumps(project.to_dict(), indent=4)
29
+
30
+ if args.output:
31
+ with open(args.output, "w", encoding="utf-8") as f:
32
+ f.write(output)
33
+ else:
34
+ print(output)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,44 @@
1
+ from dataclasses import dataclass, field, asdict
2
+
3
+
4
+ @dataclass
5
+ class Segment:
6
+ start: int
7
+ duration: int
8
+
9
+ @property
10
+ def end(self) -> int:
11
+ return self.start + self.duration
12
+
13
+
14
+ @dataclass
15
+ class AudioTrack:
16
+ index: int
17
+ language: str | None = None
18
+ codec: str | None = None
19
+
20
+
21
+ @dataclass
22
+ class AvidemuxProject:
23
+ video_file: str | None = None
24
+ segments: list[Segment] = field(default_factory=list)
25
+ marker_a: int | None = None
26
+ marker_b: int | None = None
27
+ video_codec: str | None = None
28
+ container: str | None = None
29
+ container_options: dict[str, str] = field(default_factory=dict)
30
+ audio_tracks: list[AudioTrack] = field(default_factory=list)
31
+ hdr_config: tuple[int, ...] = field(default_factory=tuple)
32
+
33
+ def to_dict(self) -> dict:
34
+ return {
35
+ "video_file": self.video_file,
36
+ "segments": [asdict(s) | {"end": s.end} for s in self.segments],
37
+ "marker_a": self.marker_a,
38
+ "marker_b": self.marker_b,
39
+ "video_codec": self.video_codec,
40
+ "container": self.container,
41
+ "container_options": self.container_options,
42
+ "audio_tracks": [asdict(t) for t in self.audio_tracks],
43
+ "hdr_config": list(self.hdr_config),
44
+ }
@@ -0,0 +1,163 @@
1
+ import re
2
+
3
+ from .models import AudioTrack, AvidemuxProject, Segment
4
+
5
+
6
+ def parse_project(content: str) -> AvidemuxProject:
7
+ """
8
+ Parse an Avidemux TinyPY script into a structured AvidemuxProject object.
9
+
10
+ Parameters:
11
+ content (str): The full text of an Avidemux TinyPY project file.
12
+
13
+ Returns:
14
+ AvidemuxProject: Structured representation of the project.
15
+
16
+ Raises:
17
+ ValueError: If the content cannot be parsed as a valid Avidemux project.
18
+ """
19
+ video_file = _parse_video_file(content)
20
+ segments = _parse_segments(content)
21
+
22
+ if video_file is None and not segments:
23
+ raise ValueError("No Avidemux project data found in content")
24
+
25
+ return AvidemuxProject(
26
+ video_file=video_file,
27
+ segments=segments,
28
+ marker_a=_parse_marker(content, "A"),
29
+ marker_b=_parse_marker(content, "B"),
30
+ video_codec=_parse_video_codec(content),
31
+ container=_parse_container(content),
32
+ container_options=_parse_container_options(content),
33
+ audio_tracks=_parse_audio_tracks(content),
34
+ hdr_config=_parse_hdr_config(content),
35
+ )
36
+
37
+
38
+ def parse_project_file(path: str) -> AvidemuxProject:
39
+ """
40
+ Read and parse an Avidemux TinyPY project file.
41
+
42
+ Parameters:
43
+ path (str): Filesystem path to a TinyPY project file.
44
+
45
+ Returns:
46
+ AvidemuxProject: Structured representation of the project.
47
+ """
48
+ with open(path, encoding="utf-8") as f:
49
+ return parse_project(f.read())
50
+
51
+
52
+ def us_to_iso_time(us: int) -> str:
53
+ """
54
+ Convert microseconds to the format HH:MM:SS.mmm.
55
+
56
+ Parameters:
57
+ us (int): Microseconds to convert.
58
+
59
+ Returns:
60
+ str: Formatted time string.
61
+ """
62
+ ms = int(us) // 1000
63
+ hours = ms // 3600000
64
+ minutes = (ms % 3600000) // 60000
65
+ seconds = (ms % 60000) // 1000
66
+ milliseconds = ms % 1000
67
+ return f"{hours:02}:{minutes:02}:{seconds:02}.{milliseconds:03}"
68
+
69
+
70
+ _PATTERN_VIDEO = re.compile(
71
+ r"adm = Avidemux\(\)\s*\n"
72
+ r'if not adm\.loadVideo\("(.+?)"\):\s*raise'
73
+ )
74
+
75
+ _PATTERN_SEGMENT = re.compile(r"adm\.addSegment\(0, (\d+), (\d+)\)")
76
+
77
+ _PATTERN_MARKER = re.compile(r"adm\.marker([AB])\s*=\s*(\d+)")
78
+
79
+ _PATTERN_VIDEO_CODEC = re.compile(r'adm\.videoCodec\("(.+?)"\)')
80
+
81
+ _PATTERN_CONTAINER = re.compile(r"adm\.setContainer\(([^)]+)\)")
82
+
83
+ _PATTERN_HDR_CONFIG = re.compile(r"adm\.setHDRConfig\(([^)]+)\)")
84
+
85
+ _PATTERN_SOURCE_LANG = re.compile(r'adm\.setSourceTrackLanguage\((\d+),"(.+?)"\)')
86
+
87
+ _PATTERN_AUDIO_ADD = re.compile(r"adm\.audioAddTrack\((\d+)\)")
88
+
89
+ _PATTERN_AUDIO_CODEC = re.compile(r'adm\.audioCodec\((\d+), "(.+?)"\)')
90
+
91
+
92
+ def _parse_video_file(content: str) -> str | None:
93
+ match = _PATTERN_VIDEO.search(content)
94
+ return match.group(1) if match else None
95
+
96
+
97
+ def _parse_segments(content: str) -> list[Segment]:
98
+ matches = _PATTERN_SEGMENT.findall(content)
99
+ return [Segment(start=int(start), duration=int(duration)) for start, duration in matches]
100
+
101
+
102
+ def _parse_marker(content: str, marker: str) -> int | None:
103
+ for m, val in _PATTERN_MARKER.findall(content):
104
+ if m == marker:
105
+ return int(val)
106
+ return None
107
+
108
+
109
+ def _parse_video_codec(content: str) -> str | None:
110
+ match = _PATTERN_VIDEO_CODEC.search(content)
111
+ return match.group(1) if match else None
112
+
113
+
114
+ def _parse_container(content: str) -> str | None:
115
+ match = _PATTERN_CONTAINER.search(content)
116
+ if not match:
117
+ return None
118
+ parts = re.findall(r'"([^"]*)"', match.group(1))
119
+ return parts[0] if parts else None
120
+
121
+
122
+ def _parse_container_options(content: str) -> dict[str, str]:
123
+ match = _PATTERN_CONTAINER.search(content)
124
+ if not match:
125
+ return {}
126
+ parts = re.findall(r'"([^"]*)"', match.group(1))
127
+ options = {}
128
+ for part in parts[1:]:
129
+ if "=" in part:
130
+ key, value = part.split("=", 1)
131
+ options[key] = value
132
+ return options
133
+
134
+
135
+ def _parse_hdr_config(content: str) -> tuple[int, ...]:
136
+ match = _PATTERN_HDR_CONFIG.search(content)
137
+ if not match:
138
+ return ()
139
+ return tuple(int(x.strip()) for x in match.group(1).split(","))
140
+
141
+
142
+ def _parse_audio_tracks(content: str) -> list[AudioTrack]:
143
+ source_languages = {
144
+ int(index): lang
145
+ for index, lang in _PATTERN_SOURCE_LANG.findall(content)
146
+ }
147
+ codecs = {
148
+ int(index): codec
149
+ for index, codec in _PATTERN_AUDIO_CODEC.findall(content)
150
+ }
151
+ track_indices = sorted({
152
+ int(index)
153
+ for index in _PATTERN_AUDIO_ADD.findall(content)
154
+ }.union(codecs.keys()))
155
+
156
+ tracks = []
157
+ for idx in track_indices:
158
+ tracks.append(AudioTrack(
159
+ index=idx,
160
+ language=source_languages.get(idx),
161
+ codec=codecs.get(idx),
162
+ ))
163
+ return tracks
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: parse-avidemux-project
3
+ Version: 0.1.0
4
+ Summary: Parser for Avidemux TinyPY project scripts
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/ferrous-lux/parse_avidemux_project
7
+ Project-URL: Source, https://github.com/ferrous-lux/parse_avidemux_project
8
+ Project-URL: BugTracker, https://github.com/ferrous-lux/parse_avidemux_project/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # parse-avidemux-project
17
+
18
+ Standalone Python library for parsing [Avidemux](https://avidemux.sourceforge.net/) TinyPY project scripts into structured data (video path, segments, filters, audio tracks, etc.). No ffmpeg, ccextractor, or Avidemux CLI required.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install parse-avidemux-project
24
+ ```
25
+
26
+ ## CLI Usage
27
+
28
+ ```bash
29
+ parse-avidemux-project input.py -o output.json
30
+ ```
31
+
32
+ Omitting `-o` prints the JSON to stdout.
33
+
34
+ ## Python API
35
+
36
+ ```python
37
+ from parse_avidemux_project import (
38
+ AvidemuxProject,
39
+ Segment,
40
+ AudioTrack,
41
+ parse_project,
42
+ parse_project_file,
43
+ us_to_iso_time,
44
+ )
45
+
46
+ # Parse a file
47
+ project: AvidemuxProject = parse_project_file("project.py")
48
+
49
+ # Or parse a string
50
+ project = parse_project('''
51
+ adm = Avidemux()
52
+ if not adm.loadVideo("/path/to/video.mkv"):
53
+ raise
54
+ adm.addSegment(0, 1000000, 5000000)
55
+ adm.markerA = 0
56
+ adm.markerB = 6000000
57
+ ''')
58
+
59
+ print(project.video_file) # /path/to/video.mkv
60
+ print(project.segments) # [Segment(start=1000000, duration=5000000)]
61
+ print(project.segments[0].end) # 6000000 (computed property)
62
+
63
+ # Convert to dict for JSON serialization
64
+ print(project.to_dict())
65
+
66
+ # Microsecond to ISO time conversion
67
+ print(us_to_iso_time(3661000000)) # 01:01:01.000
68
+ ```
69
+
70
+ ### Data model
71
+
72
+ | Attribute | Type | Description |
73
+ |---|---|---|
74
+ | `video_file` | `str \| None` | Path to the source video |
75
+ | `segments` | `list[Segment]` | List of cut segments (each has `start`, `duration`, and `end` property) |
76
+ | `marker_a`, `marker_b` | `int \| None` | A/B markers in microseconds |
77
+ | `video_codec` | `str \| None` | Output video codec |
78
+ | `container` | `str \| None` | Output container format |
79
+ | `container_options` | `dict[str, str]` | Container options |
80
+ | `audio_tracks` | `list[AudioTrack]` | Audio track configs (each has `index`, `language`, `codec`) |
81
+ | `hdr_config` | `tuple[int, ...]` | HDR parameters |
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,10 @@
1
+ parse_avidemux_project/__init__.py,sha256=HjRpiN_FU_1LWKVCfTBlR9R9qDjl6FyNO8yJfMGBAU8,267
2
+ parse_avidemux_project/cli.py,sha256=EeoL5OuIjkscFJI_HW6aWHmVlGXWL3FHer68M3TYPZM,924
3
+ parse_avidemux_project/models.py,sha256=MvEVIUjuXDNu5eoxF0C1eX3m6tUVGz0E_CQWoEbJ1B4,1273
4
+ parse_avidemux_project/parser.py,sha256=e65eRAO-UOJKGd9S49OY50b-Al17mWw_HFhJNWG6v18,4678
5
+ parse_avidemux_project-0.1.0.dist-info/licenses/LICENSE,sha256=6kDns8-hV9Ad7UFJpzgNy7trIwxIXnigztgbaN2jVAo,1069
6
+ parse_avidemux_project-0.1.0.dist-info/METADATA,sha256=-OOha-ZRFA_LxSVuiPuNYnDOBTd0oT0W_--e-WSVxpw,2472
7
+ parse_avidemux_project-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ parse_avidemux_project-0.1.0.dist-info/entry_points.txt,sha256=v8MHhnCteas_G6ttVEWcI8o4pdARaRkNAe_R8twcbgs,75
9
+ parse_avidemux_project-0.1.0.dist-info/top_level.txt,sha256=zl4Hhp2LHojYm-TYykTSDgMQzTRUjHth7_wj3uz3Wx0,23
10
+ parse_avidemux_project-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ parse-avidemux-project = parse_avidemux_project.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ferris Luxor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ parse_avidemux_project