Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/jabs/project/project_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,18 @@ def create_directories(self, validate: bool = True) -> None:
the jabs directory. Defaults to True.
"""
if validate and not self._jabs_dir.exists():
# Skip dotfiles (e.g. macOS AppleDouble "._*" sidecars created on
# exFAT/NTFS external drives), which are not real videos or valid
# pose HDF5 files and must not make a directory look like a project.
has_video = any(
file for ext in ("*.mp4", "*.avi") for file in self._video_dir.glob(ext)
file
for ext in ("*.mp4", "*.avi")
for file in self._video_dir.glob(ext)
if not file.name.startswith(".")
)
has_pose = any(
p for p in self._pose_dir.glob("*_pose_est_v*.h5") if not p.name.startswith(".")
)
has_pose = any(self._pose_dir.glob("*_pose_est_v*.h5"))
if not has_video or not has_pose:
raise ValueError(
f"{self._base_path} does not appear to be a valid JABS project. "
Expand Down
16 changes: 14 additions & 2 deletions src/jabs/project/video_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,20 @@ def check_video_name(self, video_filename: str):

@staticmethod
def get_videos(dir_path: Path):
"""Get list of video filenames (without path) in a directory"""
return [f.name for f in dir_path.glob("*") if f.suffix in [".avi", ".mp4"]]
"""Get list of video filenames (without path) in a directory.

Dotfiles are skipped. In particular this excludes macOS AppleDouble
sidecar files (``._<name>``) that the OS creates when writing to
non-APFS/HFS+ volumes (e.g. exFAT/NTFS external drives). Those are not
real videos, and ``pathlib.Path.glob`` matches them (unlike the shell);
their companion ``._..._pose_est_v*.h5`` sidecars are not valid HDF5 and
would otherwise break the project video scan.
"""
return [
f.name
for f in dir_path.glob("*")
if f.suffix in [".avi", ".mp4"] and not f.name.startswith(".")
]

def get_video_identity_count(self, video_name: str) -> int:
"""Get the number of identity count for a specific video.
Expand Down
39 changes: 39 additions & 0 deletions tests/project/test_project_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Tests for jabs.project.project_paths.ProjectPaths."""

import shutil
from pathlib import Path

import pytest

from jabs.project.project_paths import ProjectPaths


def test_create_directories_ignores_dotfile_videos_and_poses(tmp_path):
"""AppleDouble '._*' sidecars must not make a directory look like a JABS project.

macOS creates a ``._<name>`` sidecar for every file on non-APFS/HFS+ volumes
(e.g. exFAT external drives). Those match ``pathlib`` globs but are not real
videos or valid pose HDF5 files, so a directory containing only sidecars is
not a valid project.
"""
(tmp_path / "._vid.mp4").touch()
(tmp_path / "._vid_pose_est_v8.h5").touch()

paths = ProjectPaths(base_path=tmp_path)
with pytest.raises(ValueError, match="does not appear to be a valid JABS project"):
paths.create_directories(validate=True)


def test_create_directories_validates_real_project(tmp_path):
"""A directory with a real video and pose file passes validation even alongside sidecars."""
(tmp_path / "vid.mp4").touch()
data_dir = Path(__file__).parent.parent / "data"
shutil.copy(data_dir / "sample_pose_est_v6.h5", tmp_path / "vid_pose_est_v6.h5")
# AppleDouble sidecars sitting next to the real files must not interfere.
(tmp_path / "._vid.mp4").touch()
(tmp_path / "._vid_pose_est_v6.h5").touch()

paths = ProjectPaths(base_path=tmp_path)
paths.create_directories(validate=True) # should not raise

assert paths.jabs_dir.is_dir()
16 changes: 16 additions & 0 deletions tests/project/test_video_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ def test_get_videos(video_manager, project_paths):
assert len(videos) == 2


def test_get_videos_excludes_dotfiles(tmp_path):
"""get_videos ignores dotfiles, including macOS AppleDouble ('._*') sidecars."""
(tmp_path / "real1.mp4").touch()
(tmp_path / "real2.avi").touch()
# macOS AppleDouble sidecars written when copying to exFAT/NTFS volumes
(tmp_path / "._real1.mp4").touch()
(tmp_path / "._real2.avi").touch()
# other hidden files that should never be treated as videos
(tmp_path / ".DS_Store").touch()
(tmp_path / ".hidden.mp4").touch()

videos = VideoManager.get_videos(tmp_path)

assert sorted(videos) == ["real1.mp4", "real2.avi"]


def test_check_video_name(video_manager):
"""Test checking if a video name is valid."""
video_manager.check_video_name("video1.avi") # Should not raise an exception
Expand Down
Loading