|
|
|
import re
|
|
|
|
from typing import Optional, Sequence, NamedTuple
|
|
|
|
|
|
|
|
from .common import IntOrFloat
|
|
|
|
|
|
|
|
#: Pattern that matches both SubStation and SubRip timestamps.
|
|
|
|
TIMESTAMP = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})[.,](\d{1,3})")
|
|
|
|
|
|
|
|
#: Pattern that matches H:MM:SS or HH:MM:SS timestamps.
|
|
|
|
TIMESTAMP_SHORT = re.compile(r"(\d{1,2}):(\d{2}):(\d{2})")
|
|
|
|
|
|
|
|
|
|
|
|
class Times(NamedTuple):
|
|
|
|
"""Named tuple (h, m, s, ms) of ints."""
|
|
|
|
h: int
|
|
|
|
m: int
|
|
|
|
s: int
|
|
|
|
ms: int
|
|
|
|
|
|
|
|
|
|
|
|
def make_time(h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0,
|
|
|
|
frames: Optional[int] = None, fps: Optional[float] = None) -> int:
|
|
|
|
"""
|
|
|
|
Convert time to milliseconds.
|
|
|
|
|
|
|
|
See :func:`pysubs2.time.times_to_ms()`. When both frames and fps are specified,
|
|
|
|
:func:`pysubs2.time.frames_to_ms()` is called instead.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
ValueError: Invalid fps, or one of frames/fps is missing.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
>>> make_time(s=1.5)
|
|
|
|
1500
|
|
|
|
>>> make_time(frames=50, fps=25)
|
|
|
|
2000
|
|
|
|
|
|
|
|
"""
|
|
|
|
if frames is None and fps is None:
|
|
|
|
return times_to_ms(h, m, s, ms)
|
|
|
|
elif frames is not None and fps is not None:
|
|
|
|
return frames_to_ms(frames, fps)
|
|
|
|
else:
|
|
|
|
raise ValueError("Both fps and frames must be specified")
|
|
|
|
|
|
|
|
|
|
|
|
def timestamp_to_ms(groups: Sequence[str]) -> int:
|
|
|
|
"""
|
|
|
|
Convert groups from :data:`pysubs2.time.TIMESTAMP` or :data:`pysubs2.time.TIMESTAMP_SHORT`
|
|
|
|
match to milliseconds.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
>>> timestamp_to_ms(TIMESTAMP.match("0:00:00.42").groups())
|
|
|
|
420
|
|
|
|
>>> timestamp_to_ms(TIMESTAMP_SHORT.match("0:00:01").groups())
|
|
|
|
1000
|
|
|
|
|
|
|
|
"""
|
|
|
|
h: int
|
|
|
|
m: int
|
|
|
|
s: int
|
|
|
|
ms: int
|
|
|
|
frac: int
|
|
|
|
if len(groups) == 4:
|
|
|
|
h, m, s, frac = map(int, groups)
|
|
|
|
ms = frac * 10**(3 - len(groups[-1]))
|
|
|
|
elif len(groups) == 3:
|
|
|
|
h, m, s = map(int, groups)
|
|
|
|
ms = 0
|
|
|
|
else:
|
|
|
|
raise ValueError("Unexpected number of groups")
|
|
|
|
|
|
|
|
ms += s * 1000
|
|
|
|
ms += m * 60000
|
|
|
|
ms += h * 3600000
|
|
|
|
return ms
|
|
|
|
|
|
|
|
|
|
|
|
def times_to_ms(h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0) -> int:
|
|
|
|
"""
|
|
|
|
Convert hours, minutes, seconds to milliseconds.
|
|
|
|
|
|
|
|
Arguments may be positive or negative, int or float,
|
|
|
|
need not be normalized (``s=120`` is okay).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Number of milliseconds (rounded to int).
|
|
|
|
|
|
|
|
"""
|
|
|
|
ms += s * 1000
|
|
|
|
ms += m * 60000
|
|
|
|
ms += h * 3600000
|
|
|
|
return int(round(ms))
|
|
|
|
|
|
|
|
|
|
|
|
def frames_to_ms(frames: int, fps: float) -> int:
|
|
|
|
"""
|
|
|
|
Convert frame-based duration to milliseconds.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
frames: Number of frames (should be int).
|
|
|
|
fps: Framerate (must be a positive number, eg. 23.976).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Number of milliseconds (rounded to int).
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
ValueError: fps was negative or zero.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if fps <= 0:
|
|
|
|
raise ValueError(f"Framerate must be a positive number ({fps}).")
|
|
|
|
|
|
|
|
return int(round(frames * (1000 / fps)))
|
|
|
|
|
|
|
|
|
|
|
|
def ms_to_frames(ms: IntOrFloat, fps: float) -> int:
|
|
|
|
"""
|
|
|
|
Convert milliseconds to number of frames.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
ms: Number of milliseconds (may be int, float or other numeric class).
|
|
|
|
fps: Framerate (must be a positive number, eg. 23.976).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Number of frames (int).
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
ValueError: fps was negative or zero.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if fps <= 0:
|
|
|
|
raise ValueError(f"Framerate must be a positive number ({fps}).")
|
|
|
|
|
|
|
|
return int(round((ms / 1000) * fps))
|
|
|
|
|
|
|
|
|
|
|
|
def ms_to_times(ms: IntOrFloat) -> Times:
|
|
|
|
"""
|
|
|
|
Convert milliseconds to normalized tuple (h, m, s, ms).
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
ms: Number of milliseconds (may be int, float or other numeric class).
|
|
|
|
Should be non-negative.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Named tuple (h, m, s, ms) of ints.
|
|
|
|
Invariants: ``ms in range(1000) and s in range(60) and m in range(60)``
|
|
|
|
|
|
|
|
"""
|
|
|
|
ms = int(round(ms))
|
|
|
|
h, ms = divmod(ms, 3600000)
|
|
|
|
m, ms = divmod(ms, 60000)
|
|
|
|
s, ms = divmod(ms, 1000)
|
|
|
|
return Times(h, m, s, ms)
|
|
|
|
|
|
|
|
|
|
|
|
def ms_to_str(ms: IntOrFloat, fractions: bool = False) -> str:
|
|
|
|
"""
|
|
|
|
Prettyprint milliseconds to [-]H:MM:SS[.mmm]
|
|
|
|
|
|
|
|
Handles huge and/or negative times. Non-negative times with ``fractions=True``
|
|
|
|
are matched by :data:`pysubs2.time.TIMESTAMP`.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
ms: Number of milliseconds (int, float or other numeric class).
|
|
|
|
fractions: Whether to print up to millisecond precision.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str
|
|
|
|
|
|
|
|
"""
|
|
|
|
sgn = "-" if ms < 0 else ""
|
|
|
|
h, m, s, ms = ms_to_times(abs(ms))
|
|
|
|
if fractions:
|
|
|
|
return f"{sgn}{h:01d}:{m:02d}:{s:02d}.{ms:03d}"
|
|
|
|
else:
|
|
|
|
return f"{sgn}{h:01d}:{m:02d}:{s:02d}"
|