Skip to content

Commit 210f663

Browse files
committed
[common] Add operators for Timecode
1 parent ad9d35d commit 210f663

File tree

2 files changed

+130
-2
lines changed

2 files changed

+130
-2
lines changed

scenedetect/common.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,81 @@ class Timecode:
144144
def seconds(self) -> float:
145145
return float(self.time_base * self.pts)
146146

147+
def _get_other_as_seconds(self, other: ty.Any) -> float:
148+
if isinstance(other, Timecode):
149+
return other.seconds
150+
if isinstance(other, float):
151+
return float(other)
152+
raise TypeError(f"Unsupported type for comparison with Timecode: {type(other)}")
153+
154+
def __add__(self, other: ty.Union[float, "Timecode"]) -> "Timecode":
155+
if isinstance(other, Timecode):
156+
if self.time_base != other.time_base:
157+
raise ValueError("Timecode instances require equal time_base for arithmetic.")
158+
return Timecode(self.pts + other.pts, self.time_base)
159+
if isinstance(other, float):
160+
# Assume other is in seconds. Convert to pts.
161+
pts_offset = round(other / self.time_base)
162+
return Timecode(self.pts + pts_offset, self.time_base)
163+
return NotImplemented
164+
165+
def __sub__(self, other: ty.Union[float, "Timecode"]) -> "Timecode":
166+
if isinstance(other, Timecode):
167+
if self.time_base != other.time_base:
168+
raise ValueError("Timecode instances require equal time_base for arithmetic.")
169+
return Timecode(self.pts - other.pts, self.time_base)
170+
if isinstance(other, float):
171+
# Assume other is in seconds. Convert to pts.
172+
pts_offset = round(other / self.time_base)
173+
return Timecode(self.pts - pts_offset, self.time_base)
174+
return NotImplemented
175+
176+
def __eq__(self, other: ty.Any) -> bool:
177+
try:
178+
return math.isclose(self.seconds, self._get_other_as_seconds(other))
179+
except TypeError:
180+
return NotImplemented
181+
182+
def __ne__(self, other: ty.Any) -> bool:
183+
eq_result = self.__eq__(other)
184+
return not eq_result if eq_result is not NotImplemented else NotImplemented
185+
186+
def __lt__(self, other: ty.Any) -> bool:
187+
try:
188+
other_seconds = self._get_other_as_seconds(other)
189+
if math.isclose(self.seconds, other_seconds):
190+
return False
191+
return self.seconds < other_seconds
192+
except TypeError:
193+
return NotImplemented
194+
195+
def __le__(self, other: ty.Any) -> bool:
196+
try:
197+
other_seconds = self._get_other_as_seconds(other)
198+
if math.isclose(self.seconds, other_seconds):
199+
return True
200+
return self.seconds < other_seconds
201+
except TypeError:
202+
return NotImplemented
203+
204+
def __gt__(self, other: ty.Any) -> bool:
205+
try:
206+
other_seconds = self._get_other_as_seconds(other)
207+
if math.isclose(self.seconds, other_seconds):
208+
return False
209+
return self.seconds > other_seconds
210+
except TypeError:
211+
return NotImplemented
212+
213+
def __ge__(self, other: ty.Any) -> bool:
214+
try:
215+
other_seconds = self._get_other_as_seconds(other)
216+
if math.isclose(self.seconds, other_seconds):
217+
return True
218+
return self.seconds > other_seconds
219+
except TypeError:
220+
return NotImplemented
221+
147222

148223
class FrameTimecode:
149224
"""Object for frame-based timecodes, using the video framerate to compute back and

tests/test_timecode.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
"""
2222

2323
# Third-Party Library Imports
24+
# Standard Library Imports
25+
from fractions import Fraction
26+
2427
import pytest
2528

26-
# Standard Library Imports
27-
from scenedetect.common import MAX_FPS_DELTA, FrameTimecode
29+
from scenedetect.common import MAX_FPS_DELTA, FrameTimecode, Timecode
2830

2931

3032
def test_framerate():
@@ -300,3 +302,54 @@ def test_precision():
300302
assert FrameTimecode(990, fps).get_timecode(precision=1, use_rounding=False) == "00:00:00.9"
301303
assert FrameTimecode(990, fps).get_timecode(precision=0, use_rounding=True) == "00:00:01"
302304
assert FrameTimecode(990, fps).get_timecode(precision=0, use_rounding=False) == "00:00:00"
305+
306+
307+
def test_timecode_operators():
308+
"""Test Timecode operators."""
309+
time_base = Fraction(1, 1000) # 1ms time base
310+
t1 = Timecode(pts=1000, time_base=time_base) # 1 second
311+
312+
# Addition
313+
t2 = t1 + 0.5
314+
assert t2.pts == 1500
315+
assert t2.seconds == 1.5
316+
t3 = t1 + Timecode(pts=500, time_base=time_base)
317+
assert t3.pts == 1500
318+
assert t3.seconds == 1.5
319+
320+
# Subtraction
321+
t4 = t1 - 0.5
322+
assert t4.pts == 500
323+
assert t4.seconds == 0.5
324+
t5 = t1 - Timecode(pts=200, time_base=time_base)
325+
assert t5.pts == 800
326+
assert t5.seconds == 0.8
327+
328+
# Different time_base for arithmetic should fail
329+
with pytest.raises(ValueError):
330+
t1 + Timecode(pts=1, time_base=Fraction(1, 1))
331+
with pytest.raises(ValueError):
332+
t1 - Timecode(pts=1, time_base=Fraction(1, 1))
333+
334+
# Comparisons
335+
assert t1 == 1.0
336+
assert t1 == Timecode(pts=1000, time_base=time_base)
337+
assert t1 != 2.0
338+
assert t1 < 2.0
339+
assert t1 <= 1.0
340+
assert t1 > 0.5
341+
assert t1 >= 1.0
342+
343+
t_other_tb = Timecode(pts=1, time_base=Fraction(1, 1)) # 1 second, different time base
344+
assert t1 == t_other_tb
345+
assert t1 <= t_other_tb
346+
assert t1 >= t_other_tb
347+
348+
t_other_tb_2 = Timecode(pts=2, time_base=Fraction(1, 1)) # 2 seconds
349+
assert t1 < t_other_tb_2
350+
assert t1 != t_other_tb_2
351+
352+
# Test with different types
353+
assert t1 != "a"
354+
with pytest.raises(TypeError):
355+
_ = t1 < "a"

0 commit comments

Comments
 (0)