Skip to content

Commit 17fb0b4

Browse files
authored
Fix NDVR recording timestamp parsing crashing on numeric epoch values (#93)
The recording detail API returns startTime/endTime as epoch seconds (numbers), but _process_ndvr_state assumed ISO-8601 strings and called .replace() on them, causing an AttributeError. This left duration, show_title, media_type and image unset for recordings. Add _parse_timestamp helper that handles both numeric epochs and ISO-8601 strings with safe fallback.
1 parent b02b5ca commit 17fb0b4

2 files changed

Lines changed: 48 additions & 12 deletions

File tree

lghorizon/lghorizon_device_state_processor.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -324,18 +324,14 @@ async def _process_ndvr_state(
324324
player_state.last_speed_change_time
325325
)
326326
device_state.position = int(player_state.relative_position / 1000)
327-
if recording.start_time:
328-
device_state.start_time = int(
329-
dt.fromisoformat(
330-
recording.start_time.replace("Z", "+00:00")
331-
).timestamp()
332-
)
333-
if recording.end_time:
334-
device_state.end_time = int(
335-
dt.fromisoformat(recording.end_time.replace("Z", "+00:00")).timestamp()
336-
)
337-
if recording.start_time and recording.end_time:
338-
device_state.duration = device_state.end_time - device_state.start_time
327+
parsed_start = self._parse_timestamp(recording.start_time)
328+
parsed_end = self._parse_timestamp(recording.end_time)
329+
if parsed_start is not None:
330+
device_state.start_time = parsed_start
331+
if parsed_end is not None:
332+
device_state.end_time = parsed_end
333+
if parsed_start is not None and parsed_end is not None:
334+
device_state.duration = parsed_end - parsed_start
339335
if recording.source == LGHorizonRecordingSource.SHOW:
340336
device_state.show_title = recording.title
341337
else:
@@ -345,6 +341,17 @@ async def _process_ndvr_state(
345341

346342
device_state.image = await self._get_intent_image_url(recording.id)
347343

344+
def _parse_timestamp(self, value) -> Optional[int]:
345+
"""Parse a timestamp that may be numeric epoch seconds or an ISO-8601 string."""
346+
if value is None:
347+
return None
348+
try:
349+
if isinstance(value, (int, float)):
350+
return int(value)
351+
return int(dt.fromisoformat(value.replace("Z", "+00:00")).timestamp())
352+
except Exception:
353+
return None
354+
348355
async def _get_intent_image_url(self, intent_id: str) -> Optional[str]:
349356
"""Get intent image url."""
350357
service_config = await self._auth.get_service_config()

tests/test_models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,3 +1937,32 @@ async def tracking_request(*args, **kwargs):
19371937
assert fetch_idx < last_request_idx, (
19381938
f"fetch_access_token (idx {fetch_idx}) should come before retry request (idx {last_request_idx})"
19391939
)
1940+
1941+
1942+
class TestNdvrTimestampParsing:
1943+
"""Tests for LGHorizonDeviceStateProcessor._parse_timestamp helper."""
1944+
1945+
def _make_processor(self):
1946+
from lghorizon.lghorizon_device_state_processor import LGHorizonDeviceStateProcessor
1947+
return LGHorizonDeviceStateProcessor(None, {}, None, None)
1948+
1949+
def test_numeric_epoch_seconds(self):
1950+
proc = self._make_processor()
1951+
assert proc._parse_timestamp(1714060800) == 1714060800
1952+
1953+
def test_numeric_float(self):
1954+
proc = self._make_processor()
1955+
assert proc._parse_timestamp(1714060800.9) == 1714060800
1956+
1957+
def test_iso8601_string(self):
1958+
proc = self._make_processor()
1959+
result = proc._parse_timestamp("2024-04-25T20:00:00Z")
1960+
assert result == 1714075200
1961+
1962+
def test_none_returns_none(self):
1963+
proc = self._make_processor()
1964+
assert proc._parse_timestamp(None) is None
1965+
1966+
def test_invalid_string_returns_none(self):
1967+
proc = self._make_processor()
1968+
assert proc._parse_timestamp("not-a-date") is None

0 commit comments

Comments
 (0)