diff --git a/OTAnalytics/application/datastore.py b/OTAnalytics/application/datastore.py index 0500c3745..97ae029e8 100644 --- a/OTAnalytics/application/datastore.py +++ b/OTAnalytics/application/datastore.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from datetime import datetime, timedelta from pathlib import Path from typing import Iterable, Optional, Sequence, Tuple @@ -29,7 +28,12 @@ TrackListObserver, TrackRepository, ) -from OTAnalytics.domain.video import Video, VideoListObserver, VideoRepository +from OTAnalytics.domain.video import ( + Video, + VideoListObserver, + VideoMetadata, + VideoRepository, +) @dataclass(frozen=True) @@ -37,50 +41,6 @@ class DetectionMetadata: detection_classes: frozenset[str] -@dataclass(frozen=True) -class VideoMetadata: - path: str - recorded_start_date: datetime - expected_duration: Optional[timedelta] - recorded_fps: float - actual_fps: Optional[float] - number_of_frames: int - - @property - def start(self) -> datetime: - return self.recorded_start_date - - @property - def end(self) -> datetime: - return self.start + self.duration - - @property - def duration(self) -> timedelta: - if self.expected_duration: - return self.expected_duration - return timedelta(seconds=self.number_of_frames / self.recorded_fps) - - @property - def fps(self) -> float: - if self.actual_fps: - return self.actual_fps - return self.recorded_fps - - def to_dict(self) -> dict: - return { - "path": self.path, - "recorded_start_date": self.recorded_start_date.timestamp(), - "expected_duration": ( - self.expected_duration.total_seconds() - if self.expected_duration - else None - ), - "recorded_fps": self.recorded_fps, - "actual_fps": self.actual_fps, - "number_of_frames": self.number_of_frames, - } - - @dataclass(frozen=True) class TrackParseResult: tracks: TrackDataset @@ -104,7 +64,7 @@ def serialize( class VideoParser(ABC): @abstractmethod - def parse(self, file: Path, start_date: Optional[datetime]) -> Video: + def parse(self, file: Path, metadata: Optional[VideoMetadata]) -> Video: pass @abstractmethod @@ -179,7 +139,7 @@ class TrackVideoParser(ABC): @abstractmethod def parse( - self, file: Path, track_ids: list[TrackId] + self, file: Path, track_ids: list[TrackId], metadata: VideoMetadata ) -> Tuple[list[TrackId], list[Video]]: """ Parse the given file in ottrk format and retrieve video information from it @@ -187,6 +147,7 @@ def parse( Args: file (Path): file in ottrk format track_ids (list[TrackId]): track ids to get videos for + metadata (VideoMetadata): the video metadata Returns: Tuple[list[TrackId], list[Video]]: track ids and the corresponding videos diff --git a/OTAnalytics/application/state.py b/OTAnalytics/application/state.py index fb6598014..6e3a22c4f 100644 --- a/OTAnalytics/application/state.py +++ b/OTAnalytics/application/state.py @@ -6,7 +6,7 @@ from typing import Callable, Generic, Optional from OTAnalytics.application.config import DEFAULT_TRACK_OFFSET -from OTAnalytics.application.datastore import Datastore, VideoMetadata +from OTAnalytics.application.datastore import Datastore from OTAnalytics.application.playback import SkipTime from OTAnalytics.application.use_cases.section_repository import GetSectionsById from OTAnalytics.domain.date import DateRange @@ -29,7 +29,7 @@ TrackRepositoryEvent, TrackSubject, ) -from OTAnalytics.domain.video import Video, VideoListObserver +from OTAnalytics.domain.video import Video, VideoListObserver, VideoMetadata FIRST_DETECTION_OCCURRENCE: str = "first_detection_occurrence" LAST_DETECTION_OCCURRENCE: str = "last_detection_occurrence" @@ -242,10 +242,70 @@ def plot(self) -> Optional[TrackImage]: pass +class VideosMetadata: + def __init__(self) -> None: + self._metadata: dict[datetime, VideoMetadata] = {} + self._first_video_start: Optional[datetime] = None + self._last_video_end: Optional[datetime] = None + + def update(self, metadata: VideoMetadata) -> None: + """ + Update the stored metadata. + """ + if metadata.start in self._metadata.keys(): + raise ValueError( + f"metadata with start date {metadata.start} already exists." + ) + self._metadata[metadata.start] = metadata + self._metadata = dict(sorted(self._metadata.items())) + self._update_start_end_by(metadata) + + def _update_start_end_by(self, metadata: VideoMetadata) -> None: + if (not self._first_video_start) or metadata.start < self._first_video_start: + self._first_video_start = metadata.start + if (not self._last_video_end) or metadata.end > self._last_video_end: + self._last_video_end = metadata.end + + def get_metadata_for(self, current: datetime) -> Optional[VideoMetadata]: + """ + Find the metadata for the given datetime. If the datetime matches exactly a + start time of a video, the corresponding VideoMetadata is returned. Otherwise, + the metadata of the video containing the datetime will be returned. + """ + if current in self._metadata: + return self._metadata[current] + keys = list(self._metadata.keys()) + key = bisect.bisect_left(keys, current) - 1 + metadata = self._metadata[keys[key]] + if metadata.start <= current <= metadata.end: + return metadata + return None + + @property + def first_video_start(self) -> Optional[datetime]: + return self._first_video_start + + @property + def last_video_end(self) -> Optional[datetime]: + return self._last_video_end + + def to_dict(self) -> dict: + return { + key.timestamp(): metadata.to_dict() + for key, metadata in self._metadata.items() + } + + class SelectedVideoUpdate(TrackListObserver, VideoListObserver): - def __init__(self, datastore: Datastore, track_view_state: TrackViewState) -> None: + def __init__( + self, + datastore: Datastore, + track_view_state: TrackViewState, + videos_metadata: VideosMetadata, + ) -> None: self._datastore = datastore self._track_view_state = track_view_state + self._videos_metadata = videos_metadata def notify_tracks(self, track_event: TrackRepositoryEvent) -> None: all_tracks = self._datastore.get_all_tracks() @@ -258,6 +318,27 @@ def notify_videos(self, videos: list[Video]) -> None: if videos: self._track_view_state.selected_videos.set([videos[0]]) + def on_filter_element_change(self, filter_element: FilterElement) -> None: + start_date = filter_element.date_range.start_date + end_date = filter_element.date_range.end_date + video_repository = self._datastore._video_repository + + if not start_date and not end_date: + if first_video_start := self._videos_metadata.first_video_start: + first_video = video_repository.get_by_date(first_video_start) + self._track_view_state.selected_videos.set(first_video) + return + + if not end_date: + self._track_view_state.selected_videos.set([]) + return + + if not (videos := video_repository.get_by_date(end_date)): + self._track_view_state.selected_videos.set([]) + return + + self._track_view_state.selected_videos.set(videos) + class SectionState(SectionListObserver): """ @@ -419,60 +500,6 @@ def _update_image(self) -> None: self._track_view_state.background_image.set(self._plotter.plot()) -class VideosMetadata: - def __init__(self) -> None: - self._metadata: dict[datetime, VideoMetadata] = {} - self._first_video_start: Optional[datetime] = None - self._last_video_end: Optional[datetime] = None - - def update(self, metadata: VideoMetadata) -> None: - """ - Update the stored metadata. - """ - if metadata.start in self._metadata.keys(): - raise ValueError( - f"metadata with start date {metadata.start} already exists." - ) - self._metadata[metadata.start] = metadata - self._metadata = dict(sorted(self._metadata.items())) - self._update_start_end_by(metadata) - - def _update_start_end_by(self, metadata: VideoMetadata) -> None: - if (not self._first_video_start) or metadata.start < self._first_video_start: - self._first_video_start = metadata.start - if (not self._last_video_end) or metadata.end > self._last_video_end: - self._last_video_end = metadata.end - - def get_metadata_for(self, current: datetime) -> Optional[VideoMetadata]: - """ - Find the metadata for the given datetime. If the datetime matches exactly a - start time of a video, the corresponding VideoMetadata is returned. Otherwise, - the metadata of the video containing the datetime will be returned. - """ - if current in self._metadata: - return self._metadata[current] - keys = list(self._metadata.keys()) - key = bisect.bisect_left(keys, current) - 1 - metadata = self._metadata[keys[key]] - if metadata.start <= current <= metadata.end: - return metadata - return None - - @property - def first_video_start(self) -> Optional[datetime]: - return self._first_video_start - - @property - def last_video_end(self) -> Optional[datetime]: - return self._last_video_end - - def to_dict(self) -> dict: - return { - key.timestamp(): metadata.to_dict() - for key, metadata in self._metadata.items() - } - - class TracksMetadata(TrackListObserver): """Contains relevant information on the currently loaded tracks. diff --git a/OTAnalytics/application/use_cases/load_track_files.py b/OTAnalytics/application/use_cases/load_track_files.py index f7d1074e4..1c4b51b49 100644 --- a/OTAnalytics/application/use_cases/load_track_files.py +++ b/OTAnalytics/application/use_cases/load_track_files.py @@ -63,7 +63,9 @@ def load(self, file: Path) -> None: """ parse_result = self._track_parser.parse(file) track_ids = list(parse_result.tracks.track_ids) - track_ids, videos = self._track_video_parser.parse(file, track_ids) + track_ids, videos = self._track_video_parser.parse( + file, track_ids, parse_result.video_metadata + ) self._video_repository.add_all(videos) self._track_to_video_repository.add_all(track_ids, videos) self._track_repository.add_all(parse_result.tracks) diff --git a/OTAnalytics/domain/video.py b/OTAnalytics/domain/video.py index 72bf030d6..9fd1b901e 100644 --- a/OTAnalytics/domain/video.py +++ b/OTAnalytics/domain/video.py @@ -57,6 +57,10 @@ def get_frame(self, index: int) -> TrackImage: def get_frame_number_for(self, date: datetime) -> int: raise NotImplementedError + @abstractmethod + def contains(self, date: datetime) -> bool: + raise NotImplementedError + @abstractmethod def to_dict( self, @@ -83,6 +87,53 @@ class DifferentDrivesException(Exception): pass +@dataclass(frozen=True) +class VideoMetadata: + path: str + recorded_start_date: datetime + expected_duration: Optional[timedelta] + recorded_fps: float + actual_fps: Optional[float] + number_of_frames: int + + @property + def start(self) -> datetime: + return self.recorded_start_date + + @property + def end(self) -> datetime: + return self.start + self.duration + + @property + def duration(self) -> timedelta: + if self.expected_duration: + return self.expected_duration + return timedelta(seconds=self.number_of_frames / self.recorded_fps) + + @property + def fps(self) -> float: + if self.actual_fps: + return self.actual_fps + return self.recorded_fps + + def to_dict(self) -> dict: + return { + "path": self.path, + "recorded_start_date": self.recorded_start_date.timestamp(), + "expected_duration": ( + self.expected_duration.total_seconds() + if self.expected_duration + else None + ), + "recorded_fps": self.recorded_fps, + "actual_fps": self.actual_fps, + "number_of_frames": self.number_of_frames, + } + + def contains(self, date: datetime) -> bool: + return self.start <= date < self.end + + @dataclass class SimpleVideo(Video): """Represents a video file. @@ -97,12 +148,14 @@ class SimpleVideo(Video): video_reader: VideoReader path: Path - _start_date: Optional[datetime] + metadata: Optional[VideoMetadata] _fps: Optional[int] = None @property def start_date(self) -> Optional[datetime]: - return self._start_date + if self.metadata: + return self.metadata.recorded_start_date + return None @property def fps(self) -> float: @@ -155,6 +208,11 @@ def __build_relative_path(self, relative_to: Path) -> str: ) return path.relpath(self.path, relative_to) + def contains(self, date: datetime) -> bool: + if self.metadata: + return self.metadata.contains(date) + return False + class VideoListObserver(ABC): """ @@ -250,3 +308,6 @@ def clear(self) -> None: """ self._videos.clear() self._observers.notify([]) + + def get_by_date(self, date: datetime) -> list[Video]: + return [video for video in self._videos.values() if video.contains(date)] diff --git a/OTAnalytics/plugin_parser/otvision_parser.py b/OTAnalytics/plugin_parser/otvision_parser.py index 6fa3f6540..5b44f43e2 100644 --- a/OTAnalytics/plugin_parser/otvision_parser.py +++ b/OTAnalytics/plugin_parser/otvision_parser.py @@ -16,7 +16,6 @@ TrackParser, TrackParseResult, TrackVideoParser, - VideoMetadata, VideoParser, ) from OTAnalytics.application.logger import logger @@ -37,7 +36,13 @@ ) from OTAnalytics.domain.track_dataset import TRACK_GEOMETRY_FACTORY, TrackDataset from OTAnalytics.domain.track_repository import TrackRepository -from OTAnalytics.domain.video import PATH, SimpleVideo, Video, VideoReader +from OTAnalytics.domain.video import ( + PATH, + SimpleVideo, + Video, + VideoMetadata, + VideoReader, +) from OTAnalytics.plugin_datastore.python_track_store import ( PythonDetection, PythonTrack, @@ -812,8 +817,8 @@ class SimpleVideoParser(VideoParser): def __init__(self, video_reader: VideoReader) -> None: self._video_reader = video_reader - def parse(self, file: Path, start_date: Optional[datetime]) -> Video: - return SimpleVideo(self._video_reader, file, start_date) + def parse(self, file: Path, metadata: VideoMetadata | None) -> Video: + return SimpleVideo(self._video_reader, file, metadata) def parse_list( self, @@ -844,6 +849,7 @@ def convert( @dataclass class CachedVideo(Video): + other: Video cache: dict[int, TrackImage] = field(default_factory=dict) @@ -871,13 +877,17 @@ def get_frame_number_for(self, date: datetime) -> int: def to_dict(self, relative_to: Path) -> dict: return self.other.to_dict(relative_to) + def contains(self, date: datetime) -> bool: + return self.other.contains(date) + class CachedVideoParser(VideoParser): + def __init__(self, other: VideoParser) -> None: self._other = other - def parse(self, file: Path, start_date: Optional[datetime]) -> Video: - other_video = self._other.parse(file, start_date) + def parse(self, file: Path, metadata: Optional[VideoMetadata]) -> Video: + other_video = self._other.parse(file, metadata) return self.__create_cached_video(other_video) def __create_cached_video(self, other_video: Video) -> Video: @@ -902,13 +912,9 @@ def __init__(self, video_parser: VideoParser) -> None: self._video_parser = video_parser def parse( - self, file: Path, track_ids: list[TrackId] + self, file: Path, track_ids: list[TrackId], metadata: VideoMetadata ) -> Tuple[list[TrackId], list[Video]]: - content = parse_json_bz2(file) - metadata = content[ottrk_format.METADATA][ottrk_format.VIDEO] - video_file = metadata[ottrk_format.FILENAME] + metadata[ottrk_format.FILETYPE] - start_date = self.__parse_recorded_start_date(metadata) - video = self._video_parser.parse(file.parent / video_file, start_date) + video = self._video_parser.parse(file.parent / metadata.path, metadata) return track_ids, [video] * len(track_ids) def __parse_recorded_start_date(self, metadata: dict) -> datetime: diff --git a/OTAnalytics/plugin_ui/main_application.py b/OTAnalytics/plugin_ui/main_application.py index 64be3e6f9..d451d3132 100644 --- a/OTAnalytics/plugin_ui/main_application.py +++ b/OTAnalytics/plugin_ui/main_application.py @@ -329,7 +329,9 @@ def start_gui(self, run_config: RunConfiguration) -> None: ) track_view_state.selected_videos.register(properties_updater.notify_videos) track_view_state.selected_videos.register(image_updater.notify_video) - selected_video_updater = SelectedVideoUpdate(datastore, track_view_state) + selected_video_updater = SelectedVideoUpdate( + datastore, track_view_state, videos_metadata + ) tracks_metadata = self._create_tracks_metadata(track_repository, run_config) # TODO: Should not register to tracks_metadata._classifications but to @@ -564,6 +566,9 @@ def start_gui(self, run_config: RunConfiguration) -> None: track_view_state.filter_date_active.register( dummy_viewmodel.change_filter_date_active ) + track_view_state.filter_element.register( + selected_video_updater.on_filter_element_change + ) # TODO: Refactor observers - move registering to subjects happening in # constructor dummy_viewmodel # cut_tracks_intersecting_section.register( diff --git a/tests/OTAnalytics/application/test_datastore.py b/tests/OTAnalytics/application/test_datastore.py index 1f3c73c91..6762cb49c 100644 --- a/tests/OTAnalytics/application/test_datastore.py +++ b/tests/OTAnalytics/application/test_datastore.py @@ -13,7 +13,6 @@ TrackParser, TrackToVideoRepository, TrackVideoParser, - VideoMetadata, VideoParser, ) from OTAnalytics.application.parser.config_parser import ConfigParser @@ -61,52 +60,20 @@ def get_frame_number_for(self, video_path: Path, date: timedelta) -> int: return 0 -class TestVideoMetadata: - def test_fully_specified_metadata(self) -> None: - recorded_fps = 20.0 - actual_fps = 20.0 - metadata = VideoMetadata( - path="video_path_1.mp4", - recorded_start_date=FIRST_START_DATE, - expected_duration=timedelta(seconds=3), - recorded_fps=recorded_fps, - actual_fps=actual_fps, - number_of_frames=60, - ) - assert metadata.start == FIRST_START_DATE - assert metadata.end == FIRST_START_DATE + timedelta(seconds=3) - assert metadata.fps == actual_fps - - def test_partially_specified_metadata(self) -> None: - recorded_fps = 20.0 - metadata = VideoMetadata( - path="video_path_1.mp4", - recorded_start_date=FIRST_START_DATE, - expected_duration=None, - recorded_fps=recorded_fps, - actual_fps=None, - number_of_frames=60, - ) - assert metadata.start == FIRST_START_DATE - expected_video_end = FIRST_START_DATE + timedelta(seconds=3) - assert metadata.end == expected_video_end - assert metadata.fps == recorded_fps - - class TestSimpleVideo: video_reader = MockVideoReader() def test_raise_error_if_file_not_exists(self) -> None: with pytest.raises(ValueError): - SimpleVideo(self.video_reader, Path("foo/bar.mp4"), FIRST_START_DATE) + SimpleVideo(self.video_reader, Path("foo/bar.mp4"), None) def test_init_with_valid_args(self, cyclist_video: Path) -> None: - video = SimpleVideo(self.video_reader, cyclist_video, FIRST_START_DATE) + video = SimpleVideo(self.video_reader, cyclist_video, None) assert video.path == cyclist_video assert video.video_reader == self.video_reader def test_get_frame_return_correct_image(self, cyclist_video: Path) -> None: - video = SimpleVideo(self.video_reader, cyclist_video, FIRST_START_DATE) + video = SimpleVideo(self.video_reader, cyclist_video, None) assert video.get_frame(0).as_image() == Image.fromarray( array([[1, 0], [0, 1]], int32) ) diff --git a/tests/OTAnalytics/application/test_plotting.py b/tests/OTAnalytics/application/test_plotting.py index 27927b0fc..5f6809821 100644 --- a/tests/OTAnalytics/application/test_plotting.py +++ b/tests/OTAnalytics/application/test_plotting.py @@ -3,7 +3,6 @@ import pytest -from OTAnalytics.application.datastore import VideoMetadata from OTAnalytics.application.plotting import ( GetCurrentFrame, GetCurrentVideoPath, @@ -17,7 +16,7 @@ from OTAnalytics.domain.date import DateRange from OTAnalytics.domain.filter import FilterElement from OTAnalytics.domain.track import TrackImage -from OTAnalytics.domain.video import Video +from OTAnalytics.domain.video import Video, VideoMetadata class TestLayeredPlotter: diff --git a/tests/OTAnalytics/application/test_state.py b/tests/OTAnalytics/application/test_state.py index bf915cdee..f765b01f8 100644 --- a/tests/OTAnalytics/application/test_state.py +++ b/tests/OTAnalytics/application/test_state.py @@ -6,7 +6,7 @@ import pytest from OTAnalytics.application.config import DEFAULT_TRACK_OFFSET -from OTAnalytics.application.datastore import Datastore, VideoMetadata +from OTAnalytics.application.datastore import Datastore from OTAnalytics.application.state import ( DEFAULT_HEIGHT, DEFAULT_WIDTH, @@ -37,6 +37,7 @@ TrackRepository, TrackRepositoryEvent, ) +from OTAnalytics.domain.video import VideoMetadata FIRST_START_DATE = datetime( year=2019, diff --git a/tests/OTAnalytics/application/ui/test_frame_control.py b/tests/OTAnalytics/application/ui/test_frame_control.py index a250e1948..afa35a6bf 100644 --- a/tests/OTAnalytics/application/ui/test_frame_control.py +++ b/tests/OTAnalytics/application/ui/test_frame_control.py @@ -3,7 +3,6 @@ import pytest -from OTAnalytics.application.datastore import VideoMetadata from OTAnalytics.application.playback import SkipTime from OTAnalytics.application.state import SectionState, TrackViewState, VideosMetadata from OTAnalytics.application.ui.frame_control import ( @@ -19,6 +18,7 @@ from OTAnalytics.domain.filter import FilterElement from OTAnalytics.domain.section import SectionId from OTAnalytics.domain.types import EventType +from OTAnalytics.domain.video import VideoMetadata from tests.utils.state import observable FPS = 1 diff --git a/tests/OTAnalytics/application/use_cases/test_load_track_files.py b/tests/OTAnalytics/application/use_cases/test_load_track_files.py index 184b77da4..6b46e0076 100644 --- a/tests/OTAnalytics/application/use_cases/test_load_track_files.py +++ b/tests/OTAnalytics/application/use_cases/test_load_track_files.py @@ -37,13 +37,18 @@ def test_load(self) -> None: some_track = Mock() some_track_id = TrackId("1") some_track.id = some_track_id - some_video = SimpleVideo(Mock(), Path(""), START_DATE) + some_video_metadata = Mock() + detection_metadata = Mock() + detection_metadata.detection_classes = {"class1", "class2"} + + some_video = SimpleVideo(Mock(), Path(""), some_video_metadata) track_dataset_result = Mock() type(track_dataset_result).track_ids = frozenset([some_track_id]) parse_result = Mock() parse_result.tracks = track_dataset_result - parse_result.metadata = detection_metadata + parse_result.detection_metadata = detection_metadata + parse_result.video_metadata = some_video_metadata track_parser.parse.return_value = parse_result track_video_parser.parse.return_value = [some_track_id], [some_video] @@ -53,6 +58,8 @@ def test_load(self) -> None: order.video_repository = video_repository order.track_repository = track_repository order.track_to_video_repository = track_to_video_repository + order.tracks_metadata = tracks_metadata + order.videos_metadata = videos_metadata load_track_file = LoadTrackFiles( track_parser, track_video_parser, @@ -69,8 +76,14 @@ def test_load(self) -> None: assert order.mock_calls == [ call.track_parser.parse(some_file), - call.track_video_parser.parse(some_file, [some_track_id]), + call.track_video_parser.parse( + some_file, [some_track_id], some_video_metadata + ), call.video_repository.add_all([some_video]), call.track_to_video_repository.add_all([some_track_id], [some_video]), call.track_repository.add_all(track_dataset_result), + call.tracks_metadata.update_detection_classes( + detection_metadata.detection_classes + ), + call.videos_metadata.update(some_video_metadata), ] diff --git a/tests/OTAnalytics/domain/test_video.py b/tests/OTAnalytics/domain/test_video.py index 68cfb0b72..148927638 100644 --- a/tests/OTAnalytics/domain/test_video.py +++ b/tests/OTAnalytics/domain/test_video.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from unittest.mock import Mock, call, patch @@ -10,9 +10,11 @@ SimpleVideo, Video, VideoListObserver, + VideoMetadata, VideoReader, VideoRepository, ) +from tests.OTAnalytics.application.test_datastore import FIRST_START_DATE START_DATE = datetime(2023, 1, 1) @@ -33,7 +35,7 @@ def test_resolve_relative_paths( config_path.parent.mkdir(parents=True) video_path.touch() config_path.touch() - video = SimpleVideo(video_reader, video_path, START_DATE) + video = SimpleVideo(video_reader, video_path, None) result = video.to_dict(config_path) @@ -44,7 +46,7 @@ def test_resolve_relative_paths_on_different_drives( ) -> None: video_path = Mock(spec=Path) config_path = Mock(spec=Path) - video = SimpleVideo(video_reader, video_path, START_DATE) + video = SimpleVideo(video_reader, video_path, None) with patch( "OTAnalytics.domain.video.splitdrive", @@ -73,7 +75,7 @@ def test_remove(self, video_reader: VideoReader, test_data_tmp_dir: Path) -> Non observer = Mock(spec=VideoListObserver) path = test_data_tmp_dir / "dummy.mp4" path.touch() - video = SimpleVideo(video_reader, path, START_DATE) + video = SimpleVideo(video_reader, path, None) repository = VideoRepository() repository.register_videos_observer(observer) @@ -108,3 +110,53 @@ def test_order_of_videos_multiple_add(self, video_1: Video, video_2: Video) -> N assert unorderd_videos != ordered_videos assert result == ordered_videos + + def test_get_by_date(self, video_1: Mock, video_2: Mock) -> None: + video_1.contains.return_value = False + video_2.contains.return_value = True + + repository = VideoRepository() + repository.add_all([video_1, video_2]) + result = repository.get_by_date(START_DATE) + assert result == [video_2] + + +class TestVideoMetadata: + @pytest.fixture + def metadata(self) -> VideoMetadata: + recorded_fps = 20.0 + actual_fps = 20.0 + return VideoMetadata( + path="video_path_1.mp4", + recorded_start_date=FIRST_START_DATE, + expected_duration=timedelta(seconds=3), + recorded_fps=recorded_fps, + actual_fps=actual_fps, + number_of_frames=60, + ) + + def test_fully_specified_metadata(self, metadata: VideoMetadata) -> None: + assert metadata.start == FIRST_START_DATE + assert metadata.end == FIRST_START_DATE + timedelta(seconds=3) + assert metadata.fps == 20.0 + + def test_partially_specified_metadata(self) -> None: + recorded_fps = 20.0 + metadata = VideoMetadata( + path="video_path_1.mp4", + recorded_start_date=FIRST_START_DATE, + expected_duration=None, + recorded_fps=recorded_fps, + actual_fps=None, + number_of_frames=60, + ) + assert metadata.start == FIRST_START_DATE + expected_video_end = FIRST_START_DATE + timedelta(seconds=3) + assert metadata.end == expected_video_end + assert metadata.fps == recorded_fps + + def test_contains(self, metadata: VideoMetadata) -> None: + assert not metadata.contains(FIRST_START_DATE - timedelta(seconds=1)) + assert metadata.contains(FIRST_START_DATE) + assert metadata.contains(FIRST_START_DATE + timedelta(seconds=2)) + assert not metadata.contains(FIRST_START_DATE + timedelta(seconds=3)) diff --git a/tests/OTAnalytics/plugin_parser/test_otvision_parser.py b/tests/OTAnalytics/plugin_parser/test_otvision_parser.py index bab8db624..8e1917bac 100644 --- a/tests/OTAnalytics/plugin_parser/test_otvision_parser.py +++ b/tests/OTAnalytics/plugin_parser/test_otvision_parser.py @@ -5,7 +5,7 @@ import pytest from OTAnalytics import version -from OTAnalytics.application.datastore import VideoMetadata, VideoParser +from OTAnalytics.application.datastore import VideoParser from OTAnalytics.domain import flow, geometry, section from OTAnalytics.domain.event import EVENT_LIST, Event, EventType from OTAnalytics.domain.flow import Flow, FlowId @@ -29,7 +29,7 @@ TrackImage, ) from OTAnalytics.domain.track_repository import TrackRepository -from OTAnalytics.domain.video import Video +from OTAnalytics.domain.video import Video, VideoMetadata from OTAnalytics.plugin_datastore.python_track_store import ( ByMaxConfidence, PythonTrack, @@ -749,6 +749,15 @@ def test_to_dict(self) -> None: other.to_dict.assert_called_once() assert cached_dict is original_dict + def test_contains(self) -> None: + date = Mock() + other = Mock(spec=Video) + other.contains.return_value = True + + cached_video = CachedVideo(other) + assert cached_video.contains(date) is True + other.contains.assert_called_with(date) + class TestCachedVideoParser: def test_parse_to_cached_video(self, test_data_tmp_dir: Path) -> None: @@ -760,7 +769,7 @@ def test_parse_to_cached_video(self, test_data_tmp_dir: Path) -> None: cached_parser = CachedVideoParser(video_parser) - parsed_video = cached_parser.parse(video_file, start_date=None) + parsed_video = cached_parser.parse(video_file, None) assert isinstance(parsed_video, CachedVideo) assert parsed_video.other == video