parse-avidemux-project 0.1.0__tar.gz

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,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,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,70 @@
1
+ # parse-avidemux-project
2
+
3
+ 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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install parse-avidemux-project
9
+ ```
10
+
11
+ ## CLI Usage
12
+
13
+ ```bash
14
+ parse-avidemux-project input.py -o output.json
15
+ ```
16
+
17
+ Omitting `-o` prints the JSON to stdout.
18
+
19
+ ## Python API
20
+
21
+ ```python
22
+ from parse_avidemux_project import (
23
+ AvidemuxProject,
24
+ Segment,
25
+ AudioTrack,
26
+ parse_project,
27
+ parse_project_file,
28
+ us_to_iso_time,
29
+ )
30
+
31
+ # Parse a file
32
+ project: AvidemuxProject = parse_project_file("project.py")
33
+
34
+ # Or parse a string
35
+ project = parse_project('''
36
+ adm = Avidemux()
37
+ if not adm.loadVideo("/path/to/video.mkv"):
38
+ raise
39
+ adm.addSegment(0, 1000000, 5000000)
40
+ adm.markerA = 0
41
+ adm.markerB = 6000000
42
+ ''')
43
+
44
+ print(project.video_file) # /path/to/video.mkv
45
+ print(project.segments) # [Segment(start=1000000, duration=5000000)]
46
+ print(project.segments[0].end) # 6000000 (computed property)
47
+
48
+ # Convert to dict for JSON serialization
49
+ print(project.to_dict())
50
+
51
+ # Microsecond to ISO time conversion
52
+ print(us_to_iso_time(3661000000)) # 01:01:01.000
53
+ ```
54
+
55
+ ### Data model
56
+
57
+ | Attribute | Type | Description |
58
+ |---|---|---|
59
+ | `video_file` | `str \| None` | Path to the source video |
60
+ | `segments` | `list[Segment]` | List of cut segments (each has `start`, `duration`, and `end` property) |
61
+ | `marker_a`, `marker_b` | `int \| None` | A/B markers in microseconds |
62
+ | `video_codec` | `str \| None` | Output video codec |
63
+ | `container` | `str \| None` | Output container format |
64
+ | `container_options` | `dict[str, str]` | Container options |
65
+ | `audio_tracks` | `list[AudioTrack]` | Audio track configs (each has `index`, `language`, `codec`) |
66
+ | `hdr_config` | `tuple[int, ...]` | HDR parameters |
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "parse-avidemux-project"
7
+ version = "0.1.0"
8
+ description = "Parser for Avidemux TinyPY project scripts"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = []
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/ferrous-lux/parse_avidemux_project"
16
+ Source = "https://github.com/ferrous-lux/parse_avidemux_project"
17
+ BugTracker = "https://github.com/ferrous-lux/parse_avidemux_project/issues"
18
+
19
+ [project.scripts]
20
+ parse-avidemux-project = "parse_avidemux_project.cli:main"
21
+
22
+ [project.optional-dependencies]
23
+ dev = ["pytest>=8"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
27
+
28
+ [tool.ruff]
29
+ exclude = ["tests/fixtures"]
30
+
31
+ [tool.mypy]
32
+ exclude = ["tests/fixtures/"]
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/parse_avidemux_project/__init__.py
5
+ src/parse_avidemux_project/cli.py
6
+ src/parse_avidemux_project/models.py
7
+ src/parse_avidemux_project/parser.py
8
+ src/parse_avidemux_project.egg-info/PKG-INFO
9
+ src/parse_avidemux_project.egg-info/SOURCES.txt
10
+ src/parse_avidemux_project.egg-info/dependency_links.txt
11
+ src/parse_avidemux_project.egg-info/entry_points.txt
12
+ src/parse_avidemux_project.egg-info/requires.txt
13
+ src/parse_avidemux_project.egg-info/top_level.txt
14
+ tests/test_parser.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ parse-avidemux-project = parse_avidemux_project.cli:main
@@ -0,0 +1,231 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from parse_avidemux_project import (
6
+ AudioTrack,
7
+ AvidemuxProject,
8
+ Segment,
9
+ parse_project,
10
+ parse_project_file,
11
+ us_to_iso_time,
12
+ )
13
+
14
+ FIXTURES = Path(__file__).parent / "fixtures"
15
+
16
+
17
+ def test_parse_project_file_adm1():
18
+ project = parse_project_file(str(FIXTURES / "test-adm.py"))
19
+ assert project.video_file == "/tmp/test.mkv"
20
+ assert len(project.segments) == 5
21
+ assert project.marker_a == 0
22
+ assert project.marker_b == 2597779000
23
+ assert project.video_codec == "Copy"
24
+ assert project.container == "MKV"
25
+ assert len(project.container_options) == 8
26
+ assert project.container_options["forceAspectRatio"] == "False"
27
+ assert project.container_options["displayWidth"] == "1280"
28
+ assert project.container_options["colMatrixCoeff"] == "2"
29
+ assert len(project.audio_tracks) == 2
30
+ assert project.audio_tracks[0] == AudioTrack(index=0, language="eng", codec="copy")
31
+ assert project.audio_tracks[1] == AudioTrack(index=1, language="spa", codec="copy")
32
+ assert project.hdr_config == (1, 1, 1, 1, 0)
33
+
34
+
35
+ def test_parse_project_file_adm2():
36
+ project = parse_project_file(str(FIXTURES / "test2-adm.py"))
37
+ assert project.video_file == "/tmp/test-video2.mkv"
38
+ assert len(project.segments) == 10
39
+ assert project.marker_a == 0
40
+ assert project.marker_b == 4905816000
41
+ assert project.video_codec == "Copy"
42
+ assert project.container == "MKV"
43
+ assert len(project.container_options) == 8
44
+ assert len(project.audio_tracks) == 2
45
+ assert project.audio_tracks[0] == AudioTrack(index=0, language="eng", codec="copy")
46
+ assert project.audio_tracks[1] == AudioTrack(index=1, language="spa", codec="copy")
47
+ assert project.hdr_config == (1, 1, 1, 1, 0)
48
+
49
+
50
+ def test_segment_end():
51
+ seg = Segment(start=1000000, duration=5000000)
52
+ assert seg.end == 6000000
53
+ assert seg.start == 1000000
54
+ assert seg.duration == 5000000
55
+
56
+
57
+ def test_segment_zero_duration():
58
+ seg = Segment(start=5000, duration=0)
59
+ assert seg.end == 5000
60
+
61
+
62
+ def test_parse_project_invalid_content():
63
+ with pytest.raises(ValueError, match="No Avidemux project data found"):
64
+ parse_project("not an avidemux file")
65
+
66
+
67
+ def test_parse_project_empty_string():
68
+ with pytest.raises(ValueError, match="No Avidemux project data found"):
69
+ parse_project("")
70
+
71
+
72
+ def test_parse_project_just_segments():
73
+ project = parse_project("adm.addSegment(0, 1000, 2000)")
74
+ assert project.video_file is None
75
+ assert len(project.segments) == 1
76
+ assert project.segments[0] == Segment(start=1000, duration=2000)
77
+
78
+
79
+ def test_parse_project_just_video():
80
+ content = (
81
+ "adm = Avidemux()\n"
82
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
83
+ " raise\n"
84
+ )
85
+ project = parse_project(content)
86
+ assert project.video_file == "/path/to/video.mkv"
87
+ assert len(project.segments) == 0
88
+
89
+
90
+ def test_parse_project_no_segments():
91
+ content = (
92
+ "adm = Avidemux()\n"
93
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
94
+ " raise\n"
95
+ "adm.markerA = 0\n"
96
+ )
97
+ project = parse_project(content)
98
+ assert project.video_file == "/path/to/video.mkv"
99
+ assert len(project.segments) == 0
100
+ assert project.marker_a == 0
101
+
102
+
103
+ def test_parse_project_no_video():
104
+ project = parse_project("adm.addSegment(0, 1000, 2000)\nadm.addSegment(0, 5000, 3000)")
105
+ assert project.video_file is None
106
+ assert len(project.segments) == 2
107
+
108
+
109
+ def test_parse_project_no_hdr():
110
+ content = (
111
+ "adm = Avidemux()\n"
112
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
113
+ " raise\n"
114
+ )
115
+ project = parse_project(content)
116
+ assert project.hdr_config == ()
117
+
118
+
119
+ def test_parse_project_no_container():
120
+ content = (
121
+ "adm = Avidemux()\n"
122
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
123
+ " raise\n"
124
+ )
125
+ project = parse_project(content)
126
+ assert project.container is None
127
+ assert project.container_options == {}
128
+
129
+
130
+ def test_parse_project_no_audio():
131
+ content = (
132
+ "adm = Avidemux()\n"
133
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
134
+ " raise\n"
135
+ )
136
+ project = parse_project(content)
137
+ assert project.audio_tracks == []
138
+
139
+
140
+ def test_parse_project_no_codec():
141
+ content = (
142
+ "adm = Avidemux()\n"
143
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
144
+ " raise\n"
145
+ )
146
+ project = parse_project(content)
147
+ assert project.video_codec is None
148
+
149
+
150
+ def test_parse_project_no_markers():
151
+ content = (
152
+ "adm = Avidemux()\n"
153
+ 'if not adm.loadVideo("/path/to/video.mkv"):\n'
154
+ " raise\n"
155
+ )
156
+ project = parse_project(content)
157
+ assert project.marker_a is None
158
+ assert project.marker_b is None
159
+
160
+
161
+ def test_us_to_iso_time_zero():
162
+ assert us_to_iso_time(0) == "00:00:00.000"
163
+
164
+
165
+ def test_us_to_iso_time_one_second():
166
+ assert us_to_iso_time(1_000_000) == "00:00:01.000"
167
+
168
+
169
+ def test_us_to_iso_time_one_minute():
170
+ assert us_to_iso_time(60_000_000) == "00:01:00.000"
171
+
172
+
173
+ def test_us_to_iso_time_one_hour():
174
+ assert us_to_iso_time(3_600_000_000) == "01:00:00.000"
175
+
176
+
177
+ def test_us_to_iso_time_milliseconds():
178
+ assert us_to_iso_time(1_500_000) == "00:00:01.500"
179
+
180
+
181
+ def test_us_to_iso_time_complex():
182
+ assert us_to_iso_time(9_999_999_999) == "02:46:39.999"
183
+
184
+
185
+ def test_to_dict_shape():
186
+ project = AvidemuxProject(
187
+ video_file="/path/to/video.mkv",
188
+ segments=[Segment(start=1000, duration=2000)],
189
+ marker_a=0,
190
+ marker_b=5000,
191
+ video_codec="Copy",
192
+ container="MKV",
193
+ container_options={"forceAspectRatio": "False"},
194
+ audio_tracks=[AudioTrack(index=0, language="eng", codec="copy")],
195
+ hdr_config=(1, 1, 1, 1, 0),
196
+ )
197
+ d = project.to_dict()
198
+ assert d["video_file"] == "/path/to/video.mkv"
199
+ assert d["segments"] == [{"start": 1000, "duration": 2000, "end": 3000}]
200
+ assert d["marker_a"] == 0
201
+ assert d["marker_b"] == 5000
202
+ assert d["video_codec"] == "Copy"
203
+ assert d["container"] == "MKV"
204
+ assert d["container_options"] == {"forceAspectRatio": "False"}
205
+ assert d["audio_tracks"] == [{"index": 0, "language": "eng", "codec": "copy"}]
206
+ assert d["hdr_config"] == [1, 1, 1, 1, 0]
207
+
208
+
209
+ def test_to_dict_empty_project():
210
+ d = AvidemuxProject().to_dict()
211
+ assert d["video_file"] is None
212
+ assert d["segments"] == []
213
+ assert d["marker_a"] is None
214
+ assert d["marker_b"] is None
215
+ assert d["video_codec"] is None
216
+ assert d["container"] is None
217
+ assert d["container_options"] == {}
218
+ assert d["audio_tracks"] == []
219
+ assert d["hdr_config"] == []
220
+
221
+
222
+ def test_audio_track_equality():
223
+ t1 = AudioTrack(index=0, language="eng", codec="copy")
224
+ t2 = AudioTrack(index=0, language="eng", codec="copy")
225
+ assert t1 == t2
226
+
227
+
228
+ def test_segment_equality():
229
+ s1 = Segment(start=1000, duration=2000)
230
+ s2 = Segment(start=1000, duration=2000)
231
+ assert s1 == s2