Improve grouping controls and cleanup
This commit is contained in:
+18
-1
@@ -17,7 +17,13 @@ from pathlib import Path
|
|||||||
|
|
||||||
from tools.console import pause_if_interactive
|
from tools.console import pause_if_interactive
|
||||||
from tools.filenames import group_stem
|
from tools.filenames import group_stem
|
||||||
from tools.filesystem import collect_files, determine_working_dir, unique_path
|
from tools.filesystem import (
|
||||||
|
cleanup_roots_from_args,
|
||||||
|
collect_files,
|
||||||
|
determine_working_dir,
|
||||||
|
remove_empty_dirs,
|
||||||
|
unique_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def collect_group_files(args: list[str]) -> list[Path]:
|
def collect_group_files(args: list[str]) -> list[Path]:
|
||||||
@@ -41,6 +47,14 @@ def group_files(files: list[Path], working_dir: Path) -> Path | None:
|
|||||||
return target_dir
|
return target_dir
|
||||||
|
|
||||||
|
|
||||||
|
def print_removed_dirs(removed_dirs: list[Path]) -> None:
|
||||||
|
if not removed_dirs:
|
||||||
|
return
|
||||||
|
print("Removed empty director" + ("y:" if len(removed_dirs) == 1 else "ies:"))
|
||||||
|
for directory in removed_dirs:
|
||||||
|
print(f" - {directory}")
|
||||||
|
|
||||||
|
|
||||||
def run(argv: list[str]) -> int:
|
def run(argv: list[str]) -> int:
|
||||||
if len(argv) < 2:
|
if len(argv) < 2:
|
||||||
print("Usage: python3 grouping.py path/to/file/or/directory [...]")
|
print("Usage: python3 grouping.py path/to/file/or/directory [...]")
|
||||||
@@ -52,6 +66,7 @@ def run(argv: list[str]) -> int:
|
|||||||
print(exc)
|
print(exc)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
cleanup_roots = cleanup_roots_from_args(argv[1:])
|
||||||
files = collect_group_files(argv[1:])
|
files = collect_group_files(argv[1:])
|
||||||
if not files:
|
if not files:
|
||||||
print("No files to group.")
|
print("No files to group.")
|
||||||
@@ -60,6 +75,8 @@ def run(argv: list[str]) -> int:
|
|||||||
target_dir = group_files(files, working_dir)
|
target_dir = group_files(files, working_dir)
|
||||||
if target_dir is not None:
|
if target_dir is not None:
|
||||||
print(f"Moved {len(files)} file(s) into {target_dir}")
|
print(f"Moved {len(files)} file(s) into {target_dir}")
|
||||||
|
removed_dirs = remove_empty_dirs(cleanup_roots, keep_dirs={working_dir.resolve()})
|
||||||
|
print_removed_dirs(removed_dirs)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+38
-14
@@ -189,6 +189,7 @@ class UserChoices:
|
|||||||
artist_value: str | None
|
artist_value: str | None
|
||||||
rename_files: bool
|
rename_files: bool
|
||||||
group_photos: bool
|
group_photos: bool
|
||||||
|
group_min_size: int
|
||||||
move_to_working_dir: bool
|
move_to_working_dir: bool
|
||||||
process_pto_files: bool
|
process_pto_files: bool
|
||||||
apply_filename_timestamps: bool
|
apply_filename_timestamps: bool
|
||||||
@@ -207,6 +208,23 @@ def ask_yes_no(prompt: str, default: bool) -> bool:
|
|||||||
print("Please answer y or n.")
|
print("Please answer y or n.")
|
||||||
|
|
||||||
|
|
||||||
|
def ask_group_min_size() -> int:
|
||||||
|
while True:
|
||||||
|
answer = prompt_input(
|
||||||
|
"Create automatic burst/HDR groups? Enter=yes, n=no, or minimum group size [2]: "
|
||||||
|
).strip().lower()
|
||||||
|
if not answer or answer in {"y", "yes"}:
|
||||||
|
return 2
|
||||||
|
if answer in {"n", "no"}:
|
||||||
|
return 0
|
||||||
|
if answer.isdigit():
|
||||||
|
value = int(answer)
|
||||||
|
if value <= 0:
|
||||||
|
return 0
|
||||||
|
return max(value, 2)
|
||||||
|
print("Please answer y, n, or an integer minimum group size.")
|
||||||
|
|
||||||
|
|
||||||
def parse_timezone_offset(value: str) -> timezone:
|
def parse_timezone_offset(value: str) -> timezone:
|
||||||
match = TZ_RE.match(value.strip())
|
match = TZ_RE.match(value.strip())
|
||||||
if not match:
|
if not match:
|
||||||
@@ -739,9 +757,10 @@ def prompt_choices(records: list[MediaRecord], working_dir: Path, has_pto_files:
|
|||||||
rename_files = ask_yes_no("Rename files to timestamps?", default=True)
|
rename_files = ask_yes_no("Rename files to timestamps?", default=True)
|
||||||
explicit_tz, authoritative_tz, write_missing_tz, video_tz = prompt_timezone(records, rename_files)
|
explicit_tz, authoritative_tz, write_missing_tz, video_tz = prompt_timezone(records, rename_files)
|
||||||
artist_action, artist_value = prompt_artist_action()
|
artist_action, artist_value = prompt_artist_action()
|
||||||
group_photos = False
|
group_min_size = 0
|
||||||
if sum(1 for record in records if record.is_image and record.adjusted_time) >= 2:
|
if sum(1 for record in records if record.is_image and record.adjusted_time) >= 2:
|
||||||
group_photos = ask_yes_no("Create automatic burst/HDR groups?", default=True)
|
group_min_size = ask_group_min_size()
|
||||||
|
group_photos = group_min_size >= 2
|
||||||
|
|
||||||
process_pto_files = False
|
process_pto_files = False
|
||||||
if has_pto_files:
|
if has_pto_files:
|
||||||
@@ -772,6 +791,7 @@ def prompt_choices(records: list[MediaRecord], working_dir: Path, has_pto_files:
|
|||||||
artist_value=artist_value,
|
artist_value=artist_value,
|
||||||
rename_files=rename_files,
|
rename_files=rename_files,
|
||||||
group_photos=group_photos,
|
group_photos=group_photos,
|
||||||
|
group_min_size=group_min_size,
|
||||||
move_to_working_dir=move_to_working_dir,
|
move_to_working_dir=move_to_working_dir,
|
||||||
process_pto_files=process_pto_files,
|
process_pto_files=process_pto_files,
|
||||||
apply_filename_timestamps=apply_filename_timestamps,
|
apply_filename_timestamps=apply_filename_timestamps,
|
||||||
@@ -806,7 +826,7 @@ def group_sort_key(record: MediaRecord) -> tuple:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def detect_photo_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
def detect_photo_groups(records: list[MediaRecord], min_size: int = 2) -> list[list[MediaRecord]]:
|
||||||
groups: list[list[MediaRecord]] = []
|
groups: list[list[MediaRecord]] = []
|
||||||
by_camera: dict[str, list[MediaRecord]] = defaultdict(list)
|
by_camera: dict[str, list[MediaRecord]] = defaultdict(list)
|
||||||
for record in records:
|
for record in records:
|
||||||
@@ -814,12 +834,12 @@ def detect_photo_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
|||||||
by_camera[record.camera_key].append(record)
|
by_camera[record.camera_key].append(record)
|
||||||
|
|
||||||
for camera_records in by_camera.values():
|
for camera_records in by_camera.values():
|
||||||
groups.extend(detect_groups_for_camera(sorted(camera_records, key=photo_sort_key)))
|
groups.extend(detect_groups_for_camera(sorted(camera_records, key=photo_sort_key), min_size))
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
|
||||||
def detect_groups_for_camera(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
def detect_groups_for_camera(records: list[MediaRecord], min_size: int) -> list[list[MediaRecord]]:
|
||||||
by_second: dict[datetime, list[MediaRecord]] = defaultdict(list)
|
by_second: dict[datetime, list[MediaRecord]] = defaultdict(list)
|
||||||
for record in records:
|
for record in records:
|
||||||
assert record.adjusted_time is not None
|
assert record.adjusted_time is not None
|
||||||
@@ -843,13 +863,13 @@ def detect_groups_for_camera(records: list[MediaRecord]) -> list[list[MediaRecor
|
|||||||
continue
|
continue
|
||||||
span_records = [record for second in span for record in by_second[second]]
|
span_records = [record for second in span for record in by_second[second]]
|
||||||
if all(record.sequence is not None for record in span_records) and len(span_records) >= 2:
|
if all(record.sequence is not None for record in span_records) and len(span_records) >= 2:
|
||||||
groups.extend(sequence_groups(span_records))
|
groups.extend(sequence_groups(span_records, min_size))
|
||||||
else:
|
else:
|
||||||
groups.extend(timestamp_groups(span, by_second))
|
groups.extend(timestamp_groups(span, by_second, min_size))
|
||||||
return [group for group in groups if len(group) >= 2]
|
return [group for group in groups if len(group) >= min_size]
|
||||||
|
|
||||||
|
|
||||||
def sequence_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
def sequence_groups(records: list[MediaRecord], min_size: int) -> list[list[MediaRecord]]:
|
||||||
ordered = sorted(records, key=photo_sort_key)
|
ordered = sorted(records, key=photo_sort_key)
|
||||||
groups: list[list[MediaRecord]] = []
|
groups: list[list[MediaRecord]] = []
|
||||||
current: list[MediaRecord] = [ordered[0]]
|
current: list[MediaRecord] = [ordered[0]]
|
||||||
@@ -858,16 +878,16 @@ def sequence_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
|||||||
if record.sequence is not None and previous.sequence is not None and record.sequence > previous.sequence:
|
if record.sequence is not None and previous.sequence is not None and record.sequence > previous.sequence:
|
||||||
current.append(record)
|
current.append(record)
|
||||||
else:
|
else:
|
||||||
if len(current) >= 2:
|
if len(current) >= min_size:
|
||||||
groups.append(current)
|
groups.append(current)
|
||||||
current = [record]
|
current = [record]
|
||||||
if len(current) >= 2:
|
if len(current) >= min_size:
|
||||||
groups.append(current)
|
groups.append(current)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
|
||||||
def timestamp_groups(
|
def timestamp_groups(
|
||||||
span: list[datetime], by_second: dict[datetime, list[MediaRecord]]
|
span: list[datetime], by_second: dict[datetime, list[MediaRecord]], min_size: int
|
||||||
) -> list[list[MediaRecord]]:
|
) -> list[list[MediaRecord]]:
|
||||||
multi_indices = [index for index, second in enumerate(span) if len(by_second[second]) >= 2]
|
multi_indices = [index for index, second in enumerate(span) if len(by_second[second]) >= 2]
|
||||||
if not multi_indices:
|
if not multi_indices:
|
||||||
@@ -893,7 +913,7 @@ def timestamp_groups(
|
|||||||
for second in seconds:
|
for second in seconds:
|
||||||
used_seconds.add(second)
|
used_seconds.add(second)
|
||||||
records = [record for second in seconds for record in sorted(by_second[second], key=photo_sort_key)]
|
records = [record for second in seconds for record in sorted(by_second[second], key=photo_sort_key)]
|
||||||
if len(records) >= 2:
|
if len(records) >= min_size:
|
||||||
groups.append(records)
|
groups.append(records)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
@@ -1448,6 +1468,10 @@ def print_preview(records: list[MediaRecord], choices: UserChoices, pto_plans: l
|
|||||||
print("\nPreview")
|
print("\nPreview")
|
||||||
print(f"Working directory: {choices.working_dir}")
|
print(f"Working directory: {choices.working_dir}")
|
||||||
print(f"Move ungrouped files into working directory: {'yes' if choices.move_to_working_dir else 'no'}")
|
print(f"Move ungrouped files into working directory: {'yes' if choices.move_to_working_dir else 'no'}")
|
||||||
|
print(
|
||||||
|
"Automatic burst/HDR grouping: "
|
||||||
|
+ (f"minimum {choices.group_min_size}" if choices.group_photos else "no")
|
||||||
|
)
|
||||||
print(f"Panorama project files (.pto): {'group/update' if choices.process_pto_files else 'ignore'}")
|
print(f"Panorama project files (.pto): {'group/update' if choices.process_pto_files else 'ignore'}")
|
||||||
print(f"Time shift: {format_timedelta(choices.time_offset) if choices.time_offset else 'none'}")
|
print(f"Time shift: {format_timedelta(choices.time_offset) if choices.time_offset else 'none'}")
|
||||||
print(
|
print(
|
||||||
@@ -1571,7 +1595,7 @@ def prepare_plan(
|
|||||||
|
|
||||||
group_candidates: list[list[MediaRecord]] = []
|
group_candidates: list[list[MediaRecord]] = []
|
||||||
if choices.group_photos:
|
if choices.group_photos:
|
||||||
group_candidates.extend(detect_photo_groups(records))
|
group_candidates.extend(detect_photo_groups(records, choices.group_min_size))
|
||||||
if choices.process_pto_files:
|
if choices.process_pto_files:
|
||||||
group_candidates.extend(reference.records for reference in pto_references)
|
group_candidates.extend(reference.records for reference in pto_references)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user