112 lines
3.0 KiB
Python
112 lines
3.0 KiB
Python
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
|