from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".mts", ".m2ts", ".avi"} @dataclass(frozen=True) class VideoInput: path: Path drag_relative: Path collapsed_relative: Path @dataclass(frozen=True) class _CollectedVideo: path: Path drag_relative: Path def _sort_key(path: Path) -> str: return str(path).lower() def _is_video(path: Path) -> bool: return path.is_file() and path.suffix.lower() in VIDEO_EXTS def _collect_from_arg(arg: str) -> list[_CollectedVideo]: path = Path(arg).expanduser() if not path.exists(): print(f"Skipping missing path: {arg}") return [] resolved = path.resolve() if _is_video(path): return [_CollectedVideo(resolved, Path(path.name))] if path.is_dir(): videos: list[_CollectedVideo] = [] for child in sorted(path.rglob("*"), key=_sort_key): if _is_video(child): relative = Path(path.name) / child.relative_to(path) videos.append(_CollectedVideo(child.resolve(), relative)) return videos return [] def _relative_depth(path: Path) -> int: return len(path.parts) def collect_video_inputs(args: list[str]) -> list[VideoInput]: collected: dict[Path, _CollectedVideo] = {} for arg in args: for item in _collect_from_arg(arg): existing = collected.get(item.path) if existing is None: collected[item.path] = item continue if _relative_depth(item.drag_relative) > _relative_depth(existing.drag_relative): collected[item.path] = item items = sorted(collected.values(), key=lambda item: _sort_key(item.drag_relative)) if not items: return [] collapsed = _collapse_drag_tree(items) return [ VideoInput( path=item.path, drag_relative=item.drag_relative, collapsed_relative=collapsed[item.path], ) for item in items ] def _collapse_drag_tree(items: list[_CollectedVideo]) -> dict[Path, Path]: if len(items) == 1: return {items[0].path: Path(items[0].path.name)} parent_names = [str(item.drag_relative.parent) for item in items] common_parent = Path(os.path.commonpath(parent_names)) if str(common_parent) in {"", "."}: common_parent = Path(".") collapsed: dict[Path, Path] = {} used: dict[Path, Path] = {} for item in items: if common_parent == Path("."): relative = item.drag_relative else: relative = item.drag_relative.relative_to(common_parent) conflict = used.get(relative) if conflict is not None and conflict != item.path: raise RuntimeError( "Two different videos collapse to the same workspace path: " f"{relative}" ) used[relative] = item.path collapsed[item.path] = relative return collapsed