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.
- parse_avidemux_project/__init__.py +11 -0
- parse_avidemux_project/cli.py +38 -0
- parse_avidemux_project/models.py +44 -0
- parse_avidemux_project/parser.py +163 -0
- parse_avidemux_project-0.1.0.dist-info/METADATA +85 -0
- parse_avidemux_project-0.1.0.dist-info/RECORD +10 -0
- parse_avidemux_project-0.1.0.dist-info/WHEEL +5 -0
- parse_avidemux_project-0.1.0.dist-info/entry_points.txt +2 -0
- parse_avidemux_project-0.1.0.dist-info/licenses/LICENSE +21 -0
- parse_avidemux_project-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|