VideoTimestamps 0.0.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.
- VideoTimestamps-0.0.1.dist-info/LICENSE +21 -0
- VideoTimestamps-0.0.1.dist-info/METADATA +48 -0
- VideoTimestamps-0.0.1.dist-info/RECORD +22 -0
- VideoTimestamps-0.0.1.dist-info/WHEEL +5 -0
- VideoTimestamps-0.0.1.dist-info/top_level.txt +1 -0
- video_timestamps/__init__.py +8 -0
- video_timestamps/_version.py +1 -0
- video_timestamps/abc_video_parser.py +32 -0
- video_timestamps/ffprobe/__init__.py +0 -0
- video_timestamps/ffprobe/ffprobe.py +140 -0
- video_timestamps/ffprobe/timestamps.py +29 -0
- video_timestamps/mkvtoolnix/__init__.py +0 -0
- video_timestamps/mkvtoolnix/exit_code.py +11 -0
- video_timestamps/mkvtoolnix/mkvextract.py +73 -0
- video_timestamps/mkvtoolnix/mkvmerge.py +79 -0
- video_timestamps/mkvtoolnix/timestamps.py +52 -0
- video_timestamps/mkvtoolnix/utils.py +144 -0
- video_timestamps/rounding_method.py +22 -0
- video_timestamps/time_type.py +60 -0
- video_timestamps/timestamps.py +225 -0
- video_timestamps/timestamps_factory.py +144 -0
- video_timestamps/timestamps_file_parser.py +238 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 moi15moi
|
|
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,48 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: VideoTimestamps
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Get video timestamps.
|
|
5
|
+
Author-email: moi15moi <moi15moismokerlolilol@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/moi15moi/VideoTimestamps/
|
|
8
|
+
Project-URL: Tracker, https://github.com/moi15moi/VideoTimestamps/issues/
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: coverage >=7.0.0 ; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy >=1.0.0 ; extra == 'dev'
|
|
26
|
+
Requires-Dist: pillow >=10.0.0 ; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest >=8.0.0 ; extra == 'dev'
|
|
28
|
+
|
|
29
|
+
# VideoTimestamps
|
|
30
|
+
[](https://pypi.org/project/VideoTimestamps)
|
|
31
|
+
[](https://pypi.org/project/VideoTimestamps)
|
|
32
|
+
[](https://app.codecov.io/github/moi15moi/VideoTimestamps)
|
|
33
|
+
[](https://github.com/moi15moi/VideoTimestamps/actions?query=branch:main)
|
|
34
|
+
|
|
35
|
+
This tool allows to recover the timestamps of each frame in a video in milliseconds.
|
|
36
|
+
It also help to convert a frame to a time in milliseconds and vice-versa.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
```
|
|
40
|
+
pip install VideoTimestamps
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Dependencies
|
|
44
|
+
- [MKVToolNix v82.0 or more](https://mkvtoolnix.download/downloads.html)
|
|
45
|
+
- [ffprobe](https://ffmpeg.org/download.html)
|
|
46
|
+
|
|
47
|
+
## How to use it?
|
|
48
|
+
See the [example file](./examples/get_timestamps.py).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
video_timestamps/__init__.py,sha256=AP8zE8hBx8JJ0MSKoJDAR35la5frctq8pZhIz_XvoaI,170
|
|
2
|
+
video_timestamps/_version.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
|
|
3
|
+
video_timestamps/abc_video_parser.py,sha256=9jp4oRx-0GQw3iNOlU4X7zqVA91Ctcc0DNr--aKACkA,1110
|
|
4
|
+
video_timestamps/rounding_method.py,sha256=N9vrIuUi-zt_FOBj2B6s9rZ7cEly7crq9NoCQDJeWxc,495
|
|
5
|
+
video_timestamps/time_type.py,sha256=8g5FHK81-7BCOzfzL2US8wBkUzYUg35jHAs2zohEyI0,1739
|
|
6
|
+
video_timestamps/timestamps.py,sha256=Y8EY9Gu7G7RMfQXalpkQ3Kx_HfG8T1oKhi2Gp_eEai0,9374
|
|
7
|
+
video_timestamps/timestamps_factory.py,sha256=a4_HK3iNie2V8J8WQ-mTIh_TN1QbkAwXmeDE4-D8EFg,6264
|
|
8
|
+
video_timestamps/timestamps_file_parser.py,sha256=PwLES5rsGEpRdwcz46VI4t1Bmc22O6XwVv6uwdDUZHc,9032
|
|
9
|
+
video_timestamps/ffprobe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
video_timestamps/ffprobe/ffprobe.py,sha256=xIhR-_qTY0yyiyOYpVOP-Bf9i0y4AykuNk_iTExCQ3M,4927
|
|
11
|
+
video_timestamps/ffprobe/timestamps.py,sha256=vUitcCpoJ6dc-3rapZ3HIzg8YpQC7R3ihskGkcHduEo,995
|
|
12
|
+
video_timestamps/mkvtoolnix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
video_timestamps/mkvtoolnix/exit_code.py,sha256=J1Vwkpsbdh9TtDGrzVAAgIlqJ7dX3X4IQuKIDgVv3kM,245
|
|
14
|
+
video_timestamps/mkvtoolnix/mkvextract.py,sha256=egnmla3fkFlWl3YNZzj0dBrcUTeRFVkbOz0OBOxfrKo,2341
|
|
15
|
+
video_timestamps/mkvtoolnix/mkvmerge.py,sha256=arPuDKwPkGUlZGcfkCq-tnbhr2XlirGReSe5YvDpYCQ,2270
|
|
16
|
+
video_timestamps/mkvtoolnix/timestamps.py,sha256=aDJMunNpS_kKDymannTj3wcW5GxSgQ6Ku62dhVE99aI,2312
|
|
17
|
+
video_timestamps/mkvtoolnix/utils.py,sha256=oqr5HRqwwMCRZi0EsUrFazJOCIBALx9Tx9ZyIykaLS8,4571
|
|
18
|
+
VideoTimestamps-0.0.1.dist-info/LICENSE,sha256=u7kVQN3qvSSfxekKWXoKKqxNqpwWYQI6CL_400ISUoU,1065
|
|
19
|
+
VideoTimestamps-0.0.1.dist-info/METADATA,sha256=rt7LUsFUxgO3MSFxGEMx4c6kYQSwm6AfERCX07E8Ets,2112
|
|
20
|
+
VideoTimestamps-0.0.1.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
|
21
|
+
VideoTimestamps-0.0.1.dist-info/top_level.txt,sha256=3tv2UWakWMhmArLdqFycgcuDlcqQbaS4w3RE25yPbdM,17
|
|
22
|
+
VideoTimestamps-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
video_timestamps
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from .rounding_method import RoundingMethod
|
|
2
|
+
from .timestamps import Timestamps
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ABCVideoParser(ABC):
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def is_available() -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Returns:
|
|
15
|
+
If the video parser is available.
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def get_timestamps(video_path: Path, index: int, normalize: bool, rounding_method: RoundingMethod) -> tuple[Timestamps, Fraction]:
|
|
22
|
+
"""Create timestamps based on the ``video_path`` provided.
|
|
23
|
+
|
|
24
|
+
Parameters:
|
|
25
|
+
video_path (Path): A video path.
|
|
26
|
+
index (int): Index of the video stream.
|
|
27
|
+
normalize (bool): If True, it will shift the timestamps to make them start from 0. If false, the option does nothing.
|
|
28
|
+
rounding_method (RoundingMethod): A rounding method. See the comment in Timestamps class about FLOOR vs ROUND.
|
|
29
|
+
Returns:
|
|
30
|
+
A tuple containing a Timestamps instance and the time_base of the video.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from ..rounding_method import RoundingMethod
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
from subprocess import CompletedProcess, run
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from shutil import which
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
__all__ = ["FFprobe"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FFprobe:
|
|
14
|
+
"""
|
|
15
|
+
This class is a collection of static methods that will help
|
|
16
|
+
the user to interact with FFprobe.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
PROGRAM_NAME = "ffprobe"
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def is_ffprobe_installed() -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Checks if the `ffprobe` program is installed and available in the system's PATH.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
True if `ffprobe` is installed, False otherwise.
|
|
28
|
+
"""
|
|
29
|
+
return which("ffprobe") is not None
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def verify_if_ffprobe_is_installed() -> None:
|
|
33
|
+
"""
|
|
34
|
+
Verifies if the `mkvextract` program is installed. Raises an error if not found.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
Exception: If `mkvextract` is not found in the system's PATH.
|
|
38
|
+
"""
|
|
39
|
+
if not FFprobe.is_ffprobe_installed():
|
|
40
|
+
raise Exception("ffprobe is not in the environment variable.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def verify_if_command_fails(cmd_output: CompletedProcess[str]) -> None:
|
|
45
|
+
"""Checks the result of a command execution and raises an error or warning based on the exit code.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
cmd_output (CompletedProcess): The result of the command execution.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
OSError: If the command reported an error.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
if cmd_output.returncode != 0:
|
|
55
|
+
raise OSError(f"{FFprobe.PROGRAM_NAME} reported an error: '{cmd_output.stderr}'.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def run_command(cmd: list[Any]) -> CompletedProcess[str]:
|
|
60
|
+
"""Runs a command and verifies if it fails.
|
|
61
|
+
|
|
62
|
+
Parameters:
|
|
63
|
+
cmd (List[Any]): The command to be run, including its arguments.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The result of the command execution.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
FFprobe.verify_if_ffprobe_is_installed()
|
|
70
|
+
output = run(cmd, capture_output=True, text=True, encoding="utf-8")
|
|
71
|
+
FFprobe.verify_if_command_fails(output)
|
|
72
|
+
|
|
73
|
+
return output
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def get_timestamps(video_path: Path, index: int, rounding_method: RoundingMethod) -> tuple[list[int], Fraction, Fraction, Fraction]:
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
video_path (Path): A video path.
|
|
82
|
+
index (int): Index of the video stream.
|
|
83
|
+
rounding_method (RoundingMethod): A rounding method. See the comment in Timestamps class about FLOOR vs ROUND.
|
|
84
|
+
Returns:
|
|
85
|
+
A tuple containing these 3 informations:
|
|
86
|
+
1. A list of each timestamps rounded to milliseconds
|
|
87
|
+
2. The time_base
|
|
88
|
+
3. The first timestamps not rounded
|
|
89
|
+
4. The last timestamps not rounded
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
cmd = [
|
|
93
|
+
FFprobe.PROGRAM_NAME,
|
|
94
|
+
"-hide_banner",
|
|
95
|
+
"-select_streams",
|
|
96
|
+
str(index),
|
|
97
|
+
"-show_entries",
|
|
98
|
+
# Technically, we should use frame=pts_time,dts_time instead of packet=pts_time,dts_time
|
|
99
|
+
# But, using frame make the execution really slow
|
|
100
|
+
# I tried to ask [here](https://ffmpeg.org/pipermail/ffmpeg-user/2024-July/058509.html) if I could use a heuristic
|
|
101
|
+
# to know when I need to switch to frame, but I never got an answer.
|
|
102
|
+
"packet=pts_time,dts_time:stream=codec_type,time_base", #todo
|
|
103
|
+
video_path,
|
|
104
|
+
"-print_format",
|
|
105
|
+
"json",
|
|
106
|
+
]
|
|
107
|
+
ffprobe_output = FFprobe.run_command(cmd)
|
|
108
|
+
|
|
109
|
+
ffprobe_output_dict = json.loads(ffprobe_output.stdout)
|
|
110
|
+
|
|
111
|
+
if len(ffprobe_output_dict["streams"]) == 0:
|
|
112
|
+
raise ValueError(f"The index {index} is not in the file {video_path}.")
|
|
113
|
+
|
|
114
|
+
if ffprobe_output_dict["streams"][0]["codec_type"] != "video":
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f'The index {index} is not a video stream. It is an "{ffprobe_output_dict["streams"][0]["codec_type"]}" stream.'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
time_base = Fraction(ffprobe_output_dict["streams"][0]["time_base"])
|
|
120
|
+
|
|
121
|
+
timestamps = []
|
|
122
|
+
lowest_timestamp: Fraction = None # type: ignore
|
|
123
|
+
highest_timestamp: Fraction = None # type: ignore
|
|
124
|
+
|
|
125
|
+
for packet in ffprobe_output_dict["packets"]:
|
|
126
|
+
timestamp = Fraction(
|
|
127
|
+
# Sometimes, pts_time isn't available.
|
|
128
|
+
# If it is the case, fallback to dts_time
|
|
129
|
+
packet.get("pts_time", packet.get("dts_time"))
|
|
130
|
+
) * Fraction(1000)
|
|
131
|
+
if highest_timestamp is None or highest_timestamp < timestamp:
|
|
132
|
+
highest_timestamp = timestamp
|
|
133
|
+
if lowest_timestamp is None or lowest_timestamp > timestamp:
|
|
134
|
+
lowest_timestamp = timestamp
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
timestamps.append(rounding_method(timestamp))
|
|
138
|
+
timestamps.sort()
|
|
139
|
+
|
|
140
|
+
return timestamps, time_base, lowest_timestamp, highest_timestamp
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .ffprobe import FFprobe
|
|
2
|
+
from ..timestamps import Timestamps
|
|
3
|
+
from ..abc_video_parser import ABCVideoParser
|
|
4
|
+
from ..rounding_method import RoundingMethod
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FFprobeTimestamps(ABCVideoParser):
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def is_available() -> bool:
|
|
13
|
+
return FFprobe.is_ffprobe_installed()
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def get_timestamps(video_path: Path, index: int, normalize: bool, rounding_method: RoundingMethod) -> tuple[Timestamps, Fraction]:
|
|
17
|
+
timestamps_list, time_base, first_frame_time, last_frame_time = FFprobe.get_timestamps(video_path, index, rounding_method)
|
|
18
|
+
|
|
19
|
+
fpms = Fraction(len(timestamps_list) - 1) / (last_frame_time - first_frame_time)
|
|
20
|
+
|
|
21
|
+
timestamps = Timestamps(
|
|
22
|
+
rounding_method=rounding_method,
|
|
23
|
+
timestamps=timestamps_list,
|
|
24
|
+
normalize=normalize,
|
|
25
|
+
fpms=fpms,
|
|
26
|
+
last_frame_time=last_frame_time,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return timestamps, time_base
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from .utils import MKVUtils
|
|
2
|
+
from tempfile import TemporaryDirectory
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
__all__ = ["MKVExtract"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MKVExtract:
|
|
9
|
+
"""
|
|
10
|
+
This class is a collection of static methods that will help
|
|
11
|
+
the user to interact with mkvextract.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
PROGRAM_NAME = "mkvextract"
|
|
15
|
+
PROGRAM_PATH = MKVUtils.get_program_path(PROGRAM_NAME)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def is_mkv_extract_installed() -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Checks if the `mkvextract` program is installed and available in the system's PATH.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if `mkvextract` is installed, False otherwise.
|
|
24
|
+
"""
|
|
25
|
+
return MKVExtract.PROGRAM_PATH is not None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def verify_if_mkvextract_installed() -> None:
|
|
30
|
+
"""
|
|
31
|
+
Verifies if the `mkvextract` program is installed. Raises an error if not found.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
Exception: If `mkvextract` is not found in the system's PATH.
|
|
35
|
+
"""
|
|
36
|
+
if not MKVExtract.is_mkv_extract_installed():
|
|
37
|
+
raise Exception(
|
|
38
|
+
f'{MKVExtract.PROGRAM_NAME} is isn\'t found. You need to correct your environnements variable or correctly install the program'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def get_timestamps_file_content(mkv_file_path: Path, index: int) -> str:
|
|
44
|
+
"""Extracts the timestamps from an MKV file for a specific track index.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
mkv_file_path (Path): The path to the MKV file.
|
|
48
|
+
index (int): Index of the video stream.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The content of the timestamps file.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
MKVExtract.verify_if_mkvextract_installed()
|
|
55
|
+
MKVUtils.verify_if_file_mkv(mkv_file_path)
|
|
56
|
+
|
|
57
|
+
with TemporaryDirectory() as tmpdirname:
|
|
58
|
+
timestamps_file_path = Path(tmpdirname, "temp timestamps.txt")
|
|
59
|
+
cmd = [
|
|
60
|
+
MKVExtract.PROGRAM_PATH,
|
|
61
|
+
mkv_file_path,
|
|
62
|
+
"timestamps_v2",
|
|
63
|
+
f"{index}:{timestamps_file_path}",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
MKVUtils.run_command(MKVExtract.PROGRAM_NAME, cmd)
|
|
67
|
+
with open(timestamps_file_path, "r", encoding="utf-8") as file:
|
|
68
|
+
lines = file.readlines()
|
|
69
|
+
|
|
70
|
+
# Ignore the last line due to this issue: https://gitlab.com/mbunkus/mkvtoolnix/-/issues/3075
|
|
71
|
+
content = "".join(lines[:-1])
|
|
72
|
+
|
|
73
|
+
return content
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from .utils import MKVUtils
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = ["MKVMerge"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MKVMerge:
|
|
10
|
+
"""
|
|
11
|
+
This class is a collection of static methods that will help
|
|
12
|
+
the user to interact with mkvmerge.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
PROGRAM_NAME = "mkvmerge"
|
|
16
|
+
PROGRAM_PATH = MKVUtils.get_program_path(PROGRAM_NAME)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def is_mkv_merge_installed() -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Checks if the `mkvmerge` program is installed and available in the system's PATH.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if `mkvmerge` is installed, False otherwise.
|
|
25
|
+
"""
|
|
26
|
+
return MKVMerge.PROGRAM_PATH is not None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def verify_if_mkvmerge_installed() -> None:
|
|
31
|
+
"""
|
|
32
|
+
Verifies if the `mkvmerge` program is installed. Raises an error if not found.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
Exception: If `mkvmerge` is not found in the system's PATH.
|
|
36
|
+
"""
|
|
37
|
+
if not MKVMerge.is_mkv_merge_installed():
|
|
38
|
+
raise Exception(f'{MKVMerge.PROGRAM_NAME} is isn\'t found. You need to correct your environnements variable or correctly install the program')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def get_version() -> tuple[int, int]:
|
|
43
|
+
"""Retrieves the version of `mkvmerge`.
|
|
44
|
+
|
|
45
|
+
Equivalent to running: `mkvmerge --version`
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The major and minor version numbers of `mkvmerge`.
|
|
49
|
+
"""
|
|
50
|
+
MKVMerge.verify_if_mkvmerge_installed()
|
|
51
|
+
|
|
52
|
+
return MKVUtils.get_version(MKVMerge.PROGRAM_NAME, MKVMerge.PROGRAM_PATH) # type: ignore
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def get_mkv_info(mkv_file_path: Path) -> Any:
|
|
57
|
+
"""Retrieves information about an MKV file in JSON format.
|
|
58
|
+
|
|
59
|
+
Equivalent to running: `mkvmerge FILE.mkv -J`
|
|
60
|
+
|
|
61
|
+
Parameters:
|
|
62
|
+
mkv_file_path (Path): The path to the MKV file.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The information about the MKV file in JSON format.
|
|
66
|
+
"""
|
|
67
|
+
MKVMerge.verify_if_mkvmerge_installed()
|
|
68
|
+
MKVUtils.verify_if_file_mkv(mkv_file_path)
|
|
69
|
+
|
|
70
|
+
cmd = [
|
|
71
|
+
MKVMerge.PROGRAM_PATH,
|
|
72
|
+
mkv_file_path,
|
|
73
|
+
"-J",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
mkvmerge_output = MKVUtils.run_command(MKVMerge.PROGRAM_NAME, cmd)
|
|
77
|
+
mkvmerge_output_dict = json.loads(mkvmerge_output.stdout)
|
|
78
|
+
|
|
79
|
+
return mkvmerge_output_dict
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from .mkvextract import MKVExtract
|
|
2
|
+
from .mkvmerge import MKVMerge
|
|
3
|
+
from ..timestamps import Timestamps
|
|
4
|
+
from ..abc_video_parser import ABCVideoParser
|
|
5
|
+
from ..rounding_method import RoundingMethod
|
|
6
|
+
from fractions import Fraction
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MKVToolNixTimestamps(ABCVideoParser):
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def is_available() -> bool:
|
|
14
|
+
return MKVExtract.is_mkv_extract_installed() and MKVMerge.is_mkv_merge_installed()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def get_timestamps(video_path: Path, index: int, normalize: bool, rounding_method: RoundingMethod) -> tuple[Timestamps, Fraction]:
|
|
19
|
+
mkvmerge_version = MKVMerge.get_version()
|
|
20
|
+
if mkvmerge_version < (82, 0):
|
|
21
|
+
# We need the version 82.0 for the timestamp_scale properties: https://help.mkvtoolnix.download/t/how-to-get-timestamp-scale-information-without-using-mkvinfo/299/3
|
|
22
|
+
raise OSError(f"You need at least the version 82.0 of mkvmerge. You have the version {mkvmerge_version[0]}.{mkvmerge_version[1]}")
|
|
23
|
+
|
|
24
|
+
mkv_info = MKVMerge.get_mkv_info(video_path)
|
|
25
|
+
|
|
26
|
+
is_index_in_video = False
|
|
27
|
+
for track in mkv_info["tracks"]:
|
|
28
|
+
if track["id"] == index:
|
|
29
|
+
if track["type"] != "video":
|
|
30
|
+
raise ValueError(f'The index {index} is not a video stream. It is an "{track["type"]}" stream.')
|
|
31
|
+
is_index_in_video = True
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
if not is_index_in_video:
|
|
35
|
+
raise ValueError(f"The index {index} is not in the file {video_path}.")
|
|
36
|
+
|
|
37
|
+
timestamp_scale = mkv_info["container"]["properties"]["timestamp_scale"]
|
|
38
|
+
# Technically, time_base = TimestampScale * TrackTimestampScale / 10^9
|
|
39
|
+
# but mkvmerge doesn't report the TrackTimestampScale.
|
|
40
|
+
# Even the MKVToolNix author suggested me to ignore the TrackTimestampScale [here](https://help.mkvtoolnix.download/t/how-to-get-timestamp-scale-information-without-using-mkvinfo/299/6?u=moi15moi)
|
|
41
|
+
time_base = Fraction(timestamp_scale, 10**9)
|
|
42
|
+
|
|
43
|
+
content = MKVExtract.get_timestamps_file_content(video_path, index)
|
|
44
|
+
|
|
45
|
+
from ..timestamps_factory import TimestampsFactory
|
|
46
|
+
timestamps = TimestampsFactory.from_timestamps_file(
|
|
47
|
+
content,
|
|
48
|
+
normalize,
|
|
49
|
+
rounding_method
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return timestamps, time_base
|